Merge branch 'develop' into fetch-outbox
This commit is contained in:
commit
5a0d7d41e6
115 changed files with 815 additions and 634 deletions
|
@ -28,6 +28,11 @@
|
||||||
- フォルダーやファイルに対しても開発者モード使用時、IDをコピーできるように
|
- フォルダーやファイルに対しても開発者モード使用時、IDをコピーできるように
|
||||||
- 引用対象を「もっと見る」で展開した場合、「閉じる」で畳めるように
|
- 引用対象を「もっと見る」で展開した場合、「閉じる」で畳めるように
|
||||||
- プロフィールURLをコピーできるボタンを追加 #11190
|
- プロフィールURLをコピーできるボタンを追加 #11190
|
||||||
|
- `CURRENT_URL`で現在表示中のURLを取得できるように(AiScript)
|
||||||
|
- ユーザーのContextMenuに「アンテナに追加」ボタンを追加
|
||||||
|
- フォローやお気に入り登録をしていないチャンネルを開く時は概要ページを開くように
|
||||||
|
- 画面ビューワをタップした場合、マウスクリックと同様に画像ビューワを閉じるように
|
||||||
|
- オフライン時の画面にリロードボタンを追加
|
||||||
- Fix: サーバーメトリクスが90度傾いている
|
- Fix: サーバーメトリクスが90度傾いている
|
||||||
- Fix: 非ログイン時にクレデンシャルが必要なページに行くとエラーが出る問題を修正
|
- Fix: 非ログイン時にクレデンシャルが必要なページに行くとエラーが出る問題を修正
|
||||||
- Fix: sparkle内にリンクを入れるとクリック不能になる問題の修正
|
- Fix: sparkle内にリンクを入れるとクリック不能になる問題の修正
|
||||||
|
|
3
locales/index.d.ts
vendored
3
locales/index.d.ts
vendored
|
@ -52,6 +52,7 @@ export interface Locale {
|
||||||
"deleteAndEdit": string;
|
"deleteAndEdit": string;
|
||||||
"deleteAndEditConfirm": string;
|
"deleteAndEditConfirm": string;
|
||||||
"addToList": string;
|
"addToList": string;
|
||||||
|
"addToAntenna": string;
|
||||||
"sendMessage": string;
|
"sendMessage": string;
|
||||||
"copyRSS": string;
|
"copyRSS": string;
|
||||||
"copyUsername": string;
|
"copyUsername": string;
|
||||||
|
@ -2157,4 +2158,4 @@ export interface Locale {
|
||||||
declare const locales: {
|
declare const locales: {
|
||||||
[lang: string]: Locale;
|
[lang: string]: Locale;
|
||||||
};
|
};
|
||||||
export = locales;
|
export default locales;
|
||||||
|
|
|
@ -49,6 +49,7 @@ delete: "削除"
|
||||||
deleteAndEdit: "削除して編集"
|
deleteAndEdit: "削除して編集"
|
||||||
deleteAndEditConfirm: "このノートを削除してもう一度編集しますか?このノートへのリアクション、Renote、返信も全て削除されます。"
|
deleteAndEditConfirm: "このノートを削除してもう一度編集しますか?このノートへのリアクション、Renote、返信も全て削除されます。"
|
||||||
addToList: "リストに追加"
|
addToList: "リストに追加"
|
||||||
|
addToAntenna: "アンテナに追加"
|
||||||
sendMessage: "メッセージを送信"
|
sendMessage: "メッセージを送信"
|
||||||
copyRSS: "RSSをコピー"
|
copyRSS: "RSSをコピー"
|
||||||
copyUsername: "ユーザー名をコピー"
|
copyUsername: "ユーザー名をコピー"
|
||||||
|
|
|
@ -31,7 +31,7 @@ function greet() {
|
||||||
console.log(themeColor(' | |_|___ ___| |_ ___ _ _ '));
|
console.log(themeColor(' | |_|___ ___| |_ ___ _ _ '));
|
||||||
console.log(themeColor(' | | | | |_ -|_ -| \'_| -_| | |'));
|
console.log(themeColor(' | | | | |_ -|_ -| \'_| -_| | |'));
|
||||||
console.log(themeColor(' |_|_|_|_|___|___|_,_|___|_ |'));
|
console.log(themeColor(' |_|_|_|_|___|___|_,_|___|_ |'));
|
||||||
console.log(' ' + chalk.gray(v) + themeColor(' |___|\n'.substr(v.length)));
|
console.log(' ' + chalk.gray(v) + themeColor(' |___|\n'.substring(v.length)));
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
console.log(' Misskey is an open-source decentralized microblogging platform.');
|
console.log(' Misskey is an open-source decentralized microblogging platform.');
|
||||||
|
|
|
@ -103,7 +103,7 @@ export class FetchInstanceMetadataService {
|
||||||
|
|
||||||
if (name) updates.name = name;
|
if (name) updates.name = name;
|
||||||
if (description) updates.description = description;
|
if (description) updates.description = description;
|
||||||
if (icon || favicon) updates.iconUrl = icon ?? favicon;
|
if (icon || favicon) updates.iconUrl = (icon && !icon.includes('data:image/png;base64')) ? icon : favicon;
|
||||||
if (favicon) updates.faviconUrl = favicon;
|
if (favicon) updates.faviconUrl = favicon;
|
||||||
if (themeColor) updates.themeColor = themeColor;
|
if (themeColor) updates.themeColor = themeColor;
|
||||||
|
|
||||||
|
|
|
@ -570,12 +570,14 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
if (data.reply) {
|
if (data.reply) {
|
||||||
// 通知
|
// 通知
|
||||||
if (data.reply.userHost === null) {
|
if (data.reply.userHost === null) {
|
||||||
const threadMuted = await this.noteThreadMutingsRepository.findOneBy({
|
const isThreadMuted = await this.noteThreadMutingsRepository.exist({
|
||||||
userId: data.reply.userId,
|
where: {
|
||||||
threadId: data.reply.threadId ?? data.reply.id,
|
userId: data.reply.userId,
|
||||||
|
threadId: data.reply.threadId ?? data.reply.id,
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!threadMuted) {
|
if (!isThreadMuted) {
|
||||||
nm.push(data.reply.userId, 'reply');
|
nm.push(data.reply.userId, 'reply');
|
||||||
this.globalEventService.publishMainStream(data.reply.userId, 'reply', noteObj);
|
this.globalEventService.publishMainStream(data.reply.userId, 'reply', noteObj);
|
||||||
|
|
||||||
|
@ -712,12 +714,14 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
@bindThis
|
@bindThis
|
||||||
private async createMentionedEvents(mentionedUsers: MinimumUser[], note: Note, nm: NotificationManager) {
|
private async createMentionedEvents(mentionedUsers: MinimumUser[], note: Note, nm: NotificationManager) {
|
||||||
for (const u of mentionedUsers.filter(u => this.userEntityService.isLocalUser(u))) {
|
for (const u of mentionedUsers.filter(u => this.userEntityService.isLocalUser(u))) {
|
||||||
const threadMuted = await this.noteThreadMutingsRepository.findOneBy({
|
const isThreadMuted = await this.noteThreadMutingsRepository.exist({
|
||||||
userId: u.id,
|
where: {
|
||||||
threadId: note.threadId ?? note.id,
|
userId: u.id,
|
||||||
|
threadId: note.threadId ?? note.id,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (threadMuted) {
|
if (isThreadMuted) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -43,11 +43,13 @@ export class NoteReadService implements OnApplicationShutdown {
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
// スレッドミュート
|
// スレッドミュート
|
||||||
const threadMute = await this.noteThreadMutingsRepository.findOneBy({
|
const isThreadMuted = await this.noteThreadMutingsRepository.exist({
|
||||||
userId: userId,
|
where: {
|
||||||
threadId: note.threadId ?? note.id,
|
userId: userId,
|
||||||
|
threadId: note.threadId ?? note.id,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
if (threadMute) return;
|
if (isThreadMuted) return;
|
||||||
|
|
||||||
const unread = {
|
const unread = {
|
||||||
id: this.idService.genId(),
|
id: this.idService.genId(),
|
||||||
|
@ -62,9 +64,9 @@ export class NoteReadService implements OnApplicationShutdown {
|
||||||
|
|
||||||
// 2秒経っても既読にならなかったら「未読の投稿がありますよ」イベントを発行する
|
// 2秒経っても既読にならなかったら「未読の投稿がありますよ」イベントを発行する
|
||||||
setTimeout(2000, 'unread note', { signal: this.#shutdownController.signal }).then(async () => {
|
setTimeout(2000, 'unread note', { signal: this.#shutdownController.signal }).then(async () => {
|
||||||
const exist = await this.noteUnreadsRepository.findOneBy({ id: unread.id });
|
const exist = await this.noteUnreadsRepository.exist({ where: { id: unread.id } });
|
||||||
|
|
||||||
if (exist == null) return;
|
if (!exist) return;
|
||||||
|
|
||||||
if (params.isMentioned) {
|
if (params.isMentioned) {
|
||||||
this.globalEventService.publishMainStream(userId, 'unreadMention', note.id);
|
this.globalEventService.publishMainStream(userId, 'unreadMention', note.id);
|
||||||
|
|
|
@ -71,12 +71,12 @@ export class SignupService {
|
||||||
const secret = generateUserToken();
|
const secret = generateUserToken();
|
||||||
|
|
||||||
// Check username duplication
|
// Check username duplication
|
||||||
if (await this.usersRepository.findOneBy({ usernameLower: username.toLowerCase(), host: IsNull() })) {
|
if (await this.usersRepository.exist({ where: { usernameLower: username.toLowerCase(), host: IsNull() } })) {
|
||||||
throw new Error('DUPLICATED_USERNAME');
|
throw new Error('DUPLICATED_USERNAME');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check deleted username duplication
|
// Check deleted username duplication
|
||||||
if (await this.usedUsernamesRepository.findOneBy({ username: username.toLowerCase() })) {
|
if (await this.usedUsernamesRepository.exist({ where: { username: username.toLowerCase() } })) {
|
||||||
throw new Error('USED_USERNAME');
|
throw new Error('USED_USERNAME');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -122,22 +122,26 @@ export class UserFollowingService implements OnModuleInit {
|
||||||
let autoAccept = false;
|
let autoAccept = false;
|
||||||
|
|
||||||
// 鍵アカウントであっても、既にフォローされていた場合はスルー
|
// 鍵アカウントであっても、既にフォローされていた場合はスルー
|
||||||
const following = await this.followingsRepository.findOneBy({
|
const isFollowing = await this.followingsRepository.exist({
|
||||||
followerId: follower.id,
|
where: {
|
||||||
followeeId: followee.id,
|
followerId: follower.id,
|
||||||
|
followeeId: followee.id,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
if (following) {
|
if (isFollowing) {
|
||||||
autoAccept = true;
|
autoAccept = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// フォローしているユーザーは自動承認オプション
|
// フォローしているユーザーは自動承認オプション
|
||||||
if (!autoAccept && (this.userEntityService.isLocalUser(followee) && followeeProfile.autoAcceptFollowed)) {
|
if (!autoAccept && (this.userEntityService.isLocalUser(followee) && followeeProfile.autoAcceptFollowed)) {
|
||||||
const followed = await this.followingsRepository.findOneBy({
|
const isFollowed = await this.followingsRepository.exist({
|
||||||
followerId: followee.id,
|
where: {
|
||||||
followeeId: follower.id,
|
followerId: followee.id,
|
||||||
|
followeeId: follower.id,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (followed) autoAccept = true;
|
if (isFollowed) autoAccept = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Automatically accept if the follower is an account who has moved and the locked followee had accepted the old account.
|
// Automatically accept if the follower is an account who has moved and the locked followee had accepted the old account.
|
||||||
|
@ -206,12 +210,14 @@ export class UserFollowingService implements OnModuleInit {
|
||||||
|
|
||||||
this.cacheService.userFollowingsCache.refresh(follower.id);
|
this.cacheService.userFollowingsCache.refresh(follower.id);
|
||||||
|
|
||||||
const req = await this.followRequestsRepository.findOneBy({
|
const requestExist = await this.followRequestsRepository.exist({
|
||||||
followeeId: followee.id,
|
where: {
|
||||||
followerId: follower.id,
|
followeeId: followee.id,
|
||||||
|
followerId: follower.id,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (req) {
|
if (requestExist) {
|
||||||
await this.followRequestsRepository.delete({
|
await this.followRequestsRepository.delete({
|
||||||
followeeId: followee.id,
|
followeeId: followee.id,
|
||||||
followerId: follower.id,
|
followerId: follower.id,
|
||||||
|
@ -505,12 +511,14 @@ export class UserFollowingService implements OnModuleInit {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const request = await this.followRequestsRepository.findOneBy({
|
const requestExist = await this.followRequestsRepository.exist({
|
||||||
followeeId: followee.id,
|
where: {
|
||||||
followerId: follower.id,
|
followeeId: followee.id,
|
||||||
|
followerId: follower.id,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (request == null) {
|
if (!requestExist) {
|
||||||
throw new IdentifiableError('17447091-ce07-46dd-b331-c1fd4f15b1e7', 'request not found');
|
throw new IdentifiableError('17447091-ce07-46dd-b331-c1fd4f15b1e7', 'request not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,8 @@ type AudienceInfo = {
|
||||||
visibleUsers: User[],
|
visibleUsers: User[],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type GroupedAudience = Record<'public' | 'followers' | 'other', string[]>;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ApAudienceService {
|
export class ApAudienceService {
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -67,11 +69,11 @@ export class ApAudienceService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private groupingAudience(ids: string[], actor: RemoteUser) {
|
private groupingAudience(ids: string[], actor: RemoteUser): GroupedAudience {
|
||||||
const groups = {
|
const groups: GroupedAudience = {
|
||||||
public: [] as string[],
|
public: [],
|
||||||
followers: [] as string[],
|
followers: [],
|
||||||
other: [] as string[],
|
other: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const id of ids) {
|
for (const id of ids) {
|
||||||
|
@ -90,7 +92,7 @@ export class ApAudienceService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private isPublic(id: string) {
|
private isPublic(id: string): boolean {
|
||||||
return [
|
return [
|
||||||
'https://www.w3.org/ns/activitystreams#Public',
|
'https://www.w3.org/ns/activitystreams#Public',
|
||||||
'as#Public',
|
'as#Public',
|
||||||
|
@ -99,9 +101,7 @@ export class ApAudienceService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private isFollowers(id: string, actor: RemoteUser) {
|
private isFollowers(id: string, actor: RemoteUser): boolean {
|
||||||
return (
|
return id === (actor.followersUri ?? `${actor.uri}/followers`);
|
||||||
id === (actor.followersUri ?? `${actor.uri}/followers`)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -99,13 +99,15 @@ export class ApDbResolverService implements OnApplicationShutdown {
|
||||||
if (parsed.local) {
|
if (parsed.local) {
|
||||||
if (parsed.type !== 'users') return null;
|
if (parsed.type !== 'users') return null;
|
||||||
|
|
||||||
return await this.cacheService.userByIdCache.fetchMaybe(parsed.id, () => this.usersRepository.findOneBy({
|
return await this.cacheService.userByIdCache.fetchMaybe(
|
||||||
id: parsed.id,
|
parsed.id,
|
||||||
}).then(x => x ?? undefined)) as LocalUser | undefined ?? null;
|
() => this.usersRepository.findOneBy({ id: parsed.id }).then(x => x ?? undefined),
|
||||||
|
) as LocalUser | undefined ?? null;
|
||||||
} else {
|
} else {
|
||||||
return await this.cacheService.uriPersonCache.fetch(parsed.uri, () => this.usersRepository.findOneBy({
|
return await this.cacheService.uriPersonCache.fetch(
|
||||||
uri: parsed.uri,
|
parsed.uri,
|
||||||
})) as RemoteUser | null;
|
() => this.usersRepository.findOneBy({ uri: parsed.uri }),
|
||||||
|
) as RemoteUser | null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -145,9 +147,11 @@ export class ApDbResolverService implements OnApplicationShutdown {
|
||||||
} | null> {
|
} | null> {
|
||||||
const user = await this.apPersonService.resolvePerson(uri) as RemoteUser;
|
const user = await this.apPersonService.resolvePerson(uri) as RemoteUser;
|
||||||
|
|
||||||
if (user == null) return null;
|
const key = await this.publicKeyByUserIdCache.fetch(
|
||||||
|
user.id,
|
||||||
const key = await this.publicKeyByUserIdCache.fetch(user.id, () => this.userPublickeysRepository.findOneBy({ userId: user.id }), v => v != null);
|
() => this.userPublickeysRepository.findOneBy({ userId: user.id }),
|
||||||
|
v => v != null,
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
user,
|
user,
|
||||||
|
|
|
@ -29,6 +29,121 @@ const isFollowers = (recipe: IRecipe): recipe is IFollowersRecipe =>
|
||||||
const isDirect = (recipe: IRecipe): recipe is IDirectRecipe =>
|
const isDirect = (recipe: IRecipe): recipe is IDirectRecipe =>
|
||||||
recipe.type === 'Direct';
|
recipe.type === 'Direct';
|
||||||
|
|
||||||
|
class DeliverManager {
|
||||||
|
private actor: ThinUser;
|
||||||
|
private activity: IActivity | null;
|
||||||
|
private recipes: IRecipe[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
* @param userEntityService
|
||||||
|
* @param followingsRepository
|
||||||
|
* @param queueService
|
||||||
|
* @param actor Actor
|
||||||
|
* @param activity Activity to deliver
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
private userEntityService: UserEntityService,
|
||||||
|
private followingsRepository: FollowingsRepository,
|
||||||
|
private queueService: QueueService,
|
||||||
|
|
||||||
|
actor: { id: User['id']; host: null; },
|
||||||
|
activity: IActivity | null,
|
||||||
|
) {
|
||||||
|
// 型で弾いてはいるが一応ローカルユーザーかチェック
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
|
if (actor.host != null) throw new Error('actor.host must be null');
|
||||||
|
|
||||||
|
// パフォーマンス向上のためキューに突っ込むのはidのみに絞る
|
||||||
|
this.actor = {
|
||||||
|
id: actor.id,
|
||||||
|
};
|
||||||
|
this.activity = activity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add recipe for followers deliver
|
||||||
|
*/
|
||||||
|
@bindThis
|
||||||
|
public addFollowersRecipe(): void {
|
||||||
|
const deliver: IFollowersRecipe = {
|
||||||
|
type: 'Followers',
|
||||||
|
};
|
||||||
|
|
||||||
|
this.addRecipe(deliver);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add recipe for direct deliver
|
||||||
|
* @param to To
|
||||||
|
*/
|
||||||
|
@bindThis
|
||||||
|
public addDirectRecipe(to: RemoteUser): void {
|
||||||
|
const recipe: IDirectRecipe = {
|
||||||
|
type: 'Direct',
|
||||||
|
to,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.addRecipe(recipe);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add recipe
|
||||||
|
* @param recipe Recipe
|
||||||
|
*/
|
||||||
|
@bindThis
|
||||||
|
public addRecipe(recipe: IRecipe): void {
|
||||||
|
this.recipes.push(recipe);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute delivers
|
||||||
|
*/
|
||||||
|
@bindThis
|
||||||
|
public async execute(): Promise<void> {
|
||||||
|
// The value flags whether it is shared or not.
|
||||||
|
// key: inbox URL, value: whether it is sharedInbox
|
||||||
|
const inboxes = new Map<string, boolean>();
|
||||||
|
|
||||||
|
// build inbox list
|
||||||
|
// Process follower recipes first to avoid duplication when processing direct recipes later.
|
||||||
|
if (this.recipes.some(r => isFollowers(r))) {
|
||||||
|
// followers deliver
|
||||||
|
// TODO: SELECT DISTINCT ON ("followerSharedInbox") "followerSharedInbox" みたいな問い合わせにすればよりパフォーマンス向上できそう
|
||||||
|
// ただ、sharedInboxがnullなリモートユーザーも稀におり、その対応ができなさそう?
|
||||||
|
const followers = await this.followingsRepository.find({
|
||||||
|
where: {
|
||||||
|
followeeId: this.actor.id,
|
||||||
|
followerHost: Not(IsNull()),
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
followerSharedInbox: true,
|
||||||
|
followerInbox: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const following of followers) {
|
||||||
|
const inbox = following.followerSharedInbox ?? following.followerInbox;
|
||||||
|
if (inbox === null) throw new Error('inbox is null');
|
||||||
|
inboxes.set(inbox, following.followerSharedInbox != null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const recipe of this.recipes.filter(isDirect)) {
|
||||||
|
// check that shared inbox has not been added yet
|
||||||
|
if (recipe.to.sharedInbox !== null && inboxes.has(recipe.to.sharedInbox)) continue;
|
||||||
|
|
||||||
|
// check that they actually have an inbox
|
||||||
|
if (recipe.to.inbox === null) continue;
|
||||||
|
|
||||||
|
inboxes.set(recipe.to.inbox, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// deliver
|
||||||
|
this.queueService.deliverMany(this.actor, this.activity, inboxes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ApDeliverManagerService {
|
export class ApDeliverManagerService {
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -52,7 +167,7 @@ export class ApDeliverManagerService {
|
||||||
* @param activity Activity
|
* @param activity Activity
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async deliverToFollowers(actor: { id: LocalUser['id']; host: null; }, activity: IActivity) {
|
public async deliverToFollowers(actor: { id: LocalUser['id']; host: null; }, activity: IActivity): Promise<void> {
|
||||||
const manager = new DeliverManager(
|
const manager = new DeliverManager(
|
||||||
this.userEntityService,
|
this.userEntityService,
|
||||||
this.followingsRepository,
|
this.followingsRepository,
|
||||||
|
@ -71,7 +186,7 @@ export class ApDeliverManagerService {
|
||||||
* @param to Target user
|
* @param to Target user
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async deliverToUser(actor: { id: LocalUser['id']; host: null; }, activity: IActivity, to: RemoteUser) {
|
public async deliverToUser(actor: { id: LocalUser['id']; host: null; }, activity: IActivity, to: RemoteUser): Promise<void> {
|
||||||
const manager = new DeliverManager(
|
const manager = new DeliverManager(
|
||||||
this.userEntityService,
|
this.userEntityService,
|
||||||
this.followingsRepository,
|
this.followingsRepository,
|
||||||
|
@ -84,7 +199,7 @@ export class ApDeliverManagerService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public createDeliverManager(actor: { id: User['id']; host: null; }, activity: IActivity | null) {
|
public createDeliverManager(actor: { id: User['id']; host: null; }, activity: IActivity | null): DeliverManager {
|
||||||
return new DeliverManager(
|
return new DeliverManager(
|
||||||
this.userEntityService,
|
this.userEntityService,
|
||||||
this.followingsRepository,
|
this.followingsRepository,
|
||||||
|
@ -95,123 +210,3 @@ export class ApDeliverManagerService {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class DeliverManager {
|
|
||||||
private actor: ThinUser;
|
|
||||||
private activity: IActivity | null;
|
|
||||||
private recipes: IRecipe[] = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor
|
|
||||||
* @param userEntityService
|
|
||||||
* @param followingsRepository
|
|
||||||
* @param queueService
|
|
||||||
* @param actor Actor
|
|
||||||
* @param activity Activity to deliver
|
|
||||||
*/
|
|
||||||
constructor(
|
|
||||||
private userEntityService: UserEntityService,
|
|
||||||
private followingsRepository: FollowingsRepository,
|
|
||||||
private queueService: QueueService,
|
|
||||||
|
|
||||||
actor: { id: User['id']; host: null; },
|
|
||||||
activity: IActivity | null,
|
|
||||||
) {
|
|
||||||
// 型で弾いてはいるが一応ローカルユーザーかチェック
|
|
||||||
if (actor.host != null) throw new Error('actor.host must be null');
|
|
||||||
|
|
||||||
// パフォーマンス向上のためキューに突っ込むのはidのみに絞る
|
|
||||||
this.actor = {
|
|
||||||
id: actor.id,
|
|
||||||
};
|
|
||||||
this.activity = activity;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add recipe for followers deliver
|
|
||||||
*/
|
|
||||||
@bindThis
|
|
||||||
public addFollowersRecipe() {
|
|
||||||
const deliver = {
|
|
||||||
type: 'Followers',
|
|
||||||
} as IFollowersRecipe;
|
|
||||||
|
|
||||||
this.addRecipe(deliver);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add recipe for direct deliver
|
|
||||||
* @param to To
|
|
||||||
*/
|
|
||||||
@bindThis
|
|
||||||
public addDirectRecipe(to: RemoteUser) {
|
|
||||||
const recipe = {
|
|
||||||
type: 'Direct',
|
|
||||||
to,
|
|
||||||
} as IDirectRecipe;
|
|
||||||
|
|
||||||
this.addRecipe(recipe);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add recipe
|
|
||||||
* @param recipe Recipe
|
|
||||||
*/
|
|
||||||
@bindThis
|
|
||||||
public addRecipe(recipe: IRecipe) {
|
|
||||||
this.recipes.push(recipe);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute delivers
|
|
||||||
*/
|
|
||||||
@bindThis
|
|
||||||
public async execute() {
|
|
||||||
// The value flags whether it is shared or not.
|
|
||||||
// key: inbox URL, value: whether it is sharedInbox
|
|
||||||
const inboxes = new Map<string, boolean>();
|
|
||||||
|
|
||||||
/*
|
|
||||||
build inbox list
|
|
||||||
|
|
||||||
Process follower recipes first to avoid duplication when processing
|
|
||||||
direct recipes later.
|
|
||||||
*/
|
|
||||||
if (this.recipes.some(r => isFollowers(r))) {
|
|
||||||
// followers deliver
|
|
||||||
// TODO: SELECT DISTINCT ON ("followerSharedInbox") "followerSharedInbox" みたいな問い合わせにすればよりパフォーマンス向上できそう
|
|
||||||
// ただ、sharedInboxがnullなリモートユーザーも稀におり、その対応ができなさそう?
|
|
||||||
const followers = await this.followingsRepository.find({
|
|
||||||
where: {
|
|
||||||
followeeId: this.actor.id,
|
|
||||||
followerHost: Not(IsNull()),
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
followerSharedInbox: true,
|
|
||||||
followerInbox: true,
|
|
||||||
},
|
|
||||||
}) as {
|
|
||||||
followerSharedInbox: string | null;
|
|
||||||
followerInbox: string;
|
|
||||||
}[];
|
|
||||||
|
|
||||||
for (const following of followers) {
|
|
||||||
const inbox = following.followerSharedInbox ?? following.followerInbox;
|
|
||||||
inboxes.set(inbox, following.followerSharedInbox != null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.recipes.filter((recipe): recipe is IDirectRecipe =>
|
|
||||||
// followers recipes have already been processed
|
|
||||||
isDirect(recipe)
|
|
||||||
// check that shared inbox has not been added yet
|
|
||||||
&& !(recipe.to.sharedInbox && inboxes.has(recipe.to.sharedInbox))
|
|
||||||
// check that they actually have an inbox
|
|
||||||
&& recipe.to.inbox != null,
|
|
||||||
)
|
|
||||||
.forEach(recipe => inboxes.set(recipe.to.inbox!, false));
|
|
||||||
|
|
||||||
// deliver
|
|
||||||
this.queueService.deliverMany(this.actor, this.activity, inboxes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -21,7 +21,7 @@ import { CacheService } from '@/core/CacheService.js';
|
||||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { QueueService } from '@/core/QueueService.js';
|
import { QueueService } from '@/core/QueueService.js';
|
||||||
import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository, } from '@/models/index.js';
|
import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository } from '@/models/index.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import type { RemoteUser } from '@/models/entities/User.js';
|
import type { RemoteUser } from '@/models/entities/User.js';
|
||||||
import { getApHrefNullable, getApId, getApIds, getApType, getOneApHrefNullable, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isOrderedCollectionPage, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js';
|
import { getApHrefNullable, getApId, getApIds, getApType, getOneApHrefNullable, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isOrderedCollectionPage, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js';
|
||||||
|
@ -115,7 +115,7 @@ export class ApInboxService {
|
||||||
if (actor.uri) {
|
if (actor.uri) {
|
||||||
if (actor.lastFetchedAt == null || Date.now() - actor.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) {
|
if (actor.lastFetchedAt == null || Date.now() - actor.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) {
|
||||||
setImmediate(() => {
|
setImmediate(() => {
|
||||||
this.apPersonService.updatePerson(actor.uri!);
|
this.apPersonService.updatePerson(actor.uri);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -237,7 +237,7 @@ export class ApInboxService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async add(actor: RemoteUser, activity: IAdd): Promise<void> {
|
private async add(actor: RemoteUser, activity: IAdd): Promise<void> {
|
||||||
if ('actor' in activity && actor.uri !== activity.actor) {
|
if (actor.uri !== activity.actor) {
|
||||||
throw new Error('invalid actor');
|
throw new Error('invalid actor');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -281,7 +281,7 @@ export class ApInboxService {
|
||||||
const unlock = await this.appLockService.getApLock(uri);
|
const unlock = await this.appLockService.getApLock(uri);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 既に同じURIを持つものが登録されていないかチェック
|
// 既に同じURIを持つものが登録されていないかチェック
|
||||||
const exist = await this.apNoteService.fetchNote(uri);
|
const exist = await this.apNoteService.fetchNote(uri);
|
||||||
if (exist) {
|
if (exist) {
|
||||||
return;
|
return;
|
||||||
|
@ -300,7 +300,7 @@ export class ApInboxService {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.warn(`Error in announce target ${targetUri} - ${err.statusCode ?? err}`);
|
this.logger.warn(`Error in announce target ${targetUri} - ${err.statusCode}`);
|
||||||
}
|
}
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
@ -417,7 +417,7 @@ export class ApInboxService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async delete(actor: RemoteUser, activity: IDelete): Promise<string> {
|
private async delete(actor: RemoteUser, activity: IDelete): Promise<string> {
|
||||||
if ('actor' in activity && actor.uri !== activity.actor) {
|
if (actor.uri !== activity.actor) {
|
||||||
throw new Error('invalid actor');
|
throw new Error('invalid actor');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -428,7 +428,7 @@ export class ApInboxService {
|
||||||
// typeが不明だけど、どうせ消えてるのでremote resolveしない
|
// typeが不明だけど、どうせ消えてるのでremote resolveしない
|
||||||
formerType = undefined;
|
formerType = undefined;
|
||||||
} else {
|
} else {
|
||||||
const object = activity.object as IObject;
|
const object = activity.object;
|
||||||
if (isTombstone(object)) {
|
if (isTombstone(object)) {
|
||||||
formerType = toSingle(object.formerType);
|
formerType = toSingle(object.formerType);
|
||||||
} else {
|
} else {
|
||||||
|
@ -511,7 +511,10 @@ export class ApInboxService {
|
||||||
// 対象ユーザーは一番最初のユーザー として あとはコメントとして格納する
|
// 対象ユーザーは一番最初のユーザー として あとはコメントとして格納する
|
||||||
const uris = getApIds(activity.object);
|
const uris = getApIds(activity.object);
|
||||||
|
|
||||||
const userIds = uris.filter(uri => uri.startsWith(this.config.url + '/users/')).map(uri => uri.split('/').pop()!);
|
const userIds = uris
|
||||||
|
.filter(uri => uri.startsWith(this.config.url + '/users/'))
|
||||||
|
.map(uri => uri.split('/').at(-1))
|
||||||
|
.filter((userId): userId is string => userId !== undefined);
|
||||||
const users = await this.usersRepository.findBy({
|
const users = await this.usersRepository.findBy({
|
||||||
id: In(userIds),
|
id: In(userIds),
|
||||||
});
|
});
|
||||||
|
@ -574,7 +577,7 @@ export class ApInboxService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async remove(actor: RemoteUser, activity: IRemove): Promise<void> {
|
private async remove(actor: RemoteUser, activity: IRemove): Promise<void> {
|
||||||
if ('actor' in activity && actor.uri !== activity.actor) {
|
if (actor.uri !== activity.actor) {
|
||||||
throw new Error('invalid actor');
|
throw new Error('invalid actor');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -594,7 +597,7 @@ export class ApInboxService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async undo(actor: RemoteUser, activity: IUndo): Promise<string> {
|
private async undo(actor: RemoteUser, activity: IUndo): Promise<string> {
|
||||||
if ('actor' in activity && actor.uri !== activity.actor) {
|
if (actor.uri !== activity.actor) {
|
||||||
throw new Error('invalid actor');
|
throw new Error('invalid actor');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -626,12 +629,14 @@ export class ApInboxService {
|
||||||
return 'skip: follower not found';
|
return 'skip: follower not found';
|
||||||
}
|
}
|
||||||
|
|
||||||
const following = await this.followingsRepository.findOneBy({
|
const isFollowing = await this.followingsRepository.exist({
|
||||||
followerId: follower.id,
|
where: {
|
||||||
followeeId: actor.id,
|
followerId: follower.id,
|
||||||
|
followeeId: actor.id,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (following) {
|
if (isFollowing) {
|
||||||
await this.userFollowingService.unfollow(follower, actor);
|
await this.userFollowingService.unfollow(follower, actor);
|
||||||
return 'ok: unfollowed';
|
return 'ok: unfollowed';
|
||||||
}
|
}
|
||||||
|
@ -681,22 +686,26 @@ export class ApInboxService {
|
||||||
return 'skip: フォロー解除しようとしているユーザーはローカルユーザーではありません';
|
return 'skip: フォロー解除しようとしているユーザーはローカルユーザーではありません';
|
||||||
}
|
}
|
||||||
|
|
||||||
const req = await this.followRequestsRepository.findOneBy({
|
const requestExist = await this.followRequestsRepository.exist({
|
||||||
followerId: actor.id,
|
where: {
|
||||||
followeeId: followee.id,
|
followerId: actor.id,
|
||||||
|
followeeId: followee.id,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const following = await this.followingsRepository.findOneBy({
|
const isFollowing = await this.followingsRepository.exist({
|
||||||
followerId: actor.id,
|
where: {
|
||||||
followeeId: followee.id,
|
followerId: actor.id,
|
||||||
|
followeeId: followee.id,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (req) {
|
if (requestExist) {
|
||||||
await this.userFollowingService.cancelFollowRequest(followee, actor);
|
await this.userFollowingService.cancelFollowRequest(followee, actor);
|
||||||
return 'ok: follow request canceled';
|
return 'ok: follow request canceled';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (following) {
|
if (isFollowing) {
|
||||||
await this.userFollowingService.unfollow(actor, followee);
|
await this.userFollowingService.unfollow(actor, followee);
|
||||||
return 'ok: unfollowed';
|
return 'ok: unfollowed';
|
||||||
}
|
}
|
||||||
|
@ -721,7 +730,7 @@ export class ApInboxService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async update(actor: RemoteUser, activity: IUpdate): Promise<string> {
|
private async update(actor: RemoteUser, activity: IUpdate): Promise<string> {
|
||||||
if ('actor' in activity && actor.uri !== activity.actor) {
|
if (actor.uri !== activity.actor) {
|
||||||
return 'skip: invalid actor';
|
return 'skip: invalid actor';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -735,7 +744,7 @@ export class ApInboxService {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isActor(object)) {
|
if (isActor(object)) {
|
||||||
await this.apPersonService.updatePerson(actor.uri!, resolver, object);
|
await this.apPersonService.updatePerson(actor.uri, resolver, object);
|
||||||
return 'ok: Person updated';
|
return 'ok: Person updated';
|
||||||
} else if (getApType(object) === 'Question') {
|
} else if (getApType(object) === 'Question') {
|
||||||
await this.apQuestionService.updateQuestion(object, resolver).catch(err => console.error(err));
|
await this.apQuestionService.updateQuestion(object, resolver).catch(err => console.error(err));
|
||||||
|
|
|
@ -4,9 +4,9 @@ import { DI } from '@/di-symbols.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { MfmService } from '@/core/MfmService.js';
|
import { MfmService } from '@/core/MfmService.js';
|
||||||
import type { Note } from '@/models/entities/Note.js';
|
import type { Note } from '@/models/entities/Note.js';
|
||||||
|
import { bindThis } from '@/decorators.js';
|
||||||
import { extractApHashtagObjects } from './models/tag.js';
|
import { extractApHashtagObjects } from './models/tag.js';
|
||||||
import type { IObject } from './type.js';
|
import type { IObject } from './type.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ApMfmService {
|
export class ApMfmService {
|
||||||
|
@ -19,14 +19,13 @@ export class ApMfmService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public htmlToMfm(html: string, tag?: IObject | IObject[]) {
|
public htmlToMfm(html: string, tag?: IObject | IObject[]): string {
|
||||||
const hashtagNames = extractApHashtagObjects(tag).map(x => x.name).filter((x): x is string => x != null);
|
const hashtagNames = extractApHashtagObjects(tag).map(x => x.name);
|
||||||
|
|
||||||
return this.mfmService.fromHtml(html, hashtagNames);
|
return this.mfmService.fromHtml(html, hashtagNames);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public getNoteHtml(note: Note) {
|
public getNoteHtml(note: Note): string | null {
|
||||||
if (!note.text) return '';
|
if (!note.text) return '';
|
||||||
return this.mfmService.toHtml(mfm.parse(note.text), JSON.parse(note.mentionedRemoteUsers));
|
return this.mfmService.toHtml(mfm.parse(note.text), JSON.parse(note.mentionedRemoteUsers));
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { createPublicKey } from 'node:crypto';
|
import { createPublicKey } from 'node:crypto';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { In, IsNull } from 'typeorm';
|
import { In } from 'typeorm';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import * as mfm from 'mfm-js';
|
import * as mfm from 'mfm-js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
|
@ -26,7 +26,6 @@ import { isNotNull } from '@/misc/is-not-null.js';
|
||||||
import { LdSignatureService } from './LdSignatureService.js';
|
import { LdSignatureService } from './LdSignatureService.js';
|
||||||
import { ApMfmService } from './ApMfmService.js';
|
import { ApMfmService } from './ApMfmService.js';
|
||||||
import type { IAccept, IActivity, IAdd, IAnnounce, IApDocument, IApEmoji, IApHashtag, IApImage, IApMention, IBlock, ICreate, IDelete, IFlag, IFollow, IKey, ILike, IMove, IObject, IPost, IQuestion, IReject, IRemove, ITombstone, IUndo, IUpdate } from './type.js';
|
import type { IAccept, IActivity, IAdd, IAnnounce, IApDocument, IApEmoji, IApHashtag, IApImage, IApMention, IBlock, ICreate, IDelete, IFlag, IFollow, IKey, ILike, IMove, IObject, IPost, IQuestion, IReject, IRemove, ITombstone, IUndo, IUpdate } from './type.js';
|
||||||
import type { IIdentifier } from './models/identifier.js';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ApRendererService {
|
export class ApRendererService {
|
||||||
|
@ -63,7 +62,7 @@ export class ApRendererService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public renderAccept(object: any, user: { id: User['id']; host: null }): IAccept {
|
public renderAccept(object: string | IObject, user: { id: User['id']; host: null }): IAccept {
|
||||||
return {
|
return {
|
||||||
type: 'Accept',
|
type: 'Accept',
|
||||||
actor: this.userEntityService.genLocalUserUri(user.id),
|
actor: this.userEntityService.genLocalUserUri(user.id),
|
||||||
|
@ -72,7 +71,7 @@ export class ApRendererService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public renderAdd(user: LocalUser, target: any, object: any): IAdd {
|
public renderAdd(user: LocalUser, target: string | IObject | undefined, object: string | IObject): IAdd {
|
||||||
return {
|
return {
|
||||||
type: 'Add',
|
type: 'Add',
|
||||||
actor: this.userEntityService.genLocalUserUri(user.id),
|
actor: this.userEntityService.genLocalUserUri(user.id),
|
||||||
|
@ -82,7 +81,7 @@ export class ApRendererService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public renderAnnounce(object: any, note: Note): IAnnounce {
|
public renderAnnounce(object: string | IObject, note: Note): IAnnounce {
|
||||||
const attributedTo = this.userEntityService.genLocalUserUri(note.userId);
|
const attributedTo = this.userEntityService.genLocalUserUri(note.userId);
|
||||||
|
|
||||||
let to: string[] = [];
|
let to: string[] = [];
|
||||||
|
@ -133,13 +132,13 @@ export class ApRendererService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public renderCreate(object: IObject, note: Note): ICreate {
|
public renderCreate(object: IObject, note: Note): ICreate {
|
||||||
const activity = {
|
const activity: ICreate = {
|
||||||
id: `${this.config.url}/notes/${note.id}/activity`,
|
id: `${this.config.url}/notes/${note.id}/activity`,
|
||||||
actor: this.userEntityService.genLocalUserUri(note.userId),
|
actor: this.userEntityService.genLocalUserUri(note.userId),
|
||||||
type: 'Create',
|
type: 'Create',
|
||||||
published: note.createdAt.toISOString(),
|
published: note.createdAt.toISOString(),
|
||||||
object,
|
object,
|
||||||
} as ICreate;
|
};
|
||||||
|
|
||||||
if (object.to) activity.to = object.to;
|
if (object.to) activity.to = object.to;
|
||||||
if (object.cc) activity.cc = object.cc;
|
if (object.cc) activity.cc = object.cc;
|
||||||
|
@ -209,7 +208,7 @@ export class ApRendererService {
|
||||||
* @param id Follower|Followee ID
|
* @param id Follower|Followee ID
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async renderFollowUser(id: User['id']) {
|
public async renderFollowUser(id: User['id']): Promise<string> {
|
||||||
const user = await this.usersRepository.findOneByOrFail({ id: id }) as PartialLocalUser | PartialRemoteUser;
|
const user = await this.usersRepository.findOneByOrFail({ id: id }) as PartialLocalUser | PartialRemoteUser;
|
||||||
return this.userEntityService.getUserUri(user);
|
return this.userEntityService.getUserUri(user);
|
||||||
}
|
}
|
||||||
|
@ -223,8 +222,8 @@ export class ApRendererService {
|
||||||
return {
|
return {
|
||||||
id: requestId ?? `${this.config.url}/follows/${follower.id}/${followee.id}`,
|
id: requestId ?? `${this.config.url}/follows/${follower.id}/${followee.id}`,
|
||||||
type: 'Follow',
|
type: 'Follow',
|
||||||
actor: this.userEntityService.getUserUri(follower)!,
|
actor: this.userEntityService.getUserUri(follower),
|
||||||
object: this.userEntityService.getUserUri(followee)!,
|
object: this.userEntityService.getUserUri(followee),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -264,14 +263,14 @@ export class ApRendererService {
|
||||||
public async renderLike(noteReaction: NoteReaction, note: { uri: string | null }): Promise<ILike> {
|
public async renderLike(noteReaction: NoteReaction, note: { uri: string | null }): Promise<ILike> {
|
||||||
const reaction = noteReaction.reaction;
|
const reaction = noteReaction.reaction;
|
||||||
|
|
||||||
const object = {
|
const object: ILike = {
|
||||||
type: 'Like',
|
type: 'Like',
|
||||||
id: `${this.config.url}/likes/${noteReaction.id}`,
|
id: `${this.config.url}/likes/${noteReaction.id}`,
|
||||||
actor: `${this.config.url}/users/${noteReaction.userId}`,
|
actor: `${this.config.url}/users/${noteReaction.userId}`,
|
||||||
object: note.uri ? note.uri : `${this.config.url}/notes/${noteReaction.noteId}`,
|
object: note.uri ? note.uri : `${this.config.url}/notes/${noteReaction.noteId}`,
|
||||||
content: reaction,
|
content: reaction,
|
||||||
_misskey_reaction: reaction,
|
_misskey_reaction: reaction,
|
||||||
} as ILike;
|
};
|
||||||
|
|
||||||
if (reaction.startsWith(':')) {
|
if (reaction.startsWith(':')) {
|
||||||
const name = reaction.replaceAll(':', '');
|
const name = reaction.replaceAll(':', '');
|
||||||
|
@ -287,7 +286,7 @@ export class ApRendererService {
|
||||||
public renderMention(mention: PartialLocalUser | PartialRemoteUser): IApMention {
|
public renderMention(mention: PartialLocalUser | PartialRemoteUser): IApMention {
|
||||||
return {
|
return {
|
||||||
type: 'Mention',
|
type: 'Mention',
|
||||||
href: this.userEntityService.getUserUri(mention)!,
|
href: this.userEntityService.getUserUri(mention),
|
||||||
name: this.userEntityService.isRemoteUser(mention) ? `@${mention.username}@${mention.host}` : `@${(mention as LocalUser).username}`,
|
name: this.userEntityService.isRemoteUser(mention) ? `@${mention.username}@${mention.host}` : `@${(mention as LocalUser).username}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -297,8 +296,8 @@ export class ApRendererService {
|
||||||
src: PartialLocalUser | PartialRemoteUser,
|
src: PartialLocalUser | PartialRemoteUser,
|
||||||
dst: PartialLocalUser | PartialRemoteUser,
|
dst: PartialLocalUser | PartialRemoteUser,
|
||||||
): IMove {
|
): IMove {
|
||||||
const actor = this.userEntityService.getUserUri(src)!;
|
const actor = this.userEntityService.getUserUri(src);
|
||||||
const target = this.userEntityService.getUserUri(dst)!;
|
const target = this.userEntityService.getUserUri(dst);
|
||||||
return {
|
return {
|
||||||
id: `${this.config.url}/moves/${src.id}/${dst.id}`,
|
id: `${this.config.url}/moves/${src.id}/${dst.id}`,
|
||||||
actor,
|
actor,
|
||||||
|
@ -310,10 +309,10 @@ export class ApRendererService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async renderNote(note: Note, dive = true): Promise<IPost> {
|
public async renderNote(note: Note, dive = true): Promise<IPost> {
|
||||||
const getPromisedFiles = async (ids: string[]) => {
|
const getPromisedFiles = async (ids: string[]): Promise<DriveFile[]> => {
|
||||||
if (!ids || ids.length === 0) return [];
|
if (ids.length === 0) return [];
|
||||||
const items = await this.driveFilesRepository.findBy({ id: In(ids) });
|
const items = await this.driveFilesRepository.findBy({ id: In(ids) });
|
||||||
return ids.map(id => items.find(item => item.id === id)).filter(item => item != null) as DriveFile[];
|
return ids.map(id => items.find(item => item.id === id)).filter((item): item is DriveFile => item != null);
|
||||||
};
|
};
|
||||||
|
|
||||||
let inReplyTo;
|
let inReplyTo;
|
||||||
|
@ -323,9 +322,9 @@ export class ApRendererService {
|
||||||
inReplyToNote = await this.notesRepository.findOneBy({ id: note.replyId });
|
inReplyToNote = await this.notesRepository.findOneBy({ id: note.replyId });
|
||||||
|
|
||||||
if (inReplyToNote != null) {
|
if (inReplyToNote != null) {
|
||||||
const inReplyToUser = await this.usersRepository.findOneBy({ id: inReplyToNote.userId });
|
const inReplyToUserExist = await this.usersRepository.exist({ where: { id: inReplyToNote.userId } });
|
||||||
|
|
||||||
if (inReplyToUser != null) {
|
if (inReplyToUserExist) {
|
||||||
if (inReplyToNote.uri) {
|
if (inReplyToNote.uri) {
|
||||||
inReplyTo = inReplyToNote.uri;
|
inReplyTo = inReplyToNote.uri;
|
||||||
} else {
|
} else {
|
||||||
|
@ -375,7 +374,7 @@ export class ApRendererService {
|
||||||
id: In(note.mentions),
|
id: In(note.mentions),
|
||||||
}) : [];
|
}) : [];
|
||||||
|
|
||||||
const hashtagTags = (note.tags ?? []).map(tag => this.renderHashtag(tag));
|
const hashtagTags = note.tags.map(tag => this.renderHashtag(tag));
|
||||||
const mentionTags = mentionedUsers.map(u => this.renderMention(u as LocalUser | RemoteUser));
|
const mentionTags = mentionedUsers.map(u => this.renderMention(u as LocalUser | RemoteUser));
|
||||||
|
|
||||||
const files = await getPromisedFiles(note.fileIds);
|
const files = await getPromisedFiles(note.fileIds);
|
||||||
|
@ -451,37 +450,26 @@ export class ApRendererService {
|
||||||
@bindThis
|
@bindThis
|
||||||
public async renderPerson(user: LocalUser) {
|
public async renderPerson(user: LocalUser) {
|
||||||
const id = this.userEntityService.genLocalUserUri(user.id);
|
const id = this.userEntityService.genLocalUserUri(user.id);
|
||||||
const isSystem = !!user.username.match(/\./);
|
const isSystem = user.username.includes('.');
|
||||||
|
|
||||||
const [avatar, banner, profile] = await Promise.all([
|
const [avatar, banner, profile] = await Promise.all([
|
||||||
user.avatarId ? this.driveFilesRepository.findOneBy({ id: user.avatarId }) : Promise.resolve(undefined),
|
user.avatarId ? this.driveFilesRepository.findOneBy({ id: user.avatarId }) : undefined,
|
||||||
user.bannerId ? this.driveFilesRepository.findOneBy({ id: user.bannerId }) : Promise.resolve(undefined),
|
user.bannerId ? this.driveFilesRepository.findOneBy({ id: user.bannerId }) : undefined,
|
||||||
this.userProfilesRepository.findOneByOrFail({ userId: user.id }),
|
this.userProfilesRepository.findOneByOrFail({ userId: user.id }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const attachment: {
|
const attachment = profile.fields.map(field => ({
|
||||||
type: 'PropertyValue',
|
type: 'PropertyValue',
|
||||||
name: string,
|
name: field.name,
|
||||||
value: string,
|
value: /^https?:/.test(field.value)
|
||||||
identifier?: IIdentifier,
|
? `<a href="${new URL(field.value).href}" rel="me nofollow noopener" target="_blank">${new URL(field.value).href}</a>`
|
||||||
}[] = [];
|
: field.value,
|
||||||
|
}));
|
||||||
if (profile.fields) {
|
|
||||||
for (const field of profile.fields) {
|
|
||||||
attachment.push({
|
|
||||||
type: 'PropertyValue',
|
|
||||||
name: field.name,
|
|
||||||
value: (field.value != null && field.value.match(/^https?:/))
|
|
||||||
? `<a href="${new URL(field.value).href}" rel="me nofollow noopener" target="_blank">${new URL(field.value).href}</a>`
|
|
||||||
: field.value,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const emojis = await this.getEmojis(user.emojis);
|
const emojis = await this.getEmojis(user.emojis);
|
||||||
const apemojis = emojis.filter(emoji => !emoji.localOnly).map(emoji => this.renderEmoji(emoji));
|
const apemojis = emojis.filter(emoji => !emoji.localOnly).map(emoji => this.renderEmoji(emoji));
|
||||||
|
|
||||||
const hashtagTags = (user.tags ?? []).map(tag => this.renderHashtag(tag));
|
const hashtagTags = user.tags.map(tag => this.renderHashtag(tag));
|
||||||
|
|
||||||
const tag = [
|
const tag = [
|
||||||
...apemojis,
|
...apemojis,
|
||||||
|
@ -490,7 +478,7 @@ export class ApRendererService {
|
||||||
|
|
||||||
const keypair = await this.userKeypairService.getUserKeypair(user.id);
|
const keypair = await this.userKeypairService.getUserKeypair(user.id);
|
||||||
|
|
||||||
const person = {
|
const person: any = {
|
||||||
type: isSystem ? 'Application' : user.isBot ? 'Service' : 'Person',
|
type: isSystem ? 'Application' : user.isBot ? 'Service' : 'Person',
|
||||||
id,
|
id,
|
||||||
inbox: `${id}/inbox`,
|
inbox: `${id}/inbox`,
|
||||||
|
@ -508,11 +496,11 @@ export class ApRendererService {
|
||||||
image: banner ? this.renderImage(banner) : null,
|
image: banner ? this.renderImage(banner) : null,
|
||||||
tag,
|
tag,
|
||||||
manuallyApprovesFollowers: user.isLocked,
|
manuallyApprovesFollowers: user.isLocked,
|
||||||
discoverable: !!user.isExplorable,
|
discoverable: user.isExplorable,
|
||||||
publicKey: this.renderKey(user, keypair, '#main-key'),
|
publicKey: this.renderKey(user, keypair, '#main-key'),
|
||||||
isCat: user.isCat,
|
isCat: user.isCat,
|
||||||
attachment: attachment.length ? attachment : undefined,
|
attachment: attachment.length ? attachment : undefined,
|
||||||
} as any;
|
};
|
||||||
|
|
||||||
if (user.movedToUri) {
|
if (user.movedToUri) {
|
||||||
person.movedTo = user.movedToUri;
|
person.movedTo = user.movedToUri;
|
||||||
|
@ -552,7 +540,7 @@ export class ApRendererService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public renderReject(object: any, user: { id: User['id'] }): IReject {
|
public renderReject(object: string | IObject, user: { id: User['id'] }): IReject {
|
||||||
return {
|
return {
|
||||||
type: 'Reject',
|
type: 'Reject',
|
||||||
actor: this.userEntityService.genLocalUserUri(user.id),
|
actor: this.userEntityService.genLocalUserUri(user.id),
|
||||||
|
@ -561,7 +549,7 @@ export class ApRendererService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public renderRemove(user: { id: User['id'] }, target: any, object: any): IRemove {
|
public renderRemove(user: { id: User['id'] }, target: string | IObject | undefined, object: string | IObject): IRemove {
|
||||||
return {
|
return {
|
||||||
type: 'Remove',
|
type: 'Remove',
|
||||||
actor: this.userEntityService.genLocalUserUri(user.id),
|
actor: this.userEntityService.genLocalUserUri(user.id),
|
||||||
|
@ -579,8 +567,8 @@ export class ApRendererService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public renderUndo(object: any, user: { id: User['id'] }): IUndo {
|
public renderUndo(object: string | IObject, user: { id: User['id'] }): IUndo {
|
||||||
const id = typeof object.id === 'string' && object.id.startsWith(this.config.url) ? `${object.id}/undo` : undefined;
|
const id = typeof object !== 'string' && typeof object.id === 'string' && object.id.startsWith(this.config.url) ? `${object.id}/undo` : undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: 'Undo',
|
type: 'Undo',
|
||||||
|
@ -592,7 +580,7 @@ export class ApRendererService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public renderUpdate(object: any, user: { id: User['id'] }): IUpdate {
|
public renderUpdate(object: string | IObject, user: { id: User['id'] }): IUpdate {
|
||||||
return {
|
return {
|
||||||
id: `${this.config.url}/users/${user.id}#updates/${new Date().getTime()}`,
|
id: `${this.config.url}/users/${user.id}#updates/${new Date().getTime()}`,
|
||||||
actor: this.userEntityService.genLocalUserUri(user.id),
|
actor: this.userEntityService.genLocalUserUri(user.id),
|
||||||
|
@ -658,7 +646,7 @@ export class ApRendererService {
|
||||||
vcard: 'http://www.w3.org/2006/vcard/ns#',
|
vcard: 'http://www.w3.org/2006/vcard/ns#',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}, x as T & { id: string; });
|
}, x as T & { id: string });
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
@ -683,13 +671,13 @@ export class ApRendererService {
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public renderOrderedCollectionPage(id: string, totalItems: any, orderedItems: any, partOf: string, prev?: string, next?: string) {
|
public renderOrderedCollectionPage(id: string, totalItems: any, orderedItems: any, partOf: string, prev?: string, next?: string) {
|
||||||
const page = {
|
const page: any = {
|
||||||
id,
|
id,
|
||||||
partOf,
|
partOf,
|
||||||
type: 'OrderedCollectionPage',
|
type: 'OrderedCollectionPage',
|
||||||
totalItems,
|
totalItems,
|
||||||
orderedItems,
|
orderedItems,
|
||||||
} as any;
|
};
|
||||||
|
|
||||||
if (prev) page.prev = prev;
|
if (prev) page.prev = prev;
|
||||||
if (next) page.next = next;
|
if (next) page.next = next;
|
||||||
|
@ -706,7 +694,7 @@ export class ApRendererService {
|
||||||
* @param orderedItems attached objects (optional)
|
* @param orderedItems attached objects (optional)
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public renderOrderedCollection(id: string | null, totalItems: any, first?: string, last?: string, orderedItems?: IObject[]) {
|
public renderOrderedCollection(id: string, totalItems: number, first?: string, last?: string, orderedItems?: IObject[]) {
|
||||||
const page: any = {
|
const page: any = {
|
||||||
id,
|
id,
|
||||||
type: 'OrderedCollection',
|
type: 'OrderedCollection',
|
||||||
|
@ -722,7 +710,7 @@ export class ApRendererService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async getEmojis(names: string[]): Promise<Emoji[]> {
|
private async getEmojis(names: string[]): Promise<Emoji[]> {
|
||||||
if (names == null || names.length === 0) return [];
|
if (names.length === 0) return [];
|
||||||
|
|
||||||
const allEmojis = await this.customEmojiService.localEmojisCache.fetch();
|
const allEmojis = await this.customEmojiService.localEmojisCache.fetch();
|
||||||
const emojis = names.map(name => allEmojis.get(name)).filter(isNotNull);
|
const emojis = names.map(name => allEmojis.get(name)).filter(isNotNull);
|
||||||
|
|
|
@ -140,7 +140,7 @@ export class ApRequestService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async signedPost(user: { id: User['id'] }, url: string, object: any) {
|
public async signedPost(user: { id: User['id'] }, url: string, object: unknown): Promise<void> {
|
||||||
const body = JSON.stringify(object);
|
const body = JSON.stringify(object);
|
||||||
|
|
||||||
const keypair = await this.userKeypairService.getUserKeypair(user.id);
|
const keypair = await this.userKeypairService.getUserKeypair(user.id);
|
||||||
|
@ -169,7 +169,7 @@ export class ApRequestService {
|
||||||
* @param url URL to fetch
|
* @param url URL to fetch
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async signedGet(url: string, user: { id: User['id'] }) {
|
public async signedGet(url: string, user: { id: User['id'] }): Promise<unknown> {
|
||||||
const keypair = await this.userKeypairService.getUserKeypair(user.id);
|
const keypair = await this.userKeypairService.getUserKeypair(user.id);
|
||||||
|
|
||||||
const req = ApRequestCreator.createSignedGet({
|
const req = ApRequestCreator.createSignedGet({
|
||||||
|
|
|
@ -73,10 +73,6 @@ export class Resolver {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async resolve(value: string | IObject): Promise<IObject> {
|
public async resolve(value: string | IObject): Promise<IObject> {
|
||||||
if (value == null) {
|
|
||||||
throw new Error('resolvee is null (or undefined)');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof value !== 'string') {
|
if (typeof value !== 'string') {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
@ -116,11 +112,11 @@ export class Resolver {
|
||||||
? await this.apRequestService.signedGet(value, this.user) as IObject
|
? await this.apRequestService.signedGet(value, this.user) as IObject
|
||||||
: await this.httpRequestService.getJson(value, 'application/activity+json, application/ld+json')) as IObject;
|
: await this.httpRequestService.getJson(value, 'application/activity+json, application/ld+json')) as IObject;
|
||||||
|
|
||||||
if (object == null || (
|
if (
|
||||||
Array.isArray(object['@context']) ?
|
Array.isArray(object['@context']) ?
|
||||||
!(object['@context'] as unknown[]).includes('https://www.w3.org/ns/activitystreams') :
|
!(object['@context'] as unknown[]).includes('https://www.w3.org/ns/activitystreams') :
|
||||||
object['@context'] !== 'https://www.w3.org/ns/activitystreams'
|
object['@context'] !== 'https://www.w3.org/ns/activitystreams'
|
||||||
)) {
|
) {
|
||||||
throw new Error('invalid response');
|
throw new Error('invalid response');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,8 @@ import { Injectable } from '@nestjs/common';
|
||||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { CONTEXTS } from './misc/contexts.js';
|
import { CONTEXTS } from './misc/contexts.js';
|
||||||
|
import type { JsonLdDocument } from 'jsonld';
|
||||||
|
import type { JsonLd, RemoteDocument } from 'jsonld/jsonld-spec.js';
|
||||||
|
|
||||||
// RsaSignature2017 based from https://github.com/transmute-industries/RsaSignature2017
|
// RsaSignature2017 based from https://github.com/transmute-industries/RsaSignature2017
|
||||||
|
|
||||||
|
@ -18,22 +20,21 @@ class LdSignature {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async signRsaSignature2017(data: any, privateKey: string, creator: string, domain?: string, created?: Date): Promise<any> {
|
public async signRsaSignature2017(data: any, privateKey: string, creator: string, domain?: string, created?: Date): Promise<any> {
|
||||||
const options = {
|
const options: {
|
||||||
type: 'RsaSignature2017',
|
|
||||||
creator,
|
|
||||||
domain,
|
|
||||||
nonce: crypto.randomBytes(16).toString('hex'),
|
|
||||||
created: (created ?? new Date()).toISOString(),
|
|
||||||
} as {
|
|
||||||
type: string;
|
type: string;
|
||||||
creator: string;
|
creator: string;
|
||||||
domain?: string;
|
domain?: string;
|
||||||
nonce: string;
|
nonce: string;
|
||||||
created: string;
|
created: string;
|
||||||
|
} = {
|
||||||
|
type: 'RsaSignature2017',
|
||||||
|
creator,
|
||||||
|
nonce: crypto.randomBytes(16).toString('hex'),
|
||||||
|
created: (created ?? new Date()).toISOString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!domain) {
|
if (domain) {
|
||||||
delete options.domain;
|
options.domain = domain;
|
||||||
}
|
}
|
||||||
|
|
||||||
const toBeSigned = await this.createVerifyData(data, options);
|
const toBeSigned = await this.createVerifyData(data, options);
|
||||||
|
@ -62,7 +63,7 @@ class LdSignature {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async createVerifyData(data: any, options: any) {
|
public async createVerifyData(data: any, options: any): Promise<string> {
|
||||||
const transformedOptions = {
|
const transformedOptions = {
|
||||||
...options,
|
...options,
|
||||||
'@context': 'https://w3id.org/identity/v1',
|
'@context': 'https://w3id.org/identity/v1',
|
||||||
|
@ -82,7 +83,7 @@ class LdSignature {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async normalize(data: any) {
|
public async normalize(data: JsonLdDocument): Promise<string> {
|
||||||
const customLoader = this.getLoader();
|
const customLoader = this.getLoader();
|
||||||
// XXX: Importing jsonld dynamically since Jest frequently fails to import it statically
|
// XXX: Importing jsonld dynamically since Jest frequently fails to import it statically
|
||||||
// https://github.com/misskey-dev/misskey/pull/9894#discussion_r1103753595
|
// https://github.com/misskey-dev/misskey/pull/9894#discussion_r1103753595
|
||||||
|
@ -93,14 +94,14 @@ class LdSignature {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private getLoader() {
|
private getLoader() {
|
||||||
return async (url: string): Promise<any> => {
|
return async (url: string): Promise<RemoteDocument> => {
|
||||||
if (!url.match('^https?\:\/\/')) throw new Error(`Invalid URL ${url}`);
|
if (!/^https?:\/\//.test(url)) throw new Error(`Invalid URL ${url}`);
|
||||||
|
|
||||||
if (this.preLoad) {
|
if (this.preLoad) {
|
||||||
if (url in CONTEXTS) {
|
if (url in CONTEXTS) {
|
||||||
if (this.debug) console.debug(`HIT: ${url}`);
|
if (this.debug) console.debug(`HIT: ${url}`);
|
||||||
return {
|
return {
|
||||||
contextUrl: null,
|
contextUrl: undefined,
|
||||||
document: CONTEXTS[url],
|
document: CONTEXTS[url],
|
||||||
documentUrl: url,
|
documentUrl: url,
|
||||||
};
|
};
|
||||||
|
@ -110,7 +111,7 @@ class LdSignature {
|
||||||
if (this.debug) console.debug(`MISS: ${url}`);
|
if (this.debug) console.debug(`MISS: ${url}`);
|
||||||
const document = await this.fetchDocument(url);
|
const document = await this.fetchDocument(url);
|
||||||
return {
|
return {
|
||||||
contextUrl: null,
|
contextUrl: undefined,
|
||||||
document: document,
|
document: document,
|
||||||
documentUrl: url,
|
documentUrl: url,
|
||||||
};
|
};
|
||||||
|
@ -118,13 +119,17 @@ class LdSignature {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async fetchDocument(url: string) {
|
private async fetchDocument(url: string): Promise<JsonLd> {
|
||||||
const json = await this.httpRequestService.send(url, {
|
const json = await this.httpRequestService.send(
|
||||||
headers: {
|
url,
|
||||||
Accept: 'application/ld+json, application/json',
|
{
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/ld+json, application/json',
|
||||||
|
},
|
||||||
|
timeout: this.loderTimeout,
|
||||||
},
|
},
|
||||||
timeout: this.loderTimeout,
|
{ throwErrorWhenResponseNotOk: false },
|
||||||
}, { throwErrorWhenResponseNotOk: false }).then(res => {
|
).then(res => {
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw new Error(`${res.status} ${res.statusText}`);
|
throw new Error(`${res.status} ${res.statusText}`);
|
||||||
} else {
|
} else {
|
||||||
|
@ -132,7 +137,7 @@ class LdSignature {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return json;
|
return json as JsonLd;
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import type { JsonLd } from 'jsonld/jsonld-spec.js';
|
||||||
|
|
||||||
/* eslint:disable:quotemark indent */
|
/* eslint:disable:quotemark indent */
|
||||||
const id_v1 = {
|
const id_v1 = {
|
||||||
'@context': {
|
'@context': {
|
||||||
|
@ -86,7 +88,7 @@ const id_v1 = {
|
||||||
'accessControl': { '@id': 'perm:accessControl', '@type': '@id' },
|
'accessControl': { '@id': 'perm:accessControl', '@type': '@id' },
|
||||||
'writePermission': { '@id': 'perm:writePermission', '@type': '@id' },
|
'writePermission': { '@id': 'perm:writePermission', '@type': '@id' },
|
||||||
},
|
},
|
||||||
};
|
} satisfies JsonLd;
|
||||||
|
|
||||||
const security_v1 = {
|
const security_v1 = {
|
||||||
'@context': {
|
'@context': {
|
||||||
|
@ -137,7 +139,7 @@ const security_v1 = {
|
||||||
'signatureAlgorithm': 'sec:signingAlgorithm',
|
'signatureAlgorithm': 'sec:signingAlgorithm',
|
||||||
'signatureValue': 'sec:signatureValue',
|
'signatureValue': 'sec:signatureValue',
|
||||||
},
|
},
|
||||||
};
|
} satisfies JsonLd;
|
||||||
|
|
||||||
const activitystreams = {
|
const activitystreams = {
|
||||||
'@context': {
|
'@context': {
|
||||||
|
@ -517,9 +519,9 @@ const activitystreams = {
|
||||||
'@type': '@id',
|
'@type': '@id',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
} satisfies JsonLd;
|
||||||
|
|
||||||
export const CONTEXTS: Record<string, unknown> = {
|
export const CONTEXTS: Record<string, JsonLd> = {
|
||||||
'https://w3id.org/identity/v1': id_v1,
|
'https://w3id.org/identity/v1': id_v1,
|
||||||
'https://w3id.org/security/v1': security_v1,
|
'https://w3id.org/security/v1': security_v1,
|
||||||
'https://www.w3.org/ns/activitystreams': activitystreams,
|
'https://www.w3.org/ns/activitystreams': activitystreams,
|
||||||
|
|
|
@ -200,7 +200,7 @@ export class ApNoteService {
|
||||||
| { status: 'ok'; res: Note }
|
| { status: 'ok'; res: Note }
|
||||||
| { status: 'permerror' | 'temperror' }
|
| { status: 'permerror' | 'temperror' }
|
||||||
> => {
|
> => {
|
||||||
if (!uri.match(/^https?:/)) return { status: 'permerror' };
|
if (!/^https?:/.test(uri)) return { status: 'permerror' };
|
||||||
try {
|
try {
|
||||||
const res = await this.resolveNote(uri);
|
const res = await this.resolveNote(uri);
|
||||||
if (res == null) return { status: 'permerror' };
|
if (res == null) return { status: 'permerror' };
|
||||||
|
|
|
@ -218,7 +218,6 @@ export interface IApPropertyValue extends IObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isPropertyValue = (object: IObject): object is IApPropertyValue =>
|
export const isPropertyValue = (object: IObject): object is IApPropertyValue =>
|
||||||
object &&
|
|
||||||
getApType(object) === 'PropertyValue' &&
|
getApType(object) === 'PropertyValue' &&
|
||||||
typeof object.name === 'string' &&
|
typeof object.name === 'string' &&
|
||||||
'value' in object &&
|
'value' in object &&
|
||||||
|
|
|
@ -254,7 +254,7 @@ export default abstract class Chart<T extends Schema> {
|
||||||
private convertRawRecord(x: RawRecord<T>): KVs<T> {
|
private convertRawRecord(x: RawRecord<T>): KVs<T> {
|
||||||
const kvs = {} as Record<string, number>;
|
const kvs = {} as Record<string, number>;
|
||||||
for (const k of Object.keys(x).filter((k) => k.startsWith(COLUMN_PREFIX)) as (keyof Columns<T>)[]) {
|
for (const k of Object.keys(x).filter((k) => k.startsWith(COLUMN_PREFIX)) as (keyof Columns<T>)[]) {
|
||||||
kvs[(k as string).substr(COLUMN_PREFIX.length).split(COLUMN_DELIMITER).join('.')] = x[k] as unknown as number;
|
kvs[(k as string).substring(COLUMN_PREFIX.length).split(COLUMN_DELIMITER).join('.')] = x[k] as unknown as number;
|
||||||
}
|
}
|
||||||
return kvs as KVs<T>;
|
return kvs as KVs<T>;
|
||||||
}
|
}
|
||||||
|
@ -627,7 +627,7 @@ export default abstract class Chart<T extends Schema> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 要求された範囲の最も古い箇所に位置するログが存在しなかったら
|
// 要求された範囲の最も古い箇所に位置するログが存在しなかったら
|
||||||
} else if (!isTimeSame(new Date(logs[logs.length - 1].date * 1000), gt)) {
|
} else if (!isTimeSame(new Date(logs.at(-1)!.date * 1000), gt)) {
|
||||||
// 要求された範囲の最も古い箇所時点での最も新しいログを持ってきて末尾に追加する
|
// 要求された範囲の最も古い箇所時点での最も新しいログを持ってきて末尾に追加する
|
||||||
// (隙間埋めできないため)
|
// (隙間埋めできないため)
|
||||||
const outdatedLog = await repository.findOne({
|
const outdatedLog = await repository.findOne({
|
||||||
|
|
|
@ -47,17 +47,26 @@ export class ChannelEntityService {
|
||||||
|
|
||||||
const banner = channel.bannerId ? await this.driveFilesRepository.findOneBy({ id: channel.bannerId }) : null;
|
const banner = channel.bannerId ? await this.driveFilesRepository.findOneBy({ id: channel.bannerId }) : null;
|
||||||
|
|
||||||
const hasUnreadNote = meId ? (await this.noteUnreadsRepository.findOneBy({ noteChannelId: channel.id, userId: meId })) != null : undefined;
|
const hasUnreadNote = meId ? await this.noteUnreadsRepository.exist({
|
||||||
|
where: {
|
||||||
|
noteChannelId: channel.id,
|
||||||
|
userId: meId
|
||||||
|
},
|
||||||
|
}) : undefined;
|
||||||
|
|
||||||
const following = meId ? await this.channelFollowingsRepository.findOneBy({
|
const isFollowing = meId ? await this.channelFollowingsRepository.exist({
|
||||||
followerId: meId,
|
where: {
|
||||||
followeeId: channel.id,
|
followerId: meId,
|
||||||
}) : null;
|
followeeId: channel.id,
|
||||||
|
},
|
||||||
|
}) : false;
|
||||||
|
|
||||||
const favorite = meId ? await this.channelFavoritesRepository.findOneBy({
|
const isFavorited = meId ? await this.channelFavoritesRepository.exist({
|
||||||
userId: meId,
|
where: {
|
||||||
channelId: channel.id,
|
userId: meId,
|
||||||
}) : null;
|
channelId: channel.id,
|
||||||
|
},
|
||||||
|
}) : false;
|
||||||
|
|
||||||
const pinnedNotes = channel.pinnedNoteIds.length > 0 ? await this.notesRepository.find({
|
const pinnedNotes = channel.pinnedNoteIds.length > 0 ? await this.notesRepository.find({
|
||||||
where: {
|
where: {
|
||||||
|
@ -80,8 +89,8 @@ export class ChannelEntityService {
|
||||||
notesCount: channel.notesCount,
|
notesCount: channel.notesCount,
|
||||||
|
|
||||||
...(me ? {
|
...(me ? {
|
||||||
isFollowing: following != null,
|
isFollowing,
|
||||||
isFavorited: favorite != null,
|
isFavorited,
|
||||||
hasUnreadNote,
|
hasUnreadNote,
|
||||||
} : {}),
|
} : {}),
|
||||||
|
|
||||||
|
|
|
@ -39,7 +39,7 @@ export class ClipEntityService {
|
||||||
description: clip.description,
|
description: clip.description,
|
||||||
isPublic: clip.isPublic,
|
isPublic: clip.isPublic,
|
||||||
favoritedCount: await this.clipFavoritesRepository.countBy({ clipId: clip.id }),
|
favoritedCount: await this.clipFavoritesRepository.countBy({ clipId: clip.id }),
|
||||||
isFavorited: meId ? await this.clipFavoritesRepository.findOneBy({ clipId: clip.id, userId: meId }).then(x => x != null) : undefined,
|
isFavorited: meId ? await this.clipFavoritesRepository.exist({ where: { clipId: clip.id, userId: meId } }) : undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -40,7 +40,7 @@ export class FlashEntityService {
|
||||||
summary: flash.summary,
|
summary: flash.summary,
|
||||||
script: flash.script,
|
script: flash.script,
|
||||||
likedCount: flash.likedCount,
|
likedCount: flash.likedCount,
|
||||||
isLiked: meId ? await this.flashLikesRepository.findOneBy({ flashId: flash.id, userId: meId }).then(x => x != null) : undefined,
|
isLiked: meId ? await this.flashLikesRepository.exist({ where: { flashId: flash.id, userId: meId } }) : undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -46,7 +46,7 @@ export class GalleryPostEntityService {
|
||||||
tags: post.tags.length > 0 ? post.tags : undefined,
|
tags: post.tags.length > 0 ? post.tags : undefined,
|
||||||
isSensitive: post.isSensitive,
|
isSensitive: post.isSensitive,
|
||||||
likedCount: post.likedCount,
|
likedCount: post.likedCount,
|
||||||
isLiked: meId ? await this.galleryLikesRepository.findOneBy({ postId: post.id, userId: meId }).then(x => x != null) : undefined,
|
isLiked: meId ? await this.galleryLikesRepository.exist({ where: { postId: post.id, userId: meId } }) : undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -106,16 +106,14 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
hide = false;
|
hide = false;
|
||||||
} else {
|
} else {
|
||||||
// フォロワーかどうか
|
// フォロワーかどうか
|
||||||
const following = await this.followingsRepository.findOneBy({
|
const isFollowing = await this.followingsRepository.exist({
|
||||||
followeeId: packedNote.userId,
|
where: {
|
||||||
followerId: meId,
|
followeeId: packedNote.userId,
|
||||||
|
followerId: meId,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (following == null) {
|
hide = !isFollowing;
|
||||||
hide = true;
|
|
||||||
} else {
|
|
||||||
hide = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -97,7 +97,7 @@ export class PageEntityService {
|
||||||
eyeCatchingImage: page.eyeCatchingImageId ? await this.driveFileEntityService.pack(page.eyeCatchingImageId) : null,
|
eyeCatchingImage: page.eyeCatchingImageId ? await this.driveFileEntityService.pack(page.eyeCatchingImageId) : null,
|
||||||
attachedFiles: this.driveFileEntityService.packMany((await Promise.all(attachedFiles)).filter((x): x is DriveFile => x != null)),
|
attachedFiles: this.driveFileEntityService.packMany((await Promise.all(attachedFiles)).filter((x): x is DriveFile => x != null)),
|
||||||
likedCount: page.likedCount,
|
likedCount: page.likedCount,
|
||||||
isLiked: meId ? await this.pageLikesRepository.findOneBy({ pageId: page.id, userId: meId }).then(x => x != null) : undefined,
|
isLiked: meId ? await this.pageLikesRepository.exist({ where: { pageId: page.id, userId: meId } }) : undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -230,12 +230,14 @@ export class UserEntityService implements OnModuleInit {
|
||||||
/*
|
/*
|
||||||
const myAntennas = (await this.antennaService.getAntennas()).filter(a => a.userId === userId);
|
const myAntennas = (await this.antennaService.getAntennas()).filter(a => a.userId === userId);
|
||||||
|
|
||||||
const unread = myAntennas.length > 0 ? await this.antennaNotesRepository.findOneBy({
|
const isUnread = (myAntennas.length > 0 ? await this.antennaNotesRepository.exist({
|
||||||
antennaId: In(myAntennas.map(x => x.id)),
|
where: {
|
||||||
read: false,
|
antennaId: In(myAntennas.map(x => x.id)),
|
||||||
}) : null;
|
read: false,
|
||||||
|
},
|
||||||
|
}) : false);
|
||||||
|
|
||||||
return unread != null;
|
return isUnread;
|
||||||
*/
|
*/
|
||||||
return false; // TODO
|
return false; // TODO
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ export type Acct = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export function parse(acct: string): Acct {
|
export function parse(acct: string): Acct {
|
||||||
if (acct.startsWith('@')) acct = acct.substr(1);
|
if (acct.startsWith('@')) acct = acct.substring(1);
|
||||||
const split = acct.split('@', 2);
|
const split = acct.split('@', 2);
|
||||||
return { username: split[0], host: split[1] ?? null };
|
return { username: split[0], host: split[1] ?? null };
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,8 +67,9 @@ export function maximum(xs: number[]): number {
|
||||||
export function groupBy<T>(f: EndoRelation<T>, xs: T[]): T[][] {
|
export function groupBy<T>(f: EndoRelation<T>, xs: T[]): T[][] {
|
||||||
const groups = [] as T[][];
|
const groups = [] as T[][];
|
||||||
for (const x of xs) {
|
for (const x of xs) {
|
||||||
if (groups.length !== 0 && f(groups[groups.length - 1][0], x)) {
|
const lastGroup = groups.at(-1);
|
||||||
groups[groups.length - 1].push(x);
|
if (lastGroup !== undefined && f(lastGroup[0], x)) {
|
||||||
|
lastGroup.push(x);
|
||||||
} else {
|
} else {
|
||||||
groups.push([x]);
|
groups.push([x]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { IsNull, MoreThan, Not } from 'typeorm';
|
import { IsNull, MoreThan, Not } from 'typeorm';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { DriveFilesRepository } from '@/models/index.js';
|
import type { DriveFile, DriveFilesRepository } from '@/models/index.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
import { DriveService } from '@/core/DriveService.js';
|
import { DriveService } from '@/core/DriveService.js';
|
||||||
|
@ -31,7 +31,7 @@ export class CleanRemoteFilesProcessorService {
|
||||||
this.logger.info('Deleting cached remote files...');
|
this.logger.info('Deleting cached remote files...');
|
||||||
|
|
||||||
let deletedCount = 0;
|
let deletedCount = 0;
|
||||||
let cursor: any = null;
|
let cursor: DriveFile['id'] | null = null;
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const files = await this.driveFilesRepository.find({
|
const files = await this.driveFilesRepository.find({
|
||||||
|
@ -51,7 +51,7 @@ export class CleanRemoteFilesProcessorService {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
cursor = files[files.length - 1].id;
|
cursor = files.at(-1)?.id ?? null;
|
||||||
|
|
||||||
await Promise.all(files.map(file => this.driveService.deleteFileSync(file, true)));
|
await Promise.all(files.map(file => this.driveService.deleteFileSync(file, true)));
|
||||||
|
|
||||||
|
|
|
@ -70,7 +70,7 @@ export class DeleteAccountProcessorService {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
cursor = notes[notes.length - 1].id;
|
cursor = notes.at(-1)?.id ?? null;
|
||||||
|
|
||||||
await this.notesRepository.delete(notes.map(note => note.id));
|
await this.notesRepository.delete(notes.map(note => note.id));
|
||||||
|
|
||||||
|
@ -101,7 +101,7 @@ export class DeleteAccountProcessorService {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
cursor = files[files.length - 1].id;
|
cursor = files.at(-1)?.id ?? null;
|
||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
await this.driveService.deleteFileSync(file);
|
await this.driveService.deleteFileSync(file);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { MoreThan } from 'typeorm';
|
import { MoreThan } from 'typeorm';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { UsersRepository, DriveFilesRepository } from '@/models/index.js';
|
import type { UsersRepository, DriveFilesRepository, DriveFile } from '@/models/index.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
import { DriveService } from '@/core/DriveService.js';
|
import { DriveService } from '@/core/DriveService.js';
|
||||||
|
@ -40,7 +40,7 @@ export class DeleteDriveFilesProcessorService {
|
||||||
}
|
}
|
||||||
|
|
||||||
let deletedCount = 0;
|
let deletedCount = 0;
|
||||||
let cursor: any = null;
|
let cursor: DriveFile['id'] | null = null;
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const files = await this.driveFilesRepository.find({
|
const files = await this.driveFilesRepository.find({
|
||||||
|
@ -59,7 +59,7 @@ export class DeleteDriveFilesProcessorService {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
cursor = files[files.length - 1].id;
|
cursor = files.at(-1)?.id ?? null;
|
||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
await this.driveService.deleteFileSync(file);
|
await this.driveService.deleteFileSync(file);
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { MoreThan } from 'typeorm';
|
import { MoreThan } from 'typeorm';
|
||||||
import { format as dateFormat } from 'date-fns';
|
import { format as dateFormat } from 'date-fns';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { UsersRepository, BlockingsRepository } from '@/models/index.js';
|
import type { UsersRepository, BlockingsRepository, Blocking } from '@/models/index.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
import { DriveService } from '@/core/DriveService.js';
|
import { DriveService } from '@/core/DriveService.js';
|
||||||
|
@ -53,7 +53,7 @@ export class ExportBlockingProcessorService {
|
||||||
const stream = fs.createWriteStream(path, { flags: 'a' });
|
const stream = fs.createWriteStream(path, { flags: 'a' });
|
||||||
|
|
||||||
let exportedCount = 0;
|
let exportedCount = 0;
|
||||||
let cursor: any = null;
|
let cursor: Blocking['id'] | null = null;
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const blockings = await this.blockingsRepository.find({
|
const blockings = await this.blockingsRepository.find({
|
||||||
|
@ -72,7 +72,7 @@ export class ExportBlockingProcessorService {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
cursor = blockings[blockings.length - 1].id;
|
cursor = blockings.at(-1)?.id ?? null;
|
||||||
|
|
||||||
for (const block of blockings) {
|
for (const block of blockings) {
|
||||||
const u = await this.usersRepository.findOneBy({ id: block.blockeeId });
|
const u = await this.usersRepository.findOneBy({ id: block.blockeeId });
|
||||||
|
|
|
@ -94,7 +94,7 @@ export class ExportFavoritesProcessorService {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
cursor = favorites[favorites.length - 1].id;
|
cursor = favorites.at(-1)?.id ?? null;
|
||||||
|
|
||||||
for (const favorite of favorites) {
|
for (const favorite of favorites) {
|
||||||
let poll: Poll | undefined;
|
let poll: Poll | undefined;
|
||||||
|
|
|
@ -79,7 +79,7 @@ export class ExportFollowingProcessorService {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
cursor = followings[followings.length - 1].id;
|
cursor = followings.at(-1)?.id ?? null;
|
||||||
|
|
||||||
for (const following of followings) {
|
for (const following of followings) {
|
||||||
const u = await this.usersRepository.findOneBy({ id: following.followeeId });
|
const u = await this.usersRepository.findOneBy({ id: following.followeeId });
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { IsNull, MoreThan } from 'typeorm';
|
import { IsNull, MoreThan } from 'typeorm';
|
||||||
import { format as dateFormat } from 'date-fns';
|
import { format as dateFormat } from 'date-fns';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { MutingsRepository, UsersRepository, BlockingsRepository } from '@/models/index.js';
|
import type { MutingsRepository, UsersRepository, BlockingsRepository, Muting } from '@/models/index.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
import { DriveService } from '@/core/DriveService.js';
|
import { DriveService } from '@/core/DriveService.js';
|
||||||
|
@ -56,7 +56,7 @@ export class ExportMutingProcessorService {
|
||||||
const stream = fs.createWriteStream(path, { flags: 'a' });
|
const stream = fs.createWriteStream(path, { flags: 'a' });
|
||||||
|
|
||||||
let exportedCount = 0;
|
let exportedCount = 0;
|
||||||
let cursor: any = null;
|
let cursor: Muting['id'] | null = null;
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const mutes = await this.mutingsRepository.find({
|
const mutes = await this.mutingsRepository.find({
|
||||||
|
@ -76,7 +76,7 @@ export class ExportMutingProcessorService {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
cursor = mutes[mutes.length - 1].id;
|
cursor = mutes.at(-1)?.id ?? null;
|
||||||
|
|
||||||
for (const mute of mutes) {
|
for (const mute of mutes) {
|
||||||
const u = await this.usersRepository.findOneBy({ id: mute.muteeId });
|
const u = await this.usersRepository.findOneBy({ id: mute.muteeId });
|
||||||
|
|
|
@ -90,7 +90,7 @@ export class ExportNotesProcessorService {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
cursor = notes[notes.length - 1].id;
|
cursor = notes.at(-1)?.id ?? null;
|
||||||
|
|
||||||
for (const note of notes) {
|
for (const note of notes) {
|
||||||
let poll: Poll | undefined;
|
let poll: Poll | undefined;
|
||||||
|
|
|
@ -181,7 +181,7 @@ export class ActivityPubServerService {
|
||||||
undefined,
|
undefined,
|
||||||
inStock ? `${partOf}?${url.query({
|
inStock ? `${partOf}?${url.query({
|
||||||
page: 'true',
|
page: 'true',
|
||||||
cursor: followings[followings.length - 1].id,
|
cursor: followings.at(-1)!.id,
|
||||||
})}` : undefined,
|
})}` : undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -189,7 +189,11 @@ export class ActivityPubServerService {
|
||||||
return (this.apRendererService.addContext(rendered));
|
return (this.apRendererService.addContext(rendered));
|
||||||
} else {
|
} else {
|
||||||
// index page
|
// index page
|
||||||
const rendered = this.apRendererService.renderOrderedCollection(partOf, user.followersCount, `${partOf}?page=true`);
|
const rendered = this.apRendererService.renderOrderedCollection(
|
||||||
|
partOf,
|
||||||
|
user.followersCount,
|
||||||
|
`${partOf}?page=true`,
|
||||||
|
);
|
||||||
reply.header('Cache-Control', 'public, max-age=180');
|
reply.header('Cache-Control', 'public, max-age=180');
|
||||||
this.setResponseType(request, reply);
|
this.setResponseType(request, reply);
|
||||||
return (this.apRendererService.addContext(rendered));
|
return (this.apRendererService.addContext(rendered));
|
||||||
|
@ -269,7 +273,7 @@ export class ActivityPubServerService {
|
||||||
undefined,
|
undefined,
|
||||||
inStock ? `${partOf}?${url.query({
|
inStock ? `${partOf}?${url.query({
|
||||||
page: 'true',
|
page: 'true',
|
||||||
cursor: followings[followings.length - 1].id,
|
cursor: followings.at(-1)!.id,
|
||||||
})}` : undefined,
|
})}` : undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -277,7 +281,11 @@ export class ActivityPubServerService {
|
||||||
return (this.apRendererService.addContext(rendered));
|
return (this.apRendererService.addContext(rendered));
|
||||||
} else {
|
} else {
|
||||||
// index page
|
// index page
|
||||||
const rendered = this.apRendererService.renderOrderedCollection(partOf, user.followingCount, `${partOf}?page=true`);
|
const rendered = this.apRendererService.renderOrderedCollection(
|
||||||
|
partOf,
|
||||||
|
user.followingCount,
|
||||||
|
`${partOf}?page=true`,
|
||||||
|
);
|
||||||
reply.header('Cache-Control', 'public, max-age=180');
|
reply.header('Cache-Control', 'public, max-age=180');
|
||||||
this.setResponseType(request, reply);
|
this.setResponseType(request, reply);
|
||||||
return (this.apRendererService.addContext(rendered));
|
return (this.apRendererService.addContext(rendered));
|
||||||
|
@ -310,7 +318,10 @@ export class ActivityPubServerService {
|
||||||
|
|
||||||
const rendered = this.apRendererService.renderOrderedCollection(
|
const rendered = this.apRendererService.renderOrderedCollection(
|
||||||
`${this.config.url}/users/${userId}/collections/featured`,
|
`${this.config.url}/users/${userId}/collections/featured`,
|
||||||
renderedNotes.length, undefined, undefined, renderedNotes,
|
renderedNotes.length,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
renderedNotes,
|
||||||
);
|
);
|
||||||
|
|
||||||
reply.header('Cache-Control', 'public, max-age=180');
|
reply.header('Cache-Control', 'public, max-age=180');
|
||||||
|
@ -387,7 +398,7 @@ export class ActivityPubServerService {
|
||||||
})}` : undefined,
|
})}` : undefined,
|
||||||
notes.length ? `${partOf}?${url.query({
|
notes.length ? `${partOf}?${url.query({
|
||||||
page: 'true',
|
page: 'true',
|
||||||
until_id: notes[notes.length - 1].id,
|
until_id: notes.at(-1)!.id,
|
||||||
})}` : undefined,
|
})}` : undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -395,7 +406,9 @@ export class ActivityPubServerService {
|
||||||
return (this.apRendererService.addContext(rendered));
|
return (this.apRendererService.addContext(rendered));
|
||||||
} else {
|
} else {
|
||||||
// index page
|
// index page
|
||||||
const rendered = this.apRendererService.renderOrderedCollection(partOf, user.notesCount,
|
const rendered = this.apRendererService.renderOrderedCollection(
|
||||||
|
partOf,
|
||||||
|
user.notesCount,
|
||||||
`${partOf}?page=true`,
|
`${partOf}?page=true`,
|
||||||
`${partOf}?page=true&since_id=000000000000000000000000`,
|
`${partOf}?page=true&since_id=000000000000000000000000`,
|
||||||
);
|
);
|
||||||
|
|
|
@ -128,12 +128,12 @@ export class SignupApiService {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (instance.emailRequiredForSignup) {
|
if (instance.emailRequiredForSignup) {
|
||||||
if (await this.usersRepository.findOneBy({ usernameLower: username.toLowerCase(), host: IsNull() })) {
|
if (await this.usersRepository.exist({ where: { usernameLower: username.toLowerCase(), host: IsNull() } })) {
|
||||||
throw new FastifyReplyError(400, 'DUPLICATED_USERNAME');
|
throw new FastifyReplyError(400, 'DUPLICATED_USERNAME');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check deleted username duplication
|
// Check deleted username duplication
|
||||||
if (await this.usedUsernamesRepository.findOneBy({ username: username.toLowerCase() })) {
|
if (await this.usedUsernamesRepository.exist({ where: { username: username.toLowerCase() } })) {
|
||||||
throw new FastifyReplyError(400, 'USED_USERNAME');
|
throw new FastifyReplyError(400, 'USED_USERNAME');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -50,9 +50,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
throw e;
|
throw e;
|
||||||
});
|
});
|
||||||
|
|
||||||
const exist = await this.promoNotesRepository.findOneBy({ noteId: note.id });
|
const exist = await this.promoNotesRepository.exist({ where: { noteId: note.id } });
|
||||||
|
|
||||||
if (exist != null) {
|
if (exist) {
|
||||||
throw new ApiError(meta.errors.alreadyPromoted);
|
throw new ApiError(meta.errors.alreadyPromoted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -69,8 +69,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps) => {
|
super(meta, paramDef, async (ps) => {
|
||||||
const role = await this.rolesRepository.findOneBy({ id: ps.roleId });
|
const roleExist = await this.rolesRepository.exist({ where: { id: ps.roleId } });
|
||||||
if (role == null) {
|
if (!roleExist) {
|
||||||
throw new ApiError(meta.errors.noSuchRole);
|
throw new ApiError(meta.errors.noSuchRole);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -58,12 +58,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
const accessToken = secureRndstr(32);
|
const accessToken = secureRndstr(32);
|
||||||
|
|
||||||
// Fetch exist access token
|
// Fetch exist access token
|
||||||
const exist = await this.accessTokensRepository.findOneBy({
|
const exist = await this.accessTokensRepository.exist({
|
||||||
appId: session.appId,
|
where: {
|
||||||
userId: me.id,
|
appId: session.appId,
|
||||||
|
userId: me.id,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (exist == null) {
|
if (!exist) {
|
||||||
const app = await this.appsRepository.findOneByOrFail({ id: session.appId });
|
const app = await this.appsRepository.findOneByOrFail({ id: session.appId });
|
||||||
|
|
||||||
// Generate Hash
|
// Generate Hash
|
||||||
|
|
|
@ -84,12 +84,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check if already blocking
|
// Check if already blocking
|
||||||
const exist = await this.blockingsRepository.findOneBy({
|
const exist = await this.blockingsRepository.exist({
|
||||||
blockerId: blocker.id,
|
where: {
|
||||||
blockeeId: blockee.id,
|
blockerId: blocker.id,
|
||||||
|
blockeeId: blockee.id,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (exist != null) {
|
if (exist) {
|
||||||
throw new ApiError(meta.errors.alreadyBlocking);
|
throw new ApiError(meta.errors.alreadyBlocking);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -84,12 +84,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check not blocking
|
// Check not blocking
|
||||||
const exist = await this.blockingsRepository.findOneBy({
|
const exist = await this.blockingsRepository.exist({
|
||||||
blockerId: blocker.id,
|
where: {
|
||||||
blockeeId: blockee.id,
|
blockerId: blocker.id,
|
||||||
|
blockeeId: blockee.id,
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (exist == null) {
|
if (!exist) {
|
||||||
throw new ApiError(meta.errors.notBlocking);
|
throw new ApiError(meta.errors.notBlocking);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -87,12 +87,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
throw e;
|
throw e;
|
||||||
});
|
});
|
||||||
|
|
||||||
const exist = await this.clipNotesRepository.findOneBy({
|
const exist = await this.clipNotesRepository.exist({
|
||||||
noteId: note.id,
|
where: {
|
||||||
clipId: clip.id,
|
noteId: note.id,
|
||||||
|
clipId: clip.id,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (exist != null) {
|
if (exist) {
|
||||||
throw new ApiError(meta.errors.alreadyClipped);
|
throw new ApiError(meta.errors.alreadyClipped);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -58,12 +58,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
throw new ApiError(meta.errors.noSuchClip);
|
throw new ApiError(meta.errors.noSuchClip);
|
||||||
}
|
}
|
||||||
|
|
||||||
const exist = await this.clipFavoritesRepository.findOneBy({
|
const exist = await this.clipFavoritesRepository.exist({
|
||||||
clipId: clip.id,
|
where: {
|
||||||
userId: me.id,
|
clipId: clip.id,
|
||||||
|
userId: me.id,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (exist != null) {
|
if (exist) {
|
||||||
throw new ApiError(meta.errors.alreadyFavorited);
|
throw new ApiError(meta.errors.alreadyFavorited);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -34,12 +34,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
private driveFilesRepository: DriveFilesRepository,
|
private driveFilesRepository: DriveFilesRepository,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const file = await this.driveFilesRepository.findOneBy({
|
const exist = await this.driveFilesRepository.exist({
|
||||||
md5: ps.md5,
|
where: {
|
||||||
userId: me.id,
|
md5: ps.md5,
|
||||||
|
userId: me.id,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return file != null;
|
return exist;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,12 +66,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// if already liked
|
// if already liked
|
||||||
const exist = await this.flashLikesRepository.findOneBy({
|
const exist = await this.flashLikesRepository.exist({
|
||||||
flashId: flash.id,
|
where: {
|
||||||
userId: me.id,
|
flashId: flash.id,
|
||||||
|
userId: me.id,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (exist != null) {
|
if (exist) {
|
||||||
throw new ApiError(meta.errors.alreadyLiked);
|
throw new ApiError(meta.errors.alreadyLiked);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -99,12 +99,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check if already following
|
// Check if already following
|
||||||
const exist = await this.followingsRepository.findOneBy({
|
const exist = await this.followingsRepository.exist({
|
||||||
followerId: follower.id,
|
where: {
|
||||||
followeeId: followee.id,
|
followerId: follower.id,
|
||||||
|
followeeId: followee.id,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (exist != null) {
|
if (exist) {
|
||||||
throw new ApiError(meta.errors.alreadyFollowing);
|
throw new ApiError(meta.errors.alreadyFollowing);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -84,12 +84,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check not following
|
// Check not following
|
||||||
const exist = await this.followingsRepository.findOneBy({
|
const exist = await this.followingsRepository.exist({
|
||||||
followerId: follower.id,
|
where: {
|
||||||
followeeId: followee.id,
|
followerId: follower.id,
|
||||||
|
followeeId: followee.id,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (exist == null) {
|
if (!exist) {
|
||||||
throw new ApiError(meta.errors.notFollowing);
|
throw new ApiError(meta.errors.notFollowing);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -66,12 +66,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// if already liked
|
// if already liked
|
||||||
const exist = await this.galleryLikesRepository.findOneBy({
|
const exist = await this.galleryLikesRepository.exist({
|
||||||
postId: post.id,
|
where: {
|
||||||
userId: me.id,
|
postId: post.id,
|
||||||
|
userId: me.id,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (exist != null) {
|
if (exist) {
|
||||||
throw new ApiError(meta.errors.alreadyLiked);
|
throw new ApiError(meta.errors.alreadyLiked);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -66,8 +66,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
private downloadService: DownloadService,
|
private downloadService: DownloadService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const users = await this.usersRepository.findOneBy({ id: me.id });
|
const userExist = await this.usersRepository.exist({ where: { id: me.id } });
|
||||||
if (users === null) throw new ApiError(meta.errors.noSuchUser);
|
if (!userExist) throw new ApiError(meta.errors.noSuchUser);
|
||||||
const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
|
const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
|
||||||
if (file === null) throw new ApiError(meta.errors.noSuchFile);
|
if (file === null) throw new ApiError(meta.errors.noSuchFile);
|
||||||
if (file.size === 0) throw new ApiError(meta.errors.emptyFile);
|
if (file.size === 0) throw new ApiError(meta.errors.emptyFile);
|
||||||
|
|
|
@ -47,19 +47,21 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
// Check if announcement exists
|
// Check if announcement exists
|
||||||
const announcement = await this.announcementsRepository.findOneBy({ id: ps.announcementId });
|
const announcementExist = await this.announcementsRepository.exist({ where: { id: ps.announcementId } });
|
||||||
|
|
||||||
if (announcement == null) {
|
if (!announcementExist) {
|
||||||
throw new ApiError(meta.errors.noSuchAnnouncement);
|
throw new ApiError(meta.errors.noSuchAnnouncement);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if already read
|
// Check if already read
|
||||||
const read = await this.announcementReadsRepository.findOneBy({
|
const alreadyRead = await this.announcementReadsRepository.exist({
|
||||||
announcementId: ps.announcementId,
|
where: {
|
||||||
userId: me.id,
|
announcementId: ps.announcementId,
|
||||||
|
userId: me.id,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (read != null) {
|
if (alreadyRead) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,9 +28,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const token = await this.accessTokensRepository.findOneBy({ id: ps.tokenId });
|
const tokenExist = await this.accessTokensRepository.exist({ where: { id: ps.tokenId } });
|
||||||
|
|
||||||
if (token) {
|
if (tokenExist) {
|
||||||
await this.accessTokensRepository.delete({
|
await this.accessTokensRepository.delete({
|
||||||
id: ps.tokenId,
|
id: ps.tokenId,
|
||||||
userId: me.id,
|
userId: me.id,
|
||||||
|
|
|
@ -79,12 +79,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check if already muting
|
// Check if already muting
|
||||||
const exist = await this.mutingsRepository.findOneBy({
|
const exist = await this.mutingsRepository.exist({
|
||||||
muterId: muter.id,
|
where: {
|
||||||
muteeId: mutee.id,
|
muterId: muter.id,
|
||||||
|
muteeId: mutee.id,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (exist != null) {
|
if (exist) {
|
||||||
throw new ApiError(meta.errors.alreadyMuting);
|
throw new ApiError(meta.errors.alreadyMuting);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -217,11 +217,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
|
|
||||||
// Check blocking
|
// Check blocking
|
||||||
if (renote.userId !== me.id) {
|
if (renote.userId !== me.id) {
|
||||||
const block = await this.blockingsRepository.findOneBy({
|
const blockExist = await this.blockingsRepository.exist({
|
||||||
blockerId: renote.userId,
|
where: {
|
||||||
blockeeId: me.id,
|
blockerId: renote.userId,
|
||||||
|
blockeeId: me.id,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
if (block) {
|
if (blockExist) {
|
||||||
throw new ApiError(meta.errors.youHaveBeenBlocked);
|
throw new ApiError(meta.errors.youHaveBeenBlocked);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -240,11 +242,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
|
|
||||||
// Check blocking
|
// Check blocking
|
||||||
if (reply.userId !== me.id) {
|
if (reply.userId !== me.id) {
|
||||||
const block = await this.blockingsRepository.findOneBy({
|
const blockExist = await this.blockingsRepository.exist({
|
||||||
blockerId: reply.userId,
|
where: {
|
||||||
blockeeId: me.id,
|
blockerId: reply.userId,
|
||||||
|
blockeeId: me.id,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
if (block) {
|
if (blockExist) {
|
||||||
throw new ApiError(meta.errors.youHaveBeenBlocked);
|
throw new ApiError(meta.errors.youHaveBeenBlocked);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,12 +63,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
});
|
});
|
||||||
|
|
||||||
// if already favorited
|
// if already favorited
|
||||||
const exist = await this.noteFavoritesRepository.findOneBy({
|
const exist = await this.noteFavoritesRepository.exist({
|
||||||
noteId: note.id,
|
where: {
|
||||||
userId: me.id,
|
noteId: note.id,
|
||||||
|
userId: me.id,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (exist != null) {
|
if (exist) {
|
||||||
throw new ApiError(meta.errors.alreadyFavorited);
|
throw new ApiError(meta.errors.alreadyFavorited);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -66,12 +66,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// if already liked
|
// if already liked
|
||||||
const exist = await this.pageLikesRepository.findOneBy({
|
const exist = await this.pageLikesRepository.exist({
|
||||||
pageId: page.id,
|
where: {
|
||||||
userId: me.id,
|
pageId: page.id,
|
||||||
|
userId: me.id,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (exist != null) {
|
if (exist) {
|
||||||
throw new ApiError(meta.errors.alreadyLiked);
|
throw new ApiError(meta.errors.alreadyLiked);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -44,12 +44,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
|
|
||||||
const exist = await this.promoReadsRepository.findOneBy({
|
const exist = await this.promoReadsRepository.exist({
|
||||||
noteId: note.id,
|
where: {
|
||||||
userId: me.id,
|
noteId: note.id,
|
||||||
|
userId: me.id,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (exist != null) {
|
if (exist) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -97,11 +97,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
if (me == null) {
|
if (me == null) {
|
||||||
throw new ApiError(meta.errors.forbidden);
|
throw new ApiError(meta.errors.forbidden);
|
||||||
} else if (me.id !== user.id) {
|
} else if (me.id !== user.id) {
|
||||||
const following = await this.followingsRepository.findOneBy({
|
const isFollowing = await this.followingsRepository.exist({
|
||||||
followeeId: user.id,
|
where: {
|
||||||
followerId: me.id,
|
followeeId: user.id,
|
||||||
|
followerId: me.id,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
if (following == null) {
|
if (!isFollowing) {
|
||||||
throw new ApiError(meta.errors.forbidden);
|
throw new ApiError(meta.errors.forbidden);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -97,11 +97,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
if (me == null) {
|
if (me == null) {
|
||||||
throw new ApiError(meta.errors.forbidden);
|
throw new ApiError(meta.errors.forbidden);
|
||||||
} else if (me.id !== user.id) {
|
} else if (me.id !== user.id) {
|
||||||
const following = await this.followingsRepository.findOneBy({
|
const isFollowing = await this.followingsRepository.exist({
|
||||||
followeeId: user.id,
|
where: {
|
||||||
followerId: me.id,
|
followeeId: user.id,
|
||||||
|
followerId: me.id,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
if (following == null) {
|
if (!isFollowing) {
|
||||||
throw new ApiError(meta.errors.forbidden);
|
throw new ApiError(meta.errors.forbidden);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -84,11 +84,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
private roleService: RoleService,
|
private roleService: RoleService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const list = await this.userListsRepository.findOneBy({
|
const listExist = await this.userListsRepository.exist({
|
||||||
id: ps.listId,
|
where: {
|
||||||
isPublic: true,
|
id: ps.listId,
|
||||||
|
isPublic: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
if (list === null) throw new ApiError(meta.errors.noSuchList);
|
if (!listExist) throw new ApiError(meta.errors.noSuchList);
|
||||||
const currentCount = await this.userListsRepository.countBy({
|
const currentCount = await this.userListsRepository.countBy({
|
||||||
userId: me.id,
|
userId: me.id,
|
||||||
});
|
});
|
||||||
|
@ -114,18 +116,22 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (currentUser.id !== me.id) {
|
if (currentUser.id !== me.id) {
|
||||||
const block = await this.blockingsRepository.findOneBy({
|
const blockExist = await this.blockingsRepository.exist({
|
||||||
blockerId: currentUser.id,
|
where: {
|
||||||
blockeeId: me.id,
|
blockerId: currentUser.id,
|
||||||
|
blockeeId: me.id,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
if (block) {
|
if (blockExist) {
|
||||||
throw new ApiError(meta.errors.youHaveBeenBlocked);
|
throw new ApiError(meta.errors.youHaveBeenBlocked);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const exist = await this.userListJoiningsRepository.findOneBy({
|
const exist = await this.userListJoiningsRepository.exist({
|
||||||
userListId: userList.id,
|
where: {
|
||||||
userId: currentUser.id,
|
userListId: userList.id,
|
||||||
|
userId: currentUser.id,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (exist) {
|
if (exist) {
|
||||||
|
|
|
@ -41,21 +41,25 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const userList = await this.userListsRepository.findOneBy({
|
const userListExist = await this.userListsRepository.exist({
|
||||||
id: ps.listId,
|
where: {
|
||||||
isPublic: true,
|
id: ps.listId,
|
||||||
|
isPublic: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (userList === null) {
|
if (!userListExist) {
|
||||||
throw new ApiError(meta.errors.noSuchList);
|
throw new ApiError(meta.errors.noSuchList);
|
||||||
}
|
}
|
||||||
|
|
||||||
const exist = await this.userListFavoritesRepository.findOneBy({
|
const exist = await this.userListFavoritesRepository.exist({
|
||||||
userId: me.id,
|
where: {
|
||||||
userListId: ps.listId,
|
userId: me.id,
|
||||||
|
userListId: ps.listId,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (exist !== null) {
|
if (exist) {
|
||||||
throw new ApiError(meta.errors.alreadyFavorited);
|
throw new ApiError(meta.errors.alreadyFavorited);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -100,18 +100,22 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
|
|
||||||
// Check blocking
|
// Check blocking
|
||||||
if (user.id !== me.id) {
|
if (user.id !== me.id) {
|
||||||
const block = await this.blockingsRepository.findOneBy({
|
const blockExist = await this.blockingsRepository.exist({
|
||||||
blockerId: user.id,
|
where: {
|
||||||
blockeeId: me.id,
|
blockerId: user.id,
|
||||||
|
blockeeId: me.id,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
if (block) {
|
if (blockExist) {
|
||||||
throw new ApiError(meta.errors.youHaveBeenBlocked);
|
throw new ApiError(meta.errors.youHaveBeenBlocked);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const exist = await this.userListJoiningsRepository.findOneBy({
|
const exist = await this.userListJoiningsRepository.exist({
|
||||||
userListId: userList.id,
|
where: {
|
||||||
userId: user.id,
|
userListId: userList.id,
|
||||||
|
userId: user.id,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (exist) {
|
if (exist) {
|
||||||
|
|
|
@ -69,10 +69,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
userListId: ps.listId,
|
userListId: ps.listId,
|
||||||
});
|
});
|
||||||
if (me !== null) {
|
if (me !== null) {
|
||||||
additionalProperties.isLiked = (await this.userListFavoritesRepository.findOneBy({
|
additionalProperties.isLiked = await this.userListFavoritesRepository.exist({
|
||||||
userId: me.id,
|
where: {
|
||||||
userListId: ps.listId,
|
userId: me.id,
|
||||||
}) !== null);
|
userListId: ps.listId,
|
||||||
|
},
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
additionalProperties.isLiked = false;
|
additionalProperties.isLiked = false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,12 +39,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
private userListFavoritesRepository: UserListFavoritesRepository,
|
private userListFavoritesRepository: UserListFavoritesRepository,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const userList = await this.userListsRepository.findOneBy({
|
const userListExist = await this.userListsRepository.exist({
|
||||||
id: ps.listId,
|
where: {
|
||||||
isPublic: true,
|
id: ps.listId,
|
||||||
|
isPublic: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (userList === null) {
|
if (!userListExist) {
|
||||||
throw new ApiError(meta.errors.noSuchList);
|
throw new ApiError(meta.errors.noSuchList);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -34,11 +34,13 @@ class UserListChannel extends Channel {
|
||||||
this.listId = params.listId as string;
|
this.listId = params.listId as string;
|
||||||
|
|
||||||
// Check existence and owner
|
// Check existence and owner
|
||||||
const list = await this.userListsRepository.findOneBy({
|
const listExist = await this.userListsRepository.exist({
|
||||||
id: this.listId,
|
where: {
|
||||||
userId: this.user!.id,
|
id: this.listId,
|
||||||
|
userId: this.user!.id,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
if (!list) return;
|
if (!listExist) return;
|
||||||
|
|
||||||
// Subscribe stream
|
// Subscribe stream
|
||||||
this.subscriber.on(`userListStream:${this.listId}`, this.send);
|
this.subscriber.on(`userListStream:${this.listId}`, this.send);
|
||||||
|
|
|
@ -447,12 +447,12 @@ export async function testPaginationConsistency<Entity extends { id: string, cre
|
||||||
for (const limit of [1, 5, 10, 100, undefined]) {
|
for (const limit of [1, 5, 10, 100, undefined]) {
|
||||||
// 1. sinceId/DateとuntilId/Dateで両端を指定して取得した結果が期待通りになっていること
|
// 1. sinceId/DateとuntilId/Dateで両端を指定して取得した結果が期待通りになっていること
|
||||||
if (ordering === 'desc') {
|
if (ordering === 'desc') {
|
||||||
const end = expected[expected.length - 1];
|
const end = expected.at(-1)!;
|
||||||
let last = await fetchEntities(rangeToParam({ limit, since: end }));
|
let last = await fetchEntities(rangeToParam({ limit, since: end }));
|
||||||
const actual: Entity[] = [];
|
const actual: Entity[] = [];
|
||||||
while (last.length !== 0) {
|
while (last.length !== 0) {
|
||||||
actual.push(...last);
|
actual.push(...last);
|
||||||
last = await fetchEntities(rangeToParam({ limit, until: last[last.length - 1], since: end }));
|
last = await fetchEntities(rangeToParam({ limit, until: last.at(-1), since: end }));
|
||||||
}
|
}
|
||||||
actual.push(end);
|
actual.push(end);
|
||||||
assert.deepStrictEqual(
|
assert.deepStrictEqual(
|
||||||
|
@ -467,7 +467,7 @@ export async function testPaginationConsistency<Entity extends { id: string, cre
|
||||||
const actual: Entity[] = [];
|
const actual: Entity[] = [];
|
||||||
while (last.length !== 0) {
|
while (last.length !== 0) {
|
||||||
actual.push(...last);
|
actual.push(...last);
|
||||||
last = await fetchEntities(rangeToParam({ limit, since: last[last.length - 1] }));
|
last = await fetchEntities(rangeToParam({ limit, since: last.at(-1) }));
|
||||||
}
|
}
|
||||||
assert.deepStrictEqual(
|
assert.deepStrictEqual(
|
||||||
actual.map(({ id, createdAt }) => id + ':' + createdAt),
|
actual.map(({ id, createdAt }) => id + ':' + createdAt),
|
||||||
|
@ -480,7 +480,7 @@ export async function testPaginationConsistency<Entity extends { id: string, cre
|
||||||
const actual: Entity[] = [];
|
const actual: Entity[] = [];
|
||||||
while (last.length !== 0) {
|
while (last.length !== 0) {
|
||||||
actual.push(...last);
|
actual.push(...last);
|
||||||
last = await fetchEntities(rangeToParam({ limit, until: last[last.length - 1] }));
|
last = await fetchEntities(rangeToParam({ limit, until: last.at(-1) }));
|
||||||
}
|
}
|
||||||
assert.deepStrictEqual(
|
assert.deepStrictEqual(
|
||||||
actual.map(({ id, createdAt }) => id + ':' + createdAt),
|
actual.map(({ id, createdAt }) => id + ':' + createdAt),
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { writeFile } from 'node:fs/promises';
|
import { writeFile } from 'node:fs/promises';
|
||||||
import * as locales from '../../../locales/index.js';
|
import locales from '../../../locales/index.js';
|
||||||
|
|
||||||
await writeFile(
|
await writeFile(
|
||||||
new URL('locale.ts', import.meta.url),
|
new URL('locale.ts', import.meta.url),
|
||||||
|
|
|
@ -4,3 +4,4 @@ import { Cache } from '@/scripts/cache';
|
||||||
export const clipsCache = new Cache<misskey.entities.Clip[]>(Infinity);
|
export const clipsCache = new Cache<misskey.entities.Clip[]>(Infinity);
|
||||||
export const rolesCache = new Cache(Infinity);
|
export const rolesCache = new Cache(Infinity);
|
||||||
export const userListsCache = new Cache<misskey.entities.UserList[]>(Infinity);
|
export const userListsCache = new Cache<misskey.entities.UserList[]>(Infinity);
|
||||||
|
export const antennasCache = new Cache<misskey.entities.Antenna[]>(Infinity);
|
||||||
|
|
|
@ -39,7 +39,7 @@
|
||||||
<MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size"/>
|
<MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size"/>
|
||||||
</template>
|
</template>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
<div v-else-if="c.type === 'container'" :class="[$style.container, { [$style.fontSerif]: c.font === 'serif', [$style.fontMonospace]: c.font === 'monospace', [$style.containerCenter]: c.align === 'center' }]" :style="{ backgroundColor: c.bgColor ?? null, color: c.fgColor ?? null, borderWidth: c.borderWidth ? `${c.borderWidth}px` : 0, borderColor: c.borderColor ?? 'var(--divider)', padding: c.padding ? `${c.padding}px` : 0, borderRadius: c.rounded ? '8px' : 0 }">
|
<div v-else-if="c.type === 'container'" :class="[$style.container, { [$style.fontSerif]: c.font === 'serif', [$style.fontMonospace]: c.font === 'monospace' }]" :style="{ textAlign: c.align ?? null, backgroundColor: c.bgColor ?? null, color: c.fgColor ?? null, borderWidth: c.borderWidth ? `${c.borderWidth}px` : 0, borderColor: c.borderColor ?? 'var(--divider)', padding: c.padding ? `${c.padding}px` : 0, borderRadius: c.rounded ? '8px' : 0 }">
|
||||||
<template v-for="child in c.children" :key="child">
|
<template v-for="child in c.children" :key="child">
|
||||||
<MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size" :align="c.align"/>
|
<MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size" :align="c.align"/>
|
||||||
</template>
|
</template>
|
||||||
|
@ -102,10 +102,6 @@ function openPostForm() {
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.containerCenter {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fontSerif {
|
.fontSerif {
|
||||||
font-family: serif;
|
font-family: serif;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,24 +1,29 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div v-for="user in users" :key="user.id" style="display:inline-block;width:32px;height:32px;margin-right:8px;">
|
<div v-for="user in users.slice(0, limit)" :key="user.id" style="display:inline-block;width:32px;height:32px;margin-right:8px;">
|
||||||
<MkAvatar :user="user" style="width:32px; height:32px;" indicator link preview/>
|
<MkAvatar :user="user" style="width:32px; height:32px;" indicator link preview/>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="users.length > limit" style="display: inline-block;">...</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
|
import { UserLite } from 'misskey-js/built/entities';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
userIds: string[];
|
userIds: string[];
|
||||||
}>();
|
limit?: number;
|
||||||
|
}>(), {
|
||||||
|
limit: Infinity,
|
||||||
|
});
|
||||||
|
|
||||||
const users = ref([]);
|
const users = ref<UserLite[]>([]);
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
users.value = await os.api('users/show', {
|
users.value = await os.api('users/show', {
|
||||||
userIds: props.userIds,
|
userIds: props.userIds,
|
||||||
});
|
}) as unknown as UserLite[];
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -25,8 +25,8 @@
|
||||||
<MkDriveFileThumbnail :class="$style.thumbnail" :file="file" fit="contain"/>
|
<MkDriveFileThumbnail :class="$style.thumbnail" :file="file" fit="contain"/>
|
||||||
|
|
||||||
<p :class="$style.name">
|
<p :class="$style.name">
|
||||||
<span>{{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }}</span>
|
<span>{{ file.name.lastIndexOf('.') != -1 ? file.name.substring(0, file.name.lastIndexOf('.')) : file.name }}</span>
|
||||||
<span v-if="file.name.lastIndexOf('.') != -1" style="opacity: 0.5;">{{ file.name.substr(file.name.lastIndexOf('.')) }}</span>
|
<span v-if="file.name.lastIndexOf('.') != -1" style="opacity: 0.5;">{{ file.name.substring(file.name.lastIndexOf('.')) }}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -568,7 +568,7 @@ function fetchMoreFolders() {
|
||||||
os.api('drive/folders', {
|
os.api('drive/folders', {
|
||||||
folderId: folder.value ? folder.value.id : null,
|
folderId: folder.value ? folder.value.id : null,
|
||||||
type: props.type,
|
type: props.type,
|
||||||
untilId: folders.value[folders.value.length - 1].id,
|
untilId: folders.value.at(-1)?.id,
|
||||||
limit: max + 1,
|
limit: max + 1,
|
||||||
}).then(folders => {
|
}).then(folders => {
|
||||||
if (folders.length === max + 1) {
|
if (folders.length === max + 1) {
|
||||||
|
@ -591,7 +591,7 @@ function fetchMoreFiles() {
|
||||||
os.api('drive/files', {
|
os.api('drive/files', {
|
||||||
folderId: folder.value ? folder.value.id : null,
|
folderId: folder.value ? folder.value.id : null,
|
||||||
type: props.type,
|
type: props.type,
|
||||||
untilId: files.value[files.value.length - 1].id,
|
untilId: files.value.at(-1)?.id,
|
||||||
limit: max + 1,
|
limit: max + 1,
|
||||||
}).then(files => {
|
}).then(files => {
|
||||||
if (files.length === max + 1) {
|
if (files.length === max + 1) {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<component
|
<component
|
||||||
:is="self ? 'MkA' : 'a'" ref="el" style="word-break: break-all;" class="_link" :[attr]="self ? url.substr(local.length) : url" :rel="rel" :target="target"
|
:is="self ? 'MkA' : 'a'" ref="el" style="word-break: break-all;" class="_link" :[attr]="self ? url.substring(local.length) : url" :rel="rel" :target="target"
|
||||||
:title="url"
|
:title="url"
|
||||||
>
|
>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
|
|
|
@ -113,7 +113,7 @@ onMounted(() => {
|
||||||
right: 0,
|
right: 0,
|
||||||
},
|
},
|
||||||
imageClickAction: 'close',
|
imageClickAction: 'close',
|
||||||
tapAction: 'toggle-controls',
|
tapAction: 'close',
|
||||||
bgOpacity: 1,
|
bgOpacity: 1,
|
||||||
showAnimationDuration: 100,
|
showAnimationDuration: 100,
|
||||||
hideAnimationDuration: 100,
|
hideAnimationDuration: 100,
|
||||||
|
|
|
@ -59,8 +59,8 @@ function draw(): void {
|
||||||
|
|
||||||
polygonPoints = `0,${ viewBoxY } ${ polylinePoints } ${ viewBoxX },${ viewBoxY }`;
|
polygonPoints = `0,${ viewBoxY } ${ polylinePoints } ${ viewBoxX },${ viewBoxY }`;
|
||||||
|
|
||||||
headX = _polylinePoints[_polylinePoints.length - 1][0];
|
headX = _polylinePoints.at(-1)![0];
|
||||||
headY = _polylinePoints[_polylinePoints.length - 1][1];
|
headY = _polylinePoints.at(-1)![1];
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(() => props.src, draw, { immediate: true });
|
watch(() => props.src, draw, { immediate: true });
|
||||||
|
|
|
@ -120,7 +120,7 @@ const contextmenu = $computed(() => ([{
|
||||||
|
|
||||||
function back() {
|
function back() {
|
||||||
history.pop();
|
history.pop();
|
||||||
router.replace(history[history.length - 1].path, history[history.length - 1].key);
|
router.replace(history.at(-1)!.path, history.at(-1)!.key);
|
||||||
}
|
}
|
||||||
|
|
||||||
function reload() {
|
function reload() {
|
||||||
|
|
|
@ -233,7 +233,7 @@ const fetchMore = async (): Promise<void> => {
|
||||||
...(props.pagination.offsetMode ? {
|
...(props.pagination.offsetMode ? {
|
||||||
offset: offset.value,
|
offset: offset.value,
|
||||||
} : {
|
} : {
|
||||||
untilId: Array.from(items.value.keys())[items.value.size - 1],
|
untilId: Array.from(items.value.keys()).at(-1),
|
||||||
}),
|
}),
|
||||||
}).then(res => {
|
}).then(res => {
|
||||||
for (let i = 0; i < res.length; i++) {
|
for (let i = 0; i < res.length; i++) {
|
||||||
|
@ -297,7 +297,7 @@ const fetchMoreAhead = async (): Promise<void> => {
|
||||||
...(props.pagination.offsetMode ? {
|
...(props.pagination.offsetMode ? {
|
||||||
offset: offset.value,
|
offset: offset.value,
|
||||||
} : {
|
} : {
|
||||||
sinceId: Array.from(items.value.keys())[items.value.size - 1],
|
sinceId: Array.from(items.value.keys()).at(-1),
|
||||||
}),
|
}),
|
||||||
}).then(res => {
|
}).then(res => {
|
||||||
if (res.length === 0) {
|
if (res.length === 0) {
|
||||||
|
|
|
@ -564,7 +564,7 @@ async function onPaste(ev: ClipboardEvent) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
quoteId = paste.substr(url.length).match(/^\/notes\/(.+?)\/?$/)[1];
|
quoteId = paste.substring(url.length).match(/^\/notes\/(.+?)\/?$/)[1];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
:class="[$style.root, { [$style.reacted]: note.myReaction == reaction, [$style.canToggle]: canToggle, [$style.large]: defaultStore.state.largeNoteReactions }]"
|
:class="[$style.root, { [$style.reacted]: note.myReaction == reaction, [$style.canToggle]: canToggle, [$style.large]: defaultStore.state.largeNoteReactions }]"
|
||||||
@click="toggleReaction()"
|
@click="toggleReaction()"
|
||||||
>
|
>
|
||||||
<MkReactionIcon :class="$style.icon" :reaction="reaction" :emojiUrl="note.reactionEmojis[reaction.substr(1, reaction.length - 2)]"/>
|
<MkReactionIcon :class="$style.icon" :reaction="reaction" :emojiUrl="note.reactionEmojis[reaction.substring(1, reaction.length - 1)]"/>
|
||||||
<span :class="$style.count">{{ count }}</span>
|
<span :class="$style.count">{{ count }}</span>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -32,7 +32,7 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<component :is="self ? 'MkA' : 'a'" :class="[$style.link, { [$style.compact]: compact }]" :[attr]="self ? url.substr(local.length) : url" rel="nofollow noopener" :target="target" :title="url">
|
<component :is="self ? 'MkA' : 'a'" :class="[$style.link, { [$style.compact]: compact }]" :[attr]="self ? url.substring(local.length) : url" rel="nofollow noopener" :target="target" :title="url">
|
||||||
<div v-if="thumbnail" :class="$style.thumbnail" :style="`background-image: url('${thumbnail}')`">
|
<div v-if="thumbnail" :class="$style.thumbnail" :style="`background-image: url('${thumbnail}')`">
|
||||||
</div>
|
</div>
|
||||||
<article :class="$style.body">
|
<article :class="$style.body">
|
||||||
|
|
|
@ -88,7 +88,7 @@ onMounted(() => {
|
||||||
user = props.q;
|
user = props.q;
|
||||||
} else {
|
} else {
|
||||||
const query = props.q.startsWith('@') ?
|
const query = props.q.startsWith('@') ?
|
||||||
Acct.parse(props.q.substr(1)) :
|
Acct.parse(props.q.substring(1)) :
|
||||||
{ userId: props.q };
|
{ userId: props.q };
|
||||||
|
|
||||||
os.api('users/show', query).then(res => {
|
os.api('users/show', query).then(res => {
|
||||||
|
|
|
@ -29,11 +29,11 @@ export const Default = {
|
||||||
const canvas = within(canvasElement);
|
const canvas = within(canvasElement);
|
||||||
const a = canvas.getByRole<HTMLAnchorElement>('link');
|
const a = canvas.getByRole<HTMLAnchorElement>('link');
|
||||||
await expect(a.href).toMatch(/^https?:\/\/.*#test$/);
|
await expect(a.href).toMatch(/^https?:\/\/.*#test$/);
|
||||||
await userEvent.click(a, { button: 2 });
|
await userEvent.pointer({ keys: '[MouseRight]', target: a });
|
||||||
await tick();
|
await tick();
|
||||||
const menu = canvas.getByRole('menu');
|
const menu = canvas.getByRole('menu');
|
||||||
await expect(menu).toBeInTheDocument();
|
await expect(menu).toBeInTheDocument();
|
||||||
await userEvent.click(a, { button: 0 });
|
await userEvent.click(a);
|
||||||
a.blur();
|
a.blur();
|
||||||
await tick();
|
await tick();
|
||||||
await expect(menu).not.toBeInTheDocument();
|
await expect(menu).not.toBeInTheDocument();
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||||
import { expect } from '@storybook/jest';
|
import { expect } from '@storybook/jest';
|
||||||
import { userEvent, within } from '@storybook/testing-library';
|
import { userEvent, waitFor, within } from '@storybook/testing-library';
|
||||||
import { StoryObj } from '@storybook/vue3';
|
import { StoryObj } from '@storybook/vue3';
|
||||||
import { i18n } from '@/i18n';
|
|
||||||
import MkAd from './MkAd.vue';
|
import MkAd from './MkAd.vue';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
const common = {
|
const common = {
|
||||||
render(args) {
|
render(args) {
|
||||||
return {
|
return {
|
||||||
|
@ -36,6 +36,7 @@ const common = {
|
||||||
const i = buttons[0];
|
const i = buttons[0];
|
||||||
await expect(i).toBeInTheDocument();
|
await expect(i).toBeInTheDocument();
|
||||||
await userEvent.click(i);
|
await userEvent.click(i);
|
||||||
|
await waitFor(() => expect(canvasElement).toHaveTextContent(i18n.ts._ad.back));
|
||||||
await expect(a).not.toBeInTheDocument();
|
await expect(a).not.toBeInTheDocument();
|
||||||
await expect(i).not.toBeInTheDocument();
|
await expect(i).not.toBeInTheDocument();
|
||||||
buttons = canvas.getAllByRole<HTMLButtonElement>('button');
|
buttons = canvas.getAllByRole<HTMLButtonElement>('button');
|
||||||
|
@ -49,6 +50,7 @@ const common = {
|
||||||
await expect(back).toBeInTheDocument();
|
await expect(back).toBeInTheDocument();
|
||||||
await expect(back).toHaveTextContent(i18n.ts._ad.back);
|
await expect(back).toHaveTextContent(i18n.ts._ad.back);
|
||||||
await userEvent.click(back);
|
await userEvent.click(back);
|
||||||
|
await waitFor(() => expect(canvas.queryByRole('img')).toBeTruthy());
|
||||||
if (reduce) {
|
if (reduce) {
|
||||||
await expect(reduce).not.toBeInTheDocument();
|
await expect(reduce).not.toBeInTheDocument();
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@ const props = defineProps<{
|
||||||
useOriginalSize?: boolean;
|
useOriginalSize?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const customEmojiName = computed(() => (props.name[0] === ':' ? props.name.substr(1, props.name.length - 2) : props.name).replace('@.', ''));
|
const customEmojiName = computed(() => (props.name[0] === ':' ? props.name.substring(1, props.name.length - 1) : props.name).replace('@.', ''));
|
||||||
const isLocal = computed(() => !props.host && (customEmojiName.value.endsWith('@.') || !customEmojiName.value.includes('@')));
|
const isLocal = computed(() => !props.host && (customEmojiName.value.endsWith('@.') || !customEmojiName.value.includes('@')));
|
||||||
|
|
||||||
const rawUrl = computed(() => {
|
const rawUrl = computed(() => {
|
||||||
|
|
|
@ -11,13 +11,13 @@ export default function(props: { src: string; tag?: string; textTag?: string; },
|
||||||
parsed.push(str);
|
parsed.push(str);
|
||||||
break;
|
break;
|
||||||
} else {
|
} else {
|
||||||
if (nextBracketOpen > 0) parsed.push(str.substr(0, nextBracketOpen));
|
if (nextBracketOpen > 0) parsed.push(str.substring(0, nextBracketOpen));
|
||||||
parsed.push({
|
parsed.push({
|
||||||
arg: str.substring(nextBracketOpen + 1, nextBracketClose),
|
arg: str.substring(nextBracketOpen + 1, nextBracketClose),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
str = str.substr(nextBracketClose + 1);
|
str = str.substring(nextBracketClose + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
return h(props.tag ?? 'span', parsed.map(x => typeof x === 'string' ? (props.textTag ? h(props.textTag, x) : x) : slots[x.arg]()));
|
return h(props.tag ?? 'span', parsed.map(x => typeof x === 'string' ? (props.textTag ? h(props.textTag, x) : x) : slots[x.arg]()));
|
||||||
|
|
|
@ -88,10 +88,13 @@
|
||||||
<template #label>Special thanks</template>
|
<template #label>Special thanks</template>
|
||||||
<div class="_gaps" style="text-align: center;">
|
<div class="_gaps" style="text-align: center;">
|
||||||
<div>
|
<div>
|
||||||
<a style="display: inline-block;" class="masknetwork" title="Mask Network" href="https://mask.io/" target="_blank"><img width="200" src="https://misskey-hub.net/sponsors/masknetwork.png" alt="Mask Network"></a>
|
<a style="display: inline-block;" class="masknetwork" title="Mask Network" href="https://mask.io/" target="_blank"><img width="180" src="https://misskey-hub.net/sponsors/masknetwork.png" alt="Mask Network"></a>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<a style="display: inline-block;" class="dcadvirth" title="DC Advirth" href="https://www.dotchain.ltd/advirth" target="_blank"><img width="200" src="https://misskey-hub.net/sponsors/dcadvirth.png" alt="DC Advirth"></a>
|
<a style="display: inline-block;" class="skeb" title="Skeb" href="https://skeb.jp/" target="_blank"><img width="180" src="https://misskey-hub.net/sponsors/skeb.svg" alt="Skeb"></a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a style="display: inline-block;" class="dcadvirth" title="DC Advirth" href="https://www.dotchain.ltd/advirth" target="_blank"><img width="100" src="https://misskey-hub.net/sponsors/dcadvirth.png" alt="DC Advirth"></a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</FormSection>
|
</FormSection>
|
||||||
|
@ -161,6 +164,9 @@ const patronsWithIcon = [{
|
||||||
}, {
|
}, {
|
||||||
name: '清遊あみ',
|
name: '清遊あみ',
|
||||||
icon: 'https://misskey-hub.net/patrons/de25195b88e940a388388bea2e7637d8.jpg',
|
icon: 'https://misskey-hub.net/patrons/de25195b88e940a388388bea2e7637d8.jpg',
|
||||||
|
}, {
|
||||||
|
name: 'Nagi8410',
|
||||||
|
icon: 'https://misskey-hub.net/patrons/31b102ab4fc540ed806b0461575d38be.jpg',
|
||||||
}];
|
}];
|
||||||
|
|
||||||
const patrons = [
|
const patrons = [
|
||||||
|
@ -256,6 +262,7 @@ const patrons = [
|
||||||
'binvinyl',
|
'binvinyl',
|
||||||
'渡志郎',
|
'渡志郎',
|
||||||
'ぷーざ',
|
'ぷーざ',
|
||||||
|
'越貝鯛丸',
|
||||||
];
|
];
|
||||||
|
|
||||||
let thereIsTreasure = $ref($i && !claimedAchievements.includes('foundTreasure'));
|
let thereIsTreasure = $ref($i && !claimedAchievements.includes('foundTreasure'));
|
||||||
|
|
|
@ -85,7 +85,7 @@ onMounted(() => {
|
||||||
connection.on('stats', onStats);
|
connection.on('stats', onStats);
|
||||||
connection.on('statsLog', onStatsLog);
|
connection.on('statsLog', onStatsLog);
|
||||||
connection.send('requestLog', {
|
connection.send('requestLog', {
|
||||||
id: Math.random().toString().substr(2, 8),
|
id: Math.random().toString().substring(2, 10),
|
||||||
length: 100,
|
length: 100,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -156,7 +156,7 @@ onMounted(async () => {
|
||||||
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
queueStatsConnection.send('requestLog', {
|
queueStatsConnection.send('requestLog', {
|
||||||
id: Math.random().toString().substr(2, 8),
|
id: Math.random().toString().substring(2, 10),
|
||||||
length: 100,
|
length: 100,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -106,7 +106,7 @@ onMounted(() => {
|
||||||
connection.on('stats', onStats);
|
connection.on('stats', onStats);
|
||||||
connection.on('statsLog', onStatsLog);
|
connection.on('statsLog', onStatsLog);
|
||||||
connection.send('requestLog', {
|
connection.send('requestLog', {
|
||||||
id: Math.random().toString().substr(2, 8),
|
id: Math.random().toString().substring(2, 10),
|
||||||
length: 200,
|
length: 200,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -87,7 +87,7 @@ const props = defineProps<{
|
||||||
channelId: string;
|
channelId: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
let tab = $ref('timeline');
|
let tab = $ref('overview');
|
||||||
let channel = $ref(null);
|
let channel = $ref(null);
|
||||||
let favorited = $ref(false);
|
let favorited = $ref(false);
|
||||||
let searchQuery = $ref('');
|
let searchQuery = $ref('');
|
||||||
|
@ -107,6 +107,9 @@ watch(() => props.channelId, async () => {
|
||||||
channelId: props.channelId,
|
channelId: props.channelId,
|
||||||
});
|
});
|
||||||
favorited = channel.isFavorited;
|
favorited = channel.isFavorited;
|
||||||
|
if (favorited || channel.isFollowing) {
|
||||||
|
tab = 'timeline';
|
||||||
|
}
|
||||||
}, { immediate: true });
|
}, { immediate: true });
|
||||||
|
|
||||||
function edit() {
|
function edit() {
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
<MkButton inline @click="setTagBulk">Set tag</MkButton>
|
<MkButton inline @click="setTagBulk">Set tag</MkButton>
|
||||||
<MkButton inline @click="addTagBulk">Add tag</MkButton>
|
<MkButton inline @click="addTagBulk">Add tag</MkButton>
|
||||||
<MkButton inline @click="removeTagBulk">Remove tag</MkButton>
|
<MkButton inline @click="removeTagBulk">Remove tag</MkButton>
|
||||||
<MkButton inline @click="setLisenceBulk">Set Lisence</MkButton>
|
<MkButton inline @click="setLicenseBulk">Set License</MkButton>
|
||||||
<MkButton inline danger @click="delBulk">Delete</MkButton>
|
<MkButton inline danger @click="delBulk">Delete</MkButton>
|
||||||
</div>
|
</div>
|
||||||
<MkPagination ref="emojisPaginationComponent" :pagination="pagination">
|
<MkPagination ref="emojisPaginationComponent" :pagination="pagination">
|
||||||
|
@ -221,7 +221,7 @@ const setCategoryBulk = async () => {
|
||||||
emojisPaginationComponent.value.reload();
|
emojisPaginationComponent.value.reload();
|
||||||
};
|
};
|
||||||
|
|
||||||
const setLisenceBulk = async () => {
|
const setLicenseBulk = async () => {
|
||||||
const { canceled, result } = await os.inputText({
|
const { canceled, result } = await os.inputText({
|
||||||
title: 'License',
|
title: 'License',
|
||||||
});
|
});
|
||||||
|
|
|
@ -9,6 +9,7 @@ import XAntenna from './editor.vue';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||||
import { useRouter } from '@/router';
|
import { useRouter } from '@/router';
|
||||||
|
import { antennasCache } from '@/cache';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
@ -26,13 +27,10 @@ let draft = $ref({
|
||||||
});
|
});
|
||||||
|
|
||||||
function onAntennaCreated() {
|
function onAntennaCreated() {
|
||||||
|
antennasCache.delete();
|
||||||
router.push('/my/antennas');
|
router.push('/my/antennas');
|
||||||
}
|
}
|
||||||
|
|
||||||
const headerActions = $computed(() => []);
|
|
||||||
|
|
||||||
const headerTabs = $computed(() => []);
|
|
||||||
|
|
||||||
definePageMetadata({
|
definePageMetadata({
|
||||||
title: i18n.ts.manageAntennas,
|
title: i18n.ts.manageAntennas,
|
||||||
icon: 'ti ti-antenna',
|
icon: 'ti ti-antenna',
|
||||||
|
|
|
@ -10,6 +10,7 @@ import * as os from '@/os';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
import { useRouter } from '@/router';
|
import { useRouter } from '@/router';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||||
|
import { antennasCache } from '@/cache';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
@ -20,6 +21,7 @@ const props = defineProps<{
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
function onAntennaUpdated() {
|
function onAntennaUpdated() {
|
||||||
|
antennasCache.delete();
|
||||||
router.push('/my/antennas');
|
router.push('/my/antennas');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,10 +29,6 @@ os.api('antennas/show', { antennaId: props.antennaId }).then((antennaResponse) =
|
||||||
antenna = antennaResponse;
|
antenna = antennaResponse;
|
||||||
});
|
});
|
||||||
|
|
||||||
const headerActions = $computed(() => []);
|
|
||||||
|
|
||||||
const headerTabs = $computed(() => []);
|
|
||||||
|
|
||||||
definePageMetadata({
|
definePageMetadata({
|
||||||
title: i18n.ts.manageAntennas,
|
title: i18n.ts.manageAntennas,
|
||||||
icon: 'ti ti-antenna',
|
icon: 'ti ti-antenna',
|
||||||
|
|
|
@ -2,15 +2,20 @@
|
||||||
<MkStickyContainer>
|
<MkStickyContainer>
|
||||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||||
<MkSpacer :contentMax="700">
|
<MkSpacer :contentMax="700">
|
||||||
<div class="ieepwinx">
|
<div>
|
||||||
<MkButton :link="true" to="/my/antennas/create" primary class="add"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
|
<div v-if="antennas.length === 0" class="empty">
|
||||||
|
<div class="_fullinfo">
|
||||||
|
<img :src="infoImageUrl" class="_ghost"/>
|
||||||
|
<div>{{ i18n.ts.nothing }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="">
|
<MkButton :link="true" to="/my/antennas/create" primary :class="$style.add"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
|
||||||
<MkPagination v-slot="{items}" ref="list" :pagination="pagination">
|
|
||||||
<MkA v-for="antenna in items" :key="antenna.id" class="ljoevbzj" :to="`/my/antennas/${antenna.id}`">
|
<div v-if="antennas.length > 0" class="_gaps">
|
||||||
<div class="name">{{ antenna.name }}</div>
|
<MkA v-for="antenna in antennas" :key="antenna.id" :class="$style.antenna" :to="`/my/antennas/${antenna.id}`">
|
||||||
</MkA>
|
<div class="name">{{ antenna.name }}</div>
|
||||||
</MkPagination>
|
</MkA>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</MkSpacer>
|
</MkSpacer>
|
||||||
|
@ -18,19 +23,31 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { } from 'vue';
|
|
||||||
import MkPagination from '@/components/MkPagination.vue';
|
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||||
|
import { antennasCache } from '@/cache';
|
||||||
|
import { api } from '@/os';
|
||||||
|
import { onActivated } from 'vue';
|
||||||
|
import { infoImageUrl } from '@/instance';
|
||||||
|
|
||||||
const pagination = {
|
const antennas = $computed(() => antennasCache.value.value ?? []);
|
||||||
endpoint: 'antennas/list' as const,
|
|
||||||
noPaging: true,
|
|
||||||
limit: 10,
|
|
||||||
};
|
|
||||||
|
|
||||||
const headerActions = $computed(() => []);
|
function fetch() {
|
||||||
|
antennasCache.fetch(() => api('antennas/list'));
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch();
|
||||||
|
|
||||||
|
const headerActions = $computed(() => [{
|
||||||
|
asFullButton: true,
|
||||||
|
icon: 'ti ti-refresh',
|
||||||
|
text: i18n.ts.reload,
|
||||||
|
handler: () => {
|
||||||
|
antennasCache.delete();
|
||||||
|
fetch();
|
||||||
|
},
|
||||||
|
}]);
|
||||||
|
|
||||||
const headerTabs = $computed(() => []);
|
const headerTabs = $computed(() => []);
|
||||||
|
|
||||||
|
@ -38,30 +55,30 @@ definePageMetadata({
|
||||||
title: i18n.ts.manageAntennas,
|
title: i18n.ts.manageAntennas,
|
||||||
icon: 'ti ti-antenna',
|
icon: 'ti ti-antenna',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onActivated(() => {
|
||||||
|
antennasCache.fetch(() => api('antennas/list'));
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" module>
|
||||||
.ieepwinx {
|
.add {
|
||||||
|
margin: 0 auto 16px auto;
|
||||||
|
}
|
||||||
|
|
||||||
> .add {
|
.antenna {
|
||||||
margin: 0 auto 16px auto;
|
display: block;
|
||||||
}
|
padding: 16px;
|
||||||
|
border: solid 1px var(--divider);
|
||||||
|
border-radius: 6px;
|
||||||
|
|
||||||
.ljoevbzj {
|
&:hover {
|
||||||
display: block;
|
border: solid 1px var(--accent);
|
||||||
padding: 16px;
|
text-decoration: none;
|
||||||
margin-bottom: 8px;
|
|
||||||
border: solid 1px var(--divider);
|
|
||||||
border-radius: 6px;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border: solid 1px var(--accent);
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .name {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -3,38 +3,43 @@
|
||||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||||
<MkSpacer :contentMax="700">
|
<MkSpacer :contentMax="700">
|
||||||
<div class="_gaps">
|
<div class="_gaps">
|
||||||
|
<div v-if="items.length === 0" class="empty">
|
||||||
|
<div class="_fullinfo">
|
||||||
|
<img :src="infoImageUrl" class="_ghost"/>
|
||||||
|
<div>{{ i18n.ts.nothing }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<MkButton primary rounded style="margin: 0 auto;" @click="create"><i class="ti ti-plus"></i> {{ i18n.ts.createList }}</MkButton>
|
<MkButton primary rounded style="margin: 0 auto;" @click="create"><i class="ti ti-plus"></i> {{ i18n.ts.createList }}</MkButton>
|
||||||
|
|
||||||
<MkPagination v-slot="{items}" ref="pagingComponent" :pagination="pagination">
|
<div v-if="items.length > 0" class="_gaps">
|
||||||
<div class="_gaps">
|
<MkA v-for="list in items" :key="list.id" class="_panel" :class="$style.list" :to="`/my/lists/${ list.id }`">
|
||||||
<MkA v-for="list in items" :key="list.id" class="_panel" :class="$style.list" :to="`/my/lists/${ list.id }`">
|
<div style="margin-bottom: 4px;">{{ list.name }}</div>
|
||||||
<div style="margin-bottom: 4px;">{{ list.name }}</div>
|
<MkAvatars :userIds="list.userIds" :limit="10"/>
|
||||||
<MkAvatars :userIds="list.userIds"/>
|
</MkA>
|
||||||
</MkA>
|
</div>
|
||||||
</div>
|
|
||||||
</MkPagination>
|
|
||||||
</div>
|
</div>
|
||||||
</MkSpacer>
|
</MkSpacer>
|
||||||
</MkStickyContainer>
|
</MkStickyContainer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { } from 'vue';
|
import { onActivated } from 'vue';
|
||||||
import MkPagination from '@/components/MkPagination.vue';
|
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import MkAvatars from '@/components/MkAvatars.vue';
|
import MkAvatars from '@/components/MkAvatars.vue';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||||
import { userListsCache } from '@/cache';
|
import { userListsCache } from '@/cache';
|
||||||
|
import { infoImageUrl } from '@/instance';
|
||||||
|
|
||||||
const pagingComponent = $shallowRef<InstanceType<typeof MkPagination>>();
|
const items = $computed(() => userListsCache.value.value ?? []);
|
||||||
|
|
||||||
const pagination = {
|
function fetch() {
|
||||||
endpoint: 'users/lists/list' as const,
|
userListsCache.fetch(() => os.api('users/lists/list'));
|
||||||
noPaging: true,
|
}
|
||||||
limit: 10,
|
|
||||||
};
|
fetch();
|
||||||
|
|
||||||
async function create() {
|
async function create() {
|
||||||
const { canceled, result: name } = await os.inputText({
|
const { canceled, result: name } = await os.inputText({
|
||||||
|
@ -43,10 +48,18 @@ async function create() {
|
||||||
if (canceled) return;
|
if (canceled) return;
|
||||||
await os.apiWithDialog('users/lists/create', { name: name });
|
await os.apiWithDialog('users/lists/create', { name: name });
|
||||||
userListsCache.delete();
|
userListsCache.delete();
|
||||||
pagingComponent.reload();
|
fetch();
|
||||||
}
|
}
|
||||||
|
|
||||||
const headerActions = $computed(() => []);
|
const headerActions = $computed(() => [{
|
||||||
|
asFullButton: true,
|
||||||
|
icon: 'ti ti-refresh',
|
||||||
|
text: i18n.ts.reload,
|
||||||
|
handler: () => {
|
||||||
|
userListsCache.delete();
|
||||||
|
fetch();
|
||||||
|
},
|
||||||
|
}]);
|
||||||
|
|
||||||
const headerTabs = $computed(() => []);
|
const headerTabs = $computed(() => []);
|
||||||
|
|
||||||
|
@ -58,6 +71,10 @@ definePageMetadata({
|
||||||
handler: create,
|
handler: create,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onActivated(() => {
|
||||||
|
fetch();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
|
|
@ -166,7 +166,7 @@ const menuDef = computed(() => [{
|
||||||
active: currentPage?.route.name === 'import-export',
|
active: currentPage?.route.name === 'import-export',
|
||||||
}, {
|
}, {
|
||||||
icon: 'ti ti-plane',
|
icon: 'ti ti-plane',
|
||||||
text: `${i18n.ts.accountMigration} (${i18n.ts.experimental})`,
|
text: `${i18n.ts.accountMigration}`,
|
||||||
to: '/settings/migration',
|
to: '/settings/migration',
|
||||||
active: currentPage?.route.name === 'migration',
|
active: currentPage?.route.name === 'migration',
|
||||||
}, {
|
}, {
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue