Fixed 267U.pre2
This commit is contained in:
@@ -0,0 +1,285 @@
|
||||
import { In, IsNull } from "typeorm";
|
||||
import { DriveFiles, Emojis, UserEmojis, UserGroups, UserProfiles, Users } from "../models/index.js";
|
||||
import { Cache } from "./cache.js";
|
||||
import { isSelfHost, toPunyNullable } from "./convert-host.js";
|
||||
import { decodeReaction } from "./reaction-lib.js";
|
||||
import config from "../config/index.js";
|
||||
import { query } from "../prelude/url.js";
|
||||
import { redisClient } from "../db/redis.js";
|
||||
import { resolveUser } from "../remote/resolve-user.js";
|
||||
const cache = new Cache("populateEmojis", 60 * 60 * 12);
|
||||
const userEmojiCache = new Cache("populateUserEmojis", 60 * 60 * 12);
|
||||
function normalizeHost(src, noteUserHost) {
|
||||
// クエリに使うホスト
|
||||
let host = src === "." ? null // .はローカルホスト (ここがマッチするのはリアクションのみ)
|
||||
: src === undefined ? noteUserHost // ノートなどでホスト省略表記の場合はローカルホスト (ここがリアクションにマッチすることはない)
|
||||
: isSelfHost(src) ? null // 自ホスト指定
|
||||
: src || noteUserHost; // 指定されたホスト || ノートなどの所有者のホスト (こっちがリアクションにマッチすることはない)
|
||||
host = toPunyNullable(host);
|
||||
return host;
|
||||
}
|
||||
function parseEmojiStr(emojiName, noteUserHost) {
|
||||
// emojiName may be of the form `emoji@host`, turn it into a suitable form
|
||||
const match = emojiName.split("@");
|
||||
const name = match[0];
|
||||
const host = toPunyNullable(normalizeHost(match[1], noteUserHost));
|
||||
return {
|
||||
name,
|
||||
host
|
||||
};
|
||||
}
|
||||
function proxiedUrl(url, host) {
|
||||
if (host == null) return url;
|
||||
return `${config.url}/proxy/${encodeURIComponent(new URL(url).pathname)}?${query({
|
||||
url
|
||||
})}`;
|
||||
}
|
||||
function proxiedGlyphUrl(url, host) {
|
||||
if (host == null) return url;
|
||||
return `${config.url}/proxy/${encodeURIComponent(new URL(url).pathname)}?${query({
|
||||
url,
|
||||
glyph: "1"
|
||||
})}`;
|
||||
}
|
||||
function parseUserIconEmoji(emojiName) {
|
||||
const match = emojiName.match(/^@([^@:\s]+)(?:@([^@:\s]+))?$/);
|
||||
if (!match) return null;
|
||||
return {
|
||||
username: match[1],
|
||||
host: toPunyNullable(normalizeHost(match[2], null))
|
||||
};
|
||||
}
|
||||
function parseGroupSymbolEmoji(emojiName) {
|
||||
const match = emojiName.match(/^@@([a-zA-Z0-9_]{1,64})$/);
|
||||
if (!match) return null;
|
||||
return {
|
||||
username: match[1].toLowerCase()
|
||||
};
|
||||
}
|
||||
function parseGroupEmoji(emojiName) {
|
||||
const match = emojiName.match(/^([a-z0-9_]{1,64})@@([a-zA-Z0-9_]{1,64})$/);
|
||||
if (!match) return null;
|
||||
return {
|
||||
name: match[1],
|
||||
username: match[2].toLowerCase()
|
||||
};
|
||||
}
|
||||
function parseUserEmoji(emojiName) {
|
||||
const parts = emojiName.split("@");
|
||||
if (parts.length !== 2 && parts.length !== 3) return null;
|
||||
if (!parts[0] || !parts[1]) return null;
|
||||
return {
|
||||
name: parts[0],
|
||||
username: parts[1],
|
||||
host: toPunyNullable(normalizeHost(parts[2], null))
|
||||
};
|
||||
}
|
||||
async function findUserByAcct(username, host) {
|
||||
const user = await Users.findOneBy({
|
||||
usernameLower: username.toLowerCase(),
|
||||
host: host ?? IsNull()
|
||||
});
|
||||
if (user) return user;
|
||||
if (host == null) return null;
|
||||
return resolveUser(username, host).catch(()=>null);
|
||||
}
|
||||
async function populateUserIconEmoji(emojiName) {
|
||||
const parsed = parseUserIconEmoji(emojiName);
|
||||
if (!parsed) return null;
|
||||
const user = await findUserByAcct(parsed.username, parsed.host);
|
||||
if (!user) return null;
|
||||
const profile = await UserProfiles.findOneBy({
|
||||
userId: user.id
|
||||
});
|
||||
if (profile?.symbolFileId) {
|
||||
const symbol = await DriveFiles.findOneBy({
|
||||
id: profile.symbolFileId
|
||||
});
|
||||
if (symbol) {
|
||||
const symbolUrl = symbol.webpublicUrl ?? symbol.url;
|
||||
return {
|
||||
name: emojiName,
|
||||
url: proxiedUrl(symbolUrl, user.host),
|
||||
glyph: true,
|
||||
glyphUrl: proxiedGlyphUrl(symbol.url, user.host),
|
||||
width: null,
|
||||
height: null
|
||||
};
|
||||
}
|
||||
}
|
||||
const avatarUrl = user.avatarUrl ?? await Users.getAvatarUrl(user);
|
||||
return {
|
||||
name: emojiName,
|
||||
url: proxiedUrl(avatarUrl, user.host),
|
||||
glyph: false,
|
||||
glyphUrl: null,
|
||||
width: null,
|
||||
height: null
|
||||
};
|
||||
}
|
||||
async function populateUserEmoji(emojiName) {
|
||||
const parsed = parseUserEmoji(emojiName);
|
||||
if (!parsed) return null;
|
||||
const user = await findUserByAcct(parsed.username, parsed.host);
|
||||
if (!user) return null;
|
||||
const userEmoji = await UserEmojis.findOneBy({
|
||||
name: parsed.name,
|
||||
userId: user.id
|
||||
});
|
||||
if (!userEmoji) return null;
|
||||
const emojiUrl = userEmoji.publicUrl || userEmoji.originalUrl;
|
||||
return {
|
||||
name: emojiName,
|
||||
url: proxiedUrl(emojiUrl, user.host),
|
||||
glyph: userEmoji.glyph,
|
||||
glyphUrl: userEmoji.glyph ? proxiedGlyphUrl(userEmoji.originalUrl, user.host) : null,
|
||||
width: userEmoji.width,
|
||||
height: userEmoji.height
|
||||
};
|
||||
}
|
||||
async function populateGroupSymbolEmoji(emojiName) {
|
||||
const parsed = parseGroupSymbolEmoji(emojiName);
|
||||
if (!parsed) return null;
|
||||
const group = await UserGroups.findOneBy({
|
||||
username: parsed.username
|
||||
});
|
||||
if (!group?.symbolFileId && !group?.iconFileId) return null;
|
||||
const file = await DriveFiles.findOneBy({
|
||||
id: group.symbolFileId ?? group.iconFileId
|
||||
});
|
||||
if (!file) return null;
|
||||
const symbolUrl = file.webpublicUrl ?? file.url;
|
||||
return {
|
||||
name: emojiName,
|
||||
url: proxiedUrl(symbolUrl, null),
|
||||
glyph: true,
|
||||
glyphUrl: proxiedGlyphUrl(file.url, null),
|
||||
width: null,
|
||||
height: null
|
||||
};
|
||||
}
|
||||
async function populateGroupEmoji(emojiName) {
|
||||
const parsed = parseGroupEmoji(emojiName);
|
||||
if (!parsed) return null;
|
||||
const group = await UserGroups.findOneBy({
|
||||
username: parsed.username
|
||||
});
|
||||
if (!group) return null;
|
||||
const groupEmoji = await UserEmojis.findOneBy({
|
||||
name: parsed.name,
|
||||
userGroupId: group.id
|
||||
});
|
||||
if (!groupEmoji) return null;
|
||||
const emojiUrl = groupEmoji.publicUrl || groupEmoji.originalUrl;
|
||||
return {
|
||||
name: emojiName,
|
||||
url: proxiedUrl(emojiUrl, null),
|
||||
glyph: groupEmoji.glyph,
|
||||
glyphUrl: groupEmoji.glyph ? proxiedGlyphUrl(groupEmoji.originalUrl, null) : null,
|
||||
width: groupEmoji.width,
|
||||
height: groupEmoji.height
|
||||
};
|
||||
}
|
||||
/**
|
||||
* 添付用絵文字情報を解決する
|
||||
* @param emojiName ノートやユーザープロフィールに添付された、またはリアクションのカスタム絵文字名 (:は含めない, リアクションでローカルホストの場合は@.を付ける (これはdecodeReactionで可能))
|
||||
* @param noteUserHost ノートやユーザープロフィールの所有者のホスト
|
||||
* @returns 絵文字情報, nullは未マッチを意味する
|
||||
*/ export async function populateEmoji(emojiName, noteUserHost) {
|
||||
const { name, host } = parseEmojiStr(emojiName, noteUserHost);
|
||||
if (name == null) return null;
|
||||
const queryOrNull = async ()=>await Emojis.findOneBy({
|
||||
name,
|
||||
host: host ?? IsNull()
|
||||
}) || null;
|
||||
const cacheKey = `${name} ${host}`;
|
||||
let emoji = await cache.fetch(cacheKey, queryOrNull);
|
||||
if (emoji && !(emoji.width && emoji.height)) {
|
||||
emoji = await queryOrNull();
|
||||
await cache.set(cacheKey, emoji);
|
||||
}
|
||||
if (emoji == null) return null;
|
||||
const isLocal = emoji.host == null;
|
||||
const emojiUrl = emoji.publicUrl || emoji.originalUrl; // || emoji.originalUrl してるのは後方互換性のため
|
||||
const url = proxiedUrl(emojiUrl, isLocal ? null : emoji.host);
|
||||
return {
|
||||
name: emojiName,
|
||||
url,
|
||||
glyph: emoji.glyph,
|
||||
glyphUrl: emoji.glyph ? proxiedGlyphUrl(emoji.originalUrl, isLocal ? null : emoji.host) : null,
|
||||
width: emoji.width,
|
||||
height: emoji.height
|
||||
};
|
||||
}
|
||||
export async function populateEmojiOrUserEmoji(emojiName, noteUserHost) {
|
||||
const emoji = await populateEmoji(emojiName, noteUserHost);
|
||||
if (emoji) return emoji;
|
||||
const userEmoji = await userEmojiCache.fetchMaybe(`user ${emojiName}`, async ()=>await populateUserIconEmoji(emojiName) ?? await populateUserEmoji(emojiName) ?? await populateGroupSymbolEmoji(emojiName) ?? await populateGroupEmoji(emojiName) ?? undefined, false, (cached)=>cached != null);
|
||||
return userEmoji ?? null;
|
||||
}
|
||||
export async function clearUserEmojiCache(name, username, host) {
|
||||
const keys = new Set([
|
||||
`user ${name}@${username}`
|
||||
]);
|
||||
if (host) {
|
||||
keys.add(`user ${name}@${username}@${host}`);
|
||||
} else {
|
||||
keys.add(`user ${name}@${username}@${config.host}`);
|
||||
}
|
||||
await userEmojiCache.delete(...keys);
|
||||
}
|
||||
export async function clearGroupEmojiCache(name, groupUsername) {
|
||||
if (!groupUsername) return;
|
||||
await userEmojiCache.delete(`user ${name}@@${groupUsername}`, `user @@${groupUsername}`);
|
||||
}
|
||||
/**
|
||||
* 複数の添付用絵文字情報を解決する (キャシュ付き, 存在しないものは結果から除外される)
|
||||
*/ export async function populateEmojis(emojiNames, noteUserHost) {
|
||||
const emojis = await Promise.all(emojiNames.map((x)=>populateEmojiOrUserEmoji(x, noteUserHost)));
|
||||
return emojis.filter((x)=>x != null);
|
||||
}
|
||||
export function aggregateNoteEmojis(notes) {
|
||||
let emojis = [];
|
||||
for (const note of notes){
|
||||
emojis = emojis.concat(note.emojis.map((e)=>parseEmojiStr(e, note.userHost)));
|
||||
if (note.renote) {
|
||||
emojis = emojis.concat(note.renote.emojis.map((e)=>parseEmojiStr(e, note.renote.userHost)));
|
||||
if (note.renote.user) {
|
||||
emojis = emojis.concat(note.renote.user.emojis.map((e)=>parseEmojiStr(e, note.renote.userHost)));
|
||||
}
|
||||
}
|
||||
const customReactions = Object.keys(note.reactions).map((x)=>decodeReaction(x)).filter((x)=>x.name != null);
|
||||
emojis = emojis.concat(customReactions);
|
||||
if (note.user) {
|
||||
emojis = emojis.concat(note.user.emojis.map((e)=>parseEmojiStr(e, note.userHost)));
|
||||
}
|
||||
}
|
||||
return emojis.filter((x)=>x.name != null);
|
||||
}
|
||||
/**
|
||||
* 与えられた絵文字のリストをデータベースから取得し、キャッシュに追加します
|
||||
*/ export async function prefetchEmojis(emojis) {
|
||||
const notCachedEmojis = emojis.filter(async (emoji)=>!await cache.get(`${emoji.name} ${emoji.host}`));
|
||||
const emojisQuery = [];
|
||||
const hosts = new Set(notCachedEmojis.map((e)=>e.host));
|
||||
for (const host of hosts){
|
||||
emojisQuery.push({
|
||||
name: In(notCachedEmojis.filter((e)=>e.host === host).map((e)=>e.name)),
|
||||
host: host ?? IsNull()
|
||||
});
|
||||
}
|
||||
const _emojis = emojisQuery.length > 0 ? await Emojis.find({
|
||||
where: emojisQuery,
|
||||
select: [
|
||||
"name",
|
||||
"host",
|
||||
"originalUrl",
|
||||
"publicUrl"
|
||||
]
|
||||
}) : [];
|
||||
const trans = redisClient.multi();
|
||||
for (const emoji of _emojis){
|
||||
cache.set(`${emoji.name} ${emoji.host}`, emoji, trans);
|
||||
}
|
||||
await trans.exec();
|
||||
}
|
||||
Reference in New Issue
Block a user