mirror of
https://codeberg.org/yeentown/barkey.git
synced 2025-07-07 12:36:57 +00:00
use more bulk hints in NoteEntityService / UserEntityService, and run the packMany queries in parallel
This commit is contained in:
parent
5e7d0e9acc
commit
bd8cd8c4e4
7 changed files with 363 additions and 218 deletions
|
@ -122,7 +122,7 @@ export class ReactionService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// check visibility
|
// check visibility
|
||||||
if (!await this.noteEntityService.isVisibleForMe(note, user.id)) {
|
if (!await this.noteEntityService.isVisibleForMe(note, user.id, { me: user })) {
|
||||||
throw new IdentifiableError('68e9d2d1-48bf-42c2-b90a-b20e09fd3d48', 'Note not accessible for you.');
|
throw new IdentifiableError('68e9d2d1-48bf-42c2-b90a-b20e09fd3d48', 'Note not accessible for you.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -365,7 +365,7 @@ export class ApInboxService {
|
||||||
const renote = await this.apNoteService.resolveNote(target, { resolver, sentFrom: getApId(target) });
|
const renote = await this.apNoteService.resolveNote(target, { resolver, sentFrom: getApId(target) });
|
||||||
if (renote == null) return 'announce target is null';
|
if (renote == null) return 'announce target is null';
|
||||||
|
|
||||||
if (!await this.noteEntityService.isVisibleForMe(renote, actor.id)) {
|
if (!await this.noteEntityService.isVisibleForMe(renote, actor.id, { me: actor })) {
|
||||||
return 'skip: invalid actor for this activity';
|
return 'skip: invalid actor for this activity';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ import type { Packed } from '@/misc/json-schema.js';
|
||||||
import { awaitAll } from '@/misc/prelude/await-all.js';
|
import { awaitAll } from '@/misc/prelude/await-all.js';
|
||||||
import type { MiUser } from '@/models/User.js';
|
import type { MiUser } from '@/models/User.js';
|
||||||
import type { MiNote } from '@/models/Note.js';
|
import type { MiNote } from '@/models/Note.js';
|
||||||
import type { UsersRepository, NotesRepository, FollowingsRepository, PollsRepository, PollVotesRepository, NoteReactionsRepository, ChannelsRepository, MiMeta } from '@/models/_.js';
|
import type { UsersRepository, NotesRepository, FollowingsRepository, PollsRepository, PollVotesRepository, NoteReactionsRepository, ChannelsRepository, MiMeta, MiPollVote, MiPoll, MiChannel } from '@/models/_.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { DebounceLoader } from '@/misc/loader.js';
|
import { DebounceLoader } from '@/misc/loader.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
|
@ -26,13 +26,13 @@ import type { UserEntityService } from './UserEntityService.js';
|
||||||
import type { DriveFileEntityService } from './DriveFileEntityService.js';
|
import type { DriveFileEntityService } from './DriveFileEntityService.js';
|
||||||
|
|
||||||
// is-renote.tsとよしなにリンク
|
// is-renote.tsとよしなにリンク
|
||||||
function isPureRenote(note: MiNote): note is MiNote & { renoteId: MiNote['id']; renote: MiNote } {
|
function isPureRenote(note: MiNote): note is MiNote & { renoteId: MiNote['id'] } {
|
||||||
return (
|
return (
|
||||||
note.renote != null &&
|
note.renoteId != null &&
|
||||||
note.reply == null &&
|
note.replyId == null &&
|
||||||
note.text == null &&
|
note.text == null &&
|
||||||
note.cw == null &&
|
note.cw == null &&
|
||||||
(note.fileIds == null || note.fileIds.length === 0) &&
|
note.fileIds.length === 0 &&
|
||||||
!note.hasPoll
|
!note.hasPoll
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -132,7 +132,10 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async hideNote(packedNote: Packed<'Note'>, meId: MiUser['id'] | null): Promise<void> {
|
public async hideNote(packedNote: Packed<'Note'>, meId: MiUser['id'] | null, hint?: {
|
||||||
|
myFollowing?: ReadonlySet<string>,
|
||||||
|
myBlockers?: ReadonlySet<string>,
|
||||||
|
}): Promise<void> {
|
||||||
if (meId === packedNote.userId) return;
|
if (meId === packedNote.userId) return;
|
||||||
|
|
||||||
// TODO: isVisibleForMe を使うようにしても良さそう(型違うけど)
|
// TODO: isVisibleForMe を使うようにしても良さそう(型違うけど)
|
||||||
|
@ -188,14 +191,9 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
} else if (packedNote.renote && (meId === packedNote.renote.userId)) {
|
} else if (packedNote.renote && (meId === packedNote.renote.userId)) {
|
||||||
hide = false;
|
hide = false;
|
||||||
} else {
|
} else {
|
||||||
// フォロワーかどうか
|
const isFollowing = hint?.myFollowing
|
||||||
// TODO: 当関数呼び出しごとにクエリが走るのは重そうだからなんとかする
|
? hint.myFollowing.has(packedNote.userId)
|
||||||
const isFollowing = await this.followingsRepository.exists({
|
: (await this.cacheService.userFollowingsCache.fetch(meId))[packedNote.userId] != null;
|
||||||
where: {
|
|
||||||
followeeId: packedNote.userId,
|
|
||||||
followerId: meId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
hide = !isFollowing;
|
hide = !isFollowing;
|
||||||
}
|
}
|
||||||
|
@ -211,7 +209,8 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hide && meId && packedNote.userId !== meId) {
|
if (!hide && meId && packedNote.userId !== meId) {
|
||||||
const isBlocked = (await this.cacheService.userBlockedCache.fetch(meId)).has(packedNote.userId);
|
const blockers = hint?.myBlockers ?? await this.cacheService.userBlockedCache.fetch(meId);
|
||||||
|
const isBlocked = blockers.has(packedNote.userId);
|
||||||
|
|
||||||
if (isBlocked) hide = true;
|
if (isBlocked) hide = true;
|
||||||
}
|
}
|
||||||
|
@ -235,8 +234,11 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async populatePoll(note: MiNote, meId: MiUser['id'] | null) {
|
private async populatePoll(note: MiNote, meId: MiUser['id'] | null, hint?: {
|
||||||
const poll = await this.pollsRepository.findOneByOrFail({ noteId: note.id });
|
poll?: MiPoll,
|
||||||
|
myVotes?: MiPollVote[],
|
||||||
|
}) {
|
||||||
|
const poll = hint?.poll ?? await this.pollsRepository.findOneByOrFail({ noteId: note.id });
|
||||||
const choices = poll.choices.map(c => ({
|
const choices = poll.choices.map(c => ({
|
||||||
text: c,
|
text: c,
|
||||||
votes: poll.votes[poll.choices.indexOf(c)],
|
votes: poll.votes[poll.choices.indexOf(c)],
|
||||||
|
@ -245,7 +247,7 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
|
|
||||||
if (meId) {
|
if (meId) {
|
||||||
if (poll.multiple) {
|
if (poll.multiple) {
|
||||||
const votes = await this.pollVotesRepository.findBy({
|
const votes = hint?.myVotes ?? await this.pollVotesRepository.findBy({
|
||||||
userId: meId,
|
userId: meId,
|
||||||
noteId: note.id,
|
noteId: note.id,
|
||||||
});
|
});
|
||||||
|
@ -255,7 +257,7 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
choices[myChoice].isVoted = true;
|
choices[myChoice].isVoted = true;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const vote = await this.pollVotesRepository.findOneBy({
|
const vote = hint?.myVotes ? hint.myVotes[0] : await this.pollVotesRepository.findOneBy({
|
||||||
userId: meId,
|
userId: meId,
|
||||||
noteId: note.id,
|
noteId: note.id,
|
||||||
});
|
});
|
||||||
|
@ -317,7 +319,12 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async isVisibleForMe(note: MiNote, meId: MiUser['id'] | null): Promise<boolean> {
|
public async isVisibleForMe(note: MiNote, meId: MiUser['id'] | null, hint?: {
|
||||||
|
myFollowing?: ReadonlySet<string>,
|
||||||
|
myBlocking?: ReadonlySet<string>,
|
||||||
|
myBlockers?: ReadonlySet<string>,
|
||||||
|
me?: Pick<MiUser, 'host'> | null,
|
||||||
|
}): Promise<boolean> {
|
||||||
// This code must always be synchronized with the checks in generateVisibilityQuery.
|
// This code must always be synchronized with the checks in generateVisibilityQuery.
|
||||||
// visibility が specified かつ自分が指定されていなかったら非表示
|
// visibility が specified かつ自分が指定されていなかったら非表示
|
||||||
if (note.visibility === 'specified') {
|
if (note.visibility === 'specified') {
|
||||||
|
@ -345,16 +352,20 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
// フォロワーかどうか
|
// フォロワーかどうか
|
||||||
const [blocked, following, user] = await Promise.all([
|
const [blocked, following, userHost] = await Promise.all([
|
||||||
this.cacheService.userBlockingCache.fetch(meId).then((ids) => ids.has(note.userId)),
|
hint?.myBlocking
|
||||||
this.followingsRepository.count({
|
? hint.myBlocking.has(note.userId)
|
||||||
where: {
|
: this.cacheService.userBlockingCache.fetch(meId).then((ids) => ids.has(note.userId)),
|
||||||
|
hint?.myFollowing
|
||||||
|
? hint.myFollowing.has(note.userId)
|
||||||
|
: this.followingsRepository.existsBy({
|
||||||
followeeId: note.userId,
|
followeeId: note.userId,
|
||||||
followerId: meId,
|
followerId: meId,
|
||||||
},
|
|
||||||
take: 1,
|
|
||||||
}),
|
}),
|
||||||
this.usersRepository.findOneByOrFail({ id: meId }),
|
hint?.me !== undefined
|
||||||
|
? (hint.me?.host ?? null)
|
||||||
|
: this.cacheService.userByIdCache.fetch(meId, () => this.usersRepository.findOneByOrFail({ id: meId }))
|
||||||
|
.then(me => me.host),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (blocked) return false;
|
if (blocked) return false;
|
||||||
|
@ -366,12 +377,13 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
in which case we can never know the following. Instead we have
|
in which case we can never know the following. Instead we have
|
||||||
to assume that the users are following each other.
|
to assume that the users are following each other.
|
||||||
*/
|
*/
|
||||||
return following > 0 || (note.userHost != null && user.host != null);
|
return following || (note.userHost != null && userHost != null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (meId != null) {
|
if (meId != null) {
|
||||||
const isBlocked = (await this.cacheService.userBlockedCache.fetch(meId)).has(note.userId);
|
const blockers = hint?.myBlockers ?? await this.cacheService.userBlockedCache.fetch(meId);
|
||||||
|
const isBlocked = blockers.has(note.userId);
|
||||||
|
|
||||||
if (isBlocked) return false;
|
if (isBlocked) return false;
|
||||||
}
|
}
|
||||||
|
@ -408,6 +420,11 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
packedFiles: Map<MiNote['fileIds'][number], Packed<'DriveFile'> | null>;
|
packedFiles: Map<MiNote['fileIds'][number], Packed<'DriveFile'> | null>;
|
||||||
packedUsers: Map<MiUser['id'], Packed<'UserLite'>>;
|
packedUsers: Map<MiUser['id'], Packed<'UserLite'>>;
|
||||||
mentionHandles: Record<string, string | undefined>;
|
mentionHandles: Record<string, string | undefined>;
|
||||||
|
userFollowings: Map<string, Set<string>>;
|
||||||
|
userBlockers: Map<string, Set<string>>;
|
||||||
|
polls: Map<string, MiPoll>;
|
||||||
|
pollVotes: Map<string, Map<string, MiPollVote[]>>;
|
||||||
|
channels: Map<string, MiChannel>;
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
): Promise<Packed<'Note'>> {
|
): Promise<Packed<'Note'>> {
|
||||||
|
@ -437,9 +454,7 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
const channel = note.channelId
|
const channel = note.channelId
|
||||||
? note.channel
|
? (opts._hint_?.channels.get(note.channelId) ?? note.channel ?? await this.channelsRepository.findOneBy({ id: note.channelId }))
|
||||||
? note.channel
|
|
||||||
: await this.channelsRepository.findOneBy({ id: note.channelId })
|
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const reactionEmojiNames = Object.keys(reactions)
|
const reactionEmojiNames = Object.keys(reactions)
|
||||||
|
@ -485,7 +500,10 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
mentionHandles: note.mentions.length > 0 ? this.getUserHandles(note.mentions, options?._hint_?.mentionHandles) : undefined,
|
mentionHandles: note.mentions.length > 0 ? this.getUserHandles(note.mentions, options?._hint_?.mentionHandles) : undefined,
|
||||||
uri: note.uri ?? undefined,
|
uri: note.uri ?? undefined,
|
||||||
url: note.url ?? undefined,
|
url: note.url ?? undefined,
|
||||||
poll: note.hasPoll ? this.populatePoll(note, meId) : undefined,
|
poll: note.hasPoll ? this.populatePoll(note, meId, {
|
||||||
|
poll: opts._hint_?.polls.get(note.id),
|
||||||
|
myVotes: opts._hint_?.pollVotes.get(note.id)?.get(note.userId),
|
||||||
|
}) : undefined,
|
||||||
|
|
||||||
...(meId && Object.keys(reactions).length > 0 ? {
|
...(meId && Object.keys(reactions).length > 0 ? {
|
||||||
myReaction: this.populateMyReaction({
|
myReaction: this.populateMyReaction({
|
||||||
|
@ -518,7 +536,10 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
this.treatVisibility(packed);
|
this.treatVisibility(packed);
|
||||||
|
|
||||||
if (!opts.skipHide) {
|
if (!opts.skipHide) {
|
||||||
await this.hideNote(packed, meId);
|
await this.hideNote(packed, meId, meId == null ? undefined : {
|
||||||
|
myFollowing: opts._hint_?.userFollowings.get(meId),
|
||||||
|
myBlockers: opts._hint_?.userBlockers.get(meId),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return packed;
|
return packed;
|
||||||
|
@ -536,69 +557,53 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
if (notes.length === 0) return [];
|
if (notes.length === 0) return [];
|
||||||
|
|
||||||
const targetNotes: MiNote[] = [];
|
const targetNotes: MiNote[] = [];
|
||||||
|
const targetNotesToFetch: string[] = [];
|
||||||
for (const note of notes) {
|
for (const note of notes) {
|
||||||
if (isPureRenote(note)) {
|
if (isPureRenote(note)) {
|
||||||
// we may need to fetch 'my reaction' for renote target.
|
// we may need to fetch 'my reaction' for renote target.
|
||||||
|
if (note.renote) {
|
||||||
targetNotes.push(note.renote);
|
targetNotes.push(note.renote);
|
||||||
if (note.renote.reply) {
|
if (note.renote.reply) {
|
||||||
// idem if the renote is also a reply.
|
// idem if the renote is also a reply.
|
||||||
targetNotes.push(note.renote.reply);
|
targetNotes.push(note.renote.reply);
|
||||||
}
|
}
|
||||||
|
} else if (options?.detail) {
|
||||||
|
targetNotesToFetch.push(note.renoteId);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (note.reply) {
|
if (note.reply) {
|
||||||
// idem for OP of a regular reply.
|
// idem for OP of a regular reply.
|
||||||
targetNotes.push(note.reply);
|
targetNotes.push(note.reply);
|
||||||
|
} else if (note.replyId && options?.detail) {
|
||||||
|
targetNotesToFetch.push(note.replyId);
|
||||||
}
|
}
|
||||||
|
|
||||||
targetNotes.push(note);
|
targetNotes.push(note);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const bufferedReactions = this.meta.enableReactionsBuffering ? await this.reactionsBufferingService.getMany([...getAppearNoteIds(notes)]) : null;
|
// Populate any relations that weren't included in the source
|
||||||
|
if (targetNotesToFetch.length > 0) {
|
||||||
const meId = me ? me.id : null;
|
const newNotes = await this.notesRepository.find({
|
||||||
const myReactionsMap = new Map<MiNote['id'], string | null>();
|
where: {
|
||||||
if (meId) {
|
id: In(targetNotesToFetch),
|
||||||
const idsNeedFetchMyReaction = new Set<MiNote['id']>();
|
},
|
||||||
|
relations: {
|
||||||
for (const note of targetNotes) {
|
user: true,
|
||||||
const reactionsCount = Object.values(this.reactionsBufferingService.mergeReactions(note.reactions, bufferedReactions?.get(note.id)?.deltas ?? {})).reduce((a, b) => a + b, 0);
|
reply: true,
|
||||||
if (reactionsCount === 0) {
|
renote: true,
|
||||||
myReactionsMap.set(note.id, null);
|
channel: true,
|
||||||
} else if (reactionsCount <= note.reactionAndUserPairCache.length + (bufferedReactions?.get(note.id)?.pairs.length ?? 0)) {
|
},
|
||||||
const pairInBuffer = bufferedReactions?.get(note.id)?.pairs.find(p => p[0] === meId);
|
});
|
||||||
if (pairInBuffer) {
|
targetNotes.push(...newNotes);
|
||||||
myReactionsMap.set(note.id, pairInBuffer[1]);
|
|
||||||
} else {
|
|
||||||
const pair = note.reactionAndUserPairCache.find(p => p.startsWith(meId));
|
|
||||||
myReactionsMap.set(note.id, pair ? pair.split('/')[1] : null);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
idsNeedFetchMyReaction.add(note.id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const myReactions = idsNeedFetchMyReaction.size > 0 ? await this.noteReactionsRepository.findBy({
|
|
||||||
userId: meId,
|
|
||||||
noteId: In(Array.from(idsNeedFetchMyReaction)),
|
|
||||||
}) : [];
|
|
||||||
|
|
||||||
for (const id of idsNeedFetchMyReaction) {
|
|
||||||
myReactionsMap.set(id, myReactions.find(reaction => reaction.noteId === id)?.reaction ?? null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.customEmojiService.prefetchEmojis(this.aggregateNoteEmojis(notes));
|
|
||||||
// TODO: 本当は renote とか reply がないのに renoteId とか replyId があったらここで解決しておく
|
|
||||||
const fileIds = notes.map(n => [n.fileIds, n.renote?.fileIds, n.reply?.fileIds]).flat(2).filter(x => x != null);
|
const fileIds = notes.map(n => [n.fileIds, n.renote?.fileIds, n.reply?.fileIds]).flat(2).filter(x => x != null);
|
||||||
const packedFiles = fileIds.length > 0 ? await this.driveFileEntityService.packManyByIdsMap(fileIds) : new Map();
|
|
||||||
const users = [
|
const users = [
|
||||||
...notes.map(({ user, userId }) => user ?? userId),
|
...notes.map(({ user, userId }) => user ?? userId),
|
||||||
...notes.map(({ replyUserId }) => replyUserId).filter(x => x != null),
|
...notes.map(({ reply, replyUserId }) => reply?.user ?? replyUserId).filter(x => x != null),
|
||||||
...notes.map(({ renoteUserId }) => renoteUserId).filter(x => x != null),
|
...notes.map(({ renote, renoteUserId }) => renote?.user ?? renoteUserId).filter(x => x != null),
|
||||||
];
|
];
|
||||||
const packedUsers = await this.userEntityService.packMany(users, me)
|
|
||||||
.then(users => new Map(users.map(u => [u.id, u])));
|
|
||||||
|
|
||||||
// Recursively add all mentioned users from all notes + replies + renotes
|
// Recursively add all mentioned users from all notes + replies + renotes
|
||||||
const allMentionedUsers = targetNotes.reduce((users, note) => {
|
const allMentionedUsers = targetNotes.reduce((users, note) => {
|
||||||
|
@ -607,7 +612,47 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
}
|
}
|
||||||
return users;
|
return users;
|
||||||
}, new Set<string>());
|
}, new Set<string>());
|
||||||
const mentionHandles = await this.getUserHandles(Array.from(allMentionedUsers));
|
|
||||||
|
const userIds = Array.from(new Set(users.map(u => typeof(u) === 'string' ? u : u.id)));
|
||||||
|
const noteIds = Array.from(new Set(targetNotes.map(n => n.id)));
|
||||||
|
const [{ bufferedReactions, myReactionsMap }, packedFiles, packedUsers, mentionHandles, userFollowings, userBlockers, polls, pollVotes, channels] = await Promise.all([
|
||||||
|
// bufferedReactions & myReactionsMap
|
||||||
|
this.getReactions(targetNotes, me),
|
||||||
|
// packedFiles
|
||||||
|
this.driveFileEntityService.packManyByIdsMap(fileIds),
|
||||||
|
// packedUsers
|
||||||
|
this.userEntityService.packMany(users, me)
|
||||||
|
.then(users => new Map(users.map(u => [u.id, u]))),
|
||||||
|
// mentionHandles
|
||||||
|
this.getUserHandles(Array.from(allMentionedUsers)),
|
||||||
|
// userFollowings
|
||||||
|
this.cacheService.getUserFollowings(userIds),
|
||||||
|
// userBlockers
|
||||||
|
this.cacheService.getUserBlockers(userIds),
|
||||||
|
// polls
|
||||||
|
this.pollsRepository.findBy({ noteId: In(noteIds) })
|
||||||
|
.then(polls => new Map(polls.map(p => [p.noteId, p]))),
|
||||||
|
// pollVotes
|
||||||
|
this.pollVotesRepository.findBy({ noteId: In(noteIds), userId: In(userIds) })
|
||||||
|
.then(votes => votes.reduce((noteMap, vote) => {
|
||||||
|
let userMap = noteMap.get(vote.noteId);
|
||||||
|
if (!userMap) {
|
||||||
|
userMap = new Map<string, MiPollVote[]>();
|
||||||
|
noteMap.set(vote.noteId, userMap);
|
||||||
|
}
|
||||||
|
let voteList = userMap.get(vote.userId);
|
||||||
|
if (!voteList) {
|
||||||
|
voteList = [];
|
||||||
|
userMap.set(vote.userId, voteList);
|
||||||
|
}
|
||||||
|
voteList.push(vote);
|
||||||
|
return noteMap;
|
||||||
|
}, new Map<string, Map<string, MiPollVote[]>>)),
|
||||||
|
// channels
|
||||||
|
this.getChannels(targetNotes),
|
||||||
|
// (not returned)
|
||||||
|
this.customEmojiService.prefetchEmojis(this.aggregateNoteEmojis(notes)),
|
||||||
|
]);
|
||||||
|
|
||||||
return await Promise.all(notes.map(n => this.pack(n, me, {
|
return await Promise.all(notes.map(n => this.pack(n, me, {
|
||||||
...options,
|
...options,
|
||||||
|
@ -617,6 +662,11 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
packedFiles,
|
packedFiles,
|
||||||
packedUsers,
|
packedUsers,
|
||||||
mentionHandles,
|
mentionHandles,
|
||||||
|
userFollowings,
|
||||||
|
userBlockers,
|
||||||
|
polls,
|
||||||
|
pollVotes,
|
||||||
|
channels,
|
||||||
},
|
},
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
|
@ -685,6 +735,68 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
}, {} as Record<string, string | undefined>);
|
}, {} as Record<string, string | undefined>);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async getChannels(notes: MiNote[]): Promise<Map<string, MiChannel>> {
|
||||||
|
const channels = new Map<string, MiChannel>();
|
||||||
|
const channelsToFetch = new Set<string>();
|
||||||
|
|
||||||
|
for (const note of notes) {
|
||||||
|
if (note.channel) {
|
||||||
|
channels.set(note.channel.id, note.channel);
|
||||||
|
} else if (note.channelId) {
|
||||||
|
channelsToFetch.add(note.channelId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (channelsToFetch.size > 0) {
|
||||||
|
const newChannels = await this.channelsRepository.findBy({
|
||||||
|
id: In(Array.from(channelsToFetch)),
|
||||||
|
});
|
||||||
|
for (const channel of newChannels) {
|
||||||
|
channels.set(channel.id, channel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return channels;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getReactions(notes: MiNote[], me: { id: string } | null | undefined) {
|
||||||
|
const bufferedReactions = this.meta.enableReactionsBuffering ? await this.reactionsBufferingService.getMany([...getAppearNoteIds(notes)]) : null;
|
||||||
|
|
||||||
|
const meId = me ? me.id : null;
|
||||||
|
const myReactionsMap = new Map<MiNote['id'], string | null>();
|
||||||
|
if (meId) {
|
||||||
|
const idsNeedFetchMyReaction = new Set<MiNote['id']>();
|
||||||
|
|
||||||
|
for (const note of notes) {
|
||||||
|
const reactionsCount = Object.values(this.reactionsBufferingService.mergeReactions(note.reactions, bufferedReactions?.get(note.id)?.deltas ?? {})).reduce((a, b) => a + b, 0);
|
||||||
|
if (reactionsCount === 0) {
|
||||||
|
myReactionsMap.set(note.id, null);
|
||||||
|
} else if (reactionsCount <= note.reactionAndUserPairCache.length + (bufferedReactions?.get(note.id)?.pairs.length ?? 0)) {
|
||||||
|
const pairInBuffer = bufferedReactions?.get(note.id)?.pairs.find(p => p[0] === meId);
|
||||||
|
if (pairInBuffer) {
|
||||||
|
myReactionsMap.set(note.id, pairInBuffer[1]);
|
||||||
|
} else {
|
||||||
|
const pair = note.reactionAndUserPairCache.find(p => p.startsWith(meId));
|
||||||
|
myReactionsMap.set(note.id, pair ? pair.split('/')[1] : null);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
idsNeedFetchMyReaction.add(note.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const myReactions = idsNeedFetchMyReaction.size > 0 ? await this.noteReactionsRepository.findBy({
|
||||||
|
userId: meId,
|
||||||
|
noteId: In(Array.from(idsNeedFetchMyReaction)),
|
||||||
|
}) : [];
|
||||||
|
|
||||||
|
for (const id of idsNeedFetchMyReaction) {
|
||||||
|
myReactionsMap.set(id, myReactions.find(reaction => reaction.noteId === id)?.reaction ?? null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { bufferedReactions, myReactionsMap };
|
||||||
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public genLocalNoteUri(noteId: string): string {
|
public genLocalNoteUri(noteId: string): string {
|
||||||
return `${this.config.url}/notes/${noteId}`;
|
return `${this.config.url}/notes/${noteId}`;
|
||||||
|
|
|
@ -30,6 +30,7 @@ import type {
|
||||||
FollowingsRepository,
|
FollowingsRepository,
|
||||||
FollowRequestsRepository,
|
FollowRequestsRepository,
|
||||||
MiFollowing,
|
MiFollowing,
|
||||||
|
MiInstance,
|
||||||
MiMeta,
|
MiMeta,
|
||||||
MiUserNotePining,
|
MiUserNotePining,
|
||||||
MiUserProfile,
|
MiUserProfile,
|
||||||
|
@ -42,7 +43,7 @@ import type {
|
||||||
UsersRepository,
|
UsersRepository,
|
||||||
} from '@/models/_.js';
|
} from '@/models/_.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { RoleService } from '@/core/RoleService.js';
|
import { RolePolicies, RoleService } from '@/core/RoleService.js';
|
||||||
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
|
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
|
||||||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
|
@ -52,6 +53,7 @@ import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
|
||||||
import { ChatService } from '@/core/ChatService.js';
|
import { ChatService } from '@/core/ChatService.js';
|
||||||
import { isSystemAccount } from '@/misc/is-system-account.js';
|
import { isSystemAccount } from '@/misc/is-system-account.js';
|
||||||
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
||||||
|
import type { CacheService } from '@/core/CacheService.js';
|
||||||
import type { OnModuleInit } from '@nestjs/common';
|
import type { OnModuleInit } from '@nestjs/common';
|
||||||
import type { NoteEntityService } from './NoteEntityService.js';
|
import type { NoteEntityService } from './NoteEntityService.js';
|
||||||
import type { PageEntityService } from './PageEntityService.js';
|
import type { PageEntityService } from './PageEntityService.js';
|
||||||
|
@ -103,6 +105,7 @@ export class UserEntityService implements OnModuleInit {
|
||||||
private idService: IdService;
|
private idService: IdService;
|
||||||
private avatarDecorationService: AvatarDecorationService;
|
private avatarDecorationService: AvatarDecorationService;
|
||||||
private chatService: ChatService;
|
private chatService: ChatService;
|
||||||
|
private cacheService: CacheService;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private moduleRef: ModuleRef,
|
private moduleRef: ModuleRef,
|
||||||
|
@ -163,6 +166,7 @@ export class UserEntityService implements OnModuleInit {
|
||||||
this.idService = this.moduleRef.get('IdService');
|
this.idService = this.moduleRef.get('IdService');
|
||||||
this.avatarDecorationService = this.moduleRef.get('AvatarDecorationService');
|
this.avatarDecorationService = this.moduleRef.get('AvatarDecorationService');
|
||||||
this.chatService = this.moduleRef.get('ChatService');
|
this.chatService = this.moduleRef.get('ChatService');
|
||||||
|
this.cacheService = this.moduleRef.get('CacheService');
|
||||||
}
|
}
|
||||||
|
|
||||||
//#region Validators
|
//#region Validators
|
||||||
|
@ -215,45 +219,23 @@ export class UserEntityService implements OnModuleInit {
|
||||||
followeeId: me,
|
followeeId: me,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
this.blockingsRepository.exists({
|
this.cacheService.userBlockedCache.fetch(me)
|
||||||
where: {
|
.then(blockers => blockers.size > 0),
|
||||||
blockerId: me,
|
this.cacheService.userBlockingCache.fetch(me)
|
||||||
blockeeId: target,
|
.then(blockees => blockees.size > 0),
|
||||||
},
|
this.cacheService.userMutingsCache.fetch(me)
|
||||||
}),
|
.then(mutings => mutings.size > 0),
|
||||||
this.blockingsRepository.exists({
|
this.cacheService.renoteMutingsCache.fetch(me)
|
||||||
where: {
|
.then(mutings => mutings.size > 0),
|
||||||
blockerId: target,
|
this.cacheService.userByIdCache.fetch(target, () => this.usersRepository.findOneByOrFail({ id: target }))
|
||||||
blockeeId: me,
|
.then(user => user.host),
|
||||||
},
|
|
||||||
}),
|
|
||||||
this.mutingsRepository.exists({
|
|
||||||
where: {
|
|
||||||
muterId: me,
|
|
||||||
muteeId: target,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
this.renoteMutingsRepository.exists({
|
|
||||||
where: {
|
|
||||||
muterId: me,
|
|
||||||
muteeId: target,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
this.usersRepository.createQueryBuilder('u')
|
|
||||||
.select('u.host')
|
|
||||||
.where({ id: target })
|
|
||||||
.getRawOne<{ u_host: string }>()
|
|
||||||
.then(it => it?.u_host ?? null),
|
|
||||||
this.userMemosRepository.createQueryBuilder('m')
|
this.userMemosRepository.createQueryBuilder('m')
|
||||||
.select('m.memo')
|
.select('m.memo')
|
||||||
.where({ userId: me, targetUserId: target })
|
.where({ userId: me, targetUserId: target })
|
||||||
.getRawOne<{ m_memo: string | null }>()
|
.getRawOne<{ m_memo: string | null }>()
|
||||||
.then(it => it?.m_memo ?? null),
|
.then(it => it?.m_memo ?? null),
|
||||||
this.userProfilesRepository.createQueryBuilder('p')
|
this.cacheService.userProfileCache.fetch(me)
|
||||||
.select('p.mutedInstances')
|
.then(profile => profile.mutedInstances),
|
||||||
.where({ userId: me })
|
|
||||||
.getRawOne<{ p_mutedInstances: string[] }>()
|
|
||||||
.then(it => it?.p_mutedInstances ?? []),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const isInstanceMuted = !!host && mutedInstances.includes(host);
|
const isInstanceMuted = !!host && mutedInstances.includes(host);
|
||||||
|
@ -306,34 +288,18 @@ export class UserEntityService implements OnModuleInit {
|
||||||
.where('f.followeeId = :me', { me })
|
.where('f.followeeId = :me', { me })
|
||||||
.getRawMany<{ f_followerId: string }>()
|
.getRawMany<{ f_followerId: string }>()
|
||||||
.then(it => it.map(it => it.f_followerId)),
|
.then(it => it.map(it => it.f_followerId)),
|
||||||
this.blockingsRepository.createQueryBuilder('b')
|
this.cacheService.userBlockedCache.fetch(me),
|
||||||
.select('b.blockeeId')
|
this.cacheService.userBlockingCache.fetch(me),
|
||||||
.where('b.blockerId = :me', { me })
|
this.cacheService.userMutingsCache.fetch(me),
|
||||||
.getRawMany<{ b_blockeeId: string }>()
|
this.cacheService.renoteMutingsCache.fetch(me),
|
||||||
.then(it => it.map(it => it.b_blockeeId)),
|
this.cacheService.getUsers(targets)
|
||||||
this.blockingsRepository.createQueryBuilder('b')
|
.then(users => {
|
||||||
.select('b.blockerId')
|
const record: Record<string, string | null> = {};
|
||||||
.where('b.blockeeId = :me', { me })
|
for (const [id, user] of users) {
|
||||||
.getRawMany<{ b_blockerId: string }>()
|
record[id] = user.host;
|
||||||
.then(it => it.map(it => it.b_blockerId)),
|
}
|
||||||
this.mutingsRepository.createQueryBuilder('m')
|
return record;
|
||||||
.select('m.muteeId')
|
}),
|
||||||
.where('m.muterId = :me', { me })
|
|
||||||
.getRawMany<{ m_muteeId: string }>()
|
|
||||||
.then(it => it.map(it => it.m_muteeId)),
|
|
||||||
this.renoteMutingsRepository.createQueryBuilder('m')
|
|
||||||
.select('m.muteeId')
|
|
||||||
.where('m.muterId = :me', { me })
|
|
||||||
.getRawMany<{ m_muteeId: string }>()
|
|
||||||
.then(it => it.map(it => it.m_muteeId)),
|
|
||||||
this.usersRepository.createQueryBuilder('u')
|
|
||||||
.select(['u.id', 'u.host'])
|
|
||||||
.where({ id: In(targets) } )
|
|
||||||
.getRawMany<{ m_id: string, m_host: string }>()
|
|
||||||
.then(it => it.reduce((map, it) => {
|
|
||||||
map[it.m_id] = it.m_host;
|
|
||||||
return map;
|
|
||||||
}, {} as Record<string, string>)),
|
|
||||||
this.userMemosRepository.createQueryBuilder('m')
|
this.userMemosRepository.createQueryBuilder('m')
|
||||||
.select(['m.targetUserId', 'm.memo'])
|
.select(['m.targetUserId', 'm.memo'])
|
||||||
.where({ userId: me, targetUserId: In(targets) })
|
.where({ userId: me, targetUserId: In(targets) })
|
||||||
|
@ -342,11 +308,8 @@ export class UserEntityService implements OnModuleInit {
|
||||||
map[it.m_targetUserId] = it.m_memo;
|
map[it.m_targetUserId] = it.m_memo;
|
||||||
return map;
|
return map;
|
||||||
}, {} as Record<string, string | null>)),
|
}, {} as Record<string, string | null>)),
|
||||||
this.userProfilesRepository.createQueryBuilder('p')
|
this.cacheService.userProfileCache.fetch(me)
|
||||||
.select('p.mutedInstances')
|
.then(p => p.mutedInstances),
|
||||||
.where({ userId: me })
|
|
||||||
.getRawOne<{ p_mutedInstances: string[] }>()
|
|
||||||
.then(it => it?.p_mutedInstances ?? []),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return new Map(
|
return new Map(
|
||||||
|
@ -362,11 +325,11 @@ export class UserEntityService implements OnModuleInit {
|
||||||
isFollowed: followees.includes(target),
|
isFollowed: followees.includes(target),
|
||||||
hasPendingFollowRequestFromYou: followersRequests.includes(target),
|
hasPendingFollowRequestFromYou: followersRequests.includes(target),
|
||||||
hasPendingFollowRequestToYou: followeesRequests.includes(target),
|
hasPendingFollowRequestToYou: followeesRequests.includes(target),
|
||||||
isBlocking: blockers.includes(target),
|
isBlocking: blockers.has(target),
|
||||||
isBlocked: blockees.includes(target),
|
isBlocked: blockees.has(target),
|
||||||
isMuted: muters.includes(target),
|
isMuted: muters.has(target),
|
||||||
isRenoteMuted: renoteMuters.includes(target),
|
isRenoteMuted: renoteMuters.has(target),
|
||||||
isInstanceMuted: mutedInstances.includes(hosts[target]),
|
isInstanceMuted: hosts[target] != null && mutedInstances.includes(hosts[target]),
|
||||||
memo: memos[target] ?? null,
|
memo: memos[target] ?? null,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
@ -391,6 +354,7 @@ export class UserEntityService implements OnModuleInit {
|
||||||
return false; // TODO
|
return false; // TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO make redis calls in MULTI?
|
||||||
@bindThis
|
@bindThis
|
||||||
public async getNotificationsInfo(userId: MiUser['id']): Promise<{
|
public async getNotificationsInfo(userId: MiUser['id']): Promise<{
|
||||||
hasUnread: boolean;
|
hasUnread: boolean;
|
||||||
|
@ -424,16 +388,14 @@ export class UserEntityService implements OnModuleInit {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async getHasPendingReceivedFollowRequest(userId: MiUser['id']): Promise<boolean> {
|
public async getHasPendingReceivedFollowRequest(userId: MiUser['id']): Promise<boolean> {
|
||||||
const count = await this.followRequestsRepository.countBy({
|
return await this.followRequestsRepository.existsBy({
|
||||||
followeeId: userId,
|
followeeId: userId,
|
||||||
});
|
});
|
||||||
|
|
||||||
return count > 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async getHasPendingSentFollowRequest(userId: MiUser['id']): Promise<boolean> {
|
public async getHasPendingSentFollowRequest(userId: MiUser['id']): Promise<boolean> {
|
||||||
return this.followRequestsRepository.existsBy({
|
return await this.followRequestsRepository.existsBy({
|
||||||
followerId: userId,
|
followerId: userId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -480,6 +442,12 @@ export class UserEntityService implements OnModuleInit {
|
||||||
userRelations?: Map<MiUser['id'], UserRelation>,
|
userRelations?: Map<MiUser['id'], UserRelation>,
|
||||||
userMemos?: Map<MiUser['id'], string | null>,
|
userMemos?: Map<MiUser['id'], string | null>,
|
||||||
pinNotes?: Map<MiUser['id'], MiUserNotePining[]>,
|
pinNotes?: Map<MiUser['id'], MiUserNotePining[]>,
|
||||||
|
iAmModerator?: boolean,
|
||||||
|
userIdsByUri?: Map<string, string>,
|
||||||
|
instances?: Map<string, MiInstance | null>,
|
||||||
|
securityKeyCounts?: Map<string, number>,
|
||||||
|
pendingReceivedFollows?: Set<string>,
|
||||||
|
pendingSentFollows?: Set<string>,
|
||||||
},
|
},
|
||||||
): Promise<Packed<S>> {
|
): Promise<Packed<S>> {
|
||||||
const opts = Object.assign({
|
const opts = Object.assign({
|
||||||
|
@ -521,7 +489,7 @@ export class UserEntityService implements OnModuleInit {
|
||||||
const isDetailed = opts.schema !== 'UserLite';
|
const isDetailed = opts.schema !== 'UserLite';
|
||||||
const meId = me ? me.id : null;
|
const meId = me ? me.id : null;
|
||||||
const isMe = meId === user.id;
|
const isMe = meId === user.id;
|
||||||
const iAmModerator = me ? await this.roleService.isModerator(me as MiUser) : false;
|
const iAmModerator = opts.iAmModerator ?? (me ? await this.roleService.isModerator(me as MiUser) : false);
|
||||||
|
|
||||||
const profile = isDetailed
|
const profile = isDetailed
|
||||||
? (opts.userProfile ?? user.userProfile ?? await this.userProfilesRepository.findOneByOrFail({ userId: user.id }))
|
? (opts.userProfile ?? user.userProfile ?? await this.userProfilesRepository.findOneByOrFail({ userId: user.id }))
|
||||||
|
@ -582,6 +550,9 @@ export class UserEntityService implements OnModuleInit {
|
||||||
const checkHost = user.host == null ? this.config.host : user.host;
|
const checkHost = user.host == null ? this.config.host : user.host;
|
||||||
const notificationsInfo = isMe && isDetailed ? await this.getNotificationsInfo(user.id) : null;
|
const notificationsInfo = isMe && isDetailed ? await this.getNotificationsInfo(user.id) : null;
|
||||||
|
|
||||||
|
let fetchPoliciesPromise: Promise<RolePolicies> | null = null;
|
||||||
|
const fetchPolicies = () => fetchPoliciesPromise ??= this.roleService.getUserPolicies(user);
|
||||||
|
|
||||||
const packed = {
|
const packed = {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
name: user.name,
|
name: user.name,
|
||||||
|
@ -607,13 +578,13 @@ export class UserEntityService implements OnModuleInit {
|
||||||
mandatoryCW: user.mandatoryCW,
|
mandatoryCW: user.mandatoryCW,
|
||||||
rejectQuotes: user.rejectQuotes,
|
rejectQuotes: user.rejectQuotes,
|
||||||
attributionDomains: user.attributionDomains,
|
attributionDomains: user.attributionDomains,
|
||||||
isSilenced: user.isSilenced || this.roleService.getUserPolicies(user.id).then(r => !r.canPublicNote),
|
isSilenced: user.isSilenced || fetchPolicies().then(r => !r.canPublicNote),
|
||||||
speakAsCat: user.speakAsCat ?? false,
|
speakAsCat: user.speakAsCat ?? false,
|
||||||
approved: user.approved,
|
approved: user.approved,
|
||||||
requireSigninToViewContents: user.requireSigninToViewContents === false ? undefined : true,
|
requireSigninToViewContents: user.requireSigninToViewContents === false ? undefined : true,
|
||||||
makeNotesFollowersOnlyBefore: user.makeNotesFollowersOnlyBefore ?? undefined,
|
makeNotesFollowersOnlyBefore: user.makeNotesFollowersOnlyBefore ?? undefined,
|
||||||
makeNotesHiddenBefore: user.makeNotesHiddenBefore ?? undefined,
|
makeNotesHiddenBefore: user.makeNotesHiddenBefore ?? undefined,
|
||||||
instance: user.host ? this.federatedInstanceService.fetch(user.host).then(instance => instance ? {
|
instance: user.host ? Promise.resolve(opts.instances?.has(user.host) ? opts.instances.get(user.host) : this.federatedInstanceService.fetch(user.host)).then(instance => instance ? {
|
||||||
name: instance.name,
|
name: instance.name,
|
||||||
softwareName: instance.softwareName,
|
softwareName: instance.softwareName,
|
||||||
softwareVersion: instance.softwareVersion,
|
softwareVersion: instance.softwareVersion,
|
||||||
|
@ -628,7 +599,7 @@ export class UserEntityService implements OnModuleInit {
|
||||||
emojis: this.customEmojiService.populateEmojis(user.emojis, checkHost),
|
emojis: this.customEmojiService.populateEmojis(user.emojis, checkHost),
|
||||||
onlineStatus: this.getOnlineStatus(user),
|
onlineStatus: this.getOnlineStatus(user),
|
||||||
// パフォーマンス上の理由でローカルユーザーのみ
|
// パフォーマンス上の理由でローカルユーザーのみ
|
||||||
badgeRoles: user.host == null ? this.roleService.getUserBadgeRoles(user.id).then((rs) => rs
|
badgeRoles: user.host == null ? this.roleService.getUserBadgeRoles(user).then((rs) => rs
|
||||||
.filter((r) => r.isPublic || iAmModerator)
|
.filter((r) => r.isPublic || iAmModerator)
|
||||||
.sort((a, b) => b.displayOrder - a.displayOrder)
|
.sort((a, b) => b.displayOrder - a.displayOrder)
|
||||||
.map((r) => ({
|
.map((r) => ({
|
||||||
|
@ -641,9 +612,9 @@ export class UserEntityService implements OnModuleInit {
|
||||||
...(isDetailed ? {
|
...(isDetailed ? {
|
||||||
url: profile!.url,
|
url: profile!.url,
|
||||||
uri: user.uri,
|
uri: user.uri,
|
||||||
movedTo: user.movedToUri ? this.apPersonService.resolvePerson(user.movedToUri).then(user => user.id).catch(() => null) : null,
|
movedTo: user.movedToUri ? Promise.resolve(opts.userIdsByUri?.get(user.movedToUri) ?? this.apPersonService.resolvePerson(user.movedToUri).then(user => user.id).catch(() => null)) : null,
|
||||||
alsoKnownAs: user.alsoKnownAs
|
alsoKnownAs: user.alsoKnownAs
|
||||||
? Promise.all(user.alsoKnownAs.map(uri => this.apPersonService.fetchPerson(uri).then(user => user?.id).catch(() => null)))
|
? Promise.all(user.alsoKnownAs.map(uri => Promise.resolve(opts.userIdsByUri?.get(uri) ?? this.apPersonService.fetchPerson(uri).then(user => user?.id).catch(() => null))))
|
||||||
.then(xs => xs.length === 0 ? null : xs.filter(x => x != null))
|
.then(xs => xs.length === 0 ? null : xs.filter(x => x != null))
|
||||||
: null,
|
: null,
|
||||||
updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null,
|
updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null,
|
||||||
|
@ -670,8 +641,8 @@ export class UserEntityService implements OnModuleInit {
|
||||||
followersVisibility: profile!.followersVisibility,
|
followersVisibility: profile!.followersVisibility,
|
||||||
followingVisibility: profile!.followingVisibility,
|
followingVisibility: profile!.followingVisibility,
|
||||||
chatScope: user.chatScope,
|
chatScope: user.chatScope,
|
||||||
canChat: this.roleService.getUserPolicies(user.id).then(r => r.chatAvailability === 'available'),
|
canChat: fetchPolicies().then(r => r.chatAvailability === 'available'),
|
||||||
roles: this.roleService.getUserRoles(user.id).then(roles => roles.filter(role => role.isPublic).sort((a, b) => b.displayOrder - a.displayOrder).map(role => ({
|
roles: this.roleService.getUserRoles(user).then(roles => roles.filter(role => role.isPublic).sort((a, b) => b.displayOrder - a.displayOrder).map(role => ({
|
||||||
id: role.id,
|
id: role.id,
|
||||||
name: role.name,
|
name: role.name,
|
||||||
color: role.color,
|
color: role.color,
|
||||||
|
@ -689,7 +660,7 @@ export class UserEntityService implements OnModuleInit {
|
||||||
twoFactorEnabled: profile!.twoFactorEnabled,
|
twoFactorEnabled: profile!.twoFactorEnabled,
|
||||||
usePasswordLessLogin: profile!.usePasswordLessLogin,
|
usePasswordLessLogin: profile!.usePasswordLessLogin,
|
||||||
securityKeys: profile!.twoFactorEnabled
|
securityKeys: profile!.twoFactorEnabled
|
||||||
? this.userSecurityKeysRepository.countBy({ userId: user.id }).then(result => result >= 1)
|
? Promise.resolve(opts.securityKeyCounts?.get(user.id) ?? this.userSecurityKeysRepository.countBy({ userId: user.id })).then(result => result >= 1)
|
||||||
: false,
|
: false,
|
||||||
} : {}),
|
} : {}),
|
||||||
|
|
||||||
|
@ -722,8 +693,8 @@ export class UserEntityService implements OnModuleInit {
|
||||||
hasUnreadAntenna: this.getHasUnreadAntenna(user.id),
|
hasUnreadAntenna: this.getHasUnreadAntenna(user.id),
|
||||||
hasUnreadChannel: false, // 後方互換性のため
|
hasUnreadChannel: false, // 後方互換性のため
|
||||||
hasUnreadNotification: notificationsInfo?.hasUnread, // 後方互換性のため
|
hasUnreadNotification: notificationsInfo?.hasUnread, // 後方互換性のため
|
||||||
hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id),
|
hasPendingReceivedFollowRequest: opts.pendingReceivedFollows?.has(user.id) ?? this.getHasPendingReceivedFollowRequest(user.id),
|
||||||
hasPendingSentFollowRequest: this.getHasPendingSentFollowRequest(user.id),
|
hasPendingSentFollowRequest: opts.pendingSentFollows?.has(user.id) ?? this.getHasPendingSentFollowRequest(user.id),
|
||||||
unreadNotificationsCount: notificationsInfo?.unreadCount,
|
unreadNotificationsCount: notificationsInfo?.unreadCount,
|
||||||
mutedWords: profile!.mutedWords,
|
mutedWords: profile!.mutedWords,
|
||||||
hardMutedWords: profile!.hardMutedWords,
|
hardMutedWords: profile!.hardMutedWords,
|
||||||
|
@ -733,7 +704,7 @@ export class UserEntityService implements OnModuleInit {
|
||||||
emailNotificationTypes: profile!.emailNotificationTypes,
|
emailNotificationTypes: profile!.emailNotificationTypes,
|
||||||
achievements: profile!.achievements,
|
achievements: profile!.achievements,
|
||||||
loggedInDays: profile!.loggedInDates.length,
|
loggedInDays: profile!.loggedInDates.length,
|
||||||
policies: this.roleService.getUserPolicies(user.id),
|
policies: fetchPolicies(),
|
||||||
defaultCW: profile!.defaultCW,
|
defaultCW: profile!.defaultCW,
|
||||||
defaultCWPriority: profile!.defaultCWPriority,
|
defaultCWPriority: profile!.defaultCWPriority,
|
||||||
allowUnsignedFetch: user.allowUnsignedFetch,
|
allowUnsignedFetch: user.allowUnsignedFetch,
|
||||||
|
@ -783,6 +754,8 @@ export class UserEntityService implements OnModuleInit {
|
||||||
includeSecrets?: boolean,
|
includeSecrets?: boolean,
|
||||||
},
|
},
|
||||||
): Promise<Packed<S>[]> {
|
): Promise<Packed<S>[]> {
|
||||||
|
if (users.length === 0) return [];
|
||||||
|
|
||||||
// -- IDのみの要素を補完して完全なエンティティ一覧を作る
|
// -- IDのみの要素を補完して完全なエンティティ一覧を作る
|
||||||
|
|
||||||
const _users = users.filter((user): user is MiUser => typeof user !== 'string');
|
const _users = users.filter((user): user is MiUser => typeof user !== 'string');
|
||||||
|
@ -800,14 +773,22 @@ export class UserEntityService implements OnModuleInit {
|
||||||
}
|
}
|
||||||
const _userIds = _users.map(u => u.id);
|
const _userIds = _users.map(u => u.id);
|
||||||
|
|
||||||
// -- 実行者の有無や指定スキーマの種別によって要否が異なる値群を取得
|
const iAmModerator = await this.roleService.isModerator(me as MiUser);
|
||||||
|
const meId = me ? me.id : null;
|
||||||
|
const isMe = meId && _userIds.includes(meId);
|
||||||
|
const isDetailed = options && options.schema !== 'UserLite';
|
||||||
|
const isDetailedAndMe = isDetailed && isMe;
|
||||||
|
const isDetailedAndMeOrMod = isDetailed && (isMe || iAmModerator);
|
||||||
|
const isDetailedAndNotMe = isDetailed && !isMe;
|
||||||
|
|
||||||
let profilesMap: Map<MiUser['id'], MiUserProfile> = new Map();
|
const userUris = new Set(_users
|
||||||
let userRelations: Map<MiUser['id'], UserRelation> = new Map();
|
.flatMap(user => [user.uri, user.movedToUri])
|
||||||
let userMemos: Map<MiUser['id'], string | null> = new Map();
|
.filter((uri): uri is string => uri != null));
|
||||||
let pinNotes: Map<MiUser['id'], MiUserNotePining[]> = new Map();
|
|
||||||
|
const userHosts = new Set(_users
|
||||||
|
.map(user => user.host)
|
||||||
|
.filter((host): host is string => host != null));
|
||||||
|
|
||||||
if (options?.schema !== 'UserLite') {
|
|
||||||
const _profiles: MiUserProfile[] = [];
|
const _profiles: MiUserProfile[] = [];
|
||||||
const _profilesToFetch: string[] = [];
|
const _profilesToFetch: string[] = [];
|
||||||
for (const user of _users) {
|
for (const user of _users) {
|
||||||
|
@ -817,20 +798,25 @@ export class UserEntityService implements OnModuleInit {
|
||||||
_profilesToFetch.push(user.id);
|
_profilesToFetch.push(user.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (_profilesToFetch.length > 0) {
|
|
||||||
const fetched = await this.userProfilesRepository.findBy({ userId: In(_profilesToFetch) });
|
// -- 実行者の有無や指定スキーマの種別によって要否が異なる値群を取得
|
||||||
_profiles.push(...fetched);
|
|
||||||
|
const [profilesMap, userMemos, userRelations, pinNotes, userIdsByUri, instances, securityKeyCounts, pendingReceivedFollows, pendingSentFollows] = await Promise.all([
|
||||||
|
// profilesMap
|
||||||
|
this.cacheService.getUserProfiles(_profilesToFetch)
|
||||||
|
.then(profiles => {
|
||||||
|
for (const profile of _profiles) {
|
||||||
|
profiles.set(profile.userId, profile);
|
||||||
}
|
}
|
||||||
profilesMap = new Map(_profiles.map(p => [p.userId, p]));
|
return profiles;
|
||||||
|
}),
|
||||||
const meId = me ? me.id : null;
|
// userMemos
|
||||||
if (meId) {
|
isDetailed && meId ? this.userMemosRepository.findBy({ userId: meId })
|
||||||
userMemos = await this.userMemosRepository.findBy({ userId: meId })
|
.then(memos => new Map(memos.map(memo => [memo.targetUserId, memo.memo]))) : new Map(),
|
||||||
.then(memos => new Map(memos.map(memo => [memo.targetUserId, memo.memo])));
|
// userRelations
|
||||||
|
isDetailedAndNotMe && meId ? this.getRelations(meId, _userIds) : new Map(),
|
||||||
if (_userIds.length > 0) {
|
// pinNotes
|
||||||
userRelations = await this.getRelations(meId, _userIds);
|
isDetailed ? this.userNotePiningsRepository.createQueryBuilder('pin')
|
||||||
pinNotes = await this.userNotePiningsRepository.createQueryBuilder('pin')
|
|
||||||
.where('pin.userId IN (:...userIds)', { userIds: _userIds })
|
.where('pin.userId IN (:...userIds)', { userIds: _userIds })
|
||||||
.innerJoinAndSelect('pin.note', 'note')
|
.innerJoinAndSelect('pin.note', 'note')
|
||||||
.getMany()
|
.getMany()
|
||||||
|
@ -846,10 +832,51 @@ export class UserEntityService implements OnModuleInit {
|
||||||
notes.sort((a, b) => b.id.localeCompare(a.id));
|
notes.sort((a, b) => b.id.localeCompare(a.id));
|
||||||
}
|
}
|
||||||
return map;
|
return map;
|
||||||
});
|
}) : new Map(),
|
||||||
}
|
// userIdsByUrl
|
||||||
}
|
isDetailed ? this.usersRepository.createQueryBuilder('user')
|
||||||
}
|
.select([
|
||||||
|
'user.id',
|
||||||
|
'user.uri',
|
||||||
|
])
|
||||||
|
.where({
|
||||||
|
uri: In(Array.from(userUris)),
|
||||||
|
})
|
||||||
|
.getRawMany<{ user_uri: string, user_id: string }>()
|
||||||
|
.then(users => new Map(users.map(u => [u.user_uri, u.user_id]))) : new Map(),
|
||||||
|
// instances
|
||||||
|
Promise.all(Array.from(userHosts).map(async host => [host, await this.federatedInstanceService.fetch(host)] as const))
|
||||||
|
.then(hosts => new Map(hosts)),
|
||||||
|
// securityKeyCounts
|
||||||
|
isDetailedAndMeOrMod ? this.userSecurityKeysRepository.createQueryBuilder('key')
|
||||||
|
.select('key.userId', 'userId')
|
||||||
|
.addSelect('count(key.id)', 'userCount')
|
||||||
|
.where({
|
||||||
|
userId: In(_userIds),
|
||||||
|
})
|
||||||
|
.groupBy('key.userId')
|
||||||
|
.getRawMany<{ userId: string, userCount: number }>()
|
||||||
|
.then(counts => new Map(counts.map(c => [c.userId, c.userCount]))) : new Map(),
|
||||||
|
// TODO check query performance
|
||||||
|
// pendingReceivedFollows
|
||||||
|
isDetailedAndMe ? this.followRequestsRepository.createQueryBuilder('req')
|
||||||
|
.select('req.followeeId', 'followeeId')
|
||||||
|
.where({
|
||||||
|
followeeId: In(_userIds),
|
||||||
|
})
|
||||||
|
.groupBy('req.followeeId')
|
||||||
|
.getRawMany<{ followeeId: string }>()
|
||||||
|
.then(reqs => new Set(reqs.map(r => r.followeeId))) : new Set<string>(),
|
||||||
|
// pendingSentFollows
|
||||||
|
isDetailedAndMe ? this.followRequestsRepository.createQueryBuilder('req')
|
||||||
|
.select('req.followerId', 'followerId')
|
||||||
|
.where({
|
||||||
|
followerId: In(_userIds),
|
||||||
|
})
|
||||||
|
.groupBy('req.followerId')
|
||||||
|
.getRawMany<{ followerId: string }>()
|
||||||
|
.then(reqs => new Set(reqs.map(r => r.followerId))) : new Set<string>(),
|
||||||
|
]);
|
||||||
|
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
_users.map(u => this.pack(
|
_users.map(u => this.pack(
|
||||||
|
@ -861,6 +888,12 @@ export class UserEntityService implements OnModuleInit {
|
||||||
userRelations: userRelations,
|
userRelations: userRelations,
|
||||||
userMemos: userMemos,
|
userMemos: userMemos,
|
||||||
pinNotes: pinNotes,
|
pinNotes: pinNotes,
|
||||||
|
iAmModerator,
|
||||||
|
userIdsByUri,
|
||||||
|
instances,
|
||||||
|
securityKeyCounts,
|
||||||
|
pendingReceivedFollows,
|
||||||
|
pendingSentFollows,
|
||||||
},
|
},
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
|
|
|
@ -348,7 +348,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
throw new ApiError(meta.errors.noSuchReplyTarget);
|
throw new ApiError(meta.errors.noSuchReplyTarget);
|
||||||
} else if (isRenote(reply) && !isQuote(reply)) {
|
} else if (isRenote(reply) && !isQuote(reply)) {
|
||||||
throw new ApiError(meta.errors.cannotReplyToPureRenote);
|
throw new ApiError(meta.errors.cannotReplyToPureRenote);
|
||||||
} else if (!await this.noteEntityService.isVisibleForMe(reply, me.id)) {
|
} else if (!await this.noteEntityService.isVisibleForMe(reply, me.id, { me })) {
|
||||||
throw new ApiError(meta.errors.cannotReplyToInvisibleNote);
|
throw new ApiError(meta.errors.cannotReplyToInvisibleNote);
|
||||||
} else if (reply.visibility === 'specified' && ps.visibility !== 'specified') {
|
} else if (reply.visibility === 'specified' && ps.visibility !== 'specified') {
|
||||||
throw new ApiError(meta.errors.cannotReplyToSpecifiedVisibilityNoteWithExtendedVisibility);
|
throw new ApiError(meta.errors.cannotReplyToSpecifiedVisibilityNoteWithExtendedVisibility);
|
||||||
|
|
|
@ -402,7 +402,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
throw new ApiError(meta.errors.noSuchReplyTarget);
|
throw new ApiError(meta.errors.noSuchReplyTarget);
|
||||||
} else if (isRenote(reply) && !isQuote(reply)) {
|
} else if (isRenote(reply) && !isQuote(reply)) {
|
||||||
throw new ApiError(meta.errors.cannotReplyToPureRenote);
|
throw new ApiError(meta.errors.cannotReplyToPureRenote);
|
||||||
} else if (!await this.noteEntityService.isVisibleForMe(reply, me.id)) {
|
} else if (!await this.noteEntityService.isVisibleForMe(reply, me.id, { me })) {
|
||||||
throw new ApiError(meta.errors.cannotReplyToInvisibleNote);
|
throw new ApiError(meta.errors.cannotReplyToInvisibleNote);
|
||||||
} else if (reply.visibility === 'specified' && ps.visibility !== 'specified') {
|
} else if (reply.visibility === 'specified' && ps.visibility !== 'specified') {
|
||||||
throw new ApiError(meta.errors.cannotReplyToSpecifiedVisibilityNoteWithExtendedVisibility);
|
throw new ApiError(meta.errors.cannotReplyToSpecifiedVisibilityNoteWithExtendedVisibility);
|
||||||
|
|
|
@ -91,7 +91,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!(await this.noteEntityService.isVisibleForMe(note, me?.id ?? null))) {
|
if (!(await this.noteEntityService.isVisibleForMe(note, me?.id ?? null, { me }))) {
|
||||||
throw new ApiError(meta.errors.cannotTranslateInvisibleNote);
|
throw new ApiError(meta.errors.cannotTranslateInvisibleNote);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue