Merge remote-tracking branch 'upstream/develop' into nodeinfostats

This commit is contained in:
Kinetix 2025-01-26 13:20:26 -08:00
commit 326bc0a24f
27 changed files with 179 additions and 148 deletions

View file

@ -528,44 +528,44 @@ export class NoteEntityService implements OnModuleInit {
// パフォーマンスのためートが作成されてから2秒以上経っていない場合はリアクションを取得しない
const oldId = this.idService.gen(Date.now() - 2000);
const targetNotes: MiNote[] = [];
for (const note of notes) {
if (isPureRenote(note)) {
const reactionsCount = Object.values(this.reactionsBufferingService.mergeReactions(note.renote.reactions, bufferedReactions?.get(note.renote.id)?.deltas ?? {})).reduce((a, b) => a + b, 0);
if (reactionsCount === 0) {
myReactionsMap.set(note.renote.id, null);
} else if (reactionsCount <= note.renote.reactionAndUserPairCache.length + (bufferedReactions?.get(note.renote.id)?.pairs.length ?? 0)) {
const pairInBuffer = bufferedReactions?.get(note.renote.id)?.pairs.find(p => p[0] === meId);
if (pairInBuffer) {
myReactionsMap.set(note.renote.id, pairInBuffer[1]);
} else {
const pair = note.renote.reactionAndUserPairCache.find(p => p.startsWith(meId));
myReactionsMap.set(note.renote.id, pair ? pair.split('/')[1] : null);
}
} else {
idsNeedFetchMyReaction.add(note.renote.id);
// we may need to fetch 'my reaction' for renote target.
targetNotes.push(note.renote);
if (note.renote.reply) {
// idem if the renote is also a reply.
targetNotes.push(note.renote.reply);
}
} else if (note.reply) {
// idem for OP of a regular reply.
targetNotes.push(note.reply);
} else {
if (note.id < oldId) {
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);
}
targetNotes.push(note);
} else {
myReactionsMap.set(note.id, null);
}
}
}
for (const note of targetNotes) {
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)),

View file

