mirror of
https://iceshrimp.dev/crimekillz/iceshrimp-161sh.git
synced 2024-11-25 05:29:07 +01:00
[mastodon-client] Aggregate applicable fields in NoteConverter and UserConverter
This commit is contained in:
parent
3ccfd0417b
commit
b1d3e1d05f
@ -1,4 +1,4 @@
|
|||||||
import { ILocalUser } from "@/models/entities/user.js";
|
import { ILocalUser, User } from "@/models/entities/user.js";
|
||||||
import { getNote } from "@/server/api/common/getters.js";
|
import { getNote } from "@/server/api/common/getters.js";
|
||||||
import { Note } from "@/models/entities/note.js";
|
import { Note } from "@/models/entities/note.js";
|
||||||
import config from "@/config/index.js";
|
import config from "@/config/index.js";
|
||||||
@ -6,7 +6,7 @@ import mfm from "mfm-js";
|
|||||||
import { UserConverter } from "@/server/api/mastodon/converters/user.js";
|
import { UserConverter } from "@/server/api/mastodon/converters/user.js";
|
||||||
import { VisibilityConverter } from "@/server/api/mastodon/converters/visibility.js";
|
import { VisibilityConverter } from "@/server/api/mastodon/converters/visibility.js";
|
||||||
import { escapeMFM } from "@/server/api/mastodon/converters/mfm.js";
|
import { escapeMFM } from "@/server/api/mastodon/converters/mfm.js";
|
||||||
import { PopulatedEmoji, populateEmojis } from "@/misc/populate-emojis.js";
|
import { aggregateNoteEmojis, PopulatedEmoji, populateEmojis, prefetchEmojis } from "@/misc/populate-emojis.js";
|
||||||
import { EmojiConverter } from "@/server/api/mastodon/converters/emoji.js";
|
import { EmojiConverter } from "@/server/api/mastodon/converters/emoji.js";
|
||||||
import { DriveFiles, NoteFavorites, NoteReactions, Notes, NoteThreadMutings, UserNotePinings } from "@/models/index.js";
|
import { DriveFiles, NoteFavorites, NoteReactions, Notes, NoteThreadMutings, UserNotePinings } from "@/models/index.js";
|
||||||
import { decodeReaction } from "@/misc/reaction-lib.js";
|
import { decodeReaction } from "@/misc/reaction-lib.js";
|
||||||
@ -16,11 +16,13 @@ import { populatePoll } from "@/models/repositories/note.js";
|
|||||||
import { FileConverter } from "@/server/api/mastodon/converters/file.js";
|
import { FileConverter } from "@/server/api/mastodon/converters/file.js";
|
||||||
import { awaitAll } from "@/prelude/await-all.js";
|
import { awaitAll } from "@/prelude/await-all.js";
|
||||||
import { UserHelpers } from "@/server/api/mastodon/helpers/user.js";
|
import { UserHelpers } from "@/server/api/mastodon/helpers/user.js";
|
||||||
import { IsNull } from "typeorm";
|
import { In, IsNull } from "typeorm";
|
||||||
import { MfmHelpers } from "@/server/api/mastodon/helpers/mfm.js";
|
import { MfmHelpers } from "@/server/api/mastodon/helpers/mfm.js";
|
||||||
import { getStubMastoContext, MastoContext } from "@/server/api/mastodon/index.js";
|
import { getStubMastoContext, MastoContext } from "@/server/api/mastodon/index.js";
|
||||||
import { NoteHelpers } from "@/server/api/mastodon/helpers/note.js";
|
import { NoteHelpers } from "@/server/api/mastodon/helpers/note.js";
|
||||||
import isQuote from "@/misc/is-quote.js";
|
import isQuote from "@/misc/is-quote.js";
|
||||||
|
import { unique } from "@/prelude/array.js";
|
||||||
|
import { NoteReaction } from "@/models/entities/note-reaction.js";
|
||||||
|
|
||||||
export class NoteConverter {
|
export class NoteConverter {
|
||||||
public static async encode(note: Note, ctx: MastoContext, recurseCounter: number = 2): Promise<MastodonEntity.Status> {
|
public static async encode(note: Note, ctx: MastoContext, recurseCounter: number = 2): Promise<MastodonEntity.Status> {
|
||||||
@ -46,39 +48,46 @@ export class NoteConverter {
|
|||||||
.filter((e) => e.name.indexOf("@") === -1)
|
.filter((e) => e.name.indexOf("@") === -1)
|
||||||
.map((e) => EmojiConverter.encode(e)));
|
.map((e) => EmojiConverter.encode(e)));
|
||||||
|
|
||||||
const reactionCount = NoteReactions.countBy({ noteId: note.id });
|
const reactionCount = Object.values(note.reactions).reduce((a, b) => a + b, 0);
|
||||||
|
|
||||||
const reaction = user ? NoteReactions.findOneBy({
|
const aggregateReaction = (ctx.reactionAggregate as Map<string, NoteReaction | null>)?.get(note.id);
|
||||||
userId: user.id,
|
|
||||||
noteId: note.id,
|
const reaction = aggregateReaction !== undefined
|
||||||
}) : null;
|
? aggregateReaction
|
||||||
|
: user ? NoteReactions.findOneBy({
|
||||||
|
userId: user.id,
|
||||||
|
noteId: note.id,
|
||||||
|
}) : null;
|
||||||
|
|
||||||
const isFavorited = Promise.resolve(reaction).then(p => !!p);
|
const isFavorited = Promise.resolve(reaction).then(p => !!p);
|
||||||
|
|
||||||
const isReblogged = user ? Notes.exist({
|
const isReblogged = (ctx.renoteAggregate as Map<string, boolean>)?.get(note.id)
|
||||||
where: {
|
?? (user ? Notes.exist({
|
||||||
userId: user.id,
|
where: {
|
||||||
renoteId: note.id,
|
userId: user.id,
|
||||||
text: IsNull(),
|
renoteId: note.id,
|
||||||
}
|
text: IsNull(),
|
||||||
}) : null;
|
}
|
||||||
|
}) : null);
|
||||||
|
|
||||||
const renote = note.renote ?? (note.renoteId && recurseCounter > 0 ? getNote(note.renoteId, user) : null);
|
const renote = note.renote ?? (note.renoteId && recurseCounter > 0 ? getNote(note.renoteId, user) : null);
|
||||||
|
|
||||||
const isBookmarked = user ? NoteFavorites.exist({
|
const isBookmarked = (ctx.bookmarkAggregate as Map<string, boolean>)?.get(note.id)
|
||||||
where: {
|
?? (user ? NoteFavorites.exist({
|
||||||
userId: user.id,
|
where: {
|
||||||
noteId: note.id,
|
userId: user.id,
|
||||||
},
|
noteId: note.id,
|
||||||
take: 1,
|
},
|
||||||
}) : false;
|
take: 1,
|
||||||
|
}) : false);
|
||||||
|
|
||||||
const isMuted = user ? NoteThreadMutings.exist({
|
const isMuted = (ctx.mutingAggregate as Map<string, boolean>)?.get(note.threadId ?? note.id)
|
||||||
where: {
|
?? (user ? NoteThreadMutings.exist({
|
||||||
userId: user.id,
|
where: {
|
||||||
threadId: note.threadId || note.id,
|
userId: user.id,
|
||||||
}
|
threadId: note.threadId || note.id,
|
||||||
}) : false;
|
}
|
||||||
|
}) : false);
|
||||||
|
|
||||||
const files = DriveFiles.packMany(note.fileIds);
|
const files = DriveFiles.packMany(note.fileIds);
|
||||||
|
|
||||||
@ -100,9 +109,10 @@ export class NoteConverter {
|
|||||||
.then(p => p ?? escapeMFM(text))
|
.then(p => p ?? escapeMFM(text))
|
||||||
: "");
|
: "");
|
||||||
|
|
||||||
const isPinned = user && note.userId === user.id
|
const isPinned = (ctx.pinAggregate as Map<string, boolean>)?.get(note.id)
|
||||||
? UserNotePinings.exist({ where: { userId: user.id, noteId: note.id } })
|
?? (user && note.userId === user.id
|
||||||
: undefined;
|
? UserNotePinings.exist({ where: { userId: user.id, noteId: note.id } })
|
||||||
|
: undefined);
|
||||||
|
|
||||||
const tags = note.tags.map(tag => {
|
const tags = note.tags.map(tag => {
|
||||||
return {
|
return {
|
||||||
@ -152,10 +162,89 @@ export class NoteConverter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static async encodeMany(notes: Note[], ctx: MastoContext): Promise<MastodonEntity.Status[]> {
|
public static async encodeMany(notes: Note[], ctx: MastoContext): Promise<MastodonEntity.Status[]> {
|
||||||
|
await this.aggregateData(notes, ctx);
|
||||||
const encoded = notes.map(n => this.encode(n, ctx));
|
const encoded = notes.map(n => this.encode(n, ctx));
|
||||||
return Promise.all(encoded);
|
return Promise.all(encoded);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async aggregateData(notes: Note[], ctx: MastoContext): Promise<void> {
|
||||||
|
if (notes.length === 0) return;
|
||||||
|
|
||||||
|
const user = ctx.user as ILocalUser | null;
|
||||||
|
const reactionAggregate = new Map<Note["id"], NoteReaction | null>();
|
||||||
|
const renoteAggregate = new Map<Note["id"], boolean>();
|
||||||
|
const mutingAggregate = new Map<Note["id"], boolean>();
|
||||||
|
const bookmarkAggregate = new Map<Note["id"], boolean>();;
|
||||||
|
const pinAggregate = new Map<Note["id"], boolean>();
|
||||||
|
|
||||||
|
if (user?.id != null) {
|
||||||
|
const renoteIds = notes
|
||||||
|
.filter((n) => n.renoteId != null)
|
||||||
|
.map((n) => n.renoteId!);
|
||||||
|
|
||||||
|
const noteIds = unique(notes.map((n) => n.id));
|
||||||
|
const targets = unique([...noteIds, ...renoteIds]);
|
||||||
|
const mutingTargets = unique([...notes.map(n => n.threadId ?? n.id)]);
|
||||||
|
const pinTargets = unique([...notes.filter(n => n.userId === user.id).map(n => n.id)]);
|
||||||
|
|
||||||
|
const reactions = await NoteReactions.findBy({
|
||||||
|
userId: user.id,
|
||||||
|
noteId: In(targets),
|
||||||
|
});
|
||||||
|
|
||||||
|
const renotes = await Notes.createQueryBuilder('note')
|
||||||
|
.select('note.renoteId')
|
||||||
|
.where('note.userId = :meId', { meId: user.id })
|
||||||
|
.andWhere('note.renoteId IN (:...targets)', { targets })
|
||||||
|
.andWhere('note.text IS NULL')
|
||||||
|
.andWhere('note.hasPoll = FALSE')
|
||||||
|
.andWhere(`note.fileIds = '{}'`)
|
||||||
|
.getMany();
|
||||||
|
|
||||||
|
const mutings = await NoteThreadMutings.createQueryBuilder('muting')
|
||||||
|
.select('muting.threadId')
|
||||||
|
.where('muting.userId = :meId', { meId: user.id })
|
||||||
|
.andWhere('muting.threadId IN (:...targets)', { targets: mutingTargets })
|
||||||
|
.getMany();
|
||||||
|
|
||||||
|
const bookmarks = await NoteFavorites.createQueryBuilder('bookmark')
|
||||||
|
.select('bookmark.noteId')
|
||||||
|
.where('bookmark.userId = :meId', { meId: user.id })
|
||||||
|
.andWhere('bookmark.noteId IN (:...targets)', { targets })
|
||||||
|
.getMany();
|
||||||
|
|
||||||
|
const pins = pinTargets.length > 0 ? await UserNotePinings.createQueryBuilder('pin')
|
||||||
|
.select('pin.noteId')
|
||||||
|
.where('pin.userId = :meId', { meId: user.id })
|
||||||
|
.andWhere('pin.noteId IN (:...targets)', { targets: pinTargets })
|
||||||
|
.getMany() : [];
|
||||||
|
|
||||||
|
for (const target of targets) {
|
||||||
|
reactionAggregate.set(target, reactions.find(r => r.noteId === target) ?? null);
|
||||||
|
renoteAggregate.set(target, !!renotes.find(n => n.renoteId === target));
|
||||||
|
bookmarkAggregate.set(target, !!bookmarks.find(b => b.noteId === target));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const target of mutingTargets) {
|
||||||
|
mutingAggregate.set(target, !!mutings.find(m => m.threadId === target));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const target of pinTargets) {
|
||||||
|
mutingAggregate.set(target, !!pins.find(m => m.noteId === target));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.reactionAggregate = reactionAggregate;
|
||||||
|
ctx.renoteAggregate = renoteAggregate;
|
||||||
|
ctx.mutingAggregate = mutingAggregate;
|
||||||
|
ctx.bookmarkAggregate = bookmarkAggregate;
|
||||||
|
ctx.pinAggregate = pinAggregate;
|
||||||
|
|
||||||
|
const users = notes.filter(p => !!p.user).map(p => p.user as User);
|
||||||
|
await UserConverter.aggregateData([...users], ctx)
|
||||||
|
await prefetchEmojis(aggregateNoteEmojis(notes));
|
||||||
|
}
|
||||||
|
|
||||||
private static encodeReactions(reactions: Record<string, number>, myReaction: string | undefined, populated: PopulatedEmoji[]): MastodonEntity.Reaction[] {
|
private static encodeReactions(reactions: Record<string, number>, myReaction: string | undefined, populated: PopulatedEmoji[]): MastodonEntity.Reaction[] {
|
||||||
return Object.keys(reactions).map(key => {
|
return Object.keys(reactions).map(key => {
|
||||||
|
|
||||||
|
@ -10,6 +10,9 @@ import { AccountCache, UserHelpers } from "@/server/api/mastodon/helpers/user.js
|
|||||||
import { MfmHelpers } from "@/server/api/mastodon/helpers/mfm.js";
|
import { MfmHelpers } from "@/server/api/mastodon/helpers/mfm.js";
|
||||||
import { MastoContext } from "@/server/api/mastodon/index.js";
|
import { MastoContext } from "@/server/api/mastodon/index.js";
|
||||||
import { IMentionedRemoteUsers } from "@/models/entities/note.js";
|
import { IMentionedRemoteUsers } from "@/models/entities/note.js";
|
||||||
|
import { UserProfile } from "@/models/entities/user-profile.js";
|
||||||
|
import { In } from "typeorm";
|
||||||
|
import { unique } from "@/prelude/array.js";
|
||||||
|
|
||||||
type Field = {
|
type Field = {
|
||||||
name: string;
|
name: string;
|
||||||
@ -32,8 +35,13 @@ export class UserConverter {
|
|||||||
acct = `${u.username}@${u.host}`;
|
acct = `${u.username}@${u.host}`;
|
||||||
acctUrl = `https://${u.host}/@${u.username}`;
|
acctUrl = `https://${u.host}/@${u.username}`;
|
||||||
}
|
}
|
||||||
const profile = UserProfiles.findOneBy({ userId: u.id });
|
|
||||||
const bio = profile.then(profile => MfmHelpers.toHtml(mfm.parse(profile?.description ?? ""), profile?.mentions, u.host).then(p => p ?? escapeMFM(profile?.description ?? "")));
|
const aggregateProfile = (ctx.userProfileAggregate as Map<string, UserProfile | null>)?.get(u.id);
|
||||||
|
|
||||||
|
const profile = aggregateProfile !== undefined
|
||||||
|
? aggregateProfile
|
||||||
|
: UserProfiles.findOneBy({ userId: u.id });
|
||||||
|
const bio = Promise.resolve(profile).then(profile => MfmHelpers.toHtml(mfm.parse(profile?.description ?? ""), profile?.mentions, u.host).then(p => p ?? escapeMFM(profile?.description ?? "")));
|
||||||
const avatar = u.avatarId
|
const avatar = u.avatarId
|
||||||
? DriveFiles.getFinalUrlMaybe(u.avatarUrl) ?? (DriveFiles.findOneBy({ id: u.avatarId }))
|
? DriveFiles.getFinalUrlMaybe(u.avatarUrl) ?? (DriveFiles.findOneBy({ id: u.avatarId }))
|
||||||
.then(p => p?.url ?? Users.getIdenticonUrl(u.id))
|
.then(p => p?.url ?? Users.getIdenticonUrl(u.id))
|
||||||
@ -45,17 +53,18 @@ export class UserConverter {
|
|||||||
.then(p => DriveFiles.getFinalUrl(p))
|
.then(p => DriveFiles.getFinalUrl(p))
|
||||||
: `${config.url}/static-assets/transparent.png`;
|
: `${config.url}/static-assets/transparent.png`;
|
||||||
|
|
||||||
const isFollowedOrSelf = !!localUser &&
|
const isFollowedOrSelf = (ctx.followedOrSelfAggregate as Map<string, boolean>)?.get(u.id)
|
||||||
(localUser.id === u.id ||
|
?? (!!localUser &&
|
||||||
Followings.exist({
|
(localUser.id === u.id ||
|
||||||
where: {
|
Followings.exist({
|
||||||
followeeId: u.id,
|
where: {
|
||||||
followerId: localUser.id,
|
followeeId: u.id,
|
||||||
},
|
followerId: localUser.id,
|
||||||
})
|
},
|
||||||
);
|
})
|
||||||
|
));
|
||||||
|
|
||||||
const followersCount = profile.then(async profile => {
|
const followersCount = Promise.resolve(profile).then(async profile => {
|
||||||
if (profile === null) return u.followersCount;
|
if (profile === null) return u.followersCount;
|
||||||
switch (profile.ffVisibility) {
|
switch (profile.ffVisibility) {
|
||||||
case "public":
|
case "public":
|
||||||
@ -66,7 +75,7 @@ export class UserConverter {
|
|||||||
return localUser?.id === profile.userId ? u.followersCount : 0;
|
return localUser?.id === profile.userId ? u.followersCount : 0;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const followingCount = profile.then(async profile => {
|
const followingCount = Promise.resolve(profile).then(async profile => {
|
||||||
if (profile === null) return u.followingCount;
|
if (profile === null) return u.followingCount;
|
||||||
switch (profile.ffVisibility) {
|
switch (profile.ffVisibility) {
|
||||||
case "public":
|
case "public":
|
||||||
@ -97,7 +106,7 @@ export class UserConverter {
|
|||||||
header_static: banner,
|
header_static: banner,
|
||||||
emojis: populateEmojis(u.emojis, u.host).then(emoji => emoji.map((e) => EmojiConverter.encode(e))),
|
emojis: populateEmojis(u.emojis, u.host).then(emoji => emoji.map((e) => EmojiConverter.encode(e))),
|
||||||
moved: null, //FIXME
|
moved: null, //FIXME
|
||||||
fields: profile.then(profile => Promise.all(profile?.fields.map(async p => this.encodeField(p, u.host, profile?.mentions)) ?? [])),
|
fields: Promise.resolve(profile).then(profile => Promise.all(profile?.fields.map(async p => this.encodeField(p, u.host, profile?.mentions)) ?? [])),
|
||||||
bot: u.isBot,
|
bot: u.isBot,
|
||||||
discoverable: u.isExplorable
|
discoverable: u.isExplorable
|
||||||
}).then(p => {
|
}).then(p => {
|
||||||
@ -109,7 +118,40 @@ export class UserConverter {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async aggregateData(users: User[], ctx: MastoContext): Promise<void> {
|
||||||
|
const user = ctx.user as ILocalUser | null;
|
||||||
|
const targets = unique(users.map(u => u.id));
|
||||||
|
|
||||||
|
const followedOrSelfAggregate = new Map<User["id"], boolean>();
|
||||||
|
const userProfileAggregate = new Map<User["id"], UserProfile | null>();
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
const followings = await Followings.createQueryBuilder('following')
|
||||||
|
.select('following.followeeId')
|
||||||
|
.where('following.followerId = :meId', { meId: user.id })
|
||||||
|
.andWhere('following.followeeId IN (:...targets)', { targets: targets.filter(u => u !== user.id) })
|
||||||
|
.getMany();
|
||||||
|
|
||||||
|
followedOrSelfAggregate.set(user.id, true);
|
||||||
|
|
||||||
|
for (const userId of targets.filter(u => u !== user.id)) {
|
||||||
|
followedOrSelfAggregate.set(userId, !!followings.find(f => f.followerId === userId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
public static async encodeMany(users: User[], ctx: MastoContext): Promise<MastodonEntity.Account[]> {
|
public static async encodeMany(users: User[], ctx: MastoContext): Promise<MastodonEntity.Account[]> {
|
||||||
|
await this.aggregateData(users, ctx);
|
||||||
const encoded = users.map(u => this.encode(u, ctx));
|
const encoded = users.map(u => this.encode(u, ctx));
|
||||||
return Promise.all(encoded);
|
return Promise.all(encoded);
|
||||||
}
|
}
|
||||||
|
@ -34,6 +34,7 @@ export class TimelineHelpers {
|
|||||||
maxId,
|
maxId,
|
||||||
minId
|
minId
|
||||||
)
|
)
|
||||||
|
.leftJoinAndSelect("note.user", "user")
|
||||||
.leftJoinAndSelect("note.renote", "renote");
|
.leftJoinAndSelect("note.renote", "renote");
|
||||||
|
|
||||||
await generateFollowingQuery(query, user);
|
await generateFollowingQuery(query, user);
|
||||||
|
Loading…
Reference in New Issue
Block a user