From ffd8cf07e683eae95858d89fa9e5d85bf271c4d6 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Sat, 15 Feb 2025 10:24:22 +0900 Subject: update deps (#15311) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * wip * bump misskey-dev/eslint-plugin * lint fixes (backend) * lint fixes (frontend) * lint fixes (frontend-embed) * rollback nsfwjs to 4.2.0 ref: infinitered/nsfwjs#904 * rollback openapi-typescript to v6 v7でOpenAPIのバリデーションが入るようになった関係でスコープ外での変更が避けられないため一時的に戻した * lint fixes (misskey-js) * temporarily disable errored lint rule (frontend-shared) * fix lint * temporarily ignore errored file for lint (frontend-shared) * rollback simplewebauthn/server to 12.0.0 v13 contains breaking changes that require some decision making * lint fixes (frontend-shared) * build misskey-js with types * fix(backend): migrate simplewebauthn/server to v12 * fix(misskey-js/autogen): ignore indent rules to generate consistent output * attempt to fix test changes due to capricorn86/happy-dom#1617 (XMLSerializer now produces valid XML) * attempt to fix test changes due to capricorn86/happy-dom#1617 (XMLSerializer now produces valid XML) * fix test * fix test * fix test * Apply suggestions from code review Co-authored-by: anatawa12 * bump summaly to v5.2.0 * update tabler-icons to v3.30.0-based --------- Co-authored-by: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> Co-authored-by: anatawa12 --- packages/backend/src/misc/json-schema.ts | 4 ++-- packages/backend/src/misc/json-value.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'packages/backend/src/misc') diff --git a/packages/backend/src/misc/json-schema.ts b/packages/backend/src/misc/json-schema.ts index f612591eda..ac74d68c95 100644 --- a/packages/backend/src/misc/json-schema.ts +++ b/packages/backend/src/misc/json-schema.ts @@ -143,7 +143,7 @@ type OfSchema = { readonly anyOf?: ReadonlyArray; readonly oneOf?: ReadonlyArray; readonly allOf?: ReadonlyArray; -} +}; export interface Schema extends OfSchema { readonly type?: TypeStringef; @@ -217,7 +217,7 @@ type ObjectSchemaTypeDef

= : p['anyOf'] extends ReadonlyArray ? never : // see CONTRIBUTING.md p['allOf'] extends ReadonlyArray ? UnionToIntersection> : - any + any; type ObjectSchemaType

= NullOrUndefined>; diff --git a/packages/backend/src/misc/json-value.ts b/packages/backend/src/misc/json-value.ts index bd7fe12058..195f7c4d47 100644 --- a/packages/backend/src/misc/json-value.ts +++ b/packages/backend/src/misc/json-value.ts @@ -4,7 +4,7 @@ */ export type JsonValue = JsonArray | JsonObject | string | number | boolean | null; -export type JsonObject = {[K in string]?: JsonValue}; +export type JsonObject = { [K in string]?: JsonValue }; export type JsonArray = JsonValue[]; export function isJsonObject(value: JsonValue | undefined): value is JsonObject { -- cgit v1.2.3-freya From c63c3462dd00e7de3f40b15ad43993a26734263f Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Fri, 28 Feb 2025 09:34:21 +0900 Subject: refactor --- packages/backend/src/core/CreateSystemUserService.ts | 2 +- packages/backend/src/core/SignupService.ts | 4 ++-- packages/backend/src/misc/generate-native-user-token.ts | 9 --------- packages/backend/src/misc/is-native-token.ts | 7 ------- packages/backend/src/misc/token.ts | 10 ++++++++++ packages/backend/src/server/api/AuthenticateService.ts | 4 ++-- .../backend/src/server/api/endpoints/i/regenerate-token.ts | 4 ++-- 7 files changed, 17 insertions(+), 23 deletions(-) delete mode 100644 packages/backend/src/misc/generate-native-user-token.ts delete mode 100644 packages/backend/src/misc/is-native-token.ts create mode 100644 packages/backend/src/misc/token.ts (limited to 'packages/backend/src/misc') diff --git a/packages/backend/src/core/CreateSystemUserService.ts b/packages/backend/src/core/CreateSystemUserService.ts index 6c5b0f6a36..7ef75edb3c 100644 --- a/packages/backend/src/core/CreateSystemUserService.ts +++ b/packages/backend/src/core/CreateSystemUserService.ts @@ -14,7 +14,7 @@ import { IdService } from '@/core/IdService.js'; import { MiUserKeypair } from '@/models/UserKeypair.js'; import { MiUsedUsername } from '@/models/UsedUsername.js'; import { DI } from '@/di-symbols.js'; -import generateNativeUserToken from '@/misc/generate-native-user-token.js'; +import { generateNativeUserToken } from '@/misc/token.js'; import { bindThis } from '@/decorators.js'; @Injectable() diff --git a/packages/backend/src/core/SignupService.ts b/packages/backend/src/core/SignupService.ts index 3865392b7f..d2f09ea15d 100644 --- a/packages/backend/src/core/SignupService.ts +++ b/packages/backend/src/core/SignupService.ts @@ -14,7 +14,7 @@ import { MiUserProfile } from '@/models/UserProfile.js'; import { IdService } from '@/core/IdService.js'; import { MiUserKeypair } from '@/models/UserKeypair.js'; import { MiUsedUsername } from '@/models/UsedUsername.js'; -import generateUserToken from '@/misc/generate-native-user-token.js'; +import { generateNativeUserToken } from '@/misc/token.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { InstanceActorService } from '@/core/InstanceActorService.js'; import { bindThis } from '@/decorators.js'; @@ -74,7 +74,7 @@ export class SignupService { } // Generate secret - const secret = generateUserToken(); + const secret = generateNativeUserToken(); // Check username duplication if (await this.usersRepository.exists({ where: { usernameLower: username.toLowerCase(), host: IsNull() } })) { diff --git a/packages/backend/src/misc/generate-native-user-token.ts b/packages/backend/src/misc/generate-native-user-token.ts deleted file mode 100644 index 85fb383ba2..0000000000 --- a/packages/backend/src/misc/generate-native-user-token.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { secureRndstr } from '@/misc/secure-rndstr.js'; - -// eslint-disable-next-line import/no-default-export -export default () => secureRndstr(16); diff --git a/packages/backend/src/misc/is-native-token.ts b/packages/backend/src/misc/is-native-token.ts deleted file mode 100644 index 300c4c05b3..0000000000 --- a/packages/backend/src/misc/is-native-token.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -// eslint-disable-next-line import/no-default-export -export default (token: string) => token.length === 16; diff --git a/packages/backend/src/misc/token.ts b/packages/backend/src/misc/token.ts new file mode 100644 index 0000000000..5d37cba26d --- /dev/null +++ b/packages/backend/src/misc/token.ts @@ -0,0 +1,10 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { secureRndstr } from '@/misc/secure-rndstr.js'; + +export const generateNativeUserToken = () => secureRndstr(16); + +export const isNativeUserToken = (token: string) => token.length === 16; diff --git a/packages/backend/src/server/api/AuthenticateService.ts b/packages/backend/src/server/api/AuthenticateService.ts index 690ff2e022..601618553e 100644 --- a/packages/backend/src/server/api/AuthenticateService.ts +++ b/packages/backend/src/server/api/AuthenticateService.ts @@ -11,7 +11,7 @@ import type { MiAccessToken } from '@/models/AccessToken.js'; import { MemoryKVCache } from '@/misc/cache.js'; import type { MiApp } from '@/models/App.js'; import { CacheService } from '@/core/CacheService.js'; -import isNativeToken from '@/misc/is-native-token.js'; +import { isNativeUserToken } from '@/misc/token.js'; import { bindThis } from '@/decorators.js'; export class AuthenticationError extends Error { @@ -46,7 +46,7 @@ export class AuthenticateService implements OnApplicationShutdown { return [null, null]; } - if (isNativeToken(token)) { + if (isNativeUserToken(token)) { const user = await this.cacheService.localUserByNativeTokenCache.fetch(token, () => this.usersRepository.findOneBy({ token }) as Promise); diff --git a/packages/backend/src/server/api/endpoints/i/regenerate-token.ts b/packages/backend/src/server/api/endpoints/i/regenerate-token.ts index 78f3cce9ad..d25d5d5e0e 100644 --- a/packages/backend/src/server/api/endpoints/i/regenerate-token.ts +++ b/packages/backend/src/server/api/endpoints/i/regenerate-token.ts @@ -7,7 +7,7 @@ import bcrypt from 'bcryptjs'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { UsersRepository, UserProfilesRepository } from '@/models/_.js'; -import generateUserToken from '@/misc/generate-native-user-token.js'; +import { generateNativeUserToken } from '@/misc/token.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; @@ -49,7 +49,7 @@ export default class extends Endpoint { // eslint- throw new Error('incorrect password'); } - const newToken = generateUserToken(); + const newToken = generateNativeUserToken(); await this.usersRepository.update(me.id, { token: newToken, -- cgit v1.2.3-freya From 9dd13f364bcc486249bbeca73cd241b01a81fc7d Mon Sep 17 00:00:00 2001 From: zyoshoka <107108195+zyoshoka@users.noreply.github.com> Date: Thu, 20 Mar 2025 09:00:58 +0900 Subject: fix(backend): mismatch in `emojis` param of test WebHook payload (#15675) * fix(backend): mismatch in `emojis` param of test WebHook payload * fix: test * fix: type --- CHANGELOG.md | 1 + packages/backend/src/core/WebhookTestService.ts | 320 ++++++++++++----------- packages/backend/src/misc/json-schema.ts | 8 + packages/backend/test/unit/WebhookTestService.ts | 6 + 4 files changed, 178 insertions(+), 157 deletions(-) (limited to 'packages/backend/src/misc') diff --git a/CHANGELOG.md b/CHANGELOG.md index 824386e6bc..78233e71a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ - Fix: プロフィール追加情報で無効なURLに入力された場合に照会エラーを出るのを修正 - Fix: ActivityPubリクエストURLチェック実装は仕様に従っていないのを修正 - Fix: 連合無しモードでも外部から照会可能だった問題を修正 +- Fix: テスト用WebHookのペイロードの`emojis`パラメータが実際のものと異なる問題を修正 ## 2025.3.1 diff --git a/packages/backend/src/core/WebhookTestService.ts b/packages/backend/src/core/WebhookTestService.ts index f83dec67bf..e1a7801bb1 100644 --- a/packages/backend/src/core/WebhookTestService.ts +++ b/packages/backend/src/core/WebhookTestService.ts @@ -7,42 +7,16 @@ import { Injectable } from '@nestjs/common'; import { MiAbuseUserReport, MiNote, MiUser, MiWebhook } from '@/models/_.js'; import { bindThis } from '@/decorators.js'; import { MiSystemWebhook, type SystemWebhookEventType } from '@/models/SystemWebhook.js'; -import { AbuseReportPayload, SystemWebhookPayload, SystemWebhookService } from '@/core/SystemWebhookService.js'; -import { Packed } from '@/misc/json-schema.js'; +import { type AbuseReportPayload, SystemWebhookPayload, SystemWebhookService } from '@/core/SystemWebhookService.js'; +import { type Packed } from '@/misc/json-schema.js'; import { type WebhookEventTypes } from '@/models/Webhook.js'; +import { CustomEmojiService } from '@/core/CustomEmojiService.js'; import { type UserWebhookPayload, UserWebhookService } from '@/core/UserWebhookService.js'; import { QueueService } from '@/core/QueueService.js'; import { ModeratorInactivityRemainingTime } from '@/queue/processors/CheckModeratorsActivityProcessorService.js'; const oneDayMillis = 24 * 60 * 60 * 1000; -function generateAbuseReport(override?: Partial): AbuseReportPayload { - const result: MiAbuseUserReport = { - id: 'dummy-abuse-report1', - targetUserId: 'dummy-target-user', - targetUser: null, - reporterId: 'dummy-reporter-user', - reporter: null, - assigneeId: null, - assignee: null, - resolved: false, - forwarded: false, - comment: 'This is a dummy report for testing purposes.', - targetUserHost: null, - reporterHost: null, - resolvedAs: null, - moderationNote: 'foo', - ...override, - }; - - return { - ...result, - targetUser: result.targetUser ? toPackedUserLite(result.targetUser) : null, - reporter: result.reporter ? toPackedUserLite(result.reporter) : null, - assignee: result.assignee ? toPackedUserLite(result.assignee) : null, - }; -} - function generateDummyUser(override?: Partial): MiUser { return { id: 'dummy-user-1', @@ -134,124 +108,6 @@ function generateDummyNote(override?: Partial): MiNote { }; } -function toPackedNote(note: MiNote, detail = true, override?: Packed<'Note'>): Packed<'Note'> { - return { - id: note.id, - createdAt: new Date().toISOString(), - deletedAt: null, - text: note.text, - cw: note.cw, - userId: note.userId, - user: toPackedUserLite(note.user ?? generateDummyUser()), - replyId: note.replyId, - renoteId: note.renoteId, - isHidden: false, - visibility: note.visibility, - mentions: note.mentions, - visibleUserIds: note.visibleUserIds, - fileIds: note.fileIds, - files: [], - tags: note.tags, - poll: null, - emojis: note.emojis, - channelId: note.channelId, - channel: note.channel, - localOnly: note.localOnly, - reactionAcceptance: note.reactionAcceptance, - reactionEmojis: {}, - reactions: {}, - reactionCount: 0, - renoteCount: note.renoteCount, - repliesCount: note.repliesCount, - uri: note.uri ?? undefined, - url: note.url ?? undefined, - reactionAndUserPairCache: note.reactionAndUserPairCache, - ...(detail ? { - clippedCount: note.clippedCount, - reply: note.reply ? toPackedNote(note.reply, false) : null, - renote: note.renote ? toPackedNote(note.renote, true) : null, - myReaction: null, - } : {}), - ...override, - }; -} - -function toPackedUserLite(user: MiUser, override?: Packed<'UserLite'>): Packed<'UserLite'> { - return { - id: user.id, - name: user.name, - username: user.username, - host: user.host, - avatarUrl: user.avatarUrl, - avatarBlurhash: user.avatarBlurhash, - avatarDecorations: user.avatarDecorations.map(it => ({ - id: it.id, - angle: it.angle, - flipH: it.flipH, - url: 'https://example.com/dummy-image001.png', - offsetX: it.offsetX, - offsetY: it.offsetY, - })), - isBot: user.isBot, - isCat: user.isCat, - emojis: user.emojis, - onlineStatus: 'active', - badgeRoles: [], - ...override, - }; -} - -function toPackedUserDetailedNotMe(user: MiUser, override?: Packed<'UserDetailedNotMe'>): Packed<'UserDetailedNotMe'> { - return { - ...toPackedUserLite(user), - url: null, - uri: null, - movedTo: null, - alsoKnownAs: [], - createdAt: new Date().toISOString(), - updatedAt: user.updatedAt?.toISOString() ?? null, - lastFetchedAt: user.lastFetchedAt?.toISOString() ?? null, - bannerUrl: user.bannerUrl, - bannerBlurhash: user.bannerBlurhash, - isLocked: user.isLocked, - isSilenced: false, - isSuspended: user.isSuspended, - description: null, - location: null, - birthday: null, - lang: null, - fields: [], - verifiedLinks: [], - followersCount: user.followersCount, - followingCount: user.followingCount, - notesCount: user.notesCount, - pinnedNoteIds: [], - pinnedNotes: [], - pinnedPageId: null, - pinnedPage: null, - publicReactions: true, - followersVisibility: 'public', - followingVisibility: 'public', - twoFactorEnabled: false, - usePasswordLessLogin: false, - securityKeys: false, - roles: [], - memo: null, - moderationNote: undefined, - isFollowing: false, - isFollowed: false, - hasPendingFollowRequestFromYou: false, - hasPendingFollowRequestToYou: false, - isBlocking: false, - isBlocked: false, - isMuted: false, - isRenoteMuted: false, - notify: 'none', - withReplies: true, - ...override, - }; -} - const dummyUser1 = generateDummyUser(); const dummyUser2 = generateDummyUser({ id: 'dummy-user-2', @@ -284,6 +140,7 @@ export class WebhookTestService { }; constructor( + private customEmojiService: CustomEmojiService, private userWebhookService: UserWebhookService, private systemWebhookService: SystemWebhookService, private queueService: QueueService, @@ -354,31 +211,31 @@ export class WebhookTestService { switch (params.type) { case 'note': { - send('note', { note: toPackedNote(dummyNote1) }); + send('note', { note: await this.toPackedNote(dummyNote1) }); break; } case 'reply': { - send('reply', { note: toPackedNote(dummyReply1) }); + send('reply', { note: await this.toPackedNote(dummyReply1) }); break; } case 'renote': { - send('renote', { note: toPackedNote(dummyRenote1) }); + send('renote', { note: await this.toPackedNote(dummyRenote1) }); break; } case 'mention': { - send('mention', { note: toPackedNote(dummyMention1) }); + send('mention', { note: await this.toPackedNote(dummyMention1) }); break; } case 'follow': { - send('follow', { user: toPackedUserDetailedNotMe(dummyUser1) }); + send('follow', { user: await this.toPackedUserDetailedNotMe(dummyUser1) }); break; } case 'followed': { - send('followed', { user: toPackedUserLite(dummyUser2) }); + send('followed', { user: await this.toPackedUserLite(dummyUser2) }); break; } case 'unfollow': { - send('unfollow', { user: toPackedUserDetailedNotMe(dummyUser3) }); + send('unfollow', { user: await this.toPackedUserDetailedNotMe(dummyUser3) }); break; } // まだ実装されていない (#9485) @@ -427,7 +284,7 @@ export class WebhookTestService { switch (params.type) { case 'abuseReport': { - send('abuseReport', generateAbuseReport({ + send('abuseReport', await this.generateAbuseReport({ targetUserId: dummyUser1.id, targetUser: dummyUser1, reporterId: dummyUser2.id, @@ -436,7 +293,7 @@ export class WebhookTestService { break; } case 'abuseReportResolved': { - send('abuseReportResolved', generateAbuseReport({ + send('abuseReportResolved', await this.generateAbuseReport({ targetUserId: dummyUser1.id, targetUser: dummyUser1, reporterId: dummyUser2.id, @@ -448,7 +305,7 @@ export class WebhookTestService { break; } case 'userCreated': { - send('userCreated', toPackedUserLite(dummyUser1)); + send('userCreated', await this.toPackedUserLite(dummyUser1)); break; } case 'inactiveModeratorsWarning': { @@ -474,4 +331,153 @@ export class WebhookTestService { } } } + + @bindThis + private async generateAbuseReport(override?: Partial): Promise { + const result: MiAbuseUserReport = { + id: 'dummy-abuse-report1', + targetUserId: 'dummy-target-user', + targetUser: null, + reporterId: 'dummy-reporter-user', + reporter: null, + assigneeId: null, + assignee: null, + resolved: false, + forwarded: false, + comment: 'This is a dummy report for testing purposes.', + targetUserHost: null, + reporterHost: null, + resolvedAs: null, + moderationNote: 'foo', + ...override, + }; + + return { + ...result, + targetUser: result.targetUser ? await this.toPackedUserLite(result.targetUser) : null, + reporter: result.reporter ? await this.toPackedUserLite(result.reporter) : null, + assignee: result.assignee ? await this.toPackedUserLite(result.assignee) : null, + }; + } + + @bindThis + private async toPackedNote(note: MiNote, detail = true, override?: Packed<'Note'>): Promise> { + return { + id: note.id, + createdAt: new Date().toISOString(), + deletedAt: null, + text: note.text, + cw: note.cw, + userId: note.userId, + user: await this.toPackedUserLite(note.user ?? generateDummyUser()), + replyId: note.replyId, + renoteId: note.renoteId, + isHidden: false, + visibility: note.visibility, + mentions: note.mentions, + visibleUserIds: note.visibleUserIds, + fileIds: note.fileIds, + files: [], + tags: note.tags, + poll: null, + emojis: await this.customEmojiService.populateEmojis(note.emojis, note.userHost), + channelId: note.channelId, + channel: note.channel, + localOnly: note.localOnly, + reactionAcceptance: note.reactionAcceptance, + reactionEmojis: {}, + reactions: {}, + reactionCount: 0, + renoteCount: note.renoteCount, + repliesCount: note.repliesCount, + uri: note.uri ?? undefined, + url: note.url ?? undefined, + reactionAndUserPairCache: note.reactionAndUserPairCache, + ...(detail ? { + clippedCount: note.clippedCount, + reply: note.reply ? await this.toPackedNote(note.reply, false) : null, + renote: note.renote ? await this.toPackedNote(note.renote, true) : null, + myReaction: null, + } : {}), + ...override, + }; + } + + @bindThis + private async toPackedUserLite(user: MiUser, override?: Packed<'UserLite'>): Promise> { + return { + id: user.id, + name: user.name, + username: user.username, + host: user.host, + avatarUrl: user.avatarUrl, + avatarBlurhash: user.avatarBlurhash, + avatarDecorations: user.avatarDecorations.map(it => ({ + id: it.id, + angle: it.angle, + flipH: it.flipH, + url: 'https://example.com/dummy-image001.png', + offsetX: it.offsetX, + offsetY: it.offsetY, + })), + isBot: user.isBot, + isCat: user.isCat, + emojis: await this.customEmojiService.populateEmojis(user.emojis, user.host), + onlineStatus: 'active', + badgeRoles: [], + ...override, + }; + } + + @bindThis + private async toPackedUserDetailedNotMe(user: MiUser, override?: Packed<'UserDetailedNotMe'>): Promise> { + return { + ...await this.toPackedUserLite(user), + url: null, + uri: null, + movedTo: null, + alsoKnownAs: [], + createdAt: new Date().toISOString(), + updatedAt: user.updatedAt?.toISOString() ?? null, + lastFetchedAt: user.lastFetchedAt?.toISOString() ?? null, + bannerUrl: user.bannerUrl, + bannerBlurhash: user.bannerBlurhash, + isLocked: user.isLocked, + isSilenced: false, + isSuspended: user.isSuspended, + description: null, + location: null, + birthday: null, + lang: null, + fields: [], + verifiedLinks: [], + followersCount: user.followersCount, + followingCount: user.followingCount, + notesCount: user.notesCount, + pinnedNoteIds: [], + pinnedNotes: [], + pinnedPageId: null, + pinnedPage: null, + publicReactions: true, + followersVisibility: 'public', + followingVisibility: 'public', + twoFactorEnabled: false, + usePasswordLessLogin: false, + securityKeys: false, + roles: [], + memo: null, + moderationNote: undefined, + isFollowing: false, + isFollowed: false, + hasPendingFollowRequestFromYou: false, + hasPendingFollowRequestToYou: false, + isBlocking: false, + isBlocked: false, + isMuted: false, + isRenoteMuted: false, + notify: 'none', + withReplies: true, + ...override, + }; + } } diff --git a/packages/backend/src/misc/json-schema.ts b/packages/backend/src/misc/json-schema.ts index ac74d68c95..100fb69585 100644 --- a/packages/backend/src/misc/json-schema.ts +++ b/packages/backend/src/misc/json-schema.ts @@ -166,6 +166,7 @@ export interface Schema extends OfSchema { readonly maximum?: number; readonly minimum?: number; readonly pattern?: string; + readonly additionalProperties?: Schema | boolean; } type RequiredPropertyNames = { @@ -217,6 +218,13 @@ type ObjectSchemaTypeDef

= : p['anyOf'] extends ReadonlyArray ? never : // see CONTRIBUTING.md p['allOf'] extends ReadonlyArray ? UnionToIntersection> : + p['additionalProperties'] extends true ? Record : + p['additionalProperties'] extends Schema ? + p['additionalProperties'] extends infer AdditionalProperties ? + AdditionalProperties extends Schema ? + Record> : + never : + never : any; type ObjectSchemaType

= NullOrUndefined>; diff --git a/packages/backend/test/unit/WebhookTestService.ts b/packages/backend/test/unit/WebhookTestService.ts index 45bc932469..736aac40b4 100644 --- a/packages/backend/test/unit/WebhookTestService.ts +++ b/packages/backend/test/unit/WebhookTestService.ts @@ -14,6 +14,7 @@ import { MiSystemWebhook, MiUser, MiWebhook, UserProfilesRepository, UsersReposi import { IdService } from '@/core/IdService.js'; import { DI } from '@/di-symbols.js'; import { QueueService } from '@/core/QueueService.js'; +import { CustomEmojiService } from '@/core/CustomEmojiService.js'; describe('WebhookTestService', () => { let app: TestingModule; @@ -56,6 +57,11 @@ describe('WebhookTestService', () => { providers: [ WebhookTestService, IdService, + { + provide: CustomEmojiService, useFactory: () => ({ + populateEmojis: jest.fn(), + }), + }, { provide: QueueService, useFactory: () => ({ systemWebhookDeliver: jest.fn(), -- cgit v1.2.3-freya From e61263cff08f318ef21932ea3a64a1b9c19f160a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 21 Mar 2025 19:38:11 +0900 Subject: fix(deps): update [backend] update dependencies (#15596) * fix(deps): update [backend] update dependencies * fix(backend/types): ensure to use nodejs-provided stream api --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: kakkokari-gtyih <67428053+kakkokari-gtyih@users.noreply.github.com> --- packages/backend/package.json | 74 +- packages/backend/src/misc/FileWriterStream.ts | 1 + pnpm-lock.yaml | 1227 ++++++++++++------------- 3 files changed, 607 insertions(+), 695 deletions(-) (limited to 'packages/backend/src/misc') diff --git a/packages/backend/package.json b/packages/backend/package.json index 502323bf61..bcaa6357ce 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -37,17 +37,17 @@ }, "optionalDependencies": { "@swc/core-android-arm64": "1.3.11", - "@swc/core-darwin-arm64": "1.10.16", - "@swc/core-darwin-x64": "1.10.16", + "@swc/core-darwin-arm64": "1.11.11", + "@swc/core-darwin-x64": "1.11.11", "@swc/core-freebsd-x64": "1.3.11", - "@swc/core-linux-arm-gnueabihf": "1.10.16", - "@swc/core-linux-arm64-gnu": "1.10.16", - "@swc/core-linux-arm64-musl": "1.10.16", - "@swc/core-linux-x64-gnu": "1.10.16", - "@swc/core-linux-x64-musl": "1.10.16", - "@swc/core-win32-arm64-msvc": "1.10.16", - "@swc/core-win32-ia32-msvc": "1.10.16", - "@swc/core-win32-x64-msvc": "1.10.16", + "@swc/core-linux-arm-gnueabihf": "1.11.11", + "@swc/core-linux-arm64-gnu": "1.11.11", + "@swc/core-linux-arm64-musl": "1.11.11", + "@swc/core-linux-x64-gnu": "1.11.11", + "@swc/core-linux-x64-musl": "1.11.11", + "@swc/core-win32-arm64-msvc": "1.11.11", + "@swc/core-win32-ia32-msvc": "1.11.11", + "@swc/core-win32-x64-msvc": "1.11.11", "@tensorflow/tfjs": "4.22.0", "@tensorflow/tfjs-node": "4.22.0", "bufferutil": "4.0.9", @@ -67,23 +67,23 @@ "utf-8-validate": "6.0.5" }, "dependencies": { - "@aws-sdk/client-s3": "3.749.0", - "@aws-sdk/lib-storage": "3.749.0", + "@aws-sdk/client-s3": "3.772.0", + "@aws-sdk/lib-storage": "3.772.0", "@discordapp/twemoji": "15.1.0", "@fastify/accepts": "5.0.2", "@fastify/cookie": "11.0.2", - "@fastify/cors": "10.0.2", + "@fastify/cors": "10.1.0", "@fastify/express": "4.0.2", "@fastify/http-proxy": "10.0.2", "@fastify/multipart": "9.0.3", - "@fastify/static": "8.1.0", + "@fastify/static": "8.1.1", "@fastify/view": "10.0.2", "@misskey-dev/sharp-read-bmp": "1.2.0", "@misskey-dev/summaly": "5.2.0", - "@napi-rs/canvas": "0.1.67", - "@nestjs/common": "11.0.9", - "@nestjs/core": "11.0.9", - "@nestjs/testing": "11.0.9", + "@napi-rs/canvas": "0.1.68", + "@nestjs/common": "11.0.12", + "@nestjs/core": "11.0.12", + "@nestjs/testing": "11.0.12", "@peertube/http-signature": "1.7.0", "@sentry/node": "8.55.0", "@sentry/profiling-node": "8.55.0", @@ -91,7 +91,7 @@ "@sinonjs/fake-timers": "11.3.1", "@smithy/node-http-handler": "2.5.0", "@swc/cli": "0.6.0", - "@swc/core": "1.10.16", + "@swc/core": "1.11.11", "@twemoji/parser": "15.1.1", "accepts": "1.3.8", "ajv": "8.17.1", @@ -100,7 +100,7 @@ "bcryptjs": "2.4.3", "blurhash": "2.0.5", "body-parser": "1.20.3", - "bullmq": "5.41.1", + "bullmq": "5.44.1", "cacheable-lookup": "7.0.0", "cbor": "9.0.2", "chalk": "5.4.1", @@ -122,7 +122,7 @@ "hpagent": "1.2.0", "htmlescape": "1.1.1", "http-link-header": "1.1.3", - "ioredis": "5.5.0", + "ioredis": "5.6.0", "ip-cidr": "4.0.2", "ipaddr.js": "2.2.0", "is-svg": "5.1.0", @@ -131,26 +131,26 @@ "json5": "2.2.3", "jsonld": "8.3.3", "jsrsasign": "11.1.0", - "juice": "11.0.0", - "meilisearch": "0.48.2", + "juice": "11.0.1", + "meilisearch": "0.49.0", "mfm-js": "0.24.0", "microformats-parser": "2.0.2", "mime-types": "2.1.35", "misskey-js": "workspace:*", "misskey-reversi": "workspace:*", "ms": "3.0.0-canary.1", - "nanoid": "5.1.0", + "nanoid": "5.1.5", "nested-property": "4.0.0", "node-fetch": "3.3.2", "nodemailer": "6.10.0", "nsfwjs": "4.2.0", - "oauth": "0.10.0", + "oauth": "0.10.2", "oauth2orize": "1.12.0", "oauth2orize-pkce": "0.1.2", "os-utils": "0.0.14", "otpauth": "9.3.6", "parse5": "7.2.1", - "pg": "8.13.3", + "pg": "8.14.1", "pkce-challenge": "4.1.0", "probe-image-size": "7.2.3", "promise-limit": "2.7.0", @@ -163,8 +163,8 @@ "reflect-metadata": "0.2.2", "rename": "1.0.4", "rss-parser": "3.13.0", - "rxjs": "7.8.1", - "sanitize-html": "2.14.0", + "rxjs": "7.8.2", + "sanitize-html": "2.15.0", "secure-json-parse": "3.0.2", "sharp": "0.33.5", "slacc": "0.0.10", @@ -173,14 +173,14 @@ "systeminformation": "5.25.11", "tinycolor2": "1.6.0", "tmp": "0.2.3", - "tsc-alias": "1.8.10", + "tsc-alias": "1.8.11", "tsconfig-paths": "4.2.0", - "typeorm": "0.3.20", - "typescript": "5.7.3", - "ulid": "2.3.0", + "typeorm": "0.3.21", + "typescript": "5.8.2", + "ulid": "2.4.0", "vary": "1.1.2", "web-push": "3.6.7", - "ws": "8.18.0", + "ws": "8.18.1", "xev": "3.0.2" }, "devDependencies": { @@ -204,7 +204,7 @@ "@types/jsrsasign": "10.5.15", "@types/mime-types": "2.1.4", "@types/ms": "0.7.34", - "@types/node": "22.13.4", + "@types/node": "22.13.10", "@types/nodemailer": "6.4.17", "@types/oauth": "0.9.6", "@types/oauth2orize": "1.11.5", @@ -223,9 +223,9 @@ "@types/tmp": "0.2.6", "@types/vary": "1.1.3", "@types/web-push": "3.6.4", - "@types/ws": "8.5.14", - "@typescript-eslint/eslint-plugin": "8.24.0", - "@typescript-eslint/parser": "8.24.0", + "@types/ws": "8.18.0", + "@typescript-eslint/eslint-plugin": "8.27.0", + "@typescript-eslint/parser": "8.27.0", "aws-sdk-client-mock": "4.1.0", "cross-env": "7.0.3", "eslint-plugin-import": "2.31.0", diff --git a/packages/backend/src/misc/FileWriterStream.ts b/packages/backend/src/misc/FileWriterStream.ts index 367a8eb560..27c67cb5df 100644 --- a/packages/backend/src/misc/FileWriterStream.ts +++ b/packages/backend/src/misc/FileWriterStream.ts @@ -4,6 +4,7 @@ */ import * as fs from 'node:fs/promises'; +import { WritableStream } from 'node:stream/web'; import type { PathLike } from 'node:fs'; /** diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 40596d43ed..78d6ca60cb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -93,11 +93,11 @@ importers: packages/backend: dependencies: '@aws-sdk/client-s3': - specifier: 3.749.0 - version: 3.749.0 + specifier: 3.772.0 + version: 3.772.0 '@aws-sdk/lib-storage': - specifier: 3.749.0 - version: 3.749.0(@aws-sdk/client-s3@3.749.0) + specifier: 3.772.0 + version: 3.772.0(@aws-sdk/client-s3@3.772.0) '@discordapp/twemoji': specifier: 15.1.0 version: 15.1.0 @@ -108,8 +108,8 @@ importers: specifier: 11.0.2 version: 11.0.2 '@fastify/cors': - specifier: 10.0.2 - version: 10.0.2 + specifier: 10.1.0 + version: 10.1.0 '@fastify/express': specifier: 4.0.2 version: 4.0.2 @@ -120,8 +120,8 @@ importers: specifier: 9.0.3 version: 9.0.3 '@fastify/static': - specifier: 8.1.0 - version: 8.1.0 + specifier: 8.1.1 + version: 8.1.1 '@fastify/view': specifier: 10.0.2 version: 10.0.2 @@ -132,17 +132,17 @@ importers: specifier: 5.2.0 version: 5.2.0 '@napi-rs/canvas': - specifier: 0.1.67 - version: 0.1.67 + specifier: 0.1.68 + version: 0.1.68 '@nestjs/common': - specifier: 11.0.9 - version: 11.0.9(reflect-metadata@0.2.2)(rxjs@7.8.1) + specifier: 11.0.12 + version: 11.0.12(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/core': - specifier: 11.0.9 - version: 11.0.9(@nestjs/common@11.0.9(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.15)(reflect-metadata@0.2.2)(rxjs@7.8.1) + specifier: 11.0.12 + version: 11.0.12(@nestjs/common@11.0.12(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.15)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/testing': - specifier: 11.0.9 - version: 11.0.9(@nestjs/common@11.0.9(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@11.0.9)(@nestjs/platform-express@10.4.15) + specifier: 11.0.12 + version: 11.0.12(@nestjs/common@11.0.12(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.0.12)(@nestjs/platform-express@10.4.15) '@peertube/http-signature': specifier: 1.7.0 version: 1.7.0 @@ -163,10 +163,10 @@ importers: version: 2.5.0 '@swc/cli': specifier: 0.6.0 - version: 0.6.0(@swc/core@1.10.16)(chokidar@4.0.3) + version: 0.6.0(@swc/core@1.11.11)(chokidar@4.0.3) '@swc/core': - specifier: 1.10.16 - version: 1.10.16 + specifier: 1.11.11 + version: 1.11.11 '@twemoji/parser': specifier: 15.1.1 version: 15.1.1 @@ -192,8 +192,8 @@ importers: specifier: 1.20.3 version: 1.20.3 bullmq: - specifier: 5.41.1 - version: 5.41.1 + specifier: 5.44.1 + version: 5.44.1 cacheable-lookup: specifier: 7.0.0 version: 7.0.0 @@ -258,8 +258,8 @@ importers: specifier: 1.1.3 version: 1.1.3 ioredis: - specifier: 5.5.0 - version: 5.5.0 + specifier: 5.6.0 + version: 5.6.0 ip-cidr: specifier: 4.0.2 version: 4.0.2 @@ -285,11 +285,11 @@ importers: specifier: 11.1.0 version: 11.1.0 juice: - specifier: 11.0.0 - version: 11.0.0 + specifier: 11.0.1 + version: 11.0.1 meilisearch: - specifier: 0.48.2 - version: 0.48.2 + specifier: 0.49.0 + version: 0.49.0 mfm-js: specifier: 0.24.0 version: 0.24.0 @@ -309,8 +309,8 @@ importers: specifier: 3.0.0-canary.1 version: 3.0.0-canary.1 nanoid: - specifier: 5.1.0 - version: 5.1.0 + specifier: 5.1.5 + version: 5.1.5 nested-property: specifier: 4.0.0 version: 4.0.0 @@ -324,8 +324,8 @@ importers: specifier: 4.2.0 version: 4.2.0(@tensorflow/tfjs@4.22.0(encoding@0.1.13)(seedrandom@3.0.5))(buffer@6.0.3) oauth: - specifier: 0.10.0 - version: 0.10.0 + specifier: 0.10.2 + version: 0.10.2 oauth2orize: specifier: 1.12.0 version: 1.12.0 @@ -342,8 +342,8 @@ importers: specifier: 7.2.1 version: 7.2.1 pg: - specifier: 8.13.3 - version: 8.13.3 + specifier: 8.14.1 + version: 8.14.1 pkce-challenge: specifier: 4.1.0 version: 4.1.0 @@ -381,11 +381,11 @@ importers: specifier: 3.13.0 version: 3.13.0 rxjs: - specifier: 7.8.1 - version: 7.8.1 + specifier: 7.8.2 + version: 7.8.2 sanitize-html: - specifier: 2.14.0 - version: 2.14.0 + specifier: 2.15.0 + version: 2.15.0 secure-json-parse: specifier: 3.0.2 version: 3.0.2 @@ -411,20 +411,20 @@ importers: specifier: 0.2.3 version: 0.2.3 tsc-alias: - specifier: 1.8.10 - version: 1.8.10 + specifier: 1.8.11 + version: 1.8.11 tsconfig-paths: specifier: 4.2.0 version: 4.2.0 typeorm: - specifier: 0.3.20 - version: 0.3.20(ioredis@5.5.0)(pg@8.13.3) + specifier: 0.3.21 + version: 0.3.21(ioredis@5.6.0)(pg@8.14.1)(reflect-metadata@0.2.2) typescript: - specifier: 5.7.3 - version: 5.7.3 + specifier: 5.8.2 + version: 5.8.2 ulid: - specifier: 2.3.0 - version: 2.3.0 + specifier: 2.4.0 + version: 2.4.0 vary: specifier: 1.1.2 version: 1.1.2 @@ -432,8 +432,8 @@ importers: specifier: 3.6.7 version: 3.6.7 ws: - specifier: 8.18.0 - version: 8.18.0(bufferutil@4.0.9)(utf-8-validate@6.0.5) + specifier: 8.18.1 + version: 8.18.1(bufferutil@4.0.9)(utf-8-validate@6.0.5) xev: specifier: 3.0.2 version: 3.0.2 @@ -443,13 +443,13 @@ importers: version: 29.7.0 '@nestjs/platform-express': specifier: 10.4.15 - version: 10.4.15(@nestjs/common@11.0.9(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@11.0.9) + version: 10.4.15(@nestjs/common@11.0.12(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.0.12) '@simplewebauthn/types': specifier: 12.0.0 version: 12.0.0 '@swc/jest': specifier: 0.2.37 - version: 0.2.37(@swc/core@1.10.16) + version: 0.2.37(@swc/core@1.11.11) '@types/accepts': specifier: 1.3.7 version: 1.3.7 @@ -499,8 +499,8 @@ importers: specifier: 0.7.34 version: 0.7.34 '@types/node': - specifier: 22.13.4 - version: 22.13.4 + specifier: 22.13.10 + version: 22.13.10 '@types/nodemailer': specifier: 6.4.17 version: 6.4.17 @@ -556,14 +556,14 @@ importers: specifier: 3.6.4 version: 3.6.4 '@types/ws': - specifier: 8.5.14 - version: 8.5.14 + specifier: 8.18.0 + version: 8.18.0 '@typescript-eslint/eslint-plugin': - specifier: 8.24.0 - version: 8.24.0(@typescript-eslint/parser@8.24.0(eslint@9.22.0)(typescript@5.7.3))(eslint@9.22.0)(typescript@5.7.3) + specifier: 8.27.0 + version: 8.27.0(@typescript-eslint/parser@8.27.0(eslint@9.22.0)(typescript@5.8.2))(eslint@9.22.0)(typescript@5.8.2) '@typescript-eslint/parser': - specifier: 8.24.0 - version: 8.24.0(eslint@9.22.0)(typescript@5.7.3) + specifier: 8.27.0 + version: 8.27.0(eslint@9.22.0)(typescript@5.8.2) aws-sdk-client-mock: specifier: 4.1.0 version: 4.1.0 @@ -572,7 +572,7 @@ importers: version: 7.0.3 eslint-plugin-import: specifier: 2.31.0 - version: 2.31.0(@typescript-eslint/parser@8.24.0(eslint@9.22.0)(typescript@5.7.3))(eslint@9.22.0) + version: 2.31.0(@typescript-eslint/parser@8.27.0(eslint@9.22.0)(typescript@5.8.2))(eslint@9.22.0) execa: specifier: 8.0.1 version: 8.0.1 @@ -581,7 +581,7 @@ importers: version: 9.0.0 jest: specifier: 29.7.0 - version: 29.7.0(@types/node@22.13.4) + version: 29.7.0(@types/node@22.13.10) jest-mock: specifier: 29.7.0 version: 29.7.0 @@ -599,38 +599,38 @@ importers: specifier: 1.3.11 version: 1.3.11 '@swc/core-darwin-arm64': - specifier: 1.10.16 - version: 1.10.16 + specifier: 1.11.11 + version: 1.11.11 '@swc/core-darwin-x64': - specifier: 1.10.16 - version: 1.10.16 + specifier: 1.11.11 + version: 1.11.11 '@swc/core-freebsd-x64': specifier: 1.3.11 version: 1.3.11 '@swc/core-linux-arm-gnueabihf': - specifier: 1.10.16 - version: 1.10.16 + specifier: 1.11.11 + version: 1.11.11 '@swc/core-linux-arm64-gnu': - specifier: 1.10.16 - version: 1.10.16 + specifier: 1.11.11 + version: 1.11.11 '@swc/core-linux-arm64-musl': - specifier: 1.10.16 - version: 1.10.16 + specifier: 1.11.11 + version: 1.11.11 '@swc/core-linux-x64-gnu': - specifier: 1.10.16 - version: 1.10.16 + specifier: 1.11.11 + version: 1.11.11 '@swc/core-linux-x64-musl': - specifier: 1.10.16 - version: 1.10.16 + specifier: 1.11.11 + version: 1.11.11 '@swc/core-win32-arm64-msvc': - specifier: 1.10.16 - version: 1.10.16 + specifier: 1.11.11 + version: 1.11.11 '@swc/core-win32-ia32-msvc': - specifier: 1.10.16 - version: 1.10.16 + specifier: 1.11.11 + version: 1.11.11 '@swc/core-win32-x64-msvc': - specifier: 1.10.16 - version: 1.10.16 + specifier: 1.11.11 + version: 1.11.11 '@tensorflow/tfjs': specifier: 4.22.0 version: 4.22.0(encoding@0.1.13)(seedrandom@3.0.5) @@ -1313,7 +1313,7 @@ importers: version: 7.51.1(@types/node@22.13.9) '@swc/jest': specifier: 0.2.37 - version: 0.2.37(@swc/core@1.10.16) + version: 0.2.37(@swc/core@1.11.11) '@types/jest': specifier: 29.5.14 version: 29.5.14 @@ -1511,51 +1511,51 @@ packages: '@aws-crypto/util@5.2.0': resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} - '@aws-sdk/client-s3@3.749.0': - resolution: {integrity: sha512-Xi+DaBeYRIa2d+1QTAlBdbRIc9j3+H+H5aMbxIrlyIYE2NOz+4fhIgpUYuF4ln63p9Fby5Wh36DhHJYyN4uE0w==} + '@aws-sdk/client-s3@3.772.0': + resolution: {integrity: sha512-HQXlQIyyLp47h1/Hdjr36yK8/gsAAFX2vRzgDJhSRaz0vWZlWX07AJdYfrxapLUXfVU6DbBu3rwi2UGqM7ixqQ==} engines: {node: '>=18.0.0'} - '@aws-sdk/client-sso@3.749.0': - resolution: {integrity: sha512-ecmuDu8EPya1LDpGRtpgN7C9PHayDh8EaW37ZBKhuxA7cg099yvTFqsGngwRXbhNjKJ4oVa9OUe0EDnu60atYA==} + '@aws-sdk/client-sso@3.772.0': + resolution: {integrity: sha512-sDdxepi74+cL6gXJJ2yw3UNSI7GBvoGTwZqFyPoNAzcURvaYwo8dBr7G4jS9GDanjTlO3CGVAf2VMcpqEvmoEw==} engines: {node: '>=18.0.0'} - '@aws-sdk/core@3.749.0': - resolution: {integrity: sha512-w5Jj573+XKwrDNZUjUJDXL5upx+RCw64TLq3Zk8FVg9MsgkzAPorQ9qmzffi6os+PWngd3pFmD8q5y+Y35LpFQ==} + '@aws-sdk/core@3.758.0': + resolution: {integrity: sha512-0RswbdR9jt/XKemaLNuxi2gGr4xGlHyGxkTdhSQzCyUe9A9OPCoLl3rIESRguQEech+oJnbHk/wuiwHqTuP9sg==} engines: {node: '>=18.0.0'} - '@aws-sdk/credential-provider-env@3.749.0': - resolution: {integrity: sha512-bhB1ds5QzcSfmCTbjVessXy8xHJROota6wOhFtBsL1aZRQyNN2a9h2QS6xkxjmqVE3yHBsPz+OiSOeLn0kxm7Q==} + '@aws-sdk/credential-provider-env@3.758.0': + resolution: {integrity: sha512-N27eFoRrO6MeUNumtNHDW9WOiwfd59LPXPqDrIa3kWL/s+fOKFHb9xIcF++bAwtcZnAxKkgpDCUP+INNZskE+w==} engines: {node: '>=18.0.0'} - '@aws-sdk/credential-provider-http@3.749.0': - resolution: {integrity: sha512-enFGT8uvETbE6+4bDA2aTOrA/83GrIVPpg2g2r7MwJb36jreXA3KDXaP/5jQsxyIZW70cnYNl/Cawdd4ZXs7CQ==} + '@aws-sdk/credential-provider-http@3.758.0': + resolution: {integrity: sha512-Xt9/U8qUCiw1hihztWkNeIR+arg6P+yda10OuCHX6kFVx3auTlU7+hCqs3UxqniGU4dguHuftf3mRpi5/GJ33Q==} engines: {node: '>=18.0.0'} - '@aws-sdk/credential-provider-ini@3.749.0': - resolution: {integrity: sha512-OB4AGK61lQdoW2mTmaMBw8L+eBo7wF3YJZXwqFI7M2cQe9WtfuKGIxbYWMBMzoLvEtmsbzeppoZZUezooaIclg==} + '@aws-sdk/credential-provider-ini@3.772.0': + resolution: {integrity: sha512-T1Ec9Q25zl5c/eZUPHZsiq8vgBeWBjHM7WM5xtZszZRPqqhQGnmFlomz1r9rwhW8RFB5k8HRaD/SLKo6jtYl/A==} engines: {node: '>=18.0.0'} - '@aws-sdk/credential-provider-node@3.749.0': - resolution: {integrity: sha512-1QGstZmGmgmY0rLSTAURlBJdR4s2PRYiZh6dS4HkzzQu7xVDWoCMD+2F7dolsNA8ChTNx2OvBW80n3O9QPICCg==} + '@aws-sdk/credential-provider-node@3.772.0': + resolution: {integrity: sha512-0IdVfjBO88Mtekq/KaScYSIEPIeR+ABRvBOWyj/c/qQ2KJyI0GRlSAzpANfxDLHVPn3yEHuZd9nRL6sOmOMI0A==} engines: {node: '>=18.0.0'} - '@aws-sdk/credential-provider-process@3.749.0': - resolution: {integrity: sha512-C/cgg/AhRabybZRY9mJ6KDz8uqfasWKuFIFGzTpeb/MIDIL53ZqP61CspiQJTRvC4zlFGqvm43XefphfrBGGlQ==} + '@aws-sdk/credential-provider-process@3.758.0': + resolution: {integrity: sha512-AzcY74QTPqcbXWVgjpPZ3HOmxQZYPROIBz2YINF0OQk0MhezDWV/O7Xec+K1+MPGQO3qS6EDrUUlnPLjsqieHA==} engines: {node: '>=18.0.0'} - '@aws-sdk/credential-provider-sso@3.749.0': - resolution: {integrity: sha512-bQNgWcYk10fYOvFwcLskYYVNLO3KMgmV1ip9ieapJb9JDg6bSBaXNjIDhdpK4biTOfrV+adtDO5EU93ogpmAWA==} + '@aws-sdk/credential-provider-sso@3.772.0': + resolution: {integrity: sha512-yR3Y5RAVPa4ogojcBOpZUx6XyRVAkynIJCjd0avdlxW1hhnzSr5/pzoiJ6u21UCbkxlJJTDZE3jfFe7tt+HA4w==} engines: {node: '>=18.0.0'} - '@aws-sdk/credential-provider-web-identity@3.749.0': - resolution: {integrity: sha512-jzHk6i4G4dnXL+L+qeILguDCiIhA1rNvJzB5lTts4R8OdmNkG12bGbYL8bL4O1b5qCimlo7HS0IZIby0pS7rcg==} + '@aws-sdk/credential-provider-web-identity@3.772.0': + resolution: {integrity: sha512-yHAT5Y2y0fnecSuWRUn8NMunKfDqFYhnOpGq8UyCEcwz9aXzibU0hqRIEm51qpR81hqo0GMFDH0EOmegZ/iW5w==} engines: {node: '>=18.0.0'} - '@aws-sdk/lib-storage@3.749.0': - resolution: {integrity: sha512-eYvsRWzukHeZ3k/hFt66or33JJg5zAm/jtstbxZaKRq/H1VfWkKh1QyQHu0UiOXZnjEkTFNJujwGME8JmAWpRg==} + '@aws-sdk/lib-storage@3.772.0': + resolution: {integrity: sha512-d59NJAHDa7mGf82ApOdIp1FsN1i7/VlNTk2t3NIepH/8FsMTzKPZQvbMzqE33ba4oik7y6QrDliYXoRXB/nZPg==} engines: {node: '>=18.0.0'} peerDependencies: - '@aws-sdk/client-s3': ^3.749.0 + '@aws-sdk/client-s3': ^3.772.0 '@aws-sdk/middleware-bucket-endpoint@3.734.0': resolution: {integrity: sha512-etC7G18aF7KdZguW27GE/wpbrNmYLVT755EsFc8kXpZj8D6AFKxc7OuveinJmiy0bYXAMspJUWsF6CrGpOw6CQ==} @@ -1565,8 +1565,8 @@ packages: resolution: {integrity: sha512-P38/v1l6HjuB2aFUewt7ueAW5IvKkFcv5dalPtbMGRhLeyivBOHwbCyuRKgVs7z7ClTpu9EaViEGki2jEQqEsQ==} engines: {node: '>=18.0.0'} - '@aws-sdk/middleware-flexible-checksums@3.749.0': - resolution: {integrity: sha512-LebOuHbO5BPeke0EB7I7aU7EK807XEyHeCQ3El4CztkGr7cY37PiYP5E0bsSs/4dlK534en/oD0dBA82gZ3xcg==} + '@aws-sdk/middleware-flexible-checksums@3.758.0': + resolution: {integrity: sha512-o8Rk71S08YTKLoSobucjnbj97OCGaXgpEDNKXpXaavUM5xLNoHCLSUPRCiEN86Ivqxg1n17Y2nSRhfbsveOXXA==} engines: {node: '>=18.0.0'} '@aws-sdk/middleware-host-header@3.734.0': @@ -1581,36 +1581,36 @@ packages: resolution: {integrity: sha512-mUMFITpJUW3LcKvFok176eI5zXAUomVtahb9IQBwLzkqFYOrMJvWAvoV4yuxrJ8TlQBG8gyEnkb9SnhZvjg67w==} engines: {node: '>=18.0.0'} - '@aws-sdk/middleware-recursion-detection@3.734.0': - resolution: {integrity: sha512-CUat2d9ITsFc2XsmeiRQO96iWpxSKYFjxvj27Hc7vo87YUHRnfMfnc8jw1EpxEwMcvBD7LsRa6vDNky6AjcrFA==} + '@aws-sdk/middleware-recursion-detection@3.772.0': + resolution: {integrity: sha512-zg0LjJa4v7fcLzn5QzZvtVS+qyvmsnu7oQnb86l6ckduZpWDCDC9+A0ZzcXTrxblPCJd3JqkoG1+Gzi4S4Ny/Q==} engines: {node: '>=18.0.0'} - '@aws-sdk/middleware-sdk-s3@3.749.0': - resolution: {integrity: sha512-A7OaNkSFzI2vCRarSKEzoVejAyiLY1M1Z6PR3TE2woAPUUAgRv5UlFOXSymBx1Ya3HUQzWky7mnpnKAFyRGzSg==} + '@aws-sdk/middleware-sdk-s3@3.758.0': + resolution: {integrity: sha512-6mJ2zyyHPYSV6bAcaFpsdoXZJeQlR1QgBnZZ6juY/+dcYiuyWCdyLUbGzSZSE7GTfx6i+9+QWFeoIMlWKgU63A==} engines: {node: '>=18.0.0'} '@aws-sdk/middleware-ssec@3.734.0': resolution: {integrity: sha512-d4yd1RrPW/sspEXizq2NSOUivnheac6LPeLSLnaeTbBG9g1KqIqvCzP1TfXEqv2CrWfHEsWtJpX7oyjySSPvDQ==} engines: {node: '>=18.0.0'} - '@aws-sdk/middleware-user-agent@3.749.0': - resolution: {integrity: sha512-dNRkZtiM8OoGb/h2Fgrgvty9ltYEubzsD5FH+VN14RrluertLQMmqHrgvq7JoAXFf7MJy+uwhRu4V6pf1sZR/w==} + '@aws-sdk/middleware-user-agent@3.758.0': + resolution: {integrity: sha512-iNyehQXtQlj69JCgfaOssgZD4HeYGOwxcaKeG6F+40cwBjTAi0+Ph1yfDwqk2qiBPIRWJ/9l2LodZbxiBqgrwg==} engines: {node: '>=18.0.0'} - '@aws-sdk/nested-clients@3.749.0': - resolution: {integrity: sha512-5L8OuVojcVQAZw+iVXTaw88AZTlMw8fD51lB6spzbZdNLl6dd5Iz1JVJAOUl2mTAZXRiN5Q9VECwY1yMgWweAw==} + '@aws-sdk/nested-clients@3.772.0': + resolution: {integrity: sha512-gNJbBxR5YlEumsCS9EWWEASXEnysL0aDnr9MNPX1ip/g1xOqRHmytgV/+t8RFZFTKg0OprbWTq5Ich3MqsEuCQ==} engines: {node: '>=18.0.0'} '@aws-sdk/region-config-resolver@3.734.0': resolution: {integrity: sha512-Lvj1kPRC5IuJBr9DyJ9T9/plkh+EfKLy+12s/mykOy1JaKHDpvj+XGy2YO6YgYVOb8JFtaqloid+5COtje4JTQ==} engines: {node: '>=18.0.0'} - '@aws-sdk/signature-v4-multi-region@3.749.0': - resolution: {integrity: sha512-8ZR3vnJn1Tgg/qpalkyQMUILzZhw6XV1aCDxYMevqHYu6/f0ujvytJjiUd+j92sfxgN781f6bWpt/jVY0jarxg==} + '@aws-sdk/signature-v4-multi-region@3.758.0': + resolution: {integrity: sha512-0RPCo8fYJcrenJ6bRtiUbFOSgQ1CX/GpvwtLU2Fam1tS9h2klKK8d74caeV6A1mIUvBU7bhyQ0wMGlwMtn3EYw==} engines: {node: '>=18.0.0'} - '@aws-sdk/token-providers@3.749.0': - resolution: {integrity: sha512-s3ExVWoNZan6U7ljMqjiHq3bOe4EqL+U+cVPwqNxAsMaJpGyCiA8VQFlXNGg0EPAFbz/0DVBcozYht6/vyH3sg==} + '@aws-sdk/token-providers@3.772.0': + resolution: {integrity: sha512-d1Waa1vyebuokcAWYlkZdtFlciIgob7B39vPRmtxMObbGumJKiOy/qCe2/FB/72h1Ej9Ih32lwvbxUjORQWN4g==} engines: {node: '>=18.0.0'} '@aws-sdk/types@3.734.0': @@ -1632,8 +1632,8 @@ packages: '@aws-sdk/util-user-agent-browser@3.734.0': resolution: {integrity: sha512-xQTCus6Q9LwUuALW+S76OL0jcWtMOVu14q+GoLnWPUM7QeUw963oQcLhF7oq0CtaLLKyl4GOUfcwc773Zmwwng==} - '@aws-sdk/util-user-agent-node@3.749.0': - resolution: {integrity: sha512-uBzolGGrwvZKhpYlGIy9tw6gRdqVs2zyjjXUiifdgbr3WiQXJe8sE1KhLjzyN/VOPcZB0rY34ybqiKEDOymOeQ==} + '@aws-sdk/util-user-agent-node@3.758.0': + resolution: {integrity: sha512-A5EZw85V6WhoKMV2hbuFRvb9NPlxEErb4HPO6/SPXYY4QrjprIzScHxikqcWv1w4J3apB1wto9LPU3IMsYtfrw==} engines: {node: '>=18.0.0'} peerDependencies: aws-crt: '>=1.0.0' @@ -2167,8 +2167,8 @@ packages: '@fastify/cookie@11.0.2': resolution: {integrity: sha512-GWdwdGlgJxyvNv+QcKiGNevSspMQXncjMZ1J8IvuDQk0jvkzgWWZFNC2En3s+nHndZBGV8IbLwOI/sxCZw/mzA==} - '@fastify/cors@10.0.2': - resolution: {integrity: sha512-DGdxOG36sS/tZv1NFiCJGi7wGuXOSPL2CmNX5PbOVKx0C6LuIALRMrqLByHTCcX1Rbl8NJ9IWlJex32bzydvlw==} + '@fastify/cors@10.1.0': + resolution: {integrity: sha512-MZyBCBJtII60CU9Xme/iE4aEy8G7QpzGR8zkdXZkDFt7ElEMachbE61tfhAG/bvSaULlqlf0huMT12T7iqEmdQ==} '@fastify/deepmerge@2.0.0': resolution: {integrity: sha512-fsaybTGDyQ5KpPsplQqb9yKdCf2x/pbNpMNk8Tvp3rRz7lVcupKysH4b2ELMN2P4Hak1+UqTYdTj/u4FNV2p0g==} @@ -2203,8 +2203,8 @@ packages: '@fastify/send@3.3.1': resolution: {integrity: sha512-6pofeVwaHN+E/MAofCwDqkWUliE3i++jlD0VH/LOfU8TJlCkMUSgKvA9bawDdVXxjve7XrdYMyDmkiYaoGWEtA==} - '@fastify/static@8.1.0': - resolution: {integrity: sha512-lPb8+1ulvbGSUSQ0/adBDyp/Ye/MX+pBwhkLAr8/GU88kNnJlSu7KXdyW6CCOROcr5BgrqJD01lEOosozFAegw==} + '@fastify/static@8.1.1': + resolution: {integrity: sha512-TW9eyVHJLytZNpBlSIqd0bl1giJkEaRaPZG+5AT3L/OBKq9U8D7g/OYmc2NPQZnzPURGhMt3IAWuyVkvd2nOkQ==} '@fastify/view@10.0.2': resolution: {integrity: sha512-tGjXFyDUMj5a+E8BBrQ2wpsVnpOfMq3cqy4WD8pnjWPE/HGNItBASUPoPUcX/QjPhxfuZZTYv2XdCmKXdcMZPw==} @@ -2602,72 +2602,72 @@ packages: resolution: {integrity: sha512-AAwRb5vXFcY4L+FvZ7LZusDuZ0vEe0Zm8ohn1FM6/X7A3bj4mqmkAcGRWuvC2JwSygNwHAAmMnAI73vPHeqsHA==} engines: {node: '>=18'} - '@napi-rs/canvas-android-arm64@0.1.67': - resolution: {integrity: sha512-W+3DFG5h0WU8Vqqb3W5fNmm5/TPH5ECZRinQDK4CAKFSUkc4iZcDwrmyFG9sB4KdHazf1mFVHCpEeVMO6Mk6Zg==} + '@napi-rs/canvas-android-arm64@0.1.68': + resolution: {integrity: sha512-h1KcSR4LKLfRfzeBH65xMxbWOGa1OtMFQbCMVlxPCkN1Zr+2gK+70pXO5ktojIYcUrP6KDcOwoc8clho5ccM/w==} engines: {node: '>= 10'} cpu: [arm64] os: [android] - '@napi-rs/canvas-darwin-arm64@0.1.67': - resolution: {integrity: sha512-xzrv7QboI47yhIHR5P5u/9KGswokuOKLiKSukr1Ku03RRJxP6lGuVtrAZAgdRg7F9FsuF2REf2yK53YVb6pMlA==} + '@napi-rs/canvas-darwin-arm64@0.1.68': + resolution: {integrity: sha512-/VURlrAD4gDoxW1GT/b0nP3fRz/fhxmHI/xznTq2FTwkQLPOlLkDLCvTmQ7v6LtGKdc2Ed6rvYpRan+JXThInQ==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@napi-rs/canvas-darwin-x64@0.1.67': - resolution: {integrity: sha512-SNk9lYBr84N0gW8MZ2IrjygFtbFBILr3SEqMdHzHHuph20SQmssFvJGPZwSSCMEyKAvyqhogbmlew0te5Z4w9Q==} + '@napi-rs/canvas-darwin-x64@0.1.68': + resolution: {integrity: sha512-tEpvGR6vCLTo1Tx9wmDnoOKROpw57wiCWwCpDOuVlj/7rqEJOUYr9ixW4aRJgmeGBrZHgevI0EURys2ER6whmg==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@napi-rs/canvas-linux-arm-gnueabihf@0.1.67': - resolution: {integrity: sha512-qmBlSvUpl567bzH8tNXi82u5FrL4d0qINqd6K9O7GWGGGFmKMJdrgi2/SW3wwCTxqHBasIDdVWc4KSJfwyaoDQ==} + '@napi-rs/canvas-linux-arm-gnueabihf@0.1.68': + resolution: {integrity: sha512-U9xbJsumPOiAYeAFZMlHf62b9dGs2HJ6Q5xt7xTB0uEyPeurwhgYBWGgabdsEidyj38YuzI/c3LGBbSQB3vagw==} engines: {node: '>= 10'} cpu: [arm] os: [linux] - '@napi-rs/canvas-linux-arm64-gnu@0.1.67': - resolution: {integrity: sha512-k3nAPQefkMeFuJ65Rqdnx92KX1JXQhEKjjWeKsCJB+7sIBgQUWtHo9c3etfVLv5pkWJJDFi/Zc2soNkH3E8dRA==} + '@napi-rs/canvas-linux-arm64-gnu@0.1.68': + resolution: {integrity: sha512-KFkn8wEm3mPnWD4l8+OUUkxylSJuN5q9PnJRZJgv15RtCA1bgxIwTkBhI/+xuyVMcHqON9sXq7cDkEJtHm35dg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@napi-rs/canvas-linux-arm64-musl@0.1.67': - resolution: {integrity: sha512-lZwHWR1cCP408l86n3Qbs3X1oFeAYMjJIQvQl1VMZh6wo5PfI+jaZSKBUOd8x44TnVllX9yhLY9unNRztk/sUQ==} + '@napi-rs/canvas-linux-arm64-musl@0.1.68': + resolution: {integrity: sha512-IQzts91rCdOALXBWQxLZRCEDrfFTGDtNRJMNu+2SKZ1uT8cmPQkPwVk5rycvFpvgAcmiFiOSCp1aRrlfU8KPpQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@napi-rs/canvas-linux-riscv64-gnu@0.1.67': - resolution: {integrity: sha512-PdBC9p6bLHA1W3OdA0vTHj701SB/kioGQ1uCFBRMs5KBCaMLb/H4aNi8uaIUIEvBWnxeAjoNcLU7//q0FxEosw==} + '@napi-rs/canvas-linux-riscv64-gnu@0.1.68': + resolution: {integrity: sha512-e9AS5UttoIKqXSmBzKZdd3NErSVyOEYzJfNOCGtafGk1//gibTwQXGlSXmAKuErqMp09pyk9aqQRSYzm1AQfBw==} engines: {node: '>= 10'} cpu: [riscv64] os: [linux] - '@napi-rs/canvas-linux-x64-gnu@0.1.67': - resolution: {integrity: sha512-kJJX6eWzjipL/LdKOWCJctc88e5yzuXri8+s0V/lN06OwuLGW62TWS3lvi8qlUrGMOfRGabSWWlB4omhASSB8w==} + '@napi-rs/canvas-linux-x64-gnu@0.1.68': + resolution: {integrity: sha512-Pa/I36VE3j57I3Obhrr+J48KGFfkZk2cJN/2NmW/vCgmoF7kCP6aTVq5n+cGdGWLd/cN9CJ9JvNwEoMRDghu0g==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@napi-rs/canvas-linux-x64-musl@0.1.67': - resolution: {integrity: sha512-jLKiPWGeN6ZzhnaLG7ex7eexsiHJ1mdtPK1qKvETIcu45dApMXyUIHvdL6XWB5gFFtj5ScHzLUxv1vkfPZsoxA==} + '@napi-rs/canvas-linux-x64-musl@0.1.68': + resolution: {integrity: sha512-9c6rkc5195wNxuUHJdf4/mmnq433OQey9TNvQ9LspJazvHbfSkTij8wtKjASVQsJyPDva4fkWOeV/OQ7cLw0GQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@napi-rs/canvas-win32-x64-msvc@0.1.67': - resolution: {integrity: sha512-K/JmkOFbc4iRZYUqJhj0jwqfHA/wNQEmTiGNsgZ6d59yF/IBNp5T0D5eg3B8ghjI8GxDYCiSJ6DNX8mC3Oh2EQ==} + '@napi-rs/canvas-win32-x64-msvc@0.1.68': + resolution: {integrity: sha512-Fc5Dez23u0FoSATurT6/w1oMytiRnKWEinHivdMvXpge6nG4YvhrASrtqMk8dGJMVQpHr8QJYF45rOrx2YU2Aw==} engines: {node: '>= 10'} cpu: [x64] os: [win32] - '@napi-rs/canvas@0.1.67': - resolution: {integrity: sha512-VA4Khm/5Kg2bQGx3jXotTC4MloOG8b1Ung80exafUK0k5u6yJmIz3Q2iXeeWZs5weV+LQOEB+CPKsYwEYaGAjw==} + '@napi-rs/canvas@0.1.68': + resolution: {integrity: sha512-LQESrePLEBLvhuFkXx9jjBXRC2ClYsO5mqQ1m/puth5z9SOuM3N/B3vDuqnC3RJFktDktyK9khGvo7dTkqO9uQ==} engines: {node: '>= 10'} - '@nestjs/common@11.0.9': - resolution: {integrity: sha512-+SKMYQE7O55tJGQVibnAlR9sXRBCF/8gM1cILdLT7cLj7+51NYD7eAHuAcGYiQIyXsTehZoO7C+B7S3ibNi3aw==} + '@nestjs/common@11.0.12': + resolution: {integrity: sha512-6PXxmDe2iYmb57xacnxzpW1NAxRZ7Gf+acMT7/hmRB/4KpZiFU/cNvLWwgbM2BL5QSzQulOwY6ny5bbKnPpB+A==} peerDependencies: class-transformer: '*' class-validator: '*' @@ -2679,8 +2679,8 @@ packages: class-validator: optional: true - '@nestjs/core@11.0.9': - resolution: {integrity: sha512-w32ZF1acSnidiRERGRC/Ki7eSrnTdP7twRnjy15Qnmij3oYRCdgX5aZvbUNv17A7bn9S22p+wMNcpHxqwUiOsQ==} + '@nestjs/core@11.0.12': + resolution: {integrity: sha512-micQrbh9iL0PuYVx2vsUojuNmMUyqoMCuj7eGAUhvjiZUh4DBLPdxYmJEayCT/equHSiw9vNC95Vm0JigVZ44g==} engines: {node: '>= 20'} peerDependencies: '@nestjs/common': ^11.0.0 @@ -2703,8 +2703,8 @@ packages: '@nestjs/common': ^10.0.0 '@nestjs/core': ^10.0.0 - '@nestjs/testing@11.0.9': - resolution: {integrity: sha512-49UV5tC4N0pLf6waVjBVr/CyTkQPGZG/wLmbmu1INd/brprKOJ2lT3nl+6qfWJIqGbxGqZHUQr5FcDk+T/w9lQ==} + '@nestjs/testing@11.0.12': + resolution: {integrity: sha512-jl1McTqrY+zRBFIWcFMVwesY2v++mAdHrrzXsLxatgkf6wRVh6te1MQ6LikgQ6qz4P5qzVV6EiXQVLGvARe5Xw==} peerDependencies: '@nestjs/common': ^11.0.0 '@nestjs/core': ^11.0.0 @@ -3381,8 +3381,8 @@ packages: resolution: {integrity: sha512-Igfg8lKu3dRVkTSEm98QpZUvKEOa71jDX4vKRcvJVyRc3UgN3j7vFMf0s7xLQhYmKa8kyJGQgUJDOV5V3neVlQ==} engines: {node: '>=18.0.0'} - '@smithy/core@3.1.4': - resolution: {integrity: sha512-wFExFGK+7r2wYriOqe7RRIBNpvxwiS95ih09+GSLRBdoyK/O1uZA7K7pKesj5CBvwJuSBeXwLyR88WwIAY+DGA==} + '@smithy/core@3.1.5': + resolution: {integrity: sha512-HLclGWPkCsekQgsyzxLhCQLa8THWXtB5PxyYN+2O6nkyLt550KQKTlbV2D1/j5dNIQapAZM1+qFnpBFxZQkgCA==} engines: {node: '>=18.0.0'} '@smithy/credential-provider-imds@4.0.1': @@ -3445,12 +3445,12 @@ packages: resolution: {integrity: sha512-OGXo7w5EkB5pPiac7KNzVtfCW2vKBTZNuCctn++TTSOMpe6RZO/n6WEC1AxJINn3+vWLKW49uad3lo/u0WJ9oQ==} engines: {node: '>=18.0.0'} - '@smithy/middleware-endpoint@4.0.5': - resolution: {integrity: sha512-cPzGZV7qStHwboFrm6GfrzQE+YDiCzWcTh4+7wKrP/ZQ4gkw+r7qDjV8GjM4N0UYsuUyLfpzLGg5hxsYTU11WA==} + '@smithy/middleware-endpoint@4.0.6': + resolution: {integrity: sha512-ftpmkTHIFqgaFugcjzLZv3kzPEFsBFSnq1JsIkr2mwFzCraZVhQk2gqN51OOeRxqhbPTkRFj39Qd2V91E/mQxg==} engines: {node: '>=18.0.0'} - '@smithy/middleware-retry@4.0.6': - resolution: {integrity: sha512-s8QzuOQnbdvRymD9Gt9c9zMq10wUQAHQ3z72uirrBHCwZcLTrL5iCOuVTMdka2IXOYhQE890WD5t6G24+F+Qcg==} + '@smithy/middleware-retry@4.0.7': + resolution: {integrity: sha512-58j9XbUPLkqAcV1kHzVX/kAR16GT+j7DUZJqwzsxh1jtz7G82caZiGyyFgUvogVfNTg3TeAOIJepGc8TXF4AVQ==} engines: {node: '>=18.0.0'} '@smithy/middleware-serde@4.0.2': @@ -3469,8 +3469,8 @@ packages: resolution: {integrity: sha512-mVGyPBzkkGQsPoxQUbxlEfRjrj6FPyA3u3u2VXGr9hT8wilsoQdZdvKpMBFMB8Crfhv5dNkKHIW0Yyuc7eABqA==} engines: {node: '>=14.0.0'} - '@smithy/node-http-handler@4.0.2': - resolution: {integrity: sha512-X66H9aah9hisLLSnGuzRYba6vckuFtGE+a5DcHLliI/YlqKrGoxhisD5XbX44KyoeRzoNlGr94eTsMVHFAzPOw==} + '@smithy/node-http-handler@4.0.3': + resolution: {integrity: sha512-dYCLeINNbYdvmMLtW0VdhW1biXt+PPCGazzT5ZjKw46mOtdgToQEwjqZSS9/EN8+tNs/RO0cEWG044+YZs97aA==} engines: {node: '>=18.0.0'} '@smithy/property-provider@4.0.1': @@ -3509,8 +3509,8 @@ packages: resolution: {integrity: sha512-nCe6fQ+ppm1bQuw5iKoeJ0MJfz2os7Ic3GBjOkLOPtavbD1ONoyE3ygjBfz2ythFWm4YnRm6OxW+8p/m9uCoIA==} engines: {node: '>=18.0.0'} - '@smithy/smithy-client@4.1.5': - resolution: {integrity: sha512-DMXYoYeL4QkElr216n1yodTFeATbfb4jwYM9gKn71Rw/FNA1/Sm36tkTSCsZEs7mgpG3OINmkxL9vgVFzyGPaw==} + '@smithy/smithy-client@4.1.6': + resolution: {integrity: sha512-UYDolNg6h2O0L+cJjtgSyKKvEKCOa/8FHYJnBobyeoeWDmNpXjwOAtw16ezyeu1ETuuLEOZbrynK0ZY1Lx9Jbw==} engines: {node: '>=18.0.0'} '@smithy/types@2.12.0': @@ -3549,12 +3549,12 @@ packages: resolution: {integrity: sha512-L1RBVzLyfE8OXH+1hsJ8p+acNUSirQnWQ6/EgpchV88G6zGBTDPdXiiExei6Z1wR2RxYvxY/XLw6AMNCCt8H3w==} engines: {node: '>=18.0.0'} - '@smithy/util-defaults-mode-browser@4.0.6': - resolution: {integrity: sha512-N8+VCt+piupH1A7DgSVDNrVHqRLz8r6DvBkpS7EWHiIxsUk4jqGuQLjqC/gnCzmwGkVBdNruHoYAzzaSQ8e80w==} + '@smithy/util-defaults-mode-browser@4.0.7': + resolution: {integrity: sha512-CZgDDrYHLv0RUElOsmZtAnp1pIjwDVCSuZWOPhIOBvG36RDfX1Q9+6lS61xBf+qqvHoqRjHxgINeQz47cYFC2Q==} engines: {node: '>=18.0.0'} - '@smithy/util-defaults-mode-node@4.0.6': - resolution: {integrity: sha512-9zhx1shd1VwSSVvLZB8CM3qQ3RPD3le7A3h/UPuyh/PC7g4OaWDi2xUNzamsVoSmCGtmUBONl56lM2EU6LcH7A==} + '@smithy/util-defaults-mode-node@4.0.7': + resolution: {integrity: sha512-79fQW3hnfCdrfIi1soPbK3zmooRFnLpSx3Vxi6nUlqaaQeC5dm8plt4OTNDNqEEEDkvKghZSaoti684dQFVrGQ==} engines: {node: '>=18.0.0'} '@smithy/util-endpoints@3.0.1': @@ -3573,8 +3573,8 @@ packages: resolution: {integrity: sha512-WmRHqNVwn3kI3rKk1LsKcVgPBG6iLTBGC1iYOV3GQegwJ3E8yjzHytPt26VNzOWr1qu0xE03nK0Ug8S7T7oufw==} engines: {node: '>=18.0.0'} - '@smithy/util-stream@4.1.1': - resolution: {integrity: sha512-+Xvh8nhy0Wjv1y71rBVyV3eJU3356XsFQNI8dEZVNrQju7Eib8G31GWtO+zMa9kTCGd41Mflu+ZKfmQL/o2XzQ==} + '@smithy/util-stream@4.1.2': + resolution: {integrity: sha512-44PKEqQ303d3rlQuiDpcCcu//hV8sn+u2JBo84dWCE0rvgeiVl0IlLMagbU++o0jCWhYCsHaAt9wZuZqNe05Hw==} engines: {node: '>=18.0.0'} '@smithy/util-uri-escape@2.2.0': @@ -3832,14 +3832,14 @@ packages: cpu: [arm64] os: [android] - '@swc/core-darwin-arm64@1.10.16': - resolution: {integrity: sha512-iikIxwqCQ4Bvz79vJ4ELh26efPf1u5D9TFdmXSJUBs7C3mmMHvk5zyWD9A9cTowXiW6WHs2gE58U1R9HOTTIcg==} + '@swc/core-darwin-arm64@1.11.11': + resolution: {integrity: sha512-vJcjGVDB8cZH7zyOkC0AfpFYI/7GHKG0NSsH3tpuKrmoAXJyCYspKPGid7FT53EAlWreN7+Pew+bukYf5j+Fmg==} engines: {node: '>=10'} cpu: [arm64] os: [darwin] - '@swc/core-darwin-x64@1.10.16': - resolution: {integrity: sha512-R2Eb9aktWd62vPfW9H/c/OaQ0e94iURibBo4uzUUcgxNNmB4+wb6piKbHxGdr/5bEsT+vJ1lwZFSRzfb45E7DA==} + '@swc/core-darwin-x64@1.11.11': + resolution: {integrity: sha512-/N4dGdqEYvD48mCF3QBSycAbbQd3yoZ2YHSzYesQf8usNc2YpIhYqEH3sql02UsxTjEFOJSf1bxZABDdhbSl6A==} engines: {node: '>=10'} cpu: [x64] os: [darwin] @@ -3850,56 +3850,56 @@ packages: cpu: [x64] os: [freebsd] - '@swc/core-linux-arm-gnueabihf@1.10.16': - resolution: {integrity: sha512-mkqN3HBAMnuiSGZ/k2utScuH8rAPshvNj0T1LjBWon+X9DkMNHSA+aMLdWsy0yZKF1zjOPc4L3Uq2l2wzhUlzA==} + '@swc/core-linux-arm-gnueabihf@1.11.11': + resolution: {integrity: sha512-hsBhKK+wVXdN3x9MrL5GW0yT8o9GxteE5zHAI2HJjRQel3HtW7m5Nvwaq+q8rwMf4YQRd8ydbvwl4iUOZx7i2Q==} engines: {node: '>=10'} cpu: [arm] os: [linux] - '@swc/core-linux-arm64-gnu@1.10.16': - resolution: {integrity: sha512-PH/+q/L5nVZJ91CU07CL6Q9Whs6iR6nneMZMAgtVF9Ix8ST0cWVItdUhs6D38kFklCFhaOrpHhS01HlMJ72vWw==} + '@swc/core-linux-arm64-gnu@1.11.11': + resolution: {integrity: sha512-YOCdxsqbnn/HMPCNM6nrXUpSndLXMUssGTtzT7ffXqr7WuzRg2e170FVDVQFIkb08E7Ku5uOnnUVAChAJQbMOQ==} engines: {node: '>=10'} cpu: [arm64] os: [linux] - '@swc/core-linux-arm64-musl@1.10.16': - resolution: {integrity: sha512-1169+C9XbydKKc6Ec1XZxTGKtHjZHDIFn0r+Nqp/QSVwkORrOY1Vz2Hdu7tn/lWMg36ZkGePS+LnnyV67s/7yg==} + '@swc/core-linux-arm64-musl@1.11.11': + resolution: {integrity: sha512-nR2tfdQRRzwqR2XYw9NnBk9Fdvff/b8IiJzDL28gRR2QiJWLaE8LsRovtWrzCOYq6o5Uu9cJ3WbabWthLo4jLw==} engines: {node: '>=10'} cpu: [arm64] os: [linux] - '@swc/core-linux-x64-gnu@1.10.16': - resolution: {integrity: sha512-n2rV0XwkjoHn4MDJmpYp5RBrnyi94/6GsJVpbn6f+/eqSrZn3mh3dT7pdZc9zCN1Qp9eDHo+uI6e/wgvbL22uA==} + '@swc/core-linux-x64-gnu@1.11.11': + resolution: {integrity: sha512-b4gBp5HA9xNWNC5gsYbdzGBJWx4vKSGybGMGOVWWuF+ynx10+0sA/o4XJGuNHm8TEDuNh9YLKf6QkIO8+GPJ1g==} engines: {node: '>=10'} cpu: [x64] os: [linux] - '@swc/core-linux-x64-musl@1.10.16': - resolution: {integrity: sha512-EevCpwreBrkPrJjQVIbiM81lK42ukNNSlBmrSRxxbx2V9VGmOd5qxX0cJBn0TRRSLIPi62BuMS76F9iYjqsjgg==} + '@swc/core-linux-x64-musl@1.11.11': + resolution: {integrity: sha512-dEvqmQVswjNvMBwXNb8q5uSvhWrJLdttBSef3s6UC5oDSwOr00t3RQPzyS3n5qmGJ8UMTdPRmsopxmqaODISdg==} engines: {node: '>=10'} cpu: [x64] os: [linux] - '@swc/core-win32-arm64-msvc@1.10.16': - resolution: {integrity: sha512-BvE7RWAnKJeELVQWLok6env5I4GUVBTZSvaSN/VPgxnTjF+4PsTeQptYx0xCYhp5QCv68wWYsBnZKuPDS+SBsw==} + '@swc/core-win32-arm64-msvc@1.11.11': + resolution: {integrity: sha512-aZNZznem9WRnw2FbTqVpnclvl8Q2apOBW2B316gZK+qxbe+ktjOUnYaMhdCG3+BYggyIBDOnaJeQrXbKIMmNdw==} engines: {node: '>=10'} cpu: [arm64] os: [win32] - '@swc/core-win32-ia32-msvc@1.10.16': - resolution: {integrity: sha512-7Jf/7AeCgbLR/JsQgMJuacHIq4Jeie3knf6+mXxn8aCvRypsOTIEu0eh7j24SolOboxK1ijqJ86GyN1VA2Rebg==} + '@swc/core-win32-ia32-msvc@1.11.11': + resolution: {integrity: sha512-DjeJn/IfjgOddmJ8IBbWuDK53Fqw7UvOz7kyI/728CSdDYC3LXigzj3ZYs4VvyeOt+ZcQZUB2HA27edOifomGw==} engines: {node: '>=10'} cpu: [ia32] os: [win32] - '@swc/core-win32-x64-msvc@1.10.16': - resolution: {integrity: sha512-p0blVm0R8bjaTtmW+FoPmLxLSQdRNbqhuWcR/8g80OzMSkka9mk5/J3kn/5JRVWh+MaR9LHRHZc1Q1L8zan13g==} + '@swc/core-win32-x64-msvc@1.11.11': + resolution: {integrity: sha512-Gp/SLoeMtsU4n0uRoKDOlGrRC6wCfifq7bqLwSlAG8u8MyJYJCcwjg7ggm0rhLdC2vbiZ+lLVl3kkETp+JUvKg==} engines: {node: '>=10'} cpu: [x64] os: [win32] - '@swc/core@1.10.16': - resolution: {integrity: sha512-nOINg/OUcZazCW7B55QV2/UB8QAqz9FYe4+z229+4RYboBTZ102K7ebOEjY5sKn59JgAkhjZTz+5BKmXpDFopw==} + '@swc/core@1.11.11': + resolution: {integrity: sha512-pCVY2Wn6dV/labNvssk9b3Owi4WOYsapcbWm90XkIj4xH/56Z6gzja9fsU+4MdPuEfC2Smw835nZHcdCFGyX6A==} engines: {node: '>=10'} peerDependencies: '@swc/helpers': '*' @@ -3916,8 +3916,8 @@ packages: peerDependencies: '@swc/core': '*' - '@swc/types@0.1.17': - resolution: {integrity: sha512-V5gRru+aD8YVyCOMAjMpWR1Ui577DD5KSJsHP8RAxopAH22jFz6GZd/qxqjO6MJHQhcsjvjOFXyDhyLQUnMveQ==} + '@swc/types@0.1.19': + resolution: {integrity: sha512-WkAZaAfj44kh/UFdAQcrMP1I0nwRqpt27u+08LMBYMqmQfwwMofYoMh/48NGkMMRfC4ynpfwRbJuu8ErfNloeA==} '@swc/wasm@1.2.130': resolution: {integrity: sha512-rNcJsBxS70+pv8YUWwf5fRlWX6JoY/HJc25HD/F8m6Kv7XhJdqPPMhyX6TKkUBPAG7TWlZYoxa+rHAjPy4Cj3Q==} @@ -4188,9 +4188,6 @@ packages: '@types/node@22.13.10': resolution: {integrity: sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==} - '@types/node@22.13.4': - resolution: {integrity: sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg==} - '@types/node@22.13.9': resolution: {integrity: sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==} @@ -4338,9 +4335,6 @@ packages: '@types/ws@8.18.0': resolution: {integrity: sha512-8svvI3hMyvN0kKCJMvTJP/x6Y/EoQbepff882wL+Sn5QsXb3etnamgrJq4isrBxSJj5L2AuXcI0+bgkoAXGUJw==} - '@types/ws@8.5.14': - resolution: {integrity: sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw==} - '@types/yargs-parser@21.0.0': resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==} @@ -4350,14 +4344,6 @@ packages: '@types/yauzl@2.10.0': resolution: {integrity: sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==} - '@typescript-eslint/eslint-plugin@8.24.0': - resolution: {integrity: sha512-aFcXEJJCI4gUdXgoo/j9udUYIHgF23MFkg09LFz2dzEmU0+1Plk4rQWv/IYKvPHAtlkkGoB3m5e6oUp+JPsNaQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.8.0' - '@typescript-eslint/eslint-plugin@8.26.0': resolution: {integrity: sha512-cLr1J6pe56zjKYajK6SSSre6nl1Gj6xDp1TY0trpgPzjVbgDwd09v2Ws37LABxzkicmUjhEeg/fAUjPJJB1v5Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -4366,12 +4352,13 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/parser@8.24.0': - resolution: {integrity: sha512-MFDaO9CYiard9j9VepMNa9MTcqVvSny2N4hkY6roquzj8pdCBRENhErrteaQuu7Yjn1ppk0v1/ZF9CG3KIlrTA==} + '@typescript-eslint/eslint-plugin@8.27.0': + resolution: {integrity: sha512-4henw4zkePi5p252c8ncBLzLce52SEUz2Ebj8faDnuUXz2UuHEONYcJ+G0oaCF+bYCWVZtrGzq3FD7YXetmnSA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: + '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.8.0' + typescript: '>=4.8.4 <5.9.0' '@typescript-eslint/parser@8.26.0': resolution: {integrity: sha512-mNtXP9LTVBy14ZF3o7JG69gRPBK/2QWtQd0j0oH26HcY/foyJJau6pNUez7QrM5UHnSvwlQcJXKsk0I99B9pOA==} @@ -4380,20 +4367,20 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/scope-manager@8.24.0': - resolution: {integrity: sha512-HZIX0UByphEtdVBKaQBgTDdn9z16l4aTUz8e8zPQnyxwHBtf5vtl1L+OhH+m1FGV9DrRmoDuYKqzVrvWDcDozw==} + '@typescript-eslint/parser@8.27.0': + resolution: {integrity: sha512-XGwIabPallYipmcOk45DpsBSgLC64A0yvdAkrwEzwZ2viqGqRUJ8eEYoPz0CWnutgAFbNMPdsGGvzjSmcWVlEA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' '@typescript-eslint/scope-manager@8.26.0': resolution: {integrity: sha512-E0ntLvsfPqnPwng8b8y4OGuzh/iIOm2z8U3S9zic2TeMLW61u5IH2Q1wu0oSTkfrSzwbDJIB/Lm8O3//8BWMPA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/type-utils@8.24.0': - resolution: {integrity: sha512-8fitJudrnY8aq0F1wMiPM1UUgiXQRJ5i8tFjq9kGfRajU+dbPyOuHbl0qRopLEidy0MwqgTHDt6CnSeXanNIwA==} + '@typescript-eslint/scope-manager@8.27.0': + resolution: {integrity: sha512-8oI9GwPMQmBryaaxG1tOZdxXVeMDte6NyJA4i7/TWa4fBwgnAXYlIQP+uYOeqAaLJ2JRxlG9CAyL+C+YE9Xknw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.8.0' '@typescript-eslint/type-utils@8.26.0': resolution: {integrity: sha512-ruk0RNChLKz3zKGn2LwXuVoeBcUMh+jaqzN461uMMdxy5H9epZqIBtYj7UiPXRuOpaALXGbmRuZQhmwHhaS04Q==} @@ -4402,19 +4389,20 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/types@8.24.0': - resolution: {integrity: sha512-VacJCBTyje7HGAw7xp11q439A+zeGG0p0/p2zsZwpnMzjPB5WteaWqt4g2iysgGFafrqvyLWqq6ZPZAOCoefCw==} + '@typescript-eslint/type-utils@8.27.0': + resolution: {integrity: sha512-wVArTVcz1oJOIEJxui/nRhV0TXzD/zMSOYi/ggCfNq78EIszddXcJb7r4RCp/oBrjt8n9A0BSxRMKxHftpDxDA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' '@typescript-eslint/types@8.26.0': resolution: {integrity: sha512-89B1eP3tnpr9A8L6PZlSjBvnJhWXtYfZhECqlBl1D9Lme9mHO6iWlsprBtVenQvY1HMhax1mWOjhtL3fh/u+pA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.24.0': - resolution: {integrity: sha512-ITjYcP0+8kbsvT9bysygfIfb+hBj6koDsu37JZG7xrCiy3fPJyNmfVtaGsgTUSEuTzcvME5YI5uyL5LD1EV5ZQ==} + '@typescript-eslint/types@8.27.0': + resolution: {integrity: sha512-/6cp9yL72yUHAYq9g6DsAU+vVfvQmd1a8KyA81uvfDE21O2DwQ/qxlM4AR8TSdAu+kJLBDrEHKC5/W2/nxsY0A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <5.8.0' '@typescript-eslint/typescript-estree@8.26.0': resolution: {integrity: sha512-tiJ1Hvy/V/oMVRTbEOIeemA2XoylimlDQ03CgPPNaHYZbpsc78Hmngnt+WXZfJX1pjQ711V7g0H7cSJThGYfPQ==} @@ -4422,12 +4410,11 @@ packages: peerDependencies: typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/utils@8.24.0': - resolution: {integrity: sha512-07rLuUBElvvEb1ICnafYWr4hk8/U7X9RDCOqd9JcAMtjh/9oRmcfN4yGzbPVirgMR0+HLVHehmu19CWeh7fsmQ==} + '@typescript-eslint/typescript-estree@8.27.0': + resolution: {integrity: sha512-BnKq8cqPVoMw71O38a1tEb6iebEgGA80icSxW7g+kndx0o6ot6696HjG7NdgfuAVmVEtwXUr3L8R9ZuVjoQL6A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.8.0' + typescript: '>=4.8.4 <5.9.0' '@typescript-eslint/utils@8.26.0': resolution: {integrity: sha512-2L2tU3FVwhvU14LndnQCA2frYC8JnPDVKyQtWFPf8IYFMt/ykEN1bPolNhNbCVgOmdzTlWdusCTKA/9nKrf8Ig==} @@ -4436,14 +4423,21 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/visitor-keys@8.24.0': - resolution: {integrity: sha512-kArLq83QxGLbuHrTMoOEWO+l2MwsNS2TGISEdx8xgqpkbytB07XmlQyQdNDrCc1ecSqx0cnmhGvpX+VBwqqSkg==} + '@typescript-eslint/utils@8.27.0': + resolution: {integrity: sha512-njkodcwH1yvmo31YWgRHNb/x1Xhhq4/m81PhtvmRngD8iHPehxffz1SNCO+kwaePhATC+kOa/ggmvPoPza5i0Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' '@typescript-eslint/visitor-keys@8.26.0': resolution: {integrity: sha512-2z8JQJWAzPdDd51dRQ/oqIJxe99/hoLIqmf8RMCAJQtYDc535W/Jt2+RTP4bP0aKeBG1F65yjIZuczOXCmbWwg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/visitor-keys@8.27.0': + resolution: {integrity: sha512-WsXQwMkILJvffP6z4U3FYJPlbf/j07HIxmDjZpbNvBJkMfvwXj5ACRkkHwBDvLBbDbtX5TdU64/rcvKJ/vuInQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@ungap/structured-clone@1.2.0': resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} @@ -4770,6 +4764,10 @@ packages: resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} engines: {node: '>=12'} + ansis@3.17.0: + resolution: {integrity: sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==} + engines: {node: '>=14'} + any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} @@ -5079,8 +5077,8 @@ packages: resolution: {integrity: sha512-WDtdLmJvAuNNPzByAYpRo2rF1Mmradw6gvWsQKf63476DDXmomT9zUiGypLcG4ibIM67vhAj8jJRdbmEws2Aqw==} engines: {node: '>=6.14.2'} - bullmq@5.41.1: - resolution: {integrity: sha512-bVQGR4ARM+wRJSw66114AKwO8SwS2ZF5TIvwQ9NL6Iepq6f8jnG8EjMMXL8J1pyR1eNz5bOikPeED/8ErLU6FQ==} + bullmq@5.44.1: + resolution: {integrity: sha512-NEWQIixKw5QdXmSnhQU25A336N10DvxuDcmiVXQyuwSx1lrzfNIdQA3BNADfpAUwHtuJkJImHVLCM3/KEr9nWA==} buraha@0.0.1: resolution: {integrity: sha512-G563A0mTbzknm2jDaNxfZuNKIdeArs8T+XQN6t+KbmgnOoevXSXhKDkyf8Md/36Jrx99ikwbCag37VGe3myExQ==} @@ -5638,15 +5636,6 @@ packages: supports-color: optional: true - debug@4.3.4: - resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - debug@4.3.5: resolution: {integrity: sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==} engines: {node: '>=6.0'} @@ -6567,11 +6556,6 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} - glob@10.3.10: - resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==} - engines: {node: '>=16 || 14 >=14.17'} - hasBin: true - glob@10.4.5: resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} hasBin: true @@ -6928,8 +6912,8 @@ packages: intersection-observer@0.12.2: resolution: {integrity: sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg==} - ioredis@5.5.0: - resolution: {integrity: sha512-7CutT89g23FfSa8MDoIFs2GYYa0PaNiW/OrT+nRyjRXHDZd17HmIgy+reOQ/yhh72NznNjGuS8kbCAcA4Ro4mw==} + ioredis@5.6.0: + resolution: {integrity: sha512-tBZlIIWbndeWBWCXWZiqtOF/yxf6yZX3tAlTJ7nfo5jhd6dctNxF7QnYlZLZ1a0o0pDoen7CgZqO+zjNaFbJAg==} engines: {node: '>=12.22.0'} ip-address@9.0.5: @@ -7199,10 +7183,6 @@ packages: resolution: {integrity: sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==} engines: {node: '>=6'} - jackspeak@2.3.6: - resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} - engines: {node: '>=14'} - jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} @@ -7468,8 +7448,8 @@ packages: jstransformer@1.0.0: resolution: {integrity: sha512-C9YK3Rf8q6VAPDCCU9fnqo3mAfOH6vUGnMcP4AQAYIEpWtfGLpwOTmZ+igtdK5y+VvI2n3CyYSzy4Qh34eq24A==} - juice@11.0.0: - resolution: {integrity: sha512-sGF8hPz9/Wg+YXbaNDqc1Iuoaw+J/P9lBHNQKXAGc9pPNjCd4fyPai0Zxj7MRtdjMr0lcgk5PjEIkP2b8R9F3w==} + juice@11.0.1: + resolution: {integrity: sha512-R3KLud4l/sN9AMmFZs0QY7cugGSiKvPhGyIsufCV5nJ0MjSlngUE7k80TmFeK9I62wOXrjWBtYA1knVs2OkF8w==} engines: {node: '>=18.17'} hasBin: true @@ -7596,10 +7576,6 @@ packages: resolution: {integrity: sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - lru-cache@10.2.2: - resolution: {integrity: sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==} - engines: {node: 14 || >=16.14} - lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -7724,8 +7700,8 @@ packages: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} - meilisearch@0.48.2: - resolution: {integrity: sha512-auDB6grs3f/+dVzzPiAOOFnrVN/L87/+SWZfy56TpimDinWjU4u6Jc2o6lgeLluotpbMOFY1WpTTbqtUdMduQw==} + meilisearch@0.49.0: + resolution: {integrity: sha512-oMJ/e6Or6cz2+owcEKeB11p2OWiWW9NmssqOZC/KIwQB0sBGKLJ7RCpYzf+GhUIZIZ9FRYZ419ox3RGebVQX5g==} memoizerific@1.11.3: resolution: {integrity: sha512-/EuHYwAPdLtXwAwSZkh/Gutery6pD2KYd44oQLhAvQp/50mpyduZh8Q7PYHXTCJ+wuXxt7oij2LXyIJOOYFPog==} @@ -7973,18 +7949,13 @@ packages: engines: {node: '>=10'} hasBin: true - mkdirp@2.1.6: - resolution: {integrity: sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==} - engines: {node: '>=10'} - hasBin: true - mkdirp@3.0.1: resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} engines: {node: '>=10'} hasBin: true - mnemonist@0.39.8: - resolution: {integrity: sha512-vyWo2K3fjrUw8YeeZ1zF0fy6Mu59RHokURlld8ymdUPjMlD9EC9ov1/YPqTgqRvUN9nTr3Gqfz29LYAmu0PHPQ==} + mnemonist@0.40.0: + resolution: {integrity: sha512-kdd8AFNig2AD5Rkih7EPCXhu/iMvwevQFX/uEiGhZyPZi7fHqOoF4V4kHLpCfysxXMgQ4B52kdPMCwARshKvEg==} mock-socket@9.3.1: resolution: {integrity: sha512-qxBgB7Qa2sEQgHFjj0dSigq7fX4k6Saisd5Nelwp2q8mlbAFh5dHV9JTTlF8viYJLSSWgMCZFUom8PJcMNBoJw==} @@ -8054,8 +8025,8 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - nanoid@5.1.0: - resolution: {integrity: sha512-zDAl/llz8Ue/EblwSYwdxGBYfj46IM1dhjVi8dyp9LQffoIGxJEAHj2oeZ4uNcgycSRcQ83CnfcZqEJzVDLcDw==} + nanoid@5.1.5: + resolution: {integrity: sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==} engines: {node: ^18 || >=20} hasBin: true @@ -8232,8 +8203,8 @@ packages: resolution: {integrity: sha512-j4XtFDQUBsvUHPjUmvmNDUDMYed2MphMIJBhyxVVe8hGCjkuYnjIsW+D9qk8c5ciXRdnk6x6tEbiO6PLeOZdCQ==} engines: {node: '>= 0.4.0'} - oauth@0.10.0: - resolution: {integrity: sha512-1orQ9MT1vHFGQxhuy7E/0gECD3fd2fCC+PIX+/jgmU/gI3EpRocXtmtvxCO5x3WZ443FLTLFWNDjl5MPJf9u+Q==} + oauth@0.10.2: + resolution: {integrity: sha512-JtFnB+8nxDEXgNyniwz573xxbKSOu3R8D40xQKqcjwJ2CDkYqUDI53o6IuzDJBx60Z8VKCm271+t8iFjakrl8Q==} object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} @@ -8435,10 +8406,6 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - path-scurry@1.10.1: - resolution: {integrity: sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==} - engines: {node: '>=16 || 14 >=14.17'} - path-scurry@1.11.1: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} @@ -8498,8 +8465,8 @@ packages: resolution: {integrity: sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==} engines: {node: '>=4'} - pg-pool@3.7.1: - resolution: {integrity: sha512-xIOsFoh7Vdhojas6q3596mXFsR8nwBQBXX5JiV7p9buEVAGqYL4yFzclON5P9vFrpu1u7Zwl2oriyDa89n0wbw==} + pg-pool@3.8.0: + resolution: {integrity: sha512-VBw3jiVm6ZOdLBTIcXLNdSotb6Iy3uOCwDGFAksZCXmi10nyRvnP2v3jl4d+IsLYRyXf6o9hIm/ZtUzlByNUdw==} peerDependencies: pg: '>=8.0' @@ -8509,6 +8476,9 @@ packages: pg-protocol@1.7.1: resolution: {integrity: sha512-gjTHWGYWsEgy9MsY0Gp6ZJxV24IjDqdpTW7Eh0x+WfJLFsm/TJx1MzL6T0D88mBvkpxotCQ6TwW6N+Kko7lhgQ==} + pg-protocol@1.8.0: + resolution: {integrity: sha512-jvuYlEkL03NRvOoyoRktBK7+qU5kOvlAwvmrH8sr3wbLrOdVWsRxQfz8mMy9sZFsqJ1hEWNfdWKI4SAmoL+j7g==} + pg-types@2.2.0: resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} engines: {node: '>=4'} @@ -8517,8 +8487,8 @@ packages: resolution: {integrity: sha512-hRCSDuLII9/LE3smys1hRHcu5QGcLs9ggT7I/TCs0IE+2Eesxi9+9RWAAwZ0yaGjxoWICF/YHLOEjydGujoJ+g==} engines: {node: '>=10'} - pg@8.13.3: - resolution: {integrity: sha512-P6tPt9jXbL9HVu/SSRERNYaYG++MjnscnegFh9pPHihfoBSujsrka0hyuymMzeJKFWrcG8wvCKy8rCe8e5nDUQ==} + pg@8.14.1: + resolution: {integrity: sha512-0TdbqfjwIun9Fm/r89oB7RFQ0bLgduAhiIqIXOsyKoiC/L54DbuAAzIEN/9Op0f1Po9X7iCPXGoa/Ah+2aI8Xw==} engines: {node: '>= 8.0.0'} peerDependencies: pg-native: '>=3.0.1' @@ -9262,8 +9232,8 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - rxjs@7.8.1: - resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} safe-array-concat@1.0.0: resolution: {integrity: sha512-9dVEFruWIsnie89yym+xWTAYASdpw3CJV7Li/6zBewGf9z2i1j31rP6jnY0pHEO4QZh6N0K11bFjWmdR8UGdPQ==} @@ -9299,6 +9269,9 @@ packages: sanitize-html@2.14.0: resolution: {integrity: sha512-CafX+IUPxZshXqqRaG9ZClSlfPVjSxI0td7n07hk8QO2oO+9JDnlcL8iM8TWeOXOIBFgIOx6zioTzM53AOMn3g==} + sanitize-html@2.15.0: + resolution: {integrity: sha512-wIjst57vJGpLyBP8ioUbg6ThwJie5SuSIjHxJg53v5Fg+kUK+AXlb7bK3RNXpp315MvwM+0OBGCV6h5pPHsVhA==} + sass@1.85.1: resolution: {integrity: sha512-Uk8WpxM5v+0cMR0XjX9KfRIacmSG86RH4DCCZjLU2rFh5tyutt9siAXJ7G+YfxQ99Q6wrRMbMlVl6KqUms71ag==} engines: {node: '>=14.0.0'} @@ -9607,6 +9580,10 @@ packages: sprintf-js@1.1.3: resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} + sql-highlight@6.0.0: + resolution: {integrity: sha512-+fLpbAbWkQ+d0JEchJT/NrRRXbYRNbG15gFpANx73EwxQB1PRjj+k/OI0GTU0J63g8ikGkJECQp9z8XEJZvPRw==} + engines: {node: '>=14'} + sshpk@1.17.0: resolution: {integrity: sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==} engines: {node: '>=0.10.0'} @@ -10025,10 +10002,6 @@ packages: ts-map@1.0.3: resolution: {integrity: sha512-vDWbsl26LIcPGmDpoVzjEP6+hvHZkBkLW7JpvwbCv/5IYPJlsbzCVXY3wsCeAxAUeTclNOUZxnLdGh3VBD/J6w==} - tsc-alias@1.8.10: - resolution: {integrity: sha512-Ibv4KAWfFkFdKJxnWfVtdOmB0Zi1RJVxcbPGiCDsFpCQSsmpWyuzHG3rQyI5YkobWwxFPEyQfu1hdo4qLG2zPw==} - hasBin: true - tsc-alias@1.8.11: resolution: {integrity: sha512-2DuEQ58A9Rj2NE2c1+/qaGKlshni9MCK95MJzRGhQG0CYLw0bE/ACgbhhTSf/p1svLelwqafOd8stQate2bYbg==} hasBin: true @@ -10132,24 +10105,25 @@ packages: typedarray@0.0.6: resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} - typeorm@0.3.20: - resolution: {integrity: sha512-sJ0T08dV5eoZroaq9uPKBoNcGslHBR4E4y+EBHs//SiGbblGe7IeduP/IH4ddCcj0qp3PHwDwGnuvqEAnKlq/Q==} + typeorm@0.3.21: + resolution: {integrity: sha512-lh4rUWl1liZGjyPTWpwcK8RNI5x4ekln+/JJOox1wCd7xbucYDOXWD+1cSzTN3L0wbTGxxOtloM5JlxbOxEufA==} engines: {node: '>=16.13.0'} hasBin: true peerDependencies: '@google-cloud/spanner': ^5.18.0 '@sap/hana-client': ^2.12.25 - better-sqlite3: ^7.1.2 || ^8.0.0 || ^9.0.0 + better-sqlite3: ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 hdb-pool: ^0.1.6 ioredis: ^5.0.4 mongodb: ^5.8.0 - mssql: ^9.1.1 || ^10.0.1 + mssql: ^9.1.1 || ^10.0.1 || ^11.0.1 mysql2: ^2.2.5 || ^3.0.1 oracledb: ^6.3.0 pg: ^8.5.1 pg-native: ^3.0.0 pg-query-stream: ^4.0.0 redis: ^3.1.1 || ^4.0.0 + reflect-metadata: ^0.1.14 || ^0.2.0 sql.js: ^1.4.0 sqlite3: ^5.0.3 ts-node: ^10.7.0 @@ -10211,8 +10185,8 @@ packages: resolution: {integrity: sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ==} engines: {node: '>=18'} - ulid@2.3.0: - resolution: {integrity: sha512-keqHubrlpvT6G2wH0OEfSW4mquYRcbe/J8NMmveoQOjUqmo+hXtO+ORCpWhdbZ7k72UtY61BL7haGxW6enBnjw==} + ulid@2.4.0: + resolution: {integrity: sha512-fIRiVTJNcSRmXKPZtGzFQv9WRrZ3M9eoptl/teFJvjOzmpU+/K/JH6HZ8deBfb5vMEpicJcLn7JmvdknlMq7Zg==} hasBin: true unbox-primitive@1.0.2: @@ -10664,8 +10638,8 @@ packages: resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - ws@8.18.0: - resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} + ws@8.18.1: + resolution: {integrity: sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -10875,32 +10849,32 @@ snapshots: '@smithy/util-utf8': 2.0.0 tslib: 2.8.1 - '@aws-sdk/client-s3@3.749.0': + '@aws-sdk/client-s3@3.772.0': dependencies: '@aws-crypto/sha1-browser': 5.2.0 '@aws-crypto/sha256-browser': 5.2.0 '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.749.0 - '@aws-sdk/credential-provider-node': 3.749.0 + '@aws-sdk/core': 3.758.0 + '@aws-sdk/credential-provider-node': 3.772.0 '@aws-sdk/middleware-bucket-endpoint': 3.734.0 '@aws-sdk/middleware-expect-continue': 3.734.0 - '@aws-sdk/middleware-flexible-checksums': 3.749.0 + '@aws-sdk/middleware-flexible-checksums': 3.758.0 '@aws-sdk/middleware-host-header': 3.734.0 '@aws-sdk/middleware-location-constraint': 3.734.0 '@aws-sdk/middleware-logger': 3.734.0 - '@aws-sdk/middleware-recursion-detection': 3.734.0 - '@aws-sdk/middleware-sdk-s3': 3.749.0 + '@aws-sdk/middleware-recursion-detection': 3.772.0 + '@aws-sdk/middleware-sdk-s3': 3.758.0 '@aws-sdk/middleware-ssec': 3.734.0 - '@aws-sdk/middleware-user-agent': 3.749.0 + '@aws-sdk/middleware-user-agent': 3.758.0 '@aws-sdk/region-config-resolver': 3.734.0 - '@aws-sdk/signature-v4-multi-region': 3.749.0 + '@aws-sdk/signature-v4-multi-region': 3.758.0 '@aws-sdk/types': 3.734.0 '@aws-sdk/util-endpoints': 3.743.0 '@aws-sdk/util-user-agent-browser': 3.734.0 - '@aws-sdk/util-user-agent-node': 3.749.0 + '@aws-sdk/util-user-agent-node': 3.758.0 '@aws-sdk/xml-builder': 3.734.0 '@smithy/config-resolver': 4.0.1 - '@smithy/core': 3.1.4 + '@smithy/core': 3.1.5 '@smithy/eventstream-serde-browser': 4.0.1 '@smithy/eventstream-serde-config-resolver': 4.0.1 '@smithy/eventstream-serde-node': 4.0.1 @@ -10911,66 +10885,66 @@ snapshots: '@smithy/invalid-dependency': 4.0.1 '@smithy/md5-js': 4.0.1 '@smithy/middleware-content-length': 4.0.1 - '@smithy/middleware-endpoint': 4.0.5 - '@smithy/middleware-retry': 4.0.6 + '@smithy/middleware-endpoint': 4.0.6 + '@smithy/middleware-retry': 4.0.7 '@smithy/middleware-serde': 4.0.2 '@smithy/middleware-stack': 4.0.1 '@smithy/node-config-provider': 4.0.1 - '@smithy/node-http-handler': 4.0.2 + '@smithy/node-http-handler': 4.0.3 '@smithy/protocol-http': 5.0.1 - '@smithy/smithy-client': 4.1.5 + '@smithy/smithy-client': 4.1.6 '@smithy/types': 4.1.0 '@smithy/url-parser': 4.0.1 '@smithy/util-base64': 4.0.0 '@smithy/util-body-length-browser': 4.0.0 '@smithy/util-body-length-node': 4.0.0 - '@smithy/util-defaults-mode-browser': 4.0.6 - '@smithy/util-defaults-mode-node': 4.0.6 + '@smithy/util-defaults-mode-browser': 4.0.7 + '@smithy/util-defaults-mode-node': 4.0.7 '@smithy/util-endpoints': 3.0.1 '@smithy/util-middleware': 4.0.1 '@smithy/util-retry': 4.0.1 - '@smithy/util-stream': 4.1.1 + '@smithy/util-stream': 4.1.2 '@smithy/util-utf8': 4.0.0 '@smithy/util-waiter': 4.0.2 tslib: 2.8.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/client-sso@3.749.0': + '@aws-sdk/client-sso@3.772.0': dependencies: '@aws-crypto/sha256-browser': 5.2.0 '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.749.0 + '@aws-sdk/core': 3.758.0 '@aws-sdk/middleware-host-header': 3.734.0 '@aws-sdk/middleware-logger': 3.734.0 - '@aws-sdk/middleware-recursion-detection': 3.734.0 - '@aws-sdk/middleware-user-agent': 3.749.0 + '@aws-sdk/middleware-recursion-detection': 3.772.0 + '@aws-sdk/middleware-user-agent': 3.758.0 '@aws-sdk/region-config-resolver': 3.734.0 '@aws-sdk/types': 3.734.0 '@aws-sdk/util-endpoints': 3.743.0 '@aws-sdk/util-user-agent-browser': 3.734.0 - '@aws-sdk/util-user-agent-node': 3.749.0 + '@aws-sdk/util-user-agent-node': 3.758.0 '@smithy/config-resolver': 4.0.1 - '@smithy/core': 3.1.4 + '@smithy/core': 3.1.5 '@smithy/fetch-http-handler': 5.0.1 '@smithy/hash-node': 4.0.1 '@smithy/invalid-dependency': 4.0.1 '@smithy/middleware-content-length': 4.0.1 - '@smithy/middleware-endpoint': 4.0.5 - '@smithy/middleware-retry': 4.0.6 + '@smithy/middleware-endpoint': 4.0.6 + '@smithy/middleware-retry': 4.0.7 '@smithy/middleware-serde': 4.0.2 '@smithy/middleware-stack': 4.0.1 '@smithy/node-config-provider': 4.0.1 - '@smithy/node-http-handler': 4.0.2 + '@smithy/node-http-handler': 4.0.3 '@smithy/protocol-http': 5.0.1 - '@smithy/smithy-client': 4.1.5 + '@smithy/smithy-client': 4.1.6 '@smithy/types': 4.1.0 '@smithy/url-parser': 4.0.1 '@smithy/util-base64': 4.0.0 '@smithy/util-body-length-browser': 4.0.0 '@smithy/util-body-length-node': 4.0.0 - '@smithy/util-defaults-mode-browser': 4.0.6 - '@smithy/util-defaults-mode-node': 4.0.6 + '@smithy/util-defaults-mode-browser': 4.0.7 + '@smithy/util-defaults-mode-node': 4.0.7 '@smithy/util-endpoints': 3.0.1 '@smithy/util-middleware': 4.0.1 '@smithy/util-retry': 4.0.1 @@ -10979,50 +10953,50 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/core@3.749.0': + '@aws-sdk/core@3.758.0': dependencies: '@aws-sdk/types': 3.734.0 - '@smithy/core': 3.1.4 + '@smithy/core': 3.1.5 '@smithy/node-config-provider': 4.0.1 '@smithy/property-provider': 4.0.1 '@smithy/protocol-http': 5.0.1 '@smithy/signature-v4': 5.0.1 - '@smithy/smithy-client': 4.1.5 + '@smithy/smithy-client': 4.1.6 '@smithy/types': 4.1.0 '@smithy/util-middleware': 4.0.1 fast-xml-parser: 4.4.1 tslib: 2.8.1 - '@aws-sdk/credential-provider-env@3.749.0': + '@aws-sdk/credential-provider-env@3.758.0': dependencies: - '@aws-sdk/core': 3.749.0 + '@aws-sdk/core': 3.758.0 '@aws-sdk/types': 3.734.0 '@smithy/property-provider': 4.0.1 '@smithy/types': 4.1.0 tslib: 2.8.1 - '@aws-sdk/credential-provider-http@3.749.0': + '@aws-sdk/credential-provider-http@3.758.0': dependencies: - '@aws-sdk/core': 3.749.0 + '@aws-sdk/core': 3.758.0 '@aws-sdk/types': 3.734.0 '@smithy/fetch-http-handler': 5.0.1 - '@smithy/node-http-handler': 4.0.2 + '@smithy/node-http-handler': 4.0.3 '@smithy/property-provider': 4.0.1 '@smithy/protocol-http': 5.0.1 - '@smithy/smithy-client': 4.1.5 + '@smithy/smithy-client': 4.1.6 '@smithy/types': 4.1.0 - '@smithy/util-stream': 4.1.1 + '@smithy/util-stream': 4.1.2 tslib: 2.8.1 - '@aws-sdk/credential-provider-ini@3.749.0': + '@aws-sdk/credential-provider-ini@3.772.0': dependencies: - '@aws-sdk/core': 3.749.0 - '@aws-sdk/credential-provider-env': 3.749.0 - '@aws-sdk/credential-provider-http': 3.749.0 - '@aws-sdk/credential-provider-process': 3.749.0 - '@aws-sdk/credential-provider-sso': 3.749.0 - '@aws-sdk/credential-provider-web-identity': 3.749.0 - '@aws-sdk/nested-clients': 3.749.0 + '@aws-sdk/core': 3.758.0 + '@aws-sdk/credential-provider-env': 3.758.0 + '@aws-sdk/credential-provider-http': 3.758.0 + '@aws-sdk/credential-provider-process': 3.758.0 + '@aws-sdk/credential-provider-sso': 3.772.0 + '@aws-sdk/credential-provider-web-identity': 3.772.0 + '@aws-sdk/nested-clients': 3.772.0 '@aws-sdk/types': 3.734.0 '@smithy/credential-provider-imds': 4.0.1 '@smithy/property-provider': 4.0.1 @@ -11032,14 +11006,14 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-provider-node@3.749.0': + '@aws-sdk/credential-provider-node@3.772.0': dependencies: - '@aws-sdk/credential-provider-env': 3.749.0 - '@aws-sdk/credential-provider-http': 3.749.0 - '@aws-sdk/credential-provider-ini': 3.749.0 - '@aws-sdk/credential-provider-process': 3.749.0 - '@aws-sdk/credential-provider-sso': 3.749.0 - '@aws-sdk/credential-provider-web-identity': 3.749.0 + '@aws-sdk/credential-provider-env': 3.758.0 + '@aws-sdk/credential-provider-http': 3.758.0 + '@aws-sdk/credential-provider-ini': 3.772.0 + '@aws-sdk/credential-provider-process': 3.758.0 + '@aws-sdk/credential-provider-sso': 3.772.0 + '@aws-sdk/credential-provider-web-identity': 3.772.0 '@aws-sdk/types': 3.734.0 '@smithy/credential-provider-imds': 4.0.1 '@smithy/property-provider': 4.0.1 @@ -11049,20 +11023,20 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-provider-process@3.749.0': + '@aws-sdk/credential-provider-process@3.758.0': dependencies: - '@aws-sdk/core': 3.749.0 + '@aws-sdk/core': 3.758.0 '@aws-sdk/types': 3.734.0 '@smithy/property-provider': 4.0.1 '@smithy/shared-ini-file-loader': 4.0.1 '@smithy/types': 4.1.0 tslib: 2.8.1 - '@aws-sdk/credential-provider-sso@3.749.0': + '@aws-sdk/credential-provider-sso@3.772.0': dependencies: - '@aws-sdk/client-sso': 3.749.0 - '@aws-sdk/core': 3.749.0 - '@aws-sdk/token-providers': 3.749.0 + '@aws-sdk/client-sso': 3.772.0 + '@aws-sdk/core': 3.758.0 + '@aws-sdk/token-providers': 3.772.0 '@aws-sdk/types': 3.734.0 '@smithy/property-provider': 4.0.1 '@smithy/shared-ini-file-loader': 4.0.1 @@ -11071,10 +11045,10 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-provider-web-identity@3.749.0': + '@aws-sdk/credential-provider-web-identity@3.772.0': dependencies: - '@aws-sdk/core': 3.749.0 - '@aws-sdk/nested-clients': 3.749.0 + '@aws-sdk/core': 3.758.0 + '@aws-sdk/nested-clients': 3.772.0 '@aws-sdk/types': 3.734.0 '@smithy/property-provider': 4.0.1 '@smithy/types': 4.1.0 @@ -11082,12 +11056,12 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/lib-storage@3.749.0(@aws-sdk/client-s3@3.749.0)': + '@aws-sdk/lib-storage@3.772.0(@aws-sdk/client-s3@3.772.0)': dependencies: - '@aws-sdk/client-s3': 3.749.0 + '@aws-sdk/client-s3': 3.772.0 '@smithy/abort-controller': 4.0.1 - '@smithy/middleware-endpoint': 4.0.5 - '@smithy/smithy-client': 4.1.5 + '@smithy/middleware-endpoint': 4.0.6 + '@smithy/smithy-client': 4.1.6 buffer: 5.6.0 events: 3.3.0 stream-browserify: 3.0.0 @@ -11110,19 +11084,19 @@ snapshots: '@smithy/types': 4.1.0 tslib: 2.8.1 - '@aws-sdk/middleware-flexible-checksums@3.749.0': + '@aws-sdk/middleware-flexible-checksums@3.758.0': dependencies: '@aws-crypto/crc32': 5.2.0 '@aws-crypto/crc32c': 5.2.0 '@aws-crypto/util': 5.2.0 - '@aws-sdk/core': 3.749.0 + '@aws-sdk/core': 3.758.0 '@aws-sdk/types': 3.734.0 '@smithy/is-array-buffer': 4.0.0 '@smithy/node-config-provider': 4.0.1 '@smithy/protocol-http': 5.0.1 '@smithy/types': 4.1.0 '@smithy/util-middleware': 4.0.1 - '@smithy/util-stream': 4.1.1 + '@smithy/util-stream': 4.1.2 '@smithy/util-utf8': 4.0.0 tslib: 2.8.1 @@ -11145,27 +11119,27 @@ snapshots: '@smithy/types': 4.1.0 tslib: 2.8.1 - '@aws-sdk/middleware-recursion-detection@3.734.0': + '@aws-sdk/middleware-recursion-detection@3.772.0': dependencies: '@aws-sdk/types': 3.734.0 '@smithy/protocol-http': 5.0.1 '@smithy/types': 4.1.0 tslib: 2.8.1 - '@aws-sdk/middleware-sdk-s3@3.749.0': + '@aws-sdk/middleware-sdk-s3@3.758.0': dependencies: - '@aws-sdk/core': 3.749.0 + '@aws-sdk/core': 3.758.0 '@aws-sdk/types': 3.734.0 '@aws-sdk/util-arn-parser': 3.723.0 - '@smithy/core': 3.1.4 + '@smithy/core': 3.1.5 '@smithy/node-config-provider': 4.0.1 '@smithy/protocol-http': 5.0.1 '@smithy/signature-v4': 5.0.1 - '@smithy/smithy-client': 4.1.5 + '@smithy/smithy-client': 4.1.6 '@smithy/types': 4.1.0 '@smithy/util-config-provider': 4.0.0 '@smithy/util-middleware': 4.0.1 - '@smithy/util-stream': 4.1.1 + '@smithy/util-stream': 4.1.2 '@smithy/util-utf8': 4.0.0 tslib: 2.8.1 @@ -11175,51 +11149,51 @@ snapshots: '@smithy/types': 4.1.0 tslib: 2.8.1 - '@aws-sdk/middleware-user-agent@3.749.0': + '@aws-sdk/middleware-user-agent@3.758.0': dependencies: - '@aws-sdk/core': 3.749.0 + '@aws-sdk/core': 3.758.0 '@aws-sdk/types': 3.734.0 '@aws-sdk/util-endpoints': 3.743.0 - '@smithy/core': 3.1.4 + '@smithy/core': 3.1.5 '@smithy/protocol-http': 5.0.1 '@smithy/types': 4.1.0 tslib: 2.8.1 - '@aws-sdk/nested-clients@3.749.0': + '@aws-sdk/nested-clients@3.772.0': dependencies: '@aws-crypto/sha256-browser': 5.2.0 '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.749.0 + '@aws-sdk/core': 3.758.0 '@aws-sdk/middleware-host-header': 3.734.0 '@aws-sdk/middleware-logger': 3.734.0 - '@aws-sdk/middleware-recursion-detection': 3.734.0 - '@aws-sdk/middleware-user-agent': 3.749.0 + '@aws-sdk/middleware-recursion-detection': 3.772.0 + '@aws-sdk/middleware-user-agent': 3.758.0 '@aws-sdk/region-config-resolver': 3.734.0 '@aws-sdk/types': 3.734.0 '@aws-sdk/util-endpoints': 3.743.0 '@aws-sdk/util-user-agent-browser': 3.734.0 - '@aws-sdk/util-user-agent-node': 3.749.0 + '@aws-sdk/util-user-agent-node': 3.758.0 '@smithy/config-resolver': 4.0.1 - '@smithy/core': 3.1.4 + '@smithy/core': 3.1.5 '@smithy/fetch-http-handler': 5.0.1 '@smithy/hash-node': 4.0.1 '@smithy/invalid-dependency': 4.0.1 '@smithy/middleware-content-length': 4.0.1 - '@smithy/middleware-endpoint': 4.0.5 - '@smithy/middleware-retry': 4.0.6 + '@smithy/middleware-endpoint': 4.0.6 + '@smithy/middleware-retry': 4.0.7 '@smithy/middleware-serde': 4.0.2 '@smithy/middleware-stack': 4.0.1 '@smithy/node-config-provider': 4.0.1 - '@smithy/node-http-handler': 4.0.2 + '@smithy/node-http-handler': 4.0.3 '@smithy/protocol-http': 5.0.1 - '@smithy/smithy-client': 4.1.5 + '@smithy/smithy-client': 4.1.6 '@smithy/types': 4.1.0 '@smithy/url-parser': 4.0.1 '@smithy/util-base64': 4.0.0 '@smithy/util-body-length-browser': 4.0.0 '@smithy/util-body-length-node': 4.0.0 - '@smithy/util-defaults-mode-browser': 4.0.6 - '@smithy/util-defaults-mode-node': 4.0.6 + '@smithy/util-defaults-mode-browser': 4.0.7 + '@smithy/util-defaults-mode-node': 4.0.7 '@smithy/util-endpoints': 3.0.1 '@smithy/util-middleware': 4.0.1 '@smithy/util-retry': 4.0.1 @@ -11237,18 +11211,18 @@ snapshots: '@smithy/util-middleware': 4.0.1 tslib: 2.8.1 - '@aws-sdk/signature-v4-multi-region@3.749.0': + '@aws-sdk/signature-v4-multi-region@3.758.0': dependencies: - '@aws-sdk/middleware-sdk-s3': 3.749.0 + '@aws-sdk/middleware-sdk-s3': 3.758.0 '@aws-sdk/types': 3.734.0 '@smithy/protocol-http': 5.0.1 '@smithy/signature-v4': 5.0.1 '@smithy/types': 4.1.0 tslib: 2.8.1 - '@aws-sdk/token-providers@3.749.0': + '@aws-sdk/token-providers@3.772.0': dependencies: - '@aws-sdk/nested-clients': 3.749.0 + '@aws-sdk/nested-clients': 3.772.0 '@aws-sdk/types': 3.734.0 '@smithy/property-provider': 4.0.1 '@smithy/shared-ini-file-loader': 4.0.1 @@ -11284,9 +11258,9 @@ snapshots: bowser: 2.11.0 tslib: 2.8.1 - '@aws-sdk/util-user-agent-node@3.749.0': + '@aws-sdk/util-user-agent-node@3.758.0': dependencies: - '@aws-sdk/middleware-user-agent': 3.749.0 + '@aws-sdk/middleware-user-agent': 3.758.0 '@aws-sdk/types': 3.734.0 '@smithy/node-config-provider': 4.0.1 '@smithy/types': 4.1.0 @@ -11879,10 +11853,10 @@ snapshots: cookie: 1.0.1 fastify-plugin: 5.0.0 - '@fastify/cors@10.0.2': + '@fastify/cors@10.1.0': dependencies: fastify-plugin: 5.0.0 - mnemonist: 0.39.8 + mnemonist: 0.40.0 '@fastify/deepmerge@2.0.0': {} @@ -11906,7 +11880,7 @@ snapshots: '@fastify/reply-from': 11.0.0 fast-querystring: 1.1.2 fastify-plugin: 5.0.0 - ws: 8.18.0(bufferutil@4.0.9)(utf-8-validate@6.0.5) + ws: 8.18.1(bufferutil@4.0.9)(utf-8-validate@6.0.5) transitivePeerDependencies: - bufferutil - utf-8-validate @@ -11946,7 +11920,7 @@ snapshots: http-errors: 2.0.0 mime: 3.0.0 - '@fastify/static@8.1.0': + '@fastify/static@8.1.1': dependencies: '@fastify/accept-negotiator': 2.0.0 '@fastify/send': 3.3.1 @@ -12450,75 +12424,75 @@ snapshots: outvariant: 1.4.3 strict-event-emitter: 0.5.1 - '@napi-rs/canvas-android-arm64@0.1.67': + '@napi-rs/canvas-android-arm64@0.1.68': optional: true - '@napi-rs/canvas-darwin-arm64@0.1.67': + '@napi-rs/canvas-darwin-arm64@0.1.68': optional: true - '@napi-rs/canvas-darwin-x64@0.1.67': + '@napi-rs/canvas-darwin-x64@0.1.68': optional: true - '@napi-rs/canvas-linux-arm-gnueabihf@0.1.67': + '@napi-rs/canvas-linux-arm-gnueabihf@0.1.68': optional: true - '@napi-rs/canvas-linux-arm64-gnu@0.1.67': + '@napi-rs/canvas-linux-arm64-gnu@0.1.68': optional: true - '@napi-rs/canvas-linux-arm64-musl@0.1.67': + '@napi-rs/canvas-linux-arm64-musl@0.1.68': optional: true - '@napi-rs/canvas-linux-riscv64-gnu@0.1.67': + '@napi-rs/canvas-linux-riscv64-gnu@0.1.68': optional: true - '@napi-rs/canvas-linux-x64-gnu@0.1.67': + '@napi-rs/canvas-linux-x64-gnu@0.1.68': optional: true - '@napi-rs/canvas-linux-x64-musl@0.1.67': + '@napi-rs/canvas-linux-x64-musl@0.1.68': optional: true - '@napi-rs/canvas-win32-x64-msvc@0.1.67': + '@napi-rs/canvas-win32-x64-msvc@0.1.68': optional: true - '@napi-rs/canvas@0.1.67': + '@napi-rs/canvas@0.1.68': optionalDependencies: - '@napi-rs/canvas-android-arm64': 0.1.67 - '@napi-rs/canvas-darwin-arm64': 0.1.67 - '@napi-rs/canvas-darwin-x64': 0.1.67 - '@napi-rs/canvas-linux-arm-gnueabihf': 0.1.67 - '@napi-rs/canvas-linux-arm64-gnu': 0.1.67 - '@napi-rs/canvas-linux-arm64-musl': 0.1.67 - '@napi-rs/canvas-linux-riscv64-gnu': 0.1.67 - '@napi-rs/canvas-linux-x64-gnu': 0.1.67 - '@napi-rs/canvas-linux-x64-musl': 0.1.67 - '@napi-rs/canvas-win32-x64-msvc': 0.1.67 - - '@nestjs/common@11.0.9(reflect-metadata@0.2.2)(rxjs@7.8.1)': + '@napi-rs/canvas-android-arm64': 0.1.68 + '@napi-rs/canvas-darwin-arm64': 0.1.68 + '@napi-rs/canvas-darwin-x64': 0.1.68 + '@napi-rs/canvas-linux-arm-gnueabihf': 0.1.68 + '@napi-rs/canvas-linux-arm64-gnu': 0.1.68 + '@napi-rs/canvas-linux-arm64-musl': 0.1.68 + '@napi-rs/canvas-linux-riscv64-gnu': 0.1.68 + '@napi-rs/canvas-linux-x64-gnu': 0.1.68 + '@napi-rs/canvas-linux-x64-musl': 0.1.68 + '@napi-rs/canvas-win32-x64-msvc': 0.1.68 + + '@nestjs/common@11.0.12(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: iterare: 1.2.1 reflect-metadata: 0.2.2 - rxjs: 7.8.1 + rxjs: 7.8.2 tslib: 2.8.1 uid: 2.0.2 - '@nestjs/core@11.0.9(@nestjs/common@11.0.9(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.15)(reflect-metadata@0.2.2)(rxjs@7.8.1)': + '@nestjs/core@11.0.12(@nestjs/common@11.0.12(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.15)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.0.9(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/common': 11.0.12(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nuxt/opencollective': 0.4.1 fast-safe-stringify: 2.1.1 iterare: 1.2.1 path-to-regexp: 8.2.0 reflect-metadata: 0.2.2 - rxjs: 7.8.1 + rxjs: 7.8.2 tslib: 2.8.1 uid: 2.0.2 optionalDependencies: - '@nestjs/platform-express': 10.4.15(@nestjs/common@11.0.9(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@11.0.9) + '@nestjs/platform-express': 10.4.15(@nestjs/common@11.0.12(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.0.12) - '@nestjs/platform-express@10.4.15(@nestjs/common@11.0.9(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@11.0.9)': + '@nestjs/platform-express@10.4.15(@nestjs/common@11.0.12(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.0.12)': dependencies: - '@nestjs/common': 11.0.9(reflect-metadata@0.2.2)(rxjs@7.8.1) - '@nestjs/core': 11.0.9(@nestjs/common@11.0.9(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.15)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/common': 11.0.12(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.0.12(@nestjs/common@11.0.12(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.15)(reflect-metadata@0.2.2)(rxjs@7.8.2) body-parser: 1.20.3 cors: 2.8.5 express: 4.21.2 @@ -12527,13 +12501,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@nestjs/testing@11.0.9(@nestjs/common@11.0.9(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@11.0.9)(@nestjs/platform-express@10.4.15)': + '@nestjs/testing@11.0.12(@nestjs/common@11.0.12(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.0.12)(@nestjs/platform-express@10.4.15)': dependencies: - '@nestjs/common': 11.0.9(reflect-metadata@0.2.2)(rxjs@7.8.1) - '@nestjs/core': 11.0.9(@nestjs/common@11.0.9(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.15)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/common': 11.0.12(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.0.12(@nestjs/common@11.0.12(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.15)(reflect-metadata@0.2.2)(rxjs@7.8.2) tslib: 2.8.1 optionalDependencies: - '@nestjs/platform-express': 10.4.15(@nestjs/common@11.0.9(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@11.0.9) + '@nestjs/platform-express': 10.4.15(@nestjs/common@11.0.12(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.0.12) '@noble/hashes@1.6.1': {} @@ -13321,14 +13295,14 @@ snapshots: '@smithy/util-middleware': 4.0.1 tslib: 2.8.1 - '@smithy/core@3.1.4': + '@smithy/core@3.1.5': dependencies: '@smithy/middleware-serde': 4.0.2 '@smithy/protocol-http': 5.0.1 '@smithy/types': 4.1.0 '@smithy/util-body-length-browser': 4.0.0 '@smithy/util-middleware': 4.0.1 - '@smithy/util-stream': 4.1.1 + '@smithy/util-stream': 4.1.2 '@smithy/util-utf8': 4.0.0 tslib: 2.8.1 @@ -13423,9 +13397,9 @@ snapshots: '@smithy/types': 4.1.0 tslib: 2.8.1 - '@smithy/middleware-endpoint@4.0.5': + '@smithy/middleware-endpoint@4.0.6': dependencies: - '@smithy/core': 3.1.4 + '@smithy/core': 3.1.5 '@smithy/middleware-serde': 4.0.2 '@smithy/node-config-provider': 4.0.1 '@smithy/shared-ini-file-loader': 4.0.1 @@ -13434,12 +13408,12 @@ snapshots: '@smithy/util-middleware': 4.0.1 tslib: 2.8.1 - '@smithy/middleware-retry@4.0.6': + '@smithy/middleware-retry@4.0.7': dependencies: '@smithy/node-config-provider': 4.0.1 '@smithy/protocol-http': 5.0.1 '@smithy/service-error-classification': 4.0.1 - '@smithy/smithy-client': 4.1.5 + '@smithy/smithy-client': 4.1.6 '@smithy/types': 4.1.0 '@smithy/util-middleware': 4.0.1 '@smithy/util-retry': 4.0.1 @@ -13471,7 +13445,7 @@ snapshots: '@smithy/types': 2.12.0 tslib: 2.6.2 - '@smithy/node-http-handler@4.0.2': + '@smithy/node-http-handler@4.0.3': dependencies: '@smithy/abort-controller': 4.0.1 '@smithy/protocol-http': 5.0.1 @@ -13531,14 +13505,14 @@ snapshots: '@smithy/util-utf8': 4.0.0 tslib: 2.8.1 - '@smithy/smithy-client@4.1.5': + '@smithy/smithy-client@4.1.6': dependencies: - '@smithy/core': 3.1.4 - '@smithy/middleware-endpoint': 4.0.5 + '@smithy/core': 3.1.5 + '@smithy/middleware-endpoint': 4.0.6 '@smithy/middleware-stack': 4.0.1 '@smithy/protocol-http': 5.0.1 '@smithy/types': 4.1.0 - '@smithy/util-stream': 4.1.1 + '@smithy/util-stream': 4.1.2 tslib: 2.8.1 '@smithy/types@2.12.0': @@ -13583,21 +13557,21 @@ snapshots: dependencies: tslib: 2.8.1 - '@smithy/util-defaults-mode-browser@4.0.6': + '@smithy/util-defaults-mode-browser@4.0.7': dependencies: '@smithy/property-provider': 4.0.1 - '@smithy/smithy-client': 4.1.5 + '@smithy/smithy-client': 4.1.6 '@smithy/types': 4.1.0 bowser: 2.11.0 tslib: 2.8.1 - '@smithy/util-defaults-mode-node@4.0.6': + '@smithy/util-defaults-mode-node@4.0.7': dependencies: '@smithy/config-resolver': 4.0.1 '@smithy/credential-provider-imds': 4.0.1 '@smithy/node-config-provider': 4.0.1 '@smithy/property-provider': 4.0.1 - '@smithy/smithy-client': 4.1.5 + '@smithy/smithy-client': 4.1.6 '@smithy/types': 4.1.0 tslib: 2.8.1 @@ -13622,10 +13596,10 @@ snapshots: '@smithy/types': 4.1.0 tslib: 2.8.1 - '@smithy/util-stream@4.1.1': + '@smithy/util-stream@4.1.2': dependencies: '@smithy/fetch-http-handler': 5.0.1 - '@smithy/node-http-handler': 4.0.2 + '@smithy/node-http-handler': 4.0.3 '@smithy/types': 4.1.0 '@smithy/util-base64': 4.0.0 '@smithy/util-buffer-from': 4.0.0 @@ -13806,7 +13780,7 @@ snapshots: recast: 0.23.6 semver: 7.6.3 util: 0.12.5 - ws: 8.18.0(bufferutil@4.0.9)(utf-8-validate@6.0.5) + ws: 8.18.1(bufferutil@4.0.9)(utf-8-validate@6.0.5) optionalDependencies: prettier: 3.5.3 transitivePeerDependencies: @@ -13950,9 +13924,9 @@ snapshots: - supports-color - typescript - '@swc/cli@0.6.0(@swc/core@1.10.16)(chokidar@4.0.3)': + '@swc/cli@0.6.0(@swc/core@1.11.11)(chokidar@4.0.3)': dependencies: - '@swc/core': 1.10.16 + '@swc/core': 1.11.11 '@swc/counter': 0.1.3 '@xhmikosr/bin-wrapper': 13.0.5 commander: 8.3.0 @@ -13970,10 +13944,10 @@ snapshots: '@swc/wasm': 1.2.130 optional: true - '@swc/core-darwin-arm64@1.10.16': + '@swc/core-darwin-arm64@1.11.11': optional: true - '@swc/core-darwin-x64@1.10.16': + '@swc/core-darwin-x64@1.11.11': optional: true '@swc/core-freebsd-x64@1.3.11': @@ -13981,56 +13955,56 @@ snapshots: '@swc/wasm': 1.2.130 optional: true - '@swc/core-linux-arm-gnueabihf@1.10.16': + '@swc/core-linux-arm-gnueabihf@1.11.11': optional: true - '@swc/core-linux-arm64-gnu@1.10.16': + '@swc/core-linux-arm64-gnu@1.11.11': optional: true - '@swc/core-linux-arm64-musl@1.10.16': + '@swc/core-linux-arm64-musl@1.11.11': optional: true - '@swc/core-linux-x64-gnu@1.10.16': + '@swc/core-linux-x64-gnu@1.11.11': optional: true - '@swc/core-linux-x64-musl@1.10.16': + '@swc/core-linux-x64-musl@1.11.11': optional: true - '@swc/core-win32-arm64-msvc@1.10.16': + '@swc/core-win32-arm64-msvc@1.11.11': optional: true - '@swc/core-win32-ia32-msvc@1.10.16': + '@swc/core-win32-ia32-msvc@1.11.11': optional: true - '@swc/core-win32-x64-msvc@1.10.16': + '@swc/core-win32-x64-msvc@1.11.11': optional: true - '@swc/core@1.10.16': + '@swc/core@1.11.11': dependencies: '@swc/counter': 0.1.3 - '@swc/types': 0.1.17 + '@swc/types': 0.1.19 optionalDependencies: - '@swc/core-darwin-arm64': 1.10.16 - '@swc/core-darwin-x64': 1.10.16 - '@swc/core-linux-arm-gnueabihf': 1.10.16 - '@swc/core-linux-arm64-gnu': 1.10.16 - '@swc/core-linux-arm64-musl': 1.10.16 - '@swc/core-linux-x64-gnu': 1.10.16 - '@swc/core-linux-x64-musl': 1.10.16 - '@swc/core-win32-arm64-msvc': 1.10.16 - '@swc/core-win32-ia32-msvc': 1.10.16 - '@swc/core-win32-x64-msvc': 1.10.16 + '@swc/core-darwin-arm64': 1.11.11 + '@swc/core-darwin-x64': 1.11.11 + '@swc/core-linux-arm-gnueabihf': 1.11.11 + '@swc/core-linux-arm64-gnu': 1.11.11 + '@swc/core-linux-arm64-musl': 1.11.11 + '@swc/core-linux-x64-gnu': 1.11.11 + '@swc/core-linux-x64-musl': 1.11.11 + '@swc/core-win32-arm64-msvc': 1.11.11 + '@swc/core-win32-ia32-msvc': 1.11.11 + '@swc/core-win32-x64-msvc': 1.11.11 '@swc/counter@0.1.3': {} - '@swc/jest@0.2.37(@swc/core@1.10.16)': + '@swc/jest@0.2.37(@swc/core@1.11.11)': dependencies: '@jest/create-cache-key-function': 29.7.0 - '@swc/core': 1.10.16 + '@swc/core': 1.11.11 '@swc/counter': 0.1.3 jsonc-parser: 3.2.0 - '@swc/types@0.1.17': + '@swc/types@0.1.19': dependencies: '@swc/counter': 0.1.3 @@ -14369,10 +14343,6 @@ snapshots: dependencies: undici-types: 6.20.0 - '@types/node@22.13.4': - dependencies: - undici-types: 6.20.0 - '@types/node@22.13.9': dependencies: undici-types: 6.20.0 @@ -14413,7 +14383,7 @@ snapshots: '@types/pg@8.6.1': dependencies: '@types/node': 22.13.10 - pg-protocol: 1.7.0 + pg-protocol: 1.7.1 pg-types: 2.2.0 '@types/prop-types@15.7.5': {} @@ -14515,10 +14485,6 @@ snapshots: dependencies: '@types/node': 22.13.10 - '@types/ws@8.5.14': - dependencies: - '@types/node': 22.13.10 - '@types/yargs-parser@21.0.0': {} '@types/yargs@17.0.19': @@ -14530,23 +14496,6 @@ snapshots: '@types/node': 22.13.10 optional: true - '@typescript-eslint/eslint-plugin@8.24.0(@typescript-eslint/parser@8.24.0(eslint@9.22.0)(typescript@5.7.3))(eslint@9.22.0)(typescript@5.7.3)': - dependencies: - '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.24.0(eslint@9.22.0)(typescript@5.7.3) - '@typescript-eslint/scope-manager': 8.24.0 - '@typescript-eslint/type-utils': 8.24.0(eslint@9.22.0)(typescript@5.7.3) - '@typescript-eslint/utils': 8.24.0(eslint@9.22.0)(typescript@5.7.3) - '@typescript-eslint/visitor-keys': 8.24.0 - eslint: 9.22.0 - graphemer: 1.4.0 - ignore: 5.3.1 - natural-compare: 1.4.0 - ts-api-utils: 2.0.1(typescript@5.7.3) - typescript: 5.7.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/eslint-plugin@8.26.0(@typescript-eslint/parser@8.26.0(eslint@9.22.0)(typescript@5.8.2))(eslint@9.22.0)(typescript@5.8.2)': dependencies: '@eslint-community/regexpp': 4.12.1 @@ -14564,15 +14513,20 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.24.0(eslint@9.22.0)(typescript@5.7.3)': + '@typescript-eslint/eslint-plugin@8.27.0(@typescript-eslint/parser@8.27.0(eslint@9.22.0)(typescript@5.8.2))(eslint@9.22.0)(typescript@5.8.2)': dependencies: - '@typescript-eslint/scope-manager': 8.24.0 - '@typescript-eslint/types': 8.24.0 - '@typescript-eslint/typescript-estree': 8.24.0(typescript@5.7.3) - '@typescript-eslint/visitor-keys': 8.24.0 - debug: 4.4.0(supports-color@8.1.1) + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 8.27.0(eslint@9.22.0)(typescript@5.8.2) + '@typescript-eslint/scope-manager': 8.27.0 + '@typescript-eslint/type-utils': 8.27.0(eslint@9.22.0)(typescript@5.8.2) + '@typescript-eslint/utils': 8.27.0(eslint@9.22.0)(typescript@5.8.2) + '@typescript-eslint/visitor-keys': 8.27.0 eslint: 9.22.0 - typescript: 5.7.3 + graphemer: 1.4.0 + ignore: 5.3.1 + natural-compare: 1.4.0 + ts-api-utils: 2.0.1(typescript@5.8.2) + typescript: 5.8.2 transitivePeerDependencies: - supports-color @@ -14588,26 +14542,27 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.24.0': + '@typescript-eslint/parser@8.27.0(eslint@9.22.0)(typescript@5.8.2)': dependencies: - '@typescript-eslint/types': 8.24.0 - '@typescript-eslint/visitor-keys': 8.24.0 + '@typescript-eslint/scope-manager': 8.27.0 + '@typescript-eslint/types': 8.27.0 + '@typescript-eslint/typescript-estree': 8.27.0(typescript@5.8.2) + '@typescript-eslint/visitor-keys': 8.27.0 + debug: 4.4.0(supports-color@8.1.1) + eslint: 9.22.0 + typescript: 5.8.2 + transitivePeerDependencies: + - supports-color '@typescript-eslint/scope-manager@8.26.0': dependencies: '@typescript-eslint/types': 8.26.0 '@typescript-eslint/visitor-keys': 8.26.0 - '@typescript-eslint/type-utils@8.24.0(eslint@9.22.0)(typescript@5.7.3)': + '@typescript-eslint/scope-manager@8.27.0': dependencies: - '@typescript-eslint/typescript-estree': 8.24.0(typescript@5.7.3) - '@typescript-eslint/utils': 8.24.0(eslint@9.22.0)(typescript@5.7.3) - debug: 4.4.0(supports-color@8.1.1) - eslint: 9.22.0 - ts-api-utils: 2.0.1(typescript@5.7.3) - typescript: 5.7.3 - transitivePeerDependencies: - - supports-color + '@typescript-eslint/types': 8.27.0 + '@typescript-eslint/visitor-keys': 8.27.0 '@typescript-eslint/type-utils@8.26.0(eslint@9.22.0)(typescript@5.8.2)': dependencies: @@ -14620,28 +14575,39 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.24.0': {} + '@typescript-eslint/type-utils@8.27.0(eslint@9.22.0)(typescript@5.8.2)': + dependencies: + '@typescript-eslint/typescript-estree': 8.27.0(typescript@5.8.2) + '@typescript-eslint/utils': 8.27.0(eslint@9.22.0)(typescript@5.8.2) + debug: 4.4.0(supports-color@8.1.1) + eslint: 9.22.0 + ts-api-utils: 2.0.1(typescript@5.8.2) + typescript: 5.8.2 + transitivePeerDependencies: + - supports-color '@typescript-eslint/types@8.26.0': {} - '@typescript-eslint/typescript-estree@8.24.0(typescript@5.7.3)': + '@typescript-eslint/types@8.27.0': {} + + '@typescript-eslint/typescript-estree@8.26.0(typescript@5.8.2)': dependencies: - '@typescript-eslint/types': 8.24.0 - '@typescript-eslint/visitor-keys': 8.24.0 + '@typescript-eslint/types': 8.26.0 + '@typescript-eslint/visitor-keys': 8.26.0 debug: 4.4.0(supports-color@8.1.1) fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.4 semver: 7.6.3 - ts-api-utils: 2.0.1(typescript@5.7.3) - typescript: 5.7.3 + ts-api-utils: 2.0.1(typescript@5.8.2) + typescript: 5.8.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/typescript-estree@8.26.0(typescript@5.8.2)': + '@typescript-eslint/typescript-estree@8.27.0(typescript@5.8.2)': dependencies: - '@typescript-eslint/types': 8.26.0 - '@typescript-eslint/visitor-keys': 8.26.0 + '@typescript-eslint/types': 8.27.0 + '@typescript-eslint/visitor-keys': 8.27.0 debug: 4.4.0(supports-color@8.1.1) fast-glob: 3.3.3 is-glob: 4.0.3 @@ -14652,36 +14618,36 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.24.0(eslint@9.22.0)(typescript@5.7.3)': + '@typescript-eslint/utils@8.26.0(eslint@9.22.0)(typescript@5.8.2)': dependencies: '@eslint-community/eslint-utils': 4.4.0(eslint@9.22.0) - '@typescript-eslint/scope-manager': 8.24.0 - '@typescript-eslint/types': 8.24.0 - '@typescript-eslint/typescript-estree': 8.24.0(typescript@5.7.3) + '@typescript-eslint/scope-manager': 8.26.0 + '@typescript-eslint/types': 8.26.0 + '@typescript-eslint/typescript-estree': 8.26.0(typescript@5.8.2) eslint: 9.22.0 - typescript: 5.7.3 + typescript: 5.8.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.26.0(eslint@9.22.0)(typescript@5.8.2)': + '@typescript-eslint/utils@8.27.0(eslint@9.22.0)(typescript@5.8.2)': dependencies: '@eslint-community/eslint-utils': 4.4.0(eslint@9.22.0) - '@typescript-eslint/scope-manager': 8.26.0 - '@typescript-eslint/types': 8.26.0 - '@typescript-eslint/typescript-estree': 8.26.0(typescript@5.8.2) + '@typescript-eslint/scope-manager': 8.27.0 + '@typescript-eslint/types': 8.27.0 + '@typescript-eslint/typescript-estree': 8.27.0(typescript@5.8.2) eslint: 9.22.0 typescript: 5.8.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.24.0': + '@typescript-eslint/visitor-keys@8.26.0': dependencies: - '@typescript-eslint/types': 8.24.0 + '@typescript-eslint/types': 8.26.0 eslint-visitor-keys: 4.2.0 - '@typescript-eslint/visitor-keys@8.26.0': + '@typescript-eslint/visitor-keys@8.27.0': dependencies: - '@typescript-eslint/types': 8.26.0 + '@typescript-eslint/types': 8.27.0 eslint-visitor-keys: 4.2.0 '@ungap/structured-clone@1.2.0': {} @@ -15114,6 +15080,8 @@ snapshots: ansi-styles@6.2.1: {} + ansis@3.17.0: {} + any-promise@1.3.0: {} anymatch@3.1.3: @@ -15134,7 +15102,7 @@ snapshots: archiver-utils@5.0.2: dependencies: - glob: 10.3.10 + glob: 10.4.5 graceful-fs: 4.2.11 is-stream: 2.0.1 lazystream: 1.0.1 @@ -15506,10 +15474,10 @@ snapshots: node-gyp-build: 4.6.0 optional: true - bullmq@5.41.1: + bullmq@5.44.1: dependencies: cron-parser: 4.9.0 - ioredis: 5.5.0 + ioredis: 5.6.0 msgpackr: 1.11.2 node-abort-controller: 3.1.1 semver: 7.6.3 @@ -15913,13 +15881,13 @@ snapshots: crc-32: 1.2.2 readable-stream: 4.3.0 - create-jest@29.7.0(@types/node@22.13.4): + create-jest@29.7.0(@types/node@22.13.10): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@22.13.4) + jest-config: 29.7.0(@types/node@22.13.10) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -16158,10 +16126,6 @@ snapshots: optionalDependencies: supports-color: 8.1.1 - debug@4.3.4: - dependencies: - ms: 2.1.2 - debug@4.3.5: dependencies: ms: 2.1.2 @@ -16667,27 +16631,27 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.24.0(eslint@9.22.0)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint@9.22.0): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.26.0(eslint@9.22.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint@9.22.0): dependencies: debug: 3.2.7(supports-color@8.1.1) optionalDependencies: - '@typescript-eslint/parser': 8.24.0(eslint@9.22.0)(typescript@5.7.3) + '@typescript-eslint/parser': 8.26.0(eslint@9.22.0)(typescript@5.8.2) eslint: 9.22.0 eslint-import-resolver-node: 0.3.9 transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.26.0(eslint@9.22.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint@9.22.0): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.27.0(eslint@9.22.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint@9.22.0): dependencies: debug: 3.2.7(supports-color@8.1.1) optionalDependencies: - '@typescript-eslint/parser': 8.26.0(eslint@9.22.0)(typescript@5.8.2) + '@typescript-eslint/parser': 8.27.0(eslint@9.22.0)(typescript@5.8.2) eslint: 9.22.0 eslint-import-resolver-node: 0.3.9 transitivePeerDependencies: - supports-color - eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.24.0(eslint@9.22.0)(typescript@5.7.3))(eslint@9.22.0): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.26.0(eslint@9.22.0)(typescript@5.8.2))(eslint@9.22.0): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -16698,7 +16662,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.22.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.24.0(eslint@9.22.0)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint@9.22.0) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.26.0(eslint@9.22.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint@9.22.0) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 @@ -16710,13 +16674,13 @@ snapshots: string.prototype.trimend: 1.0.8 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.24.0(eslint@9.22.0)(typescript@5.7.3) + '@typescript-eslint/parser': 8.26.0(eslint@9.22.0)(typescript@5.8.2) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.26.0(eslint@9.22.0)(typescript@5.8.2))(eslint@9.22.0): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.27.0(eslint@9.22.0)(typescript@5.8.2))(eslint@9.22.0): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -16727,7 +16691,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.22.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.26.0(eslint@9.22.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint@9.22.0) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.27.0(eslint@9.22.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint@9.22.0) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 @@ -16739,7 +16703,7 @@ snapshots: string.prototype.trimend: 1.0.8 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.26.0(eslint@9.22.0)(typescript@5.8.2) + '@typescript-eslint/parser': 8.27.0(eslint@9.22.0)(typescript@5.8.2) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -17407,14 +17371,6 @@ snapshots: dependencies: is-glob: 4.0.3 - glob@10.3.10: - dependencies: - foreground-child: 3.1.1 - jackspeak: 2.3.6 - minimatch: 9.0.4 - minipass: 7.1.2 - path-scurry: 1.10.1 - glob@10.4.5: dependencies: foreground-child: 3.1.1 @@ -17796,7 +17752,7 @@ snapshots: intersection-observer@0.12.2: {} - ioredis@5.5.0: + ioredis@5.6.0: dependencies: '@ioredis/commands': 1.2.0 cluster-key-slot: 1.1.2 @@ -18055,12 +18011,6 @@ snapshots: iterare@1.2.1: {} - jackspeak@2.3.6: - dependencies: - '@isaacs/cliui': 8.0.2 - optionalDependencies: - '@pkgjs/parseargs': 0.11.0 - jackspeak@3.4.3: dependencies: '@isaacs/cliui': 8.0.2 @@ -18105,16 +18055,16 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@29.7.0(@types/node@22.13.4): + jest-cli@29.7.0(@types/node@22.13.10): dependencies: '@jest/core': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@22.13.4) + create-jest: 29.7.0(@types/node@22.13.10) exit: 0.1.2 import-local: 3.1.0 - jest-config: 29.7.0(@types/node@22.13.4) + jest-config: 29.7.0(@types/node@22.13.10) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -18173,36 +18123,6 @@ snapshots: - babel-plugin-macros - supports-color - jest-config@29.7.0(@types/node@22.13.4): - dependencies: - '@babel/core': 7.23.5 - '@jest/test-sequencer': 29.7.0 - '@jest/types': 29.6.3 - babel-jest: 29.7.0(@babel/core@7.23.5) - chalk: 4.1.2 - ci-info: 3.7.1 - deepmerge: 4.2.2 - glob: 7.2.3 - graceful-fs: 4.2.11 - jest-circus: 29.7.0 - jest-environment-node: 29.7.0 - jest-get-type: 29.6.3 - jest-regex-util: 29.6.3 - jest-resolve: 29.7.0 - jest-runner: 29.7.0 - jest-util: 29.7.0 - jest-validate: 29.7.0 - micromatch: 4.0.8 - parse-json: 5.2.0 - pretty-format: 29.7.0 - slash: 3.0.0 - strip-json-comments: 3.1.1 - optionalDependencies: - '@types/node': 22.13.4 - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - jest-config@29.7.0(@types/node@22.13.9): dependencies: '@babel/core': 7.23.5 @@ -18460,12 +18380,12 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.7.0(@types/node@22.13.4): + jest@29.7.0(@types/node@22.13.10): dependencies: '@jest/core': 29.7.0 '@jest/types': 29.6.3 import-local: 3.1.0 - jest-cli: 29.7.0(@types/node@22.13.4) + jest-cli: 29.7.0(@types/node@22.13.10) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -18551,7 +18471,7 @@ snapshots: whatwg-encoding: 3.1.1 whatwg-mimetype: 4.0.0 whatwg-url: 14.1.0 - ws: 8.18.0(bufferutil@4.0.9)(utf-8-validate@6.0.5) + ws: 8.18.1(bufferutil@4.0.9)(utf-8-validate@6.0.5) xml-name-validator: 5.0.0 optionalDependencies: canvas: 3.1.0 @@ -18636,10 +18556,11 @@ snapshots: is-promise: 2.2.2 promise: 7.3.1 - juice@11.0.0: + juice@11.0.1: dependencies: cheerio: 1.0.0 commander: 12.1.0 + entities: 4.5.0 mensch: 0.3.4 slick: 1.12.2 web-resource-inliner: 7.0.0 @@ -18705,7 +18626,7 @@ snapshots: log-update: 4.0.0 p-map: 4.0.0 rfdc: 1.4.1 - rxjs: 7.8.1 + rxjs: 7.8.2 through: 2.3.8 wrap-ansi: 7.0.0 optionalDependencies: @@ -18755,8 +18676,6 @@ snapshots: lowercase-keys@3.0.0: {} - lru-cache@10.2.2: {} - lru-cache@10.4.3: {} lru-cache@11.0.0: {} @@ -18953,7 +18872,7 @@ snapshots: media-typer@0.3.0: {} - meilisearch@0.48.2: {} + meilisearch@0.49.0: {} memoizerific@1.11.3: dependencies: @@ -19295,11 +19214,9 @@ snapshots: mkdirp@1.0.4: {} - mkdirp@2.1.6: {} - mkdirp@3.0.1: {} - mnemonist@0.39.8: + mnemonist@0.40.0: dependencies: obliterator: 2.0.4 @@ -19387,7 +19304,7 @@ snapshots: nanoid@3.3.8: {} - nanoid@5.1.0: {} + nanoid@5.1.5: {} napi-build-utils@2.0.0: optional: true @@ -19466,7 +19383,7 @@ snapshots: dependencies: env-paths: 2.2.1 exponential-backoff: 3.1.1 - glob: 10.3.10 + glob: 10.4.5 graceful-fs: 4.2.11 make-fetch-happen: 13.0.0 nopt: 7.2.0 @@ -19575,7 +19492,7 @@ snapshots: transitivePeerDependencies: - supports-color - oauth@0.10.0: {} + oauth@0.10.2: {} object-assign@4.1.1: {} @@ -19776,11 +19693,6 @@ snapshots: path-parse@1.0.7: {} - path-scurry@1.10.1: - dependencies: - lru-cache: 10.2.2 - minipass: 7.1.2 - path-scurry@1.11.1: dependencies: lru-cache: 10.4.3 @@ -19824,14 +19736,16 @@ snapshots: pg-numeric@1.0.2: {} - pg-pool@3.7.1(pg@8.13.3): + pg-pool@3.8.0(pg@8.14.1): dependencies: - pg: 8.13.3 + pg: 8.14.1 pg-protocol@1.7.0: {} pg-protocol@1.7.1: {} + pg-protocol@1.8.0: {} + pg-types@2.2.0: dependencies: pg-int8: 1.0.1 @@ -19850,11 +19764,11 @@ snapshots: postgres-interval: 3.0.0 postgres-range: 1.1.3 - pg@8.13.3: + pg@8.14.1: dependencies: pg-connection-string: 2.7.0 - pg-pool: 3.7.1(pg@8.13.3) - pg-protocol: 1.7.1 + pg-pool: 3.8.0(pg@8.14.1) + pg-protocol: 1.8.0 pg-types: 2.2.0 pgpass: 1.0.5 optionalDependencies: @@ -20652,9 +20566,9 @@ snapshots: dependencies: queue-microtask: 1.2.3 - rxjs@7.8.1: + rxjs@7.8.2: dependencies: - tslib: 2.6.2 + tslib: 2.8.1 safe-array-concat@1.0.0: dependencies: @@ -20703,6 +20617,15 @@ snapshots: parse-srcset: 1.0.2 postcss: 8.5.3 + sanitize-html@2.15.0: + dependencies: + deepmerge: 4.2.2 + escape-string-regexp: 4.0.0 + htmlparser2: 8.0.1 + is-plain-object: 5.0.0 + parse-srcset: 1.0.2 + postcss: 8.5.3 + sass@1.85.1: dependencies: chokidar: 4.0.3 @@ -21039,6 +20962,8 @@ snapshots: sprintf-js@1.1.3: {} + sql-highlight@6.0.0: {} + sshpk@1.17.0: dependencies: asn1: 0.2.6 @@ -21468,10 +21393,6 @@ snapshots: trough@2.2.0: {} - ts-api-utils@2.0.1(typescript@5.7.3): - dependencies: - typescript: 5.7.3 - ts-api-utils@2.0.1(typescript@5.8.2): dependencies: typescript: 5.8.2 @@ -21482,15 +21403,6 @@ snapshots: ts-map@1.0.3: {} - tsc-alias@1.8.10: - dependencies: - chokidar: 4.0.3 - commander: 9.5.0 - globby: 11.1.0 - mylas: 2.1.13 - normalize-path: 3.0.0 - plimit-lit: 1.5.0 - tsc-alias@1.8.11: dependencies: chokidar: 4.0.3 @@ -21624,26 +21536,25 @@ snapshots: typedarray@0.0.6: {} - typeorm@0.3.20(ioredis@5.5.0)(pg@8.13.3): + typeorm@0.3.21(ioredis@5.6.0)(pg@8.14.1)(reflect-metadata@0.2.2): dependencies: '@sqltools/formatter': 1.2.5 + ansis: 3.17.0 app-root-path: 3.1.0 buffer: 6.0.3 - chalk: 4.1.2 - cli-highlight: 2.1.11 dayjs: 1.11.10 - debug: 4.3.4 + debug: 4.4.0(supports-color@8.1.1) dotenv: 16.0.3 - glob: 10.3.10 - mkdirp: 2.1.6 + glob: 10.4.5 reflect-metadata: 0.2.2 sha.js: 2.4.11 - tslib: 2.6.2 - uuid: 9.0.1 + sql-highlight: 6.0.0 + tslib: 2.8.1 + uuid: 11.1.0 yargs: 17.7.2 optionalDependencies: - ioredis: 5.5.0 - pg: 8.13.3 + ioredis: 5.6.0 + pg: 8.14.1 transitivePeerDependencies: - supports-color @@ -21659,7 +21570,7 @@ snapshots: uint8array-extras@1.4.0: {} - ulid@2.3.0: {} + ulid@2.4.0: {} unbox-primitive@1.0.2: dependencies: @@ -22009,7 +21920,7 @@ snapshots: joi: 17.13.3 lodash: 4.17.21 minimist: 1.2.8 - rxjs: 7.8.1 + rxjs: 7.8.2 transitivePeerDependencies: - debug @@ -22157,7 +22068,7 @@ snapshots: imurmurhash: 0.1.4 signal-exit: 3.0.7 - ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@6.0.5): + ws@8.18.1(bufferutil@4.0.9)(utf-8-validate@6.0.5): optionalDependencies: bufferutil: 4.0.9 utf-8-validate: 6.0.5 @@ -22229,7 +22140,7 @@ snapshots: yargs@17.7.2: dependencies: cliui: 8.0.1 - escalade: 3.1.1 + escalade: 3.2.0 get-caller-file: 2.0.5 require-directory: 2.1.1 string-width: 4.2.3 -- cgit v1.2.3-freya From f1f24e39d2df3135493e2c2087230b428e2d02b7 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Mon, 24 Mar 2025 21:32:46 +0900 Subject: Feat: Chat (#15686) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * wip * wip * wip * wip * wip * wip * Update types.ts * Create 1742203321812-chat.js * wip * wip * Update room.vue * Update home.vue * Update home.vue * Update ja-JP.yml * Update index.d.ts * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * Update CHANGELOG.md * wip * Update home.vue * clean up * Update misskey-js.api.md * wip * wip * wip * wip * wip * wip * wip * wip * wip * lint fixes * lint * Update UserEntityService.ts * search * wip * 🎨 * wip * Update home.ownedRooms.vue * wip * Update CHANGELOG.md * Update style.scss * wip * improve performance * improve performance * Update timeline.test.ts --- CHANGELOG.md | 13 +- locales/index.d.ts | 189 ++- locales/ja-JP.yml | 50 +- packages/backend/migration/1742203321812-chat.js | 46 + packages/backend/migration/1742608337548-chat-2.js | 18 + packages/backend/migration/1742617546147-chat-3.js | 26 + packages/backend/migration/1742707840715-chat-4.js | 26 + packages/backend/migration/1742721896936-chat-5.js | 16 + packages/backend/migration/1742795111958-chat-6.js | 20 + packages/backend/src/core/ChatService.ts | 776 +++++++++ packages/backend/src/core/CoreModule.ts | 18 +- packages/backend/src/core/GlobalEventService.ts | 38 +- packages/backend/src/core/NoteCreateService.ts | 27 - packages/backend/src/core/NoteReadService.ts | 143 -- packages/backend/src/core/RoleService.ts | 3 + packages/backend/src/core/UserFollowingService.ts | 28 +- packages/backend/src/core/WebhookTestService.ts | 2 + .../backend/src/core/entities/ChatEntityService.ts | 376 +++++ .../backend/src/core/entities/UserEntityService.ts | 20 +- packages/backend/src/di-symbols.ts | 6 +- packages/backend/src/misc/json-schema.ts | 87 +- packages/backend/src/models/ChatApproval.ts | 39 + packages/backend/src/models/ChatMessage.ts | 85 + packages/backend/src/models/ChatRoom.ts | 41 + packages/backend/src/models/ChatRoomInvitation.ts | 45 + packages/backend/src/models/ChatRoomMembership.ts | 45 + packages/backend/src/models/NoteUnread.ts | 68 - packages/backend/src/models/RepositoryModule.ts | 60 +- packages/backend/src/models/User.ts | 11 + packages/backend/src/models/_.ts | 23 +- .../backend/src/models/json-schema/chat-message.ts | 146 ++ .../src/models/json-schema/chat-room-invitation.ts | 37 + .../src/models/json-schema/chat-room-membership.ts | 37 + .../backend/src/models/json-schema/chat-room.ts | 40 + packages/backend/src/models/json-schema/role.ts | 4 + packages/backend/src/models/json-schema/user.ts | 9 + packages/backend/src/postgres.ts | 21 +- packages/backend/src/server/ServerModule.ts | 4 + .../src/server/api/StreamingApiServerService.ts | 3 - packages/backend/src/server/api/endpoint-list.ts | 23 +- .../src/server/api/endpoints/antennas/notes.ts | 7 - .../src/server/api/endpoints/chat/history.ts | 73 + .../api/endpoints/chat/messages/create-to-room.ts | 105 ++ .../api/endpoints/chat/messages/create-to-user.ts | 122 ++ .../server/api/endpoints/chat/messages/delete.ts | 52 + .../server/api/endpoints/chat/messages/react.ts | 49 + .../api/endpoints/chat/messages/room-timeline.ts | 73 + .../server/api/endpoints/chat/messages/search.ts | 76 + .../src/server/api/endpoints/chat/messages/show.ts | 63 + .../api/endpoints/chat/messages/user-timeline.ts | 71 + .../src/server/api/endpoints/chat/rooms/create.ts | 62 + .../src/server/api/endpoints/chat/rooms/delete.ts | 52 + .../api/endpoints/chat/rooms/invitations/create.ts | 68 + .../api/endpoints/chat/rooms/invitations/ignore.ts | 48 + .../api/endpoints/chat/rooms/invitations/inbox.ts | 54 + .../src/server/api/endpoints/chat/rooms/join.ts | 48 + .../src/server/api/endpoints/chat/rooms/joining.ts | 58 + .../src/server/api/endpoints/chat/rooms/leave.ts | 48 + .../src/server/api/endpoints/chat/rooms/members.ts | 74 + .../src/server/api/endpoints/chat/rooms/mute.ts | 49 + .../src/server/api/endpoints/chat/rooms/owned.ts | 54 + .../src/server/api/endpoints/chat/rooms/show.ts | 58 + .../src/server/api/endpoints/chat/rooms/update.ts | 65 + .../api/endpoints/i/notifications-grouped.ts | 13 - .../src/server/api/endpoints/i/notifications.ts | 11 - .../api/endpoints/i/read-all-unread-notes.ts | 45 - .../backend/src/server/api/endpoints/i/update.ts | 2 + .../src/server/api/endpoints/notes/mentions.ts | 4 - .../api/endpoints/notes/thread-muting/create.ts | 4 - .../src/server/api/stream/ChannelsService.ts | 6 + .../backend/src/server/api/stream/Connection.ts | 17 +- .../src/server/api/stream/channels/chat-room.ts | 78 + .../src/server/api/stream/channels/chat-user.ts | 78 + .../backend/test-federation/test/timeline.test.ts | 2 +- packages/backend/test/e2e/mute.ts | 22 - packages/backend/test/e2e/thread-mute.ts | 42 - packages/backend/test/e2e/users.ts | 6 +- .../test/unit/entities/UserEntityService.ts | 2 + .../frontend-embed/src/components/EmPagination.vue | 20 +- packages/frontend-shared/js/const.ts | 1 + packages/frontend-shared/js/scroll.ts | 12 +- packages/frontend/src/boot/main-boot.ts | 25 +- packages/frontend/src/components/MkButton.vue | 10 +- .../src/components/MkDateSeparatedList.vue | 22 +- packages/frontend/src/components/MkFukidashi.vue | 9 +- packages/frontend/src/components/MkMediaList.vue | 1 - packages/frontend/src/components/MkMenu.vue | 66 +- packages/frontend/src/components/MkPagination.vue | 39 +- packages/frontend/src/components/MkPolkadots.vue | 40 + packages/frontend/src/components/MkUrlPreview.vue | 1 + packages/frontend/src/local-storage.ts | 2 +- packages/frontend/src/navbar.ts | 8 +- packages/frontend/src/pages/admin/roles.editor.vue | 20 + packages/frontend/src/pages/admin/roles.vue | 8 + packages/frontend/src/pages/chat/XMessage.vue | 245 +++ packages/frontend/src/pages/chat/XRoom.vue | 41 + packages/frontend/src/pages/chat/home.home.vue | 252 +++ .../frontend/src/pages/chat/home.invitations.vue | 98 ++ .../frontend/src/pages/chat/home.joiningRooms.vue | 54 + .../frontend/src/pages/chat/home.ownedRooms.vue | 54 + packages/frontend/src/pages/chat/home.vue | 60 + packages/frontend/src/pages/chat/message.vue | 55 + packages/frontend/src/pages/chat/room.form.vue | 333 ++++ packages/frontend/src/pages/chat/room.info.vue | 87 + packages/frontend/src/pages/chat/room.members.vue | 73 + packages/frontend/src/pages/chat/room.search.vue | 68 + packages/frontend/src/pages/chat/room.vue | 426 +++++ .../frontend/src/pages/settings/notifications.vue | 5 - packages/frontend/src/pages/settings/privacy.vue | 16 + packages/frontend/src/pages/settings/sounds.vue | 1 + packages/frontend/src/preferences/def.ts | 7 + packages/frontend/src/router.definition.ts | 16 + packages/frontend/src/style.scss | 8 +- packages/frontend/src/types/menu.ts | 16 +- packages/frontend/src/ui/_common_/navbar.vue | 8 +- .../src/utility/autogen/settings-search-index.ts | 13 +- packages/frontend/src/utility/get-user-menu.ts | 73 +- packages/frontend/src/utility/sound.ts | 1 + packages/frontend/src/utility/upload.ts | 2 +- packages/misskey-js/README.md | 10 +- packages/misskey-js/etc/misskey-js.api.md | 210 ++- packages/misskey-js/src/autogen/apiClientJSDoc.ts | 253 ++- packages/misskey-js/src/autogen/endpoint.ts | 67 +- packages/misskey-js/src/autogen/entities.ts | 44 + packages/misskey-js/src/autogen/models.ts | 5 + packages/misskey-js/src/autogen/types.ts | 1683 ++++++++++++++++++-- packages/misskey-js/src/consts.ts | 6 +- packages/misskey-js/src/streaming.types.ts | 7 +- packages/misskey-js/test/streaming.ts | 38 +- 129 files changed, 8226 insertions(+), 823 deletions(-) create mode 100644 packages/backend/migration/1742203321812-chat.js create mode 100644 packages/backend/migration/1742608337548-chat-2.js create mode 100644 packages/backend/migration/1742617546147-chat-3.js create mode 100644 packages/backend/migration/1742707840715-chat-4.js create mode 100644 packages/backend/migration/1742721896936-chat-5.js create mode 100644 packages/backend/migration/1742795111958-chat-6.js create mode 100644 packages/backend/src/core/ChatService.ts delete mode 100644 packages/backend/src/core/NoteReadService.ts create mode 100644 packages/backend/src/core/entities/ChatEntityService.ts create mode 100644 packages/backend/src/models/ChatApproval.ts create mode 100644 packages/backend/src/models/ChatMessage.ts create mode 100644 packages/backend/src/models/ChatRoom.ts create mode 100644 packages/backend/src/models/ChatRoomInvitation.ts create mode 100644 packages/backend/src/models/ChatRoomMembership.ts delete mode 100644 packages/backend/src/models/NoteUnread.ts create mode 100644 packages/backend/src/models/json-schema/chat-message.ts create mode 100644 packages/backend/src/models/json-schema/chat-room-invitation.ts create mode 100644 packages/backend/src/models/json-schema/chat-room-membership.ts create mode 100644 packages/backend/src/models/json-schema/chat-room.ts create mode 100644 packages/backend/src/server/api/endpoints/chat/history.ts create mode 100644 packages/backend/src/server/api/endpoints/chat/messages/create-to-room.ts create mode 100644 packages/backend/src/server/api/endpoints/chat/messages/create-to-user.ts create mode 100644 packages/backend/src/server/api/endpoints/chat/messages/delete.ts create mode 100644 packages/backend/src/server/api/endpoints/chat/messages/react.ts create mode 100644 packages/backend/src/server/api/endpoints/chat/messages/room-timeline.ts create mode 100644 packages/backend/src/server/api/endpoints/chat/messages/search.ts create mode 100644 packages/backend/src/server/api/endpoints/chat/messages/show.ts create mode 100644 packages/backend/src/server/api/endpoints/chat/messages/user-timeline.ts create mode 100644 packages/backend/src/server/api/endpoints/chat/rooms/create.ts create mode 100644 packages/backend/src/server/api/endpoints/chat/rooms/delete.ts create mode 100644 packages/backend/src/server/api/endpoints/chat/rooms/invitations/create.ts create mode 100644 packages/backend/src/server/api/endpoints/chat/rooms/invitations/ignore.ts create mode 100644 packages/backend/src/server/api/endpoints/chat/rooms/invitations/inbox.ts create mode 100644 packages/backend/src/server/api/endpoints/chat/rooms/join.ts create mode 100644 packages/backend/src/server/api/endpoints/chat/rooms/joining.ts create mode 100644 packages/backend/src/server/api/endpoints/chat/rooms/leave.ts create mode 100644 packages/backend/src/server/api/endpoints/chat/rooms/members.ts create mode 100644 packages/backend/src/server/api/endpoints/chat/rooms/mute.ts create mode 100644 packages/backend/src/server/api/endpoints/chat/rooms/owned.ts create mode 100644 packages/backend/src/server/api/endpoints/chat/rooms/show.ts create mode 100644 packages/backend/src/server/api/endpoints/chat/rooms/update.ts delete mode 100644 packages/backend/src/server/api/endpoints/i/read-all-unread-notes.ts create mode 100644 packages/backend/src/server/api/stream/channels/chat-room.ts create mode 100644 packages/backend/src/server/api/stream/channels/chat-user.ts create mode 100644 packages/frontend/src/components/MkPolkadots.vue create mode 100644 packages/frontend/src/pages/chat/XMessage.vue create mode 100644 packages/frontend/src/pages/chat/XRoom.vue create mode 100644 packages/frontend/src/pages/chat/home.home.vue create mode 100644 packages/frontend/src/pages/chat/home.invitations.vue create mode 100644 packages/frontend/src/pages/chat/home.joiningRooms.vue create mode 100644 packages/frontend/src/pages/chat/home.ownedRooms.vue create mode 100644 packages/frontend/src/pages/chat/home.vue create mode 100644 packages/frontend/src/pages/chat/message.vue create mode 100644 packages/frontend/src/pages/chat/room.form.vue create mode 100644 packages/frontend/src/pages/chat/room.info.vue create mode 100644 packages/frontend/src/pages/chat/room.members.vue create mode 100644 packages/frontend/src/pages/chat/room.search.vue create mode 100644 packages/frontend/src/pages/chat/room.vue (limited to 'packages/backend/src/misc') diff --git a/CHANGELOG.md b/CHANGELOG.md index 80d505f696..ff95b6b5dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,17 @@ ## 2025.3.2 ### General -- セキュリティを強化するため、ジョブキューのダッシュボード(bull-board)統合が削除されました。 +- Feat: チャットがリニューアルして復活しました(beta) + - 既存のDM機能よりも便利で効率的な実装になっています + - チャットを受け付ける相手を制限可能です + - 誰でも / フォローユーザーのみ / フォロワーのみ / 相互のみ / 受け付けない から選択できます + - 自分からメッセージを送った相手とは上記の設定に関わらずチャット可能です + - チャット機能を開放するかどうかをロールで制御可能です + - ルームを作成して、複数人でのチャットも可能です + - 過去自分が送ったメッセージ・自分に送られたメッセージの検索が可能です + - 参加中のルームをミュートして通知が来ないように設定可能です + - メッセージにはリアクションも可能です +- Enhance: セキュリティを強化するため、ジョブキューのダッシュボード(bull-board)統合が削除されました。 - Misskeyネイティブでダッシュボードを実装予定です ### Client @@ -37,6 +47,7 @@ - Fix: テーマ切り替え時に一部の色が変わらない問題を修正 ### Server +- Enhance 全体的なパフォーマンス向上 - Fix: プロフィール追加情報で無効なURLに入力された場合に照会エラーを出るのを修正 - Fix: ActivityPubリクエストURLチェック実装は仕様に従っていないのを修正 - Fix: 連合無しモードでも外部から照会可能だった問題を修正 diff --git a/locales/index.d.ts b/locales/index.d.ts index 2ac82edd37..9ef6f9aa39 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -1174,10 +1174,6 @@ export interface Locale extends ILocale { * 保存しました */ "saved": string; - /** - * チャット - */ - "messaging": string; /** * アップロード */ @@ -1227,9 +1223,9 @@ export interface Locale extends ILocale { */ "noMoreHistory": string; /** - * チャットを開始 + * チャットを始める */ - "startMessaging": string; + "startChat": string; /** * {n}人が読みました */ @@ -1982,14 +1978,6 @@ export interface Locale extends ILocale { * クリップボードのテキストが長いです。テキストファイルとして添付しますか? */ "attachAsFileQuestion": string; - /** - * まだチャットはありません - */ - "noMessagesYet": string; - /** - * 新しいメッセージがあります - */ - "newMessageExists": string; /** * メッセージに添付できるファイルはひとつです */ @@ -5358,6 +5346,163 @@ export interface Locale extends ILocale { * 情報 */ "information": string; + /** + * チャット + */ + "chat": string; + "_chat": { + /** + * まだメッセージはありません + */ + "noMessagesYet": string; + /** + * 新しいメッセージ + */ + "newMessage": string; + /** + * 個人チャット + */ + "individualChat": string; + /** + * 特定ユーザーとの一対一のチャットができます。 + */ + "individualChat_description": string; + /** + * ルームチャット + */ + "roomChat": string; + /** + * 複数人でのチャットができます。 + * また、個人チャットを許可していないユーザーとでも、相手が受け入れればチャットができます。 + */ + "roomChat_description": string; + /** + * ルームを作成 + */ + "createRoom": string; + /** + * ユーザーを招待してチャットを始めましょう + */ + "inviteUserToChat": string; + /** + * 作成したルーム + */ + "yourRooms": string; + /** + * 参加中のルーム + */ + "joiningRooms": string; + /** + * 招待 + */ + "invitations": string; + /** + * 招待はありません + */ + "noInvitations": string; + /** + * 履歴 + */ + "history": string; + /** + * 履歴はありません + */ + "noHistory": string; + /** + * ルームはありません + */ + "noRooms": string; + /** + * ユーザーを招待 + */ + "inviteUser": string; + /** + * 参加 + */ + "join": string; + /** + * 無視 + */ + "ignore": string; + /** + * ルームから退出 + */ + "leave": string; + /** + * メンバー + */ + "members": string; + /** + * メッセージを検索 + */ + "searchMessages": string; + /** + * ホーム + */ + "home": string; + /** + * このルームをミュート + */ + "muteThisRoom": string; + /** + * このユーザーとのチャットを開始できません + */ + "cannotChatWithTheUser": string; + /** + * チャットが使えない状態になっているか、相手がチャットを開放していません。 + */ + "cannotChatWithTheUser_description": string; + /** + * チャットする + */ + "chatWithThisUser": string; + /** + * このユーザーはフォロワーからのみチャットを受け付けています。 + */ + "thisUserAllowsChatOnlyFromFollowers": string; + /** + * このユーザーはフォローしているユーザーからのみチャットを受け付けています。 + */ + "thisUserAllowsChatOnlyFromFollowing": string; + /** + * このユーザーは相互フォローのユーザーからのみチャットを受け付けています。 + */ + "thisUserAllowsChatOnlyFromMutualFollowing": string; + /** + * このユーザーは誰からもチャットを受け付けていません。 + */ + "thisUserNotAllowedChatAnyone": string; + /** + * チャットを許可する相手 + */ + "chatAllowedUsers": string; + /** + * 自分からチャットメッセージを送った相手とはこの設定に関わらずチャットが可能です。 + */ + "chatAllowedUsers_note": string; + "_chatAllowedUsers": { + /** + * 誰でも + */ + "everyone": string; + /** + * 自分のフォロワーのみ + */ + "followers": string; + /** + * 自分がフォローしているユーザーのみ + */ + "following": string; + /** + * 相互フォローのユーザーのみ + */ + "mutual": string; + /** + * 誰も許可しない + */ + "none": string; + }; + }; "_emojiPalette": { /** * パレット @@ -7285,6 +7430,10 @@ export interface Locale extends ILocale { * リストのインポートを許可 */ "canImportUserLists": string; + /** + * チャットを許可 + */ + "canChat": string; }; "_condition": { /** @@ -8154,6 +8303,10 @@ export interface Locale extends ILocale { * リアクション選択時 */ "reaction": string; + /** + * チャットのメッセージ + */ + "chatMessage": string; }; "_soundSettings": { /** @@ -8726,6 +8879,14 @@ export interface Locale extends ILocale { * 違反を報告する */ "write:report-abuse": string; + /** + * チャットを操作する + */ + "write:chat": string; + /** + * チャットを閲覧する + */ + "read:chat": string; }; "_auth": { /** diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 35f368d3ed..fccf24e759 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -289,7 +289,6 @@ deleteAreYouSure: "「{x}」を削除しますか?" resetAreYouSure: "リセットしますか?" areYouSure: "よろしいですか?" saved: "保存しました" -messaging: "チャット" upload: "アップロード" keepOriginalUploading: "オリジナル画像を保持" keepOriginalUploadingDescription: "画像をアップロードする時にオリジナル版を保持します。オフにするとアップロード時にブラウザでWeb公開用画像を生成します。" @@ -302,7 +301,7 @@ uploadFromUrlMayTakeTime: "アップロードが完了するまで時間がか explore: "みつける" messageRead: "既読" noMoreHistory: "これより過去の履歴はありません" -startMessaging: "チャットを開始" +startChat: "チャットを始める" nUsersRead: "{n}人が読みました" agreeTo: "{0}に同意" agree: "同意する" @@ -491,8 +490,6 @@ noteOf: "{user}のノート" quoteAttached: "引用付き" quoteQuestion: "引用として添付しますか?" attachAsFileQuestion: "クリップボードのテキストが長いです。テキストファイルとして添付しますか?" -noMessagesYet: "まだチャットはありません" -newMessageExists: "新しいメッセージがあります" onlyOneFileCanBeAttached: "メッセージに添付できるファイルはひとつです" signinRequired: "続行する前に、登録またはログインが必要です" signinOrContinueOnRemote: "続行するには、お使いのサーバーに移動するか、このサーバーに登録・ログインする必要があります" @@ -1335,6 +1332,47 @@ emojiPalette: "絵文字パレット" postForm: "投稿フォーム" textCount: "文字数" information: "情報" +chat: "チャット" + +_chat: + noMessagesYet: "まだメッセージはありません" + newMessage: "新しいメッセージ" + individualChat: "個人チャット" + individualChat_description: "特定ユーザーとの一対一のチャットができます。" + roomChat: "ルームチャット" + roomChat_description: "複数人でのチャットができます。\nまた、個人チャットを許可していないユーザーとでも、相手が受け入れればチャットができます。" + createRoom: "ルームを作成" + inviteUserToChat: "ユーザーを招待してチャットを始めましょう" + yourRooms: "作成したルーム" + joiningRooms: "参加中のルーム" + invitations: "招待" + noInvitations: "招待はありません" + history: "履歴" + noHistory: "履歴はありません" + noRooms: "ルームはありません" + inviteUser: "ユーザーを招待" + join: "参加" + ignore: "無視" + leave: "ルームから退出" + members: "メンバー" + searchMessages: "メッセージを検索" + home: "ホーム" + muteThisRoom: "このルームをミュート" + cannotChatWithTheUser: "このユーザーとのチャットを開始できません" + cannotChatWithTheUser_description: "チャットが使えない状態になっているか、相手がチャットを開放していません。" + chatWithThisUser: "チャットする" + thisUserAllowsChatOnlyFromFollowers: "このユーザーはフォロワーからのみチャットを受け付けています。" + thisUserAllowsChatOnlyFromFollowing: "このユーザーはフォローしているユーザーからのみチャットを受け付けています。" + thisUserAllowsChatOnlyFromMutualFollowing: "このユーザーは相互フォローのユーザーからのみチャットを受け付けています。" + thisUserNotAllowedChatAnyone: "このユーザーは誰からもチャットを受け付けていません。" + chatAllowedUsers: "チャットを許可する相手" + chatAllowedUsers_note: "自分からチャットメッセージを送った相手とはこの設定に関わらずチャットが可能です。" + _chatAllowedUsers: + everyone: "誰でも" + followers: "自分のフォロワーのみ" + following: "自分がフォローしているユーザーのみ" + mutual: "相互フォローのユーザーのみ" + none: "誰も許可しない" _emojiPalette: palettes: "パレット" @@ -1886,6 +1924,7 @@ _role: canImportFollowing: "フォローのインポートを許可" canImportMuting: "ミュートのインポートを許可" canImportUserLists: "リストのインポートを許可" + canChat: "チャットを許可" _condition: roleAssignedTo: "マニュアルロールにアサイン済み" isLocal: "ローカルユーザー" @@ -2139,6 +2178,7 @@ _sfx: noteMy: "ノート(自分)" notification: "通知" reaction: "リアクション選択時" + chatMessage: "チャットのメッセージ" _soundSettings: driveFile: "ドライブの音声を使用" @@ -2291,6 +2331,8 @@ _permissions: "read:clip-favorite": "クリップのいいねを見る" "read:federation": "連合に関する情報を取得する" "write:report-abuse": "違反を報告する" + "write:chat": "チャットを操作する" + "read:chat": "チャットを閲覧する" _auth: shareAccessTitle: "アプリへのアクセス許可" diff --git a/packages/backend/migration/1742203321812-chat.js b/packages/backend/migration/1742203321812-chat.js new file mode 100644 index 0000000000..3d8f7276b5 --- /dev/null +++ b/packages/backend/migration/1742203321812-chat.js @@ -0,0 +1,46 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class Chat1742203321812 { + name = 'Chat1742203321812' + + async up(queryRunner) { + await queryRunner.query(`CREATE TABLE "chat_room" ("id" character varying(32) NOT NULL, "name" character varying(256) NOT NULL, "ownerId" character varying(32) NOT NULL, CONSTRAINT "PK_8aa3a52cf74c96469f0ef9fbe3e" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE INDEX "IDX_f0d8ad64243fa2ca2800da0dfd" ON "chat_room" ("ownerId") `); + await queryRunner.query(`CREATE TABLE "chat_message" ("id" character varying(32) NOT NULL, "fromUserId" character varying(32) NOT NULL, "toUserId" character varying(32), "toRoomId" character varying(32), "text" character varying(4096), "uri" character varying(512), "reads" character varying(32) array NOT NULL DEFAULT '{}', "fileId" character varying(32), "reactions" character varying(1024) array NOT NULL DEFAULT '{}', CONSTRAINT "PK_3cc0d85193aade457d3077dd06b" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE INDEX "IDX_79a26e7a4d9afa5e4fc05f134e" ON "chat_message" ("fromUserId") `); + await queryRunner.query(`CREATE INDEX "IDX_25e097b51d7622c249452c6f75" ON "chat_message" ("toUserId") `); + await queryRunner.query(`CREATE INDEX "IDX_f006b8a76efd1abf9f221c175c" ON "chat_message" ("toRoomId") `); + await queryRunner.query(`CREATE TABLE "chat_room_membership" ("id" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "roomId" character varying(32) NOT NULL, CONSTRAINT "PK_2bd59c741e571b283c048beb69a" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE INDEX "IDX_d99c5279460fb77ef58c596ce5" ON "chat_room_membership" ("userId") `); + await queryRunner.query(`CREATE INDEX "IDX_c25143ebab714e930aeca1c0e8" ON "chat_room_membership" ("roomId") `); + await queryRunner.query(`ALTER TABLE "chat_room" ADD CONSTRAINT "FK_f0d8ad64243fa2ca2800da0dfd6" FOREIGN KEY ("ownerId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "chat_message" ADD CONSTRAINT "FK_79a26e7a4d9afa5e4fc05f134ed" FOREIGN KEY ("fromUserId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "chat_message" ADD CONSTRAINT "FK_25e097b51d7622c249452c6f757" FOREIGN KEY ("toUserId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "chat_message" ADD CONSTRAINT "FK_f006b8a76efd1abf9f221c175ce" FOREIGN KEY ("toRoomId") REFERENCES "chat_room"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "chat_message" ADD CONSTRAINT "FK_fd0f9a4879430239715ad4f8e2a" FOREIGN KEY ("fileId") REFERENCES "drive_file"("id") ON DELETE SET NULL ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "chat_room_membership" ADD CONSTRAINT "FK_d99c5279460fb77ef58c596ce51" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "chat_room_membership" ADD CONSTRAINT "FK_c25143ebab714e930aeca1c0e8d" FOREIGN KEY ("roomId") REFERENCES "chat_room"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "chat_room_membership" DROP CONSTRAINT "FK_c25143ebab714e930aeca1c0e8d"`); + await queryRunner.query(`ALTER TABLE "chat_room_membership" DROP CONSTRAINT "FK_d99c5279460fb77ef58c596ce51"`); + await queryRunner.query(`ALTER TABLE "chat_message" DROP CONSTRAINT "FK_fd0f9a4879430239715ad4f8e2a"`); + await queryRunner.query(`ALTER TABLE "chat_message" DROP CONSTRAINT "FK_f006b8a76efd1abf9f221c175ce"`); + await queryRunner.query(`ALTER TABLE "chat_message" DROP CONSTRAINT "FK_25e097b51d7622c249452c6f757"`); + await queryRunner.query(`ALTER TABLE "chat_message" DROP CONSTRAINT "FK_79a26e7a4d9afa5e4fc05f134ed"`); + await queryRunner.query(`ALTER TABLE "chat_room" DROP CONSTRAINT "FK_f0d8ad64243fa2ca2800da0dfd6"`); + await queryRunner.query(`DROP INDEX "public"."IDX_c25143ebab714e930aeca1c0e8"`); + await queryRunner.query(`DROP INDEX "public"."IDX_d99c5279460fb77ef58c596ce5"`); + await queryRunner.query(`DROP TABLE "chat_room_membership"`); + await queryRunner.query(`DROP INDEX "public"."IDX_f006b8a76efd1abf9f221c175c"`); + await queryRunner.query(`DROP INDEX "public"."IDX_25e097b51d7622c249452c6f75"`); + await queryRunner.query(`DROP INDEX "public"."IDX_79a26e7a4d9afa5e4fc05f134e"`); + await queryRunner.query(`DROP TABLE "chat_message"`); + await queryRunner.query(`DROP INDEX "public"."IDX_f0d8ad64243fa2ca2800da0dfd"`); + await queryRunner.query(`DROP TABLE "chat_room"`); + } +} diff --git a/packages/backend/migration/1742608337548-chat-2.js b/packages/backend/migration/1742608337548-chat-2.js new file mode 100644 index 0000000000..9f74a263d6 --- /dev/null +++ b/packages/backend/migration/1742608337548-chat-2.js @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class Chat21742608337548 { + name = 'Chat21742608337548' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "user" ADD "chatScope" character varying(128) NOT NULL DEFAULT 'mutual'`); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_185b6b5afa707b5d36d1ce3144" ON "chat_room_membership" ("userId", "roomId") `); + } + + async down(queryRunner) { + await queryRunner.query(`DROP INDEX "public"."IDX_185b6b5afa707b5d36d1ce3144"`); + await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "chatScope"`); + } +} diff --git a/packages/backend/migration/1742617546147-chat-3.js b/packages/backend/migration/1742617546147-chat-3.js new file mode 100644 index 0000000000..116b9a738b --- /dev/null +++ b/packages/backend/migration/1742617546147-chat-3.js @@ -0,0 +1,26 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class Chat31742617546147 { + name = 'Chat31742617546147' + + async up(queryRunner) { + await queryRunner.query(`CREATE TABLE "chat_approval" ("id" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "otherId" character varying(32) NOT NULL, CONSTRAINT "PK_fbbb95d60acf5c85388345b5f5d" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE INDEX "IDX_530257863e1381a7f2f1d3282f" ON "chat_approval" ("userId") `); + await queryRunner.query(`CREATE INDEX "IDX_b1d46037f23d170da5c05fdf75" ON "chat_approval" ("otherId") `); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_12c4768a2f706fc267f2078903" ON "chat_approval" ("userId", "otherId") `); + await queryRunner.query(`ALTER TABLE "chat_approval" ADD CONSTRAINT "FK_530257863e1381a7f2f1d3282fe" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "chat_approval" ADD CONSTRAINT "FK_b1d46037f23d170da5c05fdf755" FOREIGN KEY ("otherId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "chat_approval" DROP CONSTRAINT "FK_b1d46037f23d170da5c05fdf755"`); + await queryRunner.query(`ALTER TABLE "chat_approval" DROP CONSTRAINT "FK_530257863e1381a7f2f1d3282fe"`); + await queryRunner.query(`DROP INDEX "public"."IDX_12c4768a2f706fc267f2078903"`); + await queryRunner.query(`DROP INDEX "public"."IDX_b1d46037f23d170da5c05fdf75"`); + await queryRunner.query(`DROP INDEX "public"."IDX_530257863e1381a7f2f1d3282f"`); + await queryRunner.query(`DROP TABLE "chat_approval"`); + } +} diff --git a/packages/backend/migration/1742707840715-chat-4.js b/packages/backend/migration/1742707840715-chat-4.js new file mode 100644 index 0000000000..953a53d880 --- /dev/null +++ b/packages/backend/migration/1742707840715-chat-4.js @@ -0,0 +1,26 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class Chat41742707840715 { + name = 'Chat41742707840715' + + async up(queryRunner) { + await queryRunner.query(`CREATE TABLE "chat_room_invitation" ("id" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "roomId" character varying(32) NOT NULL, CONSTRAINT "PK_9d489521a312dd28225672de2dc" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE INDEX "IDX_8552bb38e7ed038c5bdd398a38" ON "chat_room_invitation" ("userId") `); + await queryRunner.query(`CREATE INDEX "IDX_5f265075b215fc390a57523b12" ON "chat_room_invitation" ("roomId") `); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_044f2a7962b8ee5bbfaa02e8a3" ON "chat_room_invitation" ("userId", "roomId") `); + await queryRunner.query(`ALTER TABLE "chat_room_invitation" ADD CONSTRAINT "FK_8552bb38e7ed038c5bdd398a384" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "chat_room_invitation" ADD CONSTRAINT "FK_5f265075b215fc390a57523b12a" FOREIGN KEY ("roomId") REFERENCES "chat_room"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "chat_room_invitation" DROP CONSTRAINT "FK_5f265075b215fc390a57523b12a"`); + await queryRunner.query(`ALTER TABLE "chat_room_invitation" DROP CONSTRAINT "FK_8552bb38e7ed038c5bdd398a384"`); + await queryRunner.query(`DROP INDEX "public"."IDX_044f2a7962b8ee5bbfaa02e8a3"`); + await queryRunner.query(`DROP INDEX "public"."IDX_5f265075b215fc390a57523b12"`); + await queryRunner.query(`DROP INDEX "public"."IDX_8552bb38e7ed038c5bdd398a38"`); + await queryRunner.query(`DROP TABLE "chat_room_invitation"`); + } +} diff --git a/packages/backend/migration/1742721896936-chat-5.js b/packages/backend/migration/1742721896936-chat-5.js new file mode 100644 index 0000000000..00db787cb7 --- /dev/null +++ b/packages/backend/migration/1742721896936-chat-5.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class Chat51742721896936 { + name = 'Chat51742721896936' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "chat_room_invitation" ADD "ignored" boolean NOT NULL DEFAULT false`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "chat_room_invitation" DROP COLUMN "ignored"`); + } +} diff --git a/packages/backend/migration/1742795111958-chat-6.js b/packages/backend/migration/1742795111958-chat-6.js new file mode 100644 index 0000000000..9a5dc3e32f --- /dev/null +++ b/packages/backend/migration/1742795111958-chat-6.js @@ -0,0 +1,20 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class Chat61742795111958 { + name = 'Chat61742795111958' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "chat_room" ADD "description" character varying(2048) NOT NULL DEFAULT ''`); + await queryRunner.query(`ALTER TABLE "chat_room" ADD "isArchived" boolean NOT NULL DEFAULT false`); + await queryRunner.query(`ALTER TABLE "chat_room_membership" ADD "isMuted" boolean NOT NULL DEFAULT false`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "chat_room_membership" DROP COLUMN "isMuted"`); + await queryRunner.query(`ALTER TABLE "chat_room" DROP COLUMN "isArchived"`); + await queryRunner.query(`ALTER TABLE "chat_room" DROP COLUMN "description"`); + } +} diff --git a/packages/backend/src/core/ChatService.ts b/packages/backend/src/core/ChatService.ts new file mode 100644 index 0000000000..eece5d0da3 --- /dev/null +++ b/packages/backend/src/core/ChatService.ts @@ -0,0 +1,776 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import * as Redis from 'ioredis'; +import { Brackets } from 'typeorm'; +import { DI } from '@/di-symbols.js'; +import type { Config } from '@/config.js'; +import { QueueService } from '@/core/QueueService.js'; +import { IdService } from '@/core/IdService.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { ChatEntityService } from '@/core/entities/ChatEntityService.js'; +import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; +import { PushNotificationService } from '@/core/PushNotificationService.js'; +import { bindThis } from '@/decorators.js'; +import type { ChatApprovalsRepository, ChatMessagesRepository, ChatRoomInvitationsRepository, ChatRoomMembershipsRepository, ChatRoomsRepository, MiChatMessage, MiChatRoom, MiChatRoomMembership, MiDriveFile, MiUser, MutingsRepository, UsersRepository } from '@/models/_.js'; +import { UserBlockingService } from '@/core/UserBlockingService.js'; +import { QueryService } from '@/core/QueryService.js'; +import { RoleService } from '@/core/RoleService.js'; +import { UserFollowingService } from '@/core/UserFollowingService.js'; +import { MiChatRoomInvitation } from '@/models/ChatRoomInvitation.js'; +import { Packed } from '@/misc/json-schema.js'; +import { sqlLikeEscape } from '@/misc/sql-like-escape.js'; +import { CustomEmojiService } from '@/core/CustomEmojiService.js'; +import { emojiRegex } from '@/misc/emoji-regex.js'; + +const MAX_ROOM_MEMBERS = 30; +const MAX_REACTIONS_PER_MESSAGE = 100; +const isCustomEmojiRegexp = /^:([\w+-]+)(?:@\.)?:$/; + +@Injectable() +export class ChatService { + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.redis) + private redisClient: Redis.Redis, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.chatMessagesRepository) + private chatMessagesRepository: ChatMessagesRepository, + + @Inject(DI.chatApprovalsRepository) + private chatApprovalsRepository: ChatApprovalsRepository, + + @Inject(DI.chatRoomsRepository) + private chatRoomsRepository: ChatRoomsRepository, + + @Inject(DI.chatRoomInvitationsRepository) + private chatRoomInvitationsRepository: ChatRoomInvitationsRepository, + + @Inject(DI.chatRoomMembershipsRepository) + private chatRoomMembershipsRepository: ChatRoomMembershipsRepository, + + @Inject(DI.mutingsRepository) + private mutingsRepository: MutingsRepository, + + private userEntityService: UserEntityService, + private chatEntityService: ChatEntityService, + private idService: IdService, + private globalEventService: GlobalEventService, + private apRendererService: ApRendererService, + private queueService: QueueService, + private pushNotificationService: PushNotificationService, + private userBlockingService: UserBlockingService, + private queryService: QueryService, + private roleService: RoleService, + private userFollowingService: UserFollowingService, + private customEmojiService: CustomEmojiService, + ) { + } + + @bindThis + public async createMessageToUser(fromUser: { id: MiUser['id']; host: MiUser['host']; }, toUser: MiUser, params: { + text?: string | null; + file?: MiDriveFile | null; + uri?: string | null; + }): Promise> { + if (fromUser.id === toUser.id) { + throw new Error('yourself'); + } + + const approvals = await this.chatApprovalsRepository.createQueryBuilder('approval') + .where(new Brackets(qb => { // 自分が相手を許可しているか + qb.where('approval.userId = :fromUserId', { fromUserId: fromUser.id }) + .andWhere('approval.otherId = :toUserId', { toUserId: toUser.id }); + })) + .orWhere(new Brackets(qb => { // 相手が自分を許可しているか + qb.where('approval.userId = :toUserId', { toUserId: toUser.id }) + .andWhere('approval.otherId = :fromUserId', { fromUserId: fromUser.id }); + })) + .take(2) + .getMany(); + + const otherApprovedMe = approvals.some(approval => approval.userId === toUser.id); + const iApprovedOther = approvals.some(approval => approval.userId === fromUser.id); + + if (!otherApprovedMe) { + if (toUser.chatScope === 'none') { + throw new Error('recipient is cannot chat (none)'); + } else if (toUser.chatScope === 'followers') { + const isFollower = await this.userFollowingService.isFollowing(fromUser.id, toUser.id); + if (!isFollower) { + throw new Error('recipient is cannot chat (followers)'); + } + } else if (toUser.chatScope === 'following') { + const isFollowing = await this.userFollowingService.isFollowing(toUser.id, fromUser.id); + if (!isFollowing) { + throw new Error('recipient is cannot chat (following)'); + } + } else if (toUser.chatScope === 'mutual') { + const isMutual = await this.userFollowingService.isMutual(fromUser.id, toUser.id); + if (!isMutual) { + throw new Error('recipient is cannot chat (mutual)'); + } + } + } + + if (!(await this.roleService.getUserPolicies(toUser.id)).canChat) { + throw new Error('recipient is cannot chat (policy)'); + } + + const blocked = await this.userBlockingService.checkBlocked(toUser.id, fromUser.id); + if (blocked) { + throw new Error('blocked'); + } + + const message = { + id: this.idService.gen(), + fromUserId: fromUser.id, + toUserId: toUser.id, + text: params.text ? params.text.trim() : null, + fileId: params.file ? params.file.id : null, + reads: [], + uri: params.uri ?? null, + } satisfies Partial; + + const inserted = await this.chatMessagesRepository.insertOne(message); + + // 相手を許可しておく + if (!iApprovedOther) { + this.chatApprovalsRepository.insertOne({ + id: this.idService.gen(), + userId: fromUser.id, + otherId: toUser.id, + }); + } + + const packedMessage = await this.chatEntityService.packMessageLiteFor1on1(inserted); + + if (this.userEntityService.isLocalUser(toUser)) { + const redisPipeline = this.redisClient.pipeline(); + redisPipeline.set(`newUserChatMessageExists:${toUser.id}:${fromUser.id}`, message.id); + redisPipeline.sadd(`newChatMessagesExists:${toUser.id}`, `user:${fromUser.id}`); + redisPipeline.exec(); + } + + if (this.userEntityService.isLocalUser(fromUser)) { + // 自分のストリーム + this.globalEventService.publishChatUserStream(fromUser.id, toUser.id, 'message', packedMessage); + } + + if (this.userEntityService.isLocalUser(toUser)) { + // 相手のストリーム + this.globalEventService.publishChatUserStream(toUser.id, fromUser.id, 'message', packedMessage); + } + + // 3秒経っても既読にならなかったらイベント発行 + if (this.userEntityService.isLocalUser(toUser)) { + setTimeout(async () => { + const marker = await this.redisClient.get(`newUserChatMessageExists:${toUser.id}:${fromUser.id}`); + + if (marker == null) return; // 既読 + + const packedMessageForTo = await this.chatEntityService.packMessageDetailed(inserted, toUser); + this.globalEventService.publishMainStream(toUser.id, 'newChatMessage', packedMessageForTo); + //this.pushNotificationService.pushNotification(toUser.id, 'newChatMessage', packedMessageForTo); + }, 3000); + } + + return packedMessage; + } + + @bindThis + public async createMessageToRoom(fromUser: { id: MiUser['id']; host: MiUser['host']; }, toRoom: MiChatRoom, params: { + text?: string | null; + file?: MiDriveFile | null; + uri?: string | null; + }): Promise> { + const memberships = await this.chatRoomMembershipsRepository.findBy({ roomId: toRoom.id }); + + if (toRoom.ownerId !== fromUser.id && !memberships.some(member => member.userId === fromUser.id)) { + throw new Error('you are not a member of the room'); + } + + const message = { + id: this.idService.gen(), + fromUserId: fromUser.id, + toRoomId: toRoom.id, + text: params.text ? params.text.trim() : null, + fileId: params.file ? params.file.id : null, + reads: [], + uri: params.uri ?? null, + } satisfies Partial; + + const inserted = await this.chatMessagesRepository.insertOne(message); + + const packedMessage = await this.chatEntityService.packMessageLiteForRoom(inserted); + + this.globalEventService.publishChatRoomStream(toRoom.id, 'message', packedMessage); + + const redisPipeline = this.redisClient.pipeline(); + for (const membership of memberships) { + if (membership.isMuted) continue; + + redisPipeline.set(`newRoomChatMessageExists:${membership.userId}:${toRoom.id}`, message.id); + redisPipeline.sadd(`newChatMessagesExists:${membership.userId}`, `room:${toRoom.id}`); + } + redisPipeline.exec(); + + // 3秒経っても既読にならなかったらイベント発行 + setTimeout(async () => { + const redisPipeline = this.redisClient.pipeline(); + for (const membership of memberships) { + redisPipeline.get(`newRoomChatMessageExists:${membership.userId}:${toRoom.id}`); + } + const markers = await redisPipeline.exec(); + if (markers == null) throw new Error('redis error'); + + if (markers.every(marker => marker[1] == null)) return; + + const packedMessageForTo = await this.chatEntityService.packMessageDetailed(inserted); + + for (let i = 0; i < memberships.length; i++) { + const marker = markers[i][1]; + if (marker == null) continue; + + this.globalEventService.publishMainStream(memberships[i].userId, 'newChatMessage', packedMessageForTo); + //this.pushNotificationService.pushNotification(memberships[i].userId, 'newChatMessage', packedMessageForTo); + } + }, 3000); + + return packedMessage; + } + + @bindThis + public async readUserChatMessage( + readerId: MiUser['id'], + senderId: MiUser['id'], + ): Promise { + const redisPipeline = this.redisClient.pipeline(); + redisPipeline.del(`newUserChatMessageExists:${readerId}:${senderId}`); + redisPipeline.srem(`newChatMessagesExists:${readerId}`, `user:${senderId}`); + await redisPipeline.exec(); + } + + @bindThis + public async readRoomChatMessage( + readerId: MiUser['id'], + roomId: MiChatRoom['id'], + ): Promise { + const redisPipeline = this.redisClient.pipeline(); + redisPipeline.del(`newRoomChatMessageExists:${readerId}:${roomId}`); + redisPipeline.srem(`newChatMessagesExists:${readerId}`, `room:${roomId}`); + await redisPipeline.exec(); + } + + @bindThis + public findMessageById(messageId: MiChatMessage['id']) { + return this.chatMessagesRepository.findOneBy({ id: messageId }); + } + + @bindThis + public findMyMessageById(userId: MiUser['id'], messageId: MiChatMessage['id']) { + return this.chatMessagesRepository.findOneBy({ id: messageId, fromUserId: userId }); + } + + @bindThis + public async deleteMessage(message: MiChatMessage) { + await this.chatMessagesRepository.delete(message.id); + + if (message.toUserId) { + const [fromUser, toUser] = await Promise.all([ + this.usersRepository.findOneByOrFail({ id: message.fromUserId }), + this.usersRepository.findOneByOrFail({ id: message.toUserId }), + ]); + + if (this.userEntityService.isLocalUser(fromUser)) this.globalEventService.publishChatUserStream(message.fromUserId, message.toUserId, 'deleted', message.id); + if (this.userEntityService.isLocalUser(toUser)) this.globalEventService.publishChatUserStream(message.toUserId, message.fromUserId, 'deleted', message.id); + + if (this.userEntityService.isLocalUser(fromUser) && this.userEntityService.isRemoteUser(toUser)) { + //const activity = this.apRendererService.addContext(this.apRendererService.renderDelete(this.apRendererService.renderTombstone(`${this.config.url}/notes/${message.id}`), fromUser)); + //this.queueService.deliver(fromUser, activity, toUser.inbox); + } + } else if (message.toRoomId) { + this.globalEventService.publishChatRoomStream(message.toRoomId, 'deleted', message.id); + } + } + + @bindThis + public async userTimeline(meId: MiUser['id'], otherId: MiUser['id'], limit: number, sinceId?: MiChatMessage['id'] | null, untilId?: MiChatMessage['id'] | null) { + const query = this.queryService.makePaginationQuery(this.chatMessagesRepository.createQueryBuilder('message'), sinceId, untilId) + .andWhere(new Brackets(qb => { + qb + .where(new Brackets(qb => { + qb + .where('message.fromUserId = :meId') + .andWhere('message.toUserId = :otherId'); + })) + .orWhere(new Brackets(qb => { + qb + .where('message.fromUserId = :otherId') + .andWhere('message.toUserId = :meId'); + })); + })) + .setParameter('meId', meId) + .setParameter('otherId', otherId); + + const messages = await query.take(limit).getMany(); + + return messages; + } + + @bindThis + public async roomTimeline(roomId: MiChatRoom['id'], limit: number, sinceId?: MiChatMessage['id'] | null, untilId?: MiChatMessage['id'] | null) { + const query = this.queryService.makePaginationQuery(this.chatMessagesRepository.createQueryBuilder('message'), sinceId, untilId) + .where('message.toRoomId = :roomId', { roomId }) + .leftJoinAndSelect('message.file', 'file') + .leftJoinAndSelect('message.fromUser', 'fromUser'); + + const messages = await query.take(limit).getMany(); + + return messages; + } + + @bindThis + public async userHistory(meId: MiUser['id'], limit: number): Promise { + const history: MiChatMessage[] = []; + + const mutingQuery = this.mutingsRepository.createQueryBuilder('muting') + .select('muting.muteeId') + .where('muting.muterId = :muterId', { muterId: meId }); + + for (let i = 0; i < limit; i++) { + const found = history.map(m => (m.fromUserId === meId) ? m.toUserId! : m.fromUserId!); + + const query = this.chatMessagesRepository.createQueryBuilder('message') + .orderBy('message.id', 'DESC') + .where(new Brackets(qb => { + qb + .where('message.fromUserId = :meId', { meId: meId }) + .orWhere('message.toUserId = :meId', { meId: meId }); + })) + .andWhere('message.toRoomId IS NULL') + .andWhere(`message.fromUserId NOT IN (${ mutingQuery.getQuery() })`) + .andWhere(`message.toUserId NOT IN (${ mutingQuery.getQuery() })`); + + if (found.length > 0) { + query.andWhere('message.fromUserId NOT IN (:...found)', { found: found }); + query.andWhere('message.toUserId NOT IN (:...found)', { found: found }); + } + + query.setParameters(mutingQuery.getParameters()); + + const message = await query.getOne(); + + if (message) { + history.push(message); + } else { + break; + } + } + + return history; + } + + @bindThis + public async roomHistory(meId: MiUser['id'], limit: number): Promise { + // TODO: 一回のクエリにまとめられるかも + const [memberRoomIds, ownedRoomIds] = await Promise.all([ + this.chatRoomMembershipsRepository.findBy({ + userId: meId, + }).then(xs => xs.map(x => x.roomId)), + this.chatRoomsRepository.findBy({ + ownerId: meId, + }).then(xs => xs.map(x => x.id)), + ]); + + const roomIds = memberRoomIds.concat(ownedRoomIds); + + if (memberRoomIds.length === 0 && ownedRoomIds.length === 0) { + return []; + } + + const history: MiChatMessage[] = []; + + for (let i = 0; i < limit; i++) { + const found = history.map(m => m.toRoomId!); + + const query = this.chatMessagesRepository.createQueryBuilder('message') + .orderBy('message.id', 'DESC') + .where('message.toRoomId IN (:...roomIds)', { roomIds }); + + if (found.length > 0) { + query.andWhere('message.toRoomId NOT IN (:...found)', { found: found }); + } + + const message = await query.getOne(); + + if (message) { + history.push(message); + } else { + break; + } + } + + return history; + } + + @bindThis + public async getUserReadStateMap(userId: MiUser['id'], otherIds: MiUser['id'][]) { + const readStateMap: Record = {}; + + const redisPipeline = this.redisClient.pipeline(); + + for (const otherId of otherIds) { + redisPipeline.get(`newUserChatMessageExists:${userId}:${otherId}`); + } + + const markers = await redisPipeline.exec(); + if (markers == null) throw new Error('redis error'); + + for (let i = 0; i < otherIds.length; i++) { + const marker = markers[i][1]; + readStateMap[otherIds[i]] = marker == null; + } + + return readStateMap; + } + + @bindThis + public async getRoomReadStateMap(userId: MiUser['id'], roomIds: MiChatRoom['id'][]) { + const readStateMap: Record = {}; + + const redisPipeline = this.redisClient.pipeline(); + + for (const roomId of roomIds) { + redisPipeline.get(`newRoomChatMessageExists:${userId}:${roomId}`); + } + + const markers = await redisPipeline.exec(); + if (markers == null) throw new Error('redis error'); + + for (let i = 0; i < roomIds.length; i++) { + const marker = markers[i][1]; + readStateMap[roomIds[i]] = marker == null; + } + + return readStateMap; + } + + @bindThis + public async hasUnreadMessages(userId: MiUser['id']) { + const card = await this.redisClient.scard(`newChatMessagesExists:${userId}`); + return card > 0; + } + + @bindThis + public async createRoom(owner: MiUser, params: Partial<{ + name: string; + description: string; + }>) { + const room = { + id: this.idService.gen(), + name: params.name, + description: params.description, + ownerId: owner.id, + } satisfies Partial; + + const created = await this.chatRoomsRepository.insertOne(room); + + return created; + } + + @bindThis + public async deleteRoom(room: MiChatRoom) { + await this.chatRoomsRepository.delete(room.id); + } + + @bindThis + public async findMyRoomById(ownerId: MiUser['id'], roomId: MiChatRoom['id']) { + return this.chatRoomsRepository.findOneBy({ id: roomId, ownerId: ownerId }); + } + + @bindThis + public async findRoomById(roomId: MiChatRoom['id']) { + return this.chatRoomsRepository.findOne({ where: { id: roomId }, relations: ['owner'] }); + } + + @bindThis + public async isRoomMember(room: MiChatRoom, userId: MiUser['id']) { + if (room.ownerId === userId) return true; + const membership = await this.chatRoomMembershipsRepository.findOneBy({ roomId: room.id, userId }); + return membership != null; + } + + @bindThis + public async createRoomInvitation(inviterId: MiUser['id'], roomId: MiChatRoom['id'], inviteeId: MiUser['id']) { + if (inviterId === inviteeId) { + throw new Error('yourself'); + } + + const room = await this.chatRoomsRepository.findOneByOrFail({ id: roomId, ownerId: inviterId }); + + const existingInvitation = await this.chatRoomInvitationsRepository.findOneBy({ roomId, userId: inviteeId }); + if (existingInvitation) { + throw new Error('already invited'); + } + + const membershipsCount = await this.chatRoomMembershipsRepository.countBy({ roomId }); + if (membershipsCount >= MAX_ROOM_MEMBERS) { + throw new Error('room is full'); + } + + // TODO: cehck block + + const invitation = { + id: this.idService.gen(), + roomId: room.id, + userId: inviteeId, + } satisfies Partial; + + const created = await this.chatRoomInvitationsRepository.insertOne(invitation); + + return created; + } + + @bindThis + public async getOwnedRoomsWithPagination(ownerId: MiUser['id'], limit: number, sinceId?: MiChatRoom['id'] | null, untilId?: MiChatRoom['id'] | null) { + const query = this.queryService.makePaginationQuery(this.chatRoomsRepository.createQueryBuilder('room'), sinceId, untilId) + .where('room.ownerId = :ownerId', { ownerId }); + + const rooms = await query.take(limit).getMany(); + + return rooms; + } + + @bindThis + public async getReceivedRoomInvitationsWithPagination(userId: MiUser['id'], limit: number, sinceId?: MiChatRoomInvitation['id'] | null, untilId?: MiChatRoomInvitation['id'] | null) { + const query = this.queryService.makePaginationQuery(this.chatRoomInvitationsRepository.createQueryBuilder('invitation'), sinceId, untilId) + .where('invitation.userId = :userId', { userId }) + .andWhere('invitation.ignored = FALSE'); + + const invitations = await query.take(limit).getMany(); + + return invitations; + } + + @bindThis + public async joinToRoom(userId: MiUser['id'], roomId: MiChatRoom['id']) { + const invitation = await this.chatRoomInvitationsRepository.findOneByOrFail({ roomId, userId }); + + const membershipsCount = await this.chatRoomMembershipsRepository.countBy({ roomId }); + if (membershipsCount >= MAX_ROOM_MEMBERS) { + throw new Error('room is full'); + } + + const membership = { + id: this.idService.gen(), + roomId: roomId, + userId: userId, + } satisfies Partial; + + // TODO: transaction + await this.chatRoomMembershipsRepository.insertOne(membership); + await this.chatRoomInvitationsRepository.delete(invitation.id); + } + + @bindThis + public async ignoreRoomInvitation(userId: MiUser['id'], roomId: MiChatRoom['id']) { + const invitation = await this.chatRoomInvitationsRepository.findOneByOrFail({ roomId, userId }); + await this.chatRoomInvitationsRepository.update(invitation.id, { ignored: true }); + } + + @bindThis + public async leaveRoom(userId: MiUser['id'], roomId: MiChatRoom['id']) { + const membership = await this.chatRoomMembershipsRepository.findOneByOrFail({ roomId, userId }); + await this.chatRoomMembershipsRepository.delete(membership.id); + } + + @bindThis + public async muteRoom(userId: MiUser['id'], roomId: MiChatRoom['id'], mute: boolean) { + const membership = await this.chatRoomMembershipsRepository.findOneByOrFail({ roomId, userId }); + await this.chatRoomMembershipsRepository.update(membership.id, { isMuted: mute }); + } + + @bindThis + public async updateRoom(room: MiChatRoom, params: { + name?: string; + description?: string; + }): Promise { + return this.chatRoomsRepository.createQueryBuilder().update() + .set(params) + .where('id = :id', { id: room.id }) + .returning('*') + .execute() + .then((response) => { + return response.raw[0]; + }); + } + + @bindThis + public async getRoomMembershipsWithPagination(roomId: MiChatRoom['id'], limit: number, sinceId?: MiChatRoomMembership['id'] | null, untilId?: MiChatRoomMembership['id'] | null) { + const query = this.queryService.makePaginationQuery(this.chatRoomMembershipsRepository.createQueryBuilder('membership'), sinceId, untilId) + .where('membership.roomId = :roomId', { roomId }); + + const memberships = await query.take(limit).getMany(); + + return memberships; + } + + @bindThis + public async searchMessages(meId: MiUser['id'], query: string, limit: number, params: { + userId?: MiUser['id'] | null; + roomId?: MiChatRoom['id'] | null; + }) { + const q = this.chatMessagesRepository.createQueryBuilder('message'); + + if (params.userId) { + q.andWhere(new Brackets(qb => { + qb + .where(new Brackets(qb => { + qb + .where('message.fromUserId = :meId') + .andWhere('message.toUserId = :otherId'); + })) + .orWhere(new Brackets(qb => { + qb + .where('message.fromUserId = :otherId') + .andWhere('message.toUserId = :meId'); + })); + })) + .setParameter('meId', meId) + .setParameter('otherId', params.userId); + } else if (params.roomId) { + q.where('message.toRoomId = :roomId', { roomId: params.roomId }); + } else { + const membershipsQuery = this.chatRoomMembershipsRepository.createQueryBuilder('membership') + .select('membership.roomId') + .where('membership.userId = :meId', { meId: meId }); + + const ownedRoomsQuery = this.chatRoomsRepository.createQueryBuilder('room') + .select('room.id') + .where('room.ownerId = :meId', { meId }); + + q.andWhere(new Brackets(qb => { + qb + .where('message.fromUserId = :meId') + .orWhere('message.toUserId = :meId') + .orWhere(`message.toRoomId IN (${membershipsQuery.getQuery()})`) + .orWhere(`message.toRoomId IN (${ownedRoomsQuery.getQuery()})`); + })); + + q.setParameters(membershipsQuery.getParameters()); + q.setParameters(ownedRoomsQuery.getParameters()); + } + + q.andWhere('LOWER(message.text) LIKE :q', { q: `%${ sqlLikeEscape(query.toLowerCase()) }%` }); + + q.leftJoinAndSelect('message.file', 'file'); + q.leftJoinAndSelect('message.fromUser', 'fromUser'); + q.leftJoinAndSelect('message.toUser', 'toUser'); + q.leftJoinAndSelect('message.toRoom', 'toRoom'); + q.leftJoinAndSelect('toRoom.owner', 'toRoomOwner'); + + const messages = await q.orderBy('message.id', 'DESC').take(limit).getMany(); + + return messages; + } + + @bindThis + public async react(messageId: MiChatMessage['id'], userId: MiUser['id'], reaction_: string) { + let reaction; + + // TODO: ReactionServiceのやつと共通化 + function normalize(x: string) { + const match = emojiRegex.exec(x); + if (match) { + // 合字を含む1つの絵文字 + const unicode = match[0]; + + // 異体字セレクタ除去 + return unicode.match('\u200d') ? unicode : unicode.replace(/\ufe0f/g, ''); + } else { + throw new Error('invalid emoji'); + } + } + + const custom = reaction_.match(isCustomEmojiRegexp); + + if (custom == null) { + reaction = normalize(reaction_); + } else { + const name = custom[1]; + const emoji = (await this.customEmojiService.localEmojisCache.fetch()).get(name); + + if (emoji == null) { + throw new Error('no such emoji'); + } else { + reaction = `:${name}:`; + } + } + + const message = await this.chatMessagesRepository.findOneByOrFail({ id: messageId }); + + if (message.fromUserId === userId) { + throw new Error('cannot react to own message'); + } + + if (message.toRoomId === null && message.toUserId !== userId) { + throw new Error('cannot react to others message'); + } + + if (message.reactions.length >= MAX_REACTIONS_PER_MESSAGE) { + throw new Error('too many reactions'); + } + + const room = message.toRoomId ? await this.chatRoomsRepository.findOneByOrFail({ id: message.toRoomId }) : null; + + if (room) { + if (!await this.isRoomMember(room, userId)) { + throw new Error('cannot react to others message'); + } + } + + await this.chatMessagesRepository.createQueryBuilder().update() + .set({ + reactions: () => `array_append("reactions", '${userId}/${reaction}')`, + }) + .where('id = :id', { id: message.id }) + .execute(); + + if (room) { + this.globalEventService.publishChatRoomStream(room.id, 'react', { + messageId: message.id, + user: await this.userEntityService.pack(userId), + reaction, + }); + } else { + this.globalEventService.publishChatUserStream(message.fromUserId, message.toUserId!, 'react', { + messageId: message.id, + reaction, + }); + this.globalEventService.publishChatUserStream(message.toUserId!, message.fromUserId, 'react', { + messageId: message.id, + reaction, + }); + } + } + + @bindThis + public async getMyMemberships(userId: MiUser['id'], limit: number, sinceId?: MiChatRoomMembership['id'] | null, untilId?: MiChatRoomMembership['id'] | null) { + const query = this.queryService.makePaginationQuery(this.chatRoomMembershipsRepository.createQueryBuilder('membership'), sinceId, untilId) + .where('membership.userId = :userId', { userId }); + + const memberships = await query.take(limit).getMany(); + + return memberships; + } +} diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts index dc85a23e5b..d8617e343c 100644 --- a/packages/backend/src/core/CoreModule.ts +++ b/packages/backend/src/core/CoreModule.ts @@ -44,7 +44,6 @@ import { ModerationLogService } from './ModerationLogService.js'; import { NoteCreateService } from './NoteCreateService.js'; import { NoteDeleteService } from './NoteDeleteService.js'; import { NotePiningService } from './NotePiningService.js'; -import { NoteReadService } from './NoteReadService.js'; import { NotificationService } from './NotificationService.js'; import { PollService } from './PollService.js'; import { PushNotificationService } from './PushNotificationService.js'; @@ -75,6 +74,7 @@ import { ClipService } from './ClipService.js'; import { FeaturedService } from './FeaturedService.js'; import { FanoutTimelineService } from './FanoutTimelineService.js'; import { ChannelFollowingService } from './ChannelFollowingService.js'; +import { ChatService } from './ChatService.js'; import { RegistryApiService } from './RegistryApiService.js'; import { ReversiService } from './ReversiService.js'; @@ -100,6 +100,7 @@ import { AppEntityService } from './entities/AppEntityService.js'; import { AuthSessionEntityService } from './entities/AuthSessionEntityService.js'; import { BlockingEntityService } from './entities/BlockingEntityService.js'; import { ChannelEntityService } from './entities/ChannelEntityService.js'; +import { ChatEntityService } from './entities/ChatEntityService.js'; import { ClipEntityService } from './entities/ClipEntityService.js'; import { DriveFileEntityService } from './entities/DriveFileEntityService.js'; import { DriveFolderEntityService } from './entities/DriveFolderEntityService.js'; @@ -184,7 +185,6 @@ const $ModerationLogService: Provider = { provide: 'ModerationLogService', useEx const $NoteCreateService: Provider = { provide: 'NoteCreateService', useExisting: NoteCreateService }; const $NoteDeleteService: Provider = { provide: 'NoteDeleteService', useExisting: NoteDeleteService }; const $NotePiningService: Provider = { provide: 'NotePiningService', useExisting: NotePiningService }; -const $NoteReadService: Provider = { provide: 'NoteReadService', useExisting: NoteReadService }; const $NotificationService: Provider = { provide: 'NotificationService', useExisting: NotificationService }; const $PollService: Provider = { provide: 'PollService', useExisting: PollService }; const $SystemAccountService: Provider = { provide: 'SystemAccountService', useExisting: SystemAccountService }; @@ -221,6 +221,7 @@ const $FeaturedService: Provider = { provide: 'FeaturedService', useExisting: Fe const $FanoutTimelineService: Provider = { provide: 'FanoutTimelineService', useExisting: FanoutTimelineService }; const $FanoutTimelineEndpointService: Provider = { provide: 'FanoutTimelineEndpointService', useExisting: FanoutTimelineEndpointService }; const $ChannelFollowingService: Provider = { provide: 'ChannelFollowingService', useExisting: ChannelFollowingService }; +const $ChatService: Provider = { provide: 'ChatService', useExisting: ChatService }; const $RegistryApiService: Provider = { provide: 'RegistryApiService', useExisting: RegistryApiService }; const $ReversiService: Provider = { provide: 'ReversiService', useExisting: ReversiService }; @@ -247,6 +248,7 @@ const $AppEntityService: Provider = { provide: 'AppEntityService', useExisting: const $AuthSessionEntityService: Provider = { provide: 'AuthSessionEntityService', useExisting: AuthSessionEntityService }; const $BlockingEntityService: Provider = { provide: 'BlockingEntityService', useExisting: BlockingEntityService }; const $ChannelEntityService: Provider = { provide: 'ChannelEntityService', useExisting: ChannelEntityService }; +const $ChatEntityService: Provider = { provide: 'ChatEntityService', useExisting: ChatEntityService }; const $ClipEntityService: Provider = { provide: 'ClipEntityService', useExisting: ClipEntityService }; const $DriveFileEntityService: Provider = { provide: 'DriveFileEntityService', useExisting: DriveFileEntityService }; const $DriveFolderEntityService: Provider = { provide: 'DriveFolderEntityService', useExisting: DriveFolderEntityService }; @@ -333,7 +335,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting NoteCreateService, NoteDeleteService, NotePiningService, - NoteReadService, NotificationService, PollService, SystemAccountService, @@ -370,6 +371,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting FanoutTimelineService, FanoutTimelineEndpointService, ChannelFollowingService, + ChatService, RegistryApiService, ReversiService, @@ -396,6 +398,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting AuthSessionEntityService, BlockingEntityService, ChannelEntityService, + ChatEntityService, ClipEntityService, DriveFileEntityService, DriveFolderEntityService, @@ -478,7 +481,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $NoteCreateService, $NoteDeleteService, $NotePiningService, - $NoteReadService, $NotificationService, $PollService, $SystemAccountService, @@ -515,6 +517,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $FanoutTimelineService, $FanoutTimelineEndpointService, $ChannelFollowingService, + $ChatService, $RegistryApiService, $ReversiService, @@ -541,6 +544,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $AuthSessionEntityService, $BlockingEntityService, $ChannelEntityService, + $ChatEntityService, $ClipEntityService, $DriveFileEntityService, $DriveFolderEntityService, @@ -624,7 +628,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting NoteCreateService, NoteDeleteService, NotePiningService, - NoteReadService, NotificationService, PollService, SystemAccountService, @@ -661,6 +664,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting FanoutTimelineService, FanoutTimelineEndpointService, ChannelFollowingService, + ChatService, RegistryApiService, ReversiService, @@ -686,6 +690,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting AuthSessionEntityService, BlockingEntityService, ChannelEntityService, + ChatEntityService, ClipEntityService, DriveFileEntityService, DriveFolderEntityService, @@ -768,7 +773,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $NoteCreateService, $NoteDeleteService, $NotePiningService, - $NoteReadService, $NotificationService, $PollService, $SystemAccountService, @@ -804,6 +808,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $FanoutTimelineService, $FanoutTimelineEndpointService, $ChannelFollowingService, + $ChatService, $RegistryApiService, $ReversiService, @@ -829,6 +834,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $AuthSessionEntityService, $BlockingEntityService, $ChannelEntityService, + $ChatEntityService, $ClipEntityService, $DriveFileEntityService, $DriveFolderEntityService, diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts index 224fdabc4c..4da3b8bc78 100644 --- a/packages/backend/src/core/GlobalEventService.ts +++ b/packages/backend/src/core/GlobalEventService.ts @@ -20,7 +20,7 @@ import type { MiPage } from '@/models/Page.js'; import type { MiWebhook } from '@/models/Webhook.js'; import type { MiSystemWebhook } from '@/models/SystemWebhook.js'; import type { MiMeta } from '@/models/Meta.js'; -import { MiAvatarDecoration, MiReversiGame, MiRole, MiRoleAssignment } from '@/models/_.js'; +import { MiAvatarDecoration, MiChatMessage, MiChatRoom, MiReversiGame, MiRole, MiRoleAssignment } from '@/models/_.js'; import type { Packed } from '@/misc/json-schema.js'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; @@ -72,12 +72,8 @@ export interface MainEventTypes { readAllNotifications: undefined; notificationFlushed: undefined; unreadNotification: Packed<'Notification'>; - unreadMention: MiNote['id']; - readAllUnreadMentions: undefined; - unreadSpecifiedNote: MiNote['id']; - readAllUnreadSpecifiedNotes: undefined; - readAllAntennas: undefined; unreadAntenna: MiAntenna; + newChatMessage: Packed<'ChatMessage'>; readAllAnnouncements: undefined; myTokenRegenerated: undefined; signin: { @@ -163,6 +159,16 @@ export interface AdminEventTypes { }; } +export interface ChatEventTypes { + message: Packed<'ChatMessageLite'>; + deleted: Packed<'ChatMessageLite'>['id']; + react: { + reaction: string; + user?: Packed<'UserLite'>; + messageId: MiChatMessage['id']; + }; +} + export interface ReversiEventTypes { matched: { game: Packed<'ReversiGameDetailed'>; @@ -202,7 +208,7 @@ export interface ReversiGameEventTypes { type Events = { [K in keyof T]: { type: K; body: T[K]; } }; type EventUnionFromDictionary< T extends object, - U = Events + U = Events, > = U[keyof U]; type SerializedAll = { @@ -295,6 +301,14 @@ export type GlobalEvents = { name: 'notesStream'; payload: Serialized>; }; + chat: { + name: `chatUserStream:${MiUser['id']}-${MiUser['id']}`; + payload: EventTypesToEventPayload; + }; + chatRoom: { + name: `chatRoomStream:${MiChatRoom['id']}`; + payload: EventTypesToEventPayload; + }; reversi: { name: `reversiStream:${MiUser['id']}`; payload: EventTypesToEventPayload; @@ -393,6 +407,16 @@ export class GlobalEventService { this.publish(`adminStream:${userId}`, type, typeof value === 'undefined' ? null : value); } + @bindThis + public publishChatUserStream(fromUserId: MiUser['id'], toUserId: MiUser['id'], type: K, value?: ChatEventTypes[K]): void { + this.publish(`chatUserStream:${fromUserId}-${toUserId}`, type, typeof value === 'undefined' ? null : value); + } + + @bindThis + public publishChatRoomStream(toRoomId: MiChatRoom['id'], type: K, value?: ChatEventTypes[K]): void { + this.publish(`chatRoomStream:${toRoomId}`, type, typeof value === 'undefined' ? null : value); + } + @bindThis public publishReversiStream(userId: MiUser['id'], type: K, value?: ReversiEventTypes[K]): void { this.publish(`reversiStream:${userId}`, type, typeof value === 'undefined' ? null : value); diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 8a79908e82..8f416f398c 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -42,7 +42,6 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js'; -import { NoteReadService } from '@/core/NoteReadService.js'; import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js'; import { bindThis } from '@/decorators.js'; import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js'; @@ -199,7 +198,6 @@ export class NoteCreateService implements OnApplicationShutdown { private globalEventService: GlobalEventService, private queueService: QueueService, private fanoutTimelineService: FanoutTimelineService, - private noteReadService: NoteReadService, private notificationService: NotificationService, private relayService: RelayService, private federatedInstanceService: FederatedInstanceService, @@ -582,31 +580,6 @@ export class NoteCreateService implements OnApplicationShutdown { if (!silent) { if (this.userEntityService.isLocalUser(user)) this.activeUsersChart.write(user); - // 未読通知を作成 - if (data.visibility === 'specified') { - if (data.visibleUsers == null) throw new Error('invalid param'); - - for (const u of data.visibleUsers) { - // ローカルユーザーのみ - if (!this.userEntityService.isLocalUser(u)) continue; - - this.noteReadService.insertNoteUnread(u.id, note, { - isSpecified: true, - isMentioned: false, - }); - } - } else { - for (const u of mentionedUsers) { - // ローカルユーザーのみ - if (!this.userEntityService.isLocalUser(u)) continue; - - this.noteReadService.insertNoteUnread(u.id, note, { - isSpecified: false, - isMentioned: true, - }); - } - } - // Pack the note const noteObj = await this.noteEntityService.pack(note, null, { skipHide: true, withReactionAndUserPairCache: true }); diff --git a/packages/backend/src/core/NoteReadService.ts b/packages/backend/src/core/NoteReadService.ts deleted file mode 100644 index 181c9f7649..0000000000 --- a/packages/backend/src/core/NoteReadService.ts +++ /dev/null @@ -1,143 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { setTimeout } from 'node:timers/promises'; -import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; -import { In } from 'typeorm'; -import { DI } from '@/di-symbols.js'; -import type { MiUser } from '@/models/User.js'; -import type { Packed } from '@/misc/json-schema.js'; -import type { MiNote } from '@/models/Note.js'; -import { IdService } from '@/core/IdService.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; -import type { NoteUnreadsRepository, MutingsRepository, NoteThreadMutingsRepository } from '@/models/_.js'; -import { bindThis } from '@/decorators.js'; -import { trackPromise } from '@/misc/promise-tracker.js'; - -@Injectable() -export class NoteReadService implements OnApplicationShutdown { - #shutdownController = new AbortController(); - - constructor( - @Inject(DI.noteUnreadsRepository) - private noteUnreadsRepository: NoteUnreadsRepository, - - @Inject(DI.mutingsRepository) - private mutingsRepository: MutingsRepository, - - @Inject(DI.noteThreadMutingsRepository) - private noteThreadMutingsRepository: NoteThreadMutingsRepository, - - private idService: IdService, - private globalEventService: GlobalEventService, - ) { - } - - @bindThis - public async insertNoteUnread(userId: MiUser['id'], note: MiNote, params: { - // NOTE: isSpecifiedがtrueならisMentionedは必ずfalse - isSpecified: boolean; - isMentioned: boolean; - }): Promise { - //#region ミュートしているなら無視 - const mute = await this.mutingsRepository.findBy({ - muterId: userId, - }); - if (mute.map(m => m.muteeId).includes(note.userId)) return; - //#endregion - - // スレッドミュート - const isThreadMuted = await this.noteThreadMutingsRepository.exists({ - where: { - userId: userId, - threadId: note.threadId ?? note.id, - }, - }); - if (isThreadMuted) return; - - const unread = { - id: this.idService.gen(), - noteId: note.id, - userId: userId, - isSpecified: params.isSpecified, - isMentioned: params.isMentioned, - noteUserId: note.userId, - }; - - await this.noteUnreadsRepository.insert(unread); - - // 2秒経っても既読にならなかったら「未読の投稿がありますよ」イベントを発行する - setTimeout(2000, 'unread note', { signal: this.#shutdownController.signal }).then(async () => { - const exist = await this.noteUnreadsRepository.exists({ where: { id: unread.id } }); - - if (!exist) return; - - if (params.isMentioned) { - this.globalEventService.publishMainStream(userId, 'unreadMention', note.id); - } - if (params.isSpecified) { - this.globalEventService.publishMainStream(userId, 'unreadSpecifiedNote', note.id); - } - }, () => { /* aborted, ignore it */ }); - } - - @bindThis - public async read( - userId: MiUser['id'], - notes: (MiNote | Packed<'Note'>)[], - ): Promise { - if (notes.length === 0) return; - - const noteIds = new Set(); - - for (const note of notes) { - if (note.mentions && note.mentions.includes(userId)) { - noteIds.add(note.id); - } else if (note.visibleUserIds && note.visibleUserIds.includes(userId)) { - noteIds.add(note.id); - } - } - - if (noteIds.size === 0) return; - - // Remove the record - await this.noteUnreadsRepository.delete({ - userId: userId, - noteId: In(Array.from(noteIds)), - }); - - // TODO: ↓まとめてクエリしたい - - trackPromise(this.noteUnreadsRepository.countBy({ - userId: userId, - isMentioned: true, - }).then(mentionsCount => { - if (mentionsCount === 0) { - // 全て既読になったイベントを発行 - this.globalEventService.publishMainStream(userId, 'readAllUnreadMentions'); - } - })); - - trackPromise(this.noteUnreadsRepository.countBy({ - userId: userId, - isSpecified: true, - }).then(specifiedCount => { - if (specifiedCount === 0) { - // 全て既読になったイベントを発行 - this.globalEventService.publishMainStream(userId, 'readAllUnreadSpecifiedNotes'); - } - })); - } - - @bindThis - public dispose(): void { - this.#shutdownController.abort(); - } - - @bindThis - public onApplicationShutdown(signal?: string | undefined): void { - this.dispose(); - } -} diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index 01f3e0c116..86f8a5caa1 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -63,6 +63,7 @@ export type RolePolicies = { canImportFollowing: boolean; canImportMuting: boolean; canImportUserLists: boolean; + canChat: boolean; }; export const DEFAULT_POLICIES: RolePolicies = { @@ -97,6 +98,7 @@ export const DEFAULT_POLICIES: RolePolicies = { canImportFollowing: true, canImportMuting: true, canImportUserLists: true, + canChat: true, }; @Injectable() @@ -400,6 +402,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { canImportFollowing: calc('canImportFollowing', vs => vs.some(v => v === true)), canImportMuting: calc('canImportMuting', vs => vs.some(v => v === true)), canImportUserLists: calc('canImportUserLists', vs => vs.some(v => v === true)), + canChat: calc('canChat', vs => vs.some(v => v === true)), }; } diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts index b98ca97ec9..e7a6be99fb 100644 --- a/packages/backend/src/core/UserFollowingService.ts +++ b/packages/backend/src/core/UserFollowingService.ts @@ -5,7 +5,7 @@ import { Inject, Injectable, OnModuleInit } from '@nestjs/common'; import { ModuleRef } from '@nestjs/core'; -import { IsNull } from 'typeorm'; +import { Brackets, IsNull } from 'typeorm'; import type { MiLocalUser, MiPartialLocalUser, MiPartialRemoteUser, MiRemoteUser, MiUser } from '@/models/User.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; import { QueueService } from '@/core/QueueService.js'; @@ -736,4 +736,30 @@ export class UserFollowingService implements OnModuleInit { .where('following.followerId = :followerId', { followerId: userId }) .getMany(); } + + @bindThis + public isFollowing(followerId: MiUser['id'], followeeId: MiUser['id']) { + return this.followingsRepository.exists({ + where: { + followerId, + followeeId, + }, + }); + } + + @bindThis + public async isMutual(aUserId: MiUser['id'], bUserId: MiUser['id']) { + const count = await this.followingsRepository.createQueryBuilder('following') + .where(new Brackets(qb => { + qb.where('following.followerId = :aUserId', { aUserId }) + .andWhere('following.followeeId = :bUserId', { bUserId }); + })) + .orWhere(new Brackets(qb => { + qb.where('following.followerId = :bUserId', { bUserId }) + .andWhere('following.followeeId = :aUserId', { aUserId }); + })) + .getCount(); + + return count === 2; + } } diff --git a/packages/backend/src/core/WebhookTestService.ts b/packages/backend/src/core/WebhookTestService.ts index e1a7801bb1..a6198f7686 100644 --- a/packages/backend/src/core/WebhookTestService.ts +++ b/packages/backend/src/core/WebhookTestService.ts @@ -53,6 +53,7 @@ function generateDummyUser(override?: Partial): MiUser { requireSigninToViewContents: false, makeNotesFollowersOnlyBefore: null, makeNotesHiddenBefore: null, + chatScope: 'mutual', emojis: [], score: 0, host: null, @@ -461,6 +462,7 @@ export class WebhookTestService { publicReactions: true, followersVisibility: 'public', followingVisibility: 'public', + chatScope: 'mutual', twoFactorEnabled: false, usePasswordLessLogin: false, securityKeys: false, diff --git a/packages/backend/src/core/entities/ChatEntityService.ts b/packages/backend/src/core/entities/ChatEntityService.ts new file mode 100644 index 0000000000..099a9e3ad2 --- /dev/null +++ b/packages/backend/src/core/entities/ChatEntityService.ts @@ -0,0 +1,376 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import type { MiUser, ChatMessagesRepository, MiChatMessage, ChatRoomsRepository, MiChatRoom, MiChatRoomInvitation, ChatRoomInvitationsRepository, MiChatRoomMembership, ChatRoomMembershipsRepository } from '@/models/_.js'; +import { awaitAll } from '@/misc/prelude/await-all.js'; +import type { Packed } from '@/misc/json-schema.js'; +import type { } from '@/models/Blocking.js'; +import { bindThis } from '@/decorators.js'; +import { IdService } from '@/core/IdService.js'; +import { UserEntityService } from './UserEntityService.js'; +import { DriveFileEntityService } from './DriveFileEntityService.js'; +import { In } from 'typeorm'; + +@Injectable() +export class ChatEntityService { + constructor( + @Inject(DI.chatMessagesRepository) + private chatMessagesRepository: ChatMessagesRepository, + + @Inject(DI.chatRoomsRepository) + private chatRoomsRepository: ChatRoomsRepository, + + @Inject(DI.chatRoomInvitationsRepository) + private chatRoomInvitationsRepository: ChatRoomInvitationsRepository, + + @Inject(DI.chatRoomMembershipsRepository) + private chatRoomMembershipsRepository: ChatRoomMembershipsRepository, + + private userEntityService: UserEntityService, + private driveFileEntityService: DriveFileEntityService, + private idService: IdService, + ) { + } + + @bindThis + public async packMessageDetailed( + src: MiChatMessage['id'] | MiChatMessage, + me?: { id: MiUser['id'] }, + options?: { + _hint_?: { + packedFiles?: Map | null>; + packedUsers?: Map>; + packedRooms?: Map | null>; + }; + }, + ): Promise> { + const packedUsers = options?._hint_?.packedUsers; + const packedFiles = options?._hint_?.packedFiles; + const packedRooms = options?._hint_?.packedRooms; + + const message = typeof src === 'object' ? src : await this.chatMessagesRepository.findOneByOrFail({ id: src }); + + const reactions: { user: Packed<'UserLite'>; reaction: string; }[] = []; + + for (const record of message.reactions) { + const [userId, reaction] = record.split('/'); + reactions.push({ + user: packedUsers?.get(userId) ?? await this.userEntityService.pack(userId), + reaction, + }); + } + + return { + id: message.id, + createdAt: this.idService.parse(message.id).date.toISOString(), + text: message.text, + fromUserId: message.fromUserId, + fromUser: packedUsers?.get(message.fromUserId) ?? await this.userEntityService.pack(message.fromUser ?? message.fromUserId, me), + toUserId: message.toUserId, + toUser: message.toUserId ? (packedUsers?.get(message.toUserId) ?? await this.userEntityService.pack(message.toUser ?? message.toUserId, me)) : undefined, + toRoomId: message.toRoomId, + toRoom: message.toRoomId ? (packedRooms?.get(message.toRoomId) ?? await this.packRoom(message.toRoom ?? message.toRoomId, me)) : undefined, + fileId: message.fileId, + file: message.fileId ? (packedFiles?.get(message.fileId) ?? await this.driveFileEntityService.pack(message.file ?? message.fileId)) : null, + reactions, + }; + } + + @bindThis + public async packMessagesDetailed( + messages: MiChatMessage[], + me: { id: MiUser['id'] }, + ) { + if (messages.length === 0) return []; + + const excludeMe = (x: MiUser | string) => { + if (typeof x === 'string') { + return x !== me.id; + } else { + return x.id !== me.id; + } + }; + + const users = [ + ...messages.map((m) => m.fromUser ?? m.fromUserId).filter(excludeMe), + ...messages.map((m) => m.toUser ?? m.toUserId).filter(x => x != null).filter(excludeMe), + ]; + + const reactedUserIds = messages.flatMap(x => x.reactions.map(r => r.split('/')[0])); + + for (const reactedUserId of reactedUserIds) { + if (!users.some(x => typeof x === 'string' ? x === reactedUserId : x.id === reactedUserId)) { + users.push(reactedUserId); + } + } + + const [packedUsers, packedFiles, packedRooms] = await Promise.all([ + this.userEntityService.packMany(users, me) + .then(users => new Map(users.map(u => [u.id, u]))), + this.driveFileEntityService.packMany(messages.map(m => m.file).filter(x => x != null)) + .then(files => new Map(files.map(f => [f.id, f]))), + this.packRooms(messages.map(m => m.toRoom ?? m.toRoomId).filter(x => x != null), me) + .then(rooms => new Map(rooms.map(r => [r.id, r]))), + ]); + + return Promise.all(messages.map(message => this.packMessageDetailed(message, me, { _hint_: { packedUsers, packedFiles, packedRooms } }))); + } + + @bindThis + public async packMessageLiteFor1on1( + src: MiChatMessage['id'] | MiChatMessage, + options?: { + _hint_?: { + packedFiles: Map | null>; + }; + }, + ): Promise> { + const packedFiles = options?._hint_?.packedFiles; + + const message = typeof src === 'object' ? src : await this.chatMessagesRepository.findOneByOrFail({ id: src }); + + const reactions: { reaction: string; }[] = []; + + for (const record of message.reactions) { + const [userId, reaction] = record.split('/'); + reactions.push({ + reaction, + }); + } + + return { + id: message.id, + createdAt: this.idService.parse(message.id).date.toISOString(), + text: message.text, + fromUserId: message.fromUserId, + toUserId: message.toUserId, + fileId: message.fileId, + file: message.fileId ? (packedFiles?.get(message.fileId) ?? await this.driveFileEntityService.pack(message.file ?? message.fileId)) : null, + reactions, + }; + } + + @bindThis + public async packMessagesLiteFor1on1( + messages: MiChatMessage[], + ) { + if (messages.length === 0) return []; + + const [packedFiles] = await Promise.all([ + this.driveFileEntityService.packMany(messages.map(m => m.file).filter(x => x != null)) + .then(files => new Map(files.map(f => [f.id, f]))), + ]); + + return Promise.all(messages.map(message => this.packMessageLiteFor1on1(message, { _hint_: { packedFiles } }))); + } + + @bindThis + public async packMessageLiteForRoom( + src: MiChatMessage['id'] | MiChatMessage, + options?: { + _hint_?: { + packedFiles: Map | null>; + packedUsers: Map>; + }; + }, + ): Promise> { + const packedFiles = options?._hint_?.packedFiles; + const packedUsers = options?._hint_?.packedUsers; + + const message = typeof src === 'object' ? src : await this.chatMessagesRepository.findOneByOrFail({ id: src }); + + const reactions: { user: Packed<'UserLite'>; reaction: string; }[] = []; + + for (const record of message.reactions) { + const [userId, reaction] = record.split('/'); + reactions.push({ + user: packedUsers?.get(userId) ?? await this.userEntityService.pack(userId), + reaction, + }); + } + + return { + id: message.id, + createdAt: this.idService.parse(message.id).date.toISOString(), + text: message.text, + fromUserId: message.fromUserId, + fromUser: packedUsers?.get(message.fromUserId) ?? await this.userEntityService.pack(message.fromUser ?? message.fromUserId), + toRoomId: message.toRoomId, + fileId: message.fileId, + file: message.fileId ? (packedFiles?.get(message.fileId) ?? await this.driveFileEntityService.pack(message.file ?? message.fileId)) : null, + reactions, + }; + } + + @bindThis + public async packMessagesLiteForRoom( + messages: MiChatMessage[], + ) { + if (messages.length === 0) return []; + + const users = messages.map(x => x.fromUser ?? x.fromUserId); + const reactedUserIds = messages.flatMap(x => x.reactions.map(r => r.split('/')[0])); + + for (const reactedUserId of reactedUserIds) { + if (!users.some(x => typeof x === 'string' ? x === reactedUserId : x.id === reactedUserId)) { + users.push(reactedUserId); + } + } + + const [packedUsers, packedFiles] = await Promise.all([ + this.userEntityService.packMany(users) + .then(users => new Map(users.map(u => [u.id, u]))), + this.driveFileEntityService.packMany(messages.map(m => m.file).filter(x => x != null)) + .then(files => new Map(files.map(f => [f.id, f]))), + ]); + + return Promise.all(messages.map(message => this.packMessageLiteForRoom(message, { _hint_: { packedFiles, packedUsers } }))); + } + + @bindThis + public async packRoom( + src: MiChatRoom['id'] | MiChatRoom, + me?: { id: MiUser['id'] }, + options?: { + _hint_?: { + packedOwners: Map>; + memberships?: Map; + }; + }, + ): Promise> { + const room = typeof src === 'object' ? src : await this.chatRoomsRepository.findOneByOrFail({ id: src }); + + const membership = me && me.id !== room.ownerId ? (options?._hint_?.memberships?.get(room.id) ?? await this.chatRoomMembershipsRepository.findOneBy({ roomId: room.id, userId: me.id })) : null; + + return { + id: room.id, + createdAt: this.idService.parse(room.id).date.toISOString(), + name: room.name, + description: room.description, + ownerId: room.ownerId, + owner: options?._hint_?.packedOwners.get(room.ownerId) ?? await this.userEntityService.pack(room.owner ?? room.ownerId, me), + isMuted: membership != null ? membership.isMuted : false, + }; + } + + @bindThis + public async packRooms( + rooms: (MiChatRoom | MiChatRoom['id'])[], + me: { id: MiUser['id'] }, + ) { + if (rooms.length === 0) return []; + + const _rooms = rooms.filter((room): room is MiChatRoom => typeof room !== 'string'); + if (_rooms.length !== rooms.length) { + _rooms.push( + ...await this.chatRoomsRepository.find({ + where: { + id: In(rooms.filter((room): room is string => typeof room === 'string')), + }, + relations: ['owner'], + }), + ); + } + + const owners = _rooms.map(x => x.owner ?? x.ownerId); + + const [packedOwners, memberships] = await Promise.all([ + this.userEntityService.packMany(owners, me) + .then(users => new Map(users.map(u => [u.id, u]))), + this.chatRoomMembershipsRepository.find({ + where: { + roomId: In(_rooms.map(x => x.id)), + userId: me.id, + }, + }).then(memberships => new Map(_rooms.map(r => [r.id, memberships.find(m => m.roomId === r.id)]))), + ]); + + return Promise.all(_rooms.map(room => this.packRoom(room, me, { _hint_: { packedOwners, memberships } }))); + } + + @bindThis + public async packRoomInvitation( + src: MiChatRoomInvitation['id'] | MiChatRoomInvitation, + me: { id: MiUser['id'] }, + options?: { + _hint_?: { + packedRooms: Map>; + packedUsers: Map>; + }; + }, + ): Promise> { + const invitation = typeof src === 'object' ? src : await this.chatRoomInvitationsRepository.findOneByOrFail({ id: src }); + + return { + id: invitation.id, + createdAt: this.idService.parse(invitation.id).date.toISOString(), + roomId: invitation.roomId, + room: options?._hint_?.packedRooms.get(invitation.roomId) ?? await this.packRoom(invitation.room ?? invitation.roomId, me), + userId: invitation.userId, + user: options?._hint_?.packedUsers.get(invitation.userId) ?? await this.userEntityService.pack(invitation.user ?? invitation.userId, me), + }; + } + + @bindThis + public async packRoomInvitations( + invitations: MiChatRoomInvitation[], + me: { id: MiUser['id'] }, + ) { + if (invitations.length === 0) return []; + + return Promise.all(invitations.map(invitation => this.packRoomInvitation(invitation, me))); + } + + @bindThis + public async packRoomMembership( + src: MiChatRoomMembership['id'] | MiChatRoomMembership, + me: { id: MiUser['id'] }, + options?: { + populateUser?: boolean; + populateRoom?: boolean; + _hint_?: { + packedRooms: Map>; + packedUsers: Map>; + }; + }, + ): Promise> { + const membership = typeof src === 'object' ? src : await this.chatRoomMembershipsRepository.findOneByOrFail({ id: src }); + + return { + id: membership.id, + createdAt: this.idService.parse(membership.id).date.toISOString(), + userId: membership.userId, + user: options?.populateUser ? (options._hint_?.packedUsers.get(membership.userId) ?? await this.userEntityService.pack(membership.user ?? membership.userId, me)) : undefined, + roomId: membership.roomId, + room: options?.populateRoom ? (options._hint_?.packedRooms.get(membership.roomId) ?? await this.packRoom(membership.room ?? membership.roomId, me)) : undefined, + }; + } + + @bindThis + public async packRoomMemberships( + memberships: MiChatRoomMembership[], + me: { id: MiUser['id'] }, + options: { + populateUser?: boolean; + populateRoom?: boolean; + } = {}, + ) { + if (memberships.length === 0) return []; + + const users = memberships.map(x => x.user ?? x.userId); + const rooms = memberships.map(x => x.room ?? x.roomId); + + const [packedUsers, packedRooms] = await Promise.all([ + this.userEntityService.packMany(users, me) + .then(users => new Map(users.map(u => [u.id, u]))), + this.packRooms(rooms, me) + .then(rooms => new Map(rooms.map(r => [r.id, r]))), + ]); + + return Promise.all(memberships.map(membership => this.packRoomMembership(membership, me, { ...options, _hint_: { packedUsers, packedRooms } }))); + } +} diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index 69f698d9cb..92a684dc1a 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -32,7 +32,6 @@ import type { MiUserNotePining, MiUserProfile, MutingsRepository, - NoteUnreadsRepository, RenoteMutingsRepository, UserMemoRepository, UserNotePiningsRepository, @@ -48,9 +47,9 @@ import { IdService } from '@/core/IdService.js'; import type { AnnouncementService } from '@/core/AnnouncementService.js'; import type { CustomEmojiService } from '@/core/CustomEmojiService.js'; import { AvatarDecorationService } from '@/core/AvatarDecorationService.js'; +import { ChatService } from '@/core/ChatService.js'; import type { OnModuleInit } from '@nestjs/common'; import type { NoteEntityService } from './NoteEntityService.js'; -import type { DriveFileEntityService } from './DriveFileEntityService.js'; import type { PageEntityService } from './PageEntityService.js'; const Ajv = _Ajv.default; @@ -94,6 +93,7 @@ export class UserEntityService implements OnModuleInit { private federatedInstanceService: FederatedInstanceService; private idService: IdService; private avatarDecorationService: AvatarDecorationService; + private chatService: ChatService; constructor( private moduleRef: ModuleRef, @@ -128,9 +128,6 @@ export class UserEntityService implements OnModuleInit { @Inject(DI.renoteMutingsRepository) private renoteMutingsRepository: RenoteMutingsRepository, - @Inject(DI.noteUnreadsRepository) - private noteUnreadsRepository: NoteUnreadsRepository, - @Inject(DI.userNotePiningsRepository) private userNotePiningsRepository: UserNotePiningsRepository, @@ -152,6 +149,7 @@ export class UserEntityService implements OnModuleInit { this.federatedInstanceService = this.moduleRef.get('FederatedInstanceService'); this.idService = this.moduleRef.get('IdService'); this.avatarDecorationService = this.moduleRef.get('AvatarDecorationService'); + this.chatService = this.moduleRef.get('ChatService'); } //#region Validators @@ -558,6 +556,7 @@ export class UserEntityService implements OnModuleInit { publicReactions: this.isLocalUser(user) ? profile!.publicReactions : false, // https://github.com/misskey-dev/misskey/issues/12964 followersVisibility: profile!.followersVisibility, followingVisibility: profile!.followingVisibility, + chatScope: user.chatScope, roles: this.roleService.getUserRoles(user.id).then(roles => roles.filter(role => role.isPublic).sort((a, b) => b.displayOrder - a.displayOrder).map(role => ({ id: role.id, name: role.name, @@ -598,14 +597,9 @@ export class UserEntityService implements OnModuleInit { isDeleted: user.isDeleted, twoFactorBackupCodesStock: profile?.twoFactorBackupSecret?.length === 5 ? 'full' : (profile?.twoFactorBackupSecret?.length ?? 0) > 0 ? 'partial' : 'none', hideOnlineStatus: user.hideOnlineStatus, - hasUnreadSpecifiedNotes: this.noteUnreadsRepository.count({ - where: { userId: user.id, isSpecified: true }, - take: 1, - }).then(count => count > 0), - hasUnreadMentions: this.noteUnreadsRepository.count({ - where: { userId: user.id, isMentioned: true }, - take: 1, - }).then(count => count > 0), + hasUnreadSpecifiedNotes: false, // 後方互換性のため + hasUnreadMentions: false, // 後方互換性のため + hasUnreadChatMessages: this.chatService.hasUnreadMessages(user.id), hasUnreadAnnouncement: unreadAnnouncements!.length > 0, unreadAnnouncements, hasUnreadAntenna: this.getHasUnreadAntenna(user.id), diff --git a/packages/backend/src/di-symbols.ts b/packages/backend/src/di-symbols.ts index a306aac1a1..77d2838e09 100644 --- a/packages/backend/src/di-symbols.ts +++ b/packages/backend/src/di-symbols.ts @@ -24,7 +24,6 @@ export const DI = { noteFavoritesRepository: Symbol('noteFavoritesRepository'), noteThreadMutingsRepository: Symbol('noteThreadMutingsRepository'), noteReactionsRepository: Symbol('noteReactionsRepository'), - noteUnreadsRepository: Symbol('noteUnreadsRepository'), pollsRepository: Symbol('pollsRepository'), pollVotesRepository: Symbol('pollVotesRepository'), userProfilesRepository: Symbol('userProfilesRepository'), @@ -83,6 +82,11 @@ export const DI = { flashsRepository: Symbol('flashsRepository'), flashLikesRepository: Symbol('flashLikesRepository'), userMemosRepository: Symbol('userMemosRepository'), + chatMessagesRepository: Symbol('chatMessagesRepository'), + chatApprovalsRepository: Symbol('chatApprovalsRepository'), + chatRoomsRepository: Symbol('chatRoomsRepository'), + chatRoomMembershipsRepository: Symbol('chatRoomMembershipsRepository'), + chatRoomInvitationsRepository: Symbol('chatRoomInvitationsRepository'), bubbleGameRecordsRepository: Symbol('bubbleGameRecordsRepository'), reversiGamesRepository: Symbol('reversiGamesRepository'), //#endregion diff --git a/packages/backend/src/misc/json-schema.ts b/packages/backend/src/misc/json-schema.ts index 100fb69585..bc9308ca9b 100644 --- a/packages/backend/src/misc/json-schema.ts +++ b/packages/backend/src/misc/json-schema.ts @@ -63,6 +63,10 @@ import { } from '@/models/json-schema/meta.js'; import { packedSystemWebhookSchema } from '@/models/json-schema/system-webhook.js'; import { packedAbuseReportNotificationRecipientSchema } from '@/models/json-schema/abuse-report-notification-recipient.js'; +import { packedChatMessageSchema, packedChatMessageLiteSchema } from '@/models/json-schema/chat-message.js'; +import { packedChatRoomSchema } from '@/models/json-schema/chat-room.js'; +import { packedChatRoomInvitationSchema } from '@/models/json-schema/chat-room-invitation.js'; +import { packedChatRoomMembershipSchema } from '@/models/json-schema/chat-room-membership.js'; export const refs = { UserLite: packedUserLiteSchema, @@ -120,6 +124,11 @@ export const refs = { MetaDetailed: packedMetaDetailedSchema, SystemWebhook: packedSystemWebhookSchema, AbuseReportNotificationRecipient: packedAbuseReportNotificationRecipientSchema, + ChatMessage: packedChatMessageSchema, + ChatMessageLite: packedChatMessageLiteSchema, + ChatRoom: packedChatRoomSchema, + ChatRoomInvitation: packedChatRoomInvitationSchema, + ChatRoomMembership: packedChatRoomMembershipSchema, }; export type Packed = SchemaType; @@ -171,11 +180,11 @@ export interface Schema extends OfSchema { type RequiredPropertyNames = { [K in keyof s]: - // K is not optional - s[K]['optional'] extends false ? K : - // K has default value - s[K]['default'] extends null | string | number | boolean | Record ? K : - never + // K is not optional + s[K]['optional'] extends false ? K : + // K has default value + s[K]['default'] extends null | string | number | boolean | Record ? K : + never }[keyof s]; export type Obj = Record; @@ -214,18 +223,18 @@ type ObjectSchemaTypeDef

= p['anyOf'] extends ReadonlyArray ? p['anyOf'][number]['required'] extends ReadonlyArray ? UnionObjType> & ObjType> : never - : ObjType> - : - p['anyOf'] extends ReadonlyArray ? never : // see CONTRIBUTING.md - p['allOf'] extends ReadonlyArray ? UnionToIntersection> : - p['additionalProperties'] extends true ? Record : - p['additionalProperties'] extends Schema ? - p['additionalProperties'] extends infer AdditionalProperties ? - AdditionalProperties extends Schema ? - Record> : + : ObjType> + : + p['anyOf'] extends ReadonlyArray ? never : // see CONTRIBUTING.md + p['allOf'] extends ReadonlyArray ? UnionToIntersection> : + p['additionalProperties'] extends true ? Record : + p['additionalProperties'] extends Schema ? + p['additionalProperties'] extends infer AdditionalProperties ? + AdditionalProperties extends Schema ? + Record> : + never : never : - never : - any; + any; type ObjectSchemaType

= NullOrUndefined>; @@ -235,30 +244,30 @@ export type SchemaTypeDef

= p['type'] extends 'number' ? number : p['type'] extends 'string' ? ( p['enum'] extends readonly (string | null)[] ? - p['enum'][number] : - p['format'] extends 'date-time' ? string : // Dateにする?? - string + p['enum'][number] : + p['format'] extends 'date-time' ? string : // Dateにする?? + string ) : - p['type'] extends 'boolean' ? boolean : - p['type'] extends 'object' ? ObjectSchemaTypeDef

: - p['type'] extends 'array' ? ( - p['items'] extends OfSchema ? ( - p['items']['anyOf'] extends ReadonlyArray ? UnionSchemaType>[] : - p['items']['oneOf'] extends ReadonlyArray ? ArrayUnion>> : - p['items']['allOf'] extends ReadonlyArray ? UnionToIntersection>>[] : - never - ) : - p['prefixItems'] extends ReadonlyArray ? ( - p['items'] extends NonNullable ? [...ArrayToTuple, ...SchemaType[]] : - p['items'] extends false ? ArrayToTuple : - p['unevaluatedItems'] extends false ? ArrayToTuple : - [...ArrayToTuple, ...unknown[]] + p['type'] extends 'boolean' ? boolean : + p['type'] extends 'object' ? ObjectSchemaTypeDef

: + p['type'] extends 'array' ? ( + p['items'] extends OfSchema ? ( + p['items']['anyOf'] extends ReadonlyArray ? UnionSchemaType>[] : + p['items']['oneOf'] extends ReadonlyArray ? ArrayUnion>> : + p['items']['allOf'] extends ReadonlyArray ? UnionToIntersection>>[] : + never + ) : + p['prefixItems'] extends ReadonlyArray ? ( + p['items'] extends NonNullable ? [...ArrayToTuple, ...SchemaType[]] : + p['items'] extends false ? ArrayToTuple : + p['unevaluatedItems'] extends false ? ArrayToTuple : + [...ArrayToTuple, ...unknown[]] + ) : + p['items'] extends NonNullable ? SchemaType[] : + any[] ) : - p['items'] extends NonNullable ? SchemaType[] : - any[] - ) : - p['anyOf'] extends ReadonlyArray ? UnionSchemaType & PartialIntersection> : - p['oneOf'] extends ReadonlyArray ? UnionSchemaType : - any; + p['anyOf'] extends ReadonlyArray ? UnionSchemaType & PartialIntersection> : + p['oneOf'] extends ReadonlyArray ? UnionSchemaType : + any; export type SchemaType

= NullOrUndefined>; diff --git a/packages/backend/src/models/ChatApproval.ts b/packages/backend/src/models/ChatApproval.ts new file mode 100644 index 0000000000..55c9f07e9a --- /dev/null +++ b/packages/backend/src/models/ChatApproval.ts @@ -0,0 +1,39 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; + +@Entity('chat_approval') +@Index(['userId', 'otherId'], { unique: true }) +export class MiChatApproval { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column({ + ...id(), + }) + public userId: MiUser['id']; + + @ManyToOne(type => MiUser, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: MiUser | null; + + @Index() + @Column({ + ...id(), + }) + public otherId: MiUser['id']; + + @ManyToOne(type => MiUser, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public other: MiUser | null; +} diff --git a/packages/backend/src/models/ChatMessage.ts b/packages/backend/src/models/ChatMessage.ts new file mode 100644 index 0000000000..3d2b64268e --- /dev/null +++ b/packages/backend/src/models/ChatMessage.ts @@ -0,0 +1,85 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; +import { MiDriveFile } from './DriveFile.js'; +import { MiChatRoom } from './ChatRoom.js'; + +@Entity('chat_message') +export class MiChatMessage { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column({ + ...id(), + }) + public fromUserId: MiUser['id']; + + @ManyToOne(type => MiUser, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public fromUser: MiUser | null; + + @Index() + @Column({ + ...id(), nullable: true, + }) + public toUserId: MiUser['id'] | null; + + @ManyToOne(type => MiUser, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public toUser: MiUser | null; + + @Index() + @Column({ + ...id(), nullable: true, + }) + public toRoomId: MiChatRoom['id'] | null; + + @ManyToOne(type => MiChatRoom, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public toRoom: MiChatRoom | null; + + @Column('varchar', { + length: 4096, nullable: true, + }) + public text: string | null; + + @Column('varchar', { + length: 512, nullable: true, + }) + public uri: string | null; + + @Column({ + ...id(), + array: true, default: '{}', + }) + public reads: MiUser['id'][]; + + @Column({ + ...id(), + nullable: true, + }) + public fileId: MiDriveFile['id'] | null; + + @ManyToOne(type => MiDriveFile, { + onDelete: 'SET NULL', + }) + @JoinColumn() + public file: MiDriveFile | null; + + @Column('varchar', { + length: 1024, array: true, default: '{}', + }) + public reactions: string[]; +} diff --git a/packages/backend/src/models/ChatRoom.ts b/packages/backend/src/models/ChatRoom.ts new file mode 100644 index 0000000000..ad2a910b78 --- /dev/null +++ b/packages/backend/src/models/ChatRoom.ts @@ -0,0 +1,41 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; + +@Entity('chat_room') +export class MiChatRoom { + @PrimaryColumn(id()) + public id: string; + + @Column('varchar', { + length: 256, + }) + public name: string; + + @Index() + @Column({ + ...id(), + }) + public ownerId: MiUser['id']; + + @ManyToOne(type => MiUser, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public owner: MiUser | null; + + @Column('varchar', { + length: 2048, default: '', + }) + public description: string; + + @Column('boolean', { + default: false, + }) + public isArchived: boolean; +} diff --git a/packages/backend/src/models/ChatRoomInvitation.ts b/packages/backend/src/models/ChatRoomInvitation.ts new file mode 100644 index 0000000000..36ce12bc92 --- /dev/null +++ b/packages/backend/src/models/ChatRoomInvitation.ts @@ -0,0 +1,45 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; +import { MiChatRoom } from './ChatRoom.js'; + +@Entity('chat_room_invitation') +@Index(['userId', 'roomId'], { unique: true }) +export class MiChatRoomInvitation { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column({ + ...id(), + }) + public userId: MiUser['id']; + + @ManyToOne(type => MiUser, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: MiUser | null; + + @Index() + @Column({ + ...id(), + }) + public roomId: MiChatRoom['id']; + + @ManyToOne(type => MiChatRoom, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public room: MiChatRoom | null; + + @Column('boolean', { + default: false, + }) + public ignored: boolean; +} diff --git a/packages/backend/src/models/ChatRoomMembership.ts b/packages/backend/src/models/ChatRoomMembership.ts new file mode 100644 index 0000000000..3cb5524859 --- /dev/null +++ b/packages/backend/src/models/ChatRoomMembership.ts @@ -0,0 +1,45 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; +import { MiChatRoom } from './ChatRoom.js'; + +@Entity('chat_room_membership') +@Index(['userId', 'roomId'], { unique: true }) +export class MiChatRoomMembership { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column({ + ...id(), + }) + public userId: MiUser['id']; + + @ManyToOne(type => MiUser, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: MiUser | null; + + @Index() + @Column({ + ...id(), + }) + public roomId: MiChatRoom['id']; + + @ManyToOne(type => MiChatRoom, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public room: MiChatRoom | null; + + @Column('boolean', { + default: false, + }) + public isMuted: boolean; +} diff --git a/packages/backend/src/models/NoteUnread.ts b/packages/backend/src/models/NoteUnread.ts deleted file mode 100644 index c759181117..0000000000 --- a/packages/backend/src/models/NoteUnread.ts +++ /dev/null @@ -1,68 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from './util/id.js'; -import { MiUser } from './User.js'; -import { MiNote } from './Note.js'; -import type { MiChannel } from './Channel.js'; - -@Entity('note_unread') -@Index(['userId', 'noteId'], { unique: true }) -export class MiNoteUnread { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column(id()) - public userId: MiUser['id']; - - @ManyToOne(type => MiUser, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: MiUser | null; - - @Index() - @Column(id()) - public noteId: MiNote['id']; - - @ManyToOne(type => MiNote, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public note: MiNote | null; - - /** - * メンションか否か - */ - @Index() - @Column('boolean') - public isMentioned: boolean; - - /** - * ダイレクト投稿か否か - */ - @Index() - @Column('boolean') - public isSpecified: boolean; - - //#region Denormalized fields - @Index() - @Column({ - ...id(), - comment: '[Denormalized]', - }) - public noteUserId: MiUser['id']; - - @Index() - @Column({ - ...id(), - nullable: true, - comment: '[Denormalized]', - }) - public noteChannelId: MiChannel['id'] | null; - //#endregion -} diff --git a/packages/backend/src/models/RepositoryModule.ts b/packages/backend/src/models/RepositoryModule.ts index 04a9df6cfb..b7142d91bf 100644 --- a/packages/backend/src/models/RepositoryModule.ts +++ b/packages/backend/src/models/RepositoryModule.ts @@ -42,7 +42,6 @@ import { MiNoteFavorite, MiNoteReaction, MiNoteThreadMuting, - MiNoteUnread, MiPage, MiPageLike, MiPasswordResetRequest, @@ -78,6 +77,11 @@ import { MiUserPublickey, MiUserSecurityKey, MiWebhook, + MiChatMessage, + MiChatRoom, + MiChatRoomMembership, + MiChatRoomInvitation, + MiChatApproval, } from './_.js'; import type { Provider } from '@nestjs/common'; import type { DataSource } from 'typeorm'; @@ -136,12 +140,6 @@ const $noteReactionsRepository: Provider = { inject: [DI.db], }; -const $noteUnreadsRepository: Provider = { - provide: DI.noteUnreadsRepository, - useFactory: (db: DataSource) => db.getRepository(MiNoteUnread).extend(miRepository as MiRepository), - inject: [DI.db], -}; - const $pollsRepository: Provider = { provide: DI.pollsRepository, useFactory: (db: DataSource) => db.getRepository(MiPoll).extend(miRepository as MiRepository), @@ -288,7 +286,7 @@ const $swSubscriptionsRepository: Provider = { const $systemAccountsRepository: Provider = { provide: DI.systemAccountsRepository, - useFactory: (db: DataSource) => db.getRepository(MiSystemAccount), + useFactory: (db: DataSource) => db.getRepository(MiSystemAccount).extend(miRepository as MiRepository), inject: [DI.db], }; @@ -306,7 +304,7 @@ const $abuseUserReportsRepository: Provider = { const $abuseReportNotificationRecipientRepository: Provider = { provide: DI.abuseReportNotificationRecipientRepository, - useFactory: (db: DataSource) => db.getRepository(MiAbuseReportNotificationRecipient), + useFactory: (db: DataSource) => db.getRepository(MiAbuseReportNotificationRecipient).extend(miRepository as MiRepository), inject: [DI.db], }; @@ -438,7 +436,7 @@ const $webhooksRepository: Provider = { const $systemWebhooksRepository: Provider = { provide: DI.systemWebhooksRepository, - useFactory: (db: DataSource) => db.getRepository(MiSystemWebhook), + useFactory: (db: DataSource) => db.getRepository(MiSystemWebhook).extend(miRepository as MiRepository), inject: [DI.db], }; @@ -490,6 +488,36 @@ const $userMemosRepository: Provider = { inject: [DI.db], }; +const $chatMessagesRepository: Provider = { + provide: DI.chatMessagesRepository, + useFactory: (db: DataSource) => db.getRepository(MiChatMessage).extend(miRepository as MiRepository), + inject: [DI.db], +}; + +const $chatRoomsRepository: Provider = { + provide: DI.chatRoomsRepository, + useFactory: (db: DataSource) => db.getRepository(MiChatRoom).extend(miRepository as MiRepository), + inject: [DI.db], +}; + +const $chatRoomMembershipsRepository: Provider = { + provide: DI.chatRoomMembershipsRepository, + useFactory: (db: DataSource) => db.getRepository(MiChatRoomMembership).extend(miRepository as MiRepository), + inject: [DI.db], +}; + +const $chatRoomInvitationsRepository: Provider = { + provide: DI.chatRoomInvitationsRepository, + useFactory: (db: DataSource) => db.getRepository(MiChatRoomInvitation).extend(miRepository as MiRepository), + inject: [DI.db], +}; + +const $chatApprovalsRepository: Provider = { + provide: DI.chatApprovalsRepository, + useFactory: (db: DataSource) => db.getRepository(MiChatApproval).extend(miRepository as MiRepository), + inject: [DI.db], +}; + const $bubbleGameRecordsRepository: Provider = { provide: DI.bubbleGameRecordsRepository, useFactory: (db: DataSource) => db.getRepository(MiBubbleGameRecord).extend(miRepository as MiRepository), @@ -514,7 +542,6 @@ const $reversiGamesRepository: Provider = { $noteFavoritesRepository, $noteThreadMutingsRepository, $noteReactionsRepository, - $noteUnreadsRepository, $pollsRepository, $pollVotesRepository, $userProfilesRepository, @@ -573,6 +600,11 @@ const $reversiGamesRepository: Provider = { $flashsRepository, $flashLikesRepository, $userMemosRepository, + $chatMessagesRepository, + $chatRoomsRepository, + $chatRoomMembershipsRepository, + $chatRoomInvitationsRepository, + $chatApprovalsRepository, $bubbleGameRecordsRepository, $reversiGamesRepository, ], @@ -586,7 +618,6 @@ const $reversiGamesRepository: Provider = { $noteFavoritesRepository, $noteThreadMutingsRepository, $noteReactionsRepository, - $noteUnreadsRepository, $pollsRepository, $pollVotesRepository, $userProfilesRepository, @@ -645,6 +676,11 @@ const $reversiGamesRepository: Provider = { $flashsRepository, $flashLikesRepository, $userMemosRepository, + $chatMessagesRepository, + $chatRoomsRepository, + $chatRoomMembershipsRepository, + $chatRoomInvitationsRepository, + $chatApprovalsRepository, $bubbleGameRecordsRepository, $reversiGamesRepository, ], diff --git a/packages/backend/src/models/User.ts b/packages/backend/src/models/User.ts index 630240efde..bc652cea62 100644 --- a/packages/backend/src/models/User.ts +++ b/packages/backend/src/models/User.ts @@ -225,6 +225,17 @@ export class MiUser { }) public emojis: string[]; + // チャットを許可する相手 + // everyone: 誰からでも + // followers: フォロワーのみ + // following: フォローしているユーザーのみ + // mutual: 相互フォローのみ + // none: 誰からも受け付けない + @Column('varchar', { + length: 128, default: 'mutual', + }) + public chatScope: 'everyone' | 'followers' | 'following' | 'mutual' | 'none'; + @Index() @Column('varchar', { length: 128, nullable: true, diff --git a/packages/backend/src/models/_.ts b/packages/backend/src/models/_.ts index fa15760c00..e852b302f3 100644 --- a/packages/backend/src/models/_.ts +++ b/packages/backend/src/models/_.ts @@ -3,13 +3,10 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { FindOneOptions, InsertQueryBuilder, ObjectLiteral, Repository, SelectQueryBuilder, TypeORMError } from 'typeorm'; -import { DriverUtils } from 'typeorm/driver/DriverUtils.js'; +import { FindOneOptions, InsertQueryBuilder, ObjectLiteral, Repository, SelectQueryBuilder } from 'typeorm'; import { RelationCountLoader } from 'typeorm/query-builder/relation-count/RelationCountLoader.js'; import { RelationIdLoader } from 'typeorm/query-builder/relation-id/RelationIdLoader.js'; import { RawSqlResultsToEntityTransformer } from 'typeorm/query-builder/transformer/RawSqlResultsToEntityTransformer.js'; -import { ObjectUtils } from 'typeorm/util/ObjectUtils.js'; -import { OrmUtils } from 'typeorm/util/OrmUtils.js'; import { MiAbuseUserReport } from '@/models/AbuseUserReport.js'; import { MiAbuseReportNotificationRecipient } from '@/models/AbuseReportNotificationRecipient.js'; import { MiAccessToken } from '@/models/AccessToken.js'; @@ -43,7 +40,6 @@ import { MiNote } from '@/models/Note.js'; import { MiNoteFavorite } from '@/models/NoteFavorite.js'; import { MiNoteReaction } from '@/models/NoteReaction.js'; import { MiNoteThreadMuting } from '@/models/NoteThreadMuting.js'; -import { MiNoteUnread } from '@/models/NoteUnread.js'; import { MiPage } from '@/models/Page.js'; import { MiPageLike } from '@/models/PageLike.js'; import { MiPasswordResetRequest } from '@/models/PasswordResetRequest.js'; @@ -78,6 +74,11 @@ import { MiRoleAssignment } from '@/models/RoleAssignment.js'; import { MiFlash } from '@/models/Flash.js'; import { MiFlashLike } from '@/models/FlashLike.js'; import { MiUserListFavorite } from '@/models/UserListFavorite.js'; +import { MiChatMessage } from '@/models/ChatMessage.js'; +import { MiChatRoom } from '@/models/ChatRoom.js'; +import { MiChatRoomMembership } from '@/models/ChatRoomMembership.js'; +import { MiChatRoomInvitation } from '@/models/ChatRoomInvitation.js'; +import { MiChatApproval } from '@/models/ChatApproval.js'; import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js'; import { MiReversiGame } from '@/models/ReversiGame.js'; import type { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity.js'; @@ -159,7 +160,6 @@ export { MiNoteFavorite, MiNoteReaction, MiNoteThreadMuting, - MiNoteUnread, MiPage, MiPageLike, MiPasswordResetRequest, @@ -194,6 +194,11 @@ export { MiFlash, MiFlashLike, MiUserMemo, + MiChatMessage, + MiChatRoom, + MiChatRoomMembership, + MiChatRoomInvitation, + MiChatApproval, MiBubbleGameRecord, MiReversiGame, }; @@ -231,7 +236,6 @@ export type NotesRepository = Repository & MiRepository; export type NoteFavoritesRepository = Repository & MiRepository; export type NoteReactionsRepository = Repository & MiRepository; export type NoteThreadMutingsRepository = Repository & MiRepository; -export type NoteUnreadsRepository = Repository & MiRepository; export type PagesRepository = Repository & MiRepository; export type PageLikesRepository = Repository & MiRepository; export type PasswordResetRequestsRepository = Repository & MiRepository; @@ -266,5 +270,10 @@ export type RoleAssignmentsRepository = Repository & MiReposit export type FlashsRepository = Repository & MiRepository; export type FlashLikesRepository = Repository & MiRepository; export type UserMemoRepository = Repository & MiRepository; +export type ChatMessagesRepository = Repository & MiRepository; +export type ChatRoomsRepository = Repository & MiRepository; +export type ChatRoomMembershipsRepository = Repository & MiRepository; +export type ChatRoomInvitationsRepository = Repository & MiRepository; +export type ChatApprovalsRepository = Repository & MiRepository; export type BubbleGameRecordsRepository = Repository & MiRepository; export type ReversiGamesRepository = Repository & MiRepository; diff --git a/packages/backend/src/models/json-schema/chat-message.ts b/packages/backend/src/models/json-schema/chat-message.ts new file mode 100644 index 0000000000..44b7298702 --- /dev/null +++ b/packages/backend/src/models/json-schema/chat-message.ts @@ -0,0 +1,146 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export const packedChatMessageSchema = { + type: 'object', + properties: { + id: { + type: 'string', + optional: false, nullable: false, + }, + createdAt: { + type: 'string', + format: 'date-time', + optional: false, nullable: false, + }, + fromUserId: { + type: 'string', + optional: false, nullable: false, + }, + fromUser: { + type: 'object', + optional: false, nullable: false, + ref: 'UserLite', + }, + toUserId: { + type: 'string', + optional: true, nullable: true, + }, + toUser: { + type: 'object', + optional: true, nullable: true, + ref: 'UserLite', + }, + toRoomId: { + type: 'string', + optional: true, nullable: true, + }, + toRoom: { + type: 'object', + optional: true, nullable: true, + ref: 'ChatRoom', + }, + text: { + type: 'string', + optional: true, nullable: true, + }, + fileId: { + type: 'string', + optional: true, nullable: true, + }, + file: { + type: 'object', + optional: true, nullable: true, + ref: 'DriveFile', + }, + isRead: { + type: 'boolean', + optional: true, nullable: false, + }, + reactions: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + properties: { + reaction: { + type: 'string', + optional: false, nullable: false, + }, + user: { + type: 'object', + optional: true, nullable: true, + ref: 'UserLite', + }, + }, + }, + }, + }, +} as const; + +export const packedChatMessageLiteSchema = { + type: 'object', + properties: { + id: { + type: 'string', + optional: false, nullable: false, + }, + createdAt: { + type: 'string', + format: 'date-time', + optional: false, nullable: false, + }, + fromUserId: { + type: 'string', + optional: false, nullable: false, + }, + fromUser: { + type: 'object', + optional: true, nullable: false, + ref: 'UserLite', + }, + toUserId: { + type: 'string', + optional: true, nullable: true, + }, + toRoomId: { + type: 'string', + optional: true, nullable: true, + }, + text: { + type: 'string', + optional: true, nullable: true, + }, + fileId: { + type: 'string', + optional: true, nullable: true, + }, + file: { + type: 'object', + optional: true, nullable: true, + ref: 'DriveFile', + }, + reactions: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + properties: { + reaction: { + type: 'string', + optional: false, nullable: false, + }, + user: { + type: 'object', + optional: true, nullable: true, + ref: 'UserLite', + }, + }, + }, + }, + }, +} as const; diff --git a/packages/backend/src/models/json-schema/chat-room-invitation.ts b/packages/backend/src/models/json-schema/chat-room-invitation.ts new file mode 100644 index 0000000000..204c959b2c --- /dev/null +++ b/packages/backend/src/models/json-schema/chat-room-invitation.ts @@ -0,0 +1,37 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export const packedChatRoomInvitationSchema = { + type: 'object', + properties: { + id: { + type: 'string', + optional: false, nullable: false, + }, + createdAt: { + type: 'string', + format: 'date-time', + optional: false, nullable: false, + }, + userId: { + type: 'string', + optional: false, nullable: false, + }, + user: { + type: 'object', + optional: false, nullable: false, + ref: 'UserLite', + }, + roomId: { + type: 'string', + optional: false, nullable: false, + }, + room: { + type: 'object', + optional: false, nullable: false, + ref: 'ChatRoom', + }, + }, +} as const; diff --git a/packages/backend/src/models/json-schema/chat-room-membership.ts b/packages/backend/src/models/json-schema/chat-room-membership.ts new file mode 100644 index 0000000000..adb73f9dde --- /dev/null +++ b/packages/backend/src/models/json-schema/chat-room-membership.ts @@ -0,0 +1,37 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export const packedChatRoomMembershipSchema = { + type: 'object', + properties: { + id: { + type: 'string', + optional: false, nullable: false, + }, + createdAt: { + type: 'string', + format: 'date-time', + optional: false, nullable: false, + }, + userId: { + type: 'string', + optional: false, nullable: false, + }, + user: { + type: 'object', + optional: true, nullable: false, + ref: 'UserLite', + }, + roomId: { + type: 'string', + optional: false, nullable: false, + }, + room: { + type: 'object', + optional: true, nullable: false, + ref: 'ChatRoom', + }, + }, +} as const; diff --git a/packages/backend/src/models/json-schema/chat-room.ts b/packages/backend/src/models/json-schema/chat-room.ts new file mode 100644 index 0000000000..e97556e378 --- /dev/null +++ b/packages/backend/src/models/json-schema/chat-room.ts @@ -0,0 +1,40 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export const packedChatRoomSchema = { + type: 'object', + properties: { + id: { + type: 'string', + optional: false, nullable: false, + }, + createdAt: { + type: 'string', + format: 'date-time', + optional: false, nullable: false, + }, + ownerId: { + type: 'string', + optional: false, nullable: false, + }, + owner: { + type: 'object', + optional: false, nullable: false, + ref: 'UserLite', + }, + name: { + type: 'string', + optional: false, nullable: false, + }, + description: { + type: 'string', + optional: false, nullable: false, + }, + isMuted: { + type: 'boolean', + optional: true, nullable: false, + }, + }, +} as const; diff --git a/packages/backend/src/models/json-schema/role.ts b/packages/backend/src/models/json-schema/role.ts index 3537de94c8..1685a806c9 100644 --- a/packages/backend/src/models/json-schema/role.ts +++ b/packages/backend/src/models/json-schema/role.ts @@ -292,6 +292,10 @@ export const packedRolePoliciesSchema = { type: 'boolean', optional: false, nullable: false, }, + canChat: { + type: 'boolean', + optional: false, nullable: false, + }, }, } as const; diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts index 38631f907d..b10430782b 100644 --- a/packages/backend/src/models/json-schema/user.ts +++ b/packages/backend/src/models/json-schema/user.ts @@ -358,6 +358,11 @@ export const packedUserDetailedNotMeOnlySchema = { nullable: false, optional: false, enum: ['public', 'followers', 'private'], }, + chatScope: { + type: 'string', + nullable: false, optional: false, + enum: ['everyone', 'following', 'followers', 'mutual', 'none'], + }, roles: { type: 'array', nullable: false, optional: false, @@ -540,6 +545,10 @@ export const packedMeDetailedOnlySchema = { type: 'boolean', nullable: false, optional: false, }, + hasUnreadChatMessages: { + type: 'boolean', + nullable: false, optional: false, + }, hasUnreadNotification: { type: 'boolean', nullable: false, optional: false, diff --git a/packages/backend/src/postgres.ts b/packages/backend/src/postgres.ts index 043332d4b5..4694e7003d 100644 --- a/packages/backend/src/postgres.ts +++ b/packages/backend/src/postgres.ts @@ -8,6 +8,9 @@ import pg from 'pg'; import { DataSource, Logger } from 'typeorm'; import * as highlight from 'cli-highlight'; import { entities as charts } from '@/core/chart/entities.js'; +import { Config } from '@/config.js'; +import MisskeyLogger from '@/logger.js'; +import { bindThis } from '@/decorators.js'; import { MiAbuseUserReport } from '@/models/AbuseUserReport.js'; import { MiAbuseReportNotificationRecipient } from '@/models/AbuseReportNotificationRecipient.js'; @@ -42,7 +45,6 @@ import { MiNote } from '@/models/Note.js'; import { MiNoteFavorite } from '@/models/NoteFavorite.js'; import { MiNoteReaction } from '@/models/NoteReaction.js'; import { MiNoteThreadMuting } from '@/models/NoteThreadMuting.js'; -import { MiNoteUnread } from '@/models/NoteUnread.js'; import { MiPage } from '@/models/Page.js'; import { MiPageLike } from '@/models/PageLike.js'; import { MiPasswordResetRequest } from '@/models/PasswordResetRequest.js'; @@ -76,13 +78,14 @@ import { MiRoleAssignment } from '@/models/RoleAssignment.js'; import { MiFlash } from '@/models/Flash.js'; import { MiFlashLike } from '@/models/FlashLike.js'; import { MiUserMemo } from '@/models/UserMemo.js'; +import { MiChatMessage } from '@/models/ChatMessage.js'; +import { MiChatRoom } from '@/models/ChatRoom.js'; +import { MiChatRoomMembership } from '@/models/ChatRoomMembership.js'; +import { MiChatRoomInvitation } from '@/models/ChatRoomInvitation.js'; import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js'; import { MiReversiGame } from '@/models/ReversiGame.js'; - -import { Config } from '@/config.js'; -import MisskeyLogger from '@/logger.js'; -import { bindThis } from '@/decorators.js'; -import { MiSystemAccount } from './models/SystemAccount.js'; +import { MiChatApproval } from '@/models/ChatApproval.js'; +import { MiSystemAccount } from '@/models/SystemAccount.js'; pg.types.setTypeParser(20, Number); @@ -195,7 +198,6 @@ export const entities = [ MiNoteFavorite, MiNoteReaction, MiNoteThreadMuting, - MiNoteUnread, MiPage, MiPageLike, MiGalleryPost, @@ -236,6 +238,11 @@ export const entities = [ MiFlash, MiFlashLike, MiUserMemo, + MiChatMessage, + MiChatRoom, + MiChatRoomMembership, + MiChatRoomInvitation, + MiChatApproval, MiBubbleGameRecord, MiReversiGame, ...charts, diff --git a/packages/backend/src/server/ServerModule.ts b/packages/backend/src/server/ServerModule.ts index 3ab0b815f2..0223650329 100644 --- a/packages/backend/src/server/ServerModule.ts +++ b/packages/backend/src/server/ServerModule.ts @@ -44,6 +44,8 @@ import { QueueStatsChannelService } from './api/stream/channels/queue-stats.js'; import { ServerStatsChannelService } from './api/stream/channels/server-stats.js'; import { UserListChannelService } from './api/stream/channels/user-list.js'; import { RoleTimelineChannelService } from './api/stream/channels/role-timeline.js'; +import { ChatUserChannelService } from './api/stream/channels/chat-user.js'; +import { ChatRoomChannelService } from './api/stream/channels/chat-room.js'; import { ReversiChannelService } from './api/stream/channels/reversi.js'; import { ReversiGameChannelService } from './api/stream/channels/reversi-game.js'; import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.js'; @@ -84,6 +86,8 @@ import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.j GlobalTimelineChannelService, HashtagChannelService, RoleTimelineChannelService, + ChatUserChannelService, + ChatRoomChannelService, ReversiChannelService, ReversiGameChannelService, HomeTimelineChannelService, diff --git a/packages/backend/src/server/api/StreamingApiServerService.ts b/packages/backend/src/server/api/StreamingApiServerService.ts index b8f448477b..2a4e1fc574 100644 --- a/packages/backend/src/server/api/StreamingApiServerService.ts +++ b/packages/backend/src/server/api/StreamingApiServerService.ts @@ -9,7 +9,6 @@ import * as Redis from 'ioredis'; import * as WebSocket from 'ws'; import { DI } from '@/di-symbols.js'; import type { UsersRepository, MiAccessToken } from '@/models/_.js'; -import { NoteReadService } from '@/core/NoteReadService.js'; import { NotificationService } from '@/core/NotificationService.js'; import { bindThis } from '@/decorators.js'; import { CacheService } from '@/core/CacheService.js'; @@ -35,7 +34,6 @@ export class StreamingApiServerService { private usersRepository: UsersRepository, private cacheService: CacheService, - private noteReadService: NoteReadService, private authenticateService: AuthenticateService, private channelsService: ChannelsService, private notificationService: NotificationService, @@ -96,7 +94,6 @@ export class StreamingApiServerService { const stream = new MainStreamConnection( this.channelsService, - this.noteReadService, this.notificationService, this.cacheService, this.channelFollowingService, diff --git a/packages/backend/src/server/api/endpoint-list.ts b/packages/backend/src/server/api/endpoint-list.ts index 560d3f6587..9bb29e138b 100644 --- a/packages/backend/src/server/api/endpoint-list.ts +++ b/packages/backend/src/server/api/endpoint-list.ts @@ -263,7 +263,6 @@ export * as 'i/notifications-grouped' from './endpoints/i/notifications-grouped. export * as 'i/page-likes' from './endpoints/i/page-likes.js'; export * as 'i/pages' from './endpoints/i/pages.js'; export * as 'i/pin' from './endpoints/i/pin.js'; -export * as 'i/read-all-unread-notes' from './endpoints/i/read-all-unread-notes.js'; export * as 'i/read-announcement' from './endpoints/i/read-announcement.js'; export * as 'i/regenerate-token' from './endpoints/i/regenerate-token.js'; export * as 'i/registry/get' from './endpoints/i/registry/get.js'; @@ -397,4 +396,26 @@ export * as 'users/search' from './endpoints/users/search.js'; export * as 'users/search-by-username-and-host' from './endpoints/users/search-by-username-and-host.js'; export * as 'users/show' from './endpoints/users/show.js'; export * as 'users/update-memo' from './endpoints/users/update-memo.js'; +export * as 'chat/messages/create-to-user' from './endpoints/chat/messages/create-to-user.js'; +export * as 'chat/messages/create-to-room' from './endpoints/chat/messages/create-to-room.js'; +export * as 'chat/messages/delete' from './endpoints/chat/messages/delete.js'; +export * as 'chat/messages/show' from './endpoints/chat/messages/show.js'; +export * as 'chat/messages/react' from './endpoints/chat/messages/react.js'; +export * as 'chat/messages/user-timeline' from './endpoints/chat/messages/user-timeline.js'; +export * as 'chat/messages/room-timeline' from './endpoints/chat/messages/room-timeline.js'; +export * as 'chat/messages/search' from './endpoints/chat/messages/search.js'; +export * as 'chat/rooms/create' from './endpoints/chat/rooms/create.js'; +export * as 'chat/rooms/delete' from './endpoints/chat/rooms/delete.js'; +export * as 'chat/rooms/join' from './endpoints/chat/rooms/join.js'; +export * as 'chat/rooms/leave' from './endpoints/chat/rooms/leave.js'; +export * as 'chat/rooms/mute' from './endpoints/chat/rooms/mute.js'; +export * as 'chat/rooms/show' from './endpoints/chat/rooms/show.js'; +export * as 'chat/rooms/owned' from './endpoints/chat/rooms/owned.js'; +export * as 'chat/rooms/joining' from './endpoints/chat/rooms/joining.js'; +export * as 'chat/rooms/update' from './endpoints/chat/rooms/update.js'; +export * as 'chat/rooms/members' from './endpoints/chat/rooms/members.js'; +export * as 'chat/rooms/invitations/create' from './endpoints/chat/rooms/invitations/create.js'; +export * as 'chat/rooms/invitations/ignore' from './endpoints/chat/rooms/invitations/ignore.js'; +export * as 'chat/rooms/invitations/inbox' from './endpoints/chat/rooms/invitations/inbox.js'; +export * as 'chat/history' from './endpoints/chat/history.js'; export * as 'v2/admin/emoji/list' from './endpoints/v2/admin/emoji/list.js'; diff --git a/packages/backend/src/server/api/endpoints/antennas/notes.ts b/packages/backend/src/server/api/endpoints/antennas/notes.ts index f4dfe1ecc4..727697ea14 100644 --- a/packages/backend/src/server/api/endpoints/antennas/notes.ts +++ b/packages/backend/src/server/api/endpoints/antennas/notes.ts @@ -8,7 +8,6 @@ import * as Redis from 'ioredis'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { NotesRepository, AntennasRepository } from '@/models/_.js'; import { QueryService } from '@/core/QueryService.js'; -import { NoteReadService } from '@/core/NoteReadService.js'; import { DI } from '@/di-symbols.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { IdService } from '@/core/IdService.js'; @@ -59,9 +58,6 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.redisForTimelines) - private redisForTimelines: Redis.Redis, - @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -71,7 +67,6 @@ export default class extends Endpoint { // eslint- private idService: IdService, private noteEntityService: NoteEntityService, private queryService: QueryService, - private noteReadService: NoteReadService, private fanoutTimelineService: FanoutTimelineService, private globalEventService: GlobalEventService, ) { @@ -124,8 +119,6 @@ export default class extends Endpoint { // eslint- notes.sort((a, b) => a.id > b.id ? -1 : 1); } - this.noteReadService.read(me.id, notes); - return await this.noteEntityService.packMany(notes, me); }); } diff --git a/packages/backend/src/server/api/endpoints/chat/history.ts b/packages/backend/src/server/api/endpoints/chat/history.ts new file mode 100644 index 0000000000..7553a751e0 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/chat/history.ts @@ -0,0 +1,73 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; +import { ChatService } from '@/core/ChatService.js'; +import { ChatEntityService } from '@/core/entities/ChatEntityService.js'; +import { ApiError } from '@/server/api/error.js'; + +export const meta = { + tags: ['chat'], + + requireCredential: true, + + kind: 'read:chat', + + res: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + ref: 'ChatMessage', + }, + }, + + errors: { + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + room: { type: 'boolean', default: false }, + }, +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + private chatEntityService: ChatEntityService, + private chatService: ChatService, + ) { + super(meta, paramDef, async (ps, me) => { + const history = ps.room ? await this.chatService.roomHistory(me.id, ps.limit) : await this.chatService.userHistory(me.id, ps.limit); + + const packedMessages = await this.chatEntityService.packMessagesDetailed(history, me); + + if (ps.room) { + const roomIds = history.map(m => m.toRoomId!); + const readStateMap = await this.chatService.getRoomReadStateMap(me.id, roomIds); + + for (const message of packedMessages) { + message.isRead = readStateMap[message.toRoomId!] ?? false; + } + } else { + const otherIds = history.map(m => m.fromUserId === me.id ? m.toUserId! : m.fromUserId!); + const readStateMap = await this.chatService.getUserReadStateMap(me.id, otherIds); + + for (const message of packedMessages) { + const otherId = message.fromUserId === me.id ? message.toUserId! : message.fromUserId!; + message.isRead = readStateMap[otherId] ?? false; + } + } + + return packedMessages; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/chat/messages/create-to-room.ts b/packages/backend/src/server/api/endpoints/chat/messages/create-to-room.ts new file mode 100644 index 0000000000..1f334d5750 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/chat/messages/create-to-room.ts @@ -0,0 +1,105 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import ms from 'ms'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { GetterService } from '@/server/api/GetterService.js'; +import { DI } from '@/di-symbols.js'; +import { ApiError } from '@/server/api/error.js'; +import { ChatService } from '@/core/ChatService.js'; +import type { DriveFilesRepository, MiUser } from '@/models/_.js'; + +export const meta = { + tags: ['chat'], + + requireCredential: true, + requiredRolePolicy: 'canChat', + + prohibitMoved: true, + + kind: 'write:chat', + + limit: { + duration: ms('1hour'), + max: 500, + }, + + res: { + type: 'object', + optional: false, nullable: false, + ref: 'ChatMessageLite', + }, + + errors: { + noSuchRoom: { + message: 'No such room.', + code: 'NO_SUCH_ROOM', + id: '8098520d-2da5-4e8f-8ee1-df78b55a4ec6', + }, + + noSuchFile: { + message: 'No such file.', + code: 'NO_SUCH_FILE', + id: 'b6accbd3-1d7b-4d9f-bdb7-eb185bac06db', + }, + + contentRequired: { + message: 'Content required. You need to set text or fileId.', + code: 'CONTENT_REQUIRED', + id: '340517b7-6d04-42c0-bac1-37ee804e3594', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + text: { type: 'string', nullable: true, maxLength: 2000 }, + fileId: { type: 'string', format: 'misskey:id' }, + toRoomId: { type: 'string', format: 'misskey:id' }, + }, + required: ['toRoomId'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + + private getterService: GetterService, + private chatService: ChatService, + ) { + super(meta, paramDef, async (ps, me) => { + const room = await this.chatService.findRoomById(ps.toRoomId); + if (room == null) { + throw new ApiError(meta.errors.noSuchRoom); + } + + let file = null; + if (ps.fileId != null) { + file = await this.driveFilesRepository.findOneBy({ + id: ps.fileId, + userId: me.id, + }); + + if (file == null) { + throw new ApiError(meta.errors.noSuchFile); + } + } + + // テキストが無いかつ添付ファイルも無かったらエラー + if (ps.text == null && file == null) { + throw new ApiError(meta.errors.contentRequired); + } + + return await this.chatService.createMessageToRoom(me, room, { + text: ps.text, + file: file, + }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/chat/messages/create-to-user.ts b/packages/backend/src/server/api/endpoints/chat/messages/create-to-user.ts new file mode 100644 index 0000000000..6b77a026fb --- /dev/null +++ b/packages/backend/src/server/api/endpoints/chat/messages/create-to-user.ts @@ -0,0 +1,122 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import ms from 'ms'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { GetterService } from '@/server/api/GetterService.js'; +import { DI } from '@/di-symbols.js'; +import { ApiError } from '@/server/api/error.js'; +import { ChatService } from '@/core/ChatService.js'; +import type { DriveFilesRepository, MiUser } from '@/models/_.js'; + +export const meta = { + tags: ['chat'], + + requireCredential: true, + requiredRolePolicy: 'canChat', + + prohibitMoved: true, + + kind: 'write:chat', + + limit: { + duration: ms('1hour'), + max: 500, + }, + + res: { + type: 'object', + optional: false, nullable: false, + ref: 'ChatMessageLite', + }, + + errors: { + recipientIsYourself: { + message: 'You can not send a message to yourself.', + code: 'RECIPIENT_IS_YOURSELF', + id: '17e2ba79-e22a-4cbc-bf91-d327643f4a7e', + }, + + noSuchUser: { + message: 'No such user.', + code: 'NO_SUCH_USER', + id: '11795c64-40ea-4198-b06e-3c873ed9039d', + }, + + noSuchFile: { + message: 'No such file.', + code: 'NO_SUCH_FILE', + id: '4372b8e2-185d-4146-8749-2f68864a3e5f', + }, + + contentRequired: { + message: 'Content required. You need to set text or fileId.', + code: 'CONTENT_REQUIRED', + id: '25587321-b0e6-449c-9239-f8925092942c', + }, + + youHaveBeenBlocked: { + message: 'You cannot send a message because you have been blocked by this user.', + code: 'YOU_HAVE_BEEN_BLOCKED', + id: 'c15a5199-7422-4968-941a-2a462c478f7d', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + text: { type: 'string', nullable: true, maxLength: 2000 }, + fileId: { type: 'string', format: 'misskey:id' }, + toUserId: { type: 'string', format: 'misskey:id' }, + }, + required: ['toUserId'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + + private getterService: GetterService, + private chatService: ChatService, + ) { + super(meta, paramDef, async (ps, me) => { + let file = null; + if (ps.fileId != null) { + file = await this.driveFilesRepository.findOneBy({ + id: ps.fileId, + userId: me.id, + }); + + if (file == null) { + throw new ApiError(meta.errors.noSuchFile); + } + } + + // テキストが無いかつ添付ファイルも無かったらエラー + if (ps.text == null && file == null) { + throw new ApiError(meta.errors.contentRequired); + } + + // Myself + if (ps.toUserId === me.id) { + throw new ApiError(meta.errors.recipientIsYourself); + } + + const toUser = await this.getterService.getUser(ps.toUserId).catch(err => { + if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + throw err; + }); + + return await this.chatService.createMessageToUser(me, toUser, { + text: ps.text, + file: file, + }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/chat/messages/delete.ts b/packages/backend/src/server/api/endpoints/chat/messages/delete.ts new file mode 100644 index 0000000000..959599ddcf --- /dev/null +++ b/packages/backend/src/server/api/endpoints/chat/messages/delete.ts @@ -0,0 +1,52 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; +import { ChatService } from '@/core/ChatService.js'; +import { ApiError } from '@/server/api/error.js'; + +export const meta = { + tags: ['chat'], + + requireCredential: true, + + kind: 'write:chat', + + res: { + }, + + errors: { + noSuchMessage: { + message: 'No such message.', + code: 'NO_SUCH_MESSAGE', + id: '36b67f0e-66a6-414b-83df-992a55294f17', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + messageId: { type: 'string', format: 'misskey:id' }, + }, + required: ['messageId'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + private chatService: ChatService, + ) { + super(meta, paramDef, async (ps, me) => { + const message = await this.chatService.findMyMessageById(me.id, ps.messageId); + if (message == null) { + throw new ApiError(meta.errors.noSuchMessage); + } + await this.chatService.deleteMessage(message); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/chat/messages/react.ts b/packages/backend/src/server/api/endpoints/chat/messages/react.ts new file mode 100644 index 0000000000..561e36ed19 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/chat/messages/react.ts @@ -0,0 +1,49 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; +import { ChatService } from '@/core/ChatService.js'; +import { ApiError } from '@/server/api/error.js'; + +export const meta = { + tags: ['chat'], + + requireCredential: true, + + kind: 'write:chat', + + res: { + }, + + errors: { + noSuchMessage: { + message: 'No such message.', + code: 'NO_SUCH_MESSAGE', + id: '9b5839b9-0ba0-4351-8c35-37082093d200', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + messageId: { type: 'string', format: 'misskey:id' }, + reaction: { type: 'string' }, + }, + required: ['messageId', 'reaction'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + private chatService: ChatService, + ) { + super(meta, paramDef, async (ps, me) => { + await this.chatService.react(ps.messageId, me.id, ps.reaction); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/chat/messages/room-timeline.ts b/packages/backend/src/server/api/endpoints/chat/messages/room-timeline.ts new file mode 100644 index 0000000000..ccc0030403 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/chat/messages/room-timeline.ts @@ -0,0 +1,73 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; +import { ChatService } from '@/core/ChatService.js'; +import { ChatEntityService } from '@/core/entities/ChatEntityService.js'; +import { ApiError } from '@/server/api/error.js'; + +export const meta = { + tags: ['chat'], + + requireCredential: true, + + kind: 'read:chat', + + res: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + ref: 'ChatMessageLite', + }, + }, + + errors: { + noSuchRoom: { + message: 'No such room.', + code: 'NO_SUCH_ROOM', + id: 'c4d9f88c-9270-4632-b032-6ed8cee36f7f', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + sinceId: { type: 'string', format: 'misskey:id' }, + untilId: { type: 'string', format: 'misskey:id' }, + roomId: { type: 'string', format: 'misskey:id' }, + }, + required: ['roomId'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + private chatEntityService: ChatEntityService, + private chatService: ChatService, + ) { + super(meta, paramDef, async (ps, me) => { + const room = await this.chatService.findRoomById(ps.roomId); + if (room == null) { + throw new ApiError(meta.errors.noSuchRoom); + } + + if (!(await this.chatService.isRoomMember(room, me.id))) { + throw new ApiError(meta.errors.noSuchRoom); + } + + const messages = await this.chatService.roomTimeline(room.id, ps.limit, ps.sinceId, ps.untilId); + + this.chatService.readRoomChatMessage(me.id, room.id); + + return await this.chatEntityService.packMessagesLiteForRoom(messages); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/chat/messages/search.ts b/packages/backend/src/server/api/endpoints/chat/messages/search.ts new file mode 100644 index 0000000000..4c989e5ca9 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/chat/messages/search.ts @@ -0,0 +1,76 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; +import { ChatService } from '@/core/ChatService.js'; +import { ChatEntityService } from '@/core/entities/ChatEntityService.js'; +import { ApiError } from '@/server/api/error.js'; + +export const meta = { + tags: ['chat'], + + requireCredential: true, + + kind: 'read:chat', + + res: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + ref: 'ChatMessage', + }, + }, + + errors: { + noSuchRoom: { + message: 'No such room.', + code: 'NO_SUCH_ROOM', + id: '460b3669-81b0-4dc9-a997-44442141bf83', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + query: { type: 'string', minLength: 1, maxLength: 256 }, + limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + userId: { type: 'string', format: 'misskey:id', nullable: true }, + roomId: { type: 'string', format: 'misskey:id', nullable: true }, + }, + required: ['query'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + private chatEntityService: ChatEntityService, + private chatService: ChatService, + ) { + super(meta, paramDef, async (ps, me) => { + if (ps.roomId != null) { + const room = await this.chatService.findRoomById(ps.roomId); + if (room == null) { + throw new ApiError(meta.errors.noSuchRoom); + } + + if (!(await this.chatService.isRoomMember(room, me.id))) { + throw new ApiError(meta.errors.noSuchRoom); + } + } + + const messages = await this.chatService.searchMessages(me.id, ps.query, ps.limit, { + userId: ps.userId, + roomId: ps.roomId, + }); + + return await this.chatEntityService.packMessagesDetailed(messages, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/chat/messages/show.ts b/packages/backend/src/server/api/endpoints/chat/messages/show.ts new file mode 100644 index 0000000000..371f7a7071 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/chat/messages/show.ts @@ -0,0 +1,63 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; +import { GetterService } from '@/server/api/GetterService.js'; +import { ChatService } from '@/core/ChatService.js'; +import { ChatEntityService } from '@/core/entities/ChatEntityService.js'; +import { ApiError } from '@/server/api/error.js'; +import { RoleService } from '@/core/RoleService.js'; + +export const meta = { + tags: ['chat'], + + requireCredential: true, + + kind: 'read:chat', + + res: { + type: 'object', + optional: false, nullable: false, + ref: 'ChatMessage', + }, + + errors: { + noSuchMessage: { + message: 'No such message.', + code: 'NO_SUCH_MESSAGE', + id: '3710865b-1848-4da9-8d61-cfed15510b93', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + messageId: { type: 'string', format: 'misskey:id' }, + }, + required: ['messageId'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + private chatService: ChatService, + private roleService: RoleService, + private chatEntityService: ChatEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const message = await this.chatService.findMessageById(ps.messageId); + if (message == null) { + throw new ApiError(meta.errors.noSuchMessage); + } + if (message.fromUserId !== me.id && message.toUserId !== me.id && !(await this.roleService.isModerator(me))) { + throw new ApiError(meta.errors.noSuchMessage); + } + return this.chatEntityService.packMessageDetailed(message, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/chat/messages/user-timeline.ts b/packages/backend/src/server/api/endpoints/chat/messages/user-timeline.ts new file mode 100644 index 0000000000..9d308d79b0 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/chat/messages/user-timeline.ts @@ -0,0 +1,71 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; +import { GetterService } from '@/server/api/GetterService.js'; +import { ChatService } from '@/core/ChatService.js'; +import { ChatEntityService } from '@/core/entities/ChatEntityService.js'; +import { ApiError } from '@/server/api/error.js'; + +export const meta = { + tags: ['chat'], + + requireCredential: true, + + kind: 'read:chat', + + res: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + ref: 'ChatMessageLite', + }, + }, + + errors: { + noSuchUser: { + message: 'No such user.', + code: 'NO_SUCH_USER', + id: '11795c64-40ea-4198-b06e-3c873ed9039d', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + sinceId: { type: 'string', format: 'misskey:id' }, + untilId: { type: 'string', format: 'misskey:id' }, + userId: { type: 'string', format: 'misskey:id' }, + }, + required: ['userId'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + private chatEntityService: ChatEntityService, + private chatService: ChatService, + private getterService: GetterService, + ) { + super(meta, paramDef, async (ps, me) => { + const other = await this.getterService.getUser(ps.userId).catch(err => { + if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + throw err; + }); + + const messages = await this.chatService.userTimeline(me.id, other.id, ps.limit, ps.sinceId, ps.untilId); + + this.chatService.readUserChatMessage(me.id, other.id); + + return await this.chatEntityService.packMessagesLiteFor1on1(messages); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/create.ts b/packages/backend/src/server/api/endpoints/chat/rooms/create.ts new file mode 100644 index 0000000000..fa4cc8ceb4 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/chat/rooms/create.ts @@ -0,0 +1,62 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import ms from 'ms'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; +import { ApiError } from '@/server/api/error.js'; +import { ChatService } from '@/core/ChatService.js'; +import { ChatEntityService } from '@/core/entities/ChatEntityService.js'; + +export const meta = { + tags: ['chat'], + + requireCredential: true, + requiredRolePolicy: 'canChat', + + prohibitMoved: true, + + kind: 'write:chat', + + limit: { + duration: ms('1day'), + max: 10, + }, + + res: { + type: 'object', + optional: false, nullable: false, + ref: 'ChatRoom', + }, + + errors: { + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + name: { type: 'string', maxLength: 256 }, + description: { type: 'string', maxLength: 1024 }, + }, + required: ['name'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + private chatService: ChatService, + private chatEntityService: ChatEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const room = await this.chatService.createRoom(me, { + name: ps.name, + description: ps.description ?? '', + }); + return await this.chatEntityService.packRoom(room); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/delete.ts b/packages/backend/src/server/api/endpoints/chat/rooms/delete.ts new file mode 100644 index 0000000000..2ef0a778f1 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/chat/rooms/delete.ts @@ -0,0 +1,52 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; +import { ChatService } from '@/core/ChatService.js'; +import { ApiError } from '@/server/api/error.js'; + +export const meta = { + tags: ['chat'], + + requireCredential: true, + + kind: 'write:chat', + + res: { + }, + + errors: { + noSuchRoom: { + message: 'No such room.', + code: 'NO_SUCH_ROOM', + id: 'd4e3753d-97bf-4a19-ab8e-21080fbc0f4b', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + roomId: { type: 'string', format: 'misskey:id' }, + }, + required: ['roomId'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + private chatService: ChatService, + ) { + super(meta, paramDef, async (ps, me) => { + const room = await this.chatService.findMyRoomById(me.id, ps.roomId); + if (room == null) { + throw new ApiError(meta.errors.noSuchRoom); + } + await this.chatService.deleteRoom(room); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/invitations/create.ts b/packages/backend/src/server/api/endpoints/chat/rooms/invitations/create.ts new file mode 100644 index 0000000000..5da4a1a772 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/chat/rooms/invitations/create.ts @@ -0,0 +1,68 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import ms from 'ms'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; +import { ApiError } from '@/server/api/error.js'; +import { ChatService } from '@/core/ChatService.js'; +import { ChatEntityService } from '@/core/entities/ChatEntityService.js'; + +export const meta = { + tags: ['chat'], + + requireCredential: true, + requiredRolePolicy: 'canChat', + + prohibitMoved: true, + + kind: 'write:chat', + + limit: { + duration: ms('1day'), + max: 50, + }, + + res: { + type: 'object', + optional: false, nullable: false, + ref: 'ChatRoomInvitation', + }, + + errors: { + noSuchRoom: { + message: 'No such room.', + code: 'NO_SUCH_ROOM', + id: '916f9507-49ba-4e90-b57f-1fd4deaa47a5', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + roomId: { type: 'string', format: 'misskey:id' }, + userId: { type: 'string', format: 'misskey:id' }, + }, + required: ['roomId', 'userId'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + private chatService: ChatService, + private chatEntityService: ChatEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const room = await this.chatService.findMyRoomById(me.id, ps.roomId); + if (room == null) { + throw new ApiError(meta.errors.noSuchRoom); + } + const invitation = await this.chatService.createRoomInvitation(me.id, room.id, ps.userId); + return await this.chatEntityService.packRoomInvitation(invitation, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/invitations/ignore.ts b/packages/backend/src/server/api/endpoints/chat/rooms/invitations/ignore.ts new file mode 100644 index 0000000000..8c017f7d01 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/chat/rooms/invitations/ignore.ts @@ -0,0 +1,48 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; +import { ChatService } from '@/core/ChatService.js'; +import { ApiError } from '@/server/api/error.js'; + +export const meta = { + tags: ['chat'], + + requireCredential: true, + + kind: 'write:chat', + + res: { + }, + + errors: { + noSuchRoom: { + message: 'No such room.', + code: 'NO_SUCH_ROOM', + id: '5130557e-5a11-4cfb-9cc5-fe60cda5de0d', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + roomId: { type: 'string', format: 'misskey:id' }, + }, + required: ['roomId'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + private chatService: ChatService, + ) { + super(meta, paramDef, async (ps, me) => { + await this.chatService.ignoreRoomInvitation(me.id, ps.roomId); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/invitations/inbox.ts b/packages/backend/src/server/api/endpoints/chat/rooms/invitations/inbox.ts new file mode 100644 index 0000000000..07337480fc --- /dev/null +++ b/packages/backend/src/server/api/endpoints/chat/rooms/invitations/inbox.ts @@ -0,0 +1,54 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; +import { ChatService } from '@/core/ChatService.js'; +import { ChatEntityService } from '@/core/entities/ChatEntityService.js'; +import { ApiError } from '@/server/api/error.js'; + +export const meta = { + tags: ['chat'], + + requireCredential: true, + + kind: 'read:chat', + + res: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + ref: 'ChatRoomInvitation', + }, + }, + + errors: { + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 }, + sinceId: { type: 'string', format: 'misskey:id' }, + untilId: { type: 'string', format: 'misskey:id' }, + }, +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + private chatEntityService: ChatEntityService, + private chatService: ChatService, + ) { + super(meta, paramDef, async (ps, me) => { + const invitations = await this.chatService.getReceivedRoomInvitationsWithPagination(me.id, ps.limit, ps.sinceId, ps.untilId); + return this.chatEntityService.packRoomInvitations(invitations, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/join.ts b/packages/backend/src/server/api/endpoints/chat/rooms/join.ts new file mode 100644 index 0000000000..dbd4d1ea5a --- /dev/null +++ b/packages/backend/src/server/api/endpoints/chat/rooms/join.ts @@ -0,0 +1,48 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; +import { ChatService } from '@/core/ChatService.js'; +import { ApiError } from '@/server/api/error.js'; + +export const meta = { + tags: ['chat'], + + requireCredential: true, + + kind: 'write:chat', + + res: { + }, + + errors: { + noSuchRoom: { + message: 'No such room.', + code: 'NO_SUCH_ROOM', + id: '84416476-5ce8-4a2c-b568-9569f1b10733', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + roomId: { type: 'string', format: 'misskey:id' }, + }, + required: ['roomId'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + private chatService: ChatService, + ) { + super(meta, paramDef, async (ps, me) => { + await this.chatService.joinToRoom(me.id, ps.roomId); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/joining.ts b/packages/backend/src/server/api/endpoints/chat/rooms/joining.ts new file mode 100644 index 0000000000..c4c6253236 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/chat/rooms/joining.ts @@ -0,0 +1,58 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; +import { ChatService } from '@/core/ChatService.js'; +import { ChatEntityService } from '@/core/entities/ChatEntityService.js'; +import { ApiError } from '@/server/api/error.js'; + +export const meta = { + tags: ['chat'], + + requireCredential: true, + + kind: 'read:chat', + + res: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + ref: 'ChatRoomMembership', + }, + }, + + errors: { + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 }, + sinceId: { type: 'string', format: 'misskey:id' }, + untilId: { type: 'string', format: 'misskey:id' }, + }, +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + private chatService: ChatService, + private chatEntityService: ChatEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const memberships = await this.chatService.getMyMemberships(me.id, ps.limit, ps.sinceId, ps.untilId); + + return this.chatEntityService.packRoomMemberships(memberships, me, { + populateUser: false, + populateRoom: true, + }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/leave.ts b/packages/backend/src/server/api/endpoints/chat/rooms/leave.ts new file mode 100644 index 0000000000..724ad61f7e --- /dev/null +++ b/packages/backend/src/server/api/endpoints/chat/rooms/leave.ts @@ -0,0 +1,48 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; +import { ChatService } from '@/core/ChatService.js'; +import { ApiError } from '@/server/api/error.js'; + +export const meta = { + tags: ['chat'], + + requireCredential: true, + + kind: 'write:chat', + + res: { + }, + + errors: { + noSuchRoom: { + message: 'No such room.', + code: 'NO_SUCH_ROOM', + id: 'cb7f3179-50e8-4389-8c30-dbe2650a67c9', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + roomId: { type: 'string', format: 'misskey:id' }, + }, + required: ['roomId'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + private chatService: ChatService, + ) { + super(meta, paramDef, async (ps, me) => { + await this.chatService.leaveRoom(me.id, ps.roomId); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/members.ts b/packages/backend/src/server/api/endpoints/chat/rooms/members.ts new file mode 100644 index 0000000000..407bfe74f1 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/chat/rooms/members.ts @@ -0,0 +1,74 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; +import { ChatService } from '@/core/ChatService.js'; +import { ApiError } from '@/server/api/error.js'; +import { ChatEntityService } from '@/core/entities/ChatEntityService.js'; + +export const meta = { + tags: ['chat'], + + requireCredential: true, + + kind: 'write:chat', + + res: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + ref: 'ChatRoomMembership', + }, + }, + + errors: { + noSuchRoom: { + message: 'No such room.', + code: 'NO_SUCH_ROOM', + id: '7b9fe84c-eafc-4d21-bf89-485458ed2c18', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + roomId: { type: 'string', format: 'misskey:id' }, + limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 }, + sinceId: { type: 'string', format: 'misskey:id' }, + untilId: { type: 'string', format: 'misskey:id' }, + }, + required: ['roomId'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + private chatService: ChatService, + private chatEntityService: ChatEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const room = await this.chatService.findRoomById(ps.roomId); + if (room == null) { + throw new ApiError(meta.errors.noSuchRoom); + } + + if (!(await this.chatService.isRoomMember(room, me.id))) { + throw new ApiError(meta.errors.noSuchRoom); + } + + const memberships = await this.chatService.getRoomMembershipsWithPagination(room.id, ps.limit, ps.sinceId, ps.untilId); + + return this.chatEntityService.packRoomMemberships(memberships, me, { + populateUser: true, + populateRoom: false, + }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/mute.ts b/packages/backend/src/server/api/endpoints/chat/rooms/mute.ts new file mode 100644 index 0000000000..5208b8a253 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/chat/rooms/mute.ts @@ -0,0 +1,49 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; +import { ChatService } from '@/core/ChatService.js'; +import { ApiError } from '@/server/api/error.js'; + +export const meta = { + tags: ['chat'], + + requireCredential: true, + + kind: 'write:chat', + + res: { + }, + + errors: { + noSuchRoom: { + message: 'No such room.', + code: 'NO_SUCH_ROOM', + id: 'c2cde4eb-8d0f-42f1-8f2f-c4d6bfc8e5df', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + roomId: { type: 'string', format: 'misskey:id' }, + mute: { type: 'boolean' }, + }, + required: ['roomId', 'mute'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + private chatService: ChatService, + ) { + super(meta, paramDef, async (ps, me) => { + await this.chatService.muteRoom(me.id, ps.roomId, ps.mute); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/owned.ts b/packages/backend/src/server/api/endpoints/chat/rooms/owned.ts new file mode 100644 index 0000000000..6516120bca --- /dev/null +++ b/packages/backend/src/server/api/endpoints/chat/rooms/owned.ts @@ -0,0 +1,54 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; +import { ChatService } from '@/core/ChatService.js'; +import { ChatEntityService } from '@/core/entities/ChatEntityService.js'; +import { ApiError } from '@/server/api/error.js'; + +export const meta = { + tags: ['chat'], + + requireCredential: true, + + kind: 'read:chat', + + res: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + ref: 'ChatRoom', + }, + }, + + errors: { + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 }, + sinceId: { type: 'string', format: 'misskey:id' }, + untilId: { type: 'string', format: 'misskey:id' }, + }, +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + private chatEntityService: ChatEntityService, + private chatService: ChatService, + ) { + super(meta, paramDef, async (ps, me) => { + const rooms = await this.chatService.getOwnedRoomsWithPagination(me.id, ps.limit, ps.sinceId, ps.untilId); + return this.chatEntityService.packRooms(rooms, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/show.ts b/packages/backend/src/server/api/endpoints/chat/rooms/show.ts new file mode 100644 index 0000000000..547618ee7d --- /dev/null +++ b/packages/backend/src/server/api/endpoints/chat/rooms/show.ts @@ -0,0 +1,58 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; +import { ChatService } from '@/core/ChatService.js'; +import { ApiError } from '@/server/api/error.js'; +import { ChatEntityService } from '@/core/entities/ChatEntityService.js'; + +export const meta = { + tags: ['chat'], + + requireCredential: true, + + kind: 'read:chat', + + res: { + type: 'object', + optional: false, nullable: false, + ref: 'ChatRoom', + }, + + errors: { + noSuchRoom: { + message: 'No such room.', + code: 'NO_SUCH_ROOM', + id: '857ae02f-8759-4d20-9adb-6e95fffe4fd7', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + roomId: { type: 'string', format: 'misskey:id' }, + }, + required: ['roomId'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + private chatService: ChatService, + private chatEntityService: ChatEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const room = await this.chatService.findRoomById(ps.roomId); + if (room == null) { + throw new ApiError(meta.errors.noSuchRoom); + } + + return this.chatEntityService.packRoom(room, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/chat/rooms/update.ts b/packages/backend/src/server/api/endpoints/chat/rooms/update.ts new file mode 100644 index 0000000000..6f2a9c10b5 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/chat/rooms/update.ts @@ -0,0 +1,65 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; +import { ChatService } from '@/core/ChatService.js'; +import { ApiError } from '@/server/api/error.js'; +import { ChatEntityService } from '@/core/entities/ChatEntityService.js'; + +export const meta = { + tags: ['chat'], + + requireCredential: true, + + kind: 'write:chat', + + res: { + type: 'object', + optional: false, nullable: false, + ref: 'ChatRoom', + }, + + errors: { + noSuchRoom: { + message: 'No such room.', + code: 'NO_SUCH_ROOM', + id: 'fcdb0f92-bda6-47f9-bd05-343e0e020932', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + roomId: { type: 'string', format: 'misskey:id' }, + name: { type: 'string', maxLength: 256 }, + description: { type: 'string', maxLength: 1024 }, + }, + required: ['roomId'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + private chatService: ChatService, + private chatEntityService: ChatEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const room = await this.chatService.findMyRoomById(me.id, ps.roomId); + if (room == null) { + throw new ApiError(meta.errors.noSuchRoom); + } + + const updated = await this.chatService.updateRoom(room, { + name: ps.name, + description: ps.description, + }); + + return this.chatEntityService.packRoom(updated, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/notifications-grouped.ts b/packages/backend/src/server/api/endpoints/i/notifications-grouped.ts index dc6ffd3e02..88d7f51c26 100644 --- a/packages/backend/src/server/api/endpoints/i/notifications-grouped.ts +++ b/packages/backend/src/server/api/endpoints/i/notifications-grouped.ts @@ -9,7 +9,6 @@ import { Inject, Injectable } from '@nestjs/common'; import type { NotesRepository } from '@/models/_.js'; import { obsoleteNotificationTypes, groupedNotificationTypes, FilterUnionByProperty } from '@/types.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { NoteReadService } from '@/core/NoteReadService.js'; import { NotificationEntityService } from '@/core/entities/NotificationEntityService.js'; import { NotificationService } from '@/core/NotificationService.js'; import { DI } from '@/di-symbols.js'; @@ -63,13 +62,9 @@ export default class extends Endpoint { // eslint- @Inject(DI.redis) private redisClient: Redis.Redis, - @Inject(DI.notesRepository) - private notesRepository: NotesRepository, - private idService: IdService, private notificationEntityService: NotificationEntityService, private notificationService: NotificationService, - private noteReadService: NoteReadService, ) { super(meta, paramDef, async (ps, me) => { const EXTRA_LIMIT = 100; @@ -162,14 +157,6 @@ export default class extends Endpoint { // eslint- } groupedNotifications = groupedNotifications.slice(0, ps.limit); - const noteIds = groupedNotifications - .filter((notification): notification is FilterUnionByProperty => ['mention', 'reply', 'quote'].includes(notification.type)) - .map(notification => notification.noteId!); - - if (noteIds.length > 0) { - const notes = await this.notesRepository.findBy({ id: In(noteIds) }); - this.noteReadService.read(me.id, notes); - } return await this.notificationEntityService.packGroupedMany(groupedNotifications, me.id); }); diff --git a/packages/backend/src/server/api/endpoints/i/notifications.ts b/packages/backend/src/server/api/endpoints/i/notifications.ts index 2f619380e9..be8d0cfb34 100644 --- a/packages/backend/src/server/api/endpoints/i/notifications.ts +++ b/packages/backend/src/server/api/endpoints/i/notifications.ts @@ -9,7 +9,6 @@ import { Inject, Injectable } from '@nestjs/common'; import type { NotesRepository } from '@/models/_.js'; import { FilterUnionByProperty, notificationTypes, obsoleteNotificationTypes } from '@/types.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { NoteReadService } from '@/core/NoteReadService.js'; import { NotificationEntityService } from '@/core/entities/NotificationEntityService.js'; import { NotificationService } from '@/core/NotificationService.js'; import { DI } from '@/di-symbols.js'; @@ -69,7 +68,6 @@ export default class extends Endpoint { // eslint- private idService: IdService, private notificationEntityService: NotificationEntityService, private notificationService: NotificationService, - private noteReadService: NoteReadService, ) { super(meta, paramDef, async (ps, me) => { // includeTypes が空の場合はクエリしない @@ -136,15 +134,6 @@ export default class extends Endpoint { // eslint- this.notificationService.readAllNotification(me.id); } - const noteIds = notifications - .filter((notification): notification is FilterUnionByProperty => ['mention', 'reply', 'quote'].includes(notification.type)) - .map(notification => notification.noteId); - - if (noteIds.length > 0) { - const notes = await this.notesRepository.findBy({ id: In(noteIds) }); - this.noteReadService.read(me.id, notes); - } - return await this.notificationEntityService.packMany(notifications, me.id); }); } diff --git a/packages/backend/src/server/api/endpoints/i/read-all-unread-notes.ts b/packages/backend/src/server/api/endpoints/i/read-all-unread-notes.ts deleted file mode 100644 index d1a8eccb1d..0000000000 --- a/packages/backend/src/server/api/endpoints/i/read-all-unread-notes.ts +++ /dev/null @@ -1,45 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { NoteUnreadsRepository } from '@/models/_.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; -import { DI } from '@/di-symbols.js'; - -export const meta = { - tags: ['account'], - - requireCredential: true, - - kind: 'write:account', -} as const; - -export const paramDef = { - type: 'object', - properties: {}, - required: [], -} as const; - -@Injectable() -export default class extends Endpoint { // eslint-disable-line import/no-default-export - constructor( - @Inject(DI.noteUnreadsRepository) - private noteUnreadsRepository: NoteUnreadsRepository, - - private globalEventService: GlobalEventService, - ) { - super(meta, paramDef, async (ps, me) => { - // Remove documents - await this.noteUnreadsRepository.delete({ - userId: me.id, - }); - - // 全て既読になったイベントを発行 - this.globalEventService.publishMainStream(me.id, 'readAllUnreadMentions'); - this.globalEventService.publishMainStream(me.id, 'readAllUnreadSpecifiedNotes'); - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index 4c72879b73..baf397bf06 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -190,6 +190,7 @@ export const paramDef = { autoSensitive: { type: 'boolean' }, followingVisibility: { type: 'string', enum: ['public', 'followers', 'private'] }, followersVisibility: { type: 'string', enum: ['public', 'followers', 'private'] }, + chatScope: { type: 'string', enum: ['everyone', 'followers', 'following', 'mutual', 'none'] }, pinnedPageId: { type: 'string', format: 'misskey:id', nullable: true }, mutedWords: muteWords, hardMutedWords: muteWords, @@ -288,6 +289,7 @@ export default class extends Endpoint { // eslint- if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday; if (ps.followingVisibility !== undefined) profileUpdates.followingVisibility = ps.followingVisibility; if (ps.followersVisibility !== undefined) profileUpdates.followersVisibility = ps.followersVisibility; + if (ps.chatScope !== undefined) updates.chatScope = ps.chatScope; function checkMuteWordCount(mutedWords: (string[] | string)[], limit: number) { // TODO: ちゃんと数える diff --git a/packages/backend/src/server/api/endpoints/notes/mentions.ts b/packages/backend/src/server/api/endpoints/notes/mentions.ts index 5558dd3a8b..18a3915ab5 100644 --- a/packages/backend/src/server/api/endpoints/notes/mentions.ts +++ b/packages/backend/src/server/api/endpoints/notes/mentions.ts @@ -9,7 +9,6 @@ import type { NotesRepository, FollowingsRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; -import { NoteReadService } from '@/core/NoteReadService.js'; import { DI } from '@/di-symbols.js'; export const meta = { @@ -52,7 +51,6 @@ export default class extends Endpoint { // eslint- private noteEntityService: NoteEntityService, private queryService: QueryService, - private noteReadService: NoteReadService, ) { super(meta, paramDef, async (ps, me) => { const followingQuery = this.followingsRepository.createQueryBuilder('following') @@ -89,8 +87,6 @@ export default class extends Endpoint { // eslint- const mentions = await query.limit(ps.limit).getMany(); - this.noteReadService.read(me.id, mentions); - return await this.noteEntityService.packMany(mentions, me); }); } diff --git a/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts b/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts index 732d644a29..29c6aa7434 100644 --- a/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts @@ -9,7 +9,6 @@ import type { NotesRepository, NoteThreadMutingsRepository } from '@/models/_.js import { IdService } from '@/core/IdService.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { GetterService } from '@/server/api/GetterService.js'; -import { NoteReadService } from '@/core/NoteReadService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; @@ -52,7 +51,6 @@ export default class extends Endpoint { // eslint- private noteThreadMutingsRepository: NoteThreadMutingsRepository, private getterService: GetterService, - private noteReadService: NoteReadService, private idService: IdService, ) { super(meta, paramDef, async (ps, me) => { @@ -69,8 +67,6 @@ export default class extends Endpoint { // eslint- }], }); - await this.noteReadService.read(me.id, mutedNotes); - await this.noteThreadMutingsRepository.insert({ id: this.idService.gen(), threadId: note.threadId ?? note.id, diff --git a/packages/backend/src/server/api/stream/ChannelsService.ts b/packages/backend/src/server/api/stream/ChannelsService.ts index 253409259f..c0ef589dea 100644 --- a/packages/backend/src/server/api/stream/ChannelsService.ts +++ b/packages/backend/src/server/api/stream/ChannelsService.ts @@ -19,6 +19,8 @@ import { AntennaChannelService } from './channels/antenna.js'; import { DriveChannelService } from './channels/drive.js'; import { HashtagChannelService } from './channels/hashtag.js'; import { RoleTimelineChannelService } from './channels/role-timeline.js'; +import { ChatUserChannelService } from './channels/chat-user.js'; +import { ChatRoomChannelService } from './channels/chat-room.js'; import { ReversiChannelService } from './channels/reversi.js'; import { ReversiGameChannelService } from './channels/reversi-game.js'; import { type MiChannelService } from './channel.js'; @@ -40,6 +42,8 @@ export class ChannelsService { private serverStatsChannelService: ServerStatsChannelService, private queueStatsChannelService: QueueStatsChannelService, private adminChannelService: AdminChannelService, + private chatUserChannelService: ChatUserChannelService, + private chatRoomChannelService: ChatRoomChannelService, private reversiChannelService: ReversiChannelService, private reversiGameChannelService: ReversiGameChannelService, ) { @@ -62,6 +66,8 @@ export class ChannelsService { case 'serverStats': return this.serverStatsChannelService; case 'queueStats': return this.queueStatsChannelService; case 'admin': return this.adminChannelService; + case 'chatUser': return this.chatUserChannelService; + case 'chatRoom': return this.chatRoomChannelService; case 'reversi': return this.reversiChannelService; case 'reversiGame': return this.reversiGameChannelService; diff --git a/packages/backend/src/server/api/stream/Connection.ts b/packages/backend/src/server/api/stream/Connection.ts index 0fb5238c78..c9801d8314 100644 --- a/packages/backend/src/server/api/stream/Connection.ts +++ b/packages/backend/src/server/api/stream/Connection.ts @@ -7,7 +7,6 @@ import * as WebSocket from 'ws'; import type { MiUser } from '@/models/User.js'; import type { MiAccessToken } from '@/models/AccessToken.js'; import type { Packed } from '@/misc/json-schema.js'; -import type { NoteReadService } from '@/core/NoteReadService.js'; import type { NotificationService } from '@/core/NotificationService.js'; import { bindThis } from '@/decorators.js'; import { CacheService } from '@/core/CacheService.js'; @@ -45,7 +44,6 @@ export default class Connection { constructor( private channelsService: ChannelsService, - private noteReadService: NoteReadService, private notificationService: NotificationService, private cacheService: CacheService, private channelFollowingService: ChannelFollowingService, @@ -119,7 +117,7 @@ export default class Connection { case 'readNotification': this.onReadNotification(body); break; case 'subNote': this.onSubscribeNote(body); break; case 's': this.onSubscribeNote(body); break; // alias - case 'sr': this.onSubscribeNote(body); this.readNote(body); break; + case 'sr': this.onSubscribeNote(body); break; case 'unsubNote': this.onUnsubscribeNote(body); break; case 'un': this.onUnsubscribeNote(body); break; // alias case 'connect': this.onChannelConnectRequested(body); break; @@ -154,19 +152,6 @@ export default class Connection { if (note.renote) add(note.renote); } - @bindThis - private readNote(body: JsonValue | undefined) { - if (!isJsonObject(body)) return; - const id = body.id; - - const note = this.cachedNotes.find(n => n.id === id); - if (note == null) return; - - if (this.user && (note.userId !== this.user.id)) { - this.noteReadService.read(this.user.id, [note]); - } - } - @bindThis private onReadNotification(payload: JsonValue | undefined) { this.notificationService.readAllNotification(this.user!.id); diff --git a/packages/backend/src/server/api/stream/channels/chat-room.ts b/packages/backend/src/server/api/stream/channels/chat-room.ts new file mode 100644 index 0000000000..e989969345 --- /dev/null +++ b/packages/backend/src/server/api/stream/channels/chat-room.ts @@ -0,0 +1,78 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import { bindThis } from '@/decorators.js'; +import type { GlobalEvents } from '@/core/GlobalEventService.js'; +import type { JsonObject } from '@/misc/json-value.js'; +import { ChatService } from '@/core/ChatService.js'; +import Channel, { type MiChannelService } from '../channel.js'; + +class ChatRoomChannel extends Channel { + public readonly chName = 'chatRoom'; + public static shouldShare = false; + public static requireCredential = true as const; + public static kind = 'read:chat'; + private roomId: string; + + constructor( + private chatService: ChatService, + + id: string, + connection: Channel['connection'], + ) { + super(id, connection); + } + + @bindThis + public async init(params: JsonObject) { + if (typeof params.roomId !== 'string') return; + this.roomId = params.roomId; + + this.subscriber.on(`chatRoomStream:${this.roomId}`, this.onEvent); + } + + @bindThis + private async onEvent(data: GlobalEvents['chat']['payload']) { + this.send(data.type, data.body); + } + + @bindThis + public onMessage(type: string, body: any) { + switch (type) { + case 'read': + if (this.roomId) { + this.chatService.readRoomChatMessage(this.user!.id, this.roomId); + } + break; + } + } + + @bindThis + public dispose() { + this.subscriber.off(`chatRoomStream:${this.roomId}`, this.onEvent); + } +} + +@Injectable() +export class ChatRoomChannelService implements MiChannelService { + public readonly shouldShare = ChatRoomChannel.shouldShare; + public readonly requireCredential = ChatRoomChannel.requireCredential; + public readonly kind = ChatRoomChannel.kind; + + constructor( + private chatService: ChatService, + ) { + } + + @bindThis + public create(id: string, connection: Channel['connection']): ChatRoomChannel { + return new ChatRoomChannel( + this.chatService, + id, + connection, + ); + } +} diff --git a/packages/backend/src/server/api/stream/channels/chat-user.ts b/packages/backend/src/server/api/stream/channels/chat-user.ts new file mode 100644 index 0000000000..c4e898cd5b --- /dev/null +++ b/packages/backend/src/server/api/stream/channels/chat-user.ts @@ -0,0 +1,78 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import { bindThis } from '@/decorators.js'; +import type { GlobalEvents } from '@/core/GlobalEventService.js'; +import type { JsonObject } from '@/misc/json-value.js'; +import { ChatService } from '@/core/ChatService.js'; +import Channel, { type MiChannelService } from '../channel.js'; + +class ChatUserChannel extends Channel { + public readonly chName = 'chatUser'; + public static shouldShare = false; + public static requireCredential = true as const; + public static kind = 'read:chat'; + private otherId: string; + + constructor( + private chatService: ChatService, + + id: string, + connection: Channel['connection'], + ) { + super(id, connection); + } + + @bindThis + public async init(params: JsonObject) { + if (typeof params.otherId !== 'string') return; + this.otherId = params.otherId; + + this.subscriber.on(`chatUserStream:${this.user!.id}-${this.otherId}`, this.onEvent); + } + + @bindThis + private async onEvent(data: GlobalEvents['chat']['payload']) { + this.send(data.type, data.body); + } + + @bindThis + public onMessage(type: string, body: any) { + switch (type) { + case 'read': + if (this.otherId) { + this.chatService.readUserChatMessage(this.user!.id, this.otherId); + } + break; + } + } + + @bindThis + public dispose() { + this.subscriber.off(`chatUserStream:${this.user!.id}-${this.otherId}`, this.onEvent); + } +} + +@Injectable() +export class ChatUserChannelService implements MiChannelService { + public readonly shouldShare = ChatUserChannel.shouldShare; + public readonly requireCredential = ChatUserChannel.requireCredential; + public readonly kind = ChatUserChannel.kind; + + constructor( + private chatService: ChatService, + ) { + } + + @bindThis + public create(id: string, connection: Channel['connection']): ChatUserChannel { + return new ChatUserChannel( + this.chatService, + id, + connection, + ); + } +} diff --git a/packages/backend/test-federation/test/timeline.test.ts b/packages/backend/test-federation/test/timeline.test.ts index 2250bf4a42..00635e654b 100644 --- a/packages/backend/test-federation/test/timeline.test.ts +++ b/packages/backend/test-federation/test/timeline.test.ts @@ -24,7 +24,7 @@ describe('Timeline', () => { }); type TimelineChannel = keyof Misskey.Channels & (`${string}Timeline` | 'antenna' | 'userList' | 'hashtag'); - type TimelineEndpoint = keyof Misskey.Endpoints & (`${string}timeline` | 'antennas/notes' | 'roles/notes' | 'notes/search-by-tag'); + type TimelineEndpoint = keyof Misskey.Endpoints & (`notes/${string}timeline` | 'antennas/notes' | 'roles/notes' | 'notes/search-by-tag'); const timelineMap = new Map([ ['antenna', 'antennas/notes'], ['globalTimeline', 'notes/global-timeline'], diff --git a/packages/backend/test/e2e/mute.ts b/packages/backend/test/e2e/mute.ts index f37da288b7..b464c24287 100644 --- a/packages/backend/test/e2e/mute.ts +++ b/packages/backend/test/e2e/mute.ts @@ -51,30 +51,8 @@ describe('Mute', () => { assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); }); - test('ミュートしているユーザーからメンションされても、hasUnreadMentions が true にならない', async () => { - // 状態リセット - await api('i/read-all-unread-notes', {}, alice); - - await post(carol, { text: '@alice hi' }); - - const res = await api('i', {}, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(res.body.hasUnreadMentions, false); - }); - - test('ミュートしているユーザーからメンションされても、ストリームに unreadMention イベントが流れてこない', async () => { - // 状態リセット - await api('i/read-all-unread-notes', {}, alice); - - const fired = await waitFire(alice, 'main', () => post(carol, { text: '@alice hi' }), msg => msg.type === 'unreadMention'); - - assert.strictEqual(fired, false); - }); - test('ミュートしているユーザーからメンションされても、ストリームに unreadNotification イベントが流れてこない', async () => { // 状態リセット - await api('i/read-all-unread-notes', {}, alice); await api('notifications/mark-all-as-read', {}, alice); const fired = await waitFire(alice, 'main', () => post(carol, { text: '@alice hi' }), msg => msg.type === 'unreadNotification'); diff --git a/packages/backend/test/e2e/thread-mute.ts b/packages/backend/test/e2e/thread-mute.ts index 1ac99df884..1edc178fc2 100644 --- a/packages/backend/test/e2e/thread-mute.ts +++ b/packages/backend/test/e2e/thread-mute.ts @@ -38,48 +38,6 @@ describe('Note thread mute', () => { assert.strictEqual(res.body.some(note => note.id === carolReplyWithoutMention.id), false); }); - test('ミュートしているスレッドからメンションされても、hasUnreadMentions が true にならない', async () => { - // 状態リセット - await api('i/read-all-unread-notes', {}, alice); - - const bobNote = await post(bob, { text: '@alice @carol root note' }); - - await api('notes/thread-muting/create', { noteId: bobNote.id }, alice); - - const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' }); - - const res = await api('i', {}, alice); - - assert.strictEqual(res.status, 200); - assert.strictEqual(res.body.hasUnreadMentions, false); - }); - - test('ミュートしているスレッドからメンションされても、ストリームに unreadMention イベントが流れてこない', () => new Promise(async done => { - // 状態リセット - await api('i/read-all-unread-notes', {}, alice); - - const bobNote = await post(bob, { text: '@alice @carol root note' }); - - await api('notes/thread-muting/create', { noteId: bobNote.id }, alice); - - let fired = false; - - const ws = await connectStream(alice, 'main', async ({ type, body }) => { - if (type === 'unreadMention') { - if (body === bobNote.id) return; - fired = true; - } - }); - - const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' }); - - setTimeout(() => { - assert.strictEqual(fired, false); - ws.close(); - done(); - }, 5000); - })); - test('i/notifications にミュートしているスレッドの通知が含まれない', async () => { const bobNote = await post(bob, { text: '@alice @carol root note' }); const aliceReply = await post(alice, { replyId: bobNote.id, text: '@bob @carol child note' }); diff --git a/packages/backend/test/e2e/users.ts b/packages/backend/test/e2e/users.ts index 822ca14ae6..f82d4c6a30 100644 --- a/packages/backend/test/e2e/users.ts +++ b/packages/backend/test/e2e/users.ts @@ -15,7 +15,7 @@ describe('ユーザー', () => { // エンティティとしてのユーザーを主眼においたテストを記述する // (Userを返すエンドポイントとUserエンティティを書き換えるエンドポイントをテストする) - const stripUndefined = (orig: T): Partial => { + const stripUndefined = (orig: T): Partial => { return Object.entries({ ...orig }) .filter(([, value]) => value !== undefined) .reduce((obj: Partial, [key, value]) => { @@ -83,6 +83,7 @@ describe('ユーザー', () => { publicReactions: user.publicReactions, followingVisibility: user.followingVisibility, followersVisibility: user.followersVisibility, + chatScope: user.chatScope, roles: user.roles, memo: user.memo, }); @@ -132,6 +133,7 @@ describe('ユーザー', () => { hasUnreadAnnouncement: user.hasUnreadAnnouncement, hasUnreadAntenna: user.hasUnreadAntenna, hasUnreadChannel: user.hasUnreadChannel, + hasUnreadChatMessages: user.hasUnreadChatMessages, hasUnreadNotification: user.hasUnreadNotification, unreadNotificationsCount: user.unreadNotificationsCount, hasPendingReceivedFollowRequest: user.hasPendingReceivedFollowRequest, @@ -343,6 +345,7 @@ describe('ユーザー', () => { assert.strictEqual(response.publicReactions, true); assert.strictEqual(response.followingVisibility, 'public'); assert.strictEqual(response.followersVisibility, 'public'); + assert.strictEqual(response.chatScope, 'mutual'); assert.deepStrictEqual(response.roles, []); assert.strictEqual(response.memo, null); @@ -369,6 +372,7 @@ describe('ユーザー', () => { assert.strictEqual(response.hasUnreadAnnouncement, false); assert.strictEqual(response.hasUnreadAntenna, false); assert.strictEqual(response.hasUnreadChannel, false); + assert.strictEqual(response.hasUnreadChatMessages, false); assert.strictEqual(response.hasUnreadNotification, false); assert.strictEqual(response.unreadNotificationsCount, 0); assert.strictEqual(response.hasPendingReceivedFollowRequest, false); diff --git a/packages/backend/test/unit/entities/UserEntityService.ts b/packages/backend/test/unit/entities/UserEntityService.ts index e4f42809f8..6b7eedff55 100644 --- a/packages/backend/test/unit/entities/UserEntityService.ts +++ b/packages/backend/test/unit/entities/UserEntityService.ts @@ -50,6 +50,7 @@ import { AccountMoveService } from '@/core/AccountMoveService.js'; import { ReactionService } from '@/core/ReactionService.js'; import { NotificationService } from '@/core/NotificationService.js'; import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js'; +import { ChatService } from '@/core/ChatService.js'; process.env.NODE_ENV = 'test'; @@ -172,6 +173,7 @@ describe('UserEntityService', () => { ReactionService, ReactionsBufferingService, NotificationService, + ChatService, ]; app = await Test.createTestingModule({ diff --git a/packages/frontend-embed/src/components/EmPagination.vue b/packages/frontend-embed/src/components/EmPagination.vue index 5d5317a912..4cf156ba23 100644 --- a/packages/frontend-embed/src/components/EmPagination.vue +++ b/packages/frontend-embed/src/components/EmPagination.vue @@ -34,10 +34,11 @@ SPDX-License-Identifier: AGPL-3.0-only @@ -47,6 +50,10 @@ withDefaults(defineProps<{ min-height: calc(var(--fukidashi-radius) * 2); padding-top: calc(var(--fukidashi-radius) * .13); + &.accented { + --fukidashi-bg: var(--MI_THEME-accent); + } + &.shadow { filter: drop-shadow(0 4px 32px var(--MI_THEME-shadow)); } @@ -77,7 +84,7 @@ withDefaults(defineProps<{ .content { position: relative; - padding: 8px 12px; + padding: 10px 14px; } .tail { diff --git a/packages/frontend/src/components/MkMediaList.vue b/packages/frontend/src/components/MkMediaList.vue index ae15776041..4a1100c324 100644 --- a/packages/frontend/src/components/MkMediaList.vue +++ b/packages/frontend/src/components/MkMediaList.vue @@ -227,7 +227,6 @@ defineExpose({ .container { position: relative; width: 100%; - margin-top: 4px; } .medias { diff --git a/packages/frontend/src/components/MkMenu.vue b/packages/frontend/src/components/MkMenu.vue index a84bd9b256..f2f36308ca 100644 --- a/packages/frontend/src/components/MkMenu.vue +++ b/packages/frontend/src/components/MkMenu.vue @@ -11,6 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only [$style.center]: align === 'center', [$style.big]: big, [$style.asDrawer]: asDrawer, + [$style.widthSpecified]: width != null, }" @focusin.passive.stop="() => {}" > @@ -29,15 +30,19 @@ SPDX-License-Identifier: AGPL-3.0-only > + {{ i18n.ts.none }} @@ -438,6 +473,12 @@ onBeforeUnmount(() => { } } + &:not(.widthSpecified) { + > .menu { + max-width: 400px; + } + } + &.big:not(.asDrawer) { > .menu { min-width: 230px; @@ -607,10 +648,19 @@ onBeforeUnmount(() => { .item_content_text { max-width: calc(100vw - 4rem); +} + +.item_content_text_title { text-overflow: ellipsis; overflow: hidden; } +.item_content_text_caption { + text-wrap: auto; + font-size: 85%; + opacity: 0.7; +} + .switchButton { margin-left: -2px; --height: 1.35em; diff --git a/packages/frontend/src/components/MkPagination.vue b/packages/frontend/src/components/MkPagination.vue index ab8bda403b..a729619180 100644 --- a/packages/frontend/src/components/MkPagination.vue +++ b/packages/frontend/src/components/MkPagination.vue @@ -24,16 +24,16 @@ SPDX-License-Identifier: AGPL-3.0-only -

-
- +
+
+ {{ i18n.ts.loadMore }}
-
- +
+ {{ i18n.ts.loadMore }} @@ -46,7 +46,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { computed, isRef, nextTick, onActivated, onBeforeMount, onBeforeUnmount, onDeactivated, ref, useTemplateRef, watch } from 'vue'; import * as Misskey from 'misskey-js'; import { useDocumentVisibility } from '@@/js/use-document-visibility.js'; -import { onScrollTop, isTopVisible, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scroll, isBottomVisible } from '@@/js/scroll.js'; +import { onScrollTop, isHeadVisible, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scroll, isTailVisible } from '@@/js/scroll.js'; import type { ComputedRef } from 'vue'; import type { MisskeyEntity } from '@/types/date-separated-list.js'; import { misskeyApi } from '@/utility/misskey-api.js'; @@ -74,8 +74,6 @@ export type Paging reversed?: boolean; offsetMode?: boolean; - - pageEl?: HTMLElement; }; type MisskeyEntityMap = Map; @@ -141,8 +139,7 @@ const { enableInfiniteScroll, } = prefer.r; -const contentEl = computed(() => props.pagination.pageEl ?? rootEl.value); -const scrollableElement = computed(() => contentEl.value ? getScrollContainer(contentEl.value) : window.document.body); +const scrollableElement = computed(() => rootEl.value ? getScrollContainer(rootEl.value) : window.document.body); const visibility = useDocumentVisibility(); @@ -173,13 +170,13 @@ watch(rootEl, () => { }); }); -watch([backed, contentEl], () => { +watch([backed, rootEl], () => { if (!backed.value) { - if (!contentEl.value) return; + if (!rootEl.value) return; scrollRemove.value = props.pagination.reversed - ? onScrollBottom(contentEl.value, executeQueue, TOLERANCE) - : onScrollTop(contentEl.value, (topVisible) => { if (topVisible) executeQueue(); }, TOLERANCE); + ? onScrollBottom(rootEl.value, executeQueue, TOLERANCE) + : onScrollTop(rootEl.value, (topVisible) => { if (topVisible) executeQueue(); }, TOLERANCE); } else { if (scrollRemove.value) scrollRemove.value(); scrollRemove.value = null; @@ -349,7 +346,7 @@ const appearFetchMoreAhead = async (): Promise => { fetchMoreAppearTimeout(); }; -const isTop = (): boolean => isBackTop.value || (props.pagination.reversed ? isBottomVisible : isTopVisible)(contentEl.value!, TOLERANCE); +const isHead = (): boolean => isBackTop.value || (props.pagination.reversed ? isTailVisible : isHeadVisible)(rootEl.value!, TOLERANCE); watch(visibility, () => { if (visibility.value === 'hidden') { @@ -364,7 +361,7 @@ watch(visibility, () => { timerForSetPause = null; } else { isPausingUpdate = false; - if (isTop()) { + if (isHead()) { executeQueue(); } } @@ -376,16 +373,18 @@ watch(visibility, () => { * ストリーミングから降ってきたアイテムはこれで追加する * @param item アイテム */ -const prepend = (item: MisskeyEntity): void => { +function prepend(item: MisskeyEntity): void { if (items.value.size === 0) { items.value.set(item.id, item); fetching.value = false; return; } - if (isTop() && !isPausingUpdate) unshiftItems([item]); + console.log(isHead(), isPausingUpdate); + + if (isHead() && !isPausingUpdate) unshiftItems([item]); else prependQueue(item); -}; +} /** * 新着アイテムをitemsの先頭に追加し、displayLimitを適用する @@ -447,7 +446,7 @@ onDeactivated(() => { }); function toBottom() { - scrollToBottom(contentEl.value!); + scrollToBottom(rootEl.value!); } onBeforeMount(() => { diff --git a/packages/frontend/src/components/MkPolkadots.vue b/packages/frontend/src/components/MkPolkadots.vue new file mode 100644 index 0000000000..285c4d0b79 --- /dev/null +++ b/packages/frontend/src/components/MkPolkadots.vue @@ -0,0 +1,40 @@ + + + + + + + diff --git a/packages/frontend/src/components/MkUrlPreview.vue b/packages/frontend/src/components/MkUrlPreview.vue index f20aee0ce3..20dab6f028 100644 --- a/packages/frontend/src/components/MkUrlPreview.vue +++ b/packages/frontend/src/components/MkUrlPreview.vue @@ -246,6 +246,7 @@ onUnmounted(() => { box-shadow: 0 0 0 1px var(--MI_THEME-divider); border-radius: 8px; overflow: clip; + text-align: left; &:hover { text-decoration: none; diff --git a/packages/frontend/src/local-storage.ts b/packages/frontend/src/local-storage.ts index 099339fbee..f6d6bbf0fb 100644 --- a/packages/frontend/src/local-storage.ts +++ b/packages/frontend/src/local-storage.ts @@ -28,7 +28,7 @@ export type Keys = ( 'theme' | 'themeId' | 'customCss' | - 'message_drafts' | + 'chatMessageDrafts' | 'scratchpad' | 'debug' | 'preferences' | diff --git a/packages/frontend/src/navbar.ts b/packages/frontend/src/navbar.ts index d478ece641..894df83721 100644 --- a/packages/frontend/src/navbar.ts +++ b/packages/frontend/src/navbar.ts @@ -4,6 +4,7 @@ */ import { computed, reactive } from 'vue'; +import { ui } from '@@/js/config.js'; import { clearCache } from './utility/clear-cache.js'; import { $i } from '@/i.js'; import { miLocalStorage } from '@/local-storage.js'; @@ -11,7 +12,6 @@ import { openInstanceMenu, openToolsMenu } from '@/ui/_common_/common.js'; import { lookup } from '@/utility/lookup.js'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; -import { ui } from '@@/js/config.js'; import { unisonReload } from '@/utility/unison-reload.js'; export const navbarItemDef = reactive({ @@ -110,6 +110,12 @@ export const navbarItemDef = reactive({ icon: 'ti ti-device-tv', to: '/channels', }, + chat: { + title: i18n.ts.chat, + icon: 'ti ti-message', + to: '/chat', + indicated: computed(() => $i != null && $i.hasUnreadChatMessages), + }, achievements: { title: i18n.ts.achievements, icon: 'ti ti-medal', diff --git a/packages/frontend/src/pages/admin/roles.editor.vue b/packages/frontend/src/pages/admin/roles.editor.vue index 4e9f4edb70..d1e823215a 100644 --- a/packages/frontend/src/pages/admin/roles.editor.vue +++ b/packages/frontend/src/pages/admin/roles.editor.vue @@ -160,6 +160,26 @@ SPDX-License-Identifier: AGPL-3.0-only
+ + + +
+ + + + + + + + + +
+
+