diff options
| author | Mar0xy <marie@kaifa.ch> | 2023-11-03 15:35:12 +0100 |
|---|---|---|
| committer | Mar0xy <marie@kaifa.ch> | 2023-11-03 15:35:12 +0100 |
| commit | 7c480424a60b9ebed52f5f928fb915af659110f0 (patch) | |
| tree | f88524c83bde2611ae5bc95bf66769ca83a000ee /packages/backend/src/server/api | |
| parent | Merge branch 'develop' of https://github.com/transfem-org/Sharkey into develop (diff) | |
| parent | Update CHANGELOG.md (diff) | |
| download | sharkey-7c480424a60b9ebed52f5f928fb915af659110f0.tar.gz sharkey-7c480424a60b9ebed52f5f928fb915af659110f0.tar.bz2 sharkey-7c480424a60b9ebed52f5f928fb915af659110f0.zip | |
merge: upstream
Diffstat (limited to 'packages/backend/src/server/api')
21 files changed, 339 insertions, 209 deletions
diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index c458a8fd36..fde35ffd32 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -222,6 +222,7 @@ import * as ep___i_importMuting from './endpoints/i/import-muting.js'; import * as ep___i_importUserLists from './endpoints/i/import-user-lists.js'; import * as ep___i_importAntennas from './endpoints/i/import-antennas.js'; import * as ep___i_notifications from './endpoints/i/notifications.js'; +import * as ep___i_notificationsGrouped from './endpoints/i/notifications-grouped.js'; import * as ep___i_pageLikes from './endpoints/i/page-likes.js'; import * as ep___i_pages from './endpoints/i/pages.js'; import * as ep___i_pin from './endpoints/i/pin.js'; @@ -235,7 +236,7 @@ import * as ep___i_registry_get from './endpoints/i/registry/get.js'; import * as ep___i_registry_keysWithType from './endpoints/i/registry/keys-with-type.js'; import * as ep___i_registry_keys from './endpoints/i/registry/keys.js'; import * as ep___i_registry_remove from './endpoints/i/registry/remove.js'; -import * as ep___i_registry_scopes from './endpoints/i/registry/scopes.js'; +import * as ep___i_registry_scopesWithDomain from './endpoints/i/registry/scopes-with-domain.js'; import * as ep___i_registry_set from './endpoints/i/registry/set.js'; import * as ep___i_revokeToken from './endpoints/i/revoke-token.js'; import * as ep___i_signinHistory from './endpoints/i/signin-history.js'; @@ -588,6 +589,7 @@ const $i_importMuting: Provider = { provide: 'ep:i/import-muting', useClass: ep_ const $i_importUserLists: Provider = { provide: 'ep:i/import-user-lists', useClass: ep___i_importUserLists.default }; const $i_importAntennas: Provider = { provide: 'ep:i/import-antennas', useClass: ep___i_importAntennas.default }; const $i_notifications: Provider = { provide: 'ep:i/notifications', useClass: ep___i_notifications.default }; +const $i_notificationsGrouped: Provider = { provide: 'ep:i/notifications-grouped', useClass: ep___i_notificationsGrouped.default }; const $i_pageLikes: Provider = { provide: 'ep:i/page-likes', useClass: ep___i_pageLikes.default }; const $i_pages: Provider = { provide: 'ep:i/pages', useClass: ep___i_pages.default }; const $i_pin: Provider = { provide: 'ep:i/pin', useClass: ep___i_pin.default }; @@ -601,7 +603,7 @@ const $i_registry_get: Provider = { provide: 'ep:i/registry/get', useClass: ep__ const $i_registry_keysWithType: Provider = { provide: 'ep:i/registry/keys-with-type', useClass: ep___i_registry_keysWithType.default }; const $i_registry_keys: Provider = { provide: 'ep:i/registry/keys', useClass: ep___i_registry_keys.default }; const $i_registry_remove: Provider = { provide: 'ep:i/registry/remove', useClass: ep___i_registry_remove.default }; -const $i_registry_scopes: Provider = { provide: 'ep:i/registry/scopes', useClass: ep___i_registry_scopes.default }; +const $i_registry_scopesWithDomain: Provider = { provide: 'ep:i/registry/scopes-with-domain', useClass: ep___i_registry_scopesWithDomain.default }; const $i_registry_set: Provider = { provide: 'ep:i/registry/set', useClass: ep___i_registry_set.default }; const $i_revokeToken: Provider = { provide: 'ep:i/revoke-token', useClass: ep___i_revokeToken.default }; const $i_signinHistory: Provider = { provide: 'ep:i/signin-history', useClass: ep___i_signinHistory.default }; @@ -958,6 +960,7 @@ const $sponsors: Provider = { provide: 'ep:sponsors', useClass: ep___sponsors.de $i_importUserLists, $i_importAntennas, $i_notifications, + $i_notificationsGrouped, $i_pageLikes, $i_pages, $i_pin, @@ -971,7 +974,7 @@ const $sponsors: Provider = { provide: 'ep:sponsors', useClass: ep___sponsors.de $i_registry_keysWithType, $i_registry_keys, $i_registry_remove, - $i_registry_scopes, + $i_registry_scopesWithDomain, $i_registry_set, $i_revokeToken, $i_signinHistory, @@ -1322,6 +1325,7 @@ const $sponsors: Provider = { provide: 'ep:sponsors', useClass: ep___sponsors.de $i_importUserLists, $i_importAntennas, $i_notifications, + $i_notificationsGrouped, $i_pageLikes, $i_pages, $i_pin, @@ -1335,7 +1339,7 @@ const $sponsors: Provider = { provide: 'ep:sponsors', useClass: ep___sponsors.de $i_registry_keysWithType, $i_registry_keys, $i_registry_remove, - $i_registry_scopes, + $i_registry_scopesWithDomain, $i_registry_set, $i_revokeToken, $i_signinHistory, diff --git a/packages/backend/src/server/api/SignupApiService.ts b/packages/backend/src/server/api/SignupApiService.ts index d3ece10859..f0b3961f94 100644 --- a/packages/backend/src/server/api/SignupApiService.ts +++ b/packages/backend/src/server/api/SignupApiService.ts @@ -149,7 +149,20 @@ export class SignupApiService { return; } - if (ticket.usedAt) { + // メアド認証が有効の場合 + if (instance.emailRequiredForSignup) { + // メアド認証済みならエラー + if (ticket.usedBy) { + reply.code(400); + return; + } + + // 認証しておらず、メール送信から30分以内ならエラー + if (ticket.usedAt && ticket.usedAt.getTime() + (1000 * 60 * 30) > Date.now()) { + reply.code(400); + return; + } + } else if (ticket.usedAt) { reply.code(400); return; } @@ -273,6 +286,10 @@ export class SignupApiService { try { const pendingUser = await this.userPendingsRepository.findOneByOrFail({ code }); + if (this.idService.parse(pendingUser.id).date.getTime() + (1000 * 60 * 30) < Date.now()) { + throw new FastifyReplyError(400, 'EXPIRED'); + } + const { account, secret } = await this.signupService.signup({ username: pendingUser.username, passwordHash: pendingUser.password, diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index cc87bfa539..c1cabd33e9 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -222,6 +222,7 @@ import * as ep___i_importMuting from './endpoints/i/import-muting.js'; import * as ep___i_importUserLists from './endpoints/i/import-user-lists.js'; import * as ep___i_importAntennas from './endpoints/i/import-antennas.js'; import * as ep___i_notifications from './endpoints/i/notifications.js'; +import * as ep___i_notificationsGrouped from './endpoints/i/notifications-grouped.js'; import * as ep___i_pageLikes from './endpoints/i/page-likes.js'; import * as ep___i_pages from './endpoints/i/pages.js'; import * as ep___i_pin from './endpoints/i/pin.js'; @@ -235,7 +236,7 @@ import * as ep___i_registry_get from './endpoints/i/registry/get.js'; import * as ep___i_registry_keysWithType from './endpoints/i/registry/keys-with-type.js'; import * as ep___i_registry_keys from './endpoints/i/registry/keys.js'; import * as ep___i_registry_remove from './endpoints/i/registry/remove.js'; -import * as ep___i_registry_scopes from './endpoints/i/registry/scopes.js'; +import * as ep___i_registry_scopesWithDomain from './endpoints/i/registry/scopes-with-domain.js'; import * as ep___i_registry_set from './endpoints/i/registry/set.js'; import * as ep___i_revokeToken from './endpoints/i/revoke-token.js'; import * as ep___i_signinHistory from './endpoints/i/signin-history.js'; @@ -586,6 +587,7 @@ const eps = [ ['i/import-user-lists', ep___i_importUserLists], ['i/import-antennas', ep___i_importAntennas], ['i/notifications', ep___i_notifications], + ['i/notifications-grouped', ep___i_notificationsGrouped], ['i/page-likes', ep___i_pageLikes], ['i/pages', ep___i_pages], ['i/pin', ep___i_pin], @@ -599,7 +601,7 @@ const eps = [ ['i/registry/keys-with-type', ep___i_registry_keysWithType], ['i/registry/keys', ep___i_registry_keys], ['i/registry/remove', ep___i_registry_remove], - ['i/registry/scopes', ep___i_registry_scopes], + ['i/registry/scopes-with-domain', ep___i_registry_scopesWithDomain], ['i/registry/set', ep___i_registry_set], ['i/revoke-token', ep___i_revokeToken], ['i/signin-history', ep___i_signinHistory], diff --git a/packages/backend/src/server/api/endpoints/channels/create.ts b/packages/backend/src/server/api/endpoints/channels/create.ts index 3ba411d28c..3dd1eddd01 100644 --- a/packages/backend/src/server/api/endpoints/channels/create.ts +++ b/packages/backend/src/server/api/endpoints/channels/create.ts @@ -50,6 +50,7 @@ export const paramDef = { bannerId: { type: 'string', format: 'misskey:id', nullable: true }, color: { type: 'string', minLength: 1, maxLength: 16 }, isSensitive: { type: 'boolean', nullable: true }, + allowRenoteToExternal: { type: 'boolean', nullable: true }, }, required: ['name'], } as const; @@ -87,6 +88,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- bannerId: banner ? banner.id : null, isSensitive: ps.isSensitive ?? false, ...(ps.color !== undefined ? { color: ps.color } : {}), + allowRenoteToExternal: ps.allowRenoteToExternal ?? true, } as MiChannel).then(x => this.channelsRepository.findOneByOrFail(x.identifiers[0])); return await this.channelEntityService.pack(channel, me); diff --git a/packages/backend/src/server/api/endpoints/channels/update.ts b/packages/backend/src/server/api/endpoints/channels/update.ts index ab69f62a7b..93d02e4a12 100644 --- a/packages/backend/src/server/api/endpoints/channels/update.ts +++ b/packages/backend/src/server/api/endpoints/channels/update.ts @@ -61,6 +61,7 @@ export const paramDef = { }, color: { type: 'string', minLength: 1, maxLength: 16 }, isSensitive: { type: 'boolean', nullable: true }, + allowRenoteToExternal: { type: 'boolean', nullable: true }, }, required: ['channelId'], } as const; @@ -115,6 +116,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- ...(typeof ps.isArchived === 'boolean' ? { isArchived: ps.isArchived } : {}), ...(banner ? { bannerId: banner.id } : {}), ...(typeof ps.isSensitive === 'boolean' ? { isSensitive: ps.isSensitive } : {}), + ...(typeof ps.allowRenoteToExternal === 'boolean' ? { allowRenoteToExternal: ps.allowRenoteToExternal } : {}), }); return await this.channelEntityService.pack(channel.id, me); diff --git a/packages/backend/src/server/api/endpoints/i/notifications-grouped.ts b/packages/backend/src/server/api/endpoints/i/notifications-grouped.ts new file mode 100644 index 0000000000..4ea94b07f6 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/i/notifications-grouped.ts @@ -0,0 +1,178 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Brackets, In } from 'typeorm'; +import * as Redis from 'ioredis'; +import { Inject, Injectable } from '@nestjs/common'; +import type { NotesRepository } from '@/models/_.js'; +import { obsoleteNotificationTypes, notificationTypes, 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'; +import { IdService } from '@/core/IdService.js'; +import { MiGroupedNotification, MiNotification } from '@/models/Notification.js'; + +export const meta = { + tags: ['account', 'notifications'], + + requireCredential: true, + + limit: { + duration: 30000, + max: 30, + }, + + kind: 'read:notifications', + + res: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + ref: 'Notification', + }, + }, +} 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' }, + markAsRead: { type: 'boolean', default: true }, + // 後方互換のため、廃止された通知タイプも受け付ける + includeTypes: { type: 'array', items: { + type: 'string', enum: [...notificationTypes, ...obsoleteNotificationTypes], + } }, + excludeTypes: { type: 'array', items: { + type: 'string', enum: [...notificationTypes, ...obsoleteNotificationTypes], + } }, + }, + required: [], +} as const; + +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export + constructor( + @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; + + // includeTypes が空の場合はクエリしない + if (ps.includeTypes && ps.includeTypes.length === 0) { + return []; + } + // excludeTypes に全指定されている場合はクエリしない + if (notificationTypes.every(type => ps.excludeTypes?.includes(type))) { + return []; + } + + const includeTypes = ps.includeTypes && ps.includeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][]; + const excludeTypes = ps.excludeTypes && ps.excludeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][]; + + const limit = (ps.limit + EXTRA_LIMIT) + (ps.untilId ? 1 : 0) + (ps.sinceId ? 1 : 0); // untilIdに指定したものも含まれるため+1 + const notificationsRes = await this.redisClient.xrevrange( + `notificationTimeline:${me.id}`, + ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : '+', + ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime() : '-', + 'COUNT', limit); + + if (notificationsRes.length === 0) { + return []; + } + + let notifications = notificationsRes.map(x => JSON.parse(x[1][1])).filter(x => x.id !== ps.untilId && x !== ps.sinceId) as MiNotification[]; + + if (includeTypes && includeTypes.length > 0) { + notifications = notifications.filter(notification => includeTypes.includes(notification.type)); + } else if (excludeTypes && excludeTypes.length > 0) { + notifications = notifications.filter(notification => !excludeTypes.includes(notification.type)); + } + + if (notifications.length === 0) { + return []; + } + + // Mark all as read + if (ps.markAsRead) { + this.notificationService.readAllNotification(me.id); + } + + // grouping + let groupedNotifications = [notifications[0]] as MiGroupedNotification[]; + for (let i = 1; i < notifications.length; i++) { + const notification = notifications[i]; + const prev = notifications[i - 1]; + let prevGroupedNotification = groupedNotifications.at(-1)!; + + if (prev.type === 'reaction' && notification.type === 'reaction' && prev.noteId === notification.noteId) { + if (prevGroupedNotification.type !== 'reaction:grouped') { + groupedNotifications[groupedNotifications.length - 1] = { + type: 'reaction:grouped', + id: '', + createdAt: prev.createdAt, + noteId: prev.noteId!, + reactions: [{ + userId: prev.notifierId!, + reaction: prev.reaction!, + }], + }; + prevGroupedNotification = groupedNotifications.at(-1)!; + } + (prevGroupedNotification as FilterUnionByProperty<MiGroupedNotification, 'type', 'reaction:grouped'>).reactions.push({ + userId: notification.notifierId!, + reaction: notification.reaction!, + }); + prevGroupedNotification.id = notification.id; + continue; + } + if (prev.type === 'renote' && notification.type === 'renote' && prev.targetNoteId === notification.targetNoteId) { + if (prevGroupedNotification.type !== 'renote:grouped') { + groupedNotifications[groupedNotifications.length - 1] = { + type: 'renote:grouped', + id: '', + createdAt: notification.createdAt, + noteId: prev.noteId!, + userIds: [prev.notifierId!], + }; + prevGroupedNotification = groupedNotifications.at(-1)!; + } + (prevGroupedNotification as FilterUnionByProperty<MiGroupedNotification, 'type', 'renote:grouped'>).userIds.push(notification.notifierId!); + prevGroupedNotification.id = notification.id; + continue; + } + + groupedNotifications.push(notification); + } + + groupedNotifications = groupedNotifications.slice(0, ps.limit); + + const noteIds = groupedNotifications + .filter((notification): notification is FilterUnionByProperty<MiNotification, 'type', 'mention' | 'reply' | 'quote'> => ['mention', 'reply', 'quote'].includes(notification.type)) + .map(notification => notification.noteId!); + + if (noteIds.length > 0) { + const notes = await this.notesRepository.findBy({ id: In(noteIds) }); + this.noteReadService.read(me.id, notes); + } + + return await this.notificationEntityService.packGroupedMany(groupedNotifications, me.id); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/notifications.ts b/packages/backend/src/server/api/endpoints/i/notifications.ts index 91dd72e805..039fd9454c 100644 --- a/packages/backend/src/server/api/endpoints/i/notifications.ts +++ b/packages/backend/src/server/api/endpoints/i/notifications.ts @@ -7,7 +7,7 @@ import { Brackets, In } from 'typeorm'; import * as Redis from 'ioredis'; import { Inject, Injectable } from '@nestjs/common'; import type { NotesRepository } from '@/models/_.js'; -import { obsoleteNotificationTypes, notificationTypes } from '@/types.js'; +import { obsoleteNotificationTypes, notificationTypes, 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'; @@ -113,8 +113,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- } const noteIds = notifications - .filter(notification => ['mention', 'reply', 'quote'].includes(notification.type)) - .map(notification => notification.noteId!); + .filter((notification): notification is FilterUnionByProperty<MiNotification, 'type', 'mention' | 'reply' | 'quote'> => ['mention', 'reply', 'quote'].includes(notification.type)) + .map(notification => notification.noteId); if (noteIds.length > 0) { const notes = await this.notesRepository.findBy({ id: In(noteIds) }); diff --git a/packages/backend/src/server/api/endpoints/i/registry/get-all.ts b/packages/backend/src/server/api/endpoints/i/registry/get-all.ts index 211e6637dc..29fa0a29cc 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/get-all.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/get-all.ts @@ -5,13 +5,10 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { RegistryItemsRepository } from '@/models/_.js'; -import { DI } from '@/di-symbols.js'; +import { RegistryApiService } from '@/core/RegistryApiService.js'; export const meta = { requireCredential: true, - - secure: true, } as const; export const paramDef = { @@ -20,23 +17,18 @@ export const paramDef = { scope: { type: 'array', default: [], items: { type: 'string', pattern: /^[a-zA-Z0-9_]+$/.toString().slice(1, -1), } }, + domain: { type: 'string', nullable: true }, }, - required: [], + required: ['scope'], } as const; @Injectable() export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.registryItemsRepository) - private registryItemsRepository: RegistryItemsRepository, + private registryApiService: RegistryApiService, ) { - super(meta, paramDef, async (ps, me) => { - const query = this.registryItemsRepository.createQueryBuilder('item') - .where('item.domain IS NULL') - .andWhere('item.userId = :userId', { userId: me.id }) - .andWhere('item.scope = :scope', { scope: ps.scope }); - - const items = await query.getMany(); + super(meta, paramDef, async (ps, me, accessToken) => { + const items = await this.registryApiService.getAllItemsOfScope(me.id, accessToken != null ? accessToken.id : (ps.domain ?? null), ps.scope); const res = {} as Record<string, any>; diff --git a/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts b/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts index 9c6f2d6781..5b460b45d6 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts @@ -5,15 +5,12 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { RegistryItemsRepository } from '@/models/_.js'; -import { DI } from '@/di-symbols.js'; +import { RegistryApiService } from '@/core/RegistryApiService.js'; import { ApiError } from '../../../error.js'; export const meta = { requireCredential: true, - secure: true, - errors: { noSuchKey: { message: 'No such key.', @@ -30,24 +27,18 @@ export const paramDef = { scope: { type: 'array', default: [], items: { type: 'string', pattern: /^[a-zA-Z0-9_]+$/.toString().slice(1, -1), } }, + domain: { type: 'string', nullable: true }, }, - required: ['key'], + required: ['key', 'scope'], } as const; @Injectable() export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.registryItemsRepository) - private registryItemsRepository: RegistryItemsRepository, + private registryApiService: RegistryApiService, ) { - super(meta, paramDef, async (ps, me) => { - const query = this.registryItemsRepository.createQueryBuilder('item') - .where('item.domain IS NULL') - .andWhere('item.userId = :userId', { userId: me.id }) - .andWhere('item.key = :key', { key: ps.key }) - .andWhere('item.scope = :scope', { scope: ps.scope }); - - const item = await query.getOne(); + super(meta, paramDef, async (ps, me, accessToken) => { + const item = await this.registryApiService.getItem(me.id, accessToken != null ? accessToken.id : (ps.domain ?? null), ps.scope, ps.key); if (item == null) { throw new ApiError(meta.errors.noSuchKey); diff --git a/packages/backend/src/server/api/endpoints/i/registry/get.ts b/packages/backend/src/server/api/endpoints/i/registry/get.ts index 729e729b8c..e8c28298ef 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/get.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/get.ts @@ -5,15 +5,12 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { RegistryItemsRepository } from '@/models/_.js'; -import { DI } from '@/di-symbols.js'; +import { RegistryApiService } from '@/core/RegistryApiService.js'; import { ApiError } from '../../../error.js'; export const meta = { requireCredential: true, - secure: true, - errors: { noSuchKey: { message: 'No such key.', @@ -30,24 +27,18 @@ export const paramDef = { scope: { type: 'array', default: [], items: { type: 'string', pattern: /^[a-zA-Z0-9_]+$/.toString().slice(1, -1), } }, + domain: { type: 'string', nullable: true }, }, - required: ['key'], + required: ['key', 'scope'], } as const; @Injectable() export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.registryItemsRepository) - private registryItemsRepository: RegistryItemsRepository, + private registryApiService: RegistryApiService, ) { - super(meta, paramDef, async (ps, me) => { - const query = this.registryItemsRepository.createQueryBuilder('item') - .where('item.domain IS NULL') - .andWhere('item.userId = :userId', { userId: me.id }) - .andWhere('item.key = :key', { key: ps.key }) - .andWhere('item.scope = :scope', { scope: ps.scope }); - - const item = await query.getOne(); + super(meta, paramDef, async (ps, me, accessToken) => { + const item = await this.registryApiService.getItem(me.id, accessToken != null ? accessToken.id : (ps.domain ?? null), ps.scope, ps.key); if (item == null) { throw new ApiError(meta.errors.noSuchKey); diff --git a/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts b/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts index ffd2860fde..8953ee5d3d 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts @@ -5,13 +5,10 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { RegistryItemsRepository } from '@/models/_.js'; -import { DI } from '@/di-symbols.js'; +import { RegistryApiService } from '@/core/RegistryApiService.js'; export const meta = { requireCredential: true, - - secure: true, } as const; export const paramDef = { @@ -20,36 +17,31 @@ export const paramDef = { scope: { type: 'array', default: [], items: { type: 'string', pattern: /^[a-zA-Z0-9_]+$/.toString().slice(1, -1), } }, + domain: { type: 'string', nullable: true }, }, - required: [], + required: ['scope'], } as const; @Injectable() export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.registryItemsRepository) - private registryItemsRepository: RegistryItemsRepository, + private registryApiService: RegistryApiService, ) { - super(meta, paramDef, async (ps, me) => { - const query = this.registryItemsRepository.createQueryBuilder('item') - .where('item.domain IS NULL') - .andWhere('item.userId = :userId', { userId: me.id }) - .andWhere('item.scope = :scope', { scope: ps.scope }); - - const items = await query.getMany(); + super(meta, paramDef, async (ps, me, accessToken) => { + const items = await this.registryApiService.getAllItemsOfScope(me.id, accessToken != null ? accessToken.id : (ps.domain ?? null), ps.scope); const res = {} as Record<string, string>; for (const item of items) { const type = typeof item.value; res[item.key] = - item.value === null ? 'null' : - Array.isArray(item.value) ? 'array' : - type === 'number' ? 'number' : - type === 'string' ? 'string' : - type === 'boolean' ? 'boolean' : - type === 'object' ? 'object' : - null as never; + item.value === null ? 'null' : + Array.isArray(item.value) ? 'array' : + type === 'number' ? 'number' : + type === 'string' ? 'string' : + type === 'boolean' ? 'boolean' : + type === 'object' ? 'object' : + null as never; } return res; diff --git a/packages/backend/src/server/api/endpoints/i/registry/keys.ts b/packages/backend/src/server/api/endpoints/i/registry/keys.ts index 7239bb66e1..04e120d752 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/keys.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/keys.ts @@ -5,13 +5,10 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { RegistryItemsRepository } from '@/models/_.js'; -import { DI } from '@/di-symbols.js'; +import { RegistryApiService } from '@/core/RegistryApiService.js'; export const meta = { requireCredential: true, - - secure: true, } as const; export const paramDef = { @@ -20,26 +17,18 @@ export const paramDef = { scope: { type: 'array', default: [], items: { type: 'string', pattern: /^[a-zA-Z0-9_]+$/.toString().slice(1, -1), } }, + domain: { type: 'string', nullable: true }, }, - required: [], + required: ['scope'], } as const; @Injectable() export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.registryItemsRepository) - private registryItemsRepository: RegistryItemsRepository, + private registryApiService: RegistryApiService, ) { - super(meta, paramDef, async (ps, me) => { - const query = this.registryItemsRepository.createQueryBuilder('item') - .select('item.key') - .where('item.domain IS NULL') - .andWhere('item.userId = :userId', { userId: me.id }) - .andWhere('item.scope = :scope', { scope: ps.scope }); - - const items = await query.getMany(); - - return items.map(x => x.key); + super(meta, paramDef, async (ps, me, accessToken) => { + return await this.registryApiService.getAllKeysOfScope(me.id, accessToken != null ? accessToken.id : (ps.domain ?? null), ps.scope); }); } } diff --git a/packages/backend/src/server/api/endpoints/i/registry/remove.ts b/packages/backend/src/server/api/endpoints/i/registry/remove.ts index ae687fefe9..ba8100b547 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/remove.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/remove.ts @@ -7,13 +7,12 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { RegistryItemsRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; +import { RegistryApiService } from '@/core/RegistryApiService.js'; import { ApiError } from '../../../error.js'; export const meta = { requireCredential: true, - secure: true, - errors: { noSuchKey: { message: 'No such key.', @@ -30,30 +29,18 @@ export const paramDef = { scope: { type: 'array', default: [], items: { type: 'string', pattern: /^[a-zA-Z0-9_]+$/.toString().slice(1, -1), } }, + domain: { type: 'string', nullable: true }, }, - required: ['key'], + required: ['key', 'scope'], } as const; @Injectable() export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.registryItemsRepository) - private registryItemsRepository: RegistryItemsRepository, + private registryApiService: RegistryApiService, ) { - super(meta, paramDef, async (ps, me) => { - const query = this.registryItemsRepository.createQueryBuilder('item') - .where('item.domain IS NULL') - .andWhere('item.userId = :userId', { userId: me.id }) - .andWhere('item.key = :key', { key: ps.key }) - .andWhere('item.scope = :scope', { scope: ps.scope }); - - const item = await query.getOne(); - - if (item == null) { - throw new ApiError(meta.errors.noSuchKey); - } - - await this.registryItemsRepository.remove(item); + super(meta, paramDef, async (ps, me, accessToken) => { + await this.registryApiService.remove(me.id, accessToken != null ? accessToken.id : (ps.domain ?? null), ps.scope, ps.key); }); } } diff --git a/packages/backend/src/server/api/endpoints/i/registry/scopes-with-domain.ts b/packages/backend/src/server/api/endpoints/i/registry/scopes-with-domain.ts new file mode 100644 index 0000000000..1ff994b82c --- /dev/null +++ b/packages/backend/src/server/api/endpoints/i/registry/scopes-with-domain.ts @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { RegistryApiService } from '@/core/RegistryApiService.js'; + +export const meta = { + requireCredential: true, + secure: true, +} 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( + private registryApiService: RegistryApiService, + ) { + super(meta, paramDef, async (ps, me) => { + return await this.registryApiService.getAllScopeAndDomains(me.id); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/registry/scopes.ts b/packages/backend/src/server/api/endpoints/i/registry/scopes.ts deleted file mode 100644 index 7637cdcf73..0000000000 --- a/packages/backend/src/server/api/endpoints/i/registry/scopes.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and other misskey contributors - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { RegistryItemsRepository } from '@/models/_.js'; -import { DI } from '@/di-symbols.js'; - -export const meta = { - requireCredential: true, - - secure: true, -} 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.registryItemsRepository) - private registryItemsRepository: RegistryItemsRepository, - ) { - super(meta, paramDef, async (ps, me) => { - const query = this.registryItemsRepository.createQueryBuilder('item') - .select('item.scope') - .where('item.domain IS NULL') - .andWhere('item.userId = :userId', { userId: me.id }); - - const items = await query.getMany(); - - const res = [] as string[][]; - - for (const item of items) { - if (res.some(scope => scope.join('.') === item.scope.join('.'))) continue; - res.push(item.scope); - } - - return res; - }); - } -} diff --git a/packages/backend/src/server/api/endpoints/i/registry/set.ts b/packages/backend/src/server/api/endpoints/i/registry/set.ts index 6203e7aa8b..58bb450bce 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/set.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/set.ts @@ -5,15 +5,10 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { RegistryItemsRepository } from '@/models/_.js'; -import { IdService } from '@/core/IdService.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; -import { DI } from '@/di-symbols.js'; +import { RegistryApiService } from '@/core/RegistryApiService.js'; export const meta = { requireCredential: true, - - secure: true, } as const; export const paramDef = { @@ -24,51 +19,18 @@ export const paramDef = { scope: { type: 'array', default: [], items: { type: 'string', pattern: /^[a-zA-Z0-9_]+$/.toString().slice(1, -1), } }, + domain: { type: 'string', nullable: true }, }, - required: ['key', 'value'], + required: ['key', 'value', 'scope'], } as const; @Injectable() export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.registryItemsRepository) - private registryItemsRepository: RegistryItemsRepository, - - private idService: IdService, - private globalEventService: GlobalEventService, + private registryApiService: RegistryApiService, ) { - super(meta, paramDef, async (ps, me) => { - const query = this.registryItemsRepository.createQueryBuilder('item') - .where('item.domain IS NULL') - .andWhere('item.userId = :userId', { userId: me.id }) - .andWhere('item.key = :key', { key: ps.key }) - .andWhere('item.scope = :scope', { scope: ps.scope }); - - const existingItem = await query.getOne(); - - if (existingItem) { - await this.registryItemsRepository.update(existingItem.id, { - updatedAt: new Date(), - value: ps.value, - }); - } else { - await this.registryItemsRepository.insert({ - id: this.idService.gen(), - updatedAt: new Date(), - userId: me.id, - domain: null, - scope: ps.scope, - key: ps.key, - value: ps.value, - }); - } - - // TODO: サードパーティアプリが傍受出来てしまうのでどうにかする - this.globalEventService.publishMainStream(me.id, 'registryUpdated', { - scope: ps.scope, - key: ps.key, - value: ps.value, - }); + super(meta, paramDef, async (ps, me, accessToken) => { + await this.registryApiService.set(me.id, accessToken ? accessToken.id : (ps.domain ?? null), ps.scope, ps.key, ps.value); }); } } diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index ec2d5d6579..e161c47f9f 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -45,7 +45,7 @@ export const meta = { limit: { duration: ms('1hour'), - max: 10, + max: 20, }, errors: { diff --git a/packages/backend/src/server/api/endpoints/notes/create.test.ts b/packages/backend/src/server/api/endpoints/notes/create.test.ts index bfb024bcf2..6086f99c92 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.test.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.test.ts @@ -64,7 +64,7 @@ describe('api:notes/create', () => { test('0 characters cw', () => { expect(v({ text: 'Body', cw: '' })) - .toBe(VALID); + .toBe(INVALID); }); test('reject only cw', () => { diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index 649068fb20..df02d3acb7 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -16,8 +16,8 @@ import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { NoteCreateService } from '@/core/NoteCreateService.js'; import { DI } from '@/di-symbols.js'; -import { ApiError } from '../../error.js'; import { isPureRenote } from '@/misc/is-pure-renote.js'; +import { ApiError } from '../../error.js'; export const meta = { tags: ['notes'], @@ -99,6 +99,12 @@ export const meta = { code: 'NO_SUCH_FILE', id: 'b6992544-63e7-67f0-fa7f-32444b1b5306', }, + + cannotRenoteOutsideOfChannel: { + message: 'Cannot renote outside of channel.', + code: 'CANNOT_RENOTE_OUTSIDE_OF_CHANNEL', + id: '33510210-8452-094c-6227-4a6c05d99f00', + }, }, } as const; @@ -109,7 +115,7 @@ export const paramDef = { visibleUserIds: { type: 'array', uniqueItems: true, items: { type: 'string', format: 'misskey:id', } }, - cw: { type: 'string', nullable: true, maxLength: 100 }, + cw: { type: 'string', nullable: true, minLength: 1, maxLength: 100 }, localOnly: { type: 'boolean', default: false }, reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'], default: null }, noExtractMentions: { type: 'boolean', default: false }, @@ -246,6 +252,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- // specified / direct noteはreject throw new ApiError(meta.errors.cannotRenoteDueToVisibility); } + + if (renote.channelId && renote.channelId !== ps.channelId) { + // チャンネルのノートに対しリノート要求がきたとき、チャンネル外へのリノート可否をチェック + // リノートのユースケースのうち、チャンネル内→チャンネル外は少数だと考えられるため、JOINはせず必要な時に都度取得する + const renoteChannel = await this.channelsRepository.findOneById(renote.channelId); + if (renoteChannel == null) { + // リノートしたいノートが書き込まれているチャンネルが無い + throw new ApiError(meta.errors.noSuchChannel); + } else if (!renoteChannel.allowRenoteToExternal) { + // リノート作成のリクエストだが、対象チャンネルがリノート禁止だった場合 + throw new ApiError(meta.errors.cannotRenoteOutsideOfChannel); + } + } } let reply: MiNote | null = null; diff --git a/packages/backend/src/server/api/endpoints/notes/edit.ts b/packages/backend/src/server/api/endpoints/notes/edit.ts index 07f04bba5a..6140c80a5d 100644 --- a/packages/backend/src/server/api/endpoints/notes/edit.ts +++ b/packages/backend/src/server/api/endpoints/notes/edit.ts @@ -117,6 +117,12 @@ export const meta = { code: "NOT_LOCAL_USER", id: "b907f407-2aa0-4283-800b-a2c56290b822", }, + + cannotRenoteOutsideOfChannel: { + message: 'Cannot renote outside of channel.', + code: 'CANNOT_RENOTE_OUTSIDE_OF_CHANNEL', + id: '33510210-8452-094c-6227-4a6c05d99f00', + }, }, } as const; @@ -134,7 +140,7 @@ export const paramDef = { }, }, text: { type: "string", maxLength: MAX_NOTE_TEXT_LENGTH, nullable: true }, - cw: { type: "string", nullable: true, maxLength: 250 }, + cw: { type: "string", nullable: true, minLength: 1, maxLength: 250 }, localOnly: { type: "boolean", default: false }, noExtractMentions: { type: "boolean", default: false }, noExtractHashtags: { type: "boolean", default: false }, @@ -281,6 +287,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- throw new ApiError(meta.errors.youHaveBeenBlocked); } } + + if (renote.channelId && renote.channelId !== ps.channelId) { + // チャンネルのノートに対しリノート要求がきたとき、チャンネル外へのリノート可否をチェック + // リノートのユースケースのうち、チャンネル内→チャンネル外は少数だと考えられるため、JOINはせず必要な時に都度取得する + const renoteChannel = await this.channelsRepository.findOneById(renote.channelId); + if (renoteChannel == null) { + // リノートしたいノートが書き込まれているチャンネルが無い + throw new ApiError(meta.errors.noSuchChannel); + } else if (!renoteChannel.allowRenoteToExternal) { + // リノート作成のリクエストだが、対象チャンネルがリノート禁止だった場合 + throw new ApiError(meta.errors.cannotRenoteOutsideOfChannel); + } + } } let reply: MiNote | null = null; diff --git a/packages/backend/src/server/api/endpoints/notifications/create.ts b/packages/backend/src/server/api/endpoints/notifications/create.ts index 19bc6fa8d7..7c6a979160 100644 --- a/packages/backend/src/server/api/endpoints/notifications/create.ts +++ b/packages/backend/src/server/api/endpoints/notifications/create.ts @@ -42,8 +42,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- this.notificationService.createNotification(user.id, 'app', { appAccessTokenId: token ? token.id : null, customBody: ps.body, - customHeader: ps.header ?? token?.name, - customIcon: ps.icon ?? token?.iconUrl, + customHeader: ps.header ?? token?.name ?? null, + customIcon: ps.icon ?? token?.iconUrl ?? null, }); }); } |