fix(backend): RBTの修正 (#14621)

* fix(backend): 絵文字の変換処理が不十分なのを修正

* enhance: リアクションバッファリングが無効になったら即bakeするように

* attempt to fix test

* fix
This commit is contained in:
かっこかり 2024-09-24 18:29:02 +09:00 committed by GitHub
parent 1d8bfe4f1c
commit 6a1a2bef43
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 73 additions and 40 deletions

View file

@ -126,8 +126,8 @@ const $meta: Provider = {
const { type, body } = obj.message as GlobalEvents['internal']['payload']; const { type, body } = obj.message as GlobalEvents['internal']['payload'];
switch (type) { switch (type) {
case 'metaUpdated': { case 'metaUpdated': {
for (const key in body) { for (const key in body.after) {
(meta as any)[key] = (body as any)[key]; (meta as any)[key] = (body.after as any)[key];
} }
meta.proxyAccount = null; // joinなカラムは通常取ってこないので meta.proxyAccount = null; // joinなカラムは通常取ってこないので
break; break;

View file

@ -241,7 +241,7 @@ export interface InternalEventTypes {
avatarDecorationCreated: MiAvatarDecoration; avatarDecorationCreated: MiAvatarDecoration;
avatarDecorationDeleted: MiAvatarDecoration; avatarDecorationDeleted: MiAvatarDecoration;
avatarDecorationUpdated: MiAvatarDecoration; avatarDecorationUpdated: MiAvatarDecoration;
metaUpdated: MiMeta; metaUpdated: { before?: MiMeta; after: MiMeta; };
followChannel: { userId: MiUser['id']; channelId: MiChannel['id']; }; followChannel: { userId: MiUser['id']; channelId: MiChannel['id']; };
unfollowChannel: { userId: MiUser['id']; channelId: MiChannel['id']; }; unfollowChannel: { userId: MiUser['id']; channelId: MiChannel['id']; };
updateUserProfile: MiUserProfile; updateUserProfile: MiUserProfile;

View file

@ -52,7 +52,7 @@ export class MetaService implements OnApplicationShutdown {
switch (type) { switch (type) {
case 'metaUpdated': { case 'metaUpdated': {
this.cache = { // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい this.cache = { // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
...body, ...(body.after),
proxyAccount: null, // joinなカラムは通常取ってこないので proxyAccount: null, // joinなカラムは通常取ってこないので
}; };
break; break;
@ -141,7 +141,7 @@ export class MetaService implements OnApplicationShutdown {
}); });
} }
this.globalEventService.publishInternalEvent('metaUpdated', updated); this.globalEventService.publishInternalEvent('metaUpdated', { before, after: updated });
return updated; return updated;
} }

View file

@ -337,10 +337,22 @@ export class ReactionService {
//#endregion //#endregion
} }
/**
* -
* - `@.` `decodeReaction()`
*/
@bindThis
public convertLegacyReaction(reaction: string): string {
reaction = this.decodeReaction(reaction).reaction;
if (Object.keys(legacies).includes(reaction)) return legacies[reaction];
return reaction;
}
// TODO: 廃止 // TODO: 廃止
/** /**
* * -
* 0 * - `@.` `decodeReaction()`
* - 0
*/ */
@bindThis @bindThis
public convertLegacyReactions(reactions: MiNote['reactions']): MiNote['reactions'] { public convertLegacyReactions(reactions: MiNote['reactions']): MiNote['reactions'] {
@ -353,10 +365,7 @@ export class ReactionService {
return count > 0; return count > 0;
}) })
.map(([reaction, count]) => { .map(([reaction, count]) => {
// unchecked indexed access const key = this.convertLegacyReaction(reaction);
const convertedReaction = legacies[reaction] as string | undefined;
const key = this.decodeReaction(convertedReaction ?? reaction).reaction;
return [key, count] as const; return [key, count] as const;
}) })
@ -411,11 +420,4 @@ export class ReactionService {
host: undefined, host: undefined,
}; };
} }
@bindThis
public convertLegacyReaction(reaction: string): string {
reaction = this.decodeReaction(reaction).reaction;
if (Object.keys(legacies).includes(reaction)) return legacies[reaction];
return reaction;
}
} }

View file

@ -11,22 +11,48 @@ import { bindThis } from '@/decorators.js';
import type { MiUser, NotesRepository } from '@/models/_.js'; import type { MiUser, NotesRepository } from '@/models/_.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import { PER_NOTE_REACTION_USER_PAIR_CACHE_MAX } from '@/const.js'; import { PER_NOTE_REACTION_USER_PAIR_CACHE_MAX } from '@/const.js';
import type { GlobalEvents } from '@/core/GlobalEventService.js';
import type { OnApplicationShutdown } from '@nestjs/common';
const REDIS_DELTA_PREFIX = 'reactionsBufferDeltas'; const REDIS_DELTA_PREFIX = 'reactionsBufferDeltas';
const REDIS_PAIR_PREFIX = 'reactionsBufferPairs'; const REDIS_PAIR_PREFIX = 'reactionsBufferPairs';
@Injectable() @Injectable()
export class ReactionsBufferingService { export class ReactionsBufferingService implements OnApplicationShutdown {
constructor( constructor(
@Inject(DI.config) @Inject(DI.config)
private config: Config, private config: Config,
@Inject(DI.redisForSub)
private redisForSub: Redis.Redis,
@Inject(DI.redisForReactions) @Inject(DI.redisForReactions)
private redisForReactions: Redis.Redis, // TODO: 専用のRedisインスタンスにする private redisForReactions: Redis.Redis, // TODO: 専用のRedisインスタンスにする
@Inject(DI.notesRepository) @Inject(DI.notesRepository)
private notesRepository: NotesRepository, private notesRepository: NotesRepository,
) { ) {
this.redisForSub.on('message', this.onMessage);
}
@bindThis
private async onMessage(_: string, data: string) {
const obj = JSON.parse(data);
if (obj.channel === 'internal') {
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
switch (type) {
case 'metaUpdated': {
// リアクションバッファリングが有効→無効になったら即bake
if (body.before != null && body.before.enableReactionsBuffering && !body.after.enableReactionsBuffering) {
this.bake();
}
break;
}
default:
break;
}
}
} }
@bindThis @bindThis
@ -159,4 +185,27 @@ export class ReactionsBufferingService {
.execute(); .execute();
} }
} }
@bindThis
public mergeReactions(src: MiNote['reactions'], delta: Record<string, number>): MiNote['reactions'] {
const reactions = { ...src };
for (const [name, count] of Object.entries(delta)) {
if (reactions[name] != null) {
reactions[name] += count;
} else {
reactions[name] = count;
}
}
return reactions;
}
@bindThis
public dispose(): void {
this.redisForSub.off('message', this.onMessage);
}
@bindThis
public onApplicationShutdown(signal?: string | undefined): void {
this.dispose();
}
} }

