summaryrefslogtreecommitdiff
path: root/packages/backend/src/core
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2023-04-06 11:14:43 +0900
committersyuilo <Syuilotan@yahoo.co.jp>2023-04-06 11:14:43 +0900
commit73203a3d72b355e3c230c46771292ff9520675c0 (patch)
tree5c6a2861d3c1fa65392e583229985d7cdc1b03ef /packages/backend/src/core
parentUpdate CHANGELOG.md (diff)
downloadsharkey-73203a3d72b355e3c230c46771292ff9520675c0.tar.gz
sharkey-73203a3d72b355e3c230c46771292ff9520675c0.tar.bz2
sharkey-73203a3d72b355e3c230c46771292ff9520675c0.zip
perf(backend): cache local custom emojis
Diffstat (limited to 'packages/backend/src/core')
-rw-r--r--packages/backend/src/core/CustomEmojiService.ts187
-rw-r--r--packages/backend/src/core/InstanceActorService.ts6
-rw-r--r--packages/backend/src/core/NoteCreateService.ts4
-rw-r--r--packages/backend/src/core/ReactionService.ts42
-rw-r--r--packages/backend/src/core/RelayService.ts6
-rw-r--r--packages/backend/src/core/RoleService.ts6
-rw-r--r--packages/backend/src/core/activitypub/ApRendererService.ts19
-rw-r--r--packages/backend/src/core/entities/NoteEntityService.ts26
8 files changed, 221 insertions, 75 deletions
diff --git a/packages/backend/src/core/CustomEmojiService.ts b/packages/backend/src/core/CustomEmojiService.ts
index 1c3b60e5d7..604a94707f 100644
--- a/packages/backend/src/core/CustomEmojiService.ts
+++ b/packages/backend/src/core/CustomEmojiService.ts
@@ -1,24 +1,28 @@
import { Inject, Injectable } from '@nestjs/common';
import { DataSource, In, IsNull } from 'typeorm';
+import Redis from 'ioredis';
import { DI } from '@/di-symbols.js';
import { IdService } from '@/core/IdService.js';
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import type { DriveFile } from '@/models/entities/DriveFile.js';
import type { Emoji } from '@/models/entities/Emoji.js';
-import type { EmojisRepository, Note } from '@/models/index.js';
+import type { EmojisRepository } from '@/models/index.js';
import { bindThis } from '@/decorators.js';
-import { MemoryKVCache } from '@/misc/cache.js';
+import { MemoryKVCache, RedisSingleCache } from '@/misc/cache.js';
import { UtilityService } from '@/core/UtilityService.js';
import type { Config } from '@/config.js';
-import { ReactionService } from '@/core/ReactionService.js';
import { query } from '@/misc/prelude/url.js';
@Injectable()
export class CustomEmojiService {
private cache: MemoryKVCache<Emoji | null>;
+ public localEmojisCache: RedisSingleCache<Map<string, Emoji>>;
constructor(
+ @Inject(DI.redis)
+ private redisClient: Redis.Redis,
+
@Inject(DI.config)
private config: Config,
@@ -32,9 +36,16 @@ export class CustomEmojiService {
private idService: IdService,
private emojiEntityService: EmojiEntityService,
private globalEventService: GlobalEventService,
- private reactionService: ReactionService,
) {
this.cache = new MemoryKVCache<Emoji | null>(1000 * 60 * 60 * 12);
+
+ this.localEmojisCache = new RedisSingleCache<Map<string, Emoji>>(this.redisClient, 'localEmojis', {
+ lifetime: 1000 * 60 * 30, // 30m
+ memoryCacheLifetime: 1000 * 60 * 3, // 3m
+ fetcher: () => this.emojisRepository.find({ where: { host: IsNull() } }).then(emojis => new Map(emojis.map(emoji => [emoji.name, emoji]))),
+ toRedisConverter: (value) => JSON.stringify(value.values()),
+ fromRedisConverter: (value) => new Map(JSON.parse(value).map((x: Emoji) => [x.name, x])), // TODO: Date型の変換
+ });
}
@bindThis
@@ -60,7 +71,7 @@ export class CustomEmojiService {
}).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
if (data.host == null) {
- await this.db.queryResultCache?.remove(['meta_emojis']);
+ this.localEmojisCache.refresh();
this.globalEventService.publishBroadcastStream('emojiAdded', {
emoji: await this.emojiEntityService.packDetailed(emoji.id),
@@ -71,6 +82,146 @@ export class CustomEmojiService {
}
@bindThis
+ public async update(id: Emoji['id'], data: {
+ name?: string;
+ category?: string | null;
+ aliases?: string[];
+ license?: string | null;
+ }): Promise<void> {
+ const emoji = await this.emojisRepository.findOneByOrFail({ id: id });
+ const sameNameEmoji = await this.emojisRepository.findOneBy({ name: data.name, host: IsNull() });
+ if (sameNameEmoji != null && sameNameEmoji.id !== id) throw new Error('name already exists');
+
+ await this.emojisRepository.update(emoji.id, {
+ updatedAt: new Date(),
+ name: data.name,
+ category: data.category,
+ aliases: data.aliases,
+ license: data.license,
+ });
+
+ this.localEmojisCache.refresh();
+
+ const updated = await this.emojiEntityService.packDetailed(emoji.id);
+
+ if (emoji.name === data.name) {
+ this.globalEventService.publishBroadcastStream('emojiUpdated', {
+ emojis: [updated],
+ });
+ } else {
+ this.globalEventService.publishBroadcastStream('emojiDeleted', {
+ emojis: [await this.emojiEntityService.packDetailed(emoji)],
+ });
+
+ this.globalEventService.publishBroadcastStream('emojiAdded', {
+ emoji: updated,
+ });
+ }
+ }
+
+ @bindThis
+ public async addAliasesBulk(ids: Emoji['id'][], aliases: string[]) {
+ const emojis = await this.emojisRepository.findBy({
+ id: In(ids),
+ });
+
+ for (const emoji of emojis) {
+ await this.emojisRepository.update(emoji.id, {
+ updatedAt: new Date(),
+ aliases: [...new Set(emoji.aliases.concat(aliases))],
+ });
+ }
+
+ this.localEmojisCache.refresh();
+
+ this.globalEventService.publishBroadcastStream('emojiUpdated', {
+ emojis: await this.emojiEntityService.packDetailedMany(ids),
+ });
+ }
+
+ @bindThis
+ public async setAliasesBulk(ids: Emoji['id'][], aliases: string[]) {
+ await this.emojisRepository.update({
+ id: In(ids),
+ }, {
+ updatedAt: new Date(),
+ aliases: aliases,
+ });
+
+ this.localEmojisCache.refresh();
+
+ this.globalEventService.publishBroadcastStream('emojiUpdated', {
+ emojis: await this.emojiEntityService.packDetailedMany(ids),
+ });
+ }
+
+ @bindThis
+ public async removeAliasesBulk(ids: Emoji['id'][], aliases: string[]) {
+ const emojis = await this.emojisRepository.findBy({
+ id: In(ids),
+ });
+
+ for (const emoji of emojis) {
+ await this.emojisRepository.update(emoji.id, {
+ updatedAt: new Date(),
+ aliases: emoji.aliases.filter(x => !aliases.includes(x)),
+ });
+ }
+
+ this.localEmojisCache.refresh();
+
+ this.globalEventService.publishBroadcastStream('emojiUpdated', {
+ emojis: await this.emojiEntityService.packDetailedMany(ids),
+ });
+ }
+
+ @bindThis
+ public async setCategoryBulk(ids: Emoji['id'][], category: string | null) {
+ await this.emojisRepository.update({
+ id: In(ids),
+ }, {
+ updatedAt: new Date(),
+ category: category,
+ });
+
+ this.localEmojisCache.refresh();
+
+ this.globalEventService.publishBroadcastStream('emojiUpdated', {
+ emojis: await this.emojiEntityService.packDetailedMany(ids),
+ });
+ }
+
+ @bindThis
+ public async delete(id: Emoji['id']) {
+ const emoji = await this.emojisRepository.findOneByOrFail({ id: id });
+
+ await this.emojisRepository.delete(emoji.id);
+
+ this.localEmojisCache.refresh();
+
+ this.globalEventService.publishBroadcastStream('emojiDeleted', {
+ emojis: [await this.emojiEntityService.packDetailed(emoji)],
+ });
+ }
+
+ @bindThis
+ public async deleteBulk(ids: Emoji['id'][]) {
+ const emojis = await this.emojisRepository.findBy({
+ id: In(ids),
+ });
+
+ for (const emoji of emojis) {
+ await this.emojisRepository.delete(emoji.id);
+ }
+
+ this.localEmojisCache.refresh();
+
+ this.globalEventService.publishBroadcastStream('emojiDeleted', {
+ emojis: await this.emojiEntityService.packDetailedMany(emojis),
+ });
+ }
+
+ @bindThis
private normalizeHost(src: string | undefined, noteUserHost: string | null): string | null {
// クエリに使うホスト
let host = src === '.' ? null // .はローカルホスト (ここがマッチするのはリアクションのみ)
@@ -84,7 +235,7 @@ export class CustomEmojiService {
}
@bindThis
- private parseEmojiStr(emojiName: string, noteUserHost: string | null) {
+ public parseEmojiStr(emojiName: string, noteUserHost: string | null) {
const match = emojiName.match(/^(\w+)(?:@([\w.-]+))?$/);
if (!match) return { name: null, host: null };
@@ -143,30 +294,6 @@ export class CustomEmojiService {
return res;
}
- @bindThis
- public aggregateNoteEmojis(notes: Note[]) {
- let emojis: { name: string | null; host: string | null; }[] = [];
- for (const note of notes) {
- emojis = emojis.concat(note.emojis
- .map(e => this.parseEmojiStr(e, note.userHost)));
- if (note.renote) {
- emojis = emojis.concat(note.renote.emojis
- .map(e => this.parseEmojiStr(e, note.renote!.userHost)));
- if (note.renote.user) {
- emojis = emojis.concat(note.renote.user.emojis
- .map(e => this.parseEmojiStr(e, note.renote!.userHost)));
- }
- }
- const customReactions = Object.keys(note.reactions).map(x => this.reactionService.decodeReaction(x)).filter(x => x.name != null) as typeof emojis;
- emojis = emojis.concat(customReactions);
- if (note.user) {
- emojis = emojis.concat(note.user.emojis
- .map(e => this.parseEmojiStr(e, note.userHost)));
- }
- }
- return emojis.filter(x => x.name != null && x.host != null) as { name: string; host: string; }[];
- }
-
/**
* 与えられた絵文字のリストをデータベースから取得し、キャッシュに追加します
*/
diff --git a/packages/backend/src/core/InstanceActorService.ts b/packages/backend/src/core/InstanceActorService.ts
index 898fb4ce85..4fb3fc5b4f 100644
--- a/packages/backend/src/core/InstanceActorService.ts
+++ b/packages/backend/src/core/InstanceActorService.ts
@@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { IsNull } from 'typeorm';
import type { LocalUser } from '@/models/entities/User.js';
import type { UsersRepository } from '@/models/index.js';
-import { MemoryCache } from '@/misc/cache.js';
+import { MemorySingleCache } from '@/misc/cache.js';
import { DI } from '@/di-symbols.js';
import { CreateSystemUserService } from '@/core/CreateSystemUserService.js';
import { bindThis } from '@/decorators.js';
@@ -11,7 +11,7 @@ const ACTOR_USERNAME = 'instance.actor' as const;
@Injectable()
export class InstanceActorService {
- private cache: MemoryCache<LocalUser>;
+ private cache: MemorySingleCache<LocalUser>;
constructor(
@Inject(DI.usersRepository)
@@ -19,7 +19,7 @@ export class InstanceActorService {
private createSystemUserService: CreateSystemUserService,
) {
- this.cache = new MemoryCache<LocalUser>(Infinity);
+ this.cache = new MemorySingleCache<LocalUser>(Infinity);
}
@bindThis
diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts
index fcc17ace1e..5c4d13f178 100644
--- a/packages/backend/src/core/NoteCreateService.ts
+++ b/packages/backend/src/core/NoteCreateService.ts
@@ -20,7 +20,7 @@ import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js
import { checkWordMute } from '@/misc/check-word-mute.js';
import type { Channel } from '@/models/entities/Channel.js';
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
-import { MemoryCache } from '@/misc/cache.js';
+import { MemorySingleCache } from '@/misc/cache.js';
import type { UserProfile } from '@/models/entities/UserProfile.js';
import { RelayService } from '@/core/RelayService.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
@@ -47,7 +47,7 @@ import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js';
import { RoleService } from '@/core/RoleService.js';
import { MetaService } from '@/core/MetaService.js';
-const mutedWordsCache = new MemoryCache<{ userId: UserProfile['userId']; mutedWords: UserProfile['mutedWords']; }[]>(1000 * 60 * 5);
+const mutedWordsCache = new MemorySingleCache<{ userId: UserProfile['userId']; mutedWords: UserProfile['mutedWords']; }[]>(1000 * 60 * 5);
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
diff --git a/packages/backend/src/core/ReactionService.ts b/packages/backend/src/core/ReactionService.ts
index 97a0b5ee66..a274b19e4b 100644
--- a/packages/backend/src/core/ReactionService.ts
+++ b/packages/backend/src/core/ReactionService.ts
@@ -1,7 +1,6 @@
import { Inject, Injectable } from '@nestjs/common';
-import { IsNull } from 'typeorm';
import { DI } from '@/di-symbols.js';
-import type { EmojisRepository, BlockingsRepository, NoteReactionsRepository, UsersRepository, NotesRepository } from '@/models/index.js';
+import type { EmojisRepository, NoteReactionsRepository, UsersRepository, NotesRepository } from '@/models/index.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import type { RemoteUser, User } from '@/models/entities/User.js';
import type { Note } from '@/models/entities/Note.js';
@@ -20,6 +19,7 @@ import { MetaService } from '@/core/MetaService.js';
import { bindThis } from '@/decorators.js';
import { UtilityService } from '@/core/UtilityService.js';
import { UserBlockingService } from '@/core/UserBlockingService.js';
+import { CustomEmojiService } from '@/core/CustomEmojiService.js';
const FALLBACK = '❤';
@@ -60,9 +60,6 @@ export class ReactionService {
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
- @Inject(DI.blockingsRepository)
- private blockingsRepository: BlockingsRepository,
-
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
@@ -74,6 +71,7 @@ export class ReactionService {
private utilityService: UtilityService,
private metaService: MetaService,
+ private customEmojiService: CustomEmojiService,
private userEntityService: UserEntityService,
private noteEntityService: NoteEntityService,
private userBlockingService: UserBlockingService,
@@ -104,7 +102,6 @@ export class ReactionService {
if (note.reactionAcceptance === 'likeOnly' || ((note.reactionAcceptance === 'likeOnlyForRemote') && (user.host != null))) {
reaction = '❤️';
} else {
- // TODO: cache
reaction = await this.toDbReaction(reaction, user.host);
}
@@ -158,21 +155,22 @@ export class ReactionService {
// カスタム絵文字リアクションだったら絵文字情報も送る
const decodedReaction = this.decodeReaction(reaction);
- // TODO: Cache
- const emoji = await this.emojisRepository.findOne({
- where: {
- name: decodedReaction.name,
- host: decodedReaction.host ?? IsNull(),
- },
- select: ['name', 'host', 'originalUrl', 'publicUrl'],
- });
+ const customEmoji = decodedReaction.name == null ? null : decodedReaction.host == null
+ ? (await this.customEmojiService.localEmojisCache.fetch()).get(decodedReaction.name)
+ : await this.emojisRepository.findOne(
+ {
+ where: {
+ name: decodedReaction.name,
+ host: decodedReaction.host,
+ },
+ });
this.globalEventService.publishNoteStream(note.id, 'reacted', {
reaction: decodedReaction.reaction,
- emoji: emoji != null ? {
- name: emoji.host ? `${emoji.name}@${emoji.host}` : `${emoji.name}@.`,
+ emoji: customEmoji != null ? {
+ name: customEmoji.host ? `${customEmoji.name}@${customEmoji.host}` : `${customEmoji.name}@.`,
// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
- url: emoji.publicUrl || emoji.originalUrl,
+ url: customEmoji.publicUrl || customEmoji.originalUrl,
} : null,
userId: user.id,
});
@@ -311,10 +309,12 @@ export class ReactionService {
const custom = reaction.match(/^:([\w+-]+)(?:@\.)?:$/);
if (custom) {
const name = custom[1];
- const emoji = await this.emojisRepository.findOneBy({
- host: reacterHost ?? IsNull(),
- name,
- });
+ const emoji = reacterHost == null
+ ? (await this.customEmojiService.localEmojisCache.fetch()).get(name)
+ : await this.emojisRepository.findOneBy({
+ host: reacterHost,
+ name,
+ });
if (emoji) return reacterHost ? `:${name}@${reacterHost}:` : `:${name}:`;
}
diff --git a/packages/backend/src/core/RelayService.ts b/packages/backend/src/core/RelayService.ts
index 4df7fb3bff..9d34d82be2 100644
--- a/packages/backend/src/core/RelayService.ts
+++ b/packages/backend/src/core/RelayService.ts
@@ -3,7 +3,7 @@ import { IsNull } from 'typeorm';
import type { LocalUser, User } from '@/models/entities/User.js';
import type { RelaysRepository, UsersRepository } from '@/models/index.js';
import { IdService } from '@/core/IdService.js';
-import { MemoryCache } from '@/misc/cache.js';
+import { MemorySingleCache } from '@/misc/cache.js';
import type { Relay } from '@/models/entities/Relay.js';
import { QueueService } from '@/core/QueueService.js';
import { CreateSystemUserService } from '@/core/CreateSystemUserService.js';
@@ -16,7 +16,7 @@ const ACTOR_USERNAME = 'relay.actor' as const;
@Injectable()
export class RelayService {
- private relaysCache: MemoryCache<Relay[]>;
+ private relaysCache: MemorySingleCache<Relay[]>;
constructor(
@Inject(DI.usersRepository)
@@ -30,7 +30,7 @@ export class RelayService {
private createSystemUserService: CreateSystemUserService,
private apRendererService: ApRendererService,
) {
- this.relaysCache = new MemoryCache<Relay[]>(1000 * 60 * 10);
+ this.relaysCache = new MemorySingleCache<Relay[]>(1000 * 60 * 10);
}
@bindThis
diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts
index 52e6292a1e..54e098ea52 100644
--- a/packages/backend/src/core/RoleService.ts
+++ b/packages/backend/src/core/RoleService.ts
@@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
import Redis from 'ioredis';
import { In } from 'typeorm';
import type { Role, RoleAssignment, RoleAssignmentsRepository, RolesRepository, UsersRepository } from '@/models/index.js';
-import { MemoryKVCache, MemoryCache } from '@/misc/cache.js';
+import { MemoryKVCache, MemorySingleCache } from '@/misc/cache.js';
import type { User } from '@/models/entities/User.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
@@ -57,7 +57,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
@Injectable()
export class RoleService implements OnApplicationShutdown {
- private rolesCache: MemoryCache<Role[]>;
+ private rolesCache: MemorySingleCache<Role[]>;
private roleAssignmentByUserIdCache: MemoryKVCache<RoleAssignment[]>;
public static AlreadyAssignedError = class extends Error {};
@@ -84,7 +84,7 @@ export class RoleService implements OnApplicationShutdown {
) {
//this.onMessage = this.onMessage.bind(this);
- this.rolesCache = new MemoryCache<Role[]>(Infinity);
+ this.rolesCache = new MemorySingleCache<Role[]>(Infinity);
this.roleAssignmentByUserIdCache = new MemoryKVCache<RoleAssignment[]>(Infinity);
this.redisSubscriber.on('message', this.onMessage);
diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts
index 15512c8f47..b250b796d6 100644
--- a/packages/backend/src/core/activitypub/ApRendererService.ts
+++ b/packages/backend/src/core/activitypub/ApRendererService.ts
@@ -21,6 +21,8 @@ import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.j
import type { UserKeypair } from '@/models/entities/UserKeypair.js';
import type { UsersRepository, UserProfilesRepository, NotesRepository, DriveFilesRepository, EmojisRepository, PollsRepository } from '@/models/index.js';
import { bindThis } from '@/decorators.js';
+import { CustomEmojiService } from '@/core/CustomEmojiService.js';
+import { isNotNull } from '@/misc/is-not-null.js';
import { LdSignatureService } from './LdSignatureService.js';
import { ApMfmService } from './ApMfmService.js';
import type { IAccept, IActivity, IAdd, IAnnounce, IApDocument, IApEmoji, IApHashtag, IApImage, IApMention, IBlock, ICreate, IDelete, IFlag, IFollow, IKey, ILike, IObject, IPost, IQuestion, IReject, IRemove, ITombstone, IUndo, IUpdate } from './type.js';
@@ -50,6 +52,7 @@ export class ApRendererService {
@Inject(DI.pollsRepository)
private pollsRepository: PollsRepository,
+ private customEmojiService: CustomEmojiService,
private userEntityService: UserEntityService,
private driveFileEntityService: DriveFileEntityService,
private ldSignatureService: LdSignatureService,
@@ -272,11 +275,7 @@ export class ApRendererService {
if (reaction.startsWith(':')) {
const name = reaction.replaceAll(':', '');
- // TODO: cache
- const emoji = await this.emojisRepository.findOneBy({
- name,
- host: IsNull(),
- });
+ const emoji = (await this.customEmojiService.localEmojisCache.fetch()).get(name);
if (emoji) object.tag = [this.renderEmoji(emoji)];
}
@@ -701,13 +700,9 @@ export class ApRendererService {
private async getEmojis(names: string[]): Promise<Emoji[]> {
if (names == null || names.length === 0) return [];
- const emojis = await Promise.all(
- names.map(name => this.emojisRepository.findOneBy({
- name,
- host: IsNull(),
- })),
- );
+ const allEmojis = await this.customEmojiService.localEmojisCache.fetch();
+ const emojis = names.map(name => allEmojis.get(name)).filter(isNotNull);
- return emojis.filter(emoji => emoji != null) as Emoji[];
+ return emojis;
}
}
diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts
index 5660600692..94b3029c58 100644
--- a/packages/backend/src/core/entities/NoteEntityService.ts
+++ b/packages/backend/src/core/entities/NoteEntityService.ts
@@ -406,7 +406,7 @@ export class NoteEntityService implements OnModuleInit {
}
}
- await this.customEmojiService.prefetchEmojis(this.customEmojiService.aggregateNoteEmojis(notes));
+ await this.customEmojiService.prefetchEmojis(this.aggregateNoteEmojis(notes));
// TODO: 本当は renote とか reply がないのに renoteId とか replyId があったらここで解決しておく
const fileIds = notes.map(n => [n.fileIds, n.renote?.fileIds, n.reply?.fileIds]).flat(2).filter(isNotNull);
const packedFiles = await this.driveFileEntityService.packManyByIdsMap(fileIds);
@@ -421,6 +421,30 @@ export class NoteEntityService implements OnModuleInit {
}
@bindThis
+ public aggregateNoteEmojis(notes: Note[]) {
+ let emojis: { name: string | null; host: string | null; }[] = [];
+ for (const note of notes) {
+ emojis = emojis.concat(note.emojis
+ .map(e => this.customEmojiService.parseEmojiStr(e, note.userHost)));
+ if (note.renote) {
+ emojis = emojis.concat(note.renote.emojis
+ .map(e => this.customEmojiService.parseEmojiStr(e, note.renote!.userHost)));
+ if (note.renote.user) {
+ emojis = emojis.concat(note.renote.user.emojis
+ .map(e => this.customEmojiService.parseEmojiStr(e, note.renote!.userHost)));
+ }
+ }
+ const customReactions = Object.keys(note.reactions).map(x => this.reactionService.decodeReaction(x)).filter(x => x.name != null) as typeof emojis;
+ emojis = emojis.concat(customReactions);
+ if (note.user) {
+ emojis = emojis.concat(note.user.emojis
+ .map(e => this.customEmojiService.parseEmojiStr(e, note.userHost)));
+ }
+ }
+ return emojis.filter(x => x.name != null && x.host != null) as { name: string; host: string; }[];
+ }
+
+ @bindThis
public async countSameRenotes(userId: string, renoteId: string, excludeNoteId: string | undefined): Promise<number> {
// 指定したユーザーの指定したノートのリノートがいくつあるか数える
const query = this.notesRepository.createQueryBuilder('note')