@ -17,6 +17,7 @@ import { bindThis } from '@/decorators.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
import type * as Bull from 'bullmq';
import type { DbUserImportJobData } from '../types.js';
import type { Config } from '@/config.js';
// TODO: 名前衝突時の動作を選べるようにする
@Injectable()
@ -24,6 +25,9 @@ export class ImportCustomEmojisProcessorService {
private logger: Logger;
constructor(
@Inject(DI.config)
private config: Config,
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
@ -57,7 +61,7 @@ export class ImportCustomEmojisProcessorService {
try {
fs.writeFileSync(destPath, '', 'binary');
await this.downloadService.downloadUrl(file.url, destPath);
await this.downloadService.downloadUrl(file.url, destPath, { operationTimeout: this.config.import?.downloadTimeout, maxSize: this.config.import?.maxFileSize });
} catch (e) { // TODO: 何度か再試行
if (e instanceof Error || typeof e === 'string') {
this.logger.error(e);

View file

@ -626,7 +626,7 @@ export class ImportNotesProcessorService {
if (!exists) {
try {
await this.downloadService.downloadUrl(videos[0].url, filePath);
await this.downloadUrl(videos[0].url, filePath);
} catch (e) { // TODO: 何度か再試行
this.logger.error(e instanceof Error ? e : new Error(e as string));
}
@ -651,7 +651,7 @@ export class ImportNotesProcessorService {
if (!exists) {
try {
await this.downloadService.downloadUrl(file.media_url_https, filePath);
await this.downloadUrl(file.media_url_https, filePath);
} catch (e) { // TODO: 何度か再試行
this.logger.error(e instanceof Error ? e : new Error(e as string));
}

View file

@ -68,6 +68,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
for (let i = 0; i < ps.count; i++) {
ticketsPromises.push(this.registrationTicketsRepository.insertOne({
id: this.idService.gen(),
createdBy: me,
createdById: me.id,
expiresAt: ps.expiresAt ? new Date(ps.expiresAt) : null,
code: generateInviteCode(),
}));

View file

@ -3,7 +3,6 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { IsNull } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
import type { EmojisRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
@ -59,7 +58,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const emojis = await this.emojisRepository.createQueryBuilder()
.where('host IS NULL')
.orderBy('LOWER(category)', 'ASC')
.orderBy('LOWER(name)', 'ASC')
.addOrderBy('LOWER(name)', 'ASC')
.getMany();
return {
emojis: await this.emojiEntityService.packSimpleMany(emojis),

View file

@ -103,11 +103,42 @@ export default abstract class Channel {
public onMessage?(type: string, body: JsonValue): void;
public async assignMyReaction(note: Packed<'Note'>, noteEntityService: NoteEntityService) {
if (this.user && Object.keys(note.reactions).length > 0) {
const myReaction = await noteEntityService.populateMyReaction(note, this.user.id);
note.myReaction = myReaction;
public async assignMyReaction(note: Packed<'Note'>, noteEntityService: NoteEntityService): Promise<Packed<'Note'>> {
let changed = false;
// StreamingApiServerService creates a single EventEmitter per server process,
// so a new note arriving from redis gets de-serialised once per server process,
// and then that single object is passed to all active channels on each connection.
// If we didn't clone the notes here, different connections would asynchronously write
// different values to the same object, resulting in a random value being sent to each frontend. -- Dakkar
const clonedNote = { ...note };
if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
const myReaction = await noteEntityService.populateMyReaction(note.renote, this.user.id);
if (myReaction) {
changed = true;
clonedNote.renote = { ...note.renote };
clonedNote.renote.myReaction = myReaction;
}
}
if (note.renote?.reply && Object.keys(note.renote.reply.reactions).length > 0) {
const myReaction = await noteEntityService.populateMyReaction(note.renote.reply, this.user.id);
if (myReaction) {
changed = true;
clonedNote.renote = { ...note.renote };
clonedNote.renote.reply = { ...note.renote.reply };
clonedNote.renote.reply.myReaction = myReaction;
}
}
}
if (this.user && note.reply && Object.keys(note.reply.reactions).length > 0) {
const myReaction = await noteEntityService.populateMyReaction(note.reply, this.user.id);
if (myReaction) {
changed = true;
clonedNote.reply = { ...note.reply };
clonedNote.reply.myReaction = myReaction;
}
}
return changed ? clonedNote : note;
}
}

View file

@ -65,24 +65,11 @@ class BubbleTimelineChannel extends Channel {
if (this.isNoteMutedOrBlocked(note)) return;
const reactionsToFetch = [];
if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
if (note.renote) {
reactionsToFetch.push(this.assignMyReaction(note.renote, this.noteEntityService));
if (note.renote.reply) {
reactionsToFetch.push(this.assignMyReaction(note.renote.reply, this.noteEntityService));
}
}
}
if (this.user && note.reply) {
reactionsToFetch.push(this.assignMyReaction(note.reply, this.noteEntityService));
}
const clonedNote = await this.assignMyReaction(note, this.noteEntityService);
await Promise.all(reactionsToFetch);
this.connection.cacheNote(clonedNote);
this.connection.cacheNote(note);
this.send('note', note);
this.send('note', clonedNote);
}
@bindThis

View file

@ -60,24 +60,11 @@ class GlobalTimelineChannel extends Channel {
if (this.isNoteMutedOrBlocked(note)) return;
const reactionsToFetch = [];
if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
if (note.renote) {
reactionsToFetch.push(this.assignMyReaction(note.renote, this.noteEntityService));
if (note.renote.reply) {
reactionsToFetch.push(this.assignMyReaction(note.renote.reply, this.noteEntityService));
}
}
}
if (this.user && note.reply) {
reactionsToFetch.push(this.assignMyReaction(note.reply, this.noteEntityService));
}
const clonedNote = await this.assignMyReaction(note, this.noteEntityService);
await Promise.all(reactionsToFetch);
this.connection.cacheNote(clonedNote);
this.connection.cacheNote(note);
this.send('note', note);
this.send('note', clonedNote);
}
@bindThis

View file

@ -81,24 +81,11 @@ class HomeTimelineChannel extends Channel {
if (this.isNoteMutedOrBlocked(note)) return;
const reactionsToFetch = [];
if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
if (note.renote) {
reactionsToFetch.push(this.assignMyReaction(note.renote, this.noteEntityService));
if (note.renote.reply) {
reactionsToFetch.push(this.assignMyReaction(note.renote.reply, this.noteEntityService));
}
}
}
if (this.user && note.reply) {
reactionsToFetch.push(this.assignMyReaction(note.reply, this.noteEntityService));
}
const clonedNote = await this.assignMyReaction(note, this.noteEntityService);
await Promise.all(reactionsToFetch);
this.connection.cacheNote(clonedNote);
this.connection.cacheNote(note);
this.send('note', note);
this.send('note', clonedNote);
}
@bindThis

View file

@ -98,24 +98,11 @@ class HybridTimelineChannel extends Channel {
}
}
const reactionsToFetch = [];
if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
if (note.renote) {
reactionsToFetch.push(this.assignMyReaction(note.renote, this.noteEntityService));
if (note.renote.reply) {
reactionsToFetch.push(this.assignMyReaction(note.renote.reply, this.noteEntityService));
}
}
}
if (this.user && note.reply) {
reactionsToFetch.push(this.assignMyReaction(note.reply, this.noteEntityService));
}
const clonedNote = await this.assignMyReaction(note, this.noteEntityService);
await Promise.all(reactionsToFetch);
this.connection.cacheNote(clonedNote);
this.connection.cacheNote(note);
this.send('note', note);
this.send('note', clonedNote);
}
@bindThis

View file

@ -70,24 +70,11 @@ class LocalTimelineChannel extends Channel {
if (this.isNoteMutedOrBlocked(note)) return;
const reactionsToFetch = [];
if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
if (note.renote) {
reactionsToFetch.push(this.assignMyReaction(note.renote, this.noteEntityService));
if (note.renote.reply) {
reactionsToFetch.push(this.assignMyReaction(note.renote.reply, this.noteEntityService));
}
}
}
if (this.user && note.reply) {
reactionsToFetch.push(this.assignMyReaction(note.reply, this.noteEntityService));
}
const clonedNote = await this.assignMyReaction(note, this.noteEntityService);
await Promise.all(reactionsToFetch);
this.connection.cacheNote(clonedNote);
this.connection.cacheNote(note);
this.send('note', note);
this.send('note', clonedNote);
}
@bindThis