populateEmojisのリファクタと絵文字情報のキャッシュ (#7378)
* revert * Refactor populateEmojis, Cache emojis * ん * fix typo * コメント
This commit is contained in:
parent
2f2a8e537d
commit
d1efe1d208
5 changed files with 89 additions and 216 deletions
|
@ -14,13 +14,30 @@ export class Cache<T> {
|
|||
});
|
||||
}
|
||||
|
||||
public get(key: string | null): T | null {
|
||||
public get(key: string | null): T | undefined {
|
||||
const cached = this.cache.get(key);
|
||||
if (cached == null) return null;
|
||||
if (cached == null) return undefined;
|
||||
if ((Date.now() - cached.date) > this.lifetime) {
|
||||
this.cache.delete(key);
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
return cached.value;
|
||||
}
|
||||
|
||||
public delete(key: string | null) {
|
||||
this.cache.delete(key);
|
||||
}
|
||||
|
||||
public async fetch(key: string | null, fetcher: () => Promise<T>): Promise<T> {
|
||||
const cachedValue = this.get(key);
|
||||
if (cachedValue !== undefined) {
|
||||
// Cache HIT
|
||||
return cachedValue;
|
||||
}
|
||||
|
||||
// Cache MISS
|
||||
const value = await fetcher();
|
||||
this.set(key, value);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
|
58
src/misc/populate-emojis.ts
Normal file
58
src/misc/populate-emojis.ts
Normal file
|
@ -0,0 +1,58 @@
|
|||
import { Emojis } from '../models';
|
||||
import { Emoji } from '../models/entities/emoji';
|
||||
import { Cache } from './cache';
|
||||
import { isSelfHost, toPunyNullable } from './convert-host';
|
||||
|
||||
const cache = new Cache<Emoji | null>(1000 * 60 * 60);
|
||||
|
||||
/**
|
||||
* 添付用絵文字情報
|
||||
*/
|
||||
type PopulatedEmoji = {
|
||||
name: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* 添付用絵文字情報を解決する
|
||||
* @param emojiName ノートやユーザープロフィールに添付された、またはリアクションのカスタム絵文字名 (:は含めない, リアクションでローカルホストの場合は@.を付ける (これはdecodeReactionで可能))
|
||||
* @param noteUserHost ノートやユーザープロフィールの所有者
|
||||
* @returns 絵文字情報, nullは未マッチを意味する
|
||||
*/
|
||||
export async function populateEmoji(emojiName: string, noteUserHost: string | null): Promise<PopulatedEmoji | null> {
|
||||
const match = emojiName.match(/^(\w+)(?:@([\w.-]+))?$/);
|
||||
if (!match) return null;
|
||||
|
||||
const name = match[1];
|
||||
|
||||
// クエリに使うホスト
|
||||
let host = match[2] === '.' ? null // .はローカルホスト (ここがマッチするのはリアクションのみ)
|
||||
: match[2] === undefined ? noteUserHost // ノートなどでホスト省略表記の場合はローカルホスト (ここがリアクションにマッチすることはない)
|
||||
: isSelfHost(match[2]) ? null // 自ホスト指定
|
||||
: (match[2] || noteUserHost); // 指定されたホスト || ノートなどの所有者のホスト (こっちがリアクションにマッチすることはない)
|
||||
|
||||
host = toPunyNullable(host);
|
||||
|
||||
const queryOrNull = async () => (await Emojis.findOne({
|
||||
name,
|
||||
host
|
||||
})) || null;
|
||||
|
||||
const emoji = await cache.fetch(`${name} ${host}`, queryOrNull);
|
||||
|
||||
if (emoji == null) return null;
|
||||
|
||||
return {
|
||||
name: emojiName,
|
||||
url: emoji.url,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 複数の添付用絵文字情報を解決する (キャシュ付き, 存在しないものは結果から除外される)
|
||||
*/
|
||||
export async function populateEmojis(emojiNames: string[], noteUserHost: string | null): Promise<PopulatedEmoji[]> {
|
||||
const emojis = await Promise.all(emojiNames.map(x => populateEmoji(x, noteUserHost)));
|
||||
return emojis.filter((x): x is PopulatedEmoji => x != null);
|
||||
}
|
||||
|
|
@ -1,15 +1,14 @@
|
|||
import { EntityRepository, Repository, In } from 'typeorm';
|
||||
import { Note } from '../entities/note';
|
||||
import { User } from '../entities/user';
|
||||
import { Emojis, Users, PollVotes, DriveFiles, NoteReactions, Followings, Polls, Channels } from '..';
|
||||
import { Users, PollVotes, DriveFiles, NoteReactions, Followings, Polls, Channels } from '..';
|
||||
import { SchemaType } from '../../misc/schema';
|
||||
import { awaitAll } from '../../prelude/await-all';
|
||||
import { convertLegacyReaction, convertLegacyReactions, decodeReaction } from '../../misc/reaction-lib';
|
||||
import { toString } from '../../mfm/to-string';
|
||||
import { parse } from '../../mfm/parse';
|
||||
import { Emoji } from '../entities/emoji';
|
||||
import { concat } from '../../prelude/array';
|
||||
import { NoteReaction } from '../entities/note-reaction';
|
||||
import { populateEmojis } from '../../misc/populate-emojis';
|
||||
|
||||
export type PackedNote = SchemaType<typeof packedNoteSchema>;
|
||||
|
||||
|
@ -85,7 +84,6 @@ export class NoteRepository extends Repository<Note> {
|
|||
detail?: boolean;
|
||||
skipHide?: boolean;
|
||||
_hint_?: {
|
||||
emojis: Emoji[] | null;
|
||||
myReactions: Map<Note['id'], NoteReaction | null>;
|
||||
};
|
||||
}
|
||||
|
@ -135,93 +133,6 @@ export class NoteRepository extends Repository<Note> {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 添付用emojisを解決する
|
||||
* @param emojiNames Note等に添付されたカスタム絵文字名 (:は含めない)
|
||||
* @param noteUserHost Noteのホスト
|
||||
* @param reactionNames Note等にリアクションされたカスタム絵文字名 (:は含めない)
|
||||
*/
|
||||
async function populateEmojis(emojiNames: string[], noteUserHost: string | null, reactionNames: string[]) {
|
||||
const customReactions = reactionNames?.map(x => decodeReaction(x)).filter(x => x.name);
|
||||
|
||||
let all = [] as {
|
||||
name: string,
|
||||
url: string
|
||||
}[];
|
||||
|
||||
// 与えられたhintだけで十分(=新たにクエリする必要がない)かどうかを表すフラグ
|
||||
let enough = true;
|
||||
if (options?._hint_?.emojis) {
|
||||
for (const name of emojiNames) {
|
||||
const matched = options._hint_.emojis.find(x => x.name === name && x.host === noteUserHost);
|
||||
if (matched) {
|
||||
all.push({
|
||||
name: matched.name,
|
||||
url: matched.url,
|
||||
});
|
||||
} else {
|
||||
enough = false;
|
||||
}
|
||||
}
|
||||
for (const customReaction of customReactions) {
|
||||
const matched = options._hint_.emojis.find(x => x.name === customReaction.name && x.host === customReaction.host);
|
||||
if (matched) {
|
||||
all.push({
|
||||
name: `${matched.name}@${matched.host || '.'}`, // @host付きでローカルは.
|
||||
url: matched.url,
|
||||
});
|
||||
} else {
|
||||
enough = false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
enough = false;
|
||||
}
|
||||
if (enough) return all;
|
||||
|
||||
// カスタム絵文字
|
||||
if (emojiNames?.length > 0) {
|
||||
const tmp = await Emojis.find({
|
||||
where: {
|
||||
name: In(emojiNames),
|
||||
host: noteUserHost
|
||||
},
|
||||
select: ['name', 'host', 'url']
|
||||
}).then(emojis => emojis.map((emoji: Emoji) => {
|
||||
return {
|
||||
name: emoji.name,
|
||||
url: emoji.url,
|
||||
};
|
||||
}));
|
||||
|
||||
all = concat([all, tmp]);
|
||||
}
|
||||
|
||||
if (customReactions?.length > 0) {
|
||||
const where = [] as {}[];
|
||||
|
||||
for (const customReaction of customReactions) {
|
||||
where.push({
|
||||
name: customReaction.name,
|
||||
host: customReaction.host
|
||||
});
|
||||
}
|
||||
|
||||
const tmp = await Emojis.find({
|
||||
where,
|
||||
select: ['name', 'host', 'url']
|
||||
}).then(emojis => emojis.map((emoji: Emoji) => {
|
||||
return {
|
||||
name: `${emoji.name}@${emoji.host || '.'}`, // @host付きでローカルは.
|
||||
url: emoji.url,
|
||||
};
|
||||
}));
|
||||
all = concat([all, tmp]);
|
||||
}
|
||||
|
||||
return all;
|
||||
}
|
||||
|
||||
async function populateMyReaction() {
|
||||
if (options?._hint_?.myReactions) {
|
||||
const reaction = options._hint_.myReactions.get(note.id);
|
||||
|
@ -257,15 +168,14 @@ export class NoteRepository extends Repository<Note> {
|
|||
: await Channels.findOne(note.channelId)
|
||||
: null;
|
||||
|
||||
const reactionEmojiNames = Object.keys(note.reactions).filter(x => x?.startsWith(':')).map(x => decodeReaction(x).reaction).map(x => x.replace(/:/g, ''));
|
||||
|
||||
const packed = await awaitAll({
|
||||
id: note.id,
|
||||
createdAt: note.createdAt.toISOString(),
|
||||
userId: note.userId,
|
||||
user: Users.pack(note.user || note.userId, meId, {
|
||||
detail: false,
|
||||
_hint_: {
|
||||
emojis: options?._hint_?.emojis || null
|
||||
}
|
||||
}),
|
||||
text: text,
|
||||
cw: note.cw,
|
||||
|
@ -277,7 +187,7 @@ export class NoteRepository extends Repository<Note> {
|
|||
repliesCount: note.repliesCount,
|
||||
reactions: convertLegacyReactions(note.reactions),
|
||||
tags: note.tags.length > 0 ? note.tags : undefined,
|
||||
emojis: populateEmojis(note.emojis, host, Object.keys(note.reactions)),
|
||||
emojis: populateEmojis(note.emojis.concat(reactionEmojiNames), host),
|
||||
fileIds: note.fileIds,
|
||||
files: DriveFiles.packMany(note.fileIds),
|
||||
replyId: note.replyId,
|
||||
|
@ -350,48 +260,10 @@ export class NoteRepository extends Repository<Note> {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: ここら辺の処理をaggregateEmojisみたいな関数に切り出したい
|
||||
let emojisWhere: any[] = [];
|
||||
for (const note of notes) {
|
||||
if (typeof note !== 'object') continue;
|
||||
emojisWhere.push({
|
||||
name: In(note.emojis),
|
||||
host: note.userHost
|
||||
});
|
||||
if (note.renote) {
|
||||
emojisWhere.push({
|
||||
name: In(note.renote.emojis),
|
||||
host: note.renote.userHost
|
||||
});
|
||||
if (note.renote.user) {
|
||||
emojisWhere.push({
|
||||
name: In(note.renote.user.emojis),
|
||||
host: note.renote.userHost
|
||||
});
|
||||
}
|
||||
}
|
||||
const customReactions = Object.keys(note.reactions).map(x => decodeReaction(x)).filter(x => x.name);
|
||||
emojisWhere = emojisWhere.concat(customReactions.map(x => ({
|
||||
name: x.name,
|
||||
host: x.host
|
||||
})));
|
||||
if (note.user) {
|
||||
emojisWhere.push({
|
||||
name: In(note.user.emojis),
|
||||
host: note.userHost
|
||||
});
|
||||
}
|
||||
}
|
||||
const emojis = emojisWhere.length > 0 ? await Emojis.find({
|
||||
where: emojisWhere,
|
||||
select: ['name', 'host', 'url']
|
||||
}) : null;
|
||||
|
||||
return await Promise.all(notes.map(n => this.pack(n, me, {
|
||||
...options,
|
||||
_hint_: {
|
||||
myReactions: myReactionsMap,
|
||||
emojis: emojis
|
||||
myReactions: myReactionsMap
|
||||
}
|
||||
})));
|
||||
}
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
import { EntityRepository, In, Repository } from 'typeorm';
|
||||
import { Users, Notes, UserGroupInvitations, AccessTokens, NoteReactions, Emojis } from '..';
|
||||
import { Users, Notes, UserGroupInvitations, AccessTokens, NoteReactions } from '..';
|
||||
import { Notification } from '../entities/notification';
|
||||
import { awaitAll } from '../../prelude/await-all';
|
||||
import { SchemaType } from '../../misc/schema';
|
||||
import { Note } from '../entities/note';
|
||||
import { NoteReaction } from '../entities/note-reaction';
|
||||
import { User } from '../entities/user';
|
||||
import { decodeReaction } from '../../misc/reaction-lib';
|
||||
import { Emoji } from '../entities/emoji';
|
||||
|
||||
export type PackedNotification = SchemaType<typeof packedNotificationSchema>;
|
||||
|
||||
|
@ -17,7 +15,6 @@ export class NotificationRepository extends Repository<Notification> {
|
|||
src: Notification['id'] | Notification,
|
||||
options: {
|
||||
_hintForEachNotes_?: {
|
||||
emojis: Emoji[] | null;
|
||||
myReactions: Map<Note['id'], NoteReaction | null>;
|
||||
};
|
||||
}
|
||||
|
@ -101,47 +98,9 @@ export class NotificationRepository extends Repository<Notification> {
|
|||
myReactionsMap.set(target, myReactions.find(reaction => reaction.noteId === target) || null);
|
||||
}
|
||||
|
||||
// TODO: ここら辺の処理をaggregateEmojisみたいな関数に切り出したい
|
||||
let emojisWhere: any[] = [];
|
||||
for (const note of notes) {
|
||||
if (typeof note !== 'object') continue;
|
||||
emojisWhere.push({
|
||||
name: In(note.emojis),
|
||||
host: note.userHost
|
||||
});
|
||||
if (note.renote) {
|
||||
emojisWhere.push({
|
||||
name: In(note.renote.emojis),
|
||||
host: note.renote.userHost
|
||||
});
|
||||
if (note.renote.user) {
|
||||
emojisWhere.push({
|
||||
name: In(note.renote.user.emojis),
|
||||
host: note.renote.userHost
|
||||
});
|
||||
}
|
||||
}
|
||||
const customReactions = Object.keys(note.reactions).map(x => decodeReaction(x)).filter(x => x.name);
|
||||
emojisWhere = emojisWhere.concat(customReactions.map(x => ({
|
||||
name: x.name,
|
||||
host: x.host
|
||||
})));
|
||||
if (note.user) {
|
||||
emojisWhere.push({
|
||||
name: In(note.user.emojis),
|
||||
host: note.userHost
|
||||
});
|
||||
}
|
||||
}
|
||||
const emojis = emojisWhere.length > 0 ? await Emojis.find({
|
||||
where: emojisWhere,
|
||||
select: ['name', 'host', 'url']
|
||||
}) : null;
|
||||
|
||||
return await Promise.all(notifications.map(x => this.pack(x, {
|
||||
_hintForEachNotes_: {
|
||||
myReactions: myReactionsMap,
|
||||
emojis: emojis,
|
||||
myReactions: myReactionsMap
|
||||
}
|
||||
})));
|
||||
}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import $ from 'cafy';
|
||||
import { EntityRepository, Repository, In, Not } from 'typeorm';
|
||||
import { User, ILocalUser, IRemoteUser } from '../entities/user';
|
||||
import { Emojis, Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, UserProfiles, UserSecurityKeys, UserGroupJoinings, Pages, Announcements, AnnouncementReads, Antennas, AntennaNotes, ChannelFollowings, Instances } from '..';
|
||||
import { Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, UserProfiles, UserSecurityKeys, UserGroupJoinings, Pages, Announcements, AnnouncementReads, Antennas, AntennaNotes, ChannelFollowings, Instances } from '..';
|
||||
import config from '../../config';
|
||||
import { SchemaType } from '../../misc/schema';
|
||||
import { awaitAll } from '../../prelude/await-all';
|
||||
import { Emoji } from '../entities/emoji';
|
||||
import { populateEmojis } from '../../misc/populate-emojis';
|
||||
|
||||
export type PackedUser = SchemaType<typeof packedUserSchema>;
|
||||
|
||||
|
@ -150,9 +150,6 @@ export class UserRepository extends Repository<User> {
|
|||
options?: {
|
||||
detail?: boolean,
|
||||
includeSecrets?: boolean,
|
||||
_hint_?: {
|
||||
emojis: Emoji[] | null;
|
||||
};
|
||||
}
|
||||
): Promise<PackedUser> {
|
||||
const opts = Object.assign({
|
||||
|
@ -170,34 +167,6 @@ export class UserRepository extends Repository<User> {
|
|||
}) : [];
|
||||
const profile = opts.detail ? await UserProfiles.findOneOrFail(user.id) : null;
|
||||
|
||||
let emojis: Emoji[] = [];
|
||||
if (user.emojis.length > 0) {
|
||||
// 与えられたhintだけで十分(=新たにクエリする必要がない)かどうかを表すフラグ
|
||||
let enough = true;
|
||||
if (options?._hint_?.emojis) {
|
||||
for (const name of user.emojis) {
|
||||
const matched = options._hint_.emojis.find(x => x.name === name && x.host === user.host);
|
||||
if (matched) {
|
||||
emojis.push(matched);
|
||||
} else {
|
||||
enough = false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
enough = false;
|
||||
}
|
||||
|
||||
if (!enough) {
|
||||
emojis = await Emojis.find({
|
||||
where: {
|
||||
name: In(user.emojis),
|
||||
host: user.host
|
||||
},
|
||||
select: ['name', 'host', 'url', 'aliases']
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const falsy = opts.detail ? false : undefined;
|
||||
|
||||
const packed = {
|
||||
|
@ -220,9 +189,7 @@ export class UserRepository extends Repository<User> {
|
|||
faviconUrl: instance.faviconUrl,
|
||||
themeColor: instance.themeColor,
|
||||
} : undefined) : undefined,
|
||||
|
||||
// カスタム絵文字添付
|
||||
emojis: emojis,
|
||||
emojis: populateEmojis(user.emojis, user.host),
|
||||
|
||||
...(opts.detail ? {
|
||||
url: profile!.url,
|
||||
|
|
Loading…
Reference in a new issue