perf(backend): cache local custom emojis
This commit is contained in:
parent
437de6417e
commit
73203a3d72
20 changed files with 335 additions and 310 deletions
|
@ -1,24 +1,28 @@
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { DataSource, In, IsNull } from 'typeorm';
|
import { DataSource, In, IsNull } from 'typeorm';
|
||||||
|
import Redis from 'ioredis';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
import type { DriveFile } from '@/models/entities/DriveFile.js';
|
import type { DriveFile } from '@/models/entities/DriveFile.js';
|
||||||
import type { Emoji } from '@/models/entities/Emoji.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 { bindThis } from '@/decorators.js';
|
||||||
import { MemoryKVCache } from '@/misc/cache.js';
|
import { MemoryKVCache, RedisSingleCache } from '@/misc/cache.js';
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { ReactionService } from '@/core/ReactionService.js';
|
|
||||||
import { query } from '@/misc/prelude/url.js';
|
import { query } from '@/misc/prelude/url.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CustomEmojiService {
|
export class CustomEmojiService {
|
||||||
private cache: MemoryKVCache<Emoji | null>;
|
private cache: MemoryKVCache<Emoji | null>;
|
||||||
|
public localEmojisCache: RedisSingleCache<Map<string, Emoji>>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(DI.redis)
|
||||||
|
private redisClient: Redis.Redis,
|
||||||
|
|
||||||
@Inject(DI.config)
|
@Inject(DI.config)
|
||||||
private config: Config,
|
private config: Config,
|
||||||
|
|
||||||
|
@ -32,9 +36,16 @@ export class CustomEmojiService {
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private emojiEntityService: EmojiEntityService,
|
private emojiEntityService: EmojiEntityService,
|
||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
private reactionService: ReactionService,
|
|
||||||
) {
|
) {
|
||||||
this.cache = new MemoryKVCache<Emoji | null>(1000 * 60 * 60 * 12);
|
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
|
@bindThis
|
||||||
|
@ -60,7 +71,7 @@ export class CustomEmojiService {
|
||||||
}).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
|
}).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
|
||||||
|
|
||||||
if (data.host == null) {
|
if (data.host == null) {
|
||||||
await this.db.queryResultCache?.remove(['meta_emojis']);
|
this.localEmojisCache.refresh();
|
||||||
|
|
||||||
this.globalEventService.publishBroadcastStream('emojiAdded', {
|
this.globalEventService.publishBroadcastStream('emojiAdded', {
|
||||||
emoji: await this.emojiEntityService.packDetailed(emoji.id),
|
emoji: await this.emojiEntityService.packDetailed(emoji.id),
|
||||||
|
@ -70,6 +81,146 @@ export class CustomEmojiService {
|
||||||
return emoji;
|
return emoji;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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
|
@bindThis
|
||||||
private normalizeHost(src: string | undefined, noteUserHost: string | null): string | null {
|
private normalizeHost(src: string | undefined, noteUserHost: string | null): string | null {
|
||||||
// クエリに使うホスト
|
// クエリに使うホスト
|
||||||
|
@ -84,7 +235,7 @@ export class CustomEmojiService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private parseEmojiStr(emojiName: string, noteUserHost: string | null) {
|
public parseEmojiStr(emojiName: string, noteUserHost: string | null) {
|
||||||
const match = emojiName.match(/^(\w+)(?:@([\w.-]+))?$/);
|
const match = emojiName.match(/^(\w+)(?:@([\w.-]+))?$/);
|
||||||
if (!match) return { name: null, host: null };
|
if (!match) return { name: null, host: null };
|
||||||
|
|
||||||
|
@ -143,30 +294,6 @@ export class CustomEmojiService {
|
||||||
return res;
|
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; }[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 与えられた絵文字のリストをデータベースから取得し、キャッシュに追加します
|
* 与えられた絵文字のリストをデータベースから取得し、キャッシュに追加します
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { IsNull } from 'typeorm';
|
import { IsNull } from 'typeorm';
|
||||||
import type { LocalUser } from '@/models/entities/User.js';
|
import type { LocalUser } from '@/models/entities/User.js';
|
||||||
import type { UsersRepository } from '@/models/index.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 { DI } from '@/di-symbols.js';
|
||||||
import { CreateSystemUserService } from '@/core/CreateSystemUserService.js';
|
import { CreateSystemUserService } from '@/core/CreateSystemUserService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
@ -11,7 +11,7 @@ const ACTOR_USERNAME = 'instance.actor' as const;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class InstanceActorService {
|
export class InstanceActorService {
|
||||||
private cache: MemoryCache<LocalUser>;
|
private cache: MemorySingleCache<LocalUser>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.usersRepository)
|
||||||
|
@ -19,7 +19,7 @@ export class InstanceActorService {
|
||||||
|
|
||||||
private createSystemUserService: CreateSystemUserService,
|
private createSystemUserService: CreateSystemUserService,
|
||||||
) {
|
) {
|
||||||
this.cache = new MemoryCache<LocalUser>(Infinity);
|
this.cache = new MemorySingleCache<LocalUser>(Infinity);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
|
|
@ -20,7 +20,7 @@ import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js
|
||||||
import { checkWordMute } from '@/misc/check-word-mute.js';
|
import { checkWordMute } from '@/misc/check-word-mute.js';
|
||||||
import type { Channel } from '@/models/entities/Channel.js';
|
import type { Channel } from '@/models/entities/Channel.js';
|
||||||
import { normalizeForSearch } from '@/misc/normalize-for-search.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 type { UserProfile } from '@/models/entities/UserProfile.js';
|
||||||
import { RelayService } from '@/core/RelayService.js';
|
import { RelayService } from '@/core/RelayService.js';
|
||||||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.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 { RoleService } from '@/core/RoleService.js';
|
||||||
import { MetaService } from '@/core/MetaService.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';
|
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { IsNull } from 'typeorm';
|
|
||||||
import { DI } from '@/di-symbols.js';
|
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 { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||||
import type { RemoteUser, User } from '@/models/entities/User.js';
|
import type { RemoteUser, User } from '@/models/entities/User.js';
|
||||||
import type { Note } from '@/models/entities/Note.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 { bindThis } from '@/decorators.js';
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||||
|
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||||
|
|
||||||
const FALLBACK = '❤';
|
const FALLBACK = '❤';
|
||||||
|
|
||||||
|
@ -60,9 +60,6 @@ export class ReactionService {
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.usersRepository)
|
||||||
private usersRepository: UsersRepository,
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
@Inject(DI.blockingsRepository)
|
|
||||||
private blockingsRepository: BlockingsRepository,
|
|
||||||
|
|
||||||
@Inject(DI.notesRepository)
|
@Inject(DI.notesRepository)
|
||||||
private notesRepository: NotesRepository,
|
private notesRepository: NotesRepository,
|
||||||
|
|
||||||
|
@ -74,6 +71,7 @@ export class ReactionService {
|
||||||
|
|
||||||
private utilityService: UtilityService,
|
private utilityService: UtilityService,
|
||||||
private metaService: MetaService,
|
private metaService: MetaService,
|
||||||
|
private customEmojiService: CustomEmojiService,
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private noteEntityService: NoteEntityService,
|
private noteEntityService: NoteEntityService,
|
||||||
private userBlockingService: UserBlockingService,
|
private userBlockingService: UserBlockingService,
|
||||||
|
@ -104,7 +102,6 @@ export class ReactionService {
|
||||||
if (note.reactionAcceptance === 'likeOnly' || ((note.reactionAcceptance === 'likeOnlyForRemote') && (user.host != null))) {
|
if (note.reactionAcceptance === 'likeOnly' || ((note.reactionAcceptance === 'likeOnlyForRemote') && (user.host != null))) {
|
||||||
reaction = '❤️';
|
reaction = '❤️';
|
||||||
} else {
|
} else {
|
||||||
// TODO: cache
|
|
||||||
reaction = await this.toDbReaction(reaction, user.host);
|
reaction = await this.toDbReaction(reaction, user.host);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -158,21 +155,22 @@ export class ReactionService {
|
||||||
// カスタム絵文字リアクションだったら絵文字情報も送る
|
// カスタム絵文字リアクションだったら絵文字情報も送る
|
||||||
const decodedReaction = this.decodeReaction(reaction);
|
const decodedReaction = this.decodeReaction(reaction);
|
||||||
|
|
||||||
// TODO: Cache
|
const customEmoji = decodedReaction.name == null ? null : decodedReaction.host == null
|
||||||
const emoji = await this.emojisRepository.findOne({
|
? (await this.customEmojiService.localEmojisCache.fetch()).get(decodedReaction.name)
|
||||||
where: {
|
: await this.emojisRepository.findOne(
|
||||||
name: decodedReaction.name,
|
{
|
||||||
host: decodedReaction.host ?? IsNull(),
|
where: {
|
||||||
},
|
name: decodedReaction.name,
|
||||||
select: ['name', 'host', 'originalUrl', 'publicUrl'],
|
host: decodedReaction.host,
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
|
||||||
this.globalEventService.publishNoteStream(note.id, 'reacted', {
|
this.globalEventService.publishNoteStream(note.id, 'reacted', {
|
||||||
reaction: decodedReaction.reaction,
|
reaction: decodedReaction.reaction,
|
||||||
emoji: emoji != null ? {
|
emoji: customEmoji != null ? {
|
||||||
name: emoji.host ? `${emoji.name}@${emoji.host}` : `${emoji.name}@.`,
|
name: customEmoji.host ? `${customEmoji.name}@${customEmoji.host}` : `${customEmoji.name}@.`,
|
||||||
// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
|
// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
|
||||||
url: emoji.publicUrl || emoji.originalUrl,
|
url: customEmoji.publicUrl || customEmoji.originalUrl,
|
||||||
} : null,
|
} : null,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
|
@ -311,10 +309,12 @@ export class ReactionService {
|
||||||
const custom = reaction.match(/^:([\w+-]+)(?:@\.)?:$/);
|
const custom = reaction.match(/^:([\w+-]+)(?:@\.)?:$/);
|
||||||
if (custom) {
|
if (custom) {
|
||||||
const name = custom[1];
|
const name = custom[1];
|
||||||
const emoji = await this.emojisRepository.findOneBy({
|
const emoji = reacterHost == null
|
||||||
host: reacterHost ?? IsNull(),
|
? (await this.customEmojiService.localEmojisCache.fetch()).get(name)
|
||||||
name,
|
: await this.emojisRepository.findOneBy({
|
||||||
});
|
host: reacterHost,
|
||||||
|
name,
|
||||||
|
});
|
||||||
|
|
||||||
if (emoji) return reacterHost ? `:${name}@${reacterHost}:` : `:${name}:`;
|
if (emoji) return reacterHost ? `:${name}@${reacterHost}:` : `:${name}:`;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { IsNull } from 'typeorm';
|
||||||
import type { LocalUser, User } from '@/models/entities/User.js';
|
import type { LocalUser, User } from '@/models/entities/User.js';
|
||||||
import type { RelaysRepository, UsersRepository } from '@/models/index.js';
|
import type { RelaysRepository, UsersRepository } from '@/models/index.js';
|
||||||
import { IdService } from '@/core/IdService.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 type { Relay } from '@/models/entities/Relay.js';
|
||||||
import { QueueService } from '@/core/QueueService.js';
|
import { QueueService } from '@/core/QueueService.js';
|
||||||
import { CreateSystemUserService } from '@/core/CreateSystemUserService.js';
|
import { CreateSystemUserService } from '@/core/CreateSystemUserService.js';
|
||||||
|
@ -16,7 +16,7 @@ const ACTOR_USERNAME = 'relay.actor' as const;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RelayService {
|
export class RelayService {
|
||||||
private relaysCache: MemoryCache<Relay[]>;
|
private relaysCache: MemorySingleCache<Relay[]>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.usersRepository)
|
||||||
|
@ -30,7 +30,7 @@ export class RelayService {
|
||||||
private createSystemUserService: CreateSystemUserService,
|
private createSystemUserService: CreateSystemUserService,
|
||||||
private apRendererService: ApRendererService,
|
private apRendererService: ApRendererService,
|
||||||
) {
|
) {
|
||||||
this.relaysCache = new MemoryCache<Relay[]>(1000 * 60 * 10);
|
this.relaysCache = new MemorySingleCache<Relay[]>(1000 * 60 * 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||||
import Redis from 'ioredis';
|
import Redis from 'ioredis';
|
||||||
import { In } from 'typeorm';
|
import { In } from 'typeorm';
|
||||||
import type { Role, RoleAssignment, RoleAssignmentsRepository, RolesRepository, UsersRepository } from '@/models/index.js';
|
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 type { User } from '@/models/entities/User.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
@ -57,7 +57,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RoleService implements OnApplicationShutdown {
|
export class RoleService implements OnApplicationShutdown {
|
||||||
private rolesCache: MemoryCache<Role[]>;
|
private rolesCache: MemorySingleCache<Role[]>;
|
||||||
private roleAssignmentByUserIdCache: MemoryKVCache<RoleAssignment[]>;
|
private roleAssignmentByUserIdCache: MemoryKVCache<RoleAssignment[]>;
|
||||||
|
|
||||||
public static AlreadyAssignedError = class extends Error {};
|
public static AlreadyAssignedError = class extends Error {};
|
||||||
|
@ -84,7 +84,7 @@ export class RoleService implements OnApplicationShutdown {
|
||||||
) {
|
) {
|
||||||
//this.onMessage = this.onMessage.bind(this);
|
//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.roleAssignmentByUserIdCache = new MemoryKVCache<RoleAssignment[]>(Infinity);
|
||||||
|
|
||||||
this.redisSubscriber.on('message', this.onMessage);
|
this.redisSubscriber.on('message', this.onMessage);
|
||||||
|
|
|
@ -21,6 +21,8 @@ import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.j
|
||||||
import type { UserKeypair } from '@/models/entities/UserKeypair.js';
|
import type { UserKeypair } from '@/models/entities/UserKeypair.js';
|
||||||
import type { UsersRepository, UserProfilesRepository, NotesRepository, DriveFilesRepository, EmojisRepository, PollsRepository } from '@/models/index.js';
|
import type { UsersRepository, UserProfilesRepository, NotesRepository, DriveFilesRepository, EmojisRepository, PollsRepository } from '@/models/index.js';
|
||||||
import { bindThis } from '@/decorators.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 { LdSignatureService } from './LdSignatureService.js';
|
||||||
import { ApMfmService } from './ApMfmService.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';
|
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)
|
@Inject(DI.pollsRepository)
|
||||||
private pollsRepository: PollsRepository,
|
private pollsRepository: PollsRepository,
|
||||||
|
|
||||||
|
private customEmojiService: CustomEmojiService,
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private driveFileEntityService: DriveFileEntityService,
|
private driveFileEntityService: DriveFileEntityService,
|
||||||
private ldSignatureService: LdSignatureService,
|
private ldSignatureService: LdSignatureService,
|
||||||
|
@ -272,11 +275,7 @@ export class ApRendererService {
|
||||||
|
|
||||||
if (reaction.startsWith(':')) {
|
if (reaction.startsWith(':')) {
|
||||||
const name = reaction.replaceAll(':', '');
|
const name = reaction.replaceAll(':', '');
|
||||||
// TODO: cache
|
const emoji = (await this.customEmojiService.localEmojisCache.fetch()).get(name);
|
||||||
const emoji = await this.emojisRepository.findOneBy({
|
|
||||||
name,
|
|
||||||
host: IsNull(),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (emoji) object.tag = [this.renderEmoji(emoji)];
|
if (emoji) object.tag = [this.renderEmoji(emoji)];
|
||||||
}
|
}
|
||||||
|
@ -701,13 +700,9 @@ export class ApRendererService {
|
||||||
private async getEmojis(names: string[]): Promise<Emoji[]> {
|
private async getEmojis(names: string[]): Promise<Emoji[]> {
|
||||||
if (names == null || names.length === 0) return [];
|
if (names == null || names.length === 0) return [];
|
||||||
|
|
||||||
const emojis = await Promise.all(
|
const allEmojis = await this.customEmojiService.localEmojisCache.fetch();
|
||||||
names.map(name => this.emojisRepository.findOneBy({
|
const emojis = names.map(name => allEmojis.get(name)).filter(isNotNull);
|
||||||
name,
|
|
||||||
host: IsNull(),
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
|
|
||||||
return emojis.filter(emoji => emoji != null) as Emoji[];
|
return emojis;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 があったらここで解決しておく
|
// TODO: 本当は renote とか reply がないのに renoteId とか replyId があったらここで解決しておく
|
||||||
const fileIds = notes.map(n => [n.fileIds, n.renote?.fileIds, n.reply?.fileIds]).flat(2).filter(isNotNull);
|
const fileIds = notes.map(n => [n.fileIds, n.renote?.fileIds, n.reply?.fileIds]).flat(2).filter(isNotNull);
|
||||||
const packedFiles = await this.driveFileEntityService.packManyByIdsMap(fileIds);
|
const packedFiles = await this.driveFileEntityService.packManyByIdsMap(fileIds);
|
||||||
|
@ -420,6 +420,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
|
@bindThis
|
||||||
public async countSameRenotes(userId: string, renoteId: string, excludeNoteId: string | undefined): Promise<number> {
|
public async countSameRenotes(userId: string, renoteId: string, excludeNoteId: string | undefined): Promise<number> {
|
||||||
// 指定したユーザーの指定したノートのリノートがいくつあるか数える
|
// 指定したユーザーの指定したノートのリノートがいくつあるか数える
|
||||||
|
|
|
@ -85,6 +85,90 @@ export class RedisKVCache<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class RedisSingleCache<T> {
|
||||||
|
private redisClient: Redis.Redis;
|
||||||
|
private name: string;
|
||||||
|
private lifetime: number;
|
||||||
|
private memoryCache: MemorySingleCache<T>;
|
||||||
|
private fetcher: () => Promise<T>;
|
||||||
|
private toRedisConverter: (value: T) => string;
|
||||||
|
private fromRedisConverter: (value: string) => T;
|
||||||
|
|
||||||
|
constructor(redisClient: RedisSingleCache<T>['redisClient'], name: RedisSingleCache<T>['name'], opts: {
|
||||||
|
lifetime: RedisSingleCache<T>['lifetime'];
|
||||||
|
memoryCacheLifetime: number;
|
||||||
|
fetcher: RedisSingleCache<T>['fetcher'];
|
||||||
|
toRedisConverter: RedisSingleCache<T>['toRedisConverter'];
|
||||||
|
fromRedisConverter: RedisSingleCache<T>['fromRedisConverter'];
|
||||||
|
}) {
|
||||||
|
this.redisClient = redisClient;
|
||||||
|
this.name = name;
|
||||||
|
this.lifetime = opts.lifetime;
|
||||||
|
this.memoryCache = new MemorySingleCache(opts.memoryCacheLifetime);
|
||||||
|
this.fetcher = opts.fetcher;
|
||||||
|
this.toRedisConverter = opts.toRedisConverter;
|
||||||
|
this.fromRedisConverter = opts.fromRedisConverter;
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async set(value: T): Promise<void> {
|
||||||
|
this.memoryCache.set(value);
|
||||||
|
if (this.lifetime === Infinity) {
|
||||||
|
await this.redisClient.set(
|
||||||
|
`singlecache:${this.name}`,
|
||||||
|
this.toRedisConverter(value),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await this.redisClient.set(
|
||||||
|
`singlecache:${this.name}`,
|
||||||
|
this.toRedisConverter(value),
|
||||||
|
'ex', Math.round(this.lifetime / 1000),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async get(): Promise<T | undefined> {
|
||||||
|
const memoryCached = this.memoryCache.get();
|
||||||
|
if (memoryCached !== undefined) return memoryCached;
|
||||||
|
|
||||||
|
const cached = await this.redisClient.get(`singlecache:${this.name}`);
|
||||||
|
if (cached == null) return undefined;
|
||||||
|
return this.fromRedisConverter(cached);
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async delete(): Promise<void> {
|
||||||
|
this.memoryCache.delete();
|
||||||
|
await this.redisClient.del(`singlecache:${this.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します
|
||||||
|
*/
|
||||||
|
@bindThis
|
||||||
|
public async fetch(): Promise<T> {
|
||||||
|
const cachedValue = await this.get();
|
||||||
|
if (cachedValue !== undefined) {
|
||||||
|
// Cache HIT
|
||||||
|
return cachedValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache MISS
|
||||||
|
const value = await this.fetcher();
|
||||||
|
this.set(value);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async refresh() {
|
||||||
|
const value = await this.fetcher();
|
||||||
|
this.set(value);
|
||||||
|
|
||||||
|
// TODO: イベント発行して他プロセスのメモリキャッシュも更新できるようにする
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: メモリ節約のためあまり参照されないキーを定期的に削除できるようにする?
|
// TODO: メモリ節約のためあまり参照されないキーを定期的に削除できるようにする?
|
||||||
|
|
||||||
export class MemoryKVCache<T> {
|
export class MemoryKVCache<T> {
|
||||||
|
@ -173,12 +257,12 @@ export class MemoryKVCache<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MemoryCache<T> {
|
export class MemorySingleCache<T> {
|
||||||
private cachedAt: number | null = null;
|
private cachedAt: number | null = null;
|
||||||
private value: T | undefined;
|
private value: T | undefined;
|
||||||
private lifetime: number;
|
private lifetime: number;
|
||||||
|
|
||||||
constructor(lifetime: MemoryCache<never>['lifetime']) {
|
constructor(lifetime: MemorySingleCache<never>['lifetime']) {
|
||||||
this.lifetime = lifetime;
|
this.lifetime = lifetime;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { MetaService } from '@/core/MetaService.js';
|
||||||
import { ApRequestService } from '@/core/activitypub/ApRequestService.js';
|
import { ApRequestService } from '@/core/activitypub/ApRequestService.js';
|
||||||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
||||||
import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js';
|
import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js';
|
||||||
import { MemoryCache } from '@/misc/cache.js';
|
import { MemorySingleCache } from '@/misc/cache.js';
|
||||||
import type { Instance } from '@/models/entities/Instance.js';
|
import type { Instance } from '@/models/entities/Instance.js';
|
||||||
import InstanceChart from '@/core/chart/charts/instance.js';
|
import InstanceChart from '@/core/chart/charts/instance.js';
|
||||||
import ApRequestChart from '@/core/chart/charts/ap-request.js';
|
import ApRequestChart from '@/core/chart/charts/ap-request.js';
|
||||||
|
@ -22,7 +22,7 @@ import type { DeliverJobData } from '../types.js';
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DeliverProcessorService {
|
export class DeliverProcessorService {
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
private suspendedHostsCache: MemoryCache<Instance[]>;
|
private suspendedHostsCache: MemorySingleCache<Instance[]>;
|
||||||
private latest: string | null;
|
private latest: string | null;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -46,7 +46,7 @@ export class DeliverProcessorService {
|
||||||
private queueLoggerService: QueueLoggerService,
|
private queueLoggerService: QueueLoggerService,
|
||||||
) {
|
) {
|
||||||
this.logger = this.queueLoggerService.logger.createSubLogger('deliver');
|
this.logger = this.queueLoggerService.logger.createSubLogger('deliver');
|
||||||
this.suspendedHostsCache = new MemoryCache<Instance[]>(1000 * 60 * 60);
|
this.suspendedHostsCache = new MemorySingleCache<Instance[]>(1000 * 60 * 60);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
|
|
@ -4,7 +4,7 @@ import type { NotesRepository, UsersRepository } from '@/models/index.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
|
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
|
||||||
import { MemoryCache } from '@/misc/cache.js';
|
import { MemorySingleCache } from '@/misc/cache.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import NotesChart from '@/core/chart/charts/notes.js';
|
import NotesChart from '@/core/chart/charts/notes.js';
|
||||||
|
@ -118,7 +118,7 @@ export class NodeinfoServerService {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const cache = new MemoryCache<Awaited<ReturnType<typeof nodeinfo2>>>(1000 * 60 * 10);
|
const cache = new MemorySingleCache<Awaited<ReturnType<typeof nodeinfo2>>>(1000 * 60 * 10);
|
||||||
|
|
||||||
fastify.get(nodeinfo2_1path, async (request, reply) => {
|
fastify.get(nodeinfo2_1path, async (request, reply) => {
|
||||||
const base = await cache.fetch(() => nodeinfo2());
|
const base = await cache.fetch(() => nodeinfo2());
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { DataSource, In } from 'typeorm';
|
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import type { EmojisRepository } from '@/models/index.js';
|
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
|
||||||
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['admin'],
|
tags: ['admin'],
|
||||||
|
@ -26,38 +22,14 @@ export const paramDef = {
|
||||||
required: ['ids', 'aliases'],
|
required: ['ids', 'aliases'],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// TODO: ロジックをサービスに切り出す
|
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.db)
|
private customEmojiService: CustomEmojiService,
|
||||||
private db: DataSource,
|
|
||||||
|
|
||||||
@Inject(DI.emojisRepository)
|
|
||||||
private emojisRepository: EmojisRepository,
|
|
||||||
|
|
||||||
private emojiEntityService: EmojiEntityService,
|
|
||||||
private globalEventService: GlobalEventService,
|
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const emojis = await this.emojisRepository.findBy({
|
await this.customEmojiService.addAliasesBulk(ps.ids, ps.aliases);
|
||||||
id: In(ps.ids),
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const emoji of emojis) {
|
|
||||||
await this.emojisRepository.update(emoji.id, {
|
|
||||||
updatedAt: new Date(),
|
|
||||||
aliases: [...new Set(emoji.aliases.concat(ps.aliases))],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.db.queryResultCache?.remove(['meta_emojis']);
|
|
||||||
|
|
||||||
this.globalEventService.publishBroadcastStream('emojiUpdated', {
|
|
||||||
emojis: await this.emojiEntityService.packDetailedMany(ps.ids),
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -90,8 +90,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
license: emoji.license,
|
license: emoji.license,
|
||||||
}).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
|
}).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
|
||||||
|
|
||||||
await this.db.queryResultCache?.remove(['meta_emojis']);
|
|
||||||
|
|
||||||
this.globalEventService.publishBroadcastStream('emojiAdded', {
|
this.globalEventService.publishBroadcastStream('emojiAdded', {
|
||||||
emoji: await this.emojiEntityService.packDetailed(copied.id),
|
emoji: await this.emojiEntityService.packDetailed(copied.id),
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,11 +1,6 @@
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { DataSource, In } from 'typeorm';
|
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import type { EmojisRepository } from '@/models/index.js';
|
import { CustomEmojiService } from '@/core/CustomEmojiService';
|
||||||
import { DI } from '@/di-symbols.js';
|
|
||||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
|
||||||
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['admin'],
|
tags: ['admin'],
|
||||||
|
@ -24,38 +19,14 @@ export const paramDef = {
|
||||||
required: ['ids'],
|
required: ['ids'],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// TODO: ロジックをサービスに切り出す
|
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.db)
|
private customEmojiService: CustomEmojiService,
|
||||||
private db: DataSource,
|
|
||||||
|
|
||||||
@Inject(DI.emojisRepository)
|
|
||||||
private emojisRepository: EmojisRepository,
|
|
||||||
|
|
||||||
private moderationLogService: ModerationLogService,
|
|
||||||
private emojiEntityService: EmojiEntityService,
|
|
||||||
private globalEventService: GlobalEventService,
|
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const emojis = await this.emojisRepository.findBy({
|
await this.customEmojiService.deleteBulk(ps.ids);
|
||||||
id: In(ps.ids),
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const emoji of emojis) {
|
|
||||||
await this.emojisRepository.delete(emoji.id);
|
|
||||||
await this.db.queryResultCache?.remove(['meta_emojis']);
|
|
||||||
this.moderationLogService.insertModerationLog(me, 'deleteEmoji', {
|
|
||||||
emoji: emoji,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.globalEventService.publishBroadcastStream('emojiDeleted', {
|
|
||||||
emojis: await this.emojiEntityService.packDetailedMany(emojis),
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,6 @@
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { DataSource } from 'typeorm';
|
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import type { EmojisRepository } from '@/models/index.js';
|
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
|
||||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
|
||||||
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
|
||||||
import { ApiError } from '../../../error.js';
|
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['admin'],
|
tags: ['admin'],
|
||||||
|
@ -31,38 +25,14 @@ export const paramDef = {
|
||||||
required: ['id'],
|
required: ['id'],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// TODO: ロジックをサービスに切り出す
|
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.db)
|
private customEmojiService: CustomEmojiService,
|
||||||
private db: DataSource,
|
|
||||||
|
|
||||||
@Inject(DI.emojisRepository)
|
|
||||||
private emojisRepository: EmojisRepository,
|
|
||||||
|
|
||||||
private moderationLogService: ModerationLogService,
|
|
||||||
private emojiEntityService: EmojiEntityService,
|
|
||||||
private globalEventService: GlobalEventService,
|
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const emoji = await this.emojisRepository.findOneBy({ id: ps.id });
|
await this.customEmojiService.delete(ps.id);
|
||||||
|
|
||||||
if (emoji == null) throw new ApiError(meta.errors.noSuchEmoji);
|
|
||||||
|
|
||||||
await this.emojisRepository.delete(emoji.id);
|
|
||||||
|
|
||||||
await this.db.queryResultCache?.remove(['meta_emojis']);
|
|
||||||
|
|
||||||
this.globalEventService.publishBroadcastStream('emojiDeleted', {
|
|
||||||
emojis: [await this.emojiEntityService.packDetailed(emoji)],
|
|
||||||
});
|
|
||||||
|
|
||||||
this.moderationLogService.insertModerationLog(me, 'deleteEmoji', {
|
|
||||||
emoji: emoji,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { DataSource, In } from 'typeorm';
|
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import type { EmojisRepository } from '@/models/index.js';
|
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
|
||||||
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['admin'],
|
tags: ['admin'],
|
||||||
|
@ -26,38 +22,14 @@ export const paramDef = {
|
||||||
required: ['ids', 'aliases'],
|
required: ['ids', 'aliases'],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// TODO: ロジックをサービスに切り出す
|
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.db)
|
private customEmojiService: CustomEmojiService,
|
||||||
private db: DataSource,
|
|
||||||
|
|
||||||
@Inject(DI.emojisRepository)
|
|
||||||
private emojisRepository: EmojisRepository,
|
|
||||||
|
|
||||||
private emojiEntityService: EmojiEntityService,
|
|
||||||
private globalEventService: GlobalEventService,
|
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const emojis = await this.emojisRepository.findBy({
|
await this.customEmojiService.removeAliasesBulk(ps.ids, ps.aliases);
|
||||||
id: In(ps.ids),
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const emoji of emojis) {
|
|
||||||
await this.emojisRepository.update(emoji.id, {
|
|
||||||
updatedAt: new Date(),
|
|
||||||
aliases: emoji.aliases.filter(x => !ps.aliases.includes(x)),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.db.queryResultCache?.remove(['meta_emojis']);
|
|
||||||
|
|
||||||
this.globalEventService.publishBroadcastStream('emojiUpdated', {
|
|
||||||
emojis: await this.emojiEntityService.packDetailedMany(ps.ids),
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { DataSource, In } from 'typeorm';
|
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import type { EmojisRepository } from '@/models/index.js';
|
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
|
||||||
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['admin'],
|
tags: ['admin'],
|
||||||
|
@ -26,34 +22,14 @@ export const paramDef = {
|
||||||
required: ['ids', 'aliases'],
|
required: ['ids', 'aliases'],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// TODO: ロジックをサービスに切り出す
|
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.db)
|
private customEmojiService: CustomEmojiService,
|
||||||
private db: DataSource,
|
|
||||||
|
|
||||||
@Inject(DI.emojisRepository)
|
|
||||||
private emojisRepository: EmojisRepository,
|
|
||||||
|
|
||||||
private emojiEntityService: EmojiEntityService,
|
|
||||||
private globalEventService: GlobalEventService,
|
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
await this.emojisRepository.update({
|
await this.customEmojiService.setAliasesBulk(ps.ids, ps.aliases);
|
||||||
id: In(ps.ids),
|
|
||||||
}, {
|
|
||||||
updatedAt: new Date(),
|
|
||||||
aliases: ps.aliases,
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.db.queryResultCache?.remove(['meta_emojis']);
|
|
||||||
|
|
||||||
this.globalEventService.publishBroadcastStream('emojiUpdated', {
|
|
||||||
emojis: await this.emojiEntityService.packDetailedMany(ps.ids),
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { DataSource, In } from 'typeorm';
|
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import type { EmojisRepository } from '@/models/index.js';
|
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
|
||||||
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['admin'],
|
tags: ['admin'],
|
||||||
|
@ -28,34 +24,14 @@ export const paramDef = {
|
||||||
required: ['ids'],
|
required: ['ids'],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// TODO: ロジックをサービスに切り出す
|
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.db)
|
private customEmojiService: CustomEmojiService,
|
||||||
private db: DataSource,
|
|
||||||
|
|
||||||
@Inject(DI.emojisRepository)
|
|
||||||
private emojisRepository: EmojisRepository,
|
|
||||||
|
|
||||||
private emojiEntityService: EmojiEntityService,
|
|
||||||
private globalEventService: GlobalEventService,
|
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
await this.emojisRepository.update({
|
await this.customEmojiService.setCategoryBulk(ps.ids, ps.category ?? null);
|
||||||
id: In(ps.ids),
|
|
||||||
}, {
|
|
||||||
updatedAt: new Date(),
|
|
||||||
category: ps.category,
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.db.queryResultCache?.remove(['meta_emojis']);
|
|
||||||
|
|
||||||
this.globalEventService.publishBroadcastStream('emojiUpdated', {
|
|
||||||
emojis: await this.emojiEntityService.packDetailedMany(ps.ids),
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { DataSource, IsNull } from 'typeorm';
|
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import type { EmojisRepository } from '@/models/index.js';
|
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
|
||||||
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
|
||||||
import { ApiError } from '../../../error.js';
|
import { ApiError } from '../../../error.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
|
@ -45,51 +41,19 @@ export const paramDef = {
|
||||||
required: ['id', 'name', 'aliases'],
|
required: ['id', 'name', 'aliases'],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// TODO: ロジックをサービスに切り出す
|
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.db)
|
private customEmojiService: CustomEmojiService,
|
||||||
private db: DataSource,
|
|
||||||
|
|
||||||
@Inject(DI.emojisRepository)
|
|
||||||
private emojisRepository: EmojisRepository,
|
|
||||||
|
|
||||||
private emojiEntityService: EmojiEntityService,
|
|
||||||
private globalEventService: GlobalEventService,
|
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const emoji = await this.emojisRepository.findOneBy({ id: ps.id });
|
await this.customEmojiService.update(ps.id, {
|
||||||
const sameNameEmoji = await this.emojisRepository.findOneBy({ name: ps.name, host: IsNull() });
|
|
||||||
if (emoji == null) throw new ApiError(meta.errors.noSuchEmoji);
|
|
||||||
if (sameNameEmoji != null && sameNameEmoji.id !== ps.id) throw new ApiError(meta.errors.sameNameEmojiExists);
|
|
||||||
await this.emojisRepository.update(emoji.id, {
|
|
||||||
updatedAt: new Date(),
|
|
||||||
name: ps.name,
|
name: ps.name,
|
||||||
category: ps.category,
|
category: ps.category ?? null,
|
||||||
aliases: ps.aliases,
|
aliases: ps.aliases,
|
||||||
license: ps.license,
|
license: ps.license ?? null,
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.db.queryResultCache?.remove(['meta_emojis']);
|
|
||||||
|
|
||||||
const updated = await this.emojiEntityService.packDetailed(emoji.id);
|
|
||||||
|
|
||||||
if (emoji.name === ps.name) {
|
|
||||||
this.globalEventService.publishBroadcastStream('emojiUpdated', {
|
|
||||||
emojis: [updated],
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.globalEventService.publishBroadcastStream('emojiDeleted', {
|
|
||||||
emojis: [await this.emojiEntityService.packDetailed(emoji)],
|
|
||||||
});
|
|
||||||
|
|
||||||
this.globalEventService.publishBroadcastStream('emojiAdded', {
|
|
||||||
emoji: updated,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,10 +58,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
category: 'ASC',
|
category: 'ASC',
|
||||||
name: 'ASC',
|
name: 'ASC',
|
||||||
},
|
},
|
||||||
cache: {
|
|
||||||
id: 'meta_emojis',
|
|
||||||
milliseconds: 3600000, // 1 hour
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
Loading…
Reference in a new issue