diff options
| author | Hazelnoot <acomputerdog@gmail.com> | 2025-03-25 16:14:53 -0400 |
|---|---|---|
| committer | Hazelnoot <acomputerdog@gmail.com> | 2025-03-25 16:14:53 -0400 |
| commit | d8908ef2d8fa84d8e0fc1d30ab90a600a3d88054 (patch) | |
| tree | 0c8d3e0385ce7021c7187ef8b608f1abd87496e5 /packages/backend/src/server/api | |
| parent | merge: enhance: Update de-DE.yml (!949) (diff) | |
| parent | enhance(frontend): 設定の移行を手動でトリガーできるように (diff) | |
| download | sharkey-d8908ef2d8fa84d8e0fc1d30ab90a600a3d88054.tar.gz sharkey-d8908ef2d8fa84d8e0fc1d30ab90a600a3d88054.tar.bz2 sharkey-d8908ef2d8fa84d8e0fc1d30ab90a600a3d88054.zip | |
merge upstream
Diffstat (limited to 'packages/backend/src/server/api')
82 files changed, 1791 insertions, 191 deletions
diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts index 5ce358d68f..b22a8c1837 100644 --- a/packages/backend/src/server/api/ApiCallService.ts +++ b/packages/backend/src/server/api/ApiCallService.ts @@ -369,7 +369,7 @@ export class ApiCallService implements OnApplicationShutdown { } } - if ((ep.meta.requireModerator || ep.meta.requireAdmin) && !user!.isRoot) { + if ((ep.meta.requireModerator || ep.meta.requireAdmin) && (this.meta.rootUserId !== user!.id)) { const myRoles = await this.roleService.getUserRoles(user!.id); if (ep.meta.requireModerator && !myRoles.some(r => r.isModerator || r.isAdministrator)) { throw new ApiError({ @@ -389,10 +389,10 @@ export class ApiCallService implements OnApplicationShutdown { } } - if (ep.meta.requireRolePolicy != null && !user!.isRoot) { + if (ep.meta.requiredRolePolicy != null && (this.meta.rootUserId !== user!.id)) { const myRoles = await this.roleService.getUserRoles(user!.id); const policies = await this.roleService.getUserPolicies(user!.id); - if (!policies[ep.meta.requireRolePolicy] && !myRoles.some(r => r.isAdministrator)) { + if (!policies[ep.meta.requiredRolePolicy] && !myRoles.some(r => r.isAdministrator)) { throw new ApiError({ message: 'You are not assigned to a required role.', code: 'ROLE_PERMISSION_DENIED', diff --git a/packages/backend/src/server/api/ApiServerService.ts b/packages/backend/src/server/api/ApiServerService.ts index 0d77309537..12459d5698 100644 --- a/packages/backend/src/server/api/ApiServerService.ts +++ b/packages/backend/src/server/api/ApiServerService.ts @@ -6,7 +6,6 @@ import { Inject, Injectable } from '@nestjs/common'; import cors from '@fastify/cors'; import multipart from '@fastify/multipart'; -import fastifyCookie from '@fastify/cookie'; import { ModuleRef } from '@nestjs/core'; import { AuthenticationResponseJSON } from '@simplewebauthn/types'; import type { Config } from '@/config.js'; @@ -57,8 +56,6 @@ export class ApiServerService { }, }); - fastify.register(fastifyCookie, {}); - // Prevent cache fastify.addHook('onRequest', (request, reply, done) => { reply.header('Cache-Control', 'private, max-age=0, must-revalidate'); 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<MiLocalUser | null>); diff --git a/packages/backend/src/server/api/StreamingApiServerService.ts b/packages/backend/src/server/api/StreamingApiServerService.ts index 0954744f81..9e6b1d8761 100644 --- a/packages/backend/src/server/api/StreamingApiServerService.ts +++ b/packages/backend/src/server/api/StreamingApiServerService.ts @@ -11,7 +11,6 @@ import proxyAddr from 'proxy-addr'; import ms from 'ms'; 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'; @@ -42,7 +41,6 @@ export class StreamingApiServerService { private usersRepository: UsersRepository, private cacheService: CacheService, - private noteReadService: NoteReadService, private authenticateService: AuthenticateService, private channelsService: ChannelsService, private notificationService: NotificationService, @@ -154,7 +152,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 a641a14448..562f498cbd 100644 --- a/packages/backend/src/server/api/endpoint-list.ts +++ b/packages/backend/src/server/api/endpoint-list.ts @@ -109,6 +109,7 @@ export * as 'admin/unsilence-user' from './endpoints/admin/unsilence-user.js'; export * as 'admin/unsuspend-user' from './endpoints/admin/unsuspend-user.js'; export * as 'admin/update-abuse-user-report' from './endpoints/admin/update-abuse-user-report.js'; export * as 'admin/update-meta' from './endpoints/admin/update-meta.js'; +export * as 'admin/update-proxy-account' from './endpoints/admin/update-proxy-account.js'; export * as 'admin/update-user-note' from './endpoints/admin/update-user-note.js'; export * as 'announcements' from './endpoints/announcements.js'; export * as 'announcements/show' from './endpoints/announcements/show.js'; @@ -273,7 +274,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'; @@ -418,4 +418,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.ts b/packages/backend/src/server/api/endpoints.ts index fd6b9bb14b..0ba041c536 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -40,7 +40,7 @@ interface IEndpointMetaBase { */ readonly requireAdmin?: boolean; - readonly requireRolePolicy?: KeyOf<'RolePolicies'>; + readonly requiredRolePolicy?: KeyOf<'RolePolicies'>; /** * 引っ越し済みのユーザーによるリクエストを禁止するか @@ -100,7 +100,7 @@ export type IEndpointMeta = (Omit<IEndpointMetaBase, 'requireCrential' | 'requir }) | (Omit<IEndpointMetaBase, 'requireAdmin' | 'kind'> & { requireAdmin: true, kind: (typeof permissions)[number], -}) +}); export interface IEndpoint { name: string; diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts index 1a47f56bc6..88490800cf 100644 --- a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts @@ -5,10 +5,9 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UsersRepository } from '@/models/_.js'; +import type { MiMeta, UsersRepository } from '@/models/_.js'; import { SignupService } from '@/core/SignupService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import { InstanceActorService } from '@/core/InstanceActorService.js'; import { localUsernameSchema, passwordSchema } from '@/models/User.js'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; @@ -90,20 +89,21 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- @Inject(DI.config) private config: Config, + @Inject(DI.meta) + private serverSettings: MiMeta, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, private roleService: RoleService, private userEntityService: UserEntityService, private signupService: SignupService, - private instanceActorService: InstanceActorService, private readonly moderationLogService: ModerationLogService, ) { super(meta, paramDef, async (ps, _me, token) => { const me = _me ? await this.usersRepository.findOneByOrFail({ id: _me.id }) : null; - const realUsers = await this.instanceActorService.realLocalUsersPresent(); - if (!realUsers && me == null && token == null) { + if (this.serverSettings.rootUserId == null && me == null && token == null) { // 初回セットアップの場合 if (this.config.setupPassword != null) { // 初期パスワードが設定されている場合 @@ -127,7 +127,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- } // Anonymous access is only allowed for initial instance setup (this check may be redundant) - if (!me && realUsers) { + if (!me && this.serverSettings.rootUserId != null) { throw new ApiError(meta.errors.noCredential); } } diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts b/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts index ece1984cff..d04f52dd64 100644 --- a/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts +++ b/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts @@ -42,10 +42,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- throw new Error('user not found'); } - if (user.isRoot) { - throw new Error('cannot delete a root account'); - } - await this.deleteAccoountService.deleteAccount(user, me); }); } diff --git a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts index 87d80cbe80..0121c302ac 100644 --- a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts @@ -12,7 +12,7 @@ export const meta = { tags: ['admin'], requireCredential: true, - requireRolePolicy: 'canManageAvatarDecorations', + requiredRolePolicy: 'canManageAvatarDecorations', kind: 'write:admin:avatar-decorations', res: { diff --git a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/delete.ts b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/delete.ts index 3a5673d99d..13660d0b8c 100644 --- a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/delete.ts +++ b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/delete.ts @@ -13,7 +13,7 @@ export const meta = { tags: ['admin'], requireCredential: true, - requireRolePolicy: 'canManageAvatarDecorations', + requiredRolePolicy: 'canManageAvatarDecorations', kind: 'write:admin:avatar-decorations', errors: { }, diff --git a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts index d785f085ac..d4d9a7235b 100644 --- a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts @@ -13,7 +13,7 @@ export const meta = { tags: ['admin'], requireCredential: true, - requireRolePolicy: 'canManageAvatarDecorations', + requiredRolePolicy: 'canManageAvatarDecorations', kind: 'read:admin:avatar-decorations', res: { diff --git a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/update.ts b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/update.ts index 34b3b5a11f..22476a6888 100644 --- a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/update.ts @@ -13,7 +13,7 @@ export const meta = { tags: ['admin'], requireCredential: true, - requireRolePolicy: 'canManageAvatarDecorations', + requiredRolePolicy: 'canManageAvatarDecorations', kind: 'write:admin:avatar-decorations', errors: { diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts index 795b579041..56db393996 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts @@ -12,7 +12,7 @@ export const meta = { tags: ['admin'], requireCredential: true, - requireRolePolicy: 'canManageCustomEmojis', + requiredRolePolicy: 'canManageCustomEmojis', kind: 'write:admin:emoji', } as const; diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts index 1c5316a002..5ef8307df0 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts @@ -16,7 +16,7 @@ export const meta = { tags: ['admin'], requireCredential: true, - requireRolePolicy: 'canManageCustomEmojis', + requiredRolePolicy: 'canManageCustomEmojis', kind: 'write:admin:emoji', errors: { diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts index 07ffa0b1c7..cbf78ada3e 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts @@ -17,7 +17,7 @@ export const meta = { tags: ['admin'], requireCredential: true, - requireRolePolicy: 'canManageCustomEmojis', + requiredRolePolicy: 'canManageCustomEmojis', kind: 'write:admin:emoji', errors: { diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts index cec9f700c3..7993edcc07 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts @@ -11,7 +11,7 @@ export const meta = { tags: ['admin'], requireCredential: true, - requireRolePolicy: 'canManageCustomEmojis', + requiredRolePolicy: 'canManageCustomEmojis', kind: 'write:admin:emoji', } as const; diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts b/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts index 50c45b6ac5..87ed3f5f18 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts @@ -11,7 +11,7 @@ export const meta = { tags: ['admin'], requireCredential: true, - requireRolePolicy: 'canManageCustomEmojis', + requiredRolePolicy: 'canManageCustomEmojis', kind: 'write:admin:emoji', errors: { diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/import-zip.ts b/packages/backend/src/server/api/endpoints/admin/emoji/import-zip.ts index ee7706f31a..7b544bee8d 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/import-zip.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/import-zip.ts @@ -13,7 +13,7 @@ import { DI } from '@/di-symbols.js'; export const meta = { secure: true, requireCredential: true, - requireRolePolicy: 'canManageCustomEmojis', + requiredRolePolicy: 'canManageCustomEmojis', } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts index 1182918ea2..7982c1f0bd 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts @@ -16,7 +16,7 @@ export const meta = { tags: ['admin'], requireCredential: true, - requireRolePolicy: 'canManageCustomEmojis', + requiredRolePolicy: 'canManageCustomEmojis', kind: 'read:admin:emoji', res: { diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts index f35a6667f4..b1b8e63d2f 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts @@ -16,7 +16,7 @@ export const meta = { tags: ['admin'], requireCredential: true, - requireRolePolicy: 'canManageCustomEmojis', + requiredRolePolicy: 'canManageCustomEmojis', kind: 'read:admin:emoji', res: { diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts index 066eb1c7d9..2d8867b9fd 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts @@ -12,7 +12,7 @@ export const meta = { tags: ['admin'], requireCredential: true, - requireRolePolicy: 'canManageCustomEmojis', + requiredRolePolicy: 'canManageCustomEmojis', kind: 'write:admin:emoji', } as const; diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts index 8980ef0c86..8086af8ed5 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts @@ -12,7 +12,7 @@ export const meta = { tags: ['admin'], requireCredential: true, - requireRolePolicy: 'canManageCustomEmojis', + requiredRolePolicy: 'canManageCustomEmojis', kind: 'write:admin:emoji', } as const; diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts index 2510349210..5d3b39d7da 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts @@ -12,7 +12,7 @@ export const meta = { tags: ['admin'], requireCredential: true, - requireRolePolicy: 'canManageCustomEmojis', + requiredRolePolicy: 'canManageCustomEmojis', kind: 'write:admin:emoji', } as const; diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/set-license-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/set-license-bulk.ts index a0205ae24a..4b916508a7 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/set-license-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/set-license-bulk.ts @@ -12,7 +12,7 @@ export const meta = { tags: ['admin'], requireCredential: true, - requireRolePolicy: 'canManageCustomEmojis', + requiredRolePolicy: 'canManageCustomEmojis', kind: 'write:admin:emoji', } as const; diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts index fd6db9c4ab..492122422c 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts @@ -14,7 +14,7 @@ export const meta = { tags: ['admin'], requireCredential: true, - requireRolePolicy: 'canManageCustomEmojis', + requiredRolePolicy: 'canManageCustomEmojis', kind: 'write:admin:emoji', errors: { diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index d3f24e07bb..d05f6e3c02 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -10,6 +10,7 @@ import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; import { DEFAULT_POLICIES } from '@/core/RoleService.js'; import { instanceUnsignedFetchOptions } from '@/const.js'; +import { SystemAccountService } from '@/core/SystemAccountService.js'; export const meta = { tags: ['meta'], @@ -86,6 +87,10 @@ export const meta = { type: 'boolean', optional: false, nullable: false, }, + googleAnalyticsMeasurementId: { + type: 'string', + optional: false, nullable: true, + }, swPublickey: { type: 'string', optional: false, nullable: true, @@ -265,7 +270,7 @@ export const meta = { }, proxyAccountId: { type: 'string', - optional: false, nullable: true, + optional: false, nullable: false, format: 'id', }, email: { @@ -580,6 +585,7 @@ export const meta = { }, federation: { type: 'string', + enum: ['all', 'specified', 'none'], optional: false, nullable: false, }, federationHosts: { @@ -617,10 +623,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private config: Config, private metaService: MetaService, + private systemAccountService: SystemAccountService, ) { super(meta, paramDef, async () => { const instance = await this.metaService.fetch(true); + const proxy = await this.systemAccountService.fetch('proxy'); + return { maintainerName: instance.maintainerName, maintainerEmail: instance.maintainerEmail, @@ -652,6 +661,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- enableFC: instance.enableFC, fcSiteKey: instance.fcSiteKey, enableTestcaptcha: instance.enableTestcaptcha, + googleAnalyticsMeasurementId: instance.googleAnalyticsMeasurementId, swPublickey: instance.swPublicKey, themeColor: instance.themeColor, mascotImageUrl: instance.mascotImageUrl, @@ -693,7 +703,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- setSensitiveFlagAutomatically: instance.setSensitiveFlagAutomatically, enableSensitiveMediaDetectionForVideos: instance.enableSensitiveMediaDetectionForVideos, enableBotTrending: instance.enableBotTrending, - proxyAccountId: instance.proxyAccountId, + proxyAccountId: proxy.id, email: instance.email, smtpSecure: instance.smtpSecure, smtpHost: instance.smtpHost, diff --git a/packages/backend/src/server/api/endpoints/admin/reset-password.ts b/packages/backend/src/server/api/endpoints/admin/reset-password.ts index e4bb545f5d..b99f420928 100644 --- a/packages/backend/src/server/api/endpoints/admin/reset-password.ts +++ b/packages/backend/src/server/api/endpoints/admin/reset-password.ts @@ -7,7 +7,7 @@ import { Inject, Injectable } from '@nestjs/common'; //import bcrypt from 'bcryptjs'; import * as argon2 from 'argon2'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UsersRepository, UserProfilesRepository } from '@/models/_.js'; +import type { UsersRepository, UserProfilesRepository, MiMeta } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { secureRndstr } from '@/misc/secure-rndstr.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; @@ -45,6 +45,9 @@ export const paramDef = { @Injectable() export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.meta) + private serverSettings: MiMeta, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -60,7 +63,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- throw new Error('user not found'); } - if (user.isRoot) { + if (this.serverSettings.rootUserId === user.id) { throw new Error('cannot reset password of root'); } diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index 33d4bbd00f..86dbd44e20 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -91,12 +91,12 @@ export const paramDef = { fcSiteKey: { type: 'string', nullable: true }, fcSecretKey: { type: 'string', nullable: true }, enableTestcaptcha: { type: 'boolean' }, + googleAnalyticsMeasurementId: { type: 'string', nullable: true }, sensitiveMediaDetection: { type: 'string', enum: ['none', 'all', 'local', 'remote'] }, sensitiveMediaDetectionSensitivity: { type: 'string', enum: ['medium', 'low', 'high', 'veryLow', 'veryHigh'] }, setSensitiveFlagAutomatically: { type: 'boolean' }, enableSensitiveMediaDetectionForVideos: { type: 'boolean' }, enableBotTrending: { type: 'boolean' }, - proxyAccountId: { type: 'string', format: 'misskey:id', nullable: true }, maintainerName: { type: 'string', nullable: true }, maintainerEmail: { type: 'string', nullable: true }, langs: { @@ -130,7 +130,7 @@ export const paramDef = { useObjectStorage: { type: 'boolean' }, objectStorageBaseUrl: { type: 'string', nullable: true }, objectStorageBucket: { type: 'string', nullable: true }, - objectStoragePrefix: { type: 'string', nullable: true }, + objectStoragePrefix: { type: 'string', pattern: /^[a-zA-Z0-9-._]*$/.source, nullable: true }, objectStorageEndpoint: { type: 'string', nullable: true }, objectStorageRegion: { type: 'string', nullable: true }, objectStoragePort: { type: 'integer', nullable: true }, @@ -413,6 +413,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- set.enableTestcaptcha = ps.enableTestcaptcha; } + if (ps.googleAnalyticsMeasurementId !== undefined) { + // 空文字列をnullにしたいので??は使わない + set.googleAnalyticsMeasurementId = ps.googleAnalyticsMeasurementId || null; + } + if (ps.fcSiteKey !== undefined) { set.fcSiteKey = ps.fcSiteKey; } @@ -425,10 +430,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- set.enableBotTrending = ps.enableBotTrending; } - if (ps.proxyAccountId !== undefined) { - set.proxyAccountId = ps.proxyAccountId; - } - if (ps.maintainerName !== undefined) { set.maintainerName = ps.maintainerName; } diff --git a/packages/backend/src/server/api/endpoints/admin/update-proxy-account.ts b/packages/backend/src/server/api/endpoints/admin/update-proxy-account.ts new file mode 100644 index 0000000000..6c9612c71a --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/update-proxy-account.ts @@ -0,0 +1,62 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { + descriptionSchema, +} from '@/models/User.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; +import { SystemAccountService } from '@/core/SystemAccountService.js'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireModerator: true, + kind: 'write:admin:account', + + res: { + type: 'object', + nullable: false, optional: false, + ref: 'UserDetailed', + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + description: { ...descriptionSchema, nullable: true }, + }, +} as const; + +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export + constructor( + private userEntityService: UserEntityService, + private moderationLogService: ModerationLogService, + private systemAccountService: SystemAccountService, + ) { + super(meta, paramDef, async (ps, me) => { + const proxy = await this.systemAccountService.updateCorrespondingUserProfile('proxy', { + description: ps.description, + }); + + const updated = await this.userEntityService.pack(proxy.id, proxy, { + schema: 'MeDetailed', + }); + + if (ps.description !== undefined) { + this.moderationLogService.log(me, 'updateProxyAccountDescription', { + before: null, //TODO + after: ps.description, + }); + } + + return updated; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/antennas/notes.ts b/packages/backend/src/server/api/endpoints/antennas/notes.ts index be4cf1e0ca..8ad2315209 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'; @@ -65,9 +64,6 @@ export const paramDef = { @Injectable() export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.redisForTimelines) - private redisForTimelines: Redis.Redis, - @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -77,7 +73,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private idService: IdService, private noteEntityService: NoteEntityService, private queryService: QueryService, - private noteReadService: NoteReadService, private fanoutTimelineService: FanoutTimelineService, private globalEventService: GlobalEventService, ) { @@ -130,8 +125,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // 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/ap/show.ts b/packages/backend/src/server/api/endpoints/ap/show.ts index 22bec8ef95..8cf7747d5f 100644 --- a/packages/backend/src/server/api/endpoints/ap/show.ts +++ b/packages/backend/src/server/api/endpoints/ap/show.ts @@ -21,6 +21,7 @@ import { ApRequestService } from '@/core/activitypub/ApRequestService.js'; import { InstanceActorService } from '@/core/InstanceActorService.js'; import { ApiError } from '../../error.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; +import { FetchAllowSoftFailMask } from '@/core/activitypub/misc/check-against-url.js'; export const meta = { tags: ['federation'], @@ -55,11 +56,6 @@ export const meta = { code: 'RESPONSE_INVALID', id: '70193c39-54f3-4813-82f0-70a680f7495b', }, - responseInvalidIdHostNotMatch: { - message: 'Requested URI and response URI host does not match.', - code: 'RESPONSE_INVALID_ID_HOST_NOT_MATCH', - id: 'a2c9c61a-cb72-43ab-a964-3ca5fddb410a', - }, noSuchObject: { message: 'No such object.', code: 'NO_SUCH_OBJECT', @@ -165,7 +161,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- // リモートから一旦オブジェクトフェッチ const resolver = this.apResolverService.createResolver(); - const object = await resolver.resolve(uri).catch((err) => { + // allow ap/show exclusively to lookup URLs that are cross-origin or non-canonical (like https://alice.example.com/@bob@bob.example.com -> https://bob.example.com/@bob) + const object = await resolver.resolve(uri, FetchAllowSoftFailMask.CrossOrigin | FetchAllowSoftFailMask.NonCanonicalId).catch((err) => { if (err instanceof IdentifiableError) { switch (err.id) { // resolve @@ -177,10 +174,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- case '09d79f9e-64f1-4316-9cfa-e75c4d091574': throw new ApiError(meta.errors.federationNotAllowed); case '72180409-793c-4973-868e-5a118eb5519b': - case 'ad2dc287-75c1-44c4-839d-3d2e64576675': throw new ApiError(meta.errors.responseInvalid); - case 'fd93c2fa-69a8-440f-880b-bf178e0ec877': - throw new ApiError(meta.errors.responseInvalidIdHostNotMatch); // resolveLocal case '02b40cd0-fa92-4b0c-acc9-fb2ada952ab8': 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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // 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/clips/create.ts b/packages/backend/src/server/api/endpoints/clips/create.ts index c2f72ad9ae..e3b8f33f97 100644 --- a/packages/backend/src/server/api/endpoints/clips/create.ts +++ b/packages/backend/src/server/api/endpoints/clips/create.ts @@ -45,7 +45,7 @@ export const paramDef = { properties: { name: { type: 'string', minLength: 1, maxLength: 100 }, isPublic: { type: 'boolean', default: false }, - description: { type: 'string', nullable: true, minLength: 1, maxLength: 2048 }, + description: { type: 'string', nullable: true, maxLength: 2048 }, }, required: ['name'], } as const; @@ -59,7 +59,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- super(meta, paramDef, async (ps, me) => { let clip: MiClip; try { - clip = await this.clipService.create(me, ps.name, ps.isPublic, ps.description ?? null); + // 空文字列をnullにしたいので??は使わない + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + clip = await this.clipService.create(me, ps.name, ps.isPublic, ps.description || null); } catch (e) { if (e instanceof ClipService.TooManyClipsError) { throw new ApiError(meta.errors.tooManyClips); diff --git a/packages/backend/src/server/api/endpoints/clips/update.ts b/packages/backend/src/server/api/endpoints/clips/update.ts index b776f1357d..beab427b69 100644 --- a/packages/backend/src/server/api/endpoints/clips/update.ts +++ b/packages/backend/src/server/api/endpoints/clips/update.ts @@ -45,7 +45,7 @@ export const paramDef = { clipId: { type: 'string', format: 'misskey:id' }, name: { type: 'string', minLength: 1, maxLength: 100 }, isPublic: { type: 'boolean' }, - description: { type: 'string', nullable: true, minLength: 1, maxLength: 2048 }, + description: { type: 'string', nullable: true, maxLength: 2048 }, }, required: ['clipId'], } as const; @@ -59,7 +59,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- ) { super(meta, paramDef, async (ps, me) => { try { - await this.clipService.update(me, ps.clipId, ps.name, ps.isPublic, ps.description); + // 空文字列をnullにしたいので??は使わない + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + await this.clipService.update(me, ps.clipId, ps.name, ps.isPublic, ps.description || null); } catch (e) { if (e instanceof ClipService.NoSuchClipError) { throw new ApiError(meta.errors.noSuchClip); diff --git a/packages/backend/src/server/api/endpoints/following/invalidate.ts b/packages/backend/src/server/api/endpoints/following/invalidate.ts index 8935c2c2da..b45d21410b 100644 --- a/packages/backend/src/server/api/endpoints/following/invalidate.ts +++ b/packages/backend/src/server/api/endpoints/following/invalidate.ts @@ -96,7 +96,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- await this.userFollowingService.unfollow(follower, followee); - return await this.userEntityService.pack(followee.id, me); + return await this.userEntityService.pack(follower.id, me); }); } } diff --git a/packages/backend/src/server/api/endpoints/i/import-antennas.ts b/packages/backend/src/server/api/endpoints/i/import-antennas.ts index bdf6c065e8..ccec96ffbb 100644 --- a/packages/backend/src/server/api/endpoints/i/import-antennas.ts +++ b/packages/backend/src/server/api/endpoints/i/import-antennas.ts @@ -16,7 +16,7 @@ import { ApiError } from '../../error.js'; export const meta = { secure: true, requireCredential: true, - requireRolePolicy: 'canImportAntennas', + requiredRolePolicy: 'canImportAntennas', prohibitMoved: true, limit: { diff --git a/packages/backend/src/server/api/endpoints/i/import-blocking.ts b/packages/backend/src/server/api/endpoints/i/import-blocking.ts index d7bb6bcd22..2fa450558b 100644 --- a/packages/backend/src/server/api/endpoints/i/import-blocking.ts +++ b/packages/backend/src/server/api/endpoints/i/import-blocking.ts @@ -15,7 +15,7 @@ import { ApiError } from '../../error.js'; export const meta = { secure: true, requireCredential: true, - requireRolePolicy: 'canImportBlocking', + requiredRolePolicy: 'canImportBlocking', prohibitMoved: true, limit: { diff --git a/packages/backend/src/server/api/endpoints/i/import-following.ts b/packages/backend/src/server/api/endpoints/i/import-following.ts index e03192d8c6..9186fca162 100644 --- a/packages/backend/src/server/api/endpoints/i/import-following.ts +++ b/packages/backend/src/server/api/endpoints/i/import-following.ts @@ -15,7 +15,7 @@ import { ApiError } from '../../error.js'; export const meta = { secure: true, requireCredential: true, - requireRolePolicy: 'canImportFollowing', + requiredRolePolicy: 'canImportFollowing', prohibitMoved: true, limit: { duration: ms('1hour'), diff --git a/packages/backend/src/server/api/endpoints/i/import-muting.ts b/packages/backend/src/server/api/endpoints/i/import-muting.ts index 76b285bb7e..b6dbacd371 100644 --- a/packages/backend/src/server/api/endpoints/i/import-muting.ts +++ b/packages/backend/src/server/api/endpoints/i/import-muting.ts @@ -15,7 +15,7 @@ import { ApiError } from '../../error.js'; export const meta = { secure: true, requireCredential: true, - requireRolePolicy: 'canImportMuting', + requiredRolePolicy: 'canImportMuting', prohibitMoved: true, limit: { diff --git a/packages/backend/src/server/api/endpoints/i/import-user-lists.ts b/packages/backend/src/server/api/endpoints/i/import-user-lists.ts index 76ecfd082c..5de0a70bbb 100644 --- a/packages/backend/src/server/api/endpoints/i/import-user-lists.ts +++ b/packages/backend/src/server/api/endpoints/i/import-user-lists.ts @@ -15,7 +15,7 @@ import { ApiError } from '../../error.js'; export const meta = { secure: true, requireCredential: true, - requireRolePolicy: 'canImportUserLists', + requiredRolePolicy: 'canImportUserLists', prohibitMoved: true, limit: { duration: ms('1hour'), diff --git a/packages/backend/src/server/api/endpoints/i/move.ts b/packages/backend/src/server/api/endpoints/i/move.ts index 1bd641232c..7852b5a2e1 100644 --- a/packages/backend/src/server/api/endpoints/i/move.ts +++ b/packages/backend/src/server/api/endpoints/i/move.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -19,6 +19,8 @@ import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import * as Acct from '@/misc/acct.js'; +import { DI } from '@/di-symbols.js'; +import { MiMeta } from '@/models/_.js'; export const meta = { tags: ['users'], @@ -81,6 +83,9 @@ export const paramDef = { @Injectable() export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.meta) + private serverSettings: MiMeta, + private remoteUserResolveService: RemoteUserResolveService, private apiLoggerService: ApiLoggerService, private accountMoveService: AccountMoveService, @@ -92,7 +97,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- // check parameter if (!ps.moveToAccount) throw new ApiError(meta.errors.noSuchUser); // abort if user is the root - if (me.isRoot) throw new ApiError(meta.errors.rootForbidden); + if (this.serverSettings.rootUserId === me.id) throw new ApiError(meta.errors.rootForbidden); // abort if user has already moved if (me.movedToUri) throw new ApiError(meta.errors.alreadyMoved); 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 ad4577be58..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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // eslint- } groupedNotifications = groupedNotifications.slice(0, ps.limit); - const noteIds = groupedNotifications - .filter((notification): notification is FilterUnionByProperty<MiNotification, 'type', 'mention' | 'reply' | 'quote' | 'edited'> => ['mention', 'reply', 'quote', 'edited'].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 5e97b90f99..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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // eslint- this.notificationService.readAllNotification(me.id); } - const noteIds = notifications - .filter((notification): notification is FilterUnionByProperty<MiNotification, 'type', 'mention' | 'reply' | 'quote' | 'edited'> => ['mention', 'reply', 'quote', 'edited'].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 edf570fcc5..0000000000 --- a/packages/backend/src/server/api/endpoints/i/read-all-unread-notes.ts +++ /dev/null @@ -1,51 +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', - - // 2 calls per second - limit: { - duration: 1000, - max: 2, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: {}, - required: [], -} as const; - -@Injectable() -export default class extends Endpoint<typeof meta, typeof paramDef> { // 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/regenerate-token.ts b/packages/backend/src/server/api/endpoints/i/regenerate-token.ts index 38328bb7d4..c7599aada2 100644 --- a/packages/backend/src/server/api/endpoints/i/regenerate-token.ts +++ b/packages/backend/src/server/api/endpoints/i/regenerate-token.ts @@ -9,7 +9,7 @@ import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; 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'; @@ -57,7 +57,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- throw new Error('incorrect password'); } - const newToken = generateUserToken(); + const newToken = generateNativeUserToken(); await this.usersRepository.update(me.id, { token: newToken, diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index f1d201d081..8abe250f9f 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -215,6 +215,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, @@ -325,6 +326,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- if (ps.listenbrainz !== undefined) profileUpdates.listenbrainz = ps.listenbrainz; 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/invite/create.ts b/packages/backend/src/server/api/endpoints/invite/create.ts index d661eab364..f607a35515 100644 --- a/packages/backend/src/server/api/endpoints/invite/create.ts +++ b/packages/backend/src/server/api/endpoints/invite/create.ts @@ -18,7 +18,7 @@ export const meta = { tags: ['meta'], requireCredential: true, - requireRolePolicy: 'canInvite', + requiredRolePolicy: 'canInvite', kind: 'write:invite-codes', errors: { diff --git a/packages/backend/src/server/api/endpoints/invite/delete.ts b/packages/backend/src/server/api/endpoints/invite/delete.ts index 408200164c..d15d400e9b 100644 --- a/packages/backend/src/server/api/endpoints/invite/delete.ts +++ b/packages/backend/src/server/api/endpoints/invite/delete.ts @@ -14,7 +14,7 @@ export const meta = { tags: ['meta'], requireCredential: true, - requireRolePolicy: 'canInvite', + requiredRolePolicy: 'canInvite', kind: 'write:invite-codes', errors: { diff --git a/packages/backend/src/server/api/endpoints/invite/limit.ts b/packages/backend/src/server/api/endpoints/invite/limit.ts index 8d92c4957c..150f4de441 100644 --- a/packages/backend/src/server/api/endpoints/invite/limit.ts +++ b/packages/backend/src/server/api/endpoints/invite/limit.ts @@ -15,7 +15,7 @@ export const meta = { tags: ['meta'], requireCredential: true, - requireRolePolicy: 'canInvite', + requiredRolePolicy: 'canInvite', kind: 'read:invite-codes', res: { diff --git a/packages/backend/src/server/api/endpoints/invite/list.ts b/packages/backend/src/server/api/endpoints/invite/list.ts index b63b41edd3..12e3873304 100644 --- a/packages/backend/src/server/api/endpoints/invite/list.ts +++ b/packages/backend/src/server/api/endpoints/invite/list.ts @@ -14,7 +14,7 @@ export const meta = { tags: ['meta'], requireCredential: true, - requireRolePolicy: 'canInvite', + requiredRolePolicy: 'canInvite', kind: 'read:invite-codes', res: { diff --git a/packages/backend/src/server/api/endpoints/miauth/gen-token.ts b/packages/backend/src/server/api/endpoints/miauth/gen-token.ts index c42df7ca80..f962bd49f1 100644 --- a/packages/backend/src/server/api/endpoints/miauth/gen-token.ts +++ b/packages/backend/src/server/api/endpoints/miauth/gen-token.ts @@ -7,6 +7,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { AccessTokensRepository } from '@/models/_.js'; import { IdService } from '@/core/IdService.js'; +import { NotificationService } from '@/core/NotificationService.js'; import { secureRndstr } from '@/misc/secure-rndstr.js'; import { DI } from '@/di-symbols.js'; @@ -56,6 +57,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private accessTokensRepository: AccessTokensRepository, private idService: IdService, + private notificationService: NotificationService, ) { super(meta, paramDef, async (ps, me) => { // Generate access token @@ -77,6 +79,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- permission: ps.permission, }); + // アクセストークンが生成されたことを通知 + this.notificationService.createNotification(me.id, 'createToken', {}); + return { token: accessToken, }; diff --git a/packages/backend/src/server/api/endpoints/notes/mentions.ts b/packages/backend/src/server/api/endpoints/notes/mentions.ts index 38912421a4..30baa0edf8 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 = { @@ -58,7 +57,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private noteEntityService: NoteEntityService, private queryService: QueryService, - private noteReadService: NoteReadService, ) { super(meta, paramDef, async (ps, me) => { const followingQuery = this.followingsRepository.createQueryBuilder('following') @@ -95,8 +93,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // 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/endpoints/reset-db.ts b/packages/backend/src/server/api/endpoints/reset-db.ts index bf40e1e3e0..fe23160bb8 100644 --- a/packages/backend/src/server/api/endpoints/reset-db.ts +++ b/packages/backend/src/server/api/endpoints/reset-db.ts @@ -6,9 +6,12 @@ import { Inject, Injectable } from '@nestjs/common'; import { DataSource } from 'typeorm'; import * as Redis from 'ioredis'; +import { LoggerService } from '@/core/LoggerService.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; import { resetDb } from '@/misc/reset-db.js'; +import { MetaService } from '@/core/MetaService.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; export const meta = { tags: ['non-productive'], @@ -42,13 +45,27 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- @Inject(DI.redis) private redisClient: Redis.Redis, + + private loggerService: LoggerService, + private metaService: MetaService, + private globalEventService: GlobalEventService, ) { super(meta, paramDef, async (ps, me) => { if (process.env.NODE_ENV !== 'test') throw new Error('NODE_ENV is not a test'); - await redisClient.flushdb(); + const logger = this.loggerService.getLogger('reset-db'); + logger.info('---- Resetting database...'); + + await this.redisClient.flushdb(); await resetDb(this.db); + // DIコンテナで管理しているmetaのインスタンスには上記のリセット処理が届かないため、 + // 初期値を流して明示的にリフレッシュする + const meta = await this.metaService.fetch(true); + this.globalEventService.publishInternalEvent('metaUpdated', { after: meta }); + + logger.info('---- Database reset complete.'); + await new Promise(resolve => setTimeout(resolve, 1000)); }); } diff --git a/packages/backend/src/server/api/endpoints/v2/admin/emoji/list.ts b/packages/backend/src/server/api/endpoints/v2/admin/emoji/list.ts index 9426318e34..7139715293 100644 --- a/packages/backend/src/server/api/endpoints/v2/admin/emoji/list.ts +++ b/packages/backend/src/server/api/endpoints/v2/admin/emoji/list.ts @@ -12,7 +12,7 @@ export const meta = { tags: ['admin'], requireCredential: true, - requireRolePolicy: 'canManageCustomEmojis', + requiredRolePolicy: 'canManageCustomEmojis', kind: 'read:admin:emoji', res: { diff --git a/packages/backend/src/server/api/openapi/gen-spec.ts b/packages/backend/src/server/api/openapi/gen-spec.ts index 82ee0f47d7..85d1fd0bce 100644 --- a/packages/backend/src/server/api/openapi/gen-spec.ts +++ b/packages/backend/src/server/api/openapi/gen-spec.ts @@ -210,9 +210,15 @@ export function genOpenapiSpec(config: Config, includeSelfRef = false) { spec.paths['/' + endpoint.name] = { ...(endpoint.meta.allowGet ? { - get: info, + get: { + ...info, + operationId: 'get___' + info.operationId, + }, } : {}), - post: info, + post: { + ...info, + operationId: 'post___' + info.operationId, + }, }; } diff --git a/packages/backend/src/server/api/stream/ChannelsService.ts b/packages/backend/src/server/api/stream/ChannelsService.ts index 83c5fcdf52..13934628a8 100644 --- a/packages/backend/src/server/api/stream/ChannelsService.ts +++ b/packages/backend/src/server/api/stream/ChannelsService.ts @@ -20,6 +20,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'; @@ -42,6 +44,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, ) { @@ -65,6 +69,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 e98e2a2f3f..a1ea947d20 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'; @@ -51,7 +50,6 @@ export default class Connection { constructor( private channelsService: ChannelsService, - private noteReadService: NoteReadService, private notificationService: NotificationService, private cacheService: CacheService, private channelFollowingService: ChannelFollowingService, @@ -154,7 +152,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; @@ -190,19 +188,6 @@ export default class Connection { } @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/channel.ts b/packages/backend/src/server/api/stream/channel.ts index 5189c86de8..9af816dfbb 100644 --- a/packages/backend/src/server/api/stream/channel.ts +++ b/packages/backend/src/server/api/stream/channel.ts @@ -106,8 +106,8 @@ export default abstract class Channel { this.noteEntityService = noteEntityService; } - public send(payload: { type: string, body: JsonValue }): void - public send(type: string, payload: JsonValue): void + public send(payload: { type: string, body: JsonValue }): void; + public send(type: string, payload: JsonValue): void; @bindThis public send(typeOrPayload: { type: string, body: JsonValue } | string, payload?: JsonValue) { const type = payload === undefined ? (typeOrPayload as { type: string, body: JsonValue }).type : (typeOrPayload as string); @@ -166,4 +166,4 @@ export type MiChannelService<T extends boolean> = { requireCredential: T; kind: T extends true ? string : string | null | undefined; create: (id: string, connection: Connection) => Channel; -} +}; 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<true> { + 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<true> { + 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, + ); + } +} |