From d10d5a8d53fd214ca75613f73bb7149632ec9cf9 Mon Sep 17 00:00:00 2001 From: syuilo Date: Tue, 16 May 2023 12:16:37 +0900 Subject: enhance: タイムラインにフォロイーの行った他人へのリプライを含めるかどうかの設定をアカウントに保存するのをやめるように MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolve #10646 --- packages/backend/src/server/api/endpoints/i/update.ts | 2 -- .../backend/src/server/api/endpoints/notes/global-timeline.ts | 9 +++------ .../backend/src/server/api/endpoints/notes/hybrid-timeline.ts | 9 +++------ .../backend/src/server/api/endpoints/notes/local-timeline.ts | 9 +++------ packages/backend/src/server/api/endpoints/notes/timeline.ts | 9 +++------ 5 files changed, 12 insertions(+), 26 deletions(-) (limited to 'packages/backend/src/server/api/endpoints') diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index 74be00a8b8..d10f690a32 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -141,7 +141,6 @@ export const paramDef = { preventAiLearning: { type: 'boolean' }, isBot: { type: 'boolean' }, isCat: { type: 'boolean' }, - showTimelineReplies: { type: 'boolean' }, injectFeaturedNote: { type: 'boolean' }, receiveAnnouncementEmail: { type: 'boolean' }, alwaysMarkNsfw: { type: 'boolean' }, @@ -239,7 +238,6 @@ export default class extends Endpoint { if (typeof ps.hideOnlineStatus === 'boolean') updates.hideOnlineStatus = ps.hideOnlineStatus; if (typeof ps.publicReactions === 'boolean') profileUpdates.publicReactions = ps.publicReactions; if (typeof ps.isBot === 'boolean') updates.isBot = ps.isBot; - if (typeof ps.showTimelineReplies === 'boolean') updates.showTimelineReplies = ps.showTimelineReplies; if (typeof ps.carefulBot === 'boolean') profileUpdates.carefulBot = ps.carefulBot; if (typeof ps.autoAcceptFollowed === 'boolean') profileUpdates.autoAcceptFollowed = ps.autoAcceptFollowed; if (typeof ps.noCrawle === 'boolean') profileUpdates.noCrawle = ps.noCrawle; diff --git a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts index c11c1eac40..88c1ca7f58 100644 --- a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts @@ -34,11 +34,8 @@ export const meta = { export const paramDef = { type: 'object', properties: { - withFiles: { - type: 'boolean', - default: false, - description: 'Only show notes that have attached files.', - }, + withFiles: { type: 'boolean', default: false }, + withReplies: { type: 'boolean', default: false }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, @@ -78,7 +75,7 @@ export default class extends Endpoint { .leftJoinAndSelect('reply.user', 'replyUser') .leftJoinAndSelect('renote.user', 'renoteUser'); - this.queryService.generateRepliesQuery(query, me); + this.queryService.generateRepliesQuery(query, ps.withReplies, me); if (me) { this.queryService.generateMutedUserQuery(query, me); this.queryService.generateMutedNoteQuery(query, me); diff --git a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts index 89abd91c7e..7a3581e6e4 100644 --- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts @@ -46,11 +46,8 @@ export const paramDef = { includeMyRenotes: { type: 'boolean', default: true }, includeRenotedMyNotes: { type: 'boolean', default: true }, includeLocalRenotes: { type: 'boolean', default: true }, - withFiles: { - type: 'boolean', - default: false, - description: 'Only show notes that have attached files.', - }, + withFiles: { type: 'boolean', default: false }, + withReplies: { type: 'boolean', default: false }, }, required: [], } as const; @@ -98,7 +95,7 @@ export default class extends Endpoint { .setParameters(followingQuery.getParameters()); this.queryService.generateChannelQuery(query, me); - this.queryService.generateRepliesQuery(query, me); + this.queryService.generateRepliesQuery(query, ps.withReplies, me); this.queryService.generateVisibilityQuery(query, me); this.queryService.generateMutedUserQuery(query, me); this.queryService.generateMutedNoteQuery(query, me); diff --git a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts index afdafc7c55..2ee549232c 100644 --- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts @@ -36,11 +36,8 @@ export const meta = { export const paramDef = { type: 'object', properties: { - withFiles: { - type: 'boolean', - default: false, - description: 'Only show notes that have attached files.', - }, + withFiles: { type: 'boolean', default: false }, + withReplies: { type: 'boolean', default: false }, fileType: { type: 'array', items: { type: 'string', } }, @@ -86,7 +83,7 @@ export default class extends Endpoint { .leftJoinAndSelect('renote.user', 'renoteUser'); this.queryService.generateChannelQuery(query, me); - this.queryService.generateRepliesQuery(query, me); + this.queryService.generateRepliesQuery(query, ps.withReplies, me); this.queryService.generateVisibilityQuery(query, me); if (me) this.queryService.generateMutedUserQuery(query, me); if (me) this.queryService.generateMutedNoteQuery(query, me); diff --git a/packages/backend/src/server/api/endpoints/notes/timeline.ts b/packages/backend/src/server/api/endpoints/notes/timeline.ts index c6ee1e5c2b..e1f286439b 100644 --- a/packages/backend/src/server/api/endpoints/notes/timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/timeline.ts @@ -35,11 +35,8 @@ export const paramDef = { includeMyRenotes: { type: 'boolean', default: true }, includeRenotedMyNotes: { type: 'boolean', default: true }, includeLocalRenotes: { type: 'boolean', default: true }, - withFiles: { - type: 'boolean', - default: false, - description: 'Only show notes that have attached files.', - }, + withFiles: { type: 'boolean', default: false }, + withReplies: { type: 'boolean', default: false }, }, required: [], } as const; @@ -84,7 +81,7 @@ export default class extends Endpoint { } this.queryService.generateChannelQuery(query, me); - this.queryService.generateRepliesQuery(query, me); + this.queryService.generateRepliesQuery(query, ps.withReplies, me); this.queryService.generateVisibilityQuery(query, me); this.queryService.generateMutedUserQuery(query, me); this.queryService.generateMutedNoteQuery(query, me); -- cgit v1.2.3-freya From 7ce569424a67c786a4f44993ce74f06989eba9df Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 18 May 2023 18:45:49 +0900 Subject: feat: カスタム絵文字ごとにそれをリアクションとして使えるロールを設定できるように MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + locales/ja-JP.yml | 3 + .../migration/1684386446061-emoji-improve.js | 15 ++ packages/backend/src/core/CustomEmojiService.ts | 22 ++- packages/backend/src/core/MfmService.ts | 2 +- packages/backend/src/core/ReactionService.ts | 51 +++-- packages/backend/src/core/WebfingerService.ts | 7 +- .../src/core/entities/EmojiEntityService.ts | 4 + packages/backend/src/models/entities/Emoji.ts | 16 ++ packages/backend/src/models/json-schema/emoji.ts | 21 ++ .../ImportCustomEmojisProcessorService.ts | 3 + .../src/server/api/endpoints/admin/emoji/add.ts | 31 ++- .../src/server/api/endpoints/admin/emoji/update.ts | 27 +++ packages/backend/test/unit/ReactionService.ts | 42 ++-- packages/frontend/src/components/MkRolePreview.vue | 13 +- .../frontend/src/pages/custom-emojis-manager.vue | 19 +- packages/frontend/src/pages/emoji-edit-dialog.vue | 216 ++++++++++++++++----- 17 files changed, 377 insertions(+), 116 deletions(-) create mode 100644 packages/backend/migration/1684386446061-emoji-improve.js (limited to 'packages/backend/src/server/api/endpoints') diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ea1c2842a..8b6fefc8d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ ## 13.x.x (unreleased) ### General +- カスタム絵文字ごとにそれをリアクションとして使えるロールを設定できるように - タイムラインにフォロイーの行った他人へのリプライを含めるかどうかの設定をアカウントに保存するのをやめるように - 今後はAPI呼び出し時およびストリーミング接続時に設定するようになります diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 264908482c..f49f9c5a3c 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1049,6 +1049,9 @@ preventAiLearningDescription: "外部の文章生成AIや画像生成AIに対し options: "オプション" specifyUser: "ユーザー指定" failedToPreviewUrl: "プレビューできません" +update: "更新" +rolesThatCanBeUsedThisEmojiAsReaction: "リアクションとして使えるロール" +rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "ロールの指定が一つもない場合、誰でもリアクションとして使えます。" _initialAccountSetting: accountCreated: "アカウントの作成が完了しました!" diff --git a/packages/backend/migration/1684386446061-emoji-improve.js b/packages/backend/migration/1684386446061-emoji-improve.js new file mode 100644 index 0000000000..40b0a2bc5e --- /dev/null +++ b/packages/backend/migration/1684386446061-emoji-improve.js @@ -0,0 +1,15 @@ +export class EmojiImprove1684386446061 { + name = 'EmojiImprove1684386446061' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "emoji" ADD "localOnly" boolean NOT NULL DEFAULT false`); + await queryRunner.query(`ALTER TABLE "emoji" ADD "isSensitive" boolean NOT NULL DEFAULT false`); + await queryRunner.query(`ALTER TABLE "emoji" ADD "roleIdsThatCanBeUsedThisEmojiAsReaction" character varying(128) array NOT NULL DEFAULT '{}'`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "emoji" DROP COLUMN "roleIdsThatCanBeUsedThisEmojiAsReaction"`); + await queryRunner.query(`ALTER TABLE "emoji" DROP COLUMN "isSensitive"`); + await queryRunner.query(`ALTER TABLE "emoji" DROP COLUMN "localOnly"`); + } +} diff --git a/packages/backend/src/core/CustomEmojiService.ts b/packages/backend/src/core/CustomEmojiService.ts index 93557ce617..3499df38b7 100644 --- a/packages/backend/src/core/CustomEmojiService.ts +++ b/packages/backend/src/core/CustomEmojiService.ts @@ -7,7 +7,7 @@ import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import type { DriveFile } from '@/models/entities/DriveFile.js'; import type { Emoji } from '@/models/entities/Emoji.js'; -import type { EmojisRepository } from '@/models/index.js'; +import type { EmojisRepository, Role } from '@/models/index.js'; import { bindThis } from '@/decorators.js'; import { MemoryKVCache, RedisSingleCache } from '@/misc/cache.js'; import { UtilityService } from '@/core/UtilityService.js'; @@ -15,6 +15,8 @@ import type { Config } from '@/config.js'; import { query } from '@/misc/prelude/url.js'; import type { Serialized } from '@/server/api/stream/types.js'; +const parseEmojiStrRegexp = /^(\w+)(?:@([\w.-]+))?$/; + @Injectable() export class CustomEmojiService { private cache: MemoryKVCache; @@ -63,6 +65,9 @@ export class CustomEmojiService { aliases: string[]; host: string | null; license: string | null; + isSensitive: boolean; + localOnly: boolean; + roleIdsThatCanBeUsedThisEmojiAsReaction: Role['id'][]; }): Promise { const emoji = await this.emojisRepository.insert({ id: this.idService.genId(), @@ -75,6 +80,9 @@ export class CustomEmojiService { publicUrl: data.driveFile.webpublicUrl ?? data.driveFile.url, type: data.driveFile.webpublicType ?? data.driveFile.type, license: data.license, + isSensitive: data.isSensitive, + localOnly: data.localOnly, + roleIdsThatCanBeUsedThisEmojiAsReaction: data.roleIdsThatCanBeUsedThisEmojiAsReaction, }).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0])); if (data.host == null) { @@ -90,10 +98,14 @@ export class CustomEmojiService { @bindThis public async update(id: Emoji['id'], data: { + driveFile?: DriveFile; name?: string; category?: string | null; aliases?: string[]; license?: string | null; + isSensitive?: boolean; + localOnly?: boolean; + roleIdsThatCanBeUsedThisEmojiAsReaction?: Role['id'][]; }): Promise { const emoji = await this.emojisRepository.findOneByOrFail({ id: id }); const sameNameEmoji = await this.emojisRepository.findOneBy({ name: data.name, host: IsNull() }); @@ -105,6 +117,12 @@ export class CustomEmojiService { category: data.category, aliases: data.aliases, license: data.license, + isSensitive: data.isSensitive, + localOnly: data.localOnly, + originalUrl: data.driveFile != null ? data.driveFile.url : undefined, + publicUrl: data.driveFile != null ? (data.driveFile.webpublicUrl ?? data.driveFile.url) : undefined, + type: data.driveFile != null ? (data.driveFile.webpublicType ?? data.driveFile.type) : undefined, + roleIdsThatCanBeUsedThisEmojiAsReaction: data.roleIdsThatCanBeUsedThisEmojiAsReaction ?? undefined, }); this.localEmojisCache.refresh(); @@ -259,7 +277,7 @@ export class CustomEmojiService { @bindThis public parseEmojiStr(emojiName: string, noteUserHost: string | null) { - const match = emojiName.match(/^(\w+)(?:@([\w.-]+))?$/); + const match = emojiName.match(parseEmojiStrRegexp); if (!match) return { name: null, host: null }; const name = match[1]; diff --git a/packages/backend/src/core/MfmService.ts b/packages/backend/src/core/MfmService.ts index 9b2d5dc0ff..dffee16e08 100644 --- a/packages/backend/src/core/MfmService.ts +++ b/packages/backend/src/core/MfmService.ts @@ -83,7 +83,7 @@ export class MfmService { if (hashtagNames && href && hashtagNames.map(x => x.toLowerCase()).includes(txt.toLowerCase())) { text += txt; // メンション - } else if (txt.startsWith('@') && !(rel && rel.value.match(/^me /))) { + } else if (txt.startsWith('@') && !(rel && rel.value.startsWith('me '))) { const part = txt.split('@'); if (part.length === 2 && href) { diff --git a/packages/backend/src/core/ReactionService.ts b/packages/backend/src/core/ReactionService.ts index 2184cfeb41..27334b33e6 100644 --- a/packages/backend/src/core/ReactionService.ts +++ b/packages/backend/src/core/ReactionService.ts @@ -20,6 +20,7 @@ import { bindThis } from '@/decorators.js'; import { UtilityService } from '@/core/UtilityService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js'; +import { RoleService } from '@/core/RoleService.js'; const FALLBACK = '❤'; @@ -75,6 +76,7 @@ export class ReactionService { private utilityService: UtilityService, private metaService: MetaService, private customEmojiService: CustomEmojiService, + private roleService: RoleService, private userEntityService: UserEntityService, private noteEntityService: NoteEntityService, private userBlockingService: UserBlockingService, @@ -88,7 +90,7 @@ export class ReactionService { } @bindThis - public async create(user: { id: User['id']; host: User['host']; isBot: User['isBot'] }, note: Note, reaction?: string | null) { + public async create(user: { id: User['id']; host: User['host']; isBot: User['isBot'] }, note: Note, _reaction?: string | null) { // Check blocking if (note.userId !== user.id) { const blocked = await this.userBlockingService.checkBlocked(note.userId, user.id); @@ -102,10 +104,36 @@ export class ReactionService { throw new IdentifiableError('68e9d2d1-48bf-42c2-b90a-b20e09fd3d48', 'Note not accessible for you.'); } + let reaction = _reaction ?? FALLBACK; + if (note.reactionAcceptance === 'likeOnly' || ((note.reactionAcceptance === 'likeOnlyForRemote') && (user.host != null))) { reaction = '❤️'; - } else { - reaction = await this.toDbReaction(reaction, user.host); + } else if (_reaction) { + const custom = reaction.match(isCustomEmojiRegexp); + if (custom) { + const reacterHost = this.utilityService.toPunyNullable(user.host); + + const name = custom[1]; + const emoji = reacterHost == null + ? (await this.customEmojiService.localEmojisCache.fetch()).get(name) + : await this.emojisRepository.findOneBy({ + host: reacterHost, + name, + }); + + if (emoji) { + if (emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length === 0 || (await this.roleService.getUserRoles(user.id)).some(r => emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.includes(r.id))) { + reaction = reacterHost ? `:${name}@${reacterHost}:` : `:${name}:`; + } else { + // リアクションとして使う権限がない + reaction = FALLBACK; + } + } else { + reaction = FALLBACK; + } + } else { + reaction = this.normalize(reaction ?? null); + } } const record: NoteReaction = { @@ -291,11 +319,9 @@ export class ReactionService { } @bindThis - public async toDbReaction(reaction?: string | null, reacterHost?: string | null): Promise { + public normalize(reaction: string | null): string { if (reaction == null) return FALLBACK; - reacterHost = this.utilityService.toPunyNullable(reacterHost); - // 文字列タイプのリアクションを絵文字に変換 if (Object.keys(legacies).includes(reaction)) return legacies[reaction]; @@ -309,19 +335,6 @@ export class ReactionService { return unicode.match('\u200d') ? unicode : unicode.replace(/\ufe0f/g, ''); } - const custom = reaction.match(isCustomEmojiRegexp); - if (custom) { - const name = custom[1]; - const emoji = reacterHost == null - ? (await this.customEmojiService.localEmojisCache.fetch()).get(name) - : await this.emojisRepository.findOneBy({ - host: reacterHost, - name, - }); - - if (emoji) return reacterHost ? `:${name}@${reacterHost}:` : `:${name}:`; - } - return FALLBACK; } diff --git a/packages/backend/src/core/WebfingerService.ts b/packages/backend/src/core/WebfingerService.ts index 3ee7990643..f58a6a10fc 100644 --- a/packages/backend/src/core/WebfingerService.ts +++ b/packages/backend/src/core/WebfingerService.ts @@ -16,6 +16,9 @@ type IWebFinger = { subject: string; }; +const urlRegex = /^https?:\/\//; +const mRegex = /^([^@]+)@(.*)/; + @Injectable() export class WebfingerService { constructor( @@ -35,12 +38,12 @@ export class WebfingerService { @bindThis private genUrl(query: string): string { - if (query.match(/^https?:\/\//)) { + if (query.match(urlRegex)) { const u = new URL(query); return `${u.protocol}//${u.hostname}/.well-known/webfinger?` + urlQuery({ resource: query }); } - const m = query.match(/^([^@]+)@(.*)/); + const m = query.match(mRegex); if (m) { const hostname = m[2]; const useHttp = process.env.MISSKEY_WEBFINGER_USE_HTTP && process.env.MISSKEY_WEBFINGER_USE_HTTP.toLowerCase() === 'true'; diff --git a/packages/backend/src/core/entities/EmojiEntityService.ts b/packages/backend/src/core/entities/EmojiEntityService.ts index 3bad048bc0..0c7bd9ed9a 100644 --- a/packages/backend/src/core/entities/EmojiEntityService.ts +++ b/packages/backend/src/core/entities/EmojiEntityService.ts @@ -26,6 +26,7 @@ export class EmojiEntityService { category: emoji.category, // || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ) url: emoji.publicUrl || emoji.originalUrl, + isSensitive: emoji.isSensitive, }; } @@ -51,6 +52,9 @@ export class EmojiEntityService { // || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ) url: emoji.publicUrl || emoji.originalUrl, license: emoji.license, + isSensitive: emoji.isSensitive, + localOnly: emoji.localOnly, + roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction, }; } diff --git a/packages/backend/src/models/entities/Emoji.ts b/packages/backend/src/models/entities/Emoji.ts index dbb437d439..8fd3e65f5e 100644 --- a/packages/backend/src/models/entities/Emoji.ts +++ b/packages/backend/src/models/entities/Emoji.ts @@ -60,4 +60,20 @@ export class Emoji { length: 1024, nullable: true, }) public license: string | null; + + @Column('boolean', { + default: false, + }) + public localOnly: boolean; + + @Column('boolean', { + default: false, + }) + public isSensitive: boolean; + + // TODO: 定期ジョブで存在しなくなったロールIDを除去するようにする + @Column('varchar', { + array: true, length: 128, default: '{}', + }) + public roleIdsThatCanBeUsedThisEmojiAsReaction: string[]; } diff --git a/packages/backend/src/models/json-schema/emoji.ts b/packages/backend/src/models/json-schema/emoji.ts index db4fd62cf6..c59b5d1ef4 100644 --- a/packages/backend/src/models/json-schema/emoji.ts +++ b/packages/backend/src/models/json-schema/emoji.ts @@ -22,6 +22,10 @@ export const packedEmojiSimpleSchema = { type: 'string', optional: false, nullable: false, }, + isSensitive: { + type: 'boolean', + optional: false, nullable: false, + }, }, } as const; @@ -63,5 +67,22 @@ export const packedEmojiDetailedSchema = { type: 'string', optional: false, nullable: true, }, + isSensitive: { + type: 'boolean', + optional: false, nullable: false, + }, + localOnly: { + type: 'boolean', + optional: false, nullable: false, + }, + roleIdsThatCanBeUsedThisEmojiAsReaction: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'string', + optional: false, nullable: false, + format: 'id', + }, + }, }, } as const; diff --git a/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts b/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts index cf78d8330c..600468a286 100644 --- a/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts @@ -107,6 +107,9 @@ export class ImportCustomEmojisProcessorService { aliases: emojiInfo.aliases, driveFile, license: emojiInfo.license, + isSensitive: emojiInfo.isSensitive, + localOnly: emojiInfo.localOnly, + roleIdsThatCanBeUsedThisEmojiAsReaction: [], }); } diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts index 2fb3e489e7..509224e9c3 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts @@ -25,9 +25,24 @@ export const meta = { export const paramDef = { type: 'object', properties: { + name: { type: 'string', pattern: '^[a-zA-Z0-9_]+$' }, fileId: { type: 'string', format: 'misskey:id' }, + category: { + type: 'string', + nullable: true, + description: 'Use `null` to reset the category.', + }, + aliases: { type: 'array', items: { + type: 'string', + } }, + license: { type: 'string', nullable: true }, + isSensitive: { type: 'boolean' }, + localOnly: { type: 'boolean' }, + roleIdsThatCanBeUsedThisEmojiAsReaction: { type: 'array', items: { + type: 'string', + } }, }, - required: ['fileId'], + required: ['name', 'fileId'], } as const; // TODO: ロジックをサービスに切り出す @@ -45,18 +60,18 @@ export default class extends Endpoint { ) { super(meta, paramDef, async (ps, me) => { const driveFile = await this.driveFilesRepository.findOneBy({ id: ps.fileId }); - if (driveFile == null) throw new ApiError(meta.errors.noSuchFile); - const name = driveFile.name.split('.')[0].match(/^[a-z0-9_]+$/) ? driveFile.name.split('.')[0] : `_${rndstr('a-z0-9', 8)}_`; - const emoji = await this.customEmojiService.add({ driveFile, - name, - category: null, - aliases: [], + name: ps.name, + category: ps.category ?? null, + aliases: ps.aliases ?? [], host: null, - license: null, + license: ps.license ?? null, + isSensitive: ps.isSensitive ?? false, + localOnly: ps.localOnly ?? false, + roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction ?? [], }); this.moderationLogService.insertModerationLog(me, 'addEmoji', { diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts index f63348b60b..fb22bdc477 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts @@ -1,6 +1,8 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js'; +import type { DriveFilesRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; export const meta = { @@ -15,6 +17,11 @@ export const meta = { code: 'NO_SUCH_EMOJI', id: '684dec9d-a8c2-4364-9aa8-456c49cb1dc8', }, + noSuchFile: { + message: 'No such file.', + code: 'NO_SUCH_FILE', + id: '14fb9fd9-0731-4e2f-aeb9-f09e4740333d', + }, sameNameEmojiExists: { message: 'Emoji that have same name already exists.', code: 'SAME_NAME_EMOJI_EXISTS', @@ -28,6 +35,7 @@ export const paramDef = { properties: { id: { type: 'string', format: 'misskey:id' }, name: { type: 'string', pattern: '^[a-zA-Z0-9_]+$' }, + fileId: { type: 'string', format: 'misskey:id' }, category: { type: 'string', nullable: true, @@ -37,6 +45,11 @@ export const paramDef = { type: 'string', } }, license: { type: 'string', nullable: true }, + isSensitive: { type: 'boolean' }, + localOnly: { type: 'boolean' }, + roleIdsThatCanBeUsedThisEmojiAsReaction: { type: 'array', items: { + type: 'string', + } }, }, required: ['id', 'name', 'aliases'], } as const; @@ -45,14 +58,28 @@ export const paramDef = { @Injectable() export default class extends Endpoint { constructor( + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + private customEmojiService: CustomEmojiService, ) { super(meta, paramDef, async (ps, me) => { + let driveFile; + + if (ps.fileId) { + driveFile = await this.driveFilesRepository.findOneBy({ id: ps.fileId }); + if (driveFile == null) throw new ApiError(meta.errors.noSuchFile); + } + await this.customEmojiService.update(ps.id, { + driveFile, name: ps.name, category: ps.category ?? null, aliases: ps.aliases, license: ps.license ?? null, + isSensitive: ps.isSensitive, + localOnly: ps.localOnly, + roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction, }); }); } diff --git a/packages/backend/test/unit/ReactionService.ts b/packages/backend/test/unit/ReactionService.ts index 38db081ac0..aa68f4117d 100644 --- a/packages/backend/test/unit/ReactionService.ts +++ b/packages/backend/test/unit/ReactionService.ts @@ -15,78 +15,74 @@ describe('ReactionService', () => { reactionService = app.get(ReactionService); }); - describe('toDbReaction', () => { + describe('normalize', () => { test('絵文字リアクションはそのまま', async () => { - assert.strictEqual(await reactionService.toDbReaction('👍'), '👍'); - assert.strictEqual(await reactionService.toDbReaction('🍅'), '🍅'); + assert.strictEqual(await reactionService.normalize('👍'), '👍'); + assert.strictEqual(await reactionService.normalize('🍅'), '🍅'); }); test('既存のリアクションは絵文字化する pudding', async () => { - assert.strictEqual(await reactionService.toDbReaction('pudding'), '🍮'); + assert.strictEqual(await reactionService.normalize('pudding'), '🍮'); }); test('既存のリアクションは絵文字化する like', async () => { - assert.strictEqual(await reactionService.toDbReaction('like'), '👍'); + assert.strictEqual(await reactionService.normalize('like'), '👍'); }); test('既存のリアクションは絵文字化する love', async () => { - assert.strictEqual(await reactionService.toDbReaction('love'), '❤'); + assert.strictEqual(await reactionService.normalize('love'), '❤'); }); test('既存のリアクションは絵文字化する laugh', async () => { - assert.strictEqual(await reactionService.toDbReaction('laugh'), '😆'); + assert.strictEqual(await reactionService.normalize('laugh'), '😆'); }); test('既存のリアクションは絵文字化する hmm', async () => { - assert.strictEqual(await reactionService.toDbReaction('hmm'), '🤔'); + assert.strictEqual(await reactionService.normalize('hmm'), '🤔'); }); test('既存のリアクションは絵文字化する surprise', async () => { - assert.strictEqual(await reactionService.toDbReaction('surprise'), '😮'); + assert.strictEqual(await reactionService.normalize('surprise'), '😮'); }); test('既存のリアクションは絵文字化する congrats', async () => { - assert.strictEqual(await reactionService.toDbReaction('congrats'), '🎉'); + assert.strictEqual(await reactionService.normalize('congrats'), '🎉'); }); test('既存のリアクションは絵文字化する angry', async () => { - assert.strictEqual(await reactionService.toDbReaction('angry'), '💢'); + assert.strictEqual(await reactionService.normalize('angry'), '💢'); }); test('既存のリアクションは絵文字化する confused', async () => { - assert.strictEqual(await reactionService.toDbReaction('confused'), '😥'); + assert.strictEqual(await reactionService.normalize('confused'), '😥'); }); test('既存のリアクションは絵文字化する rip', async () => { - assert.strictEqual(await reactionService.toDbReaction('rip'), '😇'); + assert.strictEqual(await reactionService.normalize('rip'), '😇'); }); test('既存のリアクションは絵文字化する star', async () => { - assert.strictEqual(await reactionService.toDbReaction('star'), '⭐'); + assert.strictEqual(await reactionService.normalize('star'), '⭐'); }); test('異体字セレクタ除去', async () => { - assert.strictEqual(await reactionService.toDbReaction('㊗️'), '㊗'); + assert.strictEqual(await reactionService.normalize('㊗️'), '㊗'); }); test('異体字セレクタ除去 必要なし', async () => { - assert.strictEqual(await reactionService.toDbReaction('㊗'), '㊗'); - }); - - test('fallback - undefined', async () => { - assert.strictEqual(await reactionService.toDbReaction(undefined), '❤'); + assert.strictEqual(await reactionService.normalize('㊗'), '㊗'); }); test('fallback - null', async () => { - assert.strictEqual(await reactionService.toDbReaction(null), '❤'); + assert.strictEqual(await reactionService.normalize(null), '❤'); }); test('fallback - empty', async () => { - assert.strictEqual(await reactionService.toDbReaction(''), '❤'); + assert.strictEqual(await reactionService.normalize(''), '❤'); }); test('fallback - unknown', async () => { - assert.strictEqual(await reactionService.toDbReaction('unknown'), '❤'); + assert.strictEqual(await reactionService.normalize('unknown'), '❤'); }); }); }); diff --git a/packages/frontend/src/components/MkRolePreview.vue b/packages/frontend/src/components/MkRolePreview.vue index 2f5866f340..9fbe1ec993 100644 --- a/packages/frontend/src/components/MkRolePreview.vue +++ b/packages/frontend/src/components/MkRolePreview.vue @@ -12,8 +12,10 @@ {{ role.name }} - {{ role.usersCount }} users - ({{ i18n.ts._role.conditional }}) +
{{ role.description }}
@@ -23,10 +25,13 @@ import { } from 'vue'; import { i18n } from '@/i18n'; -const props = defineProps<{ +const props = withDefaults(defineProps<{ role: any; forModeration: boolean; -}>(); + detailed: boolean; +}>(), { + detailed: true, +}); -- cgit v1.2.3-freya From 3804c6e7ad8c08f87e99d7e68a7d5c0e8af68dfa Mon Sep 17 00:00:00 2001 From: syuilo Date: Fri, 19 May 2023 09:43:38 +0900 Subject: feat: センシティブなカスタム絵文字のリアクションを受け入れない設定を追加 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 2 ++ locales/ja-JP.yml | 4 +++- packages/backend/src/core/ReactionService.ts | 7 ++++++- packages/backend/src/models/entities/Note.ts | 2 +- .../backend/src/server/api/endpoints/notes/create.ts | 2 +- packages/frontend/src/components/MkPostForm.vue | 6 ++++-- packages/frontend/src/pages/settings/profile.vue | 16 +++++++++------- packages/frontend/src/store.ts | 2 +- 8 files changed, 27 insertions(+), 14 deletions(-) (limited to 'packages/backend/src/server/api/endpoints') diff --git a/CHANGELOG.md b/CHANGELOG.md index d9ce5fbe6c..03b1bb4e29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ ### General - カスタム絵文字ごとにそれをリアクションとして使えるロールを設定できるように - カスタム絵文字ごとに連合するかどうか設定できるように +- カスタム絵文字ごとにセンシティブフラグを設定できるように +- センシティブなカスタム絵文字のリアクションを受け入れない設定が可能に - タイムラインにフォロイーの行った他人へのリプライを含めるかどうかの設定をアカウントに保存するのをやめるように - 今後はAPI呼び出し時およびストリーミング接続時に設定するようになります diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 4067155a33..374eeba390 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -990,7 +990,9 @@ postToTheChannel: "チャンネルに投稿" cannotBeChangedLater: "後から変更できません。" reactionAcceptance: "リアクションの受け入れ" likeOnly: "いいねのみ" -likeOnlyForRemote: "リモートからはいいねのみ" +likeOnlyForRemote: "全て (リモートはいいねのみ)" +nonSensitiveOnly: "非センシティブのみ" +nonSensitiveOnlyForLocalLikeOnlyForRemote: "非センシティブのみ (リモートはいいねのみ)" rolesAssignedToMe: "自分に割り当てられたロール" resetPasswordConfirm: "パスワードリセットしますか?" sensitiveWords: "センシティブワード" diff --git a/packages/backend/src/core/ReactionService.ts b/packages/backend/src/core/ReactionService.ts index 27334b33e6..4b01b6af7e 100644 --- a/packages/backend/src/core/ReactionService.ts +++ b/packages/backend/src/core/ReactionService.ts @@ -106,7 +106,7 @@ export class ReactionService { let reaction = _reaction ?? FALLBACK; - if (note.reactionAcceptance === 'likeOnly' || ((note.reactionAcceptance === 'likeOnlyForRemote') && (user.host != null))) { + if (note.reactionAcceptance === 'likeOnly' || ((note.reactionAcceptance === 'likeOnlyForRemote' || note.reactionAcceptance === 'nonSensitiveOnlyForLocalLikeOnlyForRemote') && (user.host != null))) { reaction = '❤️'; } else if (_reaction) { const custom = reaction.match(isCustomEmojiRegexp); @@ -124,6 +124,11 @@ export class ReactionService { if (emoji) { if (emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length === 0 || (await this.roleService.getUserRoles(user.id)).some(r => emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.includes(r.id))) { reaction = reacterHost ? `:${name}@${reacterHost}:` : `:${name}:`; + + // センシティブ + if ((note.reactionAcceptance === 'nonSensitiveOnly') && emoji.isSensitive) { + reaction = FALLBACK; + } } else { // リアクションとして使う権限がない reaction = FALLBACK; diff --git a/packages/backend/src/models/entities/Note.ts b/packages/backend/src/models/entities/Note.ts index df508b4dca..4f49a05950 100644 --- a/packages/backend/src/models/entities/Note.ts +++ b/packages/backend/src/models/entities/Note.ts @@ -90,7 +90,7 @@ export class Note { @Column('varchar', { length: 64, nullable: true, }) - public reactionAcceptance: 'likeOnly' | 'likeOnlyForRemote' | null; + public reactionAcceptance: 'likeOnly' | 'likeOnlyForRemote' | 'nonSensitiveOnly' | 'nonSensitiveOnlyForLocalLikeOnlyForRemote' | null; @Column('smallint', { default: 0, diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index 3f7f2cdece..96be5ed844 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -99,7 +99,7 @@ export const paramDef = { } }, cw: { type: 'string', nullable: true, maxLength: 100 }, localOnly: { type: 'boolean', default: false }, - reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote'], default: null }, + reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'], default: null }, noExtractMentions: { type: 'boolean', default: false }, noExtractHashtags: { type: 'boolean', default: false }, noExtractEmojis: { type: 'boolean', default: false }, diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index c65cb7d6e5..c43d353d9c 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -31,7 +31,7 @@ -
- + @@ -88,8 +88,10 @@ - + + +
diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts index 0af4b5c021..aa4b98c05c 100644 --- a/packages/frontend/src/store.ts +++ b/packages/frontend/src/store.ts @@ -92,7 +92,7 @@ export const defaultStore = markRaw(new Storage('base', { }, reactionAcceptance: { where: 'account', - default: null as 'likeOnly' | 'likeOnlyForRemote' | null, + default: 'nonSensitiveOnly' as 'likeOnly' | 'likeOnlyForRemote' | 'nonSensitiveOnly' | 'nonSensitiveOnlyForLocalLikeOnlyForRemote' | null, }, mutedWords: { where: 'account', -- cgit v1.2.3-freya From dddbc1c894f53d6891ca7760dd9382c19931661a Mon Sep 17 00:00:00 2001 From: Chocolate Pie <106949016+chocolate-pie@users.noreply.github.com> Date: Fri, 19 May 2023 10:06:12 +0900 Subject: feat: 公開リスト (#10842) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: まず公開できるように (misskey-dev/misskey#10447) * feat: 公開したリストのページを作成 (misskey-dev/misskey#10447) * feat: いいねできるように * feat: インポートに対応 * wip * wip * CHANGELOGを編集 * add note * refactor --------- Co-authored-by: syuilo --- CHANGELOG.md | 1 + .../backend/migration/1683847157541-UserList.js | 13 ++ .../migration/1683869758873-UserListFavorites.js | 19 +++ .../src/core/entities/UserListEntityService.ts | 1 + packages/backend/src/di-symbols.ts | 1 + packages/backend/src/models/RepositoryModule.ts | 10 +- packages/backend/src/models/entities/UserList.ts | 6 + .../src/models/entities/UserListFavorite.ts | 33 +++++ packages/backend/src/models/index.ts | 3 + .../backend/src/models/json-schema/user-list.ts | 5 + packages/backend/src/postgres.ts | 2 + packages/backend/src/server/api/EndpointsModule.ts | 12 ++ packages/backend/src/server/api/endpoints.ts | 6 + .../endpoints/users/lists/create-from-public.ts | 148 +++++++++++++++++++++ .../server/api/endpoints/users/lists/favorite.ts | 70 ++++++++++ .../src/server/api/endpoints/users/lists/list.ts | 45 ++++++- .../src/server/api/endpoints/users/lists/show.ts | 35 ++++- .../server/api/endpoints/users/lists/unfavorite.ts | 63 +++++++++ .../src/server/api/endpoints/users/lists/update.ts | 5 +- .../src/components/MkReactionsViewer.reaction.vue | 2 + packages/frontend/src/pages/list.vue | 148 +++++++++++++++++++++ packages/frontend/src/pages/my-lists/index.vue | 1 + packages/frontend/src/pages/my-lists/list.vue | 90 +++++++------ packages/frontend/src/pages/user/index.vue | 6 + packages/frontend/src/pages/user/lists.vue | 51 +++++++ packages/frontend/src/router.ts | 4 + 26 files changed, 726 insertions(+), 54 deletions(-) create mode 100644 packages/backend/migration/1683847157541-UserList.js create mode 100644 packages/backend/migration/1683869758873-UserListFavorites.js create mode 100644 packages/backend/src/models/entities/UserListFavorite.ts create mode 100644 packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts create mode 100644 packages/backend/src/server/api/endpoints/users/lists/favorite.ts create mode 100644 packages/backend/src/server/api/endpoints/users/lists/unfavorite.ts create mode 100644 packages/frontend/src/pages/list.vue create mode 100644 packages/frontend/src/pages/user/lists.vue (limited to 'packages/backend/src/server/api/endpoints') diff --git a/CHANGELOG.md b/CHANGELOG.md index 9729589632..9945415380 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ - センシティブなカスタム絵文字のリアクションを受け入れない設定が可能に - タイムラインにフォロイーの行った他人へのリプライを含めるかどうかの設定をアカウントに保存するのをやめるように - 今後はAPI呼び出し時およびストリーミング接続時に設定するようになります +- リストを公開できるようになりました ### Client - リアクションの取り消し/変更時に確認ダイアログを出すように diff --git a/packages/backend/migration/1683847157541-UserList.js b/packages/backend/migration/1683847157541-UserList.js new file mode 100644 index 0000000000..b50a50eed8 --- /dev/null +++ b/packages/backend/migration/1683847157541-UserList.js @@ -0,0 +1,13 @@ +export class UserList1683847157541 { + name = 'UserList1683847157541' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_list" ADD "isPublic" boolean NOT NULL DEFAULT false`); + await queryRunner.query(`CREATE INDEX "IDX_48a00f08598662b9ca540521eb" ON "user_list" ("isPublic") `); + } + + async down(queryRunner) { + await queryRunner.query(`DROP INDEX "public"."IDX_48a00f08598662b9ca540521eb"`); + await queryRunner.query(`ALTER TABLE "user_list" DROP COLUMN "isPublic"`); + } +} diff --git a/packages/backend/migration/1683869758873-UserListFavorites.js b/packages/backend/migration/1683869758873-UserListFavorites.js new file mode 100644 index 0000000000..ac9c4c42b9 --- /dev/null +++ b/packages/backend/migration/1683869758873-UserListFavorites.js @@ -0,0 +1,19 @@ +export class UserListFavorites1683869758873 { + name = 'UserListFavorites1683869758873' + + async up(queryRunner) { + await queryRunner.query(`CREATE TABLE "user_list_favorite" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "userListId" character varying(32) NOT NULL, CONSTRAINT "PK_c0974b21e18502a4c8178e09fe6" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE INDEX "IDX_016f613dc4feb807e03e3e7da9" ON "user_list_favorite" ("userId") `); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_d6765a8c2a4c17c33f9d7f948b" ON "user_list_favorite" ("userId", "userListId") `); + await queryRunner.query(`ALTER TABLE "user_list_favorite" ADD CONSTRAINT "FK_016f613dc4feb807e03e3e7da92" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "user_list_favorite" ADD CONSTRAINT "FK_4d52b20bfe32c8552e7a61e80d2" FOREIGN KEY ("userListId") REFERENCES "user_list"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_list_favorite" DROP CONSTRAINT "FK_4d52b20bfe32c8552e7a61e80d2"`); + await queryRunner.query(`ALTER TABLE "user_list_favorite" DROP CONSTRAINT "FK_016f613dc4feb807e03e3e7da92"`); + await queryRunner.query(`DROP INDEX "public"."IDX_d6765a8c2a4c17c33f9d7f948b"`); + await queryRunner.query(`DROP INDEX "public"."IDX_016f613dc4feb807e03e3e7da9"`); + await queryRunner.query(`DROP TABLE "user_list_favorite"`); + } +} diff --git a/packages/backend/src/core/entities/UserListEntityService.ts b/packages/backend/src/core/entities/UserListEntityService.ts index 2461cb2c12..8628819278 100644 --- a/packages/backend/src/core/entities/UserListEntityService.ts +++ b/packages/backend/src/core/entities/UserListEntityService.ts @@ -35,6 +35,7 @@ export class UserListEntityService { createdAt: userList.createdAt.toISOString(), name: userList.name, userIds: users.map(x => x.userId), + isPublic: userList.isPublic, }; } } diff --git a/packages/backend/src/di-symbols.ts b/packages/backend/src/di-symbols.ts index c06c7a7159..4a073f102f 100644 --- a/packages/backend/src/di-symbols.ts +++ b/packages/backend/src/di-symbols.ts @@ -25,6 +25,7 @@ export const DI = { userSecurityKeysRepository: Symbol('userSecurityKeysRepository'), userPublickeysRepository: Symbol('userPublickeysRepository'), userListsRepository: Symbol('userListsRepository'), + userListFavoritesRepository: Symbol('userListFavoritesRepository'), userListJoiningsRepository: Symbol('userListJoiningsRepository'), userNotePiningsRepository: Symbol('userNotePiningsRepository'), userIpsRepository: Symbol('userIpsRepository'), diff --git a/packages/backend/src/models/RepositoryModule.ts b/packages/backend/src/models/RepositoryModule.ts index 588c98b58d..4231acc046 100644 --- a/packages/backend/src/models/RepositoryModule.ts +++ b/packages/backend/src/models/RepositoryModule.ts @@ -1,6 +1,6 @@ import { Module } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import { User, Note, Announcement, AnnouncementRead, App, NoteFavorite, NoteThreadMuting, NoteReaction, NoteUnread, Poll, PollVote, UserProfile, UserKeypair, UserPending, AttestationChallenge, UserSecurityKey, UserPublickey, UserList, UserListJoining, UserNotePining, UserIp, UsedUsername, Following, FollowRequest, Instance, Emoji, DriveFile, DriveFolder, Meta, Muting, RenoteMuting, Blocking, SwSubscription, Hashtag, AbuseUserReport, RegistrationTicket, AuthSession, AccessToken, Signin, Page, PageLike, GalleryPost, GalleryLike, ModerationLog, Clip, ClipNote, Antenna, PromoNote, PromoRead, Relay, MutedNote, Channel, ChannelFollowing, ChannelFavorite, RegistryItem, Webhook, Ad, PasswordResetRequest, RetentionAggregation, FlashLike, Flash, Role, RoleAssignment, ClipFavorite, UserMemo } from './index.js'; +import { User, Note, Announcement, AnnouncementRead, App, NoteFavorite, NoteThreadMuting, NoteReaction, NoteUnread, Poll, PollVote, UserProfile, UserKeypair, UserPending, AttestationChallenge, UserSecurityKey, UserPublickey, UserList, UserListJoining, UserNotePining, UserIp, UsedUsername, Following, FollowRequest, Instance, Emoji, DriveFile, DriveFolder, Meta, Muting, RenoteMuting, Blocking, SwSubscription, Hashtag, AbuseUserReport, RegistrationTicket, AuthSession, AccessToken, Signin, Page, PageLike, GalleryPost, GalleryLike, ModerationLog, Clip, ClipNote, Antenna, PromoNote, PromoRead, Relay, MutedNote, Channel, ChannelFollowing, ChannelFavorite, RegistryItem, Webhook, Ad, PasswordResetRequest, RetentionAggregation, FlashLike, Flash, Role, RoleAssignment, ClipFavorite, UserMemo, UserListFavorite } from './index.js'; import type { DataSource } from 'typeorm'; import type { Provider } from '@nestjs/common'; @@ -112,6 +112,12 @@ const $userListsRepository: Provider = { inject: [DI.db], }; +const $userListFavoritesRepository: Provider = { + provide: DI.userListFavoritesRepository, + useFactory: (db: DataSource) => db.getRepository(UserListFavorite), + inject: [DI.db], +}; + const $userListJoiningsRepository: Provider = { provide: DI.userListJoiningsRepository, useFactory: (db: DataSource) => db.getRepository(UserListJoining), @@ -416,6 +422,7 @@ const $userMemosRepository: Provider = { $userSecurityKeysRepository, $userPublickeysRepository, $userListsRepository, + $userListFavoritesRepository, $userListJoiningsRepository, $userNotePiningsRepository, $userIpsRepository, @@ -483,6 +490,7 @@ const $userMemosRepository: Provider = { $userSecurityKeysRepository, $userPublickeysRepository, $userListsRepository, + $userListFavoritesRepository, $userListJoiningsRepository, $userNotePiningsRepository, $userIpsRepository, diff --git a/packages/backend/src/models/entities/UserList.ts b/packages/backend/src/models/entities/UserList.ts index b8a4b54d4c..94f3dc3cb3 100644 --- a/packages/backend/src/models/entities/UserList.ts +++ b/packages/backend/src/models/entities/UserList.ts @@ -19,6 +19,12 @@ export class UserList { }) public userId: User['id']; + @Index() + @Column('boolean', { + default: false, + }) + public isPublic: boolean; + @ManyToOne(type => User, { onDelete: 'CASCADE', }) diff --git a/packages/backend/src/models/entities/UserListFavorite.ts b/packages/backend/src/models/entities/UserListFavorite.ts new file mode 100644 index 0000000000..e57abb460a --- /dev/null +++ b/packages/backend/src/models/entities/UserListFavorite.ts @@ -0,0 +1,33 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from '../id.js'; +import { User } from './User.js'; +import { UserList } from './UserList.js'; + +@Entity() +@Index(['userId', 'userListId'], { unique: true }) +export class UserListFavorite { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone') + public createdAt: Date; + + @Index() + @Column(id()) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: User | null; + + @Column(id()) + public userListId: UserList['id']; + + @ManyToOne(type => UserList, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public userList: UserList | null; +} diff --git a/packages/backend/src/models/index.ts b/packages/backend/src/models/index.ts index b8ba28db9b..4b230ab742 100644 --- a/packages/backend/src/models/index.ts +++ b/packages/backend/src/models/index.ts @@ -49,6 +49,7 @@ import { User } from '@/models/entities/User.js'; import { UserIp } from '@/models/entities/UserIp.js'; import { UserKeypair } from '@/models/entities/UserKeypair.js'; import { UserList } from '@/models/entities/UserList.js'; +import { UserListFavorite } from './entities/UserListFavorite.js'; import { UserListJoining } from '@/models/entities/UserListJoining.js'; import { UserNotePining } from '@/models/entities/UserNotePining.js'; import { UserPending } from '@/models/entities/UserPending.js'; @@ -117,6 +118,7 @@ export { UserIp, UserKeypair, UserList, + UserListFavorite, UserListJoining, UserNotePining, UserPending, @@ -184,6 +186,7 @@ export type UsersRepository = Repository; export type UserIpsRepository = Repository; export type UserKeypairsRepository = Repository; export type UserListsRepository = Repository; +export type UserListFavoritesRepository = Repository; export type UserListJoiningsRepository = Repository; export type UserNotePiningsRepository = Repository; export type UserPendingsRepository = Repository; diff --git a/packages/backend/src/models/json-schema/user-list.ts b/packages/backend/src/models/json-schema/user-list.ts index 3ba5dc4a8a..1e620516e4 100644 --- a/packages/backend/src/models/json-schema/user-list.ts +++ b/packages/backend/src/models/json-schema/user-list.ts @@ -25,5 +25,10 @@ export const packedUserListSchema = { format: 'id', }, }, + isPublic: { + type: 'boolean', + nullable: false, + optional: false, + }, }, } as const; diff --git a/packages/backend/src/postgres.ts b/packages/backend/src/postgres.ts index f3d404e6c9..488979c409 100644 --- a/packages/backend/src/postgres.ts +++ b/packages/backend/src/postgres.ts @@ -57,6 +57,7 @@ import { User } from '@/models/entities/User.js'; import { UserIp } from '@/models/entities/UserIp.js'; import { UserKeypair } from '@/models/entities/UserKeypair.js'; import { UserList } from '@/models/entities/UserList.js'; +import { UserListFavorite } from '@/models/entities/UserListFavorite.js'; import { UserListJoining } from '@/models/entities/UserListJoining.js'; import { UserNotePining } from '@/models/entities/UserNotePining.js'; import { UserPending } from '@/models/entities/UserPending.js'; @@ -132,6 +133,7 @@ export const entities = [ UserKeypair, UserPublickey, UserList, + UserListFavorite, UserListJoining, UserNotePining, UserSecurityKey, diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index ee1aae5b6c..1e32e9988d 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -321,6 +321,9 @@ import * as ep___users_lists_pull from './endpoints/users/lists/pull.js'; import * as ep___users_lists_push from './endpoints/users/lists/push.js'; import * as ep___users_lists_show from './endpoints/users/lists/show.js'; import * as ep___users_lists_update from './endpoints/users/lists/update.js'; +import * as ep___users_lists_favorite from './endpoints/users/lists/favorite.js'; +import * as ep___users_lists_unfavorite from './endpoints/users/lists/unfavorite.js'; +import * as ep___users_lists_create_from_public from './endpoints/users/lists/create-from-public.js'; import * as ep___users_notes from './endpoints/users/notes.js'; import * as ep___users_pages from './endpoints/users/pages.js'; import * as ep___users_reactions from './endpoints/users/reactions.js'; @@ -659,6 +662,9 @@ const $users_lists_pull: Provider = { provide: 'ep:users/lists/pull', useClass: const $users_lists_push: Provider = { provide: 'ep:users/lists/push', useClass: ep___users_lists_push.default }; const $users_lists_show: Provider = { provide: 'ep:users/lists/show', useClass: ep___users_lists_show.default }; const $users_lists_update: Provider = { provide: 'ep:users/lists/update', useClass: ep___users_lists_update.default }; +const $users_lists_favorite: Provider = { provide: 'ep:users/lists/favorite', useClass: ep___users_lists_favorite.default }; +const $users_lists_unfavorite: Provider = { provide: 'ep:users/lists/unfavorite', useClass: ep___users_lists_unfavorite.default }; +const $users_lists_create_from_public: Provider = { provide: 'ep:users/lists/create-from-public', useClass: ep___users_lists_create_from_public.default }; const $users_notes: Provider = { provide: 'ep:users/notes', useClass: ep___users_notes.default }; const $users_pages: Provider = { provide: 'ep:users/pages', useClass: ep___users_pages.default }; const $users_reactions: Provider = { provide: 'ep:users/reactions', useClass: ep___users_reactions.default }; @@ -1001,6 +1007,9 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $users_lists_push, $users_lists_show, $users_lists_update, + $users_lists_favorite, + $users_lists_unfavorite, + $users_lists_create_from_public, $users_notes, $users_pages, $users_reactions, @@ -1335,6 +1344,9 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $users_lists_push, $users_lists_show, $users_lists_update, + $users_lists_favorite, + $users_lists_unfavorite, + $users_lists_create_from_public, $users_notes, $users_pages, $users_reactions, diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 09bd7cbff4..7e678a6404 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -320,6 +320,9 @@ import * as ep___users_lists_list from './endpoints/users/lists/list.js'; import * as ep___users_lists_pull from './endpoints/users/lists/pull.js'; import * as ep___users_lists_push from './endpoints/users/lists/push.js'; import * as ep___users_lists_show from './endpoints/users/lists/show.js'; +import * as ep___users_lists_favorite from './endpoints/users/lists/favorite.js'; +import * as ep___users_lists_unfavorite from './endpoints/users/lists/unfavorite.js'; +import * as ep___users_lists_create_from_public from './endpoints/users/lists/create-from-public.js'; import * as ep___users_lists_update from './endpoints/users/lists/update.js'; import * as ep___users_notes from './endpoints/users/notes.js'; import * as ep___users_pages from './endpoints/users/pages.js'; @@ -656,7 +659,10 @@ const eps = [ ['users/lists/pull', ep___users_lists_pull], ['users/lists/push', ep___users_lists_push], ['users/lists/show', ep___users_lists_show], + ['users/lists/favorite', ep___users_lists_favorite], + ['users/lists/unfavorite', ep___users_lists_unfavorite], ['users/lists/update', ep___users_lists_update], + ['users/lists/create-from-public', ep___users_lists_create_from_public], ['users/notes', ep___users_notes], ['users/pages', ep___users_pages], ['users/reactions', ep___users_reactions], diff --git a/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts b/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts new file mode 100644 index 0000000000..8591e4ab96 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts @@ -0,0 +1,148 @@ +import { Inject, Injectable } from '@nestjs/common'; +import type { UserListsRepository, UserListJoiningsRepository, BlockingsRepository } from '@/models/index.js'; +import { IdService } from '@/core/IdService.js'; +import type { UserList } from '@/models/entities/UserList.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { GetterService } from '@/server/api/GetterService.js'; +import { UserListEntityService } from '@/core/entities/UserListEntityService.js'; +import { DI } from '@/di-symbols.js'; +import { ApiError } from '@/server/api/error.js'; +import { RoleService } from '@/core/RoleService.js'; +import { UserListService } from '@/core/UserListService.js'; + +export const meta = { + requireCredential: true, + prohibitMoved: true, + res: { + type: 'object', + optional: false, nullable: false, + ref: 'UserList', + }, + + errors: { + tooManyUserLists: { + message: 'You cannot create user list any more.', + code: 'TOO_MANY_USERLISTS', + id: 'e9c105b2-c595-47de-97fb-7f7c2c33e92f', + }, + noSuchList: { + message: 'No such list.', + code: 'NO_SUCH_LIST', + id: '9292f798-6175-4f7d-93f4-b6742279667d', + }, + noSuchUser: { + message: 'No such user.', + code: 'NO_SUCH_USER', + id: '13c457db-a8cb-4d88-b70a-211ceeeabb5f', + }, + + alreadyAdded: { + message: 'That user has already been added to that list.', + code: 'ALREADY_ADDED', + id: 'c3ad6fdb-692b-47ee-a455-7bd12c7af615', + }, + + youHaveBeenBlocked: { + message: 'You cannot push this user because you have been blocked by this user.', + code: 'YOU_HAVE_BEEN_BLOCKED', + id: 'a2497f2a-2389-439c-8626-5298540530f4', + }, + + tooManyUsers: { + message: 'You can not push users any more.', + code: 'TOO_MANY_USERS', + id: '1845ea77-38d1-426e-8e4e-8b83b24f5bd7', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + name: { type: 'string', minLength: 1, maxLength: 100 }, + listId: { type: 'string', format: 'misskey:id' }, + }, + required: ['name', 'listId'], +} as const; + +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.userListsRepository) + private userListsRepository: UserListsRepository, + + @Inject(DI.userListJoiningsRepository) + private userListJoiningsRepository: UserListJoiningsRepository, + + @Inject(DI.blockingsRepository) + private blockingsRepository: BlockingsRepository, + + private userListService: UserListService, + private userListEntityService: UserListEntityService, + private idService: IdService, + private getterService: GetterService, + private roleService: RoleService, + ) { + super(meta, paramDef, async (ps, me) => { + const list = await this.userListsRepository.findOneBy({ + id: ps.listId, + isPublic: true, + }); + if (list === null) throw new ApiError(meta.errors.noSuchList); + const currentCount = await this.userListsRepository.countBy({ + userId: me.id, + }); + if (currentCount > (await this.roleService.getUserPolicies(me.id)).userListLimit) { + throw new ApiError(meta.errors.tooManyUserLists); + } + + const userList = await this.userListsRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), + userId: me.id, + name: ps.name, + } as UserList).then(x => this.userListsRepository.findOneByOrFail(x.identifiers[0])); + + const users = (await this.userListJoiningsRepository.findBy({ + userListId: ps.listId, + })).map(x => x.userId); + + for (const user of users) { + const currentUser = await this.getterService.getUser(user).catch(err => { + if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + throw err; + }); + + if (currentUser.id !== me.id) { + const block = await this.blockingsRepository.findOneBy({ + blockerId: currentUser.id, + blockeeId: me.id, + }); + if (block) { + throw new ApiError(meta.errors.youHaveBeenBlocked); + } + } + + const exist = await this.userListJoiningsRepository.findOneBy({ + userListId: userList.id, + userId: currentUser.id, + }); + + if (exist) { + throw new ApiError(meta.errors.alreadyAdded); + } + + try { + await this.userListService.push(currentUser, userList, me); + } catch (err) { + if (err instanceof UserListService.TooManyUsersError) { + throw new ApiError(meta.errors.tooManyUsers); + } + throw err; + } + } + return await this.userListEntityService.pack(userList); + }); + } +} + diff --git a/packages/backend/src/server/api/endpoints/users/lists/favorite.ts b/packages/backend/src/server/api/endpoints/users/lists/favorite.ts new file mode 100644 index 0000000000..263852fde1 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/users/lists/favorite.ts @@ -0,0 +1,70 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { UserListFavoritesRepository, UserListsRepository } from '@/models/index.js'; +import { IdService } from '@/core/IdService.js'; +import { ApiError } from '@/server/api/error.js'; +import { DI } from '@/di-symbols.js'; + +export const meta = { + requireCredential: true, + errors: { + noSuchList: { + message: 'No such user list.', + code: 'NO_SUCH_USER_LIST', + id: '7dbaf3cf-7b42-4b8f-b431-b3919e580dbe', + }, + + alreadyFavorited: { + message: 'The list has already been favorited.', + code: 'ALREADY_FAVORITED', + id: '6425bba0-985b-461e-af1b-518070e72081', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + listId: { type: 'string', format: 'misskey:id' }, + }, + required: ['listId'], +} as const; + +@Injectable() // eslint-disable-next-line import/no-default-export +export default class extends Endpoint { + constructor ( + @Inject(DI.userListsRepository) + private userListsRepository: UserListsRepository, + + @Inject(DI.userListFavoritesRepository) + private userListFavoritesRepository: UserListFavoritesRepository, + private idService: IdService, + ) { + super(meta, paramDef, async (ps, me) => { + const userList = await this.userListsRepository.findOneBy({ + id: ps.listId, + isPublic: true, + }); + + if (userList === null) { + throw new ApiError(meta.errors.noSuchList); + } + + const exist = await this.userListFavoritesRepository.findOneBy({ + userId: me.id, + userListId: ps.listId, + }); + + if (exist !== null) { + throw new ApiError(meta.errors.alreadyFavorited); + } + + await this.userListFavoritesRepository.insert({ + id: this.idService.genId(), + createdAt: new Date(), + userId: me.id, + userListId: ps.listId, + }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/users/lists/list.ts b/packages/backend/src/server/api/endpoints/users/lists/list.ts index 2104c4377d..eab29944b2 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/list.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/list.ts @@ -1,13 +1,14 @@ import { Inject, Injectable } from '@nestjs/common'; -import type { UserListsRepository } from '@/models/index.js'; +import type { UserListsRepository, UsersRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserListEntityService } from '@/core/entities/UserListEntityService.js'; +import { ApiError } from '@/server/api/error.js'; import { DI } from '@/di-symbols.js'; export const meta = { tags: ['lists', 'account'], - requireCredential: true, + requireCredential: false, kind: 'read:account', @@ -22,26 +23,58 @@ export const meta = { ref: 'UserList', }, }, + errors: { + noSuchUser: { + message: 'No such user.', + code: 'NO_SUCH_USER', + id: 'a8af4a82-0980-4cc4-a6af-8b0ffd54465e', + }, + remoteUser: { + message: 'Not allowed to load the remote user\'s list', + code: 'REMOTE_USER_NOT_ALLOWED', + id: '53858f1b-3315-4a01-81b7-db9b48d4b79a', + }, + invalidParam: { + message: 'Invalid param.', + code: 'INVALID_PARAM', + id: 'ab36de0e-29e9-48cb-9732-d82f1281620d', + }, + }, } as const; export const paramDef = { type: 'object', - properties: {}, + properties: { + userId: { type: 'string', format: 'misskey:id' }, + }, required: [], } as const; -// eslint-disable-next-line import/no-default-export -@Injectable() +@Injectable() // eslint-disable-next-line import/no-default-export export default class extends Endpoint { constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + @Inject(DI.userListsRepository) private userListsRepository: UserListsRepository, private userListEntityService: UserListEntityService, ) { super(meta, paramDef, async (ps, me) => { - const userLists = await this.userListsRepository.findBy({ + if (typeof ps.userId !== 'undefined') { + const user = await this.usersRepository.findOneBy({ id: ps.userId }); + if (user === null) throw new ApiError(meta.errors.noSuchUser); + if (user.host !== null) throw new ApiError(meta.errors.remoteUser); + } else if (me === null) { + throw new ApiError(meta.errors.invalidParam); + } + + const userLists = await this.userListsRepository.findBy(typeof ps.userId === 'undefined' && me !== null ? { userId: me.id, + } : { + userId: ps.userId, + isPublic: true, }); return await Promise.all(userLists.map(x => this.userListEntityService.pack(x))); diff --git a/packages/backend/src/server/api/endpoints/users/lists/show.ts b/packages/backend/src/server/api/endpoints/users/lists/show.ts index 77f9cba808..8077841c8c 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/show.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/show.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import type { UserListsRepository } from '@/models/index.js'; +import type { UserListsRepository, UserListFavoritesRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserListEntityService } from '@/core/entities/UserListEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -8,7 +8,7 @@ import { ApiError } from '../../../error.js'; export const meta = { tags: ['lists', 'account'], - requireCredential: true, + requireCredential: false, kind: 'read:account', @@ -33,31 +33,54 @@ export const paramDef = { type: 'object', properties: { listId: { type: 'string', format: 'misskey:id' }, + forPublic: { type: 'boolean', default: false }, }, required: ['listId'], } as const; -// eslint-disable-next-line import/no-default-export -@Injectable() +@Injectable() // eslint-disable-next-line import/no-default-export export default class extends Endpoint { constructor( @Inject(DI.userListsRepository) private userListsRepository: UserListsRepository, + @Inject(DI.userListFavoritesRepository) + private userListFavoritesRepository: UserListFavoritesRepository, + private userListEntityService: UserListEntityService, ) { super(meta, paramDef, async (ps, me) => { + const additionalProperties: Partial<{ likedCount: number, isLiked: boolean }> = {}; // Fetch the list - const userList = await this.userListsRepository.findOneBy({ + const userList = await this.userListsRepository.findOneBy(!ps.forPublic && me !== null ? { id: ps.listId, userId: me.id, + } : { + id: ps.listId, + isPublic: true, }); if (userList == null) { throw new ApiError(meta.errors.noSuchList); } - return await this.userListEntityService.pack(userList); + if (ps.forPublic && userList.isPublic) { + additionalProperties.likedCount = await this.userListFavoritesRepository.countBy({ + userListId: ps.listId, + }); + if (me !== null) { + additionalProperties.isLiked = (await this.userListFavoritesRepository.findOneBy({ + userId: me.id, + userListId: ps.listId, + }) !== null); + } else { + additionalProperties.isLiked = false; + } + } + return { + ...await this.userListEntityService.pack(userList), + ...additionalProperties, + }; }); } } diff --git a/packages/backend/src/server/api/endpoints/users/lists/unfavorite.ts b/packages/backend/src/server/api/endpoints/users/lists/unfavorite.ts new file mode 100644 index 0000000000..be8e317816 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/users/lists/unfavorite.ts @@ -0,0 +1,63 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { UserListFavoritesRepository, UserListsRepository } from '@/models/index.js'; +import { ApiError } from '@/server/api/error.js'; +import { DI } from '@/di-symbols.js'; + +export const meta = { + requireCredential: true, + errors: { + noSuchList: { + message: 'No such user list.', + code: 'NO_SUCH_USER_LIST', + id: 'baedb33e-76b8-4b0c-86a8-9375c0a7b94b', + }, + + notFavorited: { + message: 'You have not favorited the list.', + code: 'ALREADY_FAVORITED', + id: '835c4b27-463d-4cfa-969b-a9058678d465', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + listId: { type: 'string', format: 'misskey:id' }, + }, + required: ['listId'], +} as const; + +@Injectable() // eslint-disable-next-line import/no-default-export +export default class extends Endpoint { + constructor ( + @Inject(DI.userListsRepository) + private userListsRepository: UserListsRepository, + + @Inject(DI.userListFavoritesRepository) + private userListFavoritesRepository: UserListFavoritesRepository, + ) { + super(meta, paramDef, async (ps, me) => { + const userList = await this.userListsRepository.findOneBy({ + id: ps.listId, + isPublic: true, + }); + + if (userList === null) { + throw new ApiError(meta.errors.noSuchList); + } + + const exist = await this.userListFavoritesRepository.findOneBy({ + userListId: ps.listId, + userId: me.id, + }); + + if (exist === null) { + throw new ApiError(meta.errors.notFavorited); + } + + await this.userListFavoritesRepository.delete({ id: exist.id }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/users/lists/update.ts b/packages/backend/src/server/api/endpoints/users/lists/update.ts index 6453d7d980..b0a95a2f28 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/update.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/update.ts @@ -34,8 +34,9 @@ export const paramDef = { properties: { listId: { type: 'string', format: 'misskey:id' }, name: { type: 'string', minLength: 1, maxLength: 100 }, + isPublic: { type: 'boolean' }, }, - required: ['listId', 'name'], + required: ['listId'], } as const; // eslint-disable-next-line import/no-default-export @@ -48,7 +49,6 @@ export default class extends Endpoint { private userListEntityService: UserListEntityService, ) { super(meta, paramDef, async (ps, me) => { - // Fetch the list const userList = await this.userListsRepository.findOneBy({ id: ps.listId, userId: me.id, @@ -60,6 +60,7 @@ export default class extends Endpoint { await this.userListsRepository.update(userList.id, { name: ps.name, + isPublic: ps.isPublic, }); return await this.userListEntityService.pack(userList.id); diff --git a/packages/frontend/src/components/MkReactionsViewer.reaction.vue b/packages/frontend/src/components/MkReactionsViewer.reaction.vue index b521171b2a..aabebb3abf 100644 --- a/packages/frontend/src/components/MkReactionsViewer.reaction.vue +++ b/packages/frontend/src/components/MkReactionsViewer.reaction.vue @@ -38,6 +38,8 @@ const canToggle = computed(() => !props.reaction.match(/@\w/) && $i); async function toggleReaction() { if (!canToggle.value) return; + // TODO: その絵文字を使う権限があるかどうか確認 + const oldReaction = props.note.myReaction; if (oldReaction) { const confirm = await os.confirm({ diff --git a/packages/frontend/src/pages/list.vue b/packages/frontend/src/pages/list.vue new file mode 100644 index 0000000000..a09ff854ca --- /dev/null +++ b/packages/frontend/src/pages/list.vue @@ -0,0 +1,148 @@ + + + + diff --git a/packages/frontend/src/pages/my-lists/index.vue b/packages/frontend/src/pages/my-lists/index.vue index 47437f3e57..6068e375ea 100644 --- a/packages/frontend/src/pages/my-lists/index.vue +++ b/packages/frontend/src/pages/my-lists/index.vue @@ -70,6 +70,7 @@ definePageMetadata({ padding: 16px; border: solid 1px var(--divider); border-radius: 6px; + margin-bottom: 8px; &:hover { border: solid 1px var(--accent); diff --git a/packages/frontend/src/pages/my-lists/list.vue b/packages/frontend/src/pages/my-lists/list.vue index 86201e8e0c..dd431e8dc0 100644 --- a/packages/frontend/src/pages/my-lists/list.vue +++ b/packages/frontend/src/pages/my-lists/list.vue @@ -1,35 +1,43 @@ + + diff --git a/packages/frontend/src/router.ts b/packages/frontend/src/router.ts index add4bd9217..6b11137d79 100644 --- a/packages/frontend/src/router.ts +++ b/packages/frontend/src/router.ts @@ -30,6 +30,10 @@ export const routes = [{ name: 'note', path: '/notes/:noteId', component: page(() => import('./pages/note.vue')), +}, { + name: 'list', + path: '/list/:listId', + component: page(() => import('./pages/list.vue')), }, { path: '/clips/:clipId', component: page(() => import('./pages/clip.vue')), -- cgit v1.2.3-freya From bd6666173a266dbcc10221a049f518443ed8f25b Mon Sep 17 00:00:00 2001 From: nenohi Date: Fri, 19 May 2023 17:12:22 +0900 Subject: fix:ロールタイムライン無効に関する修正 (#10843) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 修正 * note visibility change * public投稿のみ * update changelog * RN非表示機能がうごかないところだった --- CHANGELOG.md | 2 ++ packages/backend/src/core/RoleService.ts | 8 ++++++++ packages/backend/src/server/api/endpoints/roles/notes.ts | 1 + .../backend/src/server/api/stream/channels/role-timeline.ts | 11 ++++++++++- packages/frontend/src/ui/deck/role-timeline-column.vue | 2 +- 5 files changed, 22 insertions(+), 2 deletions(-) (limited to 'packages/backend/src/server/api/endpoints') diff --git a/CHANGELOG.md b/CHANGELOG.md index 454974ca23..4b462783de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,8 @@ - AiScriptを0.13.3に更新 - Fix: URLプレビューで情報が取得できなかった際の挙動を修正 - Fix: Safari、Firefoxでの新規登録時、パスワードマネージャーにメールアドレスが登録されていた挙動を修正 +- fix:ロールタイムラインが無効でも投稿が流れてしまう問題の修正 +- fix:ロールタイムラインにて全ての投稿が流れてしまう問題の修正 ## 13.12.2 diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index 68087ccc3b..130ec5ec8c 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -306,6 +306,14 @@ export class RoleService implements OnApplicationShutdown { return user.isRoot || (await this.getUserRoles(user.id)).some(r => r.isAdministrator); } + @bindThis + public async isExplorable(role: { id: Role['id']} | null): Promise { + if (role == null) return false; + const check = await this.rolesRepository.findOneBy({ id: role.id }); + if (check == null) return false; + return check.isExplorable; + } + @bindThis public async getModeratorIds(includeAdmins = true): Promise { const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({})); diff --git a/packages/backend/src/server/api/endpoints/roles/notes.ts b/packages/backend/src/server/api/endpoints/roles/notes.ts index 6202c740f1..42e36cb04a 100644 --- a/packages/backend/src/server/api/endpoints/roles/notes.ts +++ b/packages/backend/src/server/api/endpoints/roles/notes.ts @@ -93,6 +93,7 @@ export default class extends Endpoint { const query = this.notesRepository.createQueryBuilder('note') .where('note.id IN (:...noteIds)', { noteIds: noteIds }) + .andWhere('(note.visibility = \'public\')') .innerJoinAndSelect('note.user', 'user') .leftJoinAndSelect('note.reply', 'reply') .leftJoinAndSelect('note.renote', 'renote') diff --git a/packages/backend/src/server/api/stream/channels/role-timeline.ts b/packages/backend/src/server/api/stream/channels/role-timeline.ts index 9d106c8b2f..ab9c1aa0b5 100644 --- a/packages/backend/src/server/api/stream/channels/role-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/role-timeline.ts @@ -5,15 +5,17 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; import Channel from '../channel.js'; import { StreamMessages } from '../types.js'; +import { RoleService } from '@/core/RoleService.js'; class RoleTimelineChannel extends Channel { public readonly chName = 'roleTimeline'; public static shouldShare = false; public static requireCredential = false; private roleId: string; - + constructor( private noteEntityService: NoteEntityService, + private roleservice: RoleService, id: string, connection: Channel['connection'], @@ -34,6 +36,11 @@ class RoleTimelineChannel extends Channel { if (data.type === 'note') { const note = data.body; + if (!(await this.roleservice.isExplorable({ id: this.roleId }))) { + return; + } + if (note.visibility !== 'public') return; + // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する if (isUserRelated(note, this.userIdsWhoMeMuting)) return; // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する @@ -61,6 +68,7 @@ export class RoleTimelineChannelService { constructor( private noteEntityService: NoteEntityService, + private roleservice: RoleService, ) { } @@ -68,6 +76,7 @@ export class RoleTimelineChannelService { public create(id: string, connection: Channel['connection']): RoleTimelineChannel { return new RoleTimelineChannel( this.noteEntityService, + this.roleservice, id, connection, ); diff --git a/packages/frontend/src/ui/deck/role-timeline-column.vue b/packages/frontend/src/ui/deck/role-timeline-column.vue index 5783b3f071..e375a5884f 100644 --- a/packages/frontend/src/ui/deck/role-timeline-column.vue +++ b/packages/frontend/src/ui/deck/role-timeline-column.vue @@ -35,7 +35,7 @@ onMounted(() => { }); async function setRole() { - const roles = await os.api('roles/list'); + const roles = (await os.api('roles/list')).filter(x => x.isExplorable); const { canceled, result: role } = await os.select({ title: i18n.ts.role, items: roles.map(x => ({ -- cgit v1.2.3-freya From 98aef974df0bc563ac2aaf47bee996194d5ec200 Mon Sep 17 00:00:00 2001 From: Chocolate Pie <106949016+chocolate-pie@users.noreply.github.com> Date: Fri, 26 May 2023 14:47:31 +0900 Subject: enhance: ハッシュタグのノート一覧ページから、そのハッシュタグで投稿するボタンを追加、お知らせの画像URLを空にできない問題を修正 (#10878) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: お知らせの画像URLを空にできない問題を修正 (misskey-dev/misskey#10657) * ハッシュタグのノート一覧ページからノートできるように(misskey-dev/misskey#10854) * fix: 色々直した * location.reloadを使わないように * CHANGELOGを編集 * tweak * Update tag.vue --------- Co-authored-by: syuilo --- CHANGELOG.md | 4 +++ locales/index.d.ts | 1 + locales/ja-JP.yml | 1 + .../api/endpoints/admin/announcements/update.ts | 5 +-- packages/frontend/src/pages/tag.vue | 39 ++++++++++++++++++++-- 5 files changed, 46 insertions(+), 4 deletions(-) (limited to 'packages/backend/src/server/api/endpoints') diff --git a/CHANGELOG.md b/CHANGELOG.md index 829d3e2079..ecbad51e90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ - AiScriptを0.13.3に更新 - Deck UIを使用している場合、`/`以外にアクセスした際にZen UIで表示するように - メインカラムを設置していない場合の問題を解決 +- ハッシュタグのノート一覧ページから、そのハッシュタグで投稿するボタンを追加 - アカウント初期設定ウィザードに戻るボタンを追加 - アカウントの初期設定ウィザードにあとでボタンを追加 - Fix: URLプレビューで情報が取得できなかった際の挙動を修正 @@ -36,6 +37,9 @@ - fix:ロールタイムラインが無効でも投稿が流れてしまう問題の修正 - fix:ロールタイムラインにて全ての投稿が流れてしまう問題の修正 +### Server +- Fix: お知らせの画像URLを空にできない問題を修正 + ## 13.12.2 ## NOTE diff --git a/locales/index.d.ts b/locales/index.d.ts index 79f0b46d4d..a54268676e 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -795,6 +795,7 @@ export interface Locale { "noBotProtectionWarning": string; "configure": string; "postToGallery": string; + "postToHashtag": string; "gallery": string; "recentPosts": string; "popularPosts": string; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 56e2295f10..0ca37caa58 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -792,6 +792,7 @@ noMaintainerInformationWarning: "管理者情報が設定されていません noBotProtectionWarning: "Botプロテクションが設定されていません。" configure: "設定する" postToGallery: "ギャラリーへ投稿" +postToHashtag: "このハッシュタグで投稿" gallery: "ギャラリー" recentPosts: "最近の投稿" popularPosts: "人気の投稿" diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/update.ts b/packages/backend/src/server/api/endpoints/admin/announcements/update.ts index 2393c2441c..12db1f78fb 100644 --- a/packages/backend/src/server/api/endpoints/admin/announcements/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/announcements/update.ts @@ -25,7 +25,7 @@ export const paramDef = { id: { type: 'string', format: 'misskey:id' }, title: { type: 'string', minLength: 1 }, text: { type: 'string', minLength: 1 }, - imageUrl: { type: 'string', nullable: true, minLength: 1 }, + imageUrl: { type: 'string', nullable: true, minLength: 0 }, }, required: ['id', 'title', 'text', 'imageUrl'], } as const; @@ -46,7 +46,8 @@ export default class extends Endpoint { updatedAt: new Date(), title: ps.title, text: ps.text, - imageUrl: ps.imageUrl, + /* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- 空の文字列の場合、nullを渡すようにするため */ + imageUrl: ps.imageUrl || null, }); }); } diff --git a/packages/frontend/src/pages/tag.vue b/packages/frontend/src/pages/tag.vue index b53db3f67b..104e738866 100644 --- a/packages/frontend/src/pages/tag.vue +++ b/packages/frontend/src/pages/tag.vue @@ -2,15 +2,27 @@ - + + + + -- cgit v1.2.3-freya From fd7b77c542b51313d8b8ea60124725fe65a295d5 Mon Sep 17 00:00:00 2001 From: syuilo Date: Mon, 29 May 2023 11:54:49 +0900 Subject: enhance(backend): migrate bull to bullmq (#10910) * wip * wip * Update QueueService.ts * wip * refactor * :v: * fix * Update QueueStatsService.ts * refactor * Update ApNoteService.ts * Update mock-resolver.ts * refactor * Update mock-resolver.ts --- CHANGELOG.md | 1 + packages/backend/package.json | 3 +- packages/backend/src/config.ts | 2 +- packages/backend/src/core/CaptchaService.ts | 20 +- .../src/core/FetchInstanceMetadataService.ts | 6 +- packages/backend/src/core/NoteCreateService.ts | 2 +- packages/backend/src/core/QueueModule.ts | 53 +--- packages/backend/src/core/QueueService.ts | 65 +++-- .../src/core/activitypub/LdSignatureService.ts | 4 +- .../src/core/activitypub/models/ApNoteService.ts | 6 +- packages/backend/src/daemons/QueueStatsService.ts | 16 +- packages/backend/src/misc/id/aid.ts | 2 +- packages/backend/src/misc/prelude/time.ts | 19 +- .../backend/src/queue/QueueProcessorService.ts | 306 +++++++++++++-------- packages/backend/src/queue/const.ts | 26 ++ packages/backend/src/queue/get-job-info.ts | 15 - .../AggregateRetentionProcessorService.ts | 6 +- .../CheckExpiredMutingsProcessorService.ts | 5 +- .../processors/CleanChartsProcessorService.ts | 5 +- .../src/queue/processors/CleanProcessorService.ts | 5 +- .../processors/CleanRemoteFilesProcessorService.ts | 11 +- .../processors/DeleteAccountProcessorService.ts | 4 +- .../processors/DeleteDriveFilesProcessorService.ts | 12 +- .../queue/processors/DeleteFileProcessorService.ts | 4 +- .../queue/processors/DeliverProcessorService.ts | 10 +- .../EndedPollNotificationProcessorService.ts | 7 +- .../processors/ExportAntennasProcessorService.ts | 6 +- .../processors/ExportBlockingProcessorService.ts | 13 +- .../ExportCustomEmojisProcessorService.ts | 39 +-- .../processors/ExportFavoritesProcessorService.ts | 11 +- .../processors/ExportFollowingProcessorService.ts | 9 +- .../processors/ExportMutingProcessorService.ts | 13 +- .../processors/ExportNotesProcessorService.ts | 11 +- .../processors/ExportUserListsProcessorService.ts | 9 +- .../processors/ImportAntennasProcessorService.ts | 6 +- .../processors/ImportBlockingProcessorService.ts | 13 +- .../ImportCustomEmojisProcessorService.ts | 6 +- .../processors/ImportFollowingProcessorService.ts | 13 +- .../processors/ImportMutingProcessorService.ts | 11 +- .../processors/ImportUserListsProcessorService.ts | 7 +- .../src/queue/processors/InboxProcessorService.ts | 38 +-- .../processors/RelationshipProcessorService.ts | 2 +- .../processors/ResyncChartsProcessorService.ts | 5 +- .../queue/processors/TickChartsProcessorService.ts | 5 +- .../processors/WebhookDeliverProcessorService.ts | 6 +- .../src/server/api/endpoints/admin/relays/add.ts | 2 +- .../server/api/endpoints/notes/search-by-tag.ts | 4 +- .../backend/src/server/api/endpoints/reset-db.ts | 2 +- packages/backend/test/misc/mock-resolver.ts | 6 +- packages/backend/test/utils.ts | 2 +- packages/frontend/src/scripts/time.ts | 19 +- packages/shared/.eslintrc.js | 2 +- pnpm-lock.yaml | 121 ++++---- 53 files changed, 519 insertions(+), 477 deletions(-) create mode 100644 packages/backend/src/queue/const.ts delete mode 100644 packages/backend/src/queue/get-job-info.ts (limited to 'packages/backend/src/server/api/endpoints') diff --git a/CHANGELOG.md b/CHANGELOG.md index ecbad51e90..6b107c72f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ - fix:ロールタイムラインにて全ての投稿が流れてしまう問題の修正 ### Server +- bullをbull-mqにアップグレードし、ジョブキューのパフォーマンスを改善 - Fix: お知らせの画像URLを空にできない問題を修正 ## 13.12.2 diff --git a/packages/backend/package.json b/packages/backend/package.json index 71da3bb557..99c04d6bf6 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -76,7 +76,7 @@ "autwh": "0.1.0", "bcryptjs": "2.4.3", "blurhash": "2.0.5", - "bull": "4.10.4", + "bullmq": "3.14.1", "cacheable-lookup": "6.1.0", "cbor": "8.1.0", "chalk": "5.2.0", @@ -167,7 +167,6 @@ "@types/accepts": "1.3.5", "@types/archiver": "5.3.2", "@types/bcryptjs": "2.4.2", - "@types/bull": "4.10.0", "@types/cbor": "6.0.0", "@types/color-convert": "2.0.0", "@types/content-disposition": "0.5.5", diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index 744f999596..9d1945e4d4 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -190,6 +190,6 @@ function tryCreateUrl(url: string) { try { return new URL(url); } catch (e) { - throw `url="${url}" is not a valid URL.`; + throw new Error(`url="${url}" is not a valid URL.`); } } diff --git a/packages/backend/src/core/CaptchaService.ts b/packages/backend/src/core/CaptchaService.ts index 7aaa1b833f..1a52a229c5 100644 --- a/packages/backend/src/core/CaptchaService.ts +++ b/packages/backend/src/core/CaptchaService.ts @@ -30,7 +30,7 @@ export class CaptchaService { }, { throwErrorWhenResponseNotOk: false }); if (!res.ok) { - throw `${res.status}`; + throw new Error(`${res.status}`); } return await res.json() as CaptchaResponse; @@ -39,48 +39,48 @@ export class CaptchaService { @bindThis public async verifyRecaptcha(secret: string, response: string | null | undefined): Promise { if (response == null) { - throw 'recaptcha-failed: no response provided'; + throw new Error('recaptcha-failed: no response provided'); } const result = await this.getCaptchaResponse('https://www.recaptcha.net/recaptcha/api/siteverify', secret, response).catch(err => { - throw `recaptcha-request-failed: ${err}`; + throw new Error(`recaptcha-request-failed: ${err}`); }); if (result.success !== true) { const errorCodes = result['error-codes'] ? result['error-codes'].join(', ') : ''; - throw `recaptcha-failed: ${errorCodes}`; + throw new Error(`recaptcha-failed: ${errorCodes}`); } } @bindThis public async verifyHcaptcha(secret: string, response: string | null | undefined): Promise { if (response == null) { - throw 'hcaptcha-failed: no response provided'; + throw new Error('hcaptcha-failed: no response provided'); } const result = await this.getCaptchaResponse('https://hcaptcha.com/siteverify', secret, response).catch(err => { - throw `hcaptcha-request-failed: ${err}`; + throw new Error(`hcaptcha-request-failed: ${err}`); }); if (result.success !== true) { const errorCodes = result['error-codes'] ? result['error-codes'].join(', ') : ''; - throw `hcaptcha-failed: ${errorCodes}`; + throw new Error(`hcaptcha-failed: ${errorCodes}`); } } @bindThis public async verifyTurnstile(secret: string, response: string | null | undefined): Promise { if (response == null) { - throw 'turnstile-failed: no response provided'; + throw new Error('turnstile-failed: no response provided'); } const result = await this.getCaptchaResponse('https://challenges.cloudflare.com/turnstile/v0/siteverify', secret, response).catch(err => { - throw `turnstile-request-failed: ${err}`; + throw new Error(`turnstile-request-failed: ${err}`); }); if (result.success !== true) { const errorCodes = result['error-codes'] ? result['error-codes'].join(', ') : ''; - throw `turnstile-failed: ${errorCodes}`; + throw new Error(`turnstile-failed: ${errorCodes}`); } } } diff --git a/packages/backend/src/core/FetchInstanceMetadataService.ts b/packages/backend/src/core/FetchInstanceMetadataService.ts index 8103d5afe9..9de633350b 100644 --- a/packages/backend/src/core/FetchInstanceMetadataService.ts +++ b/packages/backend/src/core/FetchInstanceMetadataService.ts @@ -116,14 +116,14 @@ export class FetchInstanceMetadataService { const wellknown = await this.httpRequestService.getJson('https://' + instance.host + '/.well-known/nodeinfo') .catch(err => { if (err.statusCode === 404) { - throw 'No nodeinfo provided'; + throw new Error('No nodeinfo provided'); } else { throw err.statusCode ?? err.message; } }) as Record; if (wellknown.links == null || !Array.isArray(wellknown.links)) { - throw 'No wellknown links'; + throw new Error('No wellknown links'); } const links = wellknown.links as any[]; @@ -134,7 +134,7 @@ export class FetchInstanceMetadataService { const link = lnik2_1 ?? lnik2_0 ?? lnik1_0; if (link == null) { - throw 'No nodeinfo link provided'; + throw new Error('No nodeinfo link provided'); } const info = await this.httpRequestService.getJson(link.href) diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 977c9052c0..2fd7a8ac86 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -510,7 +510,7 @@ export class NoteCreateService implements OnApplicationShutdown { if (data.poll && data.poll.expiresAt) { const delay = data.poll.expiresAt.getTime() - Date.now(); - this.queueService.endedPollNotificationQueue.add({ + this.queueService.endedPollNotificationQueue.add(note.id, { noteId: note.id, }, { delay, diff --git a/packages/backend/src/core/QueueModule.ts b/packages/backend/src/core/QueueModule.ts index 1d73947776..6db9bb14cf 100644 --- a/packages/backend/src/core/QueueModule.ts +++ b/packages/backend/src/core/QueueModule.ts @@ -1,42 +1,11 @@ import { setTimeout } from 'node:timers/promises'; import { Inject, Module, OnApplicationShutdown } from '@nestjs/common'; -import Bull from 'bull'; +import * as Bull from 'bullmq'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; +import { QUEUE, baseQueueOptions } from '@/queue/const.js'; import type { Provider } from '@nestjs/common'; -import type { DeliverJobData, InboxJobData, DbJobData, ObjectStorageJobData, EndedPollNotificationJobData, WebhookDeliverJobData, RelationshipJobData, DbJobMap } from '../queue/types.js'; - -function q(config: Config, name: string, limitPerSec = -1) { - return new Bull(name, { - redis: { - port: config.redisForJobQueue.port, - host: config.redisForJobQueue.host, - family: config.redisForJobQueue.family == null ? 0 : config.redisForJobQueue.family, - password: config.redisForJobQueue.pass, - db: config.redisForJobQueue.db ?? 0, - }, - prefix: config.redisForJobQueue.prefix ? `${config.redisForJobQueue.prefix}:queue` : 'queue', - limiter: limitPerSec > 0 ? { - max: limitPerSec, - duration: 1000, - } : undefined, - settings: { - backoffStrategies: { - apBackoff, - }, - }, - }); -} - -// ref. https://github.com/misskey-dev/misskey/pull/7635#issue-971097019 -function apBackoff(attemptsMade: number, err: Error) { - const baseDelay = 60 * 1000; // 1min - const maxBackoff = 8 * 60 * 60 * 1000; // 8hours - let backoff = (Math.pow(2, attemptsMade) - 1) * baseDelay; - backoff = Math.min(backoff, maxBackoff); - backoff += Math.round(backoff * Math.random() * 0.2); - return backoff; -} +import type { DeliverJobData, InboxJobData, EndedPollNotificationJobData, WebhookDeliverJobData, RelationshipJobData } from '../queue/types.js'; export type SystemQueue = Bull.Queue>; export type EndedPollNotificationQueue = Bull.Queue; @@ -49,49 +18,49 @@ export type WebhookDeliverQueue = Bull.Queue; const $system: Provider = { provide: 'queue:system', - useFactory: (config: Config) => q(config, 'system'), + useFactory: (config: Config) => new Bull.Queue(QUEUE.SYSTEM, baseQueueOptions(config, QUEUE.SYSTEM)), inject: [DI.config], }; const $endedPollNotification: Provider = { provide: 'queue:endedPollNotification', - useFactory: (config: Config) => q(config, 'endedPollNotification'), + useFactory: (config: Config) => new Bull.Queue(QUEUE.ENDED_POLL_NOTIFICATION, baseQueueOptions(config, QUEUE.ENDED_POLL_NOTIFICATION)), inject: [DI.config], }; const $deliver: Provider = { provide: 'queue:deliver', - useFactory: (config: Config) => q(config, 'deliver', config.deliverJobPerSec ?? 128), + useFactory: (config: Config) => new Bull.Queue(QUEUE.DELIVER, baseQueueOptions(config, QUEUE.DELIVER)), inject: [DI.config], }; const $inbox: Provider = { provide: 'queue:inbox', - useFactory: (config: Config) => q(config, 'inbox', config.inboxJobPerSec ?? 16), + useFactory: (config: Config) => new Bull.Queue(QUEUE.INBOX, baseQueueOptions(config, QUEUE.INBOX)), inject: [DI.config], }; const $db: Provider = { provide: 'queue:db', - useFactory: (config: Config) => q(config, 'db'), + useFactory: (config: Config) => new Bull.Queue(QUEUE.DB, baseQueueOptions(config, QUEUE.DB)), inject: [DI.config], }; const $relationship: Provider = { provide: 'queue:relationship', - useFactory: (config: Config) => q(config, 'relationship', config.relashionshipJobPerSec ?? 64), + useFactory: (config: Config) => new Bull.Queue(QUEUE.RELATIONSHIP, baseQueueOptions(config, QUEUE.RELATIONSHIP)), inject: [DI.config], }; const $objectStorage: Provider = { provide: 'queue:objectStorage', - useFactory: (config: Config) => q(config, 'objectStorage'), + useFactory: (config: Config) => new Bull.Queue(QUEUE.OBJECT_STORAGE, baseQueueOptions(config, QUEUE.OBJECT_STORAGE)), inject: [DI.config], }; const $webhookDeliver: Provider = { provide: 'queue:webhookDeliver', - useFactory: (config: Config) => q(config, 'webhookDeliver', 64), + useFactory: (config: Config) => new Bull.Queue(QUEUE.WEBHOOK_DELIVER, baseQueueOptions(config, QUEUE.WEBHOOK_DELIVER)), inject: [DI.config], }; diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts index b4ffffecc0..2ae8a2b754 100644 --- a/packages/backend/src/core/QueueService.ts +++ b/packages/backend/src/core/QueueService.ts @@ -1,6 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; import { v4 as uuid } from 'uuid'; -import Bull from 'bull'; import type { IActivity } from '@/core/activitypub/type.js'; import type { DriveFile } from '@/models/entities/DriveFile.js'; import type { Webhook, webhookEventTypes } from '@/models/entities/Webhook.js'; @@ -11,6 +10,7 @@ import type { Antenna } from '@/server/api/endpoints/i/import-antennas.js'; import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, RelationshipQueue, SystemQueue, WebhookDeliverQueue } from './QueueModule.js'; import type { DbJobData, RelationshipJobData, ThinUser } from '../queue/types.js'; import type httpSignature from '@peertube/http-signature'; +import type * as Bull from 'bullmq'; @Injectable() export class QueueService { @@ -26,7 +26,43 @@ export class QueueService { @Inject('queue:relationship') public relationshipQueue: RelationshipQueue, @Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue, @Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue, - ) {} + ) { + this.systemQueue.add('tickCharts', { + }, { + repeat: { pattern: '55 * * * *' }, + removeOnComplete: true, + }); + + this.systemQueue.add('resyncCharts', { + }, { + repeat: { pattern: '0 0 * * *' }, + removeOnComplete: true, + }); + + this.systemQueue.add('cleanCharts', { + }, { + repeat: { pattern: '0 0 * * *' }, + removeOnComplete: true, + }); + + this.systemQueue.add('aggregateRetention', { + }, { + repeat: { pattern: '0 0 * * *' }, + removeOnComplete: true, + }); + + this.systemQueue.add('clean', { + }, { + repeat: { pattern: '0 0 * * *' }, + removeOnComplete: true, + }); + + this.systemQueue.add('checkExpiredMutings', { + }, { + repeat: { pattern: '*/5 * * * *' }, + removeOnComplete: true, + }); + } @bindThis public deliver(user: ThinUser, content: IActivity | null, to: string | null, isSharedInbox: boolean) { @@ -42,11 +78,10 @@ export class QueueService { isSharedInbox, }; - return this.deliverQueue.add(data, { + return this.deliverQueue.add(to, data, { attempts: this.config.deliverJobMaxAttempts ?? 12, - timeout: 1 * 60 * 1000, // 1min backoff: { - type: 'apBackoff', + type: 'custom', }, removeOnComplete: true, removeOnFail: true, @@ -60,11 +95,10 @@ export class QueueService { signature, }; - return this.inboxQueue.add(data, { + return this.inboxQueue.add('', data, { attempts: this.config.inboxJobMaxAttempts ?? 8, - timeout: 5 * 60 * 1000, // 5min backoff: { - type: 'apBackoff', + type: 'custom', }, removeOnComplete: true, removeOnFail: true, @@ -212,7 +246,7 @@ export class QueueService { private generateToDbJobData>(name: T, data: D): { name: string, data: D, - opts: Bull.JobOptions, + opts: Bull.JobsOptions, } { return { name, @@ -299,10 +333,10 @@ export class QueueService { } @bindThis - private generateRelationshipJobData(name: 'follow' | 'unfollow' | 'block' | 'unblock', data: RelationshipJobData, opts: Bull.JobOptions = {}): { + private generateRelationshipJobData(name: 'follow' | 'unfollow' | 'block' | 'unblock', data: RelationshipJobData, opts: Bull.JobsOptions = {}): { name: string, data: RelationshipJobData, - opts: Bull.JobOptions, + opts: Bull.JobsOptions, } { return { name, @@ -351,11 +385,10 @@ export class QueueService { eventId: uuid(), }; - return this.webhookDeliverQueue.add(data, { + return this.webhookDeliverQueue.add(webhook.id, data, { attempts: 4, - timeout: 1 * 60 * 1000, // 1min backoff: { - type: 'apBackoff', + type: 'custom', }, removeOnComplete: true, removeOnFail: true, @@ -367,11 +400,11 @@ export class QueueService { this.deliverQueue.once('cleaned', (jobs, status) => { //deliverLogger.succ(`Cleaned ${jobs.length} ${status} jobs`); }); - this.deliverQueue.clean(0, 'delayed'); + this.deliverQueue.clean(0, Infinity, 'delayed'); this.inboxQueue.once('cleaned', (jobs, status) => { //inboxLogger.succ(`Cleaned ${jobs.length} ${status} jobs`); }); - this.inboxQueue.clean(0, 'delayed'); + this.inboxQueue.clean(0, Infinity, 'delayed'); } } diff --git a/packages/backend/src/core/activitypub/LdSignatureService.ts b/packages/backend/src/core/activitypub/LdSignatureService.ts index 2dc1a410ac..20fe2a0a77 100644 --- a/packages/backend/src/core/activitypub/LdSignatureService.ts +++ b/packages/backend/src/core/activitypub/LdSignatureService.ts @@ -94,7 +94,7 @@ class LdSignature { @bindThis private getLoader() { return async (url: string): Promise => { - if (!url.match('^https?\:\/\/')) throw `Invalid URL ${url}`; + if (!url.match('^https?\:\/\/')) throw new Error(`Invalid URL ${url}`); if (this.preLoad) { if (url in CONTEXTS) { @@ -126,7 +126,7 @@ class LdSignature { timeout: this.loderTimeout, }, { throwErrorWhenResponseNotOk: false }).then(res => { if (!res.ok) { - throw `${res.status} ${res.statusText}`; + throw new Error(`${res.status} ${res.statusText}`); } else { return res.json(); } diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index 87a9db405f..76757f530a 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -18,6 +18,7 @@ import { PollService } from '@/core/PollService.js'; import { StatusError } from '@/misc/status-error.js'; import { UtilityService } from '@/core/UtilityService.js'; import { bindThis } from '@/decorators.js'; +import { checkHttps } from '@/misc/check-https.js'; import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType } from '../type.js'; // eslint-disable-next-line @typescript-eslint/consistent-type-imports import { ApLoggerService } from '../ApLoggerService.js'; @@ -32,7 +33,6 @@ import { ApQuestionService } from './ApQuestionService.js'; import { ApImageService } from './ApImageService.js'; import type { Resolver } from '../ApResolverService.js'; import type { IObject, IPost } from '../type.js'; -import { checkHttps } from '@/misc/check-https.js'; @Injectable() export class ApNoteService { @@ -230,7 +230,7 @@ export class ApNoteService { quote = results.filter((x): x is { status: 'ok', res: Note | null } => x.status === 'ok').map(x => x.res).find(x => x); if (!quote) { if (results.some(x => x.status === 'temperror')) { - throw 'quote resolve failed'; + throw new Error('quote resolve failed'); } } } @@ -311,7 +311,7 @@ export class ApNoteService { // ブロックしてたら中断 const meta = await this.metaService.fetch(); - if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.extractDbHost(uri))) throw { statusCode: 451 }; + if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.extractDbHost(uri))) throw new StatusError('blocked host', 451); const unlock = await this.appLockService.getApLock(uri); diff --git a/packages/backend/src/daemons/QueueStatsService.ts b/packages/backend/src/daemons/QueueStatsService.ts index b717434e09..0a5b3184d2 100644 --- a/packages/backend/src/daemons/QueueStatsService.ts +++ b/packages/backend/src/daemons/QueueStatsService.ts @@ -1,7 +1,11 @@ -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import Xev from 'xev'; +import * as Bull from 'bullmq'; import { QueueService } from '@/core/QueueService.js'; import { bindThis } from '@/decorators.js'; +import { DI } from '@/di-symbols.js'; +import type { Config } from '@/config.js'; +import { QUEUE, baseQueueOptions } from '@/queue/const.js'; import type { OnApplicationShutdown } from '@nestjs/common'; const ev = new Xev(); @@ -13,6 +17,9 @@ export class QueueStatsService implements OnApplicationShutdown { private intervalId: NodeJS.Timer; constructor( + @Inject(DI.config) + private config: Config, + private queueService: QueueService, ) { } @@ -31,11 +38,14 @@ export class QueueStatsService implements OnApplicationShutdown { let activeDeliverJobs = 0; let activeInboxJobs = 0; - this.queueService.deliverQueue.on('global:active', () => { + const deliverQueueEvents = new Bull.QueueEvents(QUEUE.DELIVER, baseQueueOptions(this.config, QUEUE.DELIVER)); + const inboxQueueEvents = new Bull.QueueEvents(QUEUE.INBOX, baseQueueOptions(this.config, QUEUE.INBOX)); + + deliverQueueEvents.on('active', () => { activeDeliverJobs++; }); - this.queueService.inboxQueue.on('global:active', () => { + inboxQueueEvents.on('active', () => { activeInboxJobs++; }); diff --git a/packages/backend/src/misc/id/aid.ts b/packages/backend/src/misc/id/aid.ts index 9e206ee98f..f0cbc9900d 100644 --- a/packages/backend/src/misc/id/aid.ts +++ b/packages/backend/src/misc/id/aid.ts @@ -21,7 +21,7 @@ function getNoise(): string { export function genAid(date: Date): string { const t = date.getTime(); - if (isNaN(t)) throw 'Failed to create AID: Invalid Date'; + if (isNaN(t)) throw new Error('Failed to create AID: Invalid Date'); counter++; return getTime(t) + getNoise(); } diff --git a/packages/backend/src/misc/prelude/time.ts b/packages/backend/src/misc/prelude/time.ts index 34e8b6b17c..b21978b186 100644 --- a/packages/backend/src/misc/prelude/time.ts +++ b/packages/backend/src/misc/prelude/time.ts @@ -5,15 +5,16 @@ const dateTimeIntervals = { }; export function dateUTC(time: number[]): Date { - const d = time.length === 2 ? Date.UTC(time[0], time[1]) - : time.length === 3 ? Date.UTC(time[0], time[1], time[2]) - : time.length === 4 ? Date.UTC(time[0], time[1], time[2], time[3]) - : time.length === 5 ? Date.UTC(time[0], time[1], time[2], time[3], time[4]) - : time.length === 6 ? Date.UTC(time[0], time[1], time[2], time[3], time[4], time[5]) - : time.length === 7 ? Date.UTC(time[0], time[1], time[2], time[3], time[4], time[5], time[6]) - : null; - - if (!d) throw 'wrong number of arguments'; + const d = + time.length === 2 ? Date.UTC(time[0], time[1]) + : time.length === 3 ? Date.UTC(time[0], time[1], time[2]) + : time.length === 4 ? Date.UTC(time[0], time[1], time[2], time[3]) + : time.length === 5 ? Date.UTC(time[0], time[1], time[2], time[3], time[4]) + : time.length === 6 ? Date.UTC(time[0], time[1], time[2], time[3], time[4], time[5]) + : time.length === 7 ? Date.UTC(time[0], time[1], time[2], time[3], time[4], time[5], time[6]) + : null; + + if (!d) throw new Error('wrong number of arguments'); return new Date(d); } diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts index dc025f9889..011082cd36 100644 --- a/packages/backend/src/queue/QueueProcessorService.ts +++ b/packages/backend/src/queue/QueueProcessorService.ts @@ -1,10 +1,9 @@ import { Inject, Injectable } from '@nestjs/common'; +import * as Bull from 'bullmq'; import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; import type Logger from '@/logger.js'; -import { QueueService } from '@/core/QueueService.js'; import { bindThis } from '@/decorators.js'; -import { getJobInfo } from './get-job-info.js'; import { WebhookDeliverProcessorService } from './processors/WebhookDeliverProcessorService.js'; import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js'; import { DeliverProcessorService } from './processors/DeliverProcessorService.js'; @@ -35,6 +34,33 @@ import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMu import { CleanProcessorService } from './processors/CleanProcessorService.js'; import { AggregateRetentionProcessorService } from './processors/AggregateRetentionProcessorService.js'; import { QueueLoggerService } from './QueueLoggerService.js'; +import { QUEUE, baseQueueOptions } from './const.js'; + +// ref. https://github.com/misskey-dev/misskey/pull/7635#issue-971097019 +function httpRelatedBackoff(attemptsMade: number) { + const baseDelay = 60 * 1000; // 1min + const maxBackoff = 8 * 60 * 60 * 1000; // 8hours + let backoff = (Math.pow(2, attemptsMade) - 1) * baseDelay; + backoff = Math.min(backoff, maxBackoff); + backoff += Math.round(backoff * Math.random() * 0.2); + return backoff; +} + +function getJobInfo(job: Bull.Job | undefined, increment = false): string { + if (job == null) return '-'; + + const age = Date.now() - job.timestamp; + + const formated = age > 60000 ? `${Math.floor(age / 1000 / 60)}m` + : age > 10000 ? `${Math.floor(age / 1000)}s` + : `${age}ms`; + + // onActiveとかonCompletedのattemptsMadeがなぜか0始まりなのでインクリメントする + const currentAttempts = job.attemptsMade + (increment ? 1 : 0); + const maxAttempts = job.opts ? job.opts.attempts : 0; + + return `id=${job.id} attempts=${currentAttempts}/${maxAttempts} age=${formated}`; +} @Injectable() export class QueueProcessorService { @@ -45,7 +71,6 @@ export class QueueProcessorService { private config: Config, private queueLoggerService: QueueLoggerService, - private queueService: QueueService, private webhookDeliverProcessorService: WebhookDeliverProcessorService, private endedPollNotificationProcessorService: EndedPollNotificationProcessorService, private deliverProcessorService: DeliverProcessorService, @@ -97,146 +122,191 @@ export class QueueProcessorService { } } + //#region system + const systemQueueWorker = new Bull.Worker(QUEUE.SYSTEM, (job) => { + switch (job.name) { + case 'tickCharts': return this.tickChartsProcessorService.process(); + case 'resyncCharts': return this.resyncChartsProcessorService.process(); + case 'cleanCharts': return this.cleanChartsProcessorService.process(); + case 'aggregateRetention': return this.aggregateRetentionProcessorService.process(); + case 'checkExpiredMutings': return this.checkExpiredMutingsProcessorService.process(); + case 'clean': return this.cleanProcessorService.process(); + default: throw new Error(`unrecognized job type ${job.name} for system`); + } + }, { + ...baseQueueOptions(this.config, QUEUE.SYSTEM), + }); + const systemLogger = this.logger.createSubLogger('system'); - const deliverLogger = this.logger.createSubLogger('deliver'); - const webhookLogger = this.logger.createSubLogger('webhook'); - const inboxLogger = this.logger.createSubLogger('inbox'); - const dbLogger = this.logger.createSubLogger('db'); - const relationshipLogger = this.logger.createSubLogger('relationship'); - const objectStorageLogger = this.logger.createSubLogger('objectStorage'); - this.queueService.systemQueue - .on('waiting', (jobId) => systemLogger.debug(`waiting id=${jobId}`)) + systemQueueWorker .on('active', (job) => systemLogger.debug(`active id=${job.id}`)) .on('completed', (job, result) => systemLogger.debug(`completed(${result}) id=${job.id}`)) - .on('failed', (job, err) => systemLogger.warn(`failed(${err}) id=${job.id}`, { job, e: renderError(err) })) - .on('error', (job: any, err: Error) => systemLogger.error(`error ${err}`, { job, e: renderError(err) })) - .on('stalled', (job) => systemLogger.warn(`stalled id=${job.id}`)); + .on('failed', (job, err) => systemLogger.warn(`failed(${err}) id=${job ? job.id : '-'}`, { job, e: renderError(err) })) + .on('error', (err: Error) => systemLogger.error(`error ${err}`, { e: renderError(err) })) + .on('stalled', (jobId) => systemLogger.warn(`stalled id=${jobId}`)); + //#endregion - this.queueService.deliverQueue - .on('waiting', (jobId) => deliverLogger.debug(`waiting id=${jobId}`)) - .on('active', (job) => deliverLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`)) - .on('completed', (job, result) => deliverLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`)) - .on('failed', (job, err) => deliverLogger.warn(`failed(${err}) ${getJobInfo(job)} to=${job.data.to}`)) - .on('error', (job: any, err: Error) => deliverLogger.error(`error ${err}`, { job, e: renderError(err) })) - .on('stalled', (job) => deliverLogger.warn(`stalled ${getJobInfo(job)} to=${job.data.to}`)); + //#region db + const dbQueueWorker = new Bull.Worker(QUEUE.DB, (job) => { + switch (job.name) { + case 'deleteDriveFiles': return this.deleteDriveFilesProcessorService.process(job); + case 'exportCustomEmojis': return this.exportCustomEmojisProcessorService.process(job); + case 'exportNotes': return this.exportNotesProcessorService.process(job); + case 'exportFavorites': return this.exportFavoritesProcessorService.process(job); + case 'exportFollowing': return this.exportFollowingProcessorService.process(job); + case 'exportMuting': return this.exportMutingProcessorService.process(job); + case 'exportBlocking': return this.exportBlockingProcessorService.process(job); + case 'exportUserLists': return this.exportUserListsProcessorService.process(job); + case 'exportAntennas': return this.exportAntennasProcessorService.process(job); + case 'importFollowing': return this.importFollowingProcessorService.process(job); + case 'importFollowingToDb': return this.importFollowingProcessorService.processDb(job); + case 'importMuting': return this.importMutingProcessorService.process(job); + case 'importBlocking': return this.importBlockingProcessorService.process(job); + case 'importBlockingToDb': return this.importBlockingProcessorService.processDb(job); + case 'importUserLists': return this.importUserListsProcessorService.process(job); + case 'importCustomEmojis': return this.importCustomEmojisProcessorService.process(job); + case 'importAntennas': return this.importAntennasProcessorService.process(job); + case 'deleteAccount': return this.deleteAccountProcessorService.process(job); + default: throw new Error(`unrecognized job type ${job.name} for db`); + } + }, { + ...baseQueueOptions(this.config, QUEUE.DB), + }); - this.queueService.inboxQueue - .on('waiting', (jobId) => inboxLogger.debug(`waiting id=${jobId}`)) - .on('active', (job) => inboxLogger.debug(`active ${getJobInfo(job, true)}`)) - .on('completed', (job, result) => inboxLogger.debug(`completed(${result}) ${getJobInfo(job, true)}`)) - .on('failed', (job, err) => inboxLogger.warn(`failed(${err}) ${getJobInfo(job)} activity=${job.data.activity ? job.data.activity.id : 'none'}`, { job, e: renderError(err) })) - .on('error', (job: any, err: Error) => inboxLogger.error(`error ${err}`, { job, e: renderError(err) })) - .on('stalled', (job) => inboxLogger.warn(`stalled ${getJobInfo(job)} activity=${job.data.activity ? job.data.activity.id : 'none'}`)); + const dbLogger = this.logger.createSubLogger('db'); - this.queueService.dbQueue - .on('waiting', (jobId) => dbLogger.debug(`waiting id=${jobId}`)) + dbQueueWorker .on('active', (job) => dbLogger.debug(`active id=${job.id}`)) .on('completed', (job, result) => dbLogger.debug(`completed(${result}) id=${job.id}`)) - .on('failed', (job, err) => dbLogger.warn(`failed(${err}) id=${job.id}`, { job, e: renderError(err) })) - .on('error', (job: any, err: Error) => dbLogger.error(`error ${err}`, { job, e: renderError(err) })) - .on('stalled', (job) => dbLogger.warn(`stalled id=${job.id}`)); + .on('failed', (job, err) => dbLogger.warn(`failed(${err}) id=${job ? job.id : '-'}`, { job, e: renderError(err) })) + .on('error', (err: Error) => dbLogger.error(`error ${err}`, { e: renderError(err) })) + .on('stalled', (jobId) => dbLogger.warn(`stalled id=${jobId}`)); + //#endregion - this.queueService.relationshipQueue - .on('waiting', (jobId) => relationshipLogger.debug(`waiting id=${jobId}`)) - .on('active', (job) => relationshipLogger.debug(`active id=${job.id}`)) - .on('completed', (job, result) => relationshipLogger.debug(`completed(${result}) id=${job.id}`)) - .on('failed', (job, err) => relationshipLogger.warn(`failed(${err}) id=${job.id}`, { job, e: renderError(err) })) - .on('error', (job: any, err: Error) => relationshipLogger.error(`error ${err}`, { job, e: renderError(err) })) - .on('stalled', (job) => relationshipLogger.warn(`stalled id=${job.id}`)); + //#region deliver + const deliverQueueWorker = new Bull.Worker(QUEUE.DELIVER, (job) => this.deliverProcessorService.process(job), { + ...baseQueueOptions(this.config, QUEUE.DELIVER), + concurrency: this.config.deliverJobConcurrency ?? 128, + limiter: { + max: this.config.deliverJobPerSec ?? 128, + duration: 1000, + }, + settings: { + backoffStrategy: httpRelatedBackoff, + }, + }); - this.queueService.objectStorageQueue - .on('waiting', (jobId) => objectStorageLogger.debug(`waiting id=${jobId}`)) - .on('active', (job) => objectStorageLogger.debug(`active id=${job.id}`)) - .on('completed', (job, result) => objectStorageLogger.debug(`completed(${result}) id=${job.id}`)) - .on('failed', (job, err) => objectStorageLogger.warn(`failed(${err}) id=${job.id}`, { job, e: renderError(err) })) - .on('error', (job: any, err: Error) => objectStorageLogger.error(`error ${err}`, { job, e: renderError(err) })) - .on('stalled', (job) => objectStorageLogger.warn(`stalled id=${job.id}`)); + const deliverLogger = this.logger.createSubLogger('deliver'); - this.queueService.webhookDeliverQueue - .on('waiting', (jobId) => webhookLogger.debug(`waiting id=${jobId}`)) - .on('active', (job) => webhookLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`)) - .on('completed', (job, result) => webhookLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`)) - .on('failed', (job, err) => webhookLogger.warn(`failed(${err}) ${getJobInfo(job)} to=${job.data.to}`)) - .on('error', (job: any, err: Error) => webhookLogger.error(`error ${err}`, { job, e: renderError(err) })) - .on('stalled', (job) => webhookLogger.warn(`stalled ${getJobInfo(job)} to=${job.data.to}`)); + deliverQueueWorker + .on('active', (job) => deliverLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`)) + .on('completed', (job, result) => deliverLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`)) + .on('failed', (job, err) => deliverLogger.warn(`failed(${err}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`)) + .on('error', (err: Error) => deliverLogger.error(`error ${err}`, { e: renderError(err) })) + .on('stalled', (jobId) => deliverLogger.warn(`stalled id=${jobId}`)); + //#endregion - this.queueService.systemQueue.add('tickCharts', { - }, { - repeat: { cron: '55 * * * *' }, - removeOnComplete: true, + //#region inbox + const inboxQueueWorker = new Bull.Worker(QUEUE.INBOX, (job) => this.inboxProcessorService.process(job), { + ...baseQueueOptions(this.config, QUEUE.INBOX), + concurrency: this.config.inboxJobConcurrency ?? 16, + limiter: { + max: this.config.inboxJobPerSec ?? 16, + duration: 1000, + }, + settings: { + backoffStrategy: httpRelatedBackoff, + }, }); - this.queueService.systemQueue.add('resyncCharts', { - }, { - repeat: { cron: '0 0 * * *' }, - removeOnComplete: true, - }); + const inboxLogger = this.logger.createSubLogger('inbox'); - this.queueService.systemQueue.add('cleanCharts', { - }, { - repeat: { cron: '0 0 * * *' }, - removeOnComplete: true, - }); + inboxQueueWorker + .on('active', (job) => inboxLogger.debug(`active ${getJobInfo(job, true)}`)) + .on('completed', (job, result) => inboxLogger.debug(`completed(${result}) ${getJobInfo(job, true)}`)) + .on('failed', (job, err) => inboxLogger.warn(`failed(${err}) ${getJobInfo(job)} activity=${job ? (job.data.activity ? job.data.activity.id : 'none') : '-'}`, { job, e: renderError(err) })) + .on('error', (err: Error) => inboxLogger.error(`error ${err}`, { e: renderError(err) })) + .on('stalled', (jobId) => inboxLogger.warn(`stalled id=${jobId}`)); + //#endregion - this.queueService.systemQueue.add('aggregateRetention', { - }, { - repeat: { cron: '0 0 * * *' }, - removeOnComplete: true, + //#region webhook deliver + const webhookDeliverQueueWorker = new Bull.Worker(QUEUE.WEBHOOK_DELIVER, (job) => this.webhookDeliverProcessorService.process(job), { + ...baseQueueOptions(this.config, QUEUE.WEBHOOK_DELIVER), + concurrency: 64, + limiter: { + max: 64, + duration: 1000, + }, + settings: { + backoffStrategy: httpRelatedBackoff, + }, }); - this.queueService.systemQueue.add('clean', { + const webhookLogger = this.logger.createSubLogger('webhook'); + + webhookDeliverQueueWorker + .on('active', (job) => webhookLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`)) + .on('completed', (job, result) => webhookLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`)) + .on('failed', (job, err) => webhookLogger.warn(`failed(${err}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`)) + .on('error', (err: Error) => webhookLogger.error(`error ${err}`, { e: renderError(err) })) + .on('stalled', (jobId) => webhookLogger.warn(`stalled id=${jobId}`)); + //#endregion + + //#region relationship + const relationshipQueueWorker = new Bull.Worker(QUEUE.RELATIONSHIP, (job) => { + switch (job.name) { + case 'follow': return this.relationshipProcessorService.processFollow(job); + case 'unfollow': return this.relationshipProcessorService.processUnfollow(job); + case 'block': return this.relationshipProcessorService.processBlock(job); + case 'unblock': return this.relationshipProcessorService.processUnblock(job); + default: throw new Error(`unrecognized job type ${job.name} for relationship`); + } }, { - repeat: { cron: '0 0 * * *' }, - removeOnComplete: true, + ...baseQueueOptions(this.config, QUEUE.RELATIONSHIP), + concurrency: this.config.relashionshipJobConcurrency ?? 16, + limiter: { + max: this.config.relashionshipJobPerSec ?? 64, + duration: 1000, + }, }); - this.queueService.systemQueue.add('checkExpiredMutings', { + const relationshipLogger = this.logger.createSubLogger('relationship'); + + relationshipQueueWorker + .on('active', (job) => relationshipLogger.debug(`active id=${job.id}`)) + .on('completed', (job, result) => relationshipLogger.debug(`completed(${result}) id=${job.id}`)) + .on('failed', (job, err) => relationshipLogger.warn(`failed(${err}) id=${job ? job.id : '-'}`, { job, e: renderError(err) })) + .on('error', (err: Error) => relationshipLogger.error(`error ${err}`, { e: renderError(err) })) + .on('stalled', (jobId) => relationshipLogger.warn(`stalled id=${jobId}`)); + //#endregion + + //#region object storage + const objectStorageQueueWorker = new Bull.Worker(QUEUE.OBJECT_STORAGE, (job) => { + switch (job.name) { + case 'deleteFile': return this.deleteFileProcessorService.process(job); + case 'cleanRemoteFiles': return this.cleanRemoteFilesProcessorService.process(job); + default: throw new Error(`unrecognized job type ${job.name} for objectStorage`); + } }, { - repeat: { cron: '*/5 * * * *' }, - removeOnComplete: true, + ...baseQueueOptions(this.config, QUEUE.OBJECT_STORAGE), + concurrency: 16, }); - this.queueService.deliverQueue.process(this.config.deliverJobConcurrency ?? 128, (job) => this.deliverProcessorService.process(job)); - this.queueService.inboxQueue.process(this.config.inboxJobConcurrency ?? 16, (job) => this.inboxProcessorService.process(job)); - this.queueService.endedPollNotificationQueue.process((job, done) => this.endedPollNotificationProcessorService.process(job, done)); - this.queueService.webhookDeliverQueue.process(64, (job) => this.webhookDeliverProcessorService.process(job)); - - this.queueService.dbQueue.process('deleteDriveFiles', (job, done) => this.deleteDriveFilesProcessorService.process(job, done)); - this.queueService.dbQueue.process('exportCustomEmojis', (job, done) => this.exportCustomEmojisProcessorService.process(job, done)); - this.queueService.dbQueue.process('exportNotes', (job, done) => this.exportNotesProcessorService.process(job, done)); - this.queueService.dbQueue.process('exportFavorites', (job, done) => this.exportFavoritesProcessorService.process(job, done)); - this.queueService.dbQueue.process('exportFollowing', (job, done) => this.exportFollowingProcessorService.process(job, done)); - this.queueService.dbQueue.process('exportMuting', (job, done) => this.exportMutingProcessorService.process(job, done)); - this.queueService.dbQueue.process('exportBlocking', (job, done) => this.exportBlockingProcessorService.process(job, done)); - this.queueService.dbQueue.process('exportUserLists', (job, done) => this.exportUserListsProcessorService.process(job, done)); - this.queueService.dbQueue.process('exportAntennas', (job, done) => this.exportAntennasProcessorService.process(job, done)); - this.queueService.dbQueue.process('importFollowing', (job, done) => this.importFollowingProcessorService.process(job, done)); - this.queueService.dbQueue.process('importFollowingToDb', (job) => this.importFollowingProcessorService.processDb(job)); - this.queueService.dbQueue.process('importMuting', (job, done) => this.importMutingProcessorService.process(job, done)); - this.queueService.dbQueue.process('importBlocking', (job, done) => this.importBlockingProcessorService.process(job, done)); - this.queueService.dbQueue.process('importBlockingToDb', (job) => this.importBlockingProcessorService.processDb(job)); - this.queueService.dbQueue.process('importUserLists', (job, done) => this.importUserListsProcessorService.process(job, done)); - this.queueService.dbQueue.process('importCustomEmojis', (job, done) => this.importCustomEmojisProcessorService.process(job, done)); - this.queueService.dbQueue.process('importAntennas', (job, done) => this.importAntennasProcessorService.process(job, done)); - this.queueService.dbQueue.process('deleteAccount', (job) => this.deleteAccountProcessorService.process(job)); - - this.queueService.objectStorageQueue.process('deleteFile', 16, (job) => this.deleteFileProcessorService.process(job)); - this.queueService.objectStorageQueue.process('cleanRemoteFiles', 16, (job, done) => this.cleanRemoteFilesProcessorService.process(job, done)); - - { - const maxJobs = this.config.relashionshipJobConcurrency ?? 16; - this.queueService.relationshipQueue.process('follow', maxJobs, (job) => this.relationshipProcessorService.processFollow(job)); - this.queueService.relationshipQueue.process('unfollow', maxJobs, (job) => this.relationshipProcessorService.processUnfollow(job)); - this.queueService.relationshipQueue.process('block', maxJobs, (job) => this.relationshipProcessorService.processBlock(job)); - this.queueService.relationshipQueue.process('unblock', maxJobs, (job) => this.relationshipProcessorService.processUnblock(job)); - } + const objectStorageLogger = this.logger.createSubLogger('objectStorage'); - this.queueService.systemQueue.process('tickCharts', (job, done) => this.tickChartsProcessorService.process(job, done)); - this.queueService.systemQueue.process('resyncCharts', (job, done) => this.resyncChartsProcessorService.process(job, done)); - this.queueService.systemQueue.process('cleanCharts', (job, done) => this.cleanChartsProcessorService.process(job, done)); - this.queueService.systemQueue.process('aggregateRetention', (job, done) => this.aggregateRetentionProcessorService.process(job, done)); - this.queueService.systemQueue.process('checkExpiredMutings', (job, done) => this.checkExpiredMutingsProcessorService.process(job, done)); - this.queueService.systemQueue.process('clean', (job, done) => this.cleanProcessorService.process(job, done)); + objectStorageQueueWorker + .on('active', (job) => objectStorageLogger.debug(`active id=${job.id}`)) + .on('completed', (job, result) => objectStorageLogger.debug(`completed(${result}) id=${job.id}`)) + .on('failed', (job, err) => objectStorageLogger.warn(`failed(${err}) id=${job ? job.id : '-'}`, { job, e: renderError(err) })) + .on('error', (err: Error) => objectStorageLogger.error(`error ${err}`, { e: renderError(err) })) + .on('stalled', (jobId) => objectStorageLogger.warn(`stalled id=${jobId}`)); + //#endregion + + //#region ended poll notification + const endedPollNotificationWorker = new Bull.Worker(QUEUE.ENDED_POLL_NOTIFICATION, (job) => this.endedPollNotificationProcessorService.process(job), { + ...baseQueueOptions(this.config, QUEUE.ENDED_POLL_NOTIFICATION), + }); + //#endregion } } diff --git a/packages/backend/src/queue/const.ts b/packages/backend/src/queue/const.ts new file mode 100644 index 0000000000..d240fe70e0 --- /dev/null +++ b/packages/backend/src/queue/const.ts @@ -0,0 +1,26 @@ +import { Config } from '@/config.js'; +import type * as Bull from 'bullmq'; + +export const QUEUE = { + DELIVER: 'deliver', + INBOX: 'inbox', + SYSTEM: 'system', + ENDED_POLL_NOTIFICATION: 'endedPollNotification', + DB: 'db', + RELATIONSHIP: 'relationship', + OBJECT_STORAGE: 'objectStorage', + WEBHOOK_DELIVER: 'webhookDeliver', +}; + +export function baseQueueOptions(config: Config, queueName: typeof QUEUE[keyof typeof QUEUE]): Bull.QueueOptions { + return { + connection: { + port: config.redisForJobQueue.port, + host: config.redisForJobQueue.host, + family: config.redisForJobQueue.family == null ? 0 : config.redisForJobQueue.family, + password: config.redisForJobQueue.pass, + db: config.redisForJobQueue.db ?? 0, + }, + prefix: config.redisForJobQueue.prefix ? `${config.redisForJobQueue.prefix}:queue:${queueName}` : `queue:${queueName}`, + }; +} diff --git a/packages/backend/src/queue/get-job-info.ts b/packages/backend/src/queue/get-job-info.ts deleted file mode 100644 index d33e349c36..0000000000 --- a/packages/backend/src/queue/get-job-info.ts +++ /dev/null @@ -1,15 +0,0 @@ -import Bull from 'bull'; - -export function getJobInfo(job: Bull.Job, increment = false) { - const age = Date.now() - job.timestamp; - - const formated = age > 60000 ? `${Math.floor(age / 1000 / 60)}m` - : age > 10000 ? `${Math.floor(age / 1000)}s` - : `${age}ms`; - - // onActiveとかonCompletedのattemptsMadeがなぜか0始まりなのでインクリメントする - const currentAttempts = job.attemptsMade + (increment ? 1 : 0); - const maxAttempts = job.opts ? job.opts.attempts : 0; - - return `id=${job.id} attempts=${currentAttempts}/${maxAttempts} age=${formated}`; -} diff --git a/packages/backend/src/queue/processors/AggregateRetentionProcessorService.ts b/packages/backend/src/queue/processors/AggregateRetentionProcessorService.ts index e2720b4fe0..600ce0828f 100644 --- a/packages/backend/src/queue/processors/AggregateRetentionProcessorService.ts +++ b/packages/backend/src/queue/processors/AggregateRetentionProcessorService.ts @@ -9,7 +9,7 @@ import { deepClone } from '@/misc/clone.js'; import { IdService } from '@/core/IdService.js'; import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; -import type Bull from 'bull'; +import type * as Bull from 'bullmq'; @Injectable() export class AggregateRetentionProcessorService { @@ -32,7 +32,7 @@ export class AggregateRetentionProcessorService { } @bindThis - public async process(job: Bull.Job>, done: () => void): Promise { + public async process(): Promise { this.logger.info('Aggregating retention...'); const now = new Date(); @@ -62,7 +62,6 @@ export class AggregateRetentionProcessorService { } catch (err) { if (isDuplicateKeyValueError(err)) { this.logger.succ('Skip because it has already been processed by another worker.'); - done(); return; } throw err; @@ -88,6 +87,5 @@ export class AggregateRetentionProcessorService { } this.logger.succ('Retention aggregated.'); - done(); } } diff --git a/packages/backend/src/queue/processors/CheckExpiredMutingsProcessorService.ts b/packages/backend/src/queue/processors/CheckExpiredMutingsProcessorService.ts index 2476d71a5e..c4ee212bab 100644 --- a/packages/backend/src/queue/processors/CheckExpiredMutingsProcessorService.ts +++ b/packages/backend/src/queue/processors/CheckExpiredMutingsProcessorService.ts @@ -7,7 +7,7 @@ import type Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; import { UserMutingService } from '@/core/UserMutingService.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; -import type Bull from 'bull'; +import type * as Bull from 'bullmq'; @Injectable() export class CheckExpiredMutingsProcessorService { @@ -27,7 +27,7 @@ export class CheckExpiredMutingsProcessorService { } @bindThis - public async process(job: Bull.Job>, done: () => void): Promise { + public async process(): Promise { this.logger.info('Checking expired mutings...'); const expired = await this.mutingsRepository.createQueryBuilder('muting') @@ -41,6 +41,5 @@ export class CheckExpiredMutingsProcessorService { } this.logger.succ('All expired mutings checked.'); - done(); } } diff --git a/packages/backend/src/queue/processors/CleanChartsProcessorService.ts b/packages/backend/src/queue/processors/CleanChartsProcessorService.ts index b458167042..22d7c1b4fb 100644 --- a/packages/backend/src/queue/processors/CleanChartsProcessorService.ts +++ b/packages/backend/src/queue/processors/CleanChartsProcessorService.ts @@ -16,7 +16,7 @@ import PerUserDriveChart from '@/core/chart/charts/per-user-drive.js'; import ApRequestChart from '@/core/chart/charts/ap-request.js'; import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; -import type Bull from 'bull'; +import type * as Bull from 'bullmq'; @Injectable() export class CleanChartsProcessorService { @@ -45,7 +45,7 @@ export class CleanChartsProcessorService { } @bindThis - public async process(job: Bull.Job>, done: () => void): Promise { + public async process(): Promise { this.logger.info('Clean charts...'); await Promise.all([ @@ -64,6 +64,5 @@ export class CleanChartsProcessorService { ]); this.logger.succ('All charts successfully cleaned.'); - done(); } } diff --git a/packages/backend/src/queue/processors/CleanProcessorService.ts b/packages/backend/src/queue/processors/CleanProcessorService.ts index 1936e8df23..cefa6da5e9 100644 --- a/packages/backend/src/queue/processors/CleanProcessorService.ts +++ b/packages/backend/src/queue/processors/CleanProcessorService.ts @@ -7,7 +7,7 @@ import type Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; import { IdService } from '@/core/IdService.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; -import type Bull from 'bull'; +import type * as Bull from 'bullmq'; @Injectable() export class CleanProcessorService { @@ -36,7 +36,7 @@ export class CleanProcessorService { } @bindThis - public async process(job: Bull.Job>, done: () => void): Promise { + public async process(): Promise { this.logger.info('Cleaning...'); this.userIpsRepository.delete({ @@ -72,6 +72,5 @@ export class CleanProcessorService { } this.logger.succ('Cleaned.'); - done(); } } diff --git a/packages/backend/src/queue/processors/CleanRemoteFilesProcessorService.ts b/packages/backend/src/queue/processors/CleanRemoteFilesProcessorService.ts index 5a33c27188..c54bf59ae4 100644 --- a/packages/backend/src/queue/processors/CleanRemoteFilesProcessorService.ts +++ b/packages/backend/src/queue/processors/CleanRemoteFilesProcessorService.ts @@ -5,9 +5,9 @@ import type { DriveFilesRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; -import { QueueLoggerService } from '../QueueLoggerService.js'; -import type Bull from 'bull'; import { bindThis } from '@/decorators.js'; +import { QueueLoggerService } from '../QueueLoggerService.js'; +import type * as Bull from 'bullmq'; @Injectable() export class CleanRemoteFilesProcessorService { @@ -27,7 +27,7 @@ export class CleanRemoteFilesProcessorService { } @bindThis - public async process(job: Bull.Job>, done: () => void): Promise { + public async process(job: Bull.Job>): Promise { this.logger.info('Deleting cached remote files...'); let deletedCount = 0; @@ -47,7 +47,7 @@ export class CleanRemoteFilesProcessorService { }); if (files.length === 0) { - job.progress(100); + job.updateProgress(100); break; } @@ -62,10 +62,9 @@ export class CleanRemoteFilesProcessorService { isLink: false, }); - job.progress(deletedCount / total); + job.updateProgress(deletedCount / total); } this.logger.succ('All cached remote files has been deleted.'); - done(); } } diff --git a/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts b/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts index e36a78de6a..39dd801af0 100644 --- a/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts +++ b/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts @@ -8,10 +8,10 @@ import { DriveService } from '@/core/DriveService.js'; import type { DriveFile } from '@/models/entities/DriveFile.js'; import type { Note } from '@/models/entities/Note.js'; import { EmailService } from '@/core/EmailService.js'; +import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; -import type Bull from 'bull'; +import type * as Bull from 'bullmq'; import type { DbUserDeleteJobData } from '../types.js'; -import { bindThis } from '@/decorators.js'; @Injectable() export class DeleteAccountProcessorService { diff --git a/packages/backend/src/queue/processors/DeleteDriveFilesProcessorService.ts b/packages/backend/src/queue/processors/DeleteDriveFilesProcessorService.ts index 604497cf54..6772c5dc76 100644 --- a/packages/backend/src/queue/processors/DeleteDriveFilesProcessorService.ts +++ b/packages/backend/src/queue/processors/DeleteDriveFilesProcessorService.ts @@ -5,10 +5,10 @@ import type { UsersRepository, DriveFilesRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; +import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; -import type Bull from 'bull'; +import type * as Bull from 'bullmq'; import type { DbJobDataWithUser } from '../types.js'; -import { bindThis } from '@/decorators.js'; @Injectable() export class DeleteDriveFilesProcessorService { @@ -31,12 +31,11 @@ export class DeleteDriveFilesProcessorService { } @bindThis - public async process(job: Bull.Job, done: () => void): Promise { + public async process(job: Bull.Job): Promise { this.logger.info(`Deleting drive files of ${job.data.user.id} ...`); const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); if (user == null) { - done(); return; } @@ -56,7 +55,7 @@ export class DeleteDriveFilesProcessorService { }); if (files.length === 0) { - job.progress(100); + job.updateProgress(100); break; } @@ -71,10 +70,9 @@ export class DeleteDriveFilesProcessorService { userId: user.id, }); - job.progress(deletedCount / total); + job.updateProgress(deletedCount / total); } this.logger.succ(`All drive files (${deletedCount}) of ${user.id} has been deleted.`); - done(); } } diff --git a/packages/backend/src/queue/processors/DeleteFileProcessorService.ts b/packages/backend/src/queue/processors/DeleteFileProcessorService.ts index 2fb2f56f8d..edf87bd921 100644 --- a/packages/backend/src/queue/processors/DeleteFileProcessorService.ts +++ b/packages/backend/src/queue/processors/DeleteFileProcessorService.ts @@ -3,10 +3,10 @@ import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; +import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; -import type Bull from 'bull'; +import type * as Bull from 'bullmq'; import type { ObjectStorageFileJobData } from '../types.js'; -import { bindThis } from '@/decorators.js'; @Injectable() export class DeleteFileProcessorService { diff --git a/packages/backend/src/queue/processors/DeliverProcessorService.ts b/packages/backend/src/queue/processors/DeliverProcessorService.ts index f293bd4d7e..406e9df850 100644 --- a/packages/backend/src/queue/processors/DeliverProcessorService.ts +++ b/packages/backend/src/queue/processors/DeliverProcessorService.ts @@ -1,4 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; +import * as Bull from 'bullmq'; import { DI } from '@/di-symbols.js'; import type { DriveFilesRepository, InstancesRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; @@ -16,7 +17,6 @@ import { StatusError } from '@/misc/status-error.js'; import { UtilityService } from '@/core/UtilityService.js'; import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; -import type Bull from 'bull'; import type { DeliverJobData } from '../types.js'; @Injectable() @@ -121,15 +121,13 @@ export class DeliverProcessorService { isSuspended: true, }); }); - return `${host} is gone`; + throw new Bull.UnrecoverableError(`${host} is gone`); } - // HTTPステータスコード4xxはクライアントエラーであり、それはつまり - // 何回再送しても成功することはないということなのでエラーにはしないでおく - return `${res.statusCode} ${res.statusMessage}`; + throw new Bull.UnrecoverableError(`${res.statusCode} ${res.statusMessage}`); } // 5xx etc. - throw `${res.statusCode} ${res.statusMessage}`; + throw new Error(`${res.statusCode} ${res.statusMessage}`); } else { // DNS error, socket error, timeout ... throw res; diff --git a/packages/backend/src/queue/processors/EndedPollNotificationProcessorService.ts b/packages/backend/src/queue/processors/EndedPollNotificationProcessorService.ts index 501ed4090a..21501592f2 100644 --- a/packages/backend/src/queue/processors/EndedPollNotificationProcessorService.ts +++ b/packages/backend/src/queue/processors/EndedPollNotificationProcessorService.ts @@ -6,7 +6,7 @@ import type Logger from '@/logger.js'; import { NotificationService } from '@/core/NotificationService.js'; import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; -import type Bull from 'bull'; +import type * as Bull from 'bullmq'; import type { EndedPollNotificationJobData } from '../types.js'; @Injectable() @@ -30,10 +30,9 @@ export class EndedPollNotificationProcessorService { } @bindThis - public async process(job: Bull.Job, done: () => void): Promise { + public async process(job: Bull.Job): Promise { const note = await this.notesRepository.findOneBy({ id: job.data.noteId }); if (note == null || !note.hasPoll) { - done(); return; } @@ -51,7 +50,5 @@ export class EndedPollNotificationProcessorService { noteId: note.id, }); } - - done(); } } diff --git a/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts b/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts index 894903e79b..ac52325c8d 100644 --- a/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts @@ -12,7 +12,7 @@ import { createTemp } from '@/misc/create-temp.js'; import { UtilityService } from '@/core/UtilityService.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type { DBExportAntennasData } from '../types.js'; -import type Bull from 'bull'; +import type * as Bull from 'bullmq'; @Injectable() export class ExportAntennasProcessorService { @@ -39,10 +39,9 @@ export class ExportAntennasProcessorService { } @bindThis - public async process(job: Bull.Job, done: () => void): Promise { + public async process(job: Bull.Job): Promise { const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); if (user == null) { - done(); return; } const [path, cleanup] = await createTemp(); @@ -96,7 +95,6 @@ export class ExportAntennasProcessorService { this.logger.succ('Exported to: ' + driveFile.id); } finally { cleanup(); - done(); } } } diff --git a/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts b/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts index c7b54070d6..eb758e162d 100644 --- a/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts @@ -9,10 +9,10 @@ import type Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; import { createTemp } from '@/misc/create-temp.js'; import { UtilityService } from '@/core/UtilityService.js'; +import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; -import type Bull from 'bull'; +import type * as Bull from 'bullmq'; import type { DbJobDataWithUser } from '../types.js'; -import { bindThis } from '@/decorators.js'; @Injectable() export class ExportBlockingProcessorService { @@ -36,12 +36,11 @@ export class ExportBlockingProcessorService { } @bindThis - public async process(job: Bull.Job, done: () => void): Promise { + public async process(job: Bull.Job): Promise { this.logger.info(`Exporting blocking of ${job.data.user.id} ...`); const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); if (user == null) { - done(); return; } @@ -69,7 +68,7 @@ export class ExportBlockingProcessorService { }); if (blockings.length === 0) { - job.progress(100); + job.updateProgress(100); break; } @@ -99,7 +98,7 @@ export class ExportBlockingProcessorService { blockerId: user.id, }); - job.progress(exportedCount / total); + job.updateProgress(exportedCount / total); } stream.end(); @@ -112,7 +111,5 @@ export class ExportBlockingProcessorService { } finally { cleanup(); } - - done(); } } diff --git a/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts b/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts index b50f373ef8..3203d9f3e5 100644 --- a/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts @@ -13,7 +13,7 @@ import { createTemp, createTempDir } from '@/misc/create-temp.js'; import { DownloadService } from '@/core/DownloadService.js'; import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; -import type Bull from 'bull'; +import type * as Bull from 'bullmq'; @Injectable() export class ExportCustomEmojisProcessorService { @@ -37,12 +37,11 @@ export class ExportCustomEmojisProcessorService { } @bindThis - public async process(job: Bull.Job, done: () => void): Promise { + public async process(job: Bull.Job): Promise { this.logger.info('Exporting custom emojis ...'); const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); if (user == null) { - done(); return; } @@ -117,24 +116,26 @@ export class ExportCustomEmojisProcessorService { metaStream.end(); // Create archive - const [archivePath, archiveCleanup] = await createTemp(); - const archiveStream = fs.createWriteStream(archivePath); - const archive = archiver('zip', { - zlib: { level: 0 }, - }); - archiveStream.on('close', async () => { - this.logger.succ(`Exported to: ${archivePath}`); + await new Promise(async (resolve) => { + const [archivePath, archiveCleanup] = await createTemp(); + const archiveStream = fs.createWriteStream(archivePath); + const archive = archiver('zip', { + zlib: { level: 0 }, + }); + archiveStream.on('close', async () => { + this.logger.succ(`Exported to: ${archivePath}`); - const fileName = 'custom-emojis-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.zip'; - const driveFile = await this.driveService.addFile({ user, path: archivePath, name: fileName, force: true }); + const fileName = 'custom-emojis-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.zip'; + const driveFile = await this.driveService.addFile({ user, path: archivePath, name: fileName, force: true }); - this.logger.succ(`Exported to: ${driveFile.id}`); - cleanup(); - archiveCleanup(); - done(); + this.logger.succ(`Exported to: ${driveFile.id}`); + cleanup(); + archiveCleanup(); + resolve(); + }); + archive.pipe(archiveStream); + archive.directory(path, false); + archive.finalize(); }); - archive.pipe(archiveStream); - archive.directory(path, false); - archive.finalize(); } } diff --git a/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts b/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts index f2f2383a88..76c38a6b86 100644 --- a/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts @@ -12,7 +12,7 @@ import type { Poll } from '@/models/entities/Poll.js'; import type { Note } from '@/models/entities/Note.js'; import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; -import type Bull from 'bull'; +import type * as Bull from 'bullmq'; import type { DbJobDataWithUser } from '../types.js'; @Injectable() @@ -42,12 +42,11 @@ export class ExportFavoritesProcessorService { } @bindThis - public async process(job: Bull.Job, done: () => void): Promise { + public async process(job: Bull.Job): Promise { this.logger.info(`Exporting favorites of ${job.data.user.id} ...`); const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); if (user == null) { - done(); return; } @@ -91,7 +90,7 @@ export class ExportFavoritesProcessorService { }) as (NoteFavorite & { note: Note & { user: User } })[]; if (favorites.length === 0) { - job.progress(100); + job.updateProgress(100); break; } @@ -112,7 +111,7 @@ export class ExportFavoritesProcessorService { userId: user.id, }); - job.progress(exportedFavoritesCount / total); + job.updateProgress(exportedFavoritesCount / total); } await write(']'); @@ -127,8 +126,6 @@ export class ExportFavoritesProcessorService { } finally { cleanup(); } - - done(); } } diff --git a/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts b/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts index fa9c1ac1ea..8726cb1402 100644 --- a/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts @@ -10,10 +10,10 @@ import { DriveService } from '@/core/DriveService.js'; import { createTemp } from '@/misc/create-temp.js'; import type { Following } from '@/models/entities/Following.js'; import { UtilityService } from '@/core/UtilityService.js'; +import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; -import type Bull from 'bull'; +import type * as Bull from 'bullmq'; import type { DbExportFollowingData } from '../types.js'; -import { bindThis } from '@/decorators.js'; @Injectable() export class ExportFollowingProcessorService { @@ -40,12 +40,11 @@ export class ExportFollowingProcessorService { } @bindThis - public async process(job: Bull.Job, done: () => void): Promise { + public async process(job: Bull.Job): Promise { this.logger.info(`Exporting following of ${job.data.user.id} ...`); const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); if (user == null) { - done(); return; } @@ -116,7 +115,5 @@ export class ExportFollowingProcessorService { } finally { cleanup(); } - - done(); } } diff --git a/packages/backend/src/queue/processors/ExportMutingProcessorService.ts b/packages/backend/src/queue/processors/ExportMutingProcessorService.ts index b14bf5f5b1..0f11a9e843 100644 --- a/packages/backend/src/queue/processors/ExportMutingProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportMutingProcessorService.ts @@ -9,10 +9,10 @@ import type Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; import { createTemp } from '@/misc/create-temp.js'; import { UtilityService } from '@/core/UtilityService.js'; +import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; -import type Bull from 'bull'; +import type * as Bull from 'bullmq'; import type { DbJobDataWithUser } from '../types.js'; -import { bindThis } from '@/decorators.js'; @Injectable() export class ExportMutingProcessorService { @@ -39,12 +39,11 @@ export class ExportMutingProcessorService { } @bindThis - public async process(job: Bull.Job, done: () => void): Promise { + public async process(job: Bull.Job): Promise { this.logger.info(`Exporting muting of ${job.data.user.id} ...`); const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); if (user == null) { - done(); return; } @@ -73,7 +72,7 @@ export class ExportMutingProcessorService { }); if (mutes.length === 0) { - job.progress(100); + job.updateProgress(100); break; } @@ -103,7 +102,7 @@ export class ExportMutingProcessorService { muterId: user.id, }); - job.progress(exportedCount / total); + job.updateProgress(exportedCount / total); } stream.end(); @@ -116,7 +115,5 @@ export class ExportMutingProcessorService { } finally { cleanup(); } - - done(); } } diff --git a/packages/backend/src/queue/processors/ExportNotesProcessorService.ts b/packages/backend/src/queue/processors/ExportNotesProcessorService.ts index e4f12ad101..24fb331883 100644 --- a/packages/backend/src/queue/processors/ExportNotesProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportNotesProcessorService.ts @@ -12,7 +12,7 @@ import type { Poll } from '@/models/entities/Poll.js'; import type { Note } from '@/models/entities/Note.js'; import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; -import type Bull from 'bull'; +import type * as Bull from 'bullmq'; import type { DbJobDataWithUser } from '../types.js'; @Injectable() @@ -39,12 +39,11 @@ export class ExportNotesProcessorService { } @bindThis - public async process(job: Bull.Job, done: () => void): Promise { + public async process(job: Bull.Job): Promise { this.logger.info(`Exporting notes of ${job.data.user.id} ...`); const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); if (user == null) { - done(); return; } @@ -87,7 +86,7 @@ export class ExportNotesProcessorService { }) as Note[]; if (notes.length === 0) { - job.progress(100); + job.updateProgress(100); break; } @@ -108,7 +107,7 @@ export class ExportNotesProcessorService { userId: user.id, }); - job.progress(exportedNotesCount / total); + job.updateProgress(exportedNotesCount / total); } await write(']'); @@ -123,8 +122,6 @@ export class ExportNotesProcessorService { } finally { cleanup(); } - - done(); } } diff --git a/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts b/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts index 54bde44044..ec63358053 100644 --- a/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts @@ -9,10 +9,10 @@ import type Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; import { createTemp } from '@/misc/create-temp.js'; import { UtilityService } from '@/core/UtilityService.js'; +import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; -import type Bull from 'bull'; +import type * as Bull from 'bullmq'; import type { DbJobDataWithUser } from '../types.js'; -import { bindThis } from '@/decorators.js'; @Injectable() export class ExportUserListsProcessorService { @@ -39,12 +39,11 @@ export class ExportUserListsProcessorService { } @bindThis - public async process(job: Bull.Job, done: () => void): Promise { + public async process(job: Bull.Job): Promise { this.logger.info(`Exporting user lists of ${job.data.user.id} ...`); const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); if (user == null) { - done(); return; } @@ -92,7 +91,5 @@ export class ExportUserListsProcessorService { } finally { cleanup(); } - - done(); } } diff --git a/packages/backend/src/queue/processors/ImportAntennasProcessorService.ts b/packages/backend/src/queue/processors/ImportAntennasProcessorService.ts index d06131b8c8..575cad69d5 100644 --- a/packages/backend/src/queue/processors/ImportAntennasProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportAntennasProcessorService.ts @@ -8,7 +8,7 @@ import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import { DBAntennaImportJobData } from '../types.js'; -import type Bull from 'bull'; +import type * as Bull from 'bullmq'; const validate = new Ajv().compile({ type: 'object', @@ -59,7 +59,7 @@ export class ImportAntennasProcessorService { } @bindThis - public async process(job: Bull.Job, done: () => void): Promise { + public async process(job: Bull.Job): Promise { const now = new Date(); try { for (const antenna of job.data.antenna) { @@ -89,8 +89,6 @@ export class ImportAntennasProcessorService { } } catch (err: any) { this.logger.error(err); - } finally { - done(); } } } diff --git a/packages/backend/src/queue/processors/ImportBlockingProcessorService.ts b/packages/backend/src/queue/processors/ImportBlockingProcessorService.ts index 3f075b02d2..2f1a9e5b03 100644 --- a/packages/backend/src/queue/processors/ImportBlockingProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportBlockingProcessorService.ts @@ -7,11 +7,11 @@ import * as Acct from '@/misc/acct.js'; import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js'; import { DownloadService } from '@/core/DownloadService.js'; import { UtilityService } from '@/core/UtilityService.js'; -import { QueueLoggerService } from '../QueueLoggerService.js'; -import type Bull from 'bull'; -import type { DbUserImportJobData, DbUserImportToDbJobData } from '../types.js'; import { bindThis } from '@/decorators.js'; import { QueueService } from '@/core/QueueService.js'; +import { QueueLoggerService } from '../QueueLoggerService.js'; +import type * as Bull from 'bullmq'; +import type { DbUserImportJobData, DbUserImportToDbJobData } from '../types.js'; @Injectable() export class ImportBlockingProcessorService { @@ -34,12 +34,11 @@ export class ImportBlockingProcessorService { } @bindThis - public async process(job: Bull.Job, done: () => void): Promise { + public async process(job: Bull.Job): Promise { this.logger.info(`Importing blocking of ${job.data.user.id} ...`); const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); if (user == null) { - done(); return; } @@ -47,7 +46,6 @@ export class ImportBlockingProcessorService { id: job.data.fileId, }); if (file == null) { - done(); return; } @@ -56,7 +54,6 @@ export class ImportBlockingProcessorService { this.queueService.createImportBlockingToDbJob({ id: user.id }, targets); this.logger.succ('Import jobs created'); - done(); } @bindThis @@ -85,7 +82,7 @@ export class ImportBlockingProcessorService { } if (target == null) { - throw `Unable to resolve user: @${username}@${host}`; + throw new Error(`Unable to resolve user: @${username}@${host}`); } // skip myself diff --git a/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts b/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts index 600468a286..d862567871 100644 --- a/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts @@ -12,7 +12,7 @@ import { DriveService } from '@/core/DriveService.js'; import { DownloadService } from '@/core/DownloadService.js'; import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; -import type Bull from 'bull'; +import type * as Bull from 'bullmq'; import type { DbUserImportJobData } from '../types.js'; // TODO: 名前衝突時の動作を選べるようにする @@ -45,14 +45,13 @@ export class ImportCustomEmojisProcessorService { } @bindThis - public async process(job: Bull.Job, done: () => void): Promise { + public async process(job: Bull.Job): Promise { this.logger.info('Importing custom emojis ...'); const file = await this.driveFilesRepository.findOneBy({ id: job.data.fileId, }); if (file == null) { - done(); return; } @@ -116,7 +115,6 @@ export class ImportCustomEmojisProcessorService { cleanup(); this.logger.succ('Imported'); - done(); }); unzipStream.pipe(extractor); this.logger.succ(`Unzipping to ${outputPath}`); diff --git a/packages/backend/src/queue/processors/ImportFollowingProcessorService.ts b/packages/backend/src/queue/processors/ImportFollowingProcessorService.ts index aa5cf12c50..15bee9672e 100644 --- a/packages/backend/src/queue/processors/ImportFollowingProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportFollowingProcessorService.ts @@ -7,11 +7,11 @@ import * as Acct from '@/misc/acct.js'; import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js'; import { DownloadService } from '@/core/DownloadService.js'; import { UtilityService } from '@/core/UtilityService.js'; -import { QueueLoggerService } from '../QueueLoggerService.js'; -import type Bull from 'bull'; -import type { DbUserImportJobData, DbUserImportToDbJobData } from '../types.js'; import { bindThis } from '@/decorators.js'; import { QueueService } from '@/core/QueueService.js'; +import { QueueLoggerService } from '../QueueLoggerService.js'; +import type * as Bull from 'bullmq'; +import type { DbUserImportJobData, DbUserImportToDbJobData } from '../types.js'; @Injectable() export class ImportFollowingProcessorService { @@ -34,12 +34,11 @@ export class ImportFollowingProcessorService { } @bindThis - public async process(job: Bull.Job, done: () => void): Promise { + public async process(job: Bull.Job): Promise { this.logger.info(`Importing following of ${job.data.user.id} ...`); const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); if (user == null) { - done(); return; } @@ -47,7 +46,6 @@ export class ImportFollowingProcessorService { id: job.data.fileId, }); if (file == null) { - done(); return; } @@ -56,7 +54,6 @@ export class ImportFollowingProcessorService { this.queueService.createImportFollowingToDbJob({ id: user.id }, targets); this.logger.succ('Import jobs created'); - done(); } @bindThis @@ -85,7 +82,7 @@ export class ImportFollowingProcessorService { } if (target == null) { - throw `Unable to resolve user: @${username}@${host}`; + throw new Error(`Unable to resolve user: @${username}@${host}`); } // skip myself diff --git a/packages/backend/src/queue/processors/ImportMutingProcessorService.ts b/packages/backend/src/queue/processors/ImportMutingProcessorService.ts index 379994ee79..723935cd31 100644 --- a/packages/backend/src/queue/processors/ImportMutingProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportMutingProcessorService.ts @@ -9,10 +9,10 @@ import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js'; import { DownloadService } from '@/core/DownloadService.js'; import { UserMutingService } from '@/core/UserMutingService.js'; import { UtilityService } from '@/core/UtilityService.js'; +import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; -import type Bull from 'bull'; +import type * as Bull from 'bullmq'; import type { DbUserImportJobData } from '../types.js'; -import { bindThis } from '@/decorators.js'; @Injectable() export class ImportMutingProcessorService { @@ -38,12 +38,11 @@ export class ImportMutingProcessorService { } @bindThis - public async process(job: Bull.Job, done: () => void): Promise { + public async process(job: Bull.Job): Promise { this.logger.info(`Importing muting of ${job.data.user.id} ...`); const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); if (user == null) { - done(); return; } @@ -51,7 +50,6 @@ export class ImportMutingProcessorService { id: job.data.fileId, }); if (file == null) { - done(); return; } @@ -83,7 +81,7 @@ export class ImportMutingProcessorService { } if (target == null) { - throw `cannot resolve user: @${username}@${host}`; + throw new Error(`cannot resolve user: @${username}@${host}`); } // skip myself @@ -98,6 +96,5 @@ export class ImportMutingProcessorService { } this.logger.succ('Imported'); - done(); } } diff --git a/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts b/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts index c423863410..824ee8157a 100644 --- a/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts @@ -12,7 +12,7 @@ import { IdService } from '@/core/IdService.js'; import { UtilityService } from '@/core/UtilityService.js'; import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; -import type Bull from 'bull'; +import type * as Bull from 'bullmq'; import type { DbUserImportJobData } from '../types.js'; @Injectable() @@ -46,12 +46,11 @@ export class ImportUserListsProcessorService { } @bindThis - public async process(job: Bull.Job, done: () => void): Promise { + public async process(job: Bull.Job): Promise { this.logger.info(`Importing user lists of ${job.data.user.id} ...`); const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); if (user == null) { - done(); return; } @@ -59,7 +58,6 @@ export class ImportUserListsProcessorService { id: job.data.fileId, }); if (file == null) { - done(); return; } @@ -109,6 +107,5 @@ export class ImportUserListsProcessorService { } this.logger.succ('Imported'); - done(); } } diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts index ab8b1e9e22..ce1d7aaa1b 100644 --- a/packages/backend/src/queue/processors/InboxProcessorService.ts +++ b/packages/backend/src/queue/processors/InboxProcessorService.ts @@ -1,8 +1,8 @@ import { URL } from 'node:url'; import { Inject, Injectable } from '@nestjs/common'; import httpSignature from '@peertube/http-signature'; +import * as Bull from 'bullmq'; import { DI } from '@/di-symbols.js'; -import type { InstancesRepository, DriveFilesRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import { MetaService } from '@/core/MetaService.js'; @@ -23,10 +23,8 @@ import { LdSignatureService } from '@/core/activitypub/LdSignatureService.js'; import { ApInboxService } from '@/core/activitypub/ApInboxService.js'; import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; -import type Bull from 'bull'; import type { InboxJobData } from '../types.js'; -// ユーザーのinboxにアクティビティが届いた時の処理 @Injectable() export class InboxProcessorService { private logger: Logger; @@ -35,12 +33,6 @@ export class InboxProcessorService { @Inject(DI.config) private config: Config, - @Inject(DI.instancesRepository) - private instancesRepository: InstancesRepository, - - @Inject(DI.driveFilesRepository) - private driveFilesRepository: DriveFilesRepository, - private utilityService: UtilityService, private metaService: MetaService, private apInboxService: ApInboxService, @@ -93,24 +85,24 @@ export class InboxProcessorService { try { authUser = await this.apDbResolverService.getAuthUserFromApId(getApId(activity.actor)); } catch (err) { - // 対象が4xxならスキップ + // 対象が4xxならスキップ if (err instanceof StatusError) { if (err.isClientError) { - return `skip: Ignored deleted actors on both ends ${activity.actor} - ${err.statusCode}`; + throw new Bull.UnrecoverableError(`skip: Ignored deleted actors on both ends ${activity.actor} - ${err.statusCode}`); } - throw `Error in actor ${activity.actor} - ${err.statusCode ?? err}`; + throw new Error(`Error in actor ${activity.actor} - ${err.statusCode ?? err}`); } } } // それでもわからなければ終了 if (authUser == null) { - return 'skip: failed to resolve user'; + throw new Bull.UnrecoverableError('skip: failed to resolve user'); } // publicKey がなくても終了 if (authUser.key == null) { - return 'skip: failed to resolve user publicKey'; + throw new Bull.UnrecoverableError('skip: failed to resolve user publicKey'); } // HTTP-Signatureの検証 @@ -118,10 +110,10 @@ export class InboxProcessorService { // また、signatureのsignerは、activity.actorと一致する必要がある if (!httpSignatureValidated || authUser.user.uri !== activity.actor) { - // 一致しなくても、でもLD-Signatureがありそうならそっちも見る + // 一致しなくても、でもLD-Signatureがありそうならそっちも見る if (activity.signature) { if (activity.signature.type !== 'RsaSignature2017') { - return `skip: unsupported LD-signature type ${activity.signature.type}`; + throw new Bull.UnrecoverableError(`skip: unsupported LD-signature type ${activity.signature.type}`); } // activity.signature.creator: https://example.oom/users/user#main-key @@ -134,32 +126,32 @@ export class InboxProcessorService { // keyIdからLD-Signatureのユーザーを取得 authUser = await this.apDbResolverService.getAuthUserFromKeyId(activity.signature.creator); if (authUser == null) { - return 'skip: LD-Signatureのユーザーが取得できませんでした'; + throw new Bull.UnrecoverableError('skip: LD-Signatureのユーザーが取得できませんでした'); } if (authUser.key == null) { - return 'skip: LD-SignatureのユーザーはpublicKeyを持っていませんでした'; + throw new Bull.UnrecoverableError('skip: LD-SignatureのユーザーはpublicKeyを持っていませんでした'); } // LD-Signature検証 const ldSignature = this.ldSignatureService.use(); const verified = await ldSignature.verifyRsaSignature2017(activity, authUser.key.keyPem).catch(() => false); if (!verified) { - return 'skip: LD-Signatureの検証に失敗しました'; + throw new Bull.UnrecoverableError('skip: LD-Signatureの検証に失敗しました'); } // もう一度actorチェック if (authUser.user.uri !== activity.actor) { - return `skip: LD-Signature user(${authUser.user.uri}) !== activity.actor(${activity.actor})`; + throw new Bull.UnrecoverableError(`skip: LD-Signature user(${authUser.user.uri}) !== activity.actor(${activity.actor})`); } // ブロックしてたら中断 const ldHost = this.utilityService.extractDbHost(authUser.user.uri); if (this.utilityService.isBlockedHost(meta.blockedHosts, ldHost)) { - return `Blocked request: ${ldHost}`; + throw new Bull.UnrecoverableError(`Blocked request: ${ldHost}`); } } else { - return `skip: http-signature verification failed and no LD-Signature. keyId=${signature.keyId}`; + throw new Bull.UnrecoverableError(`skip: http-signature verification failed and no LD-Signature. keyId=${signature.keyId}`); } } @@ -168,7 +160,7 @@ export class InboxProcessorService { const signerHost = this.utilityService.extractDbHost(authUser.user.uri!); const activityIdHost = this.utilityService.extractDbHost(activity.id); if (signerHost !== activityIdHost) { - return `skip: signerHost(${signerHost}) !== activity.id host(${activityIdHost}`; + throw new Bull.UnrecoverableError(`skip: signerHost(${signerHost}) !== activity.id host(${activityIdHost}`); } } diff --git a/packages/backend/src/queue/processors/RelationshipProcessorService.ts b/packages/backend/src/queue/processors/RelationshipProcessorService.ts index ff454df455..722260d948 100644 --- a/packages/backend/src/queue/processors/RelationshipProcessorService.ts +++ b/packages/backend/src/queue/processors/RelationshipProcessorService.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import type Bull from 'bull'; +import type * as Bull from 'bullmq'; import { UserFollowingService } from '@/core/UserFollowingService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js'; diff --git a/packages/backend/src/queue/processors/ResyncChartsProcessorService.ts b/packages/backend/src/queue/processors/ResyncChartsProcessorService.ts index e5840f3da8..eab8e1e68d 100644 --- a/packages/backend/src/queue/processors/ResyncChartsProcessorService.ts +++ b/packages/backend/src/queue/processors/ResyncChartsProcessorService.ts @@ -15,7 +15,7 @@ import PerUserDriveChart from '@/core/chart/charts/per-user-drive.js'; import ApRequestChart from '@/core/chart/charts/ap-request.js'; import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; -import type Bull from 'bull'; +import type * as Bull from 'bullmq'; @Injectable() export class ResyncChartsProcessorService { @@ -43,7 +43,7 @@ export class ResyncChartsProcessorService { } @bindThis - public async process(job: Bull.Job>, done: () => void): Promise { + public async process(): Promise { this.logger.info('Resync charts...'); // TODO: ユーザーごとのチャートも更新する @@ -55,6 +55,5 @@ export class ResyncChartsProcessorService { ]); this.logger.succ('All charts successfully resynced.'); - done(); } } diff --git a/packages/backend/src/queue/processors/TickChartsProcessorService.ts b/packages/backend/src/queue/processors/TickChartsProcessorService.ts index 7ff84c15a5..f1696bf567 100644 --- a/packages/backend/src/queue/processors/TickChartsProcessorService.ts +++ b/packages/backend/src/queue/processors/TickChartsProcessorService.ts @@ -16,7 +16,7 @@ import PerUserDriveChart from '@/core/chart/charts/per-user-drive.js'; import ApRequestChart from '@/core/chart/charts/ap-request.js'; import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; -import type Bull from 'bull'; +import type * as Bull from 'bullmq'; @Injectable() export class TickChartsProcessorService { @@ -45,7 +45,7 @@ export class TickChartsProcessorService { } @bindThis - public async process(job: Bull.Job>, done: () => void): Promise { + public async process(): Promise { this.logger.info('Tick charts...'); await Promise.all([ @@ -64,6 +64,5 @@ export class TickChartsProcessorService { ]); this.logger.succ('All charts successfully ticked.'); - done(); } } diff --git a/packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts b/packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts index 84a5c21c49..8b40c16749 100644 --- a/packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts +++ b/packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts @@ -1,4 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; +import * as Bull from 'bullmq'; import { DI } from '@/di-symbols.js'; import type { WebhooksRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; @@ -7,7 +8,6 @@ import { HttpRequestService } from '@/core/HttpRequestService.js'; import { StatusError } from '@/misc/status-error.js'; import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; -import type Bull from 'bull'; import type { WebhookDeliverJobData } from '../types.js'; @Injectable() @@ -66,11 +66,11 @@ export class WebhookDeliverProcessorService { if (res instanceof StatusError) { // 4xx if (res.isClientError) { - return `${res.statusCode} ${res.statusMessage}`; + throw new Bull.UnrecoverableError(`${res.statusCode} ${res.statusMessage}`); } // 5xx etc. - throw `${res.statusCode} ${res.statusMessage}`; + throw new Error(`${res.statusCode} ${res.statusMessage}`); } else { // DNS error, socket error, timeout ... throw res; diff --git a/packages/backend/src/server/api/endpoints/admin/relays/add.ts b/packages/backend/src/server/api/endpoints/admin/relays/add.ts index f12738bd3a..f2d4aa8996 100644 --- a/packages/backend/src/server/api/endpoints/admin/relays/add.ts +++ b/packages/backend/src/server/api/endpoints/admin/relays/add.ts @@ -62,7 +62,7 @@ export default class extends Endpoint { ) { super(meta, paramDef, async (ps, me) => { try { - if (new URL(ps.inbox).protocol !== 'https:') throw 'https only'; + if (new URL(ps.inbox).protocol !== 'https:') throw new Error('https only'); } catch { throw new ApiError(meta.errors.invalidUrl); } diff --git a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts index 2956bf1cbd..742df0ca95 100644 --- a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts +++ b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts @@ -82,14 +82,14 @@ export default class extends Endpoint { try { if (ps.tag) { - if (!safeForSql(normalizeForSearch(ps.tag))) throw 'Injection'; + if (!safeForSql(normalizeForSearch(ps.tag))) throw new Error('Injection'); query.andWhere(`'{"${normalizeForSearch(ps.tag)}"}' <@ note.tags`); } else { query.andWhere(new Brackets(qb => { for (const tags of ps.query!) { qb.orWhere(new Brackets(qb => { for (const tag of tags) { - if (!safeForSql(normalizeForSearch(tag))) throw 'Injection'; + if (!safeForSql(normalizeForSearch(tag))) throw new Error('Injection'); qb.andWhere(`'{"${normalizeForSearch(tag)}"}' <@ note.tags`); } })); diff --git a/packages/backend/src/server/api/endpoints/reset-db.ts b/packages/backend/src/server/api/endpoints/reset-db.ts index 4ced6d3ff1..1d4825f812 100644 --- a/packages/backend/src/server/api/endpoints/reset-db.ts +++ b/packages/backend/src/server/api/endpoints/reset-db.ts @@ -34,7 +34,7 @@ export default class extends Endpoint { private redisClient: Redis.Redis, ) { super(meta, paramDef, async (ps, me) => { - if (process.env.NODE_ENV !== 'test') throw 'NODE_ENV is not a test'; + if (process.env.NODE_ENV !== 'test') throw new Error('NODE_ENV is not a test'); await redisClient.flushdb(); await resetDb(this.db); diff --git a/packages/backend/test/misc/mock-resolver.ts b/packages/backend/test/misc/mock-resolver.ts index 6b31e68616..a7bcd859ae 100644 --- a/packages/backend/test/misc/mock-resolver.ts +++ b/packages/backend/test/misc/mock-resolver.ts @@ -52,11 +52,7 @@ export class MockResolver extends Resolver { const r = this._rs.get(value); if (!r) { - throw { - name: 'StatusError', - statusCode: 404, - message: 'Not registed for mock', - }; + throw new Error('Not registed for mock'); } const object = JSON.parse(r.content); diff --git a/packages/backend/test/utils.ts b/packages/backend/test/utils.ts index 1a4a0b3545..22f7d81e4e 100644 --- a/packages/backend/test/utils.ts +++ b/packages/backend/test/utils.ts @@ -478,7 +478,7 @@ export async function testPaginationConsistency=12'} + /bullmq@3.14.1: + resolution: {integrity: sha512-Fom78UKljYsnJmwbROVPx3eFLuVfQjQbw9KCnVupLzT31RQHhFHV2xd/4J4oWl4u34bZ1JmEUfNnqNBz+IOJuA==} dependencies: - cron-parser: 4.7.1 - debuglog: 1.0.1 - get-port: 5.1.1 + cron-parser: 4.8.1 + glob: 8.1.0 ioredis: 5.3.2 lodash: 4.17.21 - msgpackr: 1.8.1 + msgpackr: 1.9.2 semver: 7.5.1 - uuid: 8.3.2 + tslib: 2.5.2 + uuid: 9.0.0 transitivePeerDependencies: - supports-color + dev: false /busboy@1.6.0: resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} @@ -9306,6 +9302,7 @@ packages: /cluster-key-slot@1.1.2: resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} engines: {node: '>=0.10.0'} + dev: false /co@4.6.0: resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} @@ -9607,11 +9604,12 @@ packages: readable-stream: 3.6.0 dev: false - /cron-parser@4.7.1: - resolution: {integrity: sha512-WguFaoQ0hQ61SgsCZLHUcNbAvlK0lypKXu62ARguefYmjzaOXIVRNrAmyXzabTwUn4sQvQLkk6bjH+ipGfw8bA==} + /cron-parser@4.8.1: + resolution: {integrity: sha512-jbokKWGcyU4gl6jAfX97E1gDpY12DJ1cLJZmoDzaAln/shZ+S3KBFBuA2Q6WeUN4gJf/8klnV1EfvhA2lK5IRQ==} engines: {node: '>=12.0.0'} dependencies: - luxon: 3.2.1 + luxon: 3.3.0 + dev: false /cropperjs@2.0.0-beta.2: resolution: {integrity: sha512-jDRSODDGKmi9vp3p/+WXkxMqV/AE+GpSld1U3cHZDRdLy9UykRzurSe8k1dR0TExn45ygCMrv31qkg+K3EeXXw==} @@ -9890,9 +9888,6 @@ packages: ms: 2.1.2 supports-color: 8.1.1 - /debuglog@1.0.1: - resolution: {integrity: sha512-syBZ+rnAK3EgMsH2aYEOLUW7mZSY9Gb+0wUMCFsZvcmiz+HigA0LOcq/HoQqVuGG+EKykunc7QG2bzrponfaSw==} - /decamelize-keys@1.1.1: resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==} engines: {node: '>=0.10.0'} @@ -10084,6 +10079,7 @@ packages: /denque@2.1.0: resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} engines: {node: '>=0.10'} + dev: false /depd@1.1.2: resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==} @@ -11753,6 +11749,7 @@ packages: /get-port@5.1.1: resolution: {integrity: sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==} engines: {node: '>=8'} + dev: true /get-stream@3.0.0: resolution: {integrity: sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==} @@ -12606,6 +12603,7 @@ packages: standard-as-callback: 2.1.0 transitivePeerDependencies: - supports-color + dev: false /iota-array@1.0.0: resolution: {integrity: sha512-pZ2xT+LOHckCatGQ3DcG/a+QuEqvoxqkiL7tvE8nn3uuu+f6i1TtpB5/FtWFbxUuVr5PZCx8KskuGatbJDXOWA==} @@ -14288,6 +14286,7 @@ packages: /lodash.defaults@4.2.0: resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + dev: false /lodash.difference@4.5.0: resolution: {integrity: sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==} @@ -14319,6 +14318,7 @@ packages: /lodash.isarguments@3.1.0: resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} + dev: false /lodash.isempty@4.4.0: resolution: {integrity: sha512-oKMuF3xEeqDltrGMfDxAPGIVMSSRv8tbRSODbrs4KGsRRLEhrW8N8Rd4DRgB2+621hY8A8XwwrTVhXWpxFvMzg==} @@ -14472,9 +14472,10 @@ packages: engines: {node: '>=16.14'} dev: true - /luxon@3.2.1: - resolution: {integrity: sha512-QrwPArQCNLAKGO/C+ZIilgIuDnEnKx5QYODdDtbFaxzsbZcc/a7WFq7MhsVYgRlwawLtvOUESTlfJ+hc/USqPg==} + /luxon@3.3.0: + resolution: {integrity: sha512-An0UCfG/rSiqtAIiBPO0Y9/zAnHUZxAMiCpTd5h2smgsj7GGmcenvrvww2cqNA8/4A5ZrD1gJpHN2mIHZQF+Mg==} engines: {node: '>=12'} + dev: false /lz-string@1.5.0: resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} @@ -14950,24 +14951,27 @@ packages: engines: {node: '>=12.13'} dev: false - /msgpackr-extract@2.2.0: - resolution: {integrity: sha512-0YcvWSv7ZOGl9Od6Y5iJ3XnPww8O7WLcpYMDwX+PAA/uXLDtyw94PJv9GLQV/nnp3cWlDhMoyKZIQLrx33sWog==} + /msgpackr-extract@3.0.2: + resolution: {integrity: sha512-SdzXp4kD/Qf8agZ9+iTu6eql0m3kWm1A2y1hkpTeVNENutaB0BwHlSvAIaMxwntmRUAUjon2V4L8Z/njd0Ct8A==} + hasBin: true requiresBuild: true dependencies: - node-gyp-build-optional-packages: 5.0.3 + node-gyp-build-optional-packages: 5.0.7 optionalDependencies: - '@msgpackr-extract/msgpackr-extract-darwin-arm64': 2.2.0 - '@msgpackr-extract/msgpackr-extract-darwin-x64': 2.2.0 - '@msgpackr-extract/msgpackr-extract-linux-arm': 2.2.0 - '@msgpackr-extract/msgpackr-extract-linux-arm64': 2.2.0 - '@msgpackr-extract/msgpackr-extract-linux-x64': 2.2.0 - '@msgpackr-extract/msgpackr-extract-win32-x64': 2.2.0 + '@msgpackr-extract/msgpackr-extract-darwin-arm64': 3.0.2 + '@msgpackr-extract/msgpackr-extract-darwin-x64': 3.0.2 + '@msgpackr-extract/msgpackr-extract-linux-arm': 3.0.2 + '@msgpackr-extract/msgpackr-extract-linux-arm64': 3.0.2 + '@msgpackr-extract/msgpackr-extract-linux-x64': 3.0.2 + '@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.2 + dev: false optional: true - /msgpackr@1.8.1: - resolution: {integrity: sha512-05fT4J8ZqjYlR4QcRDIhLCYKUOHXk7C/xa62GzMKj74l3up9k2QZ3LgFc6qWdsPHl91QA2WLWqWc8b8t7GLNNw==} + /msgpackr@1.9.2: + resolution: {integrity: sha512-xtDgI3Xv0AAiZWLRGDchyzBwU6aq0rwJ+W+5Y4CZhEWtkl/hJtFFLc+3JtGTw7nz1yquxs7nL8q/yA2aqpflIQ==} optionalDependencies: - msgpackr-extract: 2.2.0 + msgpackr-extract: 3.0.2 + dev: false /msw-storybook-addon@1.8.0(msw@1.2.1): resolution: {integrity: sha512-dw3vZwqjixmiur0vouRSOax7wPSu9Og2Hspy9JZFHf49bZRjwDiLF0Pfn2NXEkGviYJOJiGxS1ejoTiUwoSg4A==} @@ -15219,8 +15223,10 @@ packages: fetch-blob: 3.2.0 formdata-polyfill: 4.0.10 - /node-gyp-build-optional-packages@5.0.3: - resolution: {integrity: sha512-k75jcVzk5wnnc/FMxsf4udAoTEUv2jY3ycfdSd3yWu6Cnd1oee6/CfZJApyscA4FJOmdoixWwiwOyf16RzD5JA==} + /node-gyp-build-optional-packages@5.0.7: + resolution: {integrity: sha512-YlCCc6Wffkx0kHkmam79GKvDQ6x+QZkMjFGrIMxgFNILFvGSbCp2fCBC55pGTT9gVaz8Na5CLmxt/urtzRv36w==} + hasBin: true + dev: false optional: true /node-gyp-build@4.6.0: @@ -17252,6 +17258,7 @@ packages: /redis-errors@1.2.0: resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} engines: {node: '>=4'} + dev: false /redis-info@3.1.0: resolution: {integrity: sha512-ER4L9Sh/vm63DkIE0bkSjxluQlioBiBgf5w1UuldaW/3vPcecdljVDisZhmnCMvsxHNiARTTDDHGg9cGwTfrKg==} @@ -17269,6 +17276,7 @@ packages: engines: {node: '>=4'} dependencies: redis-errors: 1.2.0 + dev: false /redis@4.5.1: resolution: {integrity: sha512-oxXSoIqMJCQVBTfxP6BNTCtDMyh9G6Vi5wjdPdV/sRKkufyZslDqCScSGcOr6XGR/reAWZefz7E4leM31RgdBA==} @@ -18395,6 +18403,7 @@ packages: /standard-as-callback@2.1.0: resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + dev: false /start-server-and-test@2.0.0: resolution: {integrity: sha512-UqKLw0mJbfrsG1jcRLTUlvuRi9sjNuUiDOLI42r7R5fA9dsFoywAy9DoLXNYys9B886E4RCKb+qM1Gzu96h7DQ==} -- cgit v1.2.3-freya From 8f14b42e81ba891744cad19e4c6aa0d2d30f5e53 Mon Sep 17 00:00:00 2001 From: syuilo Date: Tue, 30 May 2023 11:51:29 +0900 Subject: perf: インスタンスデフォルトテーマを予めjson5 -> jsonに変換しておくことでjson5を初期バンドルに含めずに済むように MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/server/api/endpoints/meta.ts | 6 ++++-- packages/frontend/src/boot/common.ts | 5 ++--- pnpm-lock.yaml | 19 +------------------ 3 files changed, 7 insertions(+), 23 deletions(-) (limited to 'packages/backend/src/server/api/endpoints') diff --git a/packages/backend/src/server/api/endpoints/meta.ts b/packages/backend/src/server/api/endpoints/meta.ts index 584ea07c3b..53d724a9dd 100644 --- a/packages/backend/src/server/api/endpoints/meta.ts +++ b/packages/backend/src/server/api/endpoints/meta.ts @@ -1,5 +1,6 @@ import { IsNull, LessThanOrEqual, MoreThan } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; +import * as JSON5 from 'json5'; import type { AdsRepository, UsersRepository } from '@/models/index.js'; import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -292,8 +293,9 @@ export default class extends Endpoint { backgroundImageUrl: instance.backgroundImageUrl, logoImageUrl: instance.logoImageUrl, maxNoteTextLength: MAX_NOTE_TEXT_LENGTH, - defaultLightTheme: instance.defaultLightTheme, - defaultDarkTheme: instance.defaultDarkTheme, + // クライアントの手間を減らすためあらかじめJSONに変換しておく + defaultLightTheme: instance.defaultLightTheme ? JSON.stringify(JSON5.parse(instance.defaultLightTheme)) : null, + defaultDarkTheme: instance.defaultDarkTheme ? JSON.stringify(JSON5.parse(instance.defaultDarkTheme)) : null, ads: ads.map(ad => ({ id: ad.id, url: ad.url, diff --git a/packages/frontend/src/boot/common.ts b/packages/frontend/src/boot/common.ts index e9d6586f85..e1b12fe7d6 100644 --- a/packages/frontend/src/boot/common.ts +++ b/packages/frontend/src/boot/common.ts @@ -1,6 +1,5 @@ import { computed, createApp, watch, markRaw, version as vueVersion, defineAsyncComponent, App } from 'vue'; import { compareVersions } from 'compare-versions'; -import JSON5 from 'json5'; import widgets from '@/widgets'; import directives from '@/directives'; import components from '@/components'; @@ -180,8 +179,8 @@ export async function common(createVue: () => App) { fetchInstanceMetaPromise.then(() => { if (defaultStore.state.themeInitial) { - if (instance.defaultLightTheme != null) ColdDeviceStorage.set('lightTheme', JSON5.parse(instance.defaultLightTheme)); - if (instance.defaultDarkTheme != null) ColdDeviceStorage.set('darkTheme', JSON5.parse(instance.defaultDarkTheme)); + if (instance.defaultLightTheme != null) ColdDeviceStorage.set('lightTheme', JSON.parse(instance.defaultLightTheme)); + if (instance.defaultDarkTheme != null) ColdDeviceStorage.set('darkTheme', JSON.parse(instance.defaultDarkTheme)); defaultStore.set('themeInitial', false); } }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4a1219a74b..711e0dc0ab 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5535,7 +5535,7 @@ packages: ts-dedent: 2.2.0 util-deprecate: 1.0.2 watchpack: 2.4.0 - ws: 8.13.0 + ws: 8.13.0(bufferutil@4.0.7)(utf-8-validate@6.0.3) transitivePeerDependencies: - bufferutil - encoding @@ -8702,7 +8702,6 @@ packages: requiresBuild: true dependencies: node-gyp-build: 4.6.0 - dev: false /bullmq@3.14.1: resolution: {integrity: sha512-Fom78UKljYsnJmwbROVPx3eFLuVfQjQbw9KCnVupLzT31RQHhFHV2xd/4J4oWl4u34bZ1JmEUfNnqNBz+IOJuA==} @@ -15259,7 +15258,6 @@ packages: /node-gyp-build@4.6.0: resolution: {integrity: sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==} hasBin: true - dev: false /node-gyp@9.3.1: resolution: {integrity: sha512-4Q16ZCqq3g8awk6UplT7AuxQ35XN4R/yf/+wSAwcBUAjg7l58RTactWaP8fIDTi0FzI7YcVLujwExakZlfWkXg==} @@ -19676,7 +19674,6 @@ packages: requiresBuild: true dependencies: node-gyp-build: 4.6.0 - dev: false /util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -20326,19 +20323,6 @@ packages: async-limiter: 1.0.1 dev: true - /ws@8.13.0: - resolution: {integrity: sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - dev: true - /ws@8.13.0(bufferutil@4.0.7)(utf-8-validate@6.0.3): resolution: {integrity: sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==} engines: {node: '>=10.0.0'} @@ -20353,7 +20337,6 @@ packages: dependencies: bufferutil: 4.0.7 utf-8-validate: 6.0.3 - dev: false /xev@3.0.2: resolution: {integrity: sha512-8kxuH95iMXzHZj+fwqfA4UrPcYOy6bGIgfWzo9Ji23JoEc30ge/Z++Ubkiuy8c0+M64nXmmxrmJ7C8wnuBhluw==} -- cgit v1.2.3-freya From a6cd97ca9a4684e8621766e61d4171f0a1a2c3d8 Mon Sep 17 00:00:00 2001 From: syuilo Date: Tue, 30 May 2023 17:27:47 +0900 Subject: fix(backend): 「アクセストークンの管理」画面でアプリの情報が表示されない問題を修正 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix #10920 --- CHANGELOG.md | 5 +++-- packages/backend/src/server/api/endpoints/auth/accept.ts | 3 --- packages/backend/src/server/api/endpoints/i/apps.ts | 5 +++-- 3 files changed, 6 insertions(+), 7 deletions(-) (limited to 'packages/backend/src/server/api/endpoints') diff --git a/CHANGELOG.md b/CHANGELOG.md index 36bc63b0af..a61051e32b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,8 +34,9 @@ - アカウントの初期設定ウィザードにあとでボタンを追加 - Fix: URLプレビューで情報が取得できなかった際の挙動を修正 - Fix: Safari、Firefoxでの新規登録時、パスワードマネージャーにメールアドレスが登録されていた挙動を修正 -- fix:ロールタイムラインが無効でも投稿が流れてしまう問題の修正 -- fix:ロールタイムラインにて全ての投稿が流れてしまう問題の修正 +- Fix: ロールタイムラインが無効でも投稿が流れてしまう問題の修正 +- Fix: ロールタイムラインにて全ての投稿が流れてしまう問題の修正 +- Fix: 「アクセストークンの管理」画面でアプリの情報が表示されない問題の修正 ### Server - bullをbull-mqにアップグレードし、ジョブキューのパフォーマンスを改善 diff --git a/packages/backend/src/server/api/endpoints/auth/accept.ts b/packages/backend/src/server/api/endpoints/auth/accept.ts index cb2e661bfb..05842460cf 100644 --- a/packages/backend/src/server/api/endpoints/auth/accept.ts +++ b/packages/backend/src/server/api/endpoints/auth/accept.ts @@ -55,7 +55,6 @@ export default class extends Endpoint { throw new ApiError(meta.errors.noSuchSession); } - // Generate access token const accessToken = secureRndstr(32, true); // Fetch exist access token @@ -65,7 +64,6 @@ export default class extends Endpoint { }); if (exist == null) { - // Lookup app const app = await this.appsRepository.findOneByOrFail({ id: session.appId }); // Generate Hash @@ -75,7 +73,6 @@ export default class extends Endpoint { const now = new Date(); - // Insert access token doc await this.accessTokensRepository.insert({ id: this.idService.genId(), createdAt: now, diff --git a/packages/backend/src/server/api/endpoints/i/apps.ts b/packages/backend/src/server/api/endpoints/i/apps.ts index 3361e5a4d3..48fb03a8af 100644 --- a/packages/backend/src/server/api/endpoints/i/apps.ts +++ b/packages/backend/src/server/api/endpoints/i/apps.ts @@ -26,7 +26,8 @@ export default class extends Endpoint { ) { super(meta, paramDef, async (ps, me) => { const query = this.accessTokensRepository.createQueryBuilder('token') - .where('token.userId = :userId', { userId: me.id }); + .where('token.userId = :userId', { userId: me.id }) + .leftJoinAndSelect('token.app', 'app'); switch (ps.sort) { case '+createdAt': query.orderBy('token.createdAt', 'DESC'); break; @@ -40,7 +41,7 @@ export default class extends Endpoint { return await Promise.all(tokens.map(token => ({ id: token.id, - name: token.name, + name: token.name ?? token.app?.name, createdAt: token.createdAt, lastUsedAt: token.lastUsedAt, permission: token.permission, -- cgit v1.2.3-freya From ec2f05d4f79d8f9b1a866527d906db6a2a2b5ba7 Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 1 Jun 2023 13:28:43 +0900 Subject: fix(backend): i/notificationsのsinceIdが機能しない問題を修正 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix #10902 --- CHANGELOG.md | 1 + packages/backend/src/server/api/endpoints/i/notifications.ts | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) (limited to 'packages/backend/src/server/api/endpoints') diff --git a/CHANGELOG.md b/CHANGELOG.md index a53d506ef5..5b2b4784af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,7 @@ - bullをbull-mqにアップグレードし、ジョブキューのパフォーマンスを改善 - ストリーミングのパフォーマンスを改善 - Fix: お知らせの画像URLを空にできない問題を修正 +- Fix: i/notificationsのsinceIdが機能しない問題を修正 ## 13.12.2 diff --git a/packages/backend/src/server/api/endpoints/i/notifications.ts b/packages/backend/src/server/api/endpoints/i/notifications.ts index e141be764a..f5662f4a0e 100644 --- a/packages/backend/src/server/api/endpoints/i/notifications.ts +++ b/packages/backend/src/server/api/endpoints/i/notifications.ts @@ -91,18 +91,18 @@ export default class extends Endpoint { const includeTypes = ps.includeTypes && ps.includeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][]; const excludeTypes = ps.excludeTypes && ps.excludeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][]; - const limit = ps.limit + (ps.untilId ? 1 : 0); // untilIdに指定したものも含まれるため+1 + const limit = ps.limit + (ps.untilId ? 1 : 0) + (ps.sinceId ? 1 : 0); // untilIdに指定したものも含まれるため+1 const notificationsRes = await this.redisClient.xrevrange( `notificationTimeline:${me.id}`, ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : '+', - '-', + ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime() : '-', 'COUNT', limit); if (notificationsRes.length === 0) { return []; } - let notifications = notificationsRes.map(x => JSON.parse(x[1][1])).filter(x => x.id !== ps.untilId) as Notification[]; + let notifications = notificationsRes.map(x => JSON.parse(x[1][1])).filter(x => x.id !== ps.untilId && x !== ps.sinceId) as Notification[]; if (includeTypes && includeTypes.length > 0) { notifications = notifications.filter(notification => includeTypes.includes(notification.type)); -- cgit v1.2.3-freya From 91d790bbb65521eedc783aba9e863d188d30540b Mon Sep 17 00:00:00 2001 From: syuilo Date: Fri, 2 Jun 2023 11:34:38 +0900 Subject: update deps --- package.json | 8 +- packages/backend/package.json | 40 +- .../src/server/api/endpoints/i/2fa/key-done.ts | 2 +- packages/backend/src/server/web/views/base.pug | 2 +- packages/frontend/.storybook/preview-head.html | 2 +- packages/frontend/package.json | 26 +- pnpm-lock.yaml | 990 ++++++++++++--------- 7 files changed, 631 insertions(+), 439 deletions(-) (limited to 'packages/backend/src/server/api/endpoints') diff --git a/package.json b/package.json index d9e5d9f7fd..81029514c3 100644 --- a/package.json +++ b/package.json @@ -51,16 +51,16 @@ "gulp-replace": "1.1.4", "gulp-terser": "2.1.0", "js-yaml": "4.1.0", - "typescript": "5.0.4" + "typescript": "5.1.3" }, "devDependencies": { "@types/gulp": "4.0.10", "@types/gulp-rename": "2.0.1", - "@typescript-eslint/eslint-plugin": "5.59.5", - "@typescript-eslint/parser": "5.59.5", + "@typescript-eslint/eslint-plugin": "5.59.8", + "@typescript-eslint/parser": "5.59.8", "cross-env": "7.0.3", "cypress": "12.13.0", - "eslint": "8.40.0", + "eslint": "8.41.0", "start-server-and-test": "2.0.0" }, "optionalDependencies": { diff --git a/packages/backend/package.json b/packages/backend/package.json index 66bc7ee478..56ecbc2eaf 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -60,27 +60,27 @@ "@discordapp/twemoji": "14.1.2", "@fastify/accepts": "4.1.0", "@fastify/cookie": "8.3.0", - "@fastify/cors": "8.2.1", + "@fastify/cors": "8.3.0", "@fastify/http-proxy": "9.1.0", "@fastify/multipart": "7.6.0", - "@fastify/static": "6.10.1", + "@fastify/static": "6.10.2", "@fastify/view": "7.4.1", "@nestjs/common": "9.4.2", "@nestjs/core": "9.4.2", "@nestjs/testing": "9.4.2", "@peertube/http-signature": "1.7.0", - "@sinonjs/fake-timers": "10.0.2", + "@sinonjs/fake-timers": "10.2.0", "@swc/cli": "0.1.62", - "@swc/core": "1.3.59", + "@swc/core": "1.3.61", "accepts": "1.3.8", "ajv": "8.12.0", "archiver": "5.3.1", "autwh": "0.1.0", "bcryptjs": "2.4.3", "blurhash": "2.0.5", - "bullmq": "3.14.1", + "bullmq": "3.15.0", "cacheable-lookup": "6.1.0", - "cbor": "8.1.0", + "cbor": "9.0.0", "chalk": "5.2.0", "chalk-template": "0.4.0", "chokidar": "3.5.3", @@ -96,24 +96,24 @@ "fluent-ffmpeg": "2.1.2", "form-data": "4.0.0", "got": "12.6.0", - "happy-dom": "9.19.2", + "happy-dom": "9.20.3", "hpagent": "1.2.0", "ioredis": "5.3.2", "ip-cidr": "3.1.0", "is-svg": "4.3.2", "js-yaml": "4.1.0", - "jsdom": "21.1.1", + "jsdom": "22.1.0", "json5": "2.2.3", - "jsonld": "8.1.1", + "jsonld": "8.2.0", "jsrsasign": "10.8.6", - "meilisearch": "0.32.4", + "meilisearch": "0.32.5", "mfm-js": "0.23.3", "mime-types": "2.1.35", "misskey-js": "workspace:*", "ms": "3.0.0-canary.1", "nested-property": "4.0.0", "node-fetch": "3.3.1", - "nodemailer": "6.9.2", + "nodemailer": "6.9.3", "nsfwjs": "2.4.2", "oauth": "0.10.0", "os-utils": "0.0.14", @@ -129,7 +129,7 @@ "qrcode": "1.5.3", "random-seed": "0.3.0", "ratelimiter": "3.4.1", - "re2": "1.18.0", + "re2": "1.19.0", "redis-lock": "0.1.4", "reflect-metadata": "0.1.13", "rename": "1.0.4", @@ -146,16 +146,16 @@ "strict-event-emitter-types": "2.0.0", "stringz": "2.1.0", "summaly": "github:misskey-dev/summaly", - "systeminformation": "5.17.12", + "systeminformation": "5.17.16", "tinycolor2": "1.6.0", "tmp": "0.2.1", "tsc-alias": "1.8.6", "tsconfig-paths": "4.2.0", "twemoji-parser": "14.0.0", "typeorm": "0.3.16", - "typescript": "5.0.4", + "typescript": "5.1.3", "ulid": "2.3.0", - "unzipper": "0.10.11", + "unzipper": "0.10.14", "uuid": "9.0.0", "vary": "1.1.2", "web-push": "3.6.1", @@ -173,13 +173,13 @@ "@types/content-disposition": "0.5.5", "@types/escape-regexp": "0.0.1", "@types/fluent-ffmpeg": "2.1.21", - "@types/jest": "29.5.1", + "@types/jest": "29.5.2", "@types/js-yaml": "4.0.5", "@types/jsdom": "21.1.1", "@types/jsonld": "1.5.8", "@types/jsrsasign": "10.5.8", "@types/mime-types": "2.1.1", - "@types/node": "20.2.3", + "@types/node": "20.2.5", "@types/node-fetch": "3.0.3", "@types/nodemailer": "6.4.8", "@types/oauth": "0.9.1", @@ -203,11 +203,11 @@ "@types/web-push": "3.3.2", "@types/websocket": "1.0.5", "@types/ws": "8.5.4", - "@typescript-eslint/eslint-plugin": "5.59.5", - "@typescript-eslint/parser": "5.59.5", + "@typescript-eslint/eslint-plugin": "5.59.8", + "@typescript-eslint/parser": "5.59.8", "aws-sdk-client-mock": "2.1.1", "cross-env": "7.0.3", - "eslint": "8.40.0", + "eslint": "8.41.0", "eslint-plugin-import": "2.27.5", "execa": "6.1.0", "jest": "29.5.0", diff --git a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts index ad33398da6..e8985a9cd8 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts @@ -1,6 +1,6 @@ import { promisify } from 'node:util'; import bcrypt from 'bcryptjs'; -import * as cbor from 'cbor'; +import cbor from 'cbor'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; diff --git a/packages/backend/src/server/web/views/base.pug b/packages/backend/src/server/web/views/base.pug index 25513cc4cc..69b3f68e05 100644 --- a/packages/backend/src/server/web/views/base.pug +++ b/packages/backend/src/server/web/views/base.pug @@ -35,7 +35,7 @@ html link(rel='prefetch' href='https://xn--931a.moe/assets/not-found.jpg') link(rel='prefetch' href='https://xn--931a.moe/assets/error.jpg') //- https://github.com/misskey-dev/misskey/issues/9842 - link(rel='stylesheet' href='/assets/tabler-icons/tabler-icons.min.css?v2.17.0') + link(rel='stylesheet' href='/assets/tabler-icons/tabler-icons.min.css?v2.21.0') link(rel='modulepreload' href=`/vite/${clientEntry.file}`) if !config.clientManifestExists diff --git a/packages/frontend/.storybook/preview-head.html b/packages/frontend/.storybook/preview-head.html index ab694f64fb..f6a9a4875d 100644 --- a/packages/frontend/.storybook/preview-head.html +++ b/packages/frontend/.storybook/preview-head.html @@ -1,6 +1,6 @@ - +