View file

@ -16,25 +16,12 @@ 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';
import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js'; import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js';
import { MetaService } from '@/core/MetaService.js';
import type { OnModuleInit } from '@nestjs/common'; import type { OnModuleInit } from '@nestjs/common';
import type { CustomEmojiService } from '../CustomEmojiService.js'; import type { CustomEmojiService } from '../CustomEmojiService.js';
import type { ReactionService } from '../ReactionService.js'; import type { ReactionService } from '../ReactionService.js';
import type { UserEntityService } from './UserEntityService.js'; import type { UserEntityService } from './UserEntityService.js';
import type { DriveFileEntityService } from './DriveFileEntityService.js'; import type { DriveFileEntityService } from './DriveFileEntityService.js';
function mergeReactions(src: Record<string, number>, delta: Record<string, number>) {
const reactions = { ...src };
for (const [name, count] of Object.entries(delta)) {
if (reactions[name] != null) {
reactions[name] += count;
} else {
reactions[name] = count;
}
}
return reactions;
}
@Injectable() @Injectable()
export class NoteEntityService implements OnModuleInit { export class NoteEntityService implements OnModuleInit {
private userEntityService: UserEntityService; private userEntityService: UserEntityService;
@ -329,12 +316,7 @@ export class NoteEntityService implements OnModuleInit {
: this.meta.enableReactionsBuffering : this.meta.enableReactionsBuffering
? await this.reactionsBufferingService.get(note.id) ? await this.reactionsBufferingService.get(note.id)
: { deltas: {}, pairs: [] }; : { deltas: {}, pairs: [] };
const reactions = mergeReactions(this.reactionService.convertLegacyReactions(note.reactions), bufferedReactions.deltas ?? {}); const reactions = this.reactionService.convertLegacyReactions(this.reactionsBufferingService.mergeReactions(note.reactions, bufferedReactions.deltas ?? {}));
for (const [name, count] of Object.entries(reactions)) {
if (count <= 0) {
delete reactions[name];
}
}
const reactionAndUserPairCache = note.reactionAndUserPairCache.concat(bufferedReactions.pairs.map(x => x.join('/'))); const reactionAndUserPairCache = note.reactionAndUserPairCache.concat(bufferedReactions.pairs.map(x => x.join('/')));
@ -451,7 +433,7 @@ export class NoteEntityService implements OnModuleInit {
for (const note of notes) { for (const note of notes) {
if (note.renote && (note.text == null && note.fileIds.length === 0)) { // pure renote if (note.renote && (note.text == null && note.fileIds.length === 0)) { // pure renote
const reactionsCount = Object.values(mergeReactions(note.renote.reactions, bufferedReactions?.get(note.renote.id)?.deltas ?? {})).reduce((a, b) => a + b, 0); 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) { if (reactionsCount === 0) {
myReactionsMap.set(note.renote.id, null); myReactionsMap.set(note.renote.id, null);
} else if (reactionsCount <= note.renote.reactionAndUserPairCache.length + (bufferedReactions?.get(note.renote.id)?.pairs.length ?? 0)) { } else if (reactionsCount <= note.renote.reactionAndUserPairCache.length + (bufferedReactions?.get(note.renote.id)?.pairs.length ?? 0)) {
@ -467,7 +449,7 @@ export class NoteEntityService implements OnModuleInit {
} }
} else { } else {
if (note.id < oldId) { if (note.id < oldId) {
const reactionsCount = Object.values(mergeReactions(note.reactions, bufferedReactions?.get(note.id)?.deltas ?? {})).reduce((a, b) => a + b, 0); const reactionsCount = Object.values(this.reactionsBufferingService.mergeReactions(note.reactions, bufferedReactions?.get(note.id)?.deltas ?? {})).reduce((a, b) => a + b, 0);
if (reactionsCount === 0) { if (reactionsCount === 0) {
myReactionsMap.set(note.id, null); myReactionsMap.set(note.id, null);
} else if (reactionsCount <= note.reactionAndUserPairCache.length + (bufferedReactions?.get(note.id)?.pairs.length ?? 0)) { } else if (reactionsCount <= note.reactionAndUserPairCache.length + (bufferedReactions?.get(note.id)?.pairs.length ?? 0)) {