summaryrefslogtreecommitdiff
path: root/packages/backend/src/server/api/endpoints
diff options
context:
space:
mode:
authordakkar <dakkar@thenautilus.net>2024-03-02 16:36:49 +0000
committerAmelia Yukii <amelia.yukii@shourai.de>2024-03-02 16:36:49 +0000
commitaf548d05ca821725eabd5a96241d3228b92186f0 (patch)
tree6d23f6739482466abcc71965cd83f9bbfb2c3ae0 /packages/backend/src/server/api/endpoints
parentmerge: Fix Images in ReadMe (!445) (diff)
downloadsharkey-af548d05ca821725eabd5a96241d3228b92186f0.tar.gz
sharkey-af548d05ca821725eabd5a96241d3228b92186f0.tar.bz2
sharkey-af548d05ca821725eabd5a96241d3228b92186f0.zip
merge upstream for 2024.2.1
Diffstat (limited to 'packages/backend/src/server/api/endpoints')
-rw-r--r--packages/backend/src/server/api/endpoints/admin/emoji/add.ts5
-rw-r--r--packages/backend/src/server/api/endpoints/admin/emoji/update.ts27
-rw-r--r--packages/backend/src/server/api/endpoints/following/create.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/notifications-grouped.ts15
-rw-r--r--packages/backend/src/server/api/endpoints/i/notifications.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/update.ts6
-rw-r--r--packages/backend/src/server/api/endpoints/notes/create.test.ts5
-rw-r--r--packages/backend/src/server/api/endpoints/notes/create.ts23
-rw-r--r--packages/backend/src/server/api/endpoints/notes/edit.ts125
-rw-r--r--packages/backend/src/server/api/endpoints/notifications/flush.ts33
-rw-r--r--packages/backend/src/server/api/endpoints/users/reactions.ts2
11 files changed, 172 insertions, 73 deletions
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts
index ce3eb22a32..767e517b80 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts
@@ -31,7 +31,10 @@ export const meta = {
},
},
- ref: 'EmojiDetailed',
+ res: {
+ type: 'object',
+ ref: 'EmojiDetailed',
+ },
} as const;
export const paramDef = {
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts
index a9ff4236d2..22609a16a3 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts
@@ -57,7 +57,10 @@ export const paramDef = {
type: 'string',
} },
},
- required: ['id', 'name', 'aliases'],
+ anyOf: [
+ { required: ['id'] },
+ { required: ['name'] },
+ ],
} as const;
@Injectable()
@@ -70,27 +73,33 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
) {
super(meta, paramDef, async (ps, me) => {
let driveFile;
-
if (ps.fileId) {
driveFile = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
if (driveFile == null) throw new ApiError(meta.errors.noSuchFile);
}
- const emoji = await this.customEmojiService.getEmojiById(ps.id);
- if (emoji != null) {
- if (ps.name !== emoji.name) {
+
+ let emojiId;
+ if (ps.id) {
+ emojiId = ps.id;
+ const emoji = await this.customEmojiService.getEmojiById(ps.id);
+ if (!emoji) throw new ApiError(meta.errors.noSuchEmoji);
+ if (ps.name && (ps.name !== emoji.name)) {
const isDuplicate = await this.customEmojiService.checkDuplicate(ps.name);
if (isDuplicate) throw new ApiError(meta.errors.sameNameEmojiExists);
}
} else {
- throw new ApiError(meta.errors.noSuchEmoji);
+ if (!ps.name) throw new Error('Invalid Params unexpectedly passed. This is a BUG. Please report it to the development team.');
+ const emoji = await this.customEmojiService.getEmojiByName(ps.name);
+ if (!emoji) throw new ApiError(meta.errors.noSuchEmoji);
+ emojiId = emoji.id;
}
- await this.customEmojiService.update(ps.id, {
+ await this.customEmojiService.update(emojiId, {
driveFile,
name: ps.name,
- category: ps.category ?? null,
+ category: ps.category,
aliases: ps.aliases,
- license: ps.license ?? null,
+ license: ps.license,
isSensitive: ps.isSensitive,
localOnly: ps.localOnly,
roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction,
diff --git a/packages/backend/src/server/api/endpoints/following/create.ts b/packages/backend/src/server/api/endpoints/following/create.ts
index 042d7f119d..db320e7129 100644
--- a/packages/backend/src/server/api/endpoints/following/create.ts
+++ b/packages/backend/src/server/api/endpoints/following/create.ts
@@ -71,7 +71,7 @@ export const paramDef = {
type: 'object',
properties: {
userId: { type: 'string', format: 'misskey:id' },
- withReplies: { type: 'boolean' }
+ withReplies: { type: 'boolean' },
},
required: ['userId'],
} as const;
diff --git a/packages/backend/src/server/api/endpoints/i/notifications-grouped.ts b/packages/backend/src/server/api/endpoints/i/notifications-grouped.ts
index d400cbc1c9..ad4577be58 100644
--- a/packages/backend/src/server/api/endpoints/i/notifications-grouped.ts
+++ b/packages/backend/src/server/api/endpoints/i/notifications-grouped.ts
@@ -3,11 +3,11 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { Brackets, In } from 'typeorm';
+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 { obsoleteNotificationTypes, groupedNotificationTypes, FilterUnionByProperty } from '@/types.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { NoteReadService } from '@/core/NoteReadService.js';
import { NotificationEntityService } from '@/core/entities/NotificationEntityService.js';
@@ -48,10 +48,10 @@ export const paramDef = {
markAsRead: { type: 'boolean', default: true },
// 後方互換のため、廃止された通知タイプも受け付ける
includeTypes: { type: 'array', items: {
- type: 'string', enum: [...notificationTypes, ...obsoleteNotificationTypes],
+ type: 'string', enum: [...groupedNotificationTypes, ...obsoleteNotificationTypes],
} },
excludeTypes: { type: 'array', items: {
- type: 'string', enum: [...notificationTypes, ...obsoleteNotificationTypes],
+ type: 'string', enum: [...groupedNotificationTypes, ...obsoleteNotificationTypes],
} },
},
required: [],
@@ -79,12 +79,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
return [];
}
// excludeTypes に全指定されている場合はクエリしない
- if (notificationTypes.every(type => ps.excludeTypes?.includes(type))) {
+ if (groupedNotificationTypes.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 includeTypes = ps.includeTypes && ps.includeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof groupedNotificationTypes[number][];
+ const excludeTypes = ps.excludeTypes && ps.excludeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof groupedNotificationTypes[number][];
const limit = (ps.limit + EXTRA_LIMIT) + (ps.untilId ? 1 : 0) + (ps.sinceId ? 1 : 0); // untilIdに指定したものも含まれるため+1
const notificationsRes = await this.redisClient.xrevrange(
@@ -162,7 +162,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
groupedNotifications = groupedNotifications.slice(0, ps.limit);
-
const noteIds = groupedNotifications
.filter((notification): notification is FilterUnionByProperty<MiNotification, 'type', 'mention' | 'reply' | 'quote' | 'edited'> => ['mention', 'reply', 'quote', 'edited'].includes(notification.type))
.map(notification => notification.noteId!);
diff --git a/packages/backend/src/server/api/endpoints/i/notifications.ts b/packages/backend/src/server/api/endpoints/i/notifications.ts
index d9404ea9c5..594e8b95c8 100644
--- a/packages/backend/src/server/api/endpoints/i/notifications.ts
+++ b/packages/backend/src/server/api/endpoints/i/notifications.ts
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { Brackets, In } from 'typeorm';
+import { In } from 'typeorm';
import * as Redis from 'ioredis';
import { Inject, Injectable } from '@nestjs/common';
import type { NotesRepository } from '@/models/_.js';
diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts
index 2c04b65733..06edb28578 100644
--- a/packages/backend/src/server/api/endpoints/i/update.ts
+++ b/packages/backend/src/server/api/endpoints/i/update.ts
@@ -488,9 +488,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
this.hashtagService.updateUsertags(user, tags);
//#endregion
- if (Object.keys(updates).length > 0) await this.usersRepository.update(user.id, updates);
- if (Object.keys(updates).includes('alsoKnownAs')) {
- this.cacheService.uriPersonCache.set(this.userEntityService.genLocalUserUri(user.id), { ...user, ...updates });
+ if (Object.keys(updates).length > 0) {
+ await this.usersRepository.update(user.id, updates);
+ this.globalEventService.publishInternalEvent('localUserUpdated', { id: user.id });
}
await this.userProfilesRepository.update(user.id, {
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 38183144a9..f3d887bb20 100644
--- a/packages/backend/src/server/api/endpoints/notes/create.test.ts
+++ b/packages/backend/src/server/api/endpoints/notes/create.test.ts
@@ -44,11 +44,6 @@ describe('api:notes/create', () => {
.toBe(INVALID);
});
- test('over 3000 characters post', async () => {
- expect(v({ text: await tooLong }))
- .toBe(INVALID);
- });
-
test('whitespace-only post', () => {
expect(v({ text: ' ' }))
.toBe(INVALID);
diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts
index aff3867091..95ebda2f21 100644
--- a/packages/backend/src/server/api/endpoints/notes/create.ts
+++ b/packages/backend/src/server/api/endpoints/notes/create.ts
@@ -91,6 +91,12 @@ export const meta = {
id: '3ac74a84-8fd5-4bb0-870f-01804f82ce16',
},
+ cannotReplyToSpecifiedVisibilityNoteWithExtendedVisibility: {
+ message: 'You cannot reply to a specified visibility note with extended visibility.',
+ code: 'CANNOT_REPLY_TO_SPECIFIED_VISIBILITY_NOTE_WITH_EXTENDED_VISIBILITY',
+ id: 'ed940410-535c-4d5e-bfa3-af798671e93c',
+ },
+
cannotCreateAlreadyExpiredPoll: {
message: 'Poll is already expired.',
code: 'CANNOT_CREATE_ALREADY_EXPIRED_POLL',
@@ -126,6 +132,12 @@ export const meta = {
code: 'CONTAINS_PROHIBITED_WORDS',
id: 'aa6e01d3-a85c-669d-758a-76aab43af334',
},
+
+ containsTooManyMentions: {
+ message: 'Cannot post because it exceeds the allowed number of mentions.',
+ code: 'CONTAINS_TOO_MANY_MENTIONS',
+ id: '4de0363a-3046-481b-9b0f-feff3e211025',
+ },
},
} as const;
@@ -323,7 +335,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
} else if (isPureRenote(reply)) {
throw new ApiError(meta.errors.cannotReplyToPureRenote);
} else if (!await this.noteEntityService.isVisibleForMe(reply, me.id)) {
- throw new ApiError(meta.errors.noSuchReplyTarget);
+ throw new ApiError(meta.errors.cannotReplyToInvisibleNote);
+ } else if (reply.visibility === 'specified' && ps.visibility !== 'specified') {
+ throw new ApiError(meta.errors.cannotReplyToSpecifiedVisibilityNoteWithExtendedVisibility);
}
// Check blocking
@@ -389,9 +403,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
} catch (e) {
// TODO: 他のErrorもここでキャッチしてエラーメッセージを当てるようにしたい
if (e instanceof IdentifiableError) {
- if (e.id === '689ee33f-f97c-479a-ac49-1b9f8140af99') throw new ApiError(meta.errors.containsProhibitedWords);
+ if (e.id === '689ee33f-f97c-479a-ac49-1b9f8140af99') {
+ throw new ApiError(meta.errors.containsProhibitedWords);
+ } else if (e.id === '9f466dab-c856-48cd-9e65-ff90ff750580') {
+ throw new ApiError(meta.errors.containsTooManyMentions);
+ }
}
-
throw e;
}
});
diff --git a/packages/backend/src/server/api/endpoints/notes/edit.ts b/packages/backend/src/server/api/endpoints/notes/edit.ts
index 590853e9cd..3caeda288b 100644
--- a/packages/backend/src/server/api/endpoints/notes/edit.ts
+++ b/packages/backend/src/server/api/endpoints/notes/edit.ts
@@ -11,6 +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 { IdentifiableError } from '@/misc/identifiable-error.js';
import { ApiError } from '../../error.js';
@@ -19,6 +20,8 @@ export const meta = {
requireCredential: true,
+ prohibitMoved: true,
+
limit: {
duration: ms('1hour'),
max: 300,
@@ -53,18 +56,42 @@ export const meta = {
id: 'fd4cc33e-2a37-48dd-99cc-9b806eb2031a',
},
+ cannotRenoteDueToVisibility: {
+ message: 'You can not Renote due to target visibility.',
+ code: 'CANNOT_RENOTE_DUE_TO_VISIBILITY',
+ id: 'be9529e9-fe72-4de0-ae43-0b363c4938af',
+ },
+
noSuchReplyTarget: {
message: 'No such reply target.',
code: 'NO_SUCH_REPLY_TARGET',
id: '749ee0f6-d3da-459a-bf02-282e2da4292c',
},
+ cannotReplyToInvisibleNote: {
+ message: 'You cannot reply to an invisible Note.',
+ code: 'CANNOT_REPLY_TO_AN_INVISIBLE_NOTE',
+ id: 'b98980fa-3780-406c-a935-b6d0eeee10d1',
+ },
+
cannotReplyToPureRenote: {
message: 'You can not reply to a pure Renote.',
code: 'CANNOT_REPLY_TO_A_PURE_RENOTE',
id: '3ac74a84-8fd5-4bb0-870f-01804f82ce15',
},
+ maxLength: {
+ message: 'You tried posting a note which is too long.',
+ code: 'MAX_LENGTH',
+ id: '3ac74a84-8fd5-4bb0-870f-01804f82ce16',
+ },
+
+ cannotReplyToSpecifiedVisibilityNoteWithExtendedVisibility: {
+ message: 'You cannot reply to a specified visibility note with extended visibility.',
+ code: 'CANNOT_REPLY_TO_SPECIFIED_VISIBILITY_NOTE_WITH_EXTENDED_VISIBILITY',
+ id: 'ed940410-535c-4d5e-bfa3-af798671e93c',
+ },
+
cannotCreateAlreadyExpiredPoll: {
message: 'Poll is already expired.',
code: 'CANNOT_CREATE_ALREADY_EXPIRED_POLL',
@@ -83,6 +110,12 @@ export const meta = {
id: 'b390d7e1-8a5e-46ed-b625-06271cafd3d3',
},
+ noSuchFile: {
+ message: 'Some files are not found.',
+ code: 'NO_SUCH_FILE',
+ id: 'b6992544-63e7-67f0-fa7f-32444b1b5306',
+ },
+
accountLocked: {
message: 'You migrated. Your account is now locked.',
code: 'ACCOUNT_LOCKED',
@@ -137,17 +170,17 @@ export const meta = {
id: '33510210-8452-094c-6227-4a6c05d99f02',
},
- maxLength: {
- message: 'You tried posting a note which is too long.',
- code: 'MAX_LENGTH',
- id: '3ac74a84-8fd5-4bb0-870f-01804f82ce16',
- },
-
containsProhibitedWords: {
message: 'Cannot post because it contains prohibited words.',
code: 'CONTAINS_PROHIBITED_WORDS',
id: 'aa6e01d3-a85c-669d-758a-76aab43af334',
},
+
+ containsTooManyMentions: {
+ message: 'Cannot post because it exceeds the allowed number of mentions.',
+ code: 'CONTAINS_TOO_MANY_MENTIONS',
+ id: '4de0363a-3046-481b-9b0f-feff3e211025',
+ },
},
} as const;
@@ -201,7 +234,7 @@ export const paramDef = {
uniqueItems: true,
minItems: 2,
maxItems: 10,
- items: { type: 'string', minLength: 1, maxLength: 50 },
+ items: { type: 'string', minLength: 1, maxLength: 150 },
},
multiple: { type: 'boolean' },
expiresAt: { type: 'integer', nullable: true },
@@ -210,38 +243,33 @@ export const paramDef = {
required: ['choices'],
},
},
- anyOf: [
- {
- // (re)note with text, files and poll are optional
- properties: {
- text: {
- type: 'string',
- minLength: 1,
- nullable: false,
- },
+ // (re)note with text, files and poll are optional
+ if: {
+ properties: {
+ renoteId: {
+ type: 'null',
},
- required: ['text'],
- },
- {
- // (re)note with files, text and poll are optional
- required: ['fileIds'],
- },
- {
- // (re)note with files, text and poll are optional
- required: ['mediaIds'],
- },
- {
- // (re)note with poll, text and files are optional
- properties: {
- poll: { type: 'object', nullable: false },
+ fileIds: {
+ type: 'null',
+ },
+ mediaIds: {
+ type: 'null',
+ },
+ poll: {
+ type: 'null',
},
- required: ['poll'],
},
- {
- // pure renote
- required: ['renoteId'],
+ },
+ then: {
+ properties: {
+ text: {
+ type: 'string',
+ minLength: 1,
+ pattern: '[^\\s]+',
+ },
},
- ],
+ required: ['text'],
+ },
} as const;
@Injectable()
@@ -292,7 +320,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.getMany();
if (files.length !== fileIds.length) {
- throw new ApiError(meta.errors.noSuchNote);
+ throw new ApiError(meta.errors.noSuchFile);
}
}
@@ -301,14 +329,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (ps.renoteId === ps.editId) {
throw new ApiError(meta.errors.cannotQuoteCurrentPost);
}
-
+
if (ps.renoteId != null) {
// Fetch renote to note
renote = await this.notesRepository.findOneBy({ id: ps.renoteId });
if (renote == null) {
throw new ApiError(meta.errors.noSuchRenoteTarget);
- } else if (renote.renoteId && !renote.text && !renote.fileIds && !renote.hasPoll) {
+ } else if (isPureRenote(renote)) {
throw new ApiError(meta.errors.cannotReRenote);
}
@@ -329,6 +357,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
}
+ if (renote.visibility === 'followers' && renote.userId !== me.id) {
+ // 他人のfollowers noteはreject
+ throw new ApiError(meta.errors.cannotRenoteDueToVisibility);
+ } else if (renote.visibility === 'specified') {
+ // specified / direct noteはreject
+ throw new ApiError(meta.errors.cannotRenoteDueToVisibility);
+ }
+
if (renote.channelId && renote.channelId !== ps.channelId) {
// チャンネルのノートに対しリノート要求がきたとき、チャンネル外へのリノート可否をチェック
// リノートのユースケースのうち、チャンネル内→チャンネル外は少数だと考えられるため、JOINはせず必要な時に都度取得する
@@ -350,8 +386,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (reply == null) {
throw new ApiError(meta.errors.noSuchReplyTarget);
- } else if (reply.renoteId && !reply.text && !reply.fileIds && !reply.hasPoll) {
+ } else if (isPureRenote(reply)) {
throw new ApiError(meta.errors.cannotReplyToPureRenote);
+ } else if (!await this.noteEntityService.isVisibleForMe(reply, me.id)) {
+ throw new ApiError(meta.errors.cannotReplyToInvisibleNote);
+ } else if (reply.visibility === 'specified' && ps.visibility !== 'specified') {
+ throw new ApiError(meta.errors.cannotReplyToSpecifiedVisibilityNoteWithExtendedVisibility);
}
// Check blocking
@@ -415,9 +455,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
} catch (e) {
// TODO: 他のErrorもここでキャッチしてエラーメッセージを当てるようにしたい
if (e instanceof IdentifiableError) {
- if (e.id === '689ee33f-f97c-479a-ac49-1b9f8140af99') throw new ApiError(meta.errors.containsProhibitedWords);
+ if (e.id === '689ee33f-f97c-479a-ac49-1b9f8140af99') {
+ throw new ApiError(meta.errors.containsProhibitedWords);
+ } else if (e.id === '9f466dab-c856-48cd-9e65-ff90ff750580') {
+ throw new ApiError(meta.errors.containsTooManyMentions);
+ }
}
-
throw e;
}
});
diff --git a/packages/backend/src/server/api/endpoints/notifications/flush.ts b/packages/backend/src/server/api/endpoints/notifications/flush.ts
new file mode 100644
index 0000000000..47c0642fd1
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/notifications/flush.ts
@@ -0,0 +1,33 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { NotificationService } from '@/core/NotificationService.js';
+
+export const meta = {
+ tags: ['notifications', 'account'],
+
+ requireCredential: true,
+
+ kind: 'write:notifications',
+} 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 notificationService: NotificationService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ this.notificationService.flushAllNotifications(me.id);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/users/reactions.ts b/packages/backend/src/server/api/endpoints/users/reactions.ts
index e20d896248..aca883a052 100644
--- a/packages/backend/src/server/api/endpoints/users/reactions.ts
+++ b/packages/backend/src/server/api/endpoints/users/reactions.ts
@@ -98,7 +98,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.limit(ps.limit)
.getMany();
- return await Promise.all(reactions.map(reaction => this.noteReactionEntityService.pack(reaction, me, { withNote: true })));
+ return await this.noteReactionEntityService.packMany(reactions, me, { withNote: true });
});
}
}