Fixed 267U.pre2
This commit is contained in:
@@ -0,0 +1,289 @@
|
||||
import config from "../../../../config/index.js";
|
||||
import { DriveFiles, Followings, HtmlUserCacheEntries, UserProfiles, Users } from "../../../../models/index.js";
|
||||
import { EmojiConverter } from "./emoji.js";
|
||||
import { populateEmojis } from "../../../../misc/populate-emojis.js";
|
||||
import { escapeMFM } from "./mfm.js";
|
||||
import mfm from "mfm-js";
|
||||
import { awaitAll } from "../../../../prelude/await-all.js";
|
||||
import { UserHelpers } from "../helpers/user.js";
|
||||
import { MfmHelpers } from "../helpers/mfm.js";
|
||||
import { In } from "typeorm";
|
||||
import { unique } from "../../../../prelude/array.js";
|
||||
import { Cache } from "../../../../misc/cache.js";
|
||||
import { getUser } from "../../common/getters.js";
|
||||
import AsyncLock from "async-lock";
|
||||
export class UserConverter {
|
||||
static userBioHtmlCache = new Cache('html:user:bio', config.htmlCache?.ttlSeconds ?? 60 * 60);
|
||||
static userFieldsHtmlCache = new Cache('html:user:fields', config.htmlCache?.ttlSeconds ?? 60 * 60);
|
||||
static async encode(u, ctx) {
|
||||
const localUser = ctx.user;
|
||||
const cache = ctx.cache;
|
||||
return cache.locks.acquire(u.id, async ()=>{
|
||||
const cacheHit = cache.accounts.find((p)=>p.id == u.id);
|
||||
if (cacheHit) return cacheHit;
|
||||
const identifier = `${u.id}:${(u.lastFetchedAt ?? u.updatedAt ?? u.createdAt).getTime()}`;
|
||||
let fqn = `${u.username}@${u.host ?? config.domain}`;
|
||||
let acct = u.username;
|
||||
let acctUrl = `https://${u.host || config.host}/@${u.username}`;
|
||||
if (u.host) {
|
||||
acct = `${u.username}@${u.host}`;
|
||||
acctUrl = `https://${u.host}/@${u.username}`;
|
||||
}
|
||||
const aggregateProfile = ctx.userProfileAggregate?.get(u.id);
|
||||
let htmlCacheEntry = undefined;
|
||||
const htmlCacheEntryLock = new AsyncLock();
|
||||
const profile = aggregateProfile !== undefined ? aggregateProfile : UserProfiles.findOneBy({
|
||||
userId: u.id
|
||||
});
|
||||
const bio = this.userBioHtmlCache.fetch(identifier, async ()=>{
|
||||
return htmlCacheEntryLock.acquire(u.id, async ()=>{
|
||||
if (htmlCacheEntry === undefined) htmlCacheEntry = await this.fetchFromCacheWithFallback(u, await profile, ctx);
|
||||
if (htmlCacheEntry === null) {
|
||||
return Promise.resolve(profile).then(async (profile)=>{
|
||||
return MfmHelpers.toHtml(mfm.parse(profile?.description ?? ""), profile?.mentions, u.host).then((p)=>p ?? escapeMFM(profile?.description ?? "")).then((p)=>p !== '<p></p>' ? p : null);
|
||||
});
|
||||
}
|
||||
return htmlCacheEntry?.bio ?? null;
|
||||
});
|
||||
}, true).then((p)=>p ?? '<p></p>');
|
||||
const avatar = u.avatarId ? DriveFiles.getFinalUrlMaybe(u.avatarUrl) ?? DriveFiles.findOneBy({
|
||||
id: u.avatarId
|
||||
}).then((p)=>p?.url ?? Users.getIdenticonUrl(u.id)).then((p)=>DriveFiles.getFinalUrl(p)) : Users.getIdenticonUrl(u.id);
|
||||
const banner = u.bannerId ? DriveFiles.getFinalUrlMaybe(u.bannerUrl) ?? DriveFiles.findOneBy({
|
||||
id: u.bannerId
|
||||
}).then((p)=>p?.url ?? `${config.url}/static-assets/transparent.png`).then((p)=>DriveFiles.getFinalUrl(p)) : `${config.url}/static-assets/transparent.png`;
|
||||
const isFollowedOrSelf = ctx.followedOrSelfAggregate?.get(u.id) ?? (!!localUser && (localUser.id === u.id || Followings.exist({
|
||||
where: {
|
||||
followeeId: u.id,
|
||||
followerId: localUser.id
|
||||
}
|
||||
})));
|
||||
const followersCount = Promise.resolve(profile).then(async (profile)=>{
|
||||
if (profile === null) return u.followersCount;
|
||||
switch(profile.ffVisibility){
|
||||
case "public":
|
||||
return u.followersCount;
|
||||
case "followers":
|
||||
return Promise.resolve(isFollowedOrSelf).then((isFollowedOrSelf)=>isFollowedOrSelf ? u.followersCount : 0);
|
||||
case "private":
|
||||
return localUser?.id === profile.userId ? u.followersCount : 0;
|
||||
}
|
||||
});
|
||||
const followingCount = Promise.resolve(profile).then(async (profile)=>{
|
||||
if (profile === null) return u.followingCount;
|
||||
switch(profile.ffVisibility){
|
||||
case "public":
|
||||
return u.followingCount;
|
||||
case "followers":
|
||||
return Promise.resolve(isFollowedOrSelf).then((isFollowedOrSelf)=>isFollowedOrSelf ? u.followingCount : 0);
|
||||
case "private":
|
||||
return localUser?.id === profile.userId ? u.followingCount : 0;
|
||||
}
|
||||
});
|
||||
const fields = this.userFieldsHtmlCache.fetch(identifier, async ()=>{
|
||||
return htmlCacheEntryLock.acquire(u.id, async ()=>{
|
||||
if (htmlCacheEntry === undefined) htmlCacheEntry = await this.fetchFromCacheWithFallback(u, await profile, ctx);
|
||||
if (htmlCacheEntry === null) {
|
||||
return Promise.resolve(profile).then((profile)=>Promise.all(profile?.fields.map(async (p)=>this.encodeField(p, u.host, profile?.mentions)) ?? []));
|
||||
}
|
||||
return htmlCacheEntry?.fields ?? [];
|
||||
});
|
||||
}, true);
|
||||
return awaitAll({
|
||||
id: u.id,
|
||||
username: u.username,
|
||||
acct: acct,
|
||||
fqn: fqn,
|
||||
display_name: u.name || u.username,
|
||||
locked: u.isLocked,
|
||||
created_at: u.createdAt.toISOString(),
|
||||
followers_count: followersCount,
|
||||
following_count: followingCount,
|
||||
statuses_count: u.notesCount,
|
||||
note: bio,
|
||||
url: u.uri ?? acctUrl,
|
||||
avatar: avatar,
|
||||
avatar_static: avatar,
|
||||
header: banner,
|
||||
header_static: banner,
|
||||
emojis: populateEmojis(u.emojis, u.host).then((emoji)=>emoji.map((e)=>EmojiConverter.encode(e))),
|
||||
moved: null,
|
||||
fields: fields,
|
||||
bot: u.isBot,
|
||||
discoverable: u.isExplorable
|
||||
}).then((p)=>{
|
||||
// noinspection ES6MissingAwait
|
||||
UserHelpers.updateUserInBackground(u);
|
||||
cache.accounts.push(p);
|
||||
return p;
|
||||
});
|
||||
});
|
||||
}
|
||||
static async aggregateData(users, ctx) {
|
||||
const user = ctx.user;
|
||||
const targets = unique(users.map((u)=>u.id));
|
||||
const followedOrSelfAggregate = new Map();
|
||||
const userProfileAggregate = new Map();
|
||||
const htmlUserCacheAggregate = ctx.htmlUserCacheAggregate ?? new Map();
|
||||
if (config.htmlCache?.dbFallback) {
|
||||
const htmlUserCacheEntries = await HtmlUserCacheEntries.findBy({
|
||||
userId: In(targets)
|
||||
});
|
||||
for (const target of targets){
|
||||
htmlUserCacheAggregate.set(target, htmlUserCacheEntries.find((n)=>n.userId === target) ?? null);
|
||||
}
|
||||
}
|
||||
if (user) {
|
||||
const targetsWithoutSelf = targets.filter((u)=>u !== user.id);
|
||||
if (targetsWithoutSelf.length > 0) {
|
||||
const followings = await Followings.createQueryBuilder('following').select('following.followeeId').where('following.followerId = :meId', {
|
||||
meId: user.id
|
||||
}).andWhere('following.followeeId IN (:...targets)', {
|
||||
targets: targetsWithoutSelf
|
||||
}).getMany();
|
||||
for (const userId of targetsWithoutSelf){
|
||||
followedOrSelfAggregate.set(userId, !!followings.find((f)=>f.followerId === userId));
|
||||
}
|
||||
}
|
||||
followedOrSelfAggregate.set(user.id, true);
|
||||
}
|
||||
const profiles = await UserProfiles.findBy({
|
||||
userId: In(targets)
|
||||
});
|
||||
for (const userId of targets){
|
||||
userProfileAggregate.set(userId, profiles.find((p)=>p.userId === userId) ?? null);
|
||||
}
|
||||
ctx.followedOrSelfAggregate = followedOrSelfAggregate;
|
||||
ctx.htmlUserCacheAggregate = htmlUserCacheAggregate;
|
||||
}
|
||||
static async aggregateDataByIds(userIds, ctx) {
|
||||
const targets = unique(userIds);
|
||||
const htmlUserCacheAggregate = ctx.htmlUserCacheAggregate ?? new Map();
|
||||
if (config.htmlCache?.dbFallback) {
|
||||
const htmlUserCacheEntries = await HtmlUserCacheEntries.findBy({
|
||||
userId: In(targets)
|
||||
});
|
||||
for (const target of targets){
|
||||
htmlUserCacheAggregate.set(target, htmlUserCacheEntries.find((n)=>n.userId === target) ?? null);
|
||||
}
|
||||
}
|
||||
ctx.htmlUserCacheAggregate = htmlUserCacheAggregate;
|
||||
}
|
||||
static async encodeMany(users, ctx) {
|
||||
await this.aggregateData(users, ctx);
|
||||
const encoded = users.map((u)=>this.encode(u, ctx));
|
||||
return Promise.all(encoded);
|
||||
}
|
||||
static async encodeField(f, host, mentions) {
|
||||
return {
|
||||
name: f.name,
|
||||
value: await MfmHelpers.toHtml(mfm.parse(f.value), mentions, host, true) ?? escapeMFM(f.value),
|
||||
verified_at: f.verified ? new Date().toISOString() : null
|
||||
};
|
||||
}
|
||||
static async fetchFromCacheWithFallback(user, profile, ctx) {
|
||||
if (!config.htmlCache?.dbFallback) return null;
|
||||
let dbHit = ctx.htmlUserCacheAggregate?.get(user.id);
|
||||
if (dbHit === undefined) dbHit = HtmlUserCacheEntries.findOneBy({
|
||||
userId: user.id
|
||||
});
|
||||
return Promise.resolve(dbHit).then((res)=>{
|
||||
if (res === null || res.updatedAt.getTime() !== (user.lastFetchedAt ?? user.updatedAt ?? user.createdAt).getTime()) {
|
||||
return this.dbCacheMiss(user, profile, ctx);
|
||||
}
|
||||
return res;
|
||||
});
|
||||
}
|
||||
static async dbCacheMiss(user, profile, ctx) {
|
||||
const identifier = `${user.id}:${(user.lastFetchedAt ?? user.updatedAt ?? user.createdAt).getTime()}`;
|
||||
const cache = ctx.cache;
|
||||
return cache.locks.acquire(identifier, async ()=>{
|
||||
const cachedBio = await this.userBioHtmlCache.get(identifier);
|
||||
const cachedFields = await this.userFieldsHtmlCache.get(identifier);
|
||||
if (cachedBio !== undefined && cachedFields !== undefined) {
|
||||
return {
|
||||
bio: cachedBio,
|
||||
fields: cachedFields
|
||||
};
|
||||
}
|
||||
if (profile === undefined) {
|
||||
profile = await UserProfiles.findOneBy({
|
||||
userId: user.id
|
||||
});
|
||||
}
|
||||
let bio = cachedBio;
|
||||
let fields = cachedFields;
|
||||
if (bio === undefined) {
|
||||
bio = MfmHelpers.toHtml(mfm.parse(profile?.description ?? ""), profile?.mentions, user.host).then((p)=>p ?? escapeMFM(profile?.description ?? "")).then((p)=>p !== '<p></p>' ? p : null);
|
||||
}
|
||||
if (fields === undefined) {
|
||||
fields = Promise.all(profile.fields.map(async (p)=>this.encodeField(p, user.host, profile.mentions)) ?? []);
|
||||
}
|
||||
HtmlUserCacheEntries.upsert({
|
||||
userId: user.id,
|
||||
updatedAt: user.lastFetchedAt ?? user.updatedAt ?? user.createdAt,
|
||||
bio: await bio,
|
||||
fields: await fields
|
||||
}, [
|
||||
"userId"
|
||||
]);
|
||||
await this.userBioHtmlCache.set(identifier, await bio);
|
||||
await this.userFieldsHtmlCache.set(identifier, await fields);
|
||||
return {
|
||||
bio,
|
||||
fields
|
||||
};
|
||||
});
|
||||
}
|
||||
static async prewarmCache(user, profile, oldProfile) {
|
||||
const identifier = `${user.id}:${(user.lastFetchedAt ?? user.updatedAt ?? user.createdAt).getTime()}`;
|
||||
if (profile !== null) {
|
||||
if (config.htmlCache?.dbFallback) {
|
||||
if (profile === undefined) {
|
||||
profile = await UserProfiles.findOneBy({
|
||||
userId: user.id
|
||||
});
|
||||
}
|
||||
if (oldProfile !== undefined && profile?.fields === oldProfile?.fields && profile?.description === oldProfile?.description) {
|
||||
HtmlUserCacheEntries.update({
|
||||
userId: user.id
|
||||
}, {
|
||||
updatedAt: user.lastFetchedAt ?? user.updatedAt ?? user.createdAt
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!config.htmlCache?.prewarm) return;
|
||||
if (profile === undefined) {
|
||||
profile = await UserProfiles.findOneBy({
|
||||
userId: user.id
|
||||
});
|
||||
}
|
||||
if (await this.userBioHtmlCache.get(identifier) === undefined) {
|
||||
const bio = MfmHelpers.toHtml(mfm.parse(profile?.description ?? ""), profile?.mentions, user.host).then((p)=>p ?? escapeMFM(profile?.description ?? "")).then((p)=>p !== '<p></p>' ? p : null);
|
||||
this.userBioHtmlCache.set(identifier, await bio);
|
||||
if (config.htmlCache?.dbFallback) HtmlUserCacheEntries.upsert({
|
||||
userId: user.id,
|
||||
updatedAt: user.lastFetchedAt ?? user.updatedAt ?? user.createdAt,
|
||||
bio: await bio
|
||||
}, [
|
||||
"userId"
|
||||
]);
|
||||
}
|
||||
if (await this.userFieldsHtmlCache.get(identifier) === undefined) {
|
||||
const fields = await Promise.all(profile.fields.map(async (p)=>this.encodeField(p, user.host, profile.mentions)) ?? []);
|
||||
this.userFieldsHtmlCache.set(identifier, fields);
|
||||
if (config.htmlCache?.dbFallback) HtmlUserCacheEntries.upsert({
|
||||
userId: user.id,
|
||||
updatedAt: user.lastFetchedAt ?? user.updatedAt ?? user.createdAt,
|
||||
fields: fields
|
||||
}, [
|
||||
"userId"
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
static async prewarmCacheById(userId, oldProfile) {
|
||||
await this.prewarmCache(await getUser(userId), undefined, oldProfile);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user