diff options
| author | dakkar <dakkar@thenautilus.net> | 2024-06-26 19:57:45 +0000 |
|---|---|---|
| committer | dakkar <dakkar@thenautilus.net> | 2024-06-26 19:57:45 +0000 |
| commit | dc4f6c8016ccece79f8f2b75a5378c643e884f0b (patch) | |
| tree | 3e2c9e998a2f97244a37f3f8cb7f1831e4108ab7 /packages/backend/src/server/api/endpoints | |
| parent | merge: Release 2024.3.3 (!501) (diff) | |
| parent | merge: parse `notRespondingSince` from redis instance cache (!560) (diff) | |
| download | sharkey-dc4f6c8016ccece79f8f2b75a5378c643e884f0b.tar.gz sharkey-dc4f6c8016ccece79f8f2b75a5378c643e884f0b.tar.bz2 sharkey-dc4f6c8016ccece79f8f2b75a5378c643e884f0b.zip | |
merge: release 2024.5.0 (!556)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/556
Approved-by: Tess K <me@thvxl.se>
Diffstat (limited to 'packages/backend/src/server/api/endpoints')
42 files changed, 542 insertions, 112 deletions
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 f968813197..acd2494131 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts @@ -90,7 +90,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- const addedEmoji = await this.customEmojiService.add({ driveFile, name: nameNfc, - category: emoji.category?.normalize('NFC'), + category: emoji.category?.normalize('NFC') ?? null, aliases: emoji.aliases?.map(a => a.normalize('NFC')), host: null, license: emoji.license, 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 5e21111f9f..f35a6667f4 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts @@ -66,6 +66,7 @@ export const paramDef = { properties: { query: { type: 'string', nullable: true, default: null }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + offset: { type: 'integer', minimum: 1, nullable: true, default: null }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, }, @@ -91,7 +92,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- //q.andWhere('emoji.name ILIKE :q', { q: `%${ sqlLikeEscape(ps.query) }%` }); //const emojis = await q.limit(ps.limit).getMany(); - emojis = await q.orderBy('length(emoji.name)', 'ASC').getMany(); + emojis = await q.orderBy('length(emoji.name)', 'ASC').addOrderBy('id', 'DESC').getMany(); const queryarry = ps.query.match(/:([\p{Letter}\p{Number}\p{Mark}_+-]*):/ug); if (queryarry) { @@ -105,9 +106,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- emoji.aliases.some(a => a.includes(queryNfc)) || emoji.category?.includes(queryNfc)); } - emojis.splice(ps.limit + 1); + emojis = emojis.slice((ps.offset ?? 0), ((ps.offset ?? 0) + ps.limit)); } else { - emojis = await q.limit(ps.limit).getMany(); + emojis = await q.take(ps.limit).skip(ps.offset ?? 0).getMany(); } return this.emojiEntityService.packDetailedMany(emojis); diff --git a/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts b/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts index 4ababae9f2..8b142910a6 100644 --- a/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts +++ b/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts @@ -47,13 +47,20 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- throw new Error('instance not found'); } + const isSuspendedBefore = instance.suspensionState !== 'none'; + let suspensionState: undefined | 'manuallySuspended' | 'none'; + + if (ps.isSuspended != null && isSuspendedBefore !== ps.isSuspended) { + suspensionState = ps.isSuspended ? 'manuallySuspended' : 'none'; + } + await this.federatedInstanceService.update(instance.id, { - isSuspended: ps.isSuspended, + suspensionState, isNSFW: ps.isNSFW, moderationNote: ps.moderationNote, }); - if (ps.isSuspended != null && instance.isSuspended !== ps.isSuspended) { + if (ps.isSuspended != null && isSuspendedBefore !== ps.isSuspended) { if (ps.isSuspended) { this.moderationLogService.log(me, 'suspendRemoteInstance', { id: instance.id, diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index 34454c276e..ca4d63b834 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -458,6 +458,10 @@ export const meta = { type: 'string', optional: false, nullable: true, }, + inquiryUrl: { + type: 'string', + optional: false, nullable: true, + }, repositoryUrl: { type: 'string', optional: false, nullable: true, @@ -465,6 +469,8 @@ export const meta = { summalyProxy: { type: 'string', optional: false, nullable: true, + deprecated: true, + description: '[Deprecated] Use "urlPreviewSummaryProxyUrl" instead.', }, themeColor: { type: 'string', @@ -482,6 +488,30 @@ export const meta = { type: 'string', optional: false, nullable: false, }, + urlPreviewEnabled: { + type: 'boolean', + optional: false, nullable: false, + }, + urlPreviewTimeout: { + type: 'number', + optional: false, nullable: false, + }, + urlPreviewMaximumContentLength: { + type: 'number', + optional: false, nullable: false, + }, + urlPreviewRequireContentLength: { + type: 'boolean', + optional: false, nullable: false, + }, + urlPreviewUserAgent: { + type: 'string', + optional: false, nullable: true, + }, + urlPreviewSummaryProxyUrl: { + type: 'string', + optional: false, nullable: true, + }, }, }, } as const; @@ -519,6 +549,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- impressumUrl: instance.impressumUrl, donationUrl: instance.donationUrl, privacyPolicyUrl: instance.privacyPolicyUrl, + inquiryUrl: instance.inquiryUrl, disableRegistration: instance.disableRegistration, emailRequiredForSignup: instance.emailRequiredForSignup, approvalRequiredForSignup: instance.approvalRequiredForSignup, @@ -569,7 +600,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- enableSensitiveMediaDetectionForVideos: instance.enableSensitiveMediaDetectionForVideos, enableBotTrending: instance.enableBotTrending, proxyAccountId: instance.proxyAccountId, - summalyProxy: instance.summalyProxy, email: instance.email, smtpSecure: instance.smtpSecure, smtpHost: instance.smtpHost, @@ -616,6 +646,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- perUserHomeTimelineCacheMax: instance.perUserHomeTimelineCacheMax, perUserListTimelineCacheMax: instance.perUserListTimelineCacheMax, notesPerOneAd: instance.notesPerOneAd, + summalyProxy: instance.urlPreviewSummaryProxyUrl, + urlPreviewEnabled: instance.urlPreviewEnabled, + urlPreviewTimeout: instance.urlPreviewTimeout, + urlPreviewMaximumContentLength: instance.urlPreviewMaximumContentLength, + urlPreviewRequireContentLength: instance.urlPreviewRequireContentLength, + urlPreviewUserAgent: instance.urlPreviewUserAgent, + urlPreviewSummaryProxyUrl: instance.urlPreviewSummaryProxyUrl, }; }); } diff --git a/packages/backend/src/server/api/endpoints/admin/nsfw-user.ts b/packages/backend/src/server/api/endpoints/admin/nsfw-user.ts index 2a47abe03c..d3fa4251dd 100644 --- a/packages/backend/src/server/api/endpoints/admin/nsfw-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/nsfw-user.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: marie and other Sharkey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { UsersRepository, UserProfilesRepository } from '@/models/_.js'; diff --git a/packages/backend/src/server/api/endpoints/admin/roles/users.ts b/packages/backend/src/server/api/endpoints/admin/roles/users.ts index 45758d4f50..198166bec2 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/users.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/users.ts @@ -89,10 +89,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- .limit(ps.limit) .getMany(); + const _users = assigns.map(({ user, userId }) => user ?? userId); + const _userMap = await this.userEntityService.packMany(_users, me, { schema: 'UserDetailed' }) + .then(users => new Map(users.map(u => [u.id, u]))); return await Promise.all(assigns.map(async assign => ({ id: assign.id, createdAt: this.idService.parse(assign.id).date.toISOString(), - user: await this.userEntityService.pack(assign.user!, me, { schema: 'UserDetailed' }), + user: _userMap.get(assign.userId) ?? await this.userEntityService.pack(assign.user!, me, { schema: 'UserDetailed' }), expiresAt: assign.expiresAt?.toISOString() ?? null, }))); }); diff --git a/packages/backend/src/server/api/endpoints/admin/show-users.ts b/packages/backend/src/server/api/endpoints/admin/show-users.ts index 685da928e3..5f16519403 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-users.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-users.ts @@ -16,7 +16,7 @@ export const meta = { requireCredential: true, requireModerator: true, - kind: 'read:admin:show-users', + kind: 'read:admin:show-user', res: { type: 'array', diff --git a/packages/backend/src/server/api/endpoints/admin/silence-user.ts b/packages/backend/src/server/api/endpoints/admin/silence-user.ts index 007bed5c03..7e6045049a 100644 --- a/packages/backend/src/server/api/endpoints/admin/silence-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/silence-user.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: marie and other Sharkey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { UsersRepository } from '@/models/_.js'; diff --git a/packages/backend/src/server/api/endpoints/admin/unnsfw-user.ts b/packages/backend/src/server/api/endpoints/admin/unnsfw-user.ts index 013e7771ba..26588365e1 100644 --- a/packages/backend/src/server/api/endpoints/admin/unnsfw-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/unnsfw-user.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: marie and other Sharkey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { UsersRepository, UserProfilesRepository } from '@/models/_.js'; diff --git a/packages/backend/src/server/api/endpoints/admin/unsilence-user.ts b/packages/backend/src/server/api/endpoints/admin/unsilence-user.ts index 5e514ccda6..f92be0d8e0 100644 --- a/packages/backend/src/server/api/endpoints/admin/unsilence-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/unsilence-user.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: marie and other Sharkey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { UsersRepository } from '@/models/_.js'; 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 7fea7d969e..015a1e1f7c 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -93,7 +93,6 @@ export const paramDef = { type: 'string', }, }, - summalyProxy: { type: 'string', nullable: true }, deeplAuthKey: { type: 'string', nullable: true }, deeplIsPro: { type: 'boolean' }, deeplFreeMode: { type: 'boolean' }, @@ -114,6 +113,7 @@ export const paramDef = { impressumUrl: { type: 'string', nullable: true }, donationUrl: { type: 'string', nullable: true }, privacyPolicyUrl: { type: 'string', nullable: true }, + inquiryUrl: { type: 'string', nullable: true }, useObjectStorage: { type: 'boolean' }, objectStorageBaseUrl: { type: 'string', nullable: true }, objectStorageBucket: { type: 'string', nullable: true }, @@ -158,6 +158,16 @@ export const paramDef = { type: 'string', }, }, + summalyProxy: { + type: 'string', nullable: true, + description: '[Deprecated] Use "urlPreviewSummaryProxyUrl" instead.', + }, + urlPreviewEnabled: { type: 'boolean' }, + urlPreviewTimeout: { type: 'integer' }, + urlPreviewMaximumContentLength: { type: 'integer' }, + urlPreviewRequireContentLength: { type: 'boolean' }, + urlPreviewUserAgent: { type: 'string', nullable: true }, + urlPreviewSummaryProxyUrl: { type: 'string', nullable: true }, }, required: [], } as const; @@ -357,10 +367,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- set.langs = ps.langs.filter(Boolean); } - if (ps.summalyProxy !== undefined) { - set.summalyProxy = ps.summalyProxy; - } - if (ps.enableEmail !== undefined) { set.enableEmail = ps.enableEmail; } @@ -425,6 +431,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- set.privacyPolicyUrl = ps.privacyPolicyUrl; } + if (ps.inquiryUrl !== undefined) { + set.inquiryUrl = ps.inquiryUrl; + } + if (ps.useObjectStorage !== undefined) { set.useObjectStorage = ps.useObjectStorage; } @@ -609,6 +619,32 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- set.bannedEmailDomains = ps.bannedEmailDomains; } + if (ps.urlPreviewEnabled !== undefined) { + set.urlPreviewEnabled = ps.urlPreviewEnabled; + } + + if (ps.urlPreviewTimeout !== undefined) { + set.urlPreviewTimeout = ps.urlPreviewTimeout; + } + + if (ps.urlPreviewMaximumContentLength !== undefined) { + set.urlPreviewMaximumContentLength = ps.urlPreviewMaximumContentLength; + } + + if (ps.urlPreviewRequireContentLength !== undefined) { + set.urlPreviewRequireContentLength = ps.urlPreviewRequireContentLength; + } + + if (ps.urlPreviewUserAgent !== undefined) { + const value = (ps.urlPreviewUserAgent ?? '').trim(); + set.urlPreviewUserAgent = value === '' ? null : ps.urlPreviewUserAgent; + } + + if (ps.summalyProxy !== undefined || ps.urlPreviewSummaryProxyUrl !== undefined) { + const value = ((ps.urlPreviewSummaryProxyUrl ?? ps.summalyProxy) ?? '').trim(); + set.urlPreviewSummaryProxyUrl = value === '' ? null : value; + } + const before = await this.metaService.fetch(true); await this.metaService.update(set); diff --git a/packages/backend/src/server/api/endpoints/announcements.ts b/packages/backend/src/server/api/endpoints/announcements.ts index 3b12f5b62c..ff8dd73605 100644 --- a/packages/backend/src/server/api/endpoints/announcements.ts +++ b/packages/backend/src/server/api/endpoints/announcements.ts @@ -7,9 +7,9 @@ import { Inject, Injectable } from '@nestjs/common'; import { Brackets } from 'typeorm'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; -import { AnnouncementService } from '@/core/AnnouncementService.js'; +import { AnnouncementEntityService } from '@/core/entities/AnnouncementEntityService.js'; import { DI } from '@/di-symbols.js'; -import type { AnnouncementReadsRepository, AnnouncementsRepository } from '@/models/_.js'; +import type { AnnouncementsRepository } from '@/models/_.js'; export const meta = { tags: ['meta'], @@ -44,11 +44,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- @Inject(DI.announcementsRepository) private announcementsRepository: AnnouncementsRepository, - @Inject(DI.announcementReadsRepository) - private announcementReadsRepository: AnnouncementReadsRepository, - private queryService: QueryService, - private announcementService: AnnouncementService, + private announcementEntityService: AnnouncementEntityService, ) { super(meta, paramDef, async (ps, me) => { const query = this.queryService.makePaginationQuery(this.announcementsRepository.createQueryBuilder('announcement'), ps.sinceId, ps.untilId) @@ -60,7 +57,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- const announcements = await query.limit(ps.limit).getMany(); - return this.announcementService.packMany(announcements, me); + return this.announcementEntityService.packMany(announcements, me); }); } } diff --git a/packages/backend/src/server/api/endpoints/announcements/show.ts b/packages/backend/src/server/api/endpoints/announcements/show.ts new file mode 100644 index 0000000000..6312a0a54c --- /dev/null +++ b/packages/backend/src/server/api/endpoints/announcements/show.ts @@ -0,0 +1,54 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import { EntityNotFoundError } from 'typeorm'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { AnnouncementService } from '@/core/AnnouncementService.js'; +import { ApiError } from '../../error.js'; + +export const meta = { + tags: ['meta'], + + requireCredential: false, + + res: { + type: 'object', + optional: false, nullable: false, + ref: 'Announcement', + }, + + errors: { + noSuchAnnouncement: { + message: 'No such announcement.', + code: 'NO_SUCH_ANNOUNCEMENT', + id: 'b57b5e1d-4f49-404a-9edb-46b00268f121', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + announcementId: { type: 'string', format: 'misskey:id' }, + }, + required: ['announcementId'], +} as const; + +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export + constructor( + private announcementService: AnnouncementService, + ) { + super(meta, paramDef, async (ps, me) => { + try { + return await this.announcementService.getAnnouncement(ps.announcementId, me); + } catch (err) { + if (err instanceof EntityNotFoundError) throw new ApiError(meta.errors.noSuchAnnouncement); + throw err; + } + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/antennas/create.ts b/packages/backend/src/server/api/endpoints/antennas/create.ts index 191de8f833..6b7bacb054 100644 --- a/packages/backend/src/server/api/endpoints/antennas/create.ts +++ b/packages/backend/src/server/api/endpoints/antennas/create.ts @@ -64,11 +64,11 @@ export const paramDef = { } }, caseSensitive: { type: 'boolean' }, localOnly: { type: 'boolean' }, + excludeBots: { type: 'boolean' }, withReplies: { type: 'boolean' }, withFile: { type: 'boolean' }, - notify: { type: 'boolean' }, }, - required: ['name', 'src', 'keywords', 'excludeKeywords', 'users', 'caseSensitive', 'withReplies', 'withFile', 'notify'], + required: ['name', 'src', 'keywords', 'excludeKeywords', 'users', 'caseSensitive', 'withReplies', 'withFile'], } as const; @Injectable() @@ -124,9 +124,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- users: ps.users, caseSensitive: ps.caseSensitive, localOnly: ps.localOnly, + excludeBots: ps.excludeBots, withReplies: ps.withReplies, withFile: ps.withFile, - notify: ps.notify, }).then(x => this.antennasRepository.findOneByOrFail(x.identifiers[0])); this.globalEventService.publishInternalEvent('antennaCreated', antenna); diff --git a/packages/backend/src/server/api/endpoints/antennas/update.ts b/packages/backend/src/server/api/endpoints/antennas/update.ts index 459729f61f..0c30bca9e0 100644 --- a/packages/backend/src/server/api/endpoints/antennas/update.ts +++ b/packages/backend/src/server/api/endpoints/antennas/update.ts @@ -63,11 +63,11 @@ export const paramDef = { } }, caseSensitive: { type: 'boolean' }, localOnly: { type: 'boolean' }, + excludeBots: { type: 'boolean' }, withReplies: { type: 'boolean' }, withFile: { type: 'boolean' }, - notify: { type: 'boolean' }, }, - required: ['antennaId', 'name', 'src', 'keywords', 'excludeKeywords', 'users', 'caseSensitive', 'withReplies', 'withFile', 'notify'], + required: ['antennaId'], } as const; @Injectable() @@ -83,8 +83,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private globalEventService: GlobalEventService, ) { super(meta, paramDef, async (ps, me) => { - if (ps.keywords.flat().every(x => x === '') && ps.excludeKeywords.flat().every(x => x === '')) { - throw new Error('either keywords or excludeKeywords is required.'); + if (ps.keywords && ps.excludeKeywords) { + if (ps.keywords.flat().every(x => x === '') && ps.excludeKeywords.flat().every(x => x === '')) { + throw new Error('either keywords or excludeKeywords is required.'); + } } // Fetch the antenna const antenna = await this.antennasRepository.findOneBy({ @@ -98,7 +100,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- let userList; - if (ps.src === 'list' && ps.userListId) { + if ((ps.src === 'list' || antenna.src === 'list') && ps.userListId) { userList = await this.userListsRepository.findOneBy({ id: ps.userListId, userId: me.id, @@ -112,15 +114,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- await this.antennasRepository.update(antenna.id, { name: ps.name, src: ps.src, - userListId: userList ? userList.id : null, + userListId: ps.userListId !== undefined ? userList ? userList.id : null : undefined, keywords: ps.keywords, excludeKeywords: ps.excludeKeywords, users: ps.users, caseSensitive: ps.caseSensitive, localOnly: ps.localOnly, + excludeBots: ps.excludeBots, withReplies: ps.withReplies, withFile: ps.withFile, - notify: ps.notify, isActive: true, lastUsedAt: new Date(), }); diff --git a/packages/backend/src/server/api/endpoints/channels/timeline.ts b/packages/backend/src/server/api/endpoints/channels/timeline.ts index 8c55673590..9369481649 100644 --- a/packages/backend/src/server/api/endpoints/channels/timeline.ts +++ b/packages/backend/src/server/api/endpoints/channels/timeline.ts @@ -16,6 +16,7 @@ import { MetaService } from '@/core/MetaService.js'; import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; import { MiLocalUser } from '@/models/User.js'; import { ApiError } from '../../error.js'; +import { Brackets } from 'typeorm'; export const meta = { tags: ['notes', 'channels'], @@ -51,6 +52,12 @@ export const paramDef = { sinceDate: { type: 'integer' }, untilDate: { type: 'integer' }, allowPartial: { type: 'boolean', default: false }, // true is recommended but for compatibility false by default + withRenotes: { type: 'boolean', default: true }, + withFiles: { + type: 'boolean', + default: false, + description: 'Only show notes that have attached files.', + }, }, required: ['channelId'], } as const; @@ -89,7 +96,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- if (me) this.activeUsersChart.read(me); if (!serverSettings.enableFanoutTimeline) { - return await this.noteEntityService.packMany(await this.getFromDb({ untilId, sinceId, limit: ps.limit, channelId: channel.id }, me), me); + return await this.noteEntityService.packMany(await this.getFromDb({ untilId, sinceId, limit: ps.limit, channelId: channel.id, withFiles: ps.withFiles, withRenotes: ps.withRenotes }, me), me); } return await this.fanoutTimelineEndpointService.timeline({ @@ -100,9 +107,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- me, useDbFallback: true, redisTimelines: [`channelTimeline:${channel.id}`], - excludePureRenotes: false, + excludePureRenotes: !ps.withRenotes, + excludeNoFiles: ps.withFiles, dbFallback: async (untilId, sinceId, limit) => { - return await this.getFromDb({ untilId, sinceId, limit, channelId: channel.id }, me); + return await this.getFromDb({ untilId, sinceId, limit, channelId: channel.id, withFiles: ps.withFiles, withRenotes: ps.withRenotes }, me); }, }); }); @@ -112,7 +120,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- untilId: string | null, sinceId: string | null, limit: number, - channelId: string + channelId: string, + withFiles: boolean, + withRenotes: boolean, }, me: MiLocalUser | null) { //#region fallback to database const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId) @@ -128,6 +138,20 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- this.queryService.generateMutedUserQuery(query, me); this.queryService.generateBlockedUserQuery(query, me); } + + if (ps.withRenotes === false) { + query.andWhere(new Brackets(qb => { + qb.orWhere('note.renoteId IS NULL'); + qb.orWhere(new Brackets(qb => { + qb.orWhere('note.text IS NOT NULL'); + qb.orWhere('note.fileIds != \'{}\''); + })); + })); + } + + if (ps.withFiles) { + query.andWhere('note.fileIds != \'{}\''); + } //#endregion return await query.limit(ps.limit).getMany(); diff --git a/packages/backend/src/server/api/endpoints/drive/files/find.ts b/packages/backend/src/server/api/endpoints/drive/files/find.ts index 595a6957b2..502d42f9e0 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/find.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/find.ts @@ -54,7 +54,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- folderId: ps.folderId ?? IsNull(), }); - return await Promise.all(files.map(file => this.driveFileEntityService.pack(file, { self: true }))); + return await this.driveFileEntityService.packMany(files, { self: true }); }); } } diff --git a/packages/backend/src/server/api/endpoints/federation/instances.ts b/packages/backend/src/server/api/endpoints/federation/instances.ts index 4064a415a4..c3f2247b69 100644 --- a/packages/backend/src/server/api/endpoints/federation/instances.ts +++ b/packages/backend/src/server/api/endpoints/federation/instances.ts @@ -125,9 +125,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- if (typeof ps.suspended === 'boolean') { if (ps.suspended) { - query.andWhere('instance.isSuspended = TRUE'); + query.andWhere('instance.suspensionState != \'none\''); } else { - query.andWhere('instance.isSuspended = FALSE'); + query.andWhere('instance.suspensionState = \'none\''); } } diff --git a/packages/backend/src/server/api/endpoints/fetch-rss.ts b/packages/backend/src/server/api/endpoints/fetch-rss.ts index 2085b06365..ba48b0119e 100644 --- a/packages/backend/src/server/api/endpoints/fetch-rss.ts +++ b/packages/backend/src/server/api/endpoints/fetch-rss.ts @@ -20,13 +20,188 @@ export const meta = { res: { type: 'object', properties: { + image: { + type: 'object', + optional: true, + properties: { + link: { + type: 'string', + optional: true, + }, + url: { + type: 'string', + optional: false, + }, + title: { + type: 'string', + optional: true, + }, + }, + }, + paginationLinks: { + type: 'object', + optional: true, + properties: { + self: { + type: 'string', + optional: true, + }, + first: { + type: 'string', + optional: true, + }, + next: { + type: 'string', + optional: true, + }, + last: { + type: 'string', + optional: true, + }, + prev: { + type: 'string', + optional: true, + }, + }, + }, + link: { + type: 'string', + optional: true, + }, + title: { + type: 'string', + optional: true, + }, items: { type: 'array', + optional: false, items: { type: 'object', + properties: { + link: { + type: 'string', + optional: true, + }, + guid: { + type: 'string', + optional: true, + }, + title: { + type: 'string', + optional: true, + }, + pubDate: { + type: 'string', + optional: true, + }, + creator: { + type: 'string', + optional: true, + }, + summary: { + type: 'string', + optional: true, + }, + content: { + type: 'string', + optional: true, + }, + isoDate: { + type: 'string', + optional: true, + }, + categories: { + type: 'array', + optional: true, + items: { + type: 'string', + }, + }, + contentSnippet: { + type: 'string', + optional: true, + }, + enclosure: { + type: 'object', + optional: true, + properties: { + url: { + type: 'string', + optional: false, + }, + length: { + type: 'number', + optional: true, + }, + type: { + type: 'string', + optional: true, + }, + }, + }, + }, + }, + }, + feedUrl: { + type: 'string', + optional: true, + }, + description: { + type: 'string', + optional: true, + }, + itunes: { + type: 'object', + optional: true, + additionalProperties: true, + properties: { + image: { + type: 'string', + optional: true, + }, + owner: { + type: 'object', + optional: true, + properties: { + name: { + type: 'string', + optional: true, + }, + email: { + type: 'string', + optional: true, + }, + }, + }, + author: { + type: 'string', + optional: true, + }, + summary: { + type: 'string', + optional: true, + }, + explicit: { + type: 'string', + optional: true, + }, + categories: { + type: 'array', + optional: true, + items: { + type: 'string', + }, + }, + keywords: { + type: 'array', + optional: true, + items: { + type: 'string', + }, + }, }, - } - } + }, + }, }, } as const; diff --git a/packages/backend/src/server/api/endpoints/flash/create.ts b/packages/backend/src/server/api/endpoints/flash/create.ts index 584d167a29..361496e17e 100644 --- a/packages/backend/src/server/api/endpoints/flash/create.ts +++ b/packages/backend/src/server/api/endpoints/flash/create.ts @@ -44,6 +44,7 @@ export const paramDef = { permissions: { type: 'array', items: { type: 'string', } }, + visibility: { type: 'string', enum: ['public', 'private'], default: 'public' }, }, required: ['title', 'summary', 'script', 'permissions'], } as const; @@ -66,6 +67,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- summary: ps.summary, script: ps.script, permissions: ps.permissions, + visibility: ps.visibility, }).then(x => this.flashsRepository.findOneByOrFail(x.identifiers[0])); return await this.flashEntityService.pack(flash); diff --git a/packages/backend/src/server/api/endpoints/following/requests/list.ts b/packages/backend/src/server/api/endpoints/following/requests/list.ts index 88f559138b..fa59e38976 100644 --- a/packages/backend/src/server/api/endpoints/following/requests/list.ts +++ b/packages/backend/src/server/api/endpoints/following/requests/list.ts @@ -71,7 +71,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- .limit(ps.limit) .getMany(); - return await Promise.all(requests.map(req => this.followRequestEntityService.pack(req))); + return await this.followRequestEntityService.packMany(requests, me); }); } } diff --git a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts index 74ee90b3dd..a4e61a0e8f 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts @@ -97,10 +97,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { } const keyInfo = await this.webAuthnService.verifyRegistration(me.id, ps.credential); + const keyId = keyInfo.credentialID; - const credentialId = Buffer.from(keyInfo.credentialID).toString('base64url'); await this.userSecurityKeysRepository.insert({ - id: credentialId, + id: keyId, userId: me.id, name: ps.name, publicKey: Buffer.from(keyInfo.credentialPublicKey).toString('base64url'), @@ -117,7 +117,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { })); return { - id: credentialId, + id: keyId, name: ps.name, }; }); 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 8ddbe5663e..2606108539 100644 --- a/packages/backend/src/server/api/endpoints/i/import-blocking.ts +++ b/packages/backend/src/server/api/endpoints/i/import-blocking.ts @@ -75,7 +75,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- const checkMoving = await this.accountMoveService.validateAlsoKnownAs( me, - (old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > (new Date()).getTime(), + (old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > Date.now(), true, ); if (checkMoving ? file.size > 32 * 1024 * 1024 : file.size > 64 * 1024) throw new ApiError(meta.errors.tooBigFile); 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 390dd9cd71..d5e824df27 100644 --- a/packages/backend/src/server/api/endpoints/i/import-following.ts +++ b/packages/backend/src/server/api/endpoints/i/import-following.ts @@ -75,7 +75,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- const checkMoving = await this.accountMoveService.validateAlsoKnownAs( me, - (old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > (new Date()).getTime(), + (old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > Date.now(), true, ); if (checkMoving ? file.size > 32 * 1024 * 1024 : file.size > 64 * 1024) throw new ApiError(meta.errors.tooBigFile); 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 51a9cdf5a5..0f5800404e 100644 --- a/packages/backend/src/server/api/endpoints/i/import-muting.ts +++ b/packages/backend/src/server/api/endpoints/i/import-muting.ts @@ -75,7 +75,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- const checkMoving = await this.accountMoveService.validateAlsoKnownAs( me, - (old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > (new Date()).getTime(), + (old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > Date.now(), true, ); if (checkMoving ? file.size > 32 * 1024 * 1024 : file.size > 64 * 1024) throw new ApiError(meta.errors.tooBigFile); diff --git a/packages/backend/src/server/api/endpoints/i/import-notes.ts b/packages/backend/src/server/api/endpoints/i/import-notes.ts index 4e00163550..91ef12c3e3 100644 --- a/packages/backend/src/server/api/endpoints/i/import-notes.ts +++ b/packages/backend/src/server/api/endpoints/i/import-notes.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: marie and other Sharkey contributors + * 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'; @@ -59,7 +64,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId }); if (file == null) throw new ApiError(meta.errors.noSuchFile); - + if (file.size === 0) throw new ApiError(meta.errors.emptyFile); if ((await this.roleService.getUserPolicies(me.id)).canImportNotes === false) { 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 a3b67301a7..bacdd5c88f 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 @@ -74,7 +74,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- const checkMoving = await this.accountMoveService.validateAlsoKnownAs( me, - (old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > (new Date()).getTime(), + (old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > Date.now(), true, ); if (checkMoving ? file.size > 32 * 1024 * 1024 : file.size > 64 * 1024) throw new ApiError(meta.errors.tooBigFile); diff --git a/packages/backend/src/server/api/endpoints/i/notifications.ts b/packages/backend/src/server/api/endpoints/i/notifications.ts index 594e8b95c8..5e97b90f99 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 { 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 { 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'; @@ -84,27 +84,51 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- const includeTypes = ps.includeTypes && ps.includeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][]; const excludeTypes = ps.excludeTypes && ps.excludeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][]; - const limit = ps.limit + (ps.untilId ? 1 : 0) + (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); + let sinceTime = ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime().toString() : null; + let untilTime = ps.untilId ? this.idService.parse(ps.untilId).date.getTime().toString() : null; - if (notificationsRes.length === 0) { - return []; - } + let notifications: MiNotification[]; + for (;;) { + let notificationsRes: [id: string, fields: string[]][]; - let notifications = notificationsRes.map(x => JSON.parse(x[1][1])).filter(x => x.id !== ps.untilId && x !== ps.sinceId) as MiNotification[]; + // sinceidのみの場合は古い順、そうでない場合は新しい順。 QueryService.makePaginationQueryも参照 + if (sinceTime && !untilTime) { + notificationsRes = await this.redisClient.xrange( + `notificationTimeline:${me.id}`, + '(' + sinceTime, + '+', + 'COUNT', ps.limit); + } else { + notificationsRes = await this.redisClient.xrevrange( + `notificationTimeline:${me.id}`, + untilTime ? '(' + untilTime : '+', + sinceTime ? '(' + sinceTime : '-', + 'COUNT', ps.limit); + } - 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 (notificationsRes.length === 0) { + return []; + } - if (notifications.length === 0) { - return []; + notifications = notificationsRes.map(x => JSON.parse(x[1][1])) 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) { + // 通知が1件以上ある場合は返す + break; + } + + // フィルタしたことで通知が0件になった場合、次のページを取得する + if (ps.sinceId && !ps.untilId) { + sinceTime = notificationsRes[notificationsRes.length - 1][0]; + } else { + untilTime = notificationsRes[notificationsRes.length - 1][0]; + } } // Mark all as read diff --git a/packages/backend/src/server/api/endpoints/i/registry/get-unsecure.ts b/packages/backend/src/server/api/endpoints/i/registry/get-unsecure.ts index bb471284c9..3aa256077e 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/get-unsecure.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/get-unsecure.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: marie and other Sharkey 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'; diff --git a/packages/backend/src/server/api/endpoints/i/update-email.ts b/packages/backend/src/server/api/endpoints/i/update-email.ts index 08a8301bd1..7332026d84 100644 --- a/packages/backend/src/server/api/endpoints/i/update-email.ts +++ b/packages/backend/src/server/api/endpoints/i/update-email.ts @@ -16,6 +16,7 @@ import { DI } from '@/di-symbols.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { L_CHARS, secureRndstr } from '@/misc/secure-rndstr.js'; import { UserAuthService } from '@/core/UserAuthService.js'; +import { MetaService } from '@/core/MetaService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -40,6 +41,12 @@ export const meta = { code: 'UNAVAILABLE', id: 'a2defefb-f220-8849-0af6-17f816099323', }, + + emailRequired: { + message: 'Email address is required.', + code: 'EMAIL_REQUIRED', + id: '324c7a88-59f2-492f-903f-89134f93e47e', + }, }, res: { @@ -67,6 +74,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, + private metaService: MetaService, private userEntityService: UserEntityService, private emailService: EmailService, private userAuthService: UserAuthService, @@ -98,6 +106,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- if (!res.available) { throw new ApiError(meta.errors.unavailable); } + } else if ((await this.metaService.fetch()).emailRequiredForSignup) { + throw new ApiError(meta.errors.emailRequired); } await this.userProfilesRepository.update(me.id, { diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index 06edb28578..aa2f85845f 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -530,26 +530,32 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private async verifyLink(url: string, user: MiLocalUser) { if (!safeForSql(url)) return; - const html = await this.httpRequestService.getHtml(url); + try { + const html = await this.httpRequestService.getHtml(url); - const { window } = new JSDOM(html); - const doc = window.document; + const { window } = new JSDOM(html); + const doc = window.document; - const myLink = `${this.config.url}/@${user.username}`; + const myLink = `${this.config.url}/@${user.username}`; - const aEls = Array.from(doc.getElementsByTagName('a')); - const linkEls = Array.from(doc.getElementsByTagName('link')); + const aEls = Array.from(doc.getElementsByTagName('a')); + const linkEls = Array.from(doc.getElementsByTagName('link')); - const includesMyLink = aEls.some(a => a.href === myLink); - const includesRelMeLinks = [...aEls, ...linkEls].some(link => link.rel === 'me' && link.href === myLink); + const includesMyLink = aEls.some(a => a.href === myLink); + const includesRelMeLinks = [...aEls, ...linkEls].some(link => link.rel === 'me' && link.href === myLink); - if (includesMyLink || includesRelMeLinks) { - await this.userProfilesRepository.createQueryBuilder('profile').update() - .where('userId = :userId', { userId: user.id }) - .set({ - verifiedLinks: () => `array_append("verifiedLinks", '${url}')`, // ここでSQLインジェクションされそうなのでとりあえず safeForSql で弾いている - }) - .execute(); + if (includesMyLink || includesRelMeLinks) { + await this.userProfilesRepository.createQueryBuilder('profile').update() + .where('userId = :userId', { userId: user.id }) + .set({ + verifiedLinks: () => `array_append("verifiedLinks", '${url}')`, // ここでSQLインジェクションされそうなのでとりあえず safeForSql で弾いている + }) + .execute(); + } + + window.close(); + } catch (err) { + // なにもしない } } } diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index 95ebda2f21..626f03b758 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -16,7 +16,7 @@ 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 { isPureRenote } from '@/misc/is-pure-renote.js'; +import { isQuote, isRenote } from '@/misc/is-renote.js'; import { MetaService } from '@/core/MetaService.js'; import { UtilityService } from '@/core/UtilityService.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; @@ -32,6 +32,7 @@ export const meta = { limit: { duration: ms('1hour'), max: 300, + minInterval: ms('1sec'), }, kind: 'write:notes', @@ -286,7 +287,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- if (renote == null) { throw new ApiError(meta.errors.noSuchRenoteTarget); - } else if (isPureRenote(renote)) { + } else if (isRenote(renote) && !isQuote(renote)) { throw new ApiError(meta.errors.cannotReRenote); } @@ -332,7 +333,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- if (reply == null) { throw new ApiError(meta.errors.noSuchReplyTarget); - } else if (isPureRenote(reply)) { + } else if (isRenote(reply) && !isQuote(reply)) { throw new ApiError(meta.errors.cannotReplyToPureRenote); } else if (!await this.noteEntityService.isVisibleForMe(reply, me.id)) { throw new ApiError(meta.errors.cannotReplyToInvisibleNote); diff --git a/packages/backend/src/server/api/endpoints/notes/edit.ts b/packages/backend/src/server/api/endpoints/notes/edit.ts index 3caeda288b..835cbc14fa 100644 --- a/packages/backend/src/server/api/endpoints/notes/edit.ts +++ b/packages/backend/src/server/api/endpoints/notes/edit.ts @@ -11,7 +11,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { NoteEditService } from '@/core/NoteEditService.js'; import { DI } from '@/di-symbols.js'; -import { isPureRenote } from '@/misc/is-pure-renote.js'; +import { isQuote, isRenote } from '@/misc/is-renote.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; import { ApiError } from '../../error.js'; @@ -336,7 +336,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- if (renote == null) { throw new ApiError(meta.errors.noSuchRenoteTarget); - } else if (isPureRenote(renote)) { + } else if (isRenote(renote) && !isQuote(renote)) { throw new ApiError(meta.errors.cannotReRenote); } @@ -386,7 +386,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- if (reply == null) { throw new ApiError(meta.errors.noSuchReplyTarget); - } else if (isPureRenote(reply)) { + } else if (isRenote(reply) && !isQuote(reply)) { throw new ApiError(meta.errors.cannotReplyToPureRenote); } else if (!await this.noteEntityService.isVisibleForMe(reply, me.id)) { throw new ApiError(meta.errors.cannotReplyToInvisibleNote); diff --git a/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts b/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts index ba38573065..4fd6f8682d 100644 --- a/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts +++ b/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts @@ -32,6 +32,7 @@ export const paramDef = { properties: { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, offset: { type: 'integer', default: 0 }, + excludeChannels: { type: 'boolean', default: false }, }, required: [], } as const; @@ -86,6 +87,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- query.setParameters(mutingQuery.getParameters()); //#endregion + //#region exclude channels + if (ps.excludeChannels) { + query.andWhere('poll.channelId IS NULL'); + } + //#endregion + const polls = await query .orderBy('poll.noteId', 'DESC') .limit(ps.limit) diff --git a/packages/backend/src/server/api/endpoints/notes/reactions.ts b/packages/backend/src/server/api/endpoints/notes/reactions.ts index 3beb5064ae..7e334df93e 100644 --- a/packages/backend/src/server/api/endpoints/notes/reactions.ts +++ b/packages/backend/src/server/api/endpoints/notes/reactions.ts @@ -79,7 +79,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- const reactions = await query.limit(ps.limit).getMany(); - return await Promise.all(reactions.map(reaction => this.noteReactionEntityService.pack(reaction, me))); + return await this.noteReactionEntityService.packMany(reactions, me); }); } } diff --git a/packages/backend/src/server/api/endpoints/notes/translate.ts b/packages/backend/src/server/api/endpoints/notes/translate.ts index a935f761b7..d6ef655291 100644 --- a/packages/backend/src/server/api/endpoints/notes/translate.ts +++ b/packages/backend/src/server/api/endpoints/notes/translate.ts @@ -21,7 +21,7 @@ export const meta = { res: { type: 'object', - optional: false, nullable: false, + optional: true, nullable: false, properties: { sourceLang: { type: 'string' }, text: { type: 'string' }, @@ -39,6 +39,11 @@ export const meta = { code: 'NO_SUCH_NOTE', id: 'bea9b03f-36e0-49c5-a4db-627a029f8971', }, + cannotTranslateInvisibleNote: { + message: 'Cannot translate invisible note.', + code: 'CANNOT_TRANSLATE_INVISIBLE_NOTE', + id: 'ea29f2ca-c368-43b3-aaf1-5ac3e74bbe5d', + }, }, } as const; @@ -72,21 +77,21 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- }); if (!(await this.noteEntityService.isVisibleForMe(note, me.id))) { - return 204; // TODO: 良い感じのエラー返す + throw new ApiError(meta.errors.cannotTranslateInvisibleNote); } if (note.text == null) { - return 204; + return; } const instance = await this.metaService.fetch(); if (instance.deeplAuthKey == null && !instance.deeplFreeMode) { - return 204; // TODO: 良い感じのエラー返す + throw new ApiError(meta.errors.unavailable); } if (instance.deeplFreeMode && !instance.deeplFreeInstance) { - return 204; + throw new ApiError(meta.errors.unavailable); } let targetLang = ps.targetLang; diff --git a/packages/backend/src/server/api/endpoints/roles/users.ts b/packages/backend/src/server/api/endpoints/roles/users.ts index 85d100ce1c..48d350af59 100644 --- a/packages/backend/src/server/api/endpoints/roles/users.ts +++ b/packages/backend/src/server/api/endpoints/roles/users.ts @@ -92,9 +92,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- .limit(ps.limit) .getMany(); + const _users = assigns.map(({ user, userId }) => user ?? userId); + const _userMap = await this.userEntityService.packMany(_users, me, { schema: 'UserDetailed' }) + .then(users => new Map(users.map(u => [u.id, u]))); return await Promise.all(assigns.map(async assign => ({ id: assign.id, - user: await this.userEntityService.pack(assign.user!, me, { schema: 'UserDetailed' }), + user: _userMap.get(assign.userId) ?? await this.userEntityService.pack(assign.user!, me, { schema: 'UserDetailed' }), }))); }); } diff --git a/packages/backend/src/server/api/endpoints/sponsors.ts b/packages/backend/src/server/api/endpoints/sponsors.ts index 50e1c594f2..b6ccb9b2f9 100644 --- a/packages/backend/src/server/api/endpoints/sponsors.ts +++ b/packages/backend/src/server/api/endpoints/sponsors.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: marie and other Sharkey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { Inject, Injectable } from '@nestjs/common'; import * as Redis from 'ioredis'; import { Endpoint } from '@/server/api/endpoint-base.js'; diff --git a/packages/backend/src/server/api/endpoints/users/following.ts b/packages/backend/src/server/api/endpoints/users/following.ts index 5d52ebba76..6b3389f0b2 100644 --- a/packages/backend/src/server/api/endpoints/users/following.ts +++ b/packages/backend/src/server/api/endpoints/users/following.ts @@ -6,6 +6,7 @@ import { IsNull } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; import type { UsersRepository, FollowingsRepository, UserProfilesRepository } from '@/models/_.js'; +import { birthdaySchema } from '@/models/User.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { FollowingEntityService } from '@/core/entities/FollowingEntityService.js'; @@ -66,7 +67,7 @@ export const paramDef = { description: 'The local host is represented with `null`.', }, - birthday: { type: 'string', nullable: true }, + birthday: { ...birthdaySchema, nullable: true }, }, anyOf: [ { required: ['userId'] }, @@ -127,9 +128,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- if (ps.birthday) { try { - const d = new Date(ps.birthday); - d.setHours(0, 0, 0, 0); - const birthday = `${(d.getMonth() + 1).toString().padStart(2, '0')}-${d.getDate().toString().padStart(2, '0')}`; + const birthday = ps.birthday.substring(5, 10); const birthdayUserQuery = this.userProfilesRepository.createQueryBuilder('user_profile'); birthdayUserQuery.select('user_profile.userId') .where(`SUBSTR(user_profile.birthday, 6, 5) = '${birthday}'`); diff --git a/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts b/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts index 02aa037466..9248a2fa68 100644 --- a/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts +++ b/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts @@ -118,12 +118,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- const repliedUsersSorted = Object.keys(repliedUsers).sort((a, b) => repliedUsers[b] - repliedUsers[a]); // Extract top replied users - const topRepliedUsers = repliedUsersSorted.slice(0, ps.limit); + const topRepliedUserIds = repliedUsersSorted.slice(0, ps.limit); // Make replies object (includes weights) - const repliesObj = await Promise.all(topRepliedUsers.map(async (user) => ({ - user: await this.userEntityService.pack(user, me, { schema: 'UserDetailed' }), - weight: repliedUsers[user] / peak, + const _userMap = await this.userEntityService.packMany(topRepliedUserIds, me, { schema: 'UserDetailed' }) + .then(users => new Map(users.map(u => [u.id, u]))); + const repliesObj = await Promise.all(topRepliedUserIds.map(async (userId) => ({ + user: _userMap.get(userId) ?? await this.userEntityService.pack(userId, me, { schema: 'UserDetailed' }), + weight: repliedUsers[userId] / peak, }))); return repliesObj; diff --git a/packages/backend/src/server/api/endpoints/users/relation.ts b/packages/backend/src/server/api/endpoints/users/relation.ts index 6a5b2262fa..1d75437b81 100644 --- a/packages/backend/src/server/api/endpoints/users/relation.ts +++ b/packages/backend/src/server/api/endpoints/users/relation.ts @@ -132,11 +132,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private userEntityService: UserEntityService, ) { super(meta, paramDef, async (ps, me) => { - const ids = Array.isArray(ps.userId) ? ps.userId : [ps.userId]; - - const relations = await Promise.all(ids.map(id => this.userEntityService.getRelation(me.id, id))); - - return Array.isArray(ps.userId) ? relations : relations[0]; + return Array.isArray(ps.userId) + ? await this.userEntityService.getRelations(me.id, ps.userId).then(it => [...it.values()]) + : await this.userEntityService.getRelation(me.id, ps.userId).then(it => [it]); }); } } diff --git a/packages/backend/src/server/api/endpoints/users/show.ts b/packages/backend/src/server/api/endpoints/users/show.ts index bd81989cb9..062326e28d 100644 --- a/packages/backend/src/server/api/endpoints/users/show.ts +++ b/packages/backend/src/server/api/endpoints/users/show.ts @@ -110,14 +110,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- }); // リクエストされた通りに並べ替え + // 順番は保持されるけど数は減ってる可能性がある const _users: MiUser[] = []; for (const id of ps.userIds) { - _users.push(users.find(x => x.id === id)!); + const user = users.find(x => x.id === id); + if (user != null) _users.push(user); } - return await Promise.all(_users.map(u => this.userEntityService.pack(u, me, { - schema: 'UserDetailed', - }))); + const _userMap = await this.userEntityService.packMany(_users, me, { schema: 'UserDetailed' }) + .then(users => new Map(users.map(u => [u.id, u]))); + return _users.map(u => _userMap.get(u.id)!); } else { // Lookup user if (typeof ps.host === 'string' && typeof ps.username === 'string') { |