From 20763a84ee4023cbaecca74494d1dc571cf401c5 Mon Sep 17 00:00:00 2001 From: Johann150 Date: Mon, 25 Jul 2022 18:15:21 +0200 Subject: [PATCH] Merge: enhance privacy of notes https://akkoma.dev/FoundKeyGang/FoundKey/pulls/14 --- packages/backend/src/misc/get-note-summary.ts | 4 - .../src/models/repositories/note-favorite.ts | 4 +- .../src/models/repositories/note-reaction.ts | 14 ++++ .../backend/src/models/repositories/note.ts | 77 +++---------------- packages/backend/src/models/schema/note.ts | 4 - .../backend/src/server/api/common/getters.ts | 14 +++- .../api/endpoints/admin/promo/create.ts | 6 +- .../server/api/endpoints/clips/add-note.ts | 6 +- .../src/server/api/endpoints/notes/clips.ts | 6 +- .../api/endpoints/notes/conversation.ts | 12 ++- .../src/server/api/endpoints/notes/create.ts | 20 +++-- .../src/server/api/endpoints/notes/delete.ts | 6 +- .../api/endpoints/notes/favorites/create.ts | 6 +- .../api/endpoints/notes/favorites/delete.ts | 6 +- .../server/api/endpoints/notes/polls/vote.ts | 6 +- .../server/api/endpoints/notes/reactions.ts | 9 ++- .../api/endpoints/notes/reactions/create.ts | 6 +- .../api/endpoints/notes/reactions/delete.ts | 6 +- .../src/server/api/endpoints/notes/renotes.ts | 6 +- .../src/server/api/endpoints/notes/show.ts | 10 ++- .../src/server/api/endpoints/notes/state.ts | 3 +- .../endpoints/notes/thread-muting/create.ts | 6 +- .../endpoints/notes/thread-muting/delete.ts | 6 +- .../server/api/endpoints/notes/translate.ts | 10 +-- .../server/api/endpoints/notes/unrenote.ts | 6 +- .../api/endpoints/notes/watching/create.ts | 6 +- .../api/endpoints/notes/watching/delete.ts | 6 +- .../src/server/api/endpoints/promo/read.ts | 6 +- .../server/api/endpoints/users/reactions.ts | 2 +- .../backend/src/server/api/stream/channel.ts | 30 ++++++++ .../src/server/api/stream/channels/antenna.ts | 24 ++++-- .../src/server/api/stream/channels/channel.ts | 15 +--- .../api/stream/channels/global-timeline.ts | 16 +--- .../src/server/api/stream/channels/hashtag.ts | 10 +-- .../api/stream/channels/home-timeline.ts | 26 +------ .../api/stream/channels/hybrid-timeline.ts | 26 +------ .../api/stream/channels/local-timeline.ts | 16 +--- .../src/server/api/stream/channels/main.ts | 15 ---- .../server/api/stream/channels/user-list.ts | 27 +------ .../backend/src/server/api/stream/types.ts | 2 +- packages/backend/src/server/web/index.ts | 40 ++++++---- packages/backend/src/services/note/create.ts | 55 +++++++------ packages/backend/src/services/stream.ts | 3 +- packages/backend/test/api-visibility.ts | 34 ++++---- .../client/src/components/note-detailed.vue | 1 - packages/client/src/components/note.vue | 1 - .../src/components/sub-note-content.vue | 1 - .../client/src/scripts/get-note-summary.ts | 4 - 48 files changed, 254 insertions(+), 371 deletions(-) diff --git a/packages/backend/src/misc/get-note-summary.ts b/packages/backend/src/misc/get-note-summary.ts index ddcfade42..96267400a 100644 --- a/packages/backend/src/misc/get-note-summary.ts +++ b/packages/backend/src/misc/get-note-summary.ts @@ -9,10 +9,6 @@ export const getNoteSummary = (note: Packed<'Note'>): string => { return `(❌⛔)`; } - if (note.isHidden) { - return `(⛔)`; - } - let summary = ''; // 本文 diff --git a/packages/backend/src/models/repositories/note-favorite.ts b/packages/backend/src/models/repositories/note-favorite.ts index 9bd97f988..1d5702053 100644 --- a/packages/backend/src/models/repositories/note-favorite.ts +++ b/packages/backend/src/models/repositories/note-favorite.ts @@ -14,6 +14,7 @@ export const NoteFavoriteRepository = db.getRepository(NoteFavorite).extend({ id: favorite.id, createdAt: favorite.createdAt.toISOString(), noteId: favorite.noteId, + // may throw error note: await Notes.pack(favorite.note || favorite.noteId, me), }; }, @@ -22,6 +23,7 @@ export const NoteFavoriteRepository = db.getRepository(NoteFavorite).extend({ favorites: any[], me: { id: User['id'] } ) { - return Promise.all(favorites.map(x => this.pack(x, me))); + return Promise.allSettled(favorites.map(x => this.pack(x, me))) + .then(promises => promises.flatMap(result => result.status === 'fulfilled' ? [result.value] : [])); }, }); diff --git a/packages/backend/src/models/repositories/note-reaction.ts b/packages/backend/src/models/repositories/note-reaction.ts index 4deae51c9..46084a9a1 100644 --- a/packages/backend/src/models/repositories/note-reaction.ts +++ b/packages/backend/src/models/repositories/note-reaction.ts @@ -25,8 +25,22 @@ export const NoteReactionRepository = db.getRepository(NoteReaction).extend({ user: await Users.pack(reaction.user ?? reaction.userId, me), type: convertLegacyReaction(reaction.reaction), ...(opts.withNote ? { + // may throw error note: await Notes.pack(reaction.note ?? reaction.noteId, me), } : {}), }; }, + + async packMany( + src: NoteReaction[], + me?: { id: User['id'] } | null | undefined, + options?: { + withNote: booleam; + }, + ): Promise[]> { + const reactions = await Promise.allSettled(src.map(reaction => this.pack(reaction, me, options))); + + // filter out rejected promises, only keep fulfilled values + return reactions.flatMap(result => result.status === 'fulfilled' ? [result.value] : []); + } }); diff --git a/packages/backend/src/models/repositories/note.ts b/packages/backend/src/models/repositories/note.ts index 3fefab031..e697b4cea 100644 --- a/packages/backend/src/models/repositories/note.ts +++ b/packages/backend/src/models/repositories/note.ts @@ -10,66 +10,7 @@ import { convertLegacyReaction, convertLegacyReactions, decodeReaction } from '@ import { NoteReaction } from '@/models/entities/note-reaction.js'; import { aggregateNoteEmojis, populateEmojis, prefetchEmojis } from '@/misc/populate-emojis.js'; import { db } from '@/db/postgre.js'; - -async function hideNote(packedNote: Packed<'Note'>, meId: User['id'] | null) { - // TODO: isVisibleForMe を使うようにしても良さそう(型違うけど) - let hide = false; - - // visibility が specified かつ自分が指定されていなかったら非表示 - if (packedNote.visibility === 'specified') { - if (meId == null) { - hide = true; - } else if (meId === packedNote.userId) { - hide = false; - } else { - // 指定されているかどうか - const specified = packedNote.visibleUserIds!.some((id: any) => meId === id); - - if (specified) { - hide = false; - } else { - hide = true; - } - } - } - - // visibility が followers かつ自分が投稿者のフォロワーでなかったら非表示 - if (packedNote.visibility === 'followers') { - if (meId == null) { - hide = true; - } else if (meId === packedNote.userId) { - hide = false; - } else if (packedNote.reply && (meId === packedNote.reply.userId)) { - // 自分の投稿に対するリプライ - hide = false; - } else if (packedNote.mentions && packedNote.mentions.some(id => meId === id)) { - // 自分へのメンション - hide = false; - } else { - // フォロワーかどうか - const following = await Followings.findOneBy({ - followeeId: packedNote.userId, - followerId: meId, - }); - - if (following == null) { - hide = true; - } else { - hide = false; - } - } - } - - if (hide) { - packedNote.visibleUserIds = undefined; - packedNote.fileIds = []; - packedNote.files = []; - packedNote.text = null; - packedNote.poll = undefined; - packedNote.cw = null; - packedNote.isHidden = true; - } -} +import { IdentifiableError } from '@/misc/identifiable-error.js'; async function populatePoll(note: Note, meId: User['id'] | null) { const poll = await Polls.findOneByOrFail({ noteId: note.id }); @@ -193,7 +134,6 @@ export const NoteRepository = db.getRepository(Note).extend({ me?: { id: User['id'] } | null | undefined, options?: { detail?: boolean; - skipHide?: boolean; _hint_?: { myReactions: Map; }; @@ -201,13 +141,16 @@ export const NoteRepository = db.getRepository(Note).extend({ ): Promise> { const opts = Object.assign({ detail: true, - skipHide: false, }, options); const meId = me ? me.id : null; const note = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src }); const host = note.userHost; + if (!await this.isVisibleForMe(note, meId)) { + throw new IdentifiableError('9725d0ce-ba28-4dde-95a7-2cbb2c15de24', 'No such note.'); + } + let text = note.text; if (note.name && (note.url ?? note.uri)) { @@ -282,10 +225,6 @@ export const NoteRepository = db.getRepository(Note).extend({ packed.text = mfm.toString(tokens); } - if (!opts.skipHide) { - await hideNote(packed, meId); - } - return packed; }, @@ -294,7 +233,6 @@ export const NoteRepository = db.getRepository(Note).extend({ me?: { id: User['id'] } | null | undefined, options?: { detail?: boolean; - skipHide?: boolean; } ) { if (notes.length === 0) return []; @@ -316,11 +254,14 @@ export const NoteRepository = db.getRepository(Note).extend({ await prefetchEmojis(aggregateNoteEmojis(notes)); - return await Promise.all(notes.map(n => this.pack(n, me, { + const promises = await Promise.allSettled(notes.map(n => this.pack(n, me, { ...options, _hint_: { myReactions: myReactionsMap, }, }))); + + // filter out rejected promises, only keep fulfilled values + return promises.flatMap(result => result.status === 'fulfilled' ? [result.value] : []); }, }); diff --git a/packages/backend/src/models/schema/note.ts b/packages/backend/src/models/schema/note.ts index cdf4b9a54..292bbb82f 100644 --- a/packages/backend/src/models/schema/note.ts +++ b/packages/backend/src/models/schema/note.ts @@ -52,10 +52,6 @@ export const packedNoteSchema = { optional: true, nullable: true, ref: 'Note', }, - isHidden: { - type: 'boolean', - optional: true, nullable: false, - }, visibility: { type: 'string', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/common/getters.ts b/packages/backend/src/server/api/common/getters.ts index 783ea9ef7..c5a1e765e 100644 --- a/packages/backend/src/server/api/common/getters.ts +++ b/packages/backend/src/server/api/common/getters.ts @@ -2,12 +2,20 @@ import { IdentifiableError } from '@/misc/identifiable-error.js'; import { User } from '@/models/entities/user.js'; import { Note } from '@/models/entities/note.js'; import { Notes, Users } from '@/models/index.js'; +import { generateVisibilityQuery } from './generate-visibility-query.js'; /** - * Get note for API processing + * Get note for API processing, taking into account visibility. */ -export async function getNote(noteId: Note['id']) { - const note = await Notes.findOneBy({ id: noteId }); +export async function getNote(noteId: Note['id'], me: { id: User['id'] } | null) { + const query = Notes.createQueryBuilder('note') + .where("note.id = :id", { + id: noteId, + }); + + generateVisibilityQuery(query, me); + + const note = await query.getOne(); if (note == null) { throw new IdentifiableError('9725d0ce-ba28-4dde-95a7-2cbb2c15de24', 'No such note.'); diff --git a/packages/backend/src/server/api/endpoints/admin/promo/create.ts b/packages/backend/src/server/api/endpoints/admin/promo/create.ts index 68a17867b..b5142fcf0 100644 --- a/packages/backend/src/server/api/endpoints/admin/promo/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/promo/create.ts @@ -35,9 +35,9 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, user) => { - const note = await getNote(ps.noteId).catch(e => { - if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); - throw e; + const note = await getNote(ps.noteId, user).catch(err => { + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + throw err; }); const exist = await PromoNotes.findOneBy({ noteId: note.id }); diff --git a/packages/backend/src/server/api/endpoints/clips/add-note.ts b/packages/backend/src/server/api/endpoints/clips/add-note.ts index 5d72f5c1b..91baa8eb7 100644 --- a/packages/backend/src/server/api/endpoints/clips/add-note.ts +++ b/packages/backend/src/server/api/endpoints/clips/add-note.ts @@ -52,9 +52,9 @@ export default define(meta, paramDef, async (ps, user) => { throw new ApiError(meta.errors.noSuchClip); } - const note = await getNote(ps.noteId).catch(e => { - if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); - throw e; + const note = await getNote(ps.noteId, user).catch(err => { + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + throw err; }); const exist = await ClipNotes.findOneBy({ diff --git a/packages/backend/src/server/api/endpoints/notes/clips.ts b/packages/backend/src/server/api/endpoints/notes/clips.ts index 5a4420a68..514386d73 100644 --- a/packages/backend/src/server/api/endpoints/notes/clips.ts +++ b/packages/backend/src/server/api/endpoints/notes/clips.ts @@ -39,9 +39,9 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, me) => { - const note = await getNote(ps.noteId).catch(e => { - if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); - throw e; + const note = await getNote(ps.noteId, me).catch(err => { + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + throw err; }); const clipNotes = await ClipNotes.findBy({ diff --git a/packages/backend/src/server/api/endpoints/notes/conversation.ts b/packages/backend/src/server/api/endpoints/notes/conversation.ts index 28613962a..fa9b58848 100644 --- a/packages/backend/src/server/api/endpoints/notes/conversation.ts +++ b/packages/backend/src/server/api/endpoints/notes/conversation.ts @@ -41,9 +41,9 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, user) => { - const note = await getNote(ps.noteId).catch(e => { - if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); - throw e; + const note = await getNote(ps.noteId, user).catch(err => { + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + throw err; }); const conversation: Note[] = []; @@ -51,7 +51,11 @@ export default define(meta, paramDef, async (ps, user) => { async function get(id: any) { i++; - const p = await Notes.findOneBy({ id }); + const p = await getNote(id, user).catch(e => { + if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') return null; + throw e; + }); + if (p == null) return; if (i > ps.offset!) { diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index a13329416..d8954ec73 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -7,9 +7,11 @@ import { DriveFile } from '@/models/entities/drive-file.js'; import { Note } from '@/models/entities/note.js'; import { Channel } from '@/models/entities/channel.js'; import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; +import { HOUR } from '@/const.js'; import { noteVisibilities } from '../../../../types.js'; import { ApiError } from '../../error.js'; import define from '../../define.js'; +import { getNote } from '../../common/getters.js'; export const meta = { tags: ['notes'], @@ -185,11 +187,12 @@ export default define(meta, paramDef, async (ps, user) => { let renote: Note | null = null; if (ps.renoteId != null) { // Fetch renote to note - renote = await Notes.findOneBy({ id: ps.renoteId }); + renote = await getNote(ps.renoteId, user).catch(e => { + if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchRenoteTarget); + throw e; + }); - if (renote == null) { - throw new ApiError(meta.errors.noSuchRenoteTarget); - } else if (renote.renoteId && !renote.text && !renote.fileIds && !renote.hasPoll) { + if (renote.renoteId && !renote.text && !renote.fileIds && !renote.hasPoll) { throw new ApiError(meta.errors.cannotReRenote); } @@ -208,11 +211,12 @@ export default define(meta, paramDef, async (ps, user) => { let reply: Note | null = null; if (ps.replyId != null) { // Fetch reply - reply = await Notes.findOneBy({ id: ps.replyId }); + reply = await getNote(ps.replyId, user).catch(e => { + if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchReplyTarget); + throw e; + }); - if (reply == null) { - throw new ApiError(meta.errors.noSuchReplyTarget); - } else if (reply.renoteId && !reply.text && !reply.fileIds && !reply.hasPoll) { + if (reply.renoteId && !reply.text && !reply.fileIds && !reply.hasPoll) { throw new ApiError(meta.errors.cannotReplyToPureRenote); } diff --git a/packages/backend/src/server/api/endpoints/notes/delete.ts b/packages/backend/src/server/api/endpoints/notes/delete.ts index c23ceeb5b..3169db43c 100644 --- a/packages/backend/src/server/api/endpoints/notes/delete.ts +++ b/packages/backend/src/server/api/endpoints/notes/delete.ts @@ -43,9 +43,9 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, user) => { - const note = await getNote(ps.noteId).catch(e => { - if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); - throw e; + const note = await getNote(ps.noteId, user).catch(err => { + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + throw err; }); if ((!user.isAdmin && !user.isModerator) && (note.userId !== user.id)) { diff --git a/packages/backend/src/server/api/endpoints/notes/favorites/create.ts b/packages/backend/src/server/api/endpoints/notes/favorites/create.ts index 097371a42..b5dd88a4e 100644 --- a/packages/backend/src/server/api/endpoints/notes/favorites/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/favorites/create.ts @@ -37,9 +37,9 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, user) => { // Get favoritee - const note = await getNote(ps.noteId).catch(e => { - if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); - throw e; + const note = await getNote(ps.noteId, user).catch(err => { + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + throw err; }); // if already favorited diff --git a/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts b/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts index 82ef4fa19..3f4d39254 100644 --- a/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts +++ b/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts @@ -36,9 +36,9 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, user) => { // Get favoritee - const note = await getNote(ps.noteId).catch(e => { - if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); - throw e; + const note = await getNote(ps.noteId, user).catch(err => { + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + throw err; }); // if already favorited diff --git a/packages/backend/src/server/api/endpoints/notes/polls/vote.ts b/packages/backend/src/server/api/endpoints/notes/polls/vote.ts index 45a832cbd..6dd5ddf9e 100644 --- a/packages/backend/src/server/api/endpoints/notes/polls/vote.ts +++ b/packages/backend/src/server/api/endpoints/notes/polls/vote.ts @@ -72,9 +72,9 @@ export default define(meta, paramDef, async (ps, user) => { const createdAt = new Date(); // Get votee - const note = await getNote(ps.noteId).catch(e => { - if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); - throw e; + const note = await getNote(ps.noteId, user).catch(err => { + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + throw err; }); if (!note.hasPoll) { diff --git a/packages/backend/src/server/api/endpoints/notes/reactions.ts b/packages/backend/src/server/api/endpoints/notes/reactions.ts index be2846d25..00a89b3f2 100644 --- a/packages/backend/src/server/api/endpoints/notes/reactions.ts +++ b/packages/backend/src/server/api/endpoints/notes/reactions.ts @@ -3,6 +3,7 @@ import { NoteReactions } from '@/models/index.js'; import { NoteReaction } from '@/models/entities/note-reaction.js'; import define from '../../define.js'; import { ApiError } from '../../error.js'; +import { getNote } from '../../common/getters.js'; export const meta = { tags: ['notes', 'reactions'], @@ -47,6 +48,12 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, user) => { + // check note visibility + const note = await getNote(ps.noteId, user).catch(err => { + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + throw err; + }); + const query = { noteId: ps.noteId, } as FindOptionsWhere; @@ -69,5 +76,5 @@ export default define(meta, paramDef, async (ps, user) => { relations: ['user', 'user.avatar', 'user.banner', 'note'], }); - return await Promise.all(reactions.map(reaction => NoteReactions.pack(reaction, user))); + return await NoteReactions.packMany(reactions, user); }); diff --git a/packages/backend/src/server/api/endpoints/notes/reactions/create.ts b/packages/backend/src/server/api/endpoints/notes/reactions/create.ts index 07e52a926..b5c0c9d17 100644 --- a/packages/backend/src/server/api/endpoints/notes/reactions/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/reactions/create.ts @@ -42,9 +42,9 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, user) => { - const note = await getNote(ps.noteId).catch(e => { - if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); - throw e; + const note = await getNote(ps.noteId, user).catch(err => { + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + throw err; }); await createReaction(user, note, ps.reaction).catch(e => { if (e.id === '51c42bb4-931a-456b-bff7-e5a8a70dd298') throw new ApiError(meta.errors.alreadyReacted); diff --git a/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts b/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts index c13cafa21..b8e234bb4 100644 --- a/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts +++ b/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts @@ -42,9 +42,9 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, user) => { - const note = await getNote(ps.noteId).catch(e => { - if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); - throw e; + const note = await getNote(ps.noteId, user).catch(err => { + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + throw err; }); await deleteReaction(user, note).catch(e => { if (e.id === '60527ec9-b4cb-4a88-a6bd-32d3ad26817d') throw new ApiError(meta.errors.notReacted); diff --git a/packages/backend/src/server/api/endpoints/notes/renotes.ts b/packages/backend/src/server/api/endpoints/notes/renotes.ts index 4d0cd8fc6..2f662f355 100644 --- a/packages/backend/src/server/api/endpoints/notes/renotes.ts +++ b/packages/backend/src/server/api/endpoints/notes/renotes.ts @@ -45,9 +45,9 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, user) => { - const note = await getNote(ps.noteId).catch(e => { - if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); - throw e; + const note = await getNote(ps.noteId, user).catch(err => { + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + throw err; }); const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId) diff --git a/packages/backend/src/server/api/endpoints/notes/show.ts b/packages/backend/src/server/api/endpoints/notes/show.ts index 470791b1b..83a39a855 100644 --- a/packages/backend/src/server/api/endpoints/notes/show.ts +++ b/packages/backend/src/server/api/endpoints/notes/show.ts @@ -34,12 +34,16 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, user) => { - const note = await getNote(ps.noteId).catch(e => { - if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); - throw e; + const note = await getNote(ps.noteId, user).catch(err => { + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + throw err; }); return await Notes.pack(note, user, { + // FIXME: packing with detail may throw an error if the reply or renote is not visible (#8774) detail: true, + }).catch(err => { + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + throw err; }); }); diff --git a/packages/backend/src/server/api/endpoints/notes/state.ts b/packages/backend/src/server/api/endpoints/notes/state.ts index 01afa5add..67579b2a6 100644 --- a/packages/backend/src/server/api/endpoints/notes/state.ts +++ b/packages/backend/src/server/api/endpoints/notes/state.ts @@ -1,4 +1,5 @@ import { NoteFavorites, Notes, NoteThreadMutings, NoteWatchings } from '@/models/index.js'; +import { getNote } from '../../common/getters.js'; import define from '../../define.js'; export const meta = { @@ -36,7 +37,7 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, user) => { - const note = await Notes.findOneByOrFail({ id: ps.noteId }); + const note = await getNote(ps.noteId, user); const [favorite, watching, threadMuting] = await Promise.all([ NoteFavorites.count({ diff --git a/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts b/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts index cf360526d..4154b5dc5 100644 --- a/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/thread-muting/create.ts @@ -31,9 +31,9 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, user) => { - const note = await getNote(ps.noteId).catch(e => { - if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); - throw e; + const note = await getNote(ps.noteId, user).catch(err => { + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + throw err; }); const mutedNotes = await Notes.find({ diff --git a/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts b/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts index ac310d0fe..cbc0e5ce5 100644 --- a/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts +++ b/packages/backend/src/server/api/endpoints/notes/thread-muting/delete.ts @@ -29,9 +29,9 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, user) => { - const note = await getNote(ps.noteId).catch(e => { - if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); - throw e; + const note = await getNote(ps.noteId, user).catch(err => { + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + throw err; }); await NoteThreadMutings.delete({ diff --git a/packages/backend/src/server/api/endpoints/notes/translate.ts b/packages/backend/src/server/api/endpoints/notes/translate.ts index ba6e262d6..a01dcfa48 100644 --- a/packages/backend/src/server/api/endpoints/notes/translate.ts +++ b/packages/backend/src/server/api/endpoints/notes/translate.ts @@ -39,15 +39,11 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, user) => { - const note = await getNote(ps.noteId).catch(e => { - if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); - throw e; + const note = await getNote(ps.noteId, user).catch(err => { + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + throw err; }); - if (!(await Notes.isVisibleForMe(note, user ? user.id : null))) { - return 204; // TODO: 良い感じのエラー返す - } - if (note.text == null) { return 204; } diff --git a/packages/backend/src/server/api/endpoints/notes/unrenote.ts b/packages/backend/src/server/api/endpoints/notes/unrenote.ts index 3fba0efe0..400eefb70 100644 --- a/packages/backend/src/server/api/endpoints/notes/unrenote.ts +++ b/packages/backend/src/server/api/endpoints/notes/unrenote.ts @@ -37,9 +37,9 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, user) => { - const note = await getNote(ps.noteId).catch(e => { - if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); - throw e; + const note = await getNote(ps.noteId, user).catch(err => { + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + throw err; }); const renotes = await Notes.findBy({ diff --git a/packages/backend/src/server/api/endpoints/notes/watching/create.ts b/packages/backend/src/server/api/endpoints/notes/watching/create.ts index 7d482b073..6025799fa 100644 --- a/packages/backend/src/server/api/endpoints/notes/watching/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/watching/create.ts @@ -29,9 +29,9 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, user) => { - const note = await getNote(ps.noteId).catch(e => { - if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); - throw e; + const note = await getNote(ps.noteId, user).catch(err => { + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + throw err; }); await watch(user.id, note); diff --git a/packages/backend/src/server/api/endpoints/notes/watching/delete.ts b/packages/backend/src/server/api/endpoints/notes/watching/delete.ts index 2c1a2e5fb..7021c7970 100644 --- a/packages/backend/src/server/api/endpoints/notes/watching/delete.ts +++ b/packages/backend/src/server/api/endpoints/notes/watching/delete.ts @@ -29,9 +29,9 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, user) => { - const note = await getNote(ps.noteId).catch(e => { - if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); - throw e; + const note = await getNote(ps.noteId, user).catch(err => { + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + throw err; }); await unwatch(user.id, note); diff --git a/packages/backend/src/server/api/endpoints/promo/read.ts b/packages/backend/src/server/api/endpoints/promo/read.ts index c6a940c65..7c37fcbf7 100644 --- a/packages/backend/src/server/api/endpoints/promo/read.ts +++ b/packages/backend/src/server/api/endpoints/promo/read.ts @@ -28,9 +28,9 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, user) => { - const note = await getNote(ps.noteId).catch(e => { - if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); - throw e; + const note = await getNote(ps.noteId, user).catch(err => { + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + throw err; }); const exist = await PromoReads.findOneBy({ diff --git a/packages/backend/src/server/api/endpoints/users/reactions.ts b/packages/backend/src/server/api/endpoints/users/reactions.ts index 79cf58a41..144326958 100644 --- a/packages/backend/src/server/api/endpoints/users/reactions.ts +++ b/packages/backend/src/server/api/endpoints/users/reactions.ts @@ -63,5 +63,5 @@ export default define(meta, paramDef, async (ps, me) => { .take(ps.limit) .getMany(); - return await Promise.all(reactions.map(reaction => NoteReactions.pack(reaction, me, { withNote: true }))); + return await NoteReactions.packMany(reactions, me, { withNote: true }); }); diff --git a/packages/backend/src/server/api/stream/channel.ts b/packages/backend/src/server/api/stream/channel.ts index d2cc5122d..c9cffd2d3 100644 --- a/packages/backend/src/server/api/stream/channel.ts +++ b/packages/backend/src/server/api/stream/channel.ts @@ -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; diff --git a/packages/backend/src/server/api/stream/channels/antenna.ts b/packages/backend/src/server/api/stream/channels/antenna.ts index d28320d92..a9a98e904 100644 --- a/packages/backend/src/server/api/stream/channels/antenna.ts +++ b/packages/backend/src/server/api/stream/channels/antenna.ts @@ -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,16 +24,25 @@ export default class extends Channel { private async onEvent(data: StreamMessages['antenna']['payload']) { if (data.type === 'note') { - const note = await Notes.pack(data.body.id, this.user, { detail: true }); + try { + const note = await Notes.pack(data.body.id, this.user, { detail: true }); - // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する - if (isUserRelated(note, this.muting)) return; - // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する - if (isUserRelated(note, this.blocking)) return; + // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する + if (isUserRelated(note, this.muting)) return; + // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する + if (isUserRelated(note, this.blocking)) return; - this.connection.cacheNote(note); + this.connection.cacheNote(note); - this.send('note', 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); } diff --git a/packages/backend/src/server/api/stream/channels/channel.ts b/packages/backend/src/server/api/stream/channels/channel.ts index 3cdd89a8b..7ed47c389 100644 --- a/packages/backend/src/server/api/stream/channels/channel.ts +++ b/packages/backend/src/server/api/stream/channels/channel.ts @@ -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'; @@ -31,19 +31,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がブロックされているユーザーが関わるものだったら無視する diff --git a/packages/backend/src/server/api/stream/channels/global-timeline.ts b/packages/backend/src/server/api/stream/channels/global-timeline.ts index 5b4ae850e..391851ecd 100644 --- a/packages/backend/src/server/api/stream/channels/global-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/global-timeline.ts @@ -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; diff --git a/packages/backend/src/server/api/stream/channels/hashtag.ts b/packages/backend/src/server/api/stream/channels/hashtag.ts index 741db447e..f9f7ae410 100644 --- a/packages/backend/src/server/api/stream/channels/hashtag.ts +++ b/packages/backend/src/server/api/stream/channels/hashtag.ts @@ -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がブロックされているユーザーが関わるものだったら無視する diff --git a/packages/backend/src/server/api/stream/channels/home-timeline.ts b/packages/backend/src/server/api/stream/channels/home-timeline.ts index 075a242ef..9f5188547 100644 --- a/packages/backend/src/server/api/stream/channels/home-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/home-timeline.ts @@ -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(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; diff --git a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts index f5dedf77c..e73136b8e 100644 --- a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts @@ -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(this.userProfile?.mutedInstances ?? []))) return; diff --git a/packages/backend/src/server/api/stream/channels/local-timeline.ts b/packages/backend/src/server/api/stream/channels/local-timeline.ts index f01f47723..729de6d4a 100644 --- a/packages/backend/src/server/api/stream/channels/local-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/local-timeline.ts @@ -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; diff --git a/packages/backend/src/server/api/stream/channels/main.ts b/packages/backend/src/server/api/stream/channels/main.ts index 9cfea0bfc..7f42263db 100644 --- a/packages/backend/src/server/api/stream/channels/main.ts +++ b/packages/backend/src/server/api/stream/channels/main.ts @@ -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(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(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; } } diff --git a/packages/backend/src/server/api/stream/channels/user-list.ts b/packages/backend/src/server/api/stream/channels/user-list.ts index 97ad2983c..9b2476148 100644 --- a/packages/backend/src/server/api/stream/channels/user-list.ts +++ b/packages/backend/src/server/api/stream/channels/user-list.ts @@ -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がブロックされているユーザーが関わるものだったら無視する diff --git a/packages/backend/src/server/api/stream/types.ts b/packages/backend/src/server/api/stream/types.ts index 3b0a75d79..8050d8a1d 100644 --- a/packages/backend/src/server/api/stream/types.ts +++ b/packages/backend/src/server/api/stream/types.ts @@ -243,7 +243,7 @@ export type StreamMessages = { }; notes: { name: 'notesStream'; - payload: Packed<'Note'>; + payload: Note; }; }; diff --git a/packages/backend/src/server/web/index.ts b/packages/backend/src/server/web/index.ts index f26a5ee71..dab80814f 100644 --- a/packages/backend/src/server/web/index.ts +++ b/packages/backend/src/server/web/index.ts @@ -344,24 +344,32 @@ router.get('/notes/:note', async (ctx, next) => { }); if (note) { - const _note = await Notes.pack(note); - const profile = await UserProfiles.findOneByOrFail({ userId: note.userId }); - const meta = await fetchMeta(); - await ctx.render('note', { - note: _note, - profile, - avatarUrl: await Users.getAvatarUrl(await Users.findOneByOrFail({ id: note.userId })), - // TODO: Let locale changeable by instance setting - summary: getNoteSummary(_note), - instanceName: meta.name || 'Calckey', - icon: meta.iconUrl, - privateMode: meta.privateMode, - themeColor: meta.themeColor, - }); + try { + // FIXME: packing with detail may throw an error if the reply or renote is not visible (#8774) + const _note = await Notes.pack(note); + const profile = await UserProfiles.findOneByOrFail({ userId: note.userId }); + const meta = await fetchMeta(); + await ctx.render('note', { + note: _note, + profile, + avatarUrl: await Users.getAvatarUrl(await Users.findOneByOrFail({ id: note.userId })), + // TODO: Let locale changeable by instance setting + summary: getNoteSummary(_note), + instanceName: meta.name || 'Misskey', + icon: meta.iconUrl, + themeColor: meta.themeColor, + }); - ctx.set('Cache-Control', 'public, max-age=15'); + ctx.set('Cache-Control', 'public, max-age=15'); - return; + return; + } catch (err) { + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') { + // note not visible to user + } else { + throw err; + } + } } await next(); diff --git a/packages/backend/src/services/note/create.ts b/packages/backend/src/services/note/create.ts index e2bf9d5b5..3d5b308f3 100644 --- a/packages/backend/src/services/note/create.ts +++ b/packages/backend/src/services/note/create.ts @@ -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, - }); - } - }); + for (const webhook of webhooks) { + webhookDeliver(webhook, 'note', { + 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, }); } } @@ -642,17 +641,23 @@ async function createMentionedEvents(mentionedUsers: MinimumUser[], note: Note, continue; } - const detailPackedNote = await Notes.pack(note, u, { - detail: true, - }); - - publishMainStream(u.id, 'mention', detailPackedNote); - - const webhooks = (await getActiveWebhooks()).filter(x => x.userId === u.id && x.on.includes('mention')); - for (const webhook of webhooks) { - webhookDeliver(webhook, 'mention', { - note: detailPackedNote, + // note with "specified" visibility might not be visible to mentioned users + try { + const detailPackedNote = await Notes.pack(note, u, { + detail: true, }); + + publishMainStream(u.id, 'mention', detailPackedNote); + + const webhooks = (await getActiveWebhooks()).filter(x => x.userId === u.id && x.on.includes('mention')); + for (const webhook of webhooks) { + webhookDeliver(webhook, 'mention', { + note: detailPackedNote, + }); + } + } catch (err) { + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') continue; + throw err; } // Create notification diff --git a/packages/backend/src/services/stream.ts b/packages/backend/src/services/stream.ts index 9fa2b9713..4895bbace 100644 --- a/packages/backend/src/services/stream.ts +++ b/packages/backend/src/services/stream.ts @@ -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); }; diff --git a/packages/backend/test/api-visibility.ts b/packages/backend/test/api-visibility.ts index b155549f9..cde3cd2d0 100644 --- a/packages/backend/test/api-visibility.ts +++ b/packages/backend/test/api-visibility.ts @@ -154,18 +154,18 @@ describe('API visibility', () => { it('[show] followers-postを非フォロワーが見れない', async(async () => { const res = await show(fol.id, other); - assert.strictEqual(res.body.isHidden, true); + assert.strictEqual(res.status, 404); })); it('[show] followers-postを未認証が見れない', async(async () => { const res = await show(fol.id, null); - assert.strictEqual(res.body.isHidden, true); + assert.strictEqual(res.status, 404); })); // specified it('[show] specified-postを自分が見れる', async(async () => { const res = await show(spe.id, alice); - assert.strictEqual(res.body.text, 'x'); + assert.strictEqual(res.status, 404); })); it('[show] specified-postを指定ユーザーが見れる', async(async () => { @@ -175,17 +175,17 @@ describe('API visibility', () => { it('[show] specified-postをフォロワーが見れない', async(async () => { const res = await show(spe.id, follower); - assert.strictEqual(res.body.isHidden, true); + assert.strictEqual(res.status, 404); })); it('[show] specified-postを非フォロワーが見れない', async(async () => { const res = await show(spe.id, other); - assert.strictEqual(res.body.isHidden, true); + assert.strictEqual(res.status, 404); })); it('[show] specified-postを未認証が見れない', async(async () => { const res = await show(spe.id, null); - assert.strictEqual(res.body.isHidden, true); + assert.strictEqual(res.status, 404); })); //#endregion @@ -260,12 +260,12 @@ describe('API visibility', () => { it('[show] followers-replyを非フォロワーが見れない', async(async () => { const res = await show(folR.id, other); - assert.strictEqual(res.body.isHidden, true); + assert.strictEqual(res.status, 404); })); it('[show] followers-replyを未認証が見れない', async(async () => { const res = await show(folR.id, null); - assert.strictEqual(res.body.isHidden, true); + assert.strictEqual(res.status, 404); })); // specified @@ -286,17 +286,17 @@ describe('API visibility', () => { it('[show] specified-replyをフォロワーが見れない', async(async () => { const res = await show(speR.id, follower); - assert.strictEqual(res.body.isHidden, true); + assert.strictEqual(res.status, 404); })); it('[show] specified-replyを非フォロワーが見れない', async(async () => { const res = await show(speR.id, other); - assert.strictEqual(res.body.isHidden, true); + assert.strictEqual(res.status, 404); })); it('[show] specified-replyを未認証が見れない', async(async () => { const res = await show(speR.id, null); - assert.strictEqual(res.body.isHidden, true); + assert.strictEqual(res.status, 404); })); //#endregion @@ -371,12 +371,12 @@ describe('API visibility', () => { it('[show] followers-mentionを非フォロワーが見れない', async(async () => { const res = await show(folM.id, other); - assert.strictEqual(res.body.isHidden, true); + assert.strictEqual(res.status, 404); })); it('[show] followers-mentionを未認証が見れない', async(async () => { const res = await show(folM.id, null); - assert.strictEqual(res.body.isHidden, true); + assert.strictEqual(res.status, 404); })); // specified @@ -392,22 +392,22 @@ describe('API visibility', () => { it('[show] specified-mentionをされた人が指定されてなかったら見れない', async(async () => { const res = await show(speM.id, target2); - assert.strictEqual(res.body.isHidden, true); + assert.strictEqual(res.status, 404); })); it('[show] specified-mentionをフォロワーが見れない', async(async () => { const res = await show(speM.id, follower); - assert.strictEqual(res.body.isHidden, true); + assert.strictEqual(res.status, 404); })); it('[show] specified-mentionを非フォロワーが見れない', async(async () => { const res = await show(speM.id, other); - assert.strictEqual(res.body.isHidden, true); + assert.strictEqual(res.status, 404); })); it('[show] specified-mentionを未認証が見れない', async(async () => { const res = await show(speM.id, null); - assert.strictEqual(res.body.isHidden, true); + assert.strictEqual(res.status, 404); })); //#endregion diff --git a/packages/client/src/components/note-detailed.vue b/packages/client/src/components/note-detailed.vue index 650e130ed..025c7abdd 100644 --- a/packages/client/src/components/note-detailed.vue +++ b/packages/client/src/components/note-detailed.vue @@ -54,7 +54,6 @@

- ({{ i18n.ts.private }}) RN: diff --git a/packages/client/src/components/note.vue b/packages/client/src/components/note.vue index 3c9d36170..cab5c8258 100644 --- a/packages/client/src/components/note.vue +++ b/packages/client/src/components/note.vue @@ -43,7 +43,6 @@

- ({{ i18n.ts.private }}) RN: diff --git a/packages/client/src/components/sub-note-content.vue b/packages/client/src/components/sub-note-content.vue index 25ab883f4..9ee180345 100644 --- a/packages/client/src/components/sub-note-content.vue +++ b/packages/client/src/components/sub-note-content.vue @@ -1,7 +1,6 @@