refactor: remove note re-packing in streaming API

Instead of packing the note for public user before passing it to
streams, the note is now either packed for the user the respective
stream belongs to (`mainStream`) or not packed at all and then packed
later (`notesStream`).

Because this is a new common task between different channels, a shared
implementation of packing a note from notesStream is created. This
implementation will simply skip a note if it is not visible to the user
that the channel belongs to.
This commit is contained in:
Johann150 2022-05-24 10:00:46 +02:00
parent c6192ac95a
commit cfa371b52b
Signed by untrusted user: Johann150
GPG key ID: 9EE6577A2A06F8F1
13 changed files with 72 additions and 169 deletions

View file

@ -1,4 +1,8 @@
import Connection from '.';
import { Note } from '@/models/entities/note.js';
import { Notes } from '@/models/index.js';
import { Packed } from '@/misc/schema.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
/**
* Stream channel
@ -54,6 +58,32 @@ export default abstract class Channel {
});
}
protected withPackedNote(callback: (note: Packed<'Note'>) => void): (Note) => void {
return async (note: Note) => {
try {
// because `note` was previously JSON.stringify'ed, the fields that
// were objects before are now strings and have to be restored or
// removed from the object
note.createdAt = new Date(note.createdAt);
delete note.reply;
delete note.renote;
delete note.user;
delete note.channel;
const packed = await Notes.pack(note, this.user, { detail: true });
callback(packed);
} catch (err) {
if (err instanceof IdentifiableError && err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') {
// skip: note not visible to user
return;
} else {
throw err;
}
}
};
}
public abstract init(params: any): void;
public dispose?(): void;
public onMessage?(type: string, body: any): void;

View file

@ -2,6 +2,7 @@ import Channel from '../channel.js';
import { Notes } from '@/models/index.js';
import { isUserRelated } from '@/misc/is-user-related.js';
import { StreamMessages } from '../types.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
export default class extends Channel {
public readonly chName = 'antenna';
@ -23,6 +24,7 @@ export default class extends Channel {
private async onEvent(data: StreamMessages['antenna']['payload']) {
if (data.type === 'note') {
try {
const note = await Notes.pack(data.body.id, this.user, { detail: true });
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
@ -33,6 +35,14 @@ export default class extends Channel {
this.connection.cacheNote(note);
this.send('note', note);
} catch (e) {
if (e instanceof IdentifiableError && e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') {
// skip: note not visible to user
return;
} else {
throw e;
}
}
} else {
this.send(data.type, data.body);
}

View file

@ -1,5 +1,5 @@
import Channel from '../channel.js';
import { Notes, Users } from '@/models/index.js';
import { Users } from '@/models/index.js';
import { isUserRelated } from '@/misc/is-user-related.js';
import { User } from '@/models/entities/user.js';
import { StreamMessages } from '../types.js';
@ -15,7 +15,7 @@ export default class extends Channel {
constructor(id: string, connection: Channel['connection']) {
super(id, connection);
this.onNote = this.onNote.bind(this);
this.onNote = this.withPackedNote(this.onNote.bind(this));
}
public async init(params: any) {
@ -30,19 +30,6 @@ export default class extends Channel {
private async onNote(note: Packed<'Note'>) {
if (note.channelId !== this.channelId) return;
// リプライなら再pack
if (note.replyId != null) {
note.reply = await Notes.pack(note.replyId, this.user, {
detail: true,
});
}
// Renoteなら再pack
if (note.renoteId != null) {
note.renote = await Notes.pack(note.renoteId, this.user, {
detail: true,
});
}
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (isUserRelated(note, this.muting)) return;
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する

View file

@ -1,6 +1,5 @@
import Channel from '../channel.js';
import { fetchMeta } from '@/misc/fetch-meta.js';
import { Notes } from '@/models/index.js';
import { checkWordMute } from '@/misc/check-word-mute.js';
import { isInstanceMuted } from '@/misc/is-instance-muted.js';
import { isUserRelated } from '@/misc/is-user-related.js';
@ -13,7 +12,7 @@ export default class extends Channel {
constructor(id: string, connection: Channel['connection']) {
super(id, connection);
this.onNote = this.onNote.bind(this);
this.onNote = this.withPackedNote(this.onNote.bind(this));
}
public async init(params: any) {
@ -30,19 +29,6 @@ export default class extends Channel {
if (note.visibility !== 'public') return;
if (note.channelId != null) return;
// リプライなら再pack
if (note.replyId != null) {
note.reply = await Notes.pack(note.replyId, this.user, {
detail: true,
});
}
// Renoteなら再pack
if (note.renoteId != null) {
note.renote = await Notes.pack(note.renoteId, this.user, {
detail: true,
});
}
// 関係ない返信は除外
if (note.reply && !this.user!.showTimelineReplies) {
const reply = note.reply;

View file

@ -1,5 +1,4 @@
import Channel from '../channel.js';
import { Notes } from '@/models/index.js';
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
import { isUserRelated } from '@/misc/is-user-related.js';
import { Packed } from '@/misc/schema.js';
@ -12,7 +11,7 @@ export default class extends Channel {
constructor(id: string, connection: Channel['connection']) {
super(id, connection);
this.onNote = this.onNote.bind(this);
this.onNote = this.withPackedNote(this.onNote.bind(this));
}
public async init(params: any) {
@ -29,13 +28,6 @@ export default class extends Channel {
const matched = this.q.some(tags => tags.every(tag => noteTags.includes(normalizeForSearch(tag))));
if (!matched) return;
// Renoteなら再pack
if (note.renoteId != null) {
note.renote = await Notes.pack(note.renoteId, this.user, {
detail: true,
});
}
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (isUserRelated(note, this.muting)) return;
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する

View file

@ -1,5 +1,4 @@
import Channel from '../channel.js';
import { Notes } from '@/models/index.js';
import { checkWordMute } from '@/misc/check-word-mute.js';
import { isUserRelated } from '@/misc/is-user-related.js';
import { isInstanceMuted } from '@/misc/is-instance-muted.js';
@ -12,7 +11,7 @@ export default class extends Channel {
constructor(id: string, connection: Channel['connection']) {
super(id, connection);
this.onNote = this.onNote.bind(this);
this.onNote = this.withPackedNote(this.onNote.bind(this));
}
public async init(params: any) {
@ -31,29 +30,6 @@ export default class extends Channel {
// Ignore notes from instances the user has muted
if (isInstanceMuted(note, new Set<string>(this.userProfile?.mutedInstances ?? []))) return;
if (['followers', 'specified'].includes(note.visibility)) {
note = await Notes.pack(note.id, this.user!, {
detail: true,
});
if (note.isHidden) {
return;
}
} else {
// リプライなら再pack
if (note.replyId != null) {
note.reply = await Notes.pack(note.replyId, this.user!, {
detail: true,
});
}
// Renoteなら再pack
if (note.renoteId != null) {
note.renote = await Notes.pack(note.renoteId, this.user!, {
detail: true,
});
}
}
// 関係ない返信は除外
if (note.reply && !this.user!.showTimelineReplies) {
const reply = note.reply;

View file

@ -1,6 +1,5 @@
import Channel from '../channel.js';
import { fetchMeta } from '@/misc/fetch-meta.js';
import { Notes } from '@/models/index.js';
import { checkWordMute } from '@/misc/check-word-mute.js';
import { isUserRelated } from '@/misc/is-user-related.js';
import { isInstanceMuted } from '@/misc/is-instance-muted.js';
@ -13,7 +12,7 @@ export default class extends Channel {
constructor(id: string, connection: Channel['connection']) {
super(id, connection);
this.onNote = this.onNote.bind(this);
this.onNote = this.withPackedNote(this.onNote.bind(this));
}
public async init(params: any) {
@ -36,29 +35,6 @@ export default class extends Channel {
(note.channelId != null && this.followingChannels.has(note.channelId))
)) return;
if (['followers', 'specified'].includes(note.visibility)) {
note = await Notes.pack(note.id, this.user!, {
detail: true,
});
if (note.isHidden) {
return;
}
} else {
// リプライなら再pack
if (note.replyId != null) {
note.reply = await Notes.pack(note.replyId, this.user!, {
detail: true,
});
}
// Renoteなら再pack
if (note.renoteId != null) {
note.renote = await Notes.pack(note.renoteId, this.user!, {
detail: true,
});
}
}
// Ignore notes from instances the user has muted
if (isInstanceMuted(note, new Set<string>(this.userProfile?.mutedInstances ?? []))) return;

View file

@ -1,6 +1,5 @@
import Channel from '../channel.js';
import { fetchMeta } from '@/misc/fetch-meta.js';
import { Notes } from '@/models/index.js';
import { checkWordMute } from '@/misc/check-word-mute.js';
import { isUserRelated } from '@/misc/is-user-related.js';
import { Packed } from '@/misc/schema.js';
@ -12,7 +11,7 @@ export default class extends Channel {
constructor(id: string, connection: Channel['connection']) {
super(id, connection);
this.onNote = this.onNote.bind(this);
this.onNote = this.withPackedNote(this.onNote.bind(this));
}
public async init(params: any) {
@ -30,19 +29,6 @@ export default class extends Channel {
if (note.visibility !== 'public') return;
if (note.channelId != null && !this.followingChannels.has(note.channelId)) return;
// リプライなら再pack
if (note.replyId != null) {
note.reply = await Notes.pack(note.replyId, this.user, {
detail: true,
});
}
// Renoteなら再pack
if (note.renoteId != null) {
note.renote = await Notes.pack(note.renoteId, this.user, {
detail: true,
});
}
// 関係ない返信は除外
if (note.reply && !this.user!.showTimelineReplies) {
const reply = note.reply;

View file

@ -1,5 +1,4 @@
import Channel from '../channel.js';
import { Notes } from '@/models/index.js';
import { isInstanceMuted, isUserFromMutedInstance } from '@/misc/is-instance-muted.js';
export default class extends Channel {
@ -16,26 +15,12 @@ export default class extends Channel {
if (isUserFromMutedInstance(data.body, new Set<string>(this.userProfile?.mutedInstances ?? []))) return;
if (data.body.userId && this.muting.has(data.body.userId)) return;
if (data.body.note && data.body.note.isHidden) {
const note = await Notes.pack(data.body.note.id, this.user, {
detail: true,
});
this.connection.cacheNote(note);
data.body.note = note;
}
break;
}
case 'mention': {
if (isInstanceMuted(data.body, new Set<string>(this.userProfile?.mutedInstances ?? []))) return;
if (this.muting.has(data.body.userId)) return;
if (data.body.isHidden) {
const note = await Notes.pack(data.body.id, this.user, {
detail: true,
});
this.connection.cacheNote(note);
data.body = note;
}
break;
}
}

View file

@ -1,5 +1,5 @@
import Channel from '../channel.js';
import { Notes, UserListJoinings, UserLists } from '@/models/index.js';
import { UserListJoinings, UserLists } from '@/models/index.js';
import { User } from '@/models/entities/user.js';
import { isUserRelated } from '@/misc/is-user-related.js';
import { Packed } from '@/misc/schema.js';
@ -15,7 +15,7 @@ export default class extends Channel {
constructor(id: string, connection: Channel['connection']) {
super(id, connection);
this.updateListUsers = this.updateListUsers.bind(this);
this.onNote = this.onNote.bind(this);
this.onNote = this.withPackedNote(this.onNote.bind(this));
}
public async init(params: any) {
@ -51,29 +51,6 @@ export default class extends Channel {
private async onNote(note: Packed<'Note'>) {
if (!this.listUsers.includes(note.userId)) return;
if (['followers', 'specified'].includes(note.visibility)) {
note = await Notes.pack(note.id, this.user, {
detail: true,
});
if (note.isHidden) {
return;
}
} else {
// リプライなら再pack
if (note.replyId != null) {
note.reply = await Notes.pack(note.replyId, this.user, {
detail: true,
});
}
// Renoteなら再pack
if (note.renoteId != null) {
note.renote = await Notes.pack(note.renoteId, this.user, {
detail: true,
});
}
}
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (isUserRelated(note, this.muting)) return;
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する

View file

@ -243,7 +243,7 @@ export type StreamMessages = {
};
notes: {
name: 'notesStream';
payload: Packed<'Note'>;
payload: Note;
};
};

View file

@ -345,19 +345,15 @@ export default async (user: { id: User['id']; username: User['username']; host:
}
}
// Pack the note
const noteObj = await Notes.pack(note);
publishNotesStream(note);
publishNotesStream(noteObj);
const webhooks = await getActiveWebhooks().then(webhooks => webhooks.filter(x => x.userId === user.id && x.on.includes('note')));
getActiveWebhooks().then(webhooks => {
webhooks = webhooks.filter(x => x.userId === user.id && x.on.includes('note'));
for (const webhook of webhooks) {
webhookDeliver(webhook, 'note', {
note: noteObj,
note: await Notes.pack(note, user),
});
}
});
const nm = new NotificationManager(user, note);
const nmRelatedPromises = [];
@ -378,12 +374,14 @@ export default async (user: { id: User['id']; username: User['username']; host:
if (!threadMuted) {
nm.push(data.reply.userId, 'reply');
publishMainStream(data.reply.userId, 'reply', noteObj);
const packedReply = await Notes.pack(note, { id: data.reply.userId });
publishMainStream(data.reply.userId, 'reply', packedReply);
const webhooks = (await getActiveWebhooks()).filter(x => x.userId === data.reply!.userId && x.on.includes('reply'));
for (const webhook of webhooks) {
webhookDeliver(webhook, 'reply', {
note: noteObj,
note: packedReply,
});
}
}
@ -404,12 +402,13 @@ export default async (user: { id: User['id']; username: User['username']; host:
// Publish event
if ((user.id !== data.renote.userId) && data.renote.userHost === null) {
publishMainStream(data.renote.userId, 'renote', noteObj);
const packedRenote = await Notes.pack(note, { id: data.renote.userId });
publishMainStream(data.renote.userId, 'renote', packedRenote);
const webhooks = (await getActiveWebhooks()).filter(x => x.userId === data.renote!.userId && x.on.includes('renote'));
for (const webhook of webhooks) {
webhookDeliver(webhook, 'renote', {
note: noteObj,
note: packedRenote,
});
}
}

View file

@ -22,7 +22,6 @@ import {
UserListStreamTypes,
UserStreamTypes,
} from '@/server/api/stream/types.js';
import { Packed } from '@/misc/schema.js';
class Publisher {
private publish = (channel: StreamChannels, type: string | null, value?: any): void => {
@ -87,7 +86,7 @@ class Publisher {
this.publish(`messagingIndexStream:${userId}`, type, typeof value === 'undefined' ? null : value);
};
public publishNotesStream = (note: Packed<'Note'>): void => {
public publishNotesStream = (note: Note): void => {
this.publish('notesStream', null, note);
};