From 9dc0d849ecd827b4241e462fed7a94d6d0be5563 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Thu, 18 Sep 2025 12:24:40 -0400 Subject: [PATCH 1/8] recurse when preventing quote chains --- packages/backend/src/core/NoteEditService.ts | 12 ++++++++++-- .../backend/src/server/api/endpoints/notes/edit.ts | 6 ++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/backend/src/core/NoteEditService.ts b/packages/backend/src/core/NoteEditService.ts index 9e8bb8b4fd..f31b267ad8 100644 --- a/packages/backend/src/core/NoteEditService.ts +++ b/packages/backend/src/core/NoteEditService.ts @@ -303,8 +303,16 @@ export class NoteEditService implements OnApplicationShutdown { } if (this.isRenote(data)) { - if (data.renote.id === oldnote.id) { - throw new IdentifiableError('ea93b7c2-3d6c-4e10-946b-00d50b1a75cb', `edit failed for ${oldnote.id}: cannot renote itself`); + // Check for recursion + let renoteId: string | null = data.renote.id; + while (renoteId) { + if (renoteId === oldnote.id) { + throw new IdentifiableError('ea93b7c2-3d6c-4e10-946b-00d50b1a75cb', `edit failed for ${oldnote.id}: cannot renote itself`); + } + + // TODO create something like threadId but for quotes, that way we don't need full recursion + const next = await this.notesRepository.findOne({ where: { id: renoteId }, select: { renoteId: true } }); + renoteId = next?.renoteId ?? null; } switch (data.renote.visibility) { diff --git a/packages/backend/src/server/api/endpoints/notes/edit.ts b/packages/backend/src/server/api/endpoints/notes/edit.ts index 717dab59e1..08d5d0daac 100644 --- a/packages/backend/src/server/api/endpoints/notes/edit.ts +++ b/packages/backend/src/server/api/endpoints/notes/edit.ts @@ -362,10 +362,6 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.cannotReRenote); } - if (renote.renoteId === ps.editId) { - throw new ApiError(meta.errors.cannotQuoteaQuoteOfCurrentPost); - } - // Check blocking if (renote.userId !== me.id) { const blockExist = await this.blockingsRepository.exists({ @@ -483,6 +479,8 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.containsTooManyMentions); } else if (e.id === '1c0ea108-d1e3-4e8e-aa3f-4d2487626153') { throw new ApiError(meta.errors.quoteDisabledForUser); + } else if (e.id === 'ea93b7c2-3d6c-4e10-946b-00d50b1a75cb') { + throw new ApiError(meta.errors.cannotQuoteaQuoteOfCurrentPost); } } throw e; From 74a7f96cc2fc97952cb931970934b07a164fca31 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Thu, 18 Sep 2025 13:07:07 -0400 Subject: [PATCH 2/8] prevent remote instances from renoting a boost --- packages/backend/src/core/NoteCreateService.ts | 5 +++++ packages/backend/src/core/NoteEditService.ts | 5 +++++ packages/backend/src/server/api/endpoints/notes/create.ts | 2 ++ packages/backend/src/server/api/endpoints/notes/edit.ts | 4 ++-- 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index ea74d3a84e..a3079d9db9 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -57,6 +57,7 @@ import { IdentifiableError } from '@/misc/identifiable-error.js'; import { LatestNoteService } from '@/core/LatestNoteService.js'; import { CollapsedQueue } from '@/misc/collapsed-queue.js'; import { CacheService } from '@/core/CacheService.js'; +import { isPureRenote } from '@/misc/is-renote.js'; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; @@ -284,6 +285,10 @@ export class NoteCreateService implements OnApplicationShutdown { } if (data.renote) { + if (isPureRenote(data.renote)) { + throw new IdentifiableError('fd4cc33e-2a37-48dd-99cc-9b806eb2031a', 'Cannot renote a pure renote (boost)'); + } + switch (data.renote.visibility) { case 'public': // public noteは無条件にrenote可能 diff --git a/packages/backend/src/core/NoteEditService.ts b/packages/backend/src/core/NoteEditService.ts index f31b267ad8..d96a835b8a 100644 --- a/packages/backend/src/core/NoteEditService.ts +++ b/packages/backend/src/core/NoteEditService.ts @@ -52,6 +52,7 @@ import { IdentifiableError } from '@/misc/identifiable-error.js'; import { LatestNoteService } from '@/core/LatestNoteService.js'; import { CollapsedQueue } from '@/misc/collapsed-queue.js'; import { NoteCreateService } from '@/core/NoteCreateService.js'; +import { isPureRenote } from '@/misc/is-renote.js'; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention' | 'edited'; @@ -303,6 +304,10 @@ export class NoteEditService implements OnApplicationShutdown { } if (this.isRenote(data)) { + if (isPureRenote(data.renote)) { + throw new IdentifiableError('fd4cc33e-2a37-48dd-99cc-9b806eb2031a', 'Cannot renote a pure renote (boost)'); + } + // Check for recursion let renoteId: string | null = data.renote.id; while (renoteId) { diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index d6fccd1b84..98bf827d25 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -428,6 +428,8 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.containsTooManyMentions); } else if (e.id === '1c0ea108-d1e3-4e8e-aa3f-4d2487626153') { throw new ApiError(meta.errors.quoteDisabledForUser); + } else if (e.id === 'fd4cc33e-2a37-48dd-99cc-9b806eb2031a') { + throw new ApiError(meta.errors.cannotReRenote); } } throw e; diff --git a/packages/backend/src/server/api/endpoints/notes/edit.ts b/packages/backend/src/server/api/endpoints/notes/edit.ts index 08d5d0daac..8a65e016d5 100644 --- a/packages/backend/src/server/api/endpoints/notes/edit.ts +++ b/packages/backend/src/server/api/endpoints/notes/edit.ts @@ -358,8 +358,6 @@ export default class extends Endpoint { // eslint- if (renote == null) { throw new ApiError(meta.errors.noSuchRenoteTarget); - } else if (isRenote(renote) && !isQuote(renote)) { - throw new ApiError(meta.errors.cannotReRenote); } // Check blocking @@ -481,6 +479,8 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.quoteDisabledForUser); } else if (e.id === 'ea93b7c2-3d6c-4e10-946b-00d50b1a75cb') { throw new ApiError(meta.errors.cannotQuoteaQuoteOfCurrentPost); + } else if (e.id === 'fd4cc33e-2a37-48dd-99cc-9b806eb2031a') { + throw new ApiError(meta.errors.cannotReRenote); } } throw e; From c2e52f6ae740a9acdafd4245cc7b557be5aafbf4 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Thu, 18 Sep 2025 14:15:59 -0400 Subject: [PATCH 3/8] prevent packing renotes at infinite depth --- .../backend/src/core/entities/NoteEntityService.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts index 4e7ac59f41..95b464c56f 100644 --- a/packages/backend/src/core/entities/NoteEntityService.ts +++ b/packages/backend/src/core/entities/NoteEntityService.ts @@ -508,6 +508,8 @@ export class NoteEntityService implements OnModuleInit { me?: { id: MiUser['id'] } | null | undefined, options?: { detail?: boolean; + recurseReply?: boolean; // Defaults to the value of detail, which defaults to true. + recurseRenote?: boolean; // Defaults to the value of detail, which defaults to true. skipHide?: boolean; withReactionAndUserPairCache?: boolean; bypassSilence?: boolean; @@ -535,6 +537,8 @@ export class NoteEntityService implements OnModuleInit { skipHide: false, withReactionAndUserPairCache: false, }, options); + opts.recurseRenote ??= opts.detail; + opts.recurseReply ??= opts.detail; const meId = me ? me.id : null; const note = typeof src === 'object' ? src : await this.noteLoader.load(src); @@ -647,8 +651,9 @@ export class NoteEntityService implements OnModuleInit { ...(opts.detail ? { clippedCount: note.clippedCount, processErrors: note.processErrors, + } : {}), - reply: note.replyId ? this.pack(note.reply ?? opts._hint_?.notes.get(note.replyId) ?? note.replyId, me, { + reply: opts.recurseReply && note.replyId ? this.pack(note.reply ?? opts._hint_?.notes.get(note.replyId) ?? note.replyId, me, { detail: false, skipHide: opts.skipHide, withReactionAndUserPairCache: opts.withReactionAndUserPairCache, @@ -658,8 +663,11 @@ export class NoteEntityService implements OnModuleInit { bypassSilence: bypassSilence || note.userId === note.replyUserId, }) : undefined, - renote: note.renoteId ? this.pack(note.renote ?? opts._hint_?.notes.get(note.renoteId) ?? note.renoteId, me, { - detail: true, + // The renote target needs to be packed with the reply, but we *must not* recurse any further. + // Pass detail=false and recurseReply=true to make sure we only include the right data. + renote: opts.recurseRenote && note.renoteId ? this.pack(note.renote ?? opts._hint_?.notes.get(note.renoteId) ?? note.renoteId, me, { + detail: false, + recurseReply: true, skipHide: opts.skipHide, withReactionAndUserPairCache: opts.withReactionAndUserPairCache, _hint_: options?._hint_, From 24bc1c653d47d674bfb2734d2859a1b26f79bcc5 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Thu, 18 Sep 2025 14:16:30 -0400 Subject: [PATCH 4/8] fix DMs converting to public notes when the audience can't be resolved --- .../src/core/activitypub/ApAudienceService.ts | 13 +++++++++++++ .../src/core/activitypub/models/ApNoteService.ts | 14 ++++---------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/packages/backend/src/core/activitypub/ApAudienceService.ts b/packages/backend/src/core/activitypub/ApAudienceService.ts index 5a5a76f7d6..15c4546063 100644 --- a/packages/backend/src/core/activitypub/ApAudienceService.ts +++ b/packages/backend/src/core/activitypub/ApAudienceService.ts @@ -42,6 +42,19 @@ export class ApAudienceService { others.map(id => limit(() => this.apPersonService.resolvePerson(id, resolver).catch(() => null))), )).filter(x => x != null); + // If no audience is specified, then assume public + if ( + toGroups.public.length === 0 && toGroups.followers.length === 0 && + ccGroups.public.length === 0 && ccGroups.followers.length === 0 && + others.length === 0 + ) { + return { + visibility: 'public', + mentionedUsers: [], + visibleUsers: [], + }; + } + if (toGroups.public.length > 0) { return { visibility: 'public', diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index 32661fb6d9..b451cbe1dc 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -239,15 +239,12 @@ export class ApNoteService { } const noteAudience = await this.apAudienceService.parseAudience(actor, note.to, note.cc, resolver); - let visibility = noteAudience.visibility; + const visibility = noteAudience.visibility; const visibleUsers = noteAudience.visibleUsers; // Audience (to, cc) が指定されてなかった場合 if (visibility === 'specified' && visibleUsers.length === 0) { - if (typeof value === 'string') { // 入力がstringならばresolverでGETが発生している - // こちらから匿名GET出来たものならばpublic - visibility = 'public'; - } + throw new IdentifiableError('dc2ad0d1-36bf-41f5-8e4c-a4d265a28387', `failed to create note ${entryUri}: could not resolve any recipients`); } const processErrors: string[] = []; @@ -430,15 +427,12 @@ export class ApNoteService { //#endregion const noteAudience = await this.apAudienceService.parseAudience(actor, note.to, note.cc, resolver); - let visibility = noteAudience.visibility; + const visibility = noteAudience.visibility; const visibleUsers = noteAudience.visibleUsers; // Audience (to, cc) が指定されてなかった場合 if (visibility === 'specified' && visibleUsers.length === 0) { - if (typeof value === 'string') { // 入力がstringならばresolverでGETが発生している - // こちらから匿名GET出来たものならばpublic - visibility = 'public'; - } + throw new IdentifiableError('dc2ad0d1-36bf-41f5-8e4c-a4d265a28387', `failed to create note ${entryUri}: could not resolve any recipients`); } const processErrors: string[] = []; From edb74066da93cef46cc510c3ded3796b5e7113a5 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Thu, 18 Sep 2025 14:16:58 -0400 Subject: [PATCH 5/8] unify note create/edit logic to fix various logic bugs and validation gaps --- .../backend/src/core/NoteCreateService.ts | 64 ++++++++++++------- packages/backend/src/core/NoteEditService.ts | 59 +++++++++-------- .../core/activitypub/models/ApNoteService.ts | 11 ---- .../src/server/api/endpoints/notes/create.ts | 50 +++------------ .../src/server/api/endpoints/notes/edit.ts | 53 +++------------ 5 files changed, 93 insertions(+), 144 deletions(-) diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index a3079d9db9..59f9c32191 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -302,7 +302,7 @@ export class NoteCreateService implements OnApplicationShutdown { case 'followers': // 他人のfollowers noteはreject if (data.renote.userId !== user.id) { - throw new IdentifiableError('b6352a84-e5cd-4b05-a26c-63437a6b98ba', 'Renote target is not public or home'); + throw new IdentifiableError('be9529e9-fe72-4de0-ae43-0b363c4938af', 'Renote target is not public or home'); } // Renote対象がfollowersならfollowersにする @@ -310,25 +310,53 @@ export class NoteCreateService implements OnApplicationShutdown { break; case 'specified': // specified / direct noteはreject - throw new IdentifiableError('b6352a84-e5cd-4b05-a26c-63437a6b98ba', 'Renote target is not public or home'); + throw new IdentifiableError('be9529e9-fe72-4de0-ae43-0b363c4938af', 'Renote target is not public or home'); + } + + if (data.renote.userId !== user.id) { + // Check local-only + if (data.renote.localOnly && user.host != null) { + throw new IdentifiableError('12e23cec-edd9-442b-aa48-9c21f0c3b215', 'Remote user cannot renote a local-only note'); + } + + // Check visibility + if (!await this.noteEntityService.isVisibleForMe(data.renote, user.id)) { + throw new IdentifiableError('be9529e9-fe72-4de0-ae43-0b363c4938af', 'Cannot renote an invisible note'); + } + + // Check blocking + if (await this.userBlockingService.checkBlocked(data.renote.userId, user.id)) { + throw new IdentifiableError('b6352a84-e5cd-4b05-a26c-63437a6b98ba', 'Renote target is blocked'); + } + } + } + + if (data.reply) { + if (isPureRenote(data.reply)) { + throw new IdentifiableError('3ac74a84-8fd5-4bb0-870f-01804f82ce15', 'Cannot reply to a pure renote (boost)'); + } + + if (data.reply.userId !== user.id) { + // Check local-only + if (data.reply.localOnly && user.host != null) { + throw new IdentifiableError('12e23cec-edd9-442b-aa48-9c21f0c3b215', 'Remote user cannot reply to a local-only note'); + } + + // Check visibility + if (!await this.noteEntityService.isVisibleForMe(data.reply, user.id)) { + throw new IdentifiableError('b98980fa-3780-406c-a935-b6d0eeee10d1', 'Cannot reply to an invisible note'); + } + + // Check blocking + if (await this.userBlockingService.checkBlocked(data.reply.userId, user.id)) { + throw new IdentifiableError('b6352a84-e5cd-4b05-a26c-63437a6b98ba', 'Reply target is blocked'); + } } } // Check quote permissions await this.checkQuotePermissions(data, user); - // Check blocking - if (this.isRenote(data) && !this.isQuote(data)) { - if (data.renote.userHost === null) { - if (data.renote.userId !== user.id) { - const blocked = await this.userBlockingService.checkBlocked(data.renote.userId, user.id); - if (blocked) { - throw new IdentifiableError('b6352a84-e5cd-4b05-a26c-63437a6b98ba', 'Renote target is blocked'); - } - } - } - } - // 返信対象がpublicではないならhomeにする if (data.reply && data.reply.visibility !== 'public' && data.visibility === 'public') { data.visibility = 'home'; @@ -486,14 +514,6 @@ export class NoteCreateService implements OnApplicationShutdown { mandatoryCW: data.mandatoryCW, }); - // should really not happen, but better safe than sorry - if (data.reply?.id === insert.id) { - throw new IdentifiableError('ea93b7c2-3d6c-4e10-946b-00d50b1a75cb', 'A note can\'t reply to itself'); - } - if (data.renote?.id === insert.id) { - throw new IdentifiableError('ea93b7c2-3d6c-4e10-946b-00d50b1a75cb', 'A note can\'t renote itself'); - } - if (data.uri != null) insert.uri = data.uri; if (data.url != null) insert.url = data.url; diff --git a/packages/backend/src/core/NoteEditService.ts b/packages/backend/src/core/NoteEditService.ts index d96a835b8a..7b51ce2022 100644 --- a/packages/backend/src/core/NoteEditService.ts +++ b/packages/backend/src/core/NoteEditService.ts @@ -308,18 +308,6 @@ export class NoteEditService implements OnApplicationShutdown { throw new IdentifiableError('fd4cc33e-2a37-48dd-99cc-9b806eb2031a', 'Cannot renote a pure renote (boost)'); } - // Check for recursion - let renoteId: string | null = data.renote.id; - while (renoteId) { - if (renoteId === oldnote.id) { - throw new IdentifiableError('ea93b7c2-3d6c-4e10-946b-00d50b1a75cb', `edit failed for ${oldnote.id}: cannot renote itself`); - } - - // TODO create something like threadId but for quotes, that way we don't need full recursion - const next = await this.notesRepository.findOne({ where: { id: renoteId }, select: { renoteId: true } }); - renoteId = next?.renoteId ?? null; - } - switch (data.renote.visibility) { case 'public': // public noteは無条件にrenote可能 @@ -333,7 +321,7 @@ export class NoteEditService implements OnApplicationShutdown { case 'followers': // 他人のfollowers noteはreject if (data.renote.userId !== user.id) { - throw new IdentifiableError('b6352a84-e5cd-4b05-a26c-63437a6b98ba', 'Renote target is not public or home'); + throw new IdentifiableError('be9529e9-fe72-4de0-ae43-0b363c4938af', 'Renote target is not public or home'); } // Renote対象がfollowersならfollowersにする @@ -341,25 +329,44 @@ export class NoteEditService implements OnApplicationShutdown { break; case 'specified': // specified / direct noteはreject - throw new IdentifiableError('b6352a84-e5cd-4b05-a26c-63437a6b98ba', 'Renote target is not public or home'); + throw new IdentifiableError('be9529e9-fe72-4de0-ae43-0b363c4938af', 'Renote target is not public or home'); + } + + if (data.renote.userId !== user.id) { + // Check local-only + if (data.renote.localOnly && user.host != null) { + throw new IdentifiableError('12e23cec-edd9-442b-aa48-9c21f0c3b215', 'Remote user cannot renote a local-only note'); + } + + // Check visibility + if (!await this.noteEntityService.isVisibleForMe(data.renote, user.id)) { + throw new IdentifiableError('be9529e9-fe72-4de0-ae43-0b363c4938af', 'Cannot renote an invisible note'); + } + + // Check blocking + if (await this.userBlockingService.checkBlocked(data.renote.userId, user.id)) { + throw new IdentifiableError('b6352a84-e5cd-4b05-a26c-63437a6b98ba', 'Renote target is blocked'); + } + } + + // Check for recursion + if (data.renote.id === oldnote.id) { + throw new IdentifiableError('33510210-8452-094c-6227-4a6c05d99f02', `edit failed for ${oldnote.id}: note cannot quote itself`); + } + for (let nextRenoteId = data.renote.renoteId; nextRenoteId != null;) { + if (nextRenoteId === oldnote.id) { + throw new IdentifiableError('ea93b7c2-3d6c-4e10-946b-00d50b1a75cb', `edit failed for ${oldnote.id}: note cannot quote a quote of itself`); + } + + // TODO create something like threadId but for quotes, that way we don't need full recursion + const next = await this.notesRepository.findOne({ where: { id: nextRenoteId }, select: { renoteId: true } }); + nextRenoteId = next?.renoteId ?? null; } } // Check quote permissions await this.noteCreateService.checkQuotePermissions(data, user); - // Check blocking - if (this.isRenote(data) && !this.isQuote(data)) { - if (data.renote.userHost === null) { - if (data.renote.userId !== user.id) { - const blocked = await this.userBlockingService.checkBlocked(data.renote.userId, user.id); - if (blocked) { - throw new Error('blocked'); - } - } - } - } - // 返信対象がpublicではないならhomeにする if (data.reply && data.reply.visibility !== 'public' && data.visibility === 'public') { data.visibility = 'home'; diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index b451cbe1dc..652efd46b2 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -280,13 +280,6 @@ export class ApNoteService { processErrors.push('quoteUnavailable'); } - if (reply && reply.userHost == null && reply.localOnly) { - throw new IdentifiableError('12e23cec-edd9-442b-aa48-9c21f0c3b215', 'Cannot reply to local-only note'); - } - if (quote && quote.userHost == null && quote.localOnly) { - throw new IdentifiableError('12e23cec-edd9-442b-aa48-9c21f0c3b215', 'Cannot quote a local-only note'); - } - // vote if (reply && reply.hasPoll) { const poll = await this.pollsRepository.findOneByOrFail({ noteId: reply.id }); @@ -466,10 +459,6 @@ export class ApNoteService { processErrors.push('quoteUnavailable'); } - if (quote && quote.userHost == null && quote.localOnly) { - throw new IdentifiableError('12e23cec-edd9-442b-aa48-9c21f0c3b215', 'Cannot quote a local-only note'); - } - // vote if (reply && reply.hasPoll) { const poll = await this.pollsRepository.findOneByOrFail({ noteId: reply.id }); diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index 98bf827d25..cf61a297b0 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -303,31 +303,6 @@ export default class extends Endpoint { // eslint- if (renote == null) { throw new ApiError(meta.errors.noSuchRenoteTarget); - } else if (isRenote(renote) && !isQuote(renote)) { - throw new ApiError(meta.errors.cannotReRenote); - } else if (!(await this.noteVisibilityService.checkNoteVisibilityAsync(renote, me)).accessible) { - throw new ApiError(meta.errors.cannotRenoteDueToVisibility); - } - - // Check blocking - if (renote.userId !== me.id) { - const blockExist = await this.blockingsRepository.exists({ - where: { - blockerId: renote.userId, - blockeeId: me.id, - }, - }); - if (blockExist) { - throw new ApiError(meta.errors.youHaveBeenBlocked); - } - } - - if (renote.visibility === 'followers' && renote.userId !== me.id) { - // 他人のfollowers noteはreject - throw new ApiError(meta.errors.cannotRenoteDueToVisibility); - } else if (renote.visibility === 'specified') { - // specified / direct noteはreject - throw new ApiError(meta.errors.cannotRenoteDueToVisibility); } if (renote.channelId && renote.channelId !== ps.channelId) { @@ -351,26 +326,9 @@ export default class extends Endpoint { // eslint- if (reply == null) { throw new ApiError(meta.errors.noSuchReplyTarget); - } else if (isRenote(reply) && !isQuote(reply)) { - throw new ApiError(meta.errors.cannotReplyToPureRenote); - } else if (!(await this.noteVisibilityService.checkNoteVisibilityAsync(reply, me)).accessible) { - throw new ApiError(meta.errors.cannotReplyToInvisibleNote); } else if (reply.visibility === 'specified' && ps.visibility !== 'specified') { throw new ApiError(meta.errors.cannotReplyToSpecifiedVisibilityNoteWithExtendedVisibility); } - - // Check blocking - if (reply.userId !== me.id) { - const blockExist = await this.blockingsRepository.exists({ - where: { - blockerId: reply.userId, - blockeeId: me.id, - }, - }); - if (blockExist) { - throw new ApiError(meta.errors.youHaveBeenBlocked); - } - } } if (ps.poll) { @@ -430,6 +388,14 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.quoteDisabledForUser); } else if (e.id === 'fd4cc33e-2a37-48dd-99cc-9b806eb2031a') { throw new ApiError(meta.errors.cannotReRenote); + } else if (e.id === 'b6352a84-e5cd-4b05-a26c-63437a6b98ba') { + throw new ApiError(meta.errors.youHaveBeenBlocked); + } else if (e.id === 'be9529e9-fe72-4de0-ae43-0b363c4938af') { + throw new ApiError(meta.errors.cannotRenoteDueToVisibility); + } else if (e.id === '3ac74a84-8fd5-4bb0-870f-01804f82ce15') { + throw new ApiError(meta.errors.cannotReplyToPureRenote); + } else if (e.id === 'b98980fa-3780-406c-a935-b6d0eeee10d1') { + throw new ApiError(meta.errors.cannotReplyToInvisibleNote); } } throw e; diff --git a/packages/backend/src/server/api/endpoints/notes/edit.ts b/packages/backend/src/server/api/endpoints/notes/edit.ts index 8a65e016d5..2794d7376a 100644 --- a/packages/backend/src/server/api/endpoints/notes/edit.ts +++ b/packages/backend/src/server/api/endpoints/notes/edit.ts @@ -347,11 +347,6 @@ export default class extends Endpoint { // eslint- } let renote: MiNote | null = null; - - if (ps.renoteId === ps.editId) { - throw new ApiError(meta.errors.cannotQuoteCurrentPost); - } - if (ps.renoteId != null) { // Fetch renote to note renote = await this.notesRepository.findOneBy({ id: ps.renoteId }); @@ -360,27 +355,6 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.noSuchRenoteTarget); } - // Check blocking - if (renote.userId !== me.id) { - const blockExist = await this.blockingsRepository.exists({ - where: { - blockerId: renote.userId, - blockeeId: me.id, - }, - }); - if (blockExist) { - throw new ApiError(meta.errors.youHaveBeenBlocked); - } - } - - if (renote.visibility === 'followers' && renote.userId !== me.id) { - // 他人のfollowers noteはreject - throw new ApiError(meta.errors.cannotRenoteDueToVisibility); - } else if (renote.visibility === 'specified') { - // specified / direct noteはreject - throw new ApiError(meta.errors.cannotRenoteDueToVisibility); - } - if (renote.channelId && renote.channelId !== ps.channelId) { // チャンネルのノートに対しリノート要求がきたとき、チャンネル外へのリノート可否をチェック // リノートのユースケースのうち、チャンネル内→チャンネル外は少数だと考えられるため、JOINはせず必要な時に都度取得する @@ -402,26 +376,9 @@ export default class extends Endpoint { // eslint- if (reply == null) { throw new ApiError(meta.errors.noSuchReplyTarget); - } else if (isRenote(reply) && !isQuote(reply)) { - throw new ApiError(meta.errors.cannotReplyToPureRenote); - } else if (!(await this.noteVisibilityService.checkNoteVisibilityAsync(reply, me)).accessible) { - throw new ApiError(meta.errors.cannotReplyToInvisibleNote); } else if (reply.visibility === 'specified' && ps.visibility !== 'specified') { throw new ApiError(meta.errors.cannotReplyToSpecifiedVisibilityNoteWithExtendedVisibility); } - - // Check blocking - if (reply.userId !== me.id) { - const blockExist = await this.blockingsRepository.exists({ - where: { - blockerId: reply.userId, - blockeeId: me.id, - }, - }); - if (blockExist) { - throw new ApiError(meta.errors.youHaveBeenBlocked); - } - } } if (ps.poll) { @@ -477,10 +434,20 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.containsTooManyMentions); } else if (e.id === '1c0ea108-d1e3-4e8e-aa3f-4d2487626153') { throw new ApiError(meta.errors.quoteDisabledForUser); + } else if (e.id === '33510210-8452-094c-6227-4a6c05d99f02') { + throw new ApiError(meta.errors.cannotQuoteCurrentPost); } else if (e.id === 'ea93b7c2-3d6c-4e10-946b-00d50b1a75cb') { throw new ApiError(meta.errors.cannotQuoteaQuoteOfCurrentPost); } else if (e.id === 'fd4cc33e-2a37-48dd-99cc-9b806eb2031a') { throw new ApiError(meta.errors.cannotReRenote); + } else if (e.id === 'b6352a84-e5cd-4b05-a26c-63437a6b98ba') { + throw new ApiError(meta.errors.youHaveBeenBlocked); + } else if (e.id === 'be9529e9-fe72-4de0-ae43-0b363c4938af') { + throw new ApiError(meta.errors.cannotRenoteDueToVisibility); + } else if (e.id === '3ac74a84-8fd5-4bb0-870f-01804f82ce15') { + throw new ApiError(meta.errors.cannotReplyToPureRenote); + } else if (e.id === 'b98980fa-3780-406c-a935-b6d0eeee10d1') { + throw new ApiError(meta.errors.cannotReplyToInvisibleNote); } } throw e; From e0f45c60c44cb72c47e2950e8e1abe834bc26e54 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Thu, 18 Sep 2025 14:25:12 -0400 Subject: [PATCH 6/8] regenerate locales --- locales/index.d.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/locales/index.d.ts b/locales/index.d.ts index 880ee4bf56..4d86c69384 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -12525,6 +12525,14 @@ export interface Locale extends ILocale { * Failed to load note */ "cannotLoadNote": string; + /** + * Please click [OK] to unsubscribe from announcement e-mails. + */ + "clickToUnsubscribe": string; + /** + * There was a problem unsubscribing. + */ + "unsubscribeError": string; "_flash": { /** * Flash Content Hidden From 43b83ad3bcd8540096afcc6def1979dbd3b67784 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Thu, 18 Sep 2025 14:25:28 -0400 Subject: [PATCH 7/8] regenerate misskey-js --- packages/misskey-js/etc/misskey-js.api.md | 8 +++ .../misskey-js/src/autogen/apiClientJSDoc.ts | 11 ++++ packages/misskey-js/src/autogen/endpoint.ts | 3 + packages/misskey-js/src/autogen/entities.ts | 2 + packages/misskey-js/src/autogen/types.ts | 66 +++++++++++++++++++ 5 files changed, 90 insertions(+) diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index 040e9429f0..25ad5d4788 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -347,6 +347,12 @@ type AdminResetPasswordResponse = operations['admin___reset-password']['response // @public (undocumented) type AdminResolveAbuseUserReportRequest = operations['admin___resolve-abuse-user-report']['requestBody']['content']['application/json']; +// @public (undocumented) +type AdminRolesAnnotateConditionRequest = operations['admin___roles___annotate-condition']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminRolesAnnotateConditionResponse = operations['admin___roles___annotate-condition']['responses']['200']['content']['application/json']; + // @public (undocumented) type AdminRolesAssignRequest = operations['admin___roles___assign']['requestBody']['content']['application/json']; @@ -1622,6 +1628,8 @@ declare namespace entities { AdminResetPasswordRequest, AdminResetPasswordResponse, AdminResolveAbuseUserReportRequest, + AdminRolesAnnotateConditionRequest, + AdminRolesAnnotateConditionResponse, AdminRolesAssignRequest, AdminRolesCloneRequest, AdminRolesCloneResponse, diff --git a/packages/misskey-js/src/autogen/apiClientJSDoc.ts b/packages/misskey-js/src/autogen/apiClientJSDoc.ts index 0e061c8e06..61f2afb90c 100644 --- a/packages/misskey-js/src/autogen/apiClientJSDoc.ts +++ b/packages/misskey-js/src/autogen/apiClientJSDoc.ts @@ -867,6 +867,17 @@ declare module '../api.js' { credential?: string | null, ): Promise>; + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:admin:roles* + */ + request( + endpoint: E, + params: P, + credential?: string | null, + ): Promise>; + /** * No description provided. * diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts index 5bdaa58a6f..32a5013ab6 100644 --- a/packages/misskey-js/src/autogen/endpoint.ts +++ b/packages/misskey-js/src/autogen/endpoint.ts @@ -103,6 +103,8 @@ import type { AdminResetPasswordRequest, AdminResetPasswordResponse, AdminResolveAbuseUserReportRequest, + AdminRolesAnnotateConditionRequest, + AdminRolesAnnotateConditionResponse, AdminRolesAssignRequest, AdminRolesCloneRequest, AdminRolesCloneResponse, @@ -748,6 +750,7 @@ export type Endpoints = { 'admin/relays/remove': { req: AdminRelaysRemoveRequest; res: EmptyResponse }; 'admin/reset-password': { req: AdminResetPasswordRequest; res: AdminResetPasswordResponse }; 'admin/resolve-abuse-user-report': { req: AdminResolveAbuseUserReportRequest; res: EmptyResponse }; + 'admin/roles/annotate-condition': { req: AdminRolesAnnotateConditionRequest; res: AdminRolesAnnotateConditionResponse }; 'admin/roles/assign': { req: AdminRolesAssignRequest; res: EmptyResponse }; 'admin/roles/clone': { req: AdminRolesCloneRequest; res: AdminRolesCloneResponse }; 'admin/roles/create': { req: AdminRolesCreateRequest; res: AdminRolesCreateResponse }; diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts index 4ad9c9afbb..9254758109 100644 --- a/packages/misskey-js/src/autogen/entities.ts +++ b/packages/misskey-js/src/autogen/entities.ts @@ -106,6 +106,8 @@ export type AdminRelaysRemoveRequest = operations['admin___relays___remove']['re export type AdminResetPasswordRequest = operations['admin___reset-password']['requestBody']['content']['application/json']; export type AdminResetPasswordResponse = operations['admin___reset-password']['responses']['200']['content']['application/json']; export type AdminResolveAbuseUserReportRequest = operations['admin___resolve-abuse-user-report']['requestBody']['content']['application/json']; +export type AdminRolesAnnotateConditionRequest = operations['admin___roles___annotate-condition']['requestBody']['content']['application/json']; +export type AdminRolesAnnotateConditionResponse = operations['admin___roles___annotate-condition']['responses']['200']['content']['application/json']; export type AdminRolesAssignRequest = operations['admin___roles___assign']['requestBody']['content']['application/json']; export type AdminRolesCloneRequest = operations['admin___roles___clone']['requestBody']['content']['application/json']; export type AdminRolesCloneResponse = operations['admin___roles___clone']['responses']['200']['content']['application/json']; diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 03304eb69f..0551bdc44f 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -720,6 +720,15 @@ export type paths = { */ post: operations['admin___resolve-abuse-user-report']; }; + '/admin/roles/annotate-condition': { + /** + * admin/roles/annotate-condition + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:admin:roles* + */ + post: operations['admin___roles___annotate-condition']; + }; '/admin/roles/assign': { /** * admin/roles/assign @@ -10521,6 +10530,63 @@ export type operations = { }; }; }; + /** + * admin/roles/annotate-condition + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:admin:roles* + */ + 'admin___roles___annotate-condition': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + userId: string; + condFormula: Record; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': { + [key: string]: boolean; + }; + }; + }; + /** @description Client error */ + 400: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; /** * admin/roles/assign * @description No description provided. From 4c8ab73ecd803a40af57573d7175b8c3e849295c Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Wed, 8 Oct 2025 21:24:19 -0400 Subject: [PATCH 8/8] fix rebase errors --- .../backend/src/core/NoteCreateService.ts | 8 +++- packages/backend/src/core/NoteEditService.ts | 5 ++- .../src/core/entities/NoteEntityService.ts | 40 +++++++++---------- .../src/server/api/endpoints/notes/create.ts | 2 - .../src/server/api/endpoints/notes/edit.ts | 2 - 5 files changed, 29 insertions(+), 28 deletions(-) diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 59f9c32191..a168126a2f 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -57,6 +57,7 @@ import { IdentifiableError } from '@/misc/identifiable-error.js'; import { LatestNoteService } from '@/core/LatestNoteService.js'; import { CollapsedQueue } from '@/misc/collapsed-queue.js'; import { CacheService } from '@/core/CacheService.js'; +import { NoteVisibilityService } from '@/core/NoteVisibilityService.js'; import { isPureRenote } from '@/misc/is-renote.js'; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; @@ -224,6 +225,7 @@ export class NoteCreateService implements OnApplicationShutdown { private userBlockingService: UserBlockingService, private cacheService: CacheService, private latestNoteService: LatestNoteService, + private readonly noteVisibilityService: NoteVisibilityService, ) { this.updateNotesCountQueue = new CollapsedQueue(process.env.NODE_ENV !== 'test' ? 60 * 1000 * 5 : 0, this.collapseNotesCount, this.performUpdateNotesCount); } @@ -320,7 +322,8 @@ export class NoteCreateService implements OnApplicationShutdown { } // Check visibility - if (!await this.noteEntityService.isVisibleForMe(data.renote, user.id)) { + const visibilityCheck = await this.noteVisibilityService.checkNoteVisibilityAsync(data.renote, user.id); + if (!visibilityCheck.accessible) { throw new IdentifiableError('be9529e9-fe72-4de0-ae43-0b363c4938af', 'Cannot renote an invisible note'); } @@ -343,7 +346,8 @@ export class NoteCreateService implements OnApplicationShutdown { } // Check visibility - if (!await this.noteEntityService.isVisibleForMe(data.reply, user.id)) { + const visibilityCheck = await this.noteVisibilityService.checkNoteVisibilityAsync(data.reply, user.id); + if (!visibilityCheck.accessible) { throw new IdentifiableError('b98980fa-3780-406c-a935-b6d0eeee10d1', 'Cannot reply to an invisible note'); } diff --git a/packages/backend/src/core/NoteEditService.ts b/packages/backend/src/core/NoteEditService.ts index 7b51ce2022..af9538dc50 100644 --- a/packages/backend/src/core/NoteEditService.ts +++ b/packages/backend/src/core/NoteEditService.ts @@ -52,6 +52,7 @@ import { IdentifiableError } from '@/misc/identifiable-error.js'; import { LatestNoteService } from '@/core/LatestNoteService.js'; import { CollapsedQueue } from '@/misc/collapsed-queue.js'; import { NoteCreateService } from '@/core/NoteCreateService.js'; +import { NoteVisibilityService } from '@/core/NoteVisibilityService.js'; import { isPureRenote } from '@/misc/is-renote.js'; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention' | 'edited'; @@ -221,6 +222,7 @@ export class NoteEditService implements OnApplicationShutdown { private cacheService: CacheService, private latestNoteService: LatestNoteService, private noteCreateService: NoteCreateService, + private readonly noteVisibilityService: NoteVisibilityService, ) { this.updateNotesCountQueue = new CollapsedQueue(process.env.NODE_ENV !== 'test' ? 60 * 1000 * 5 : 0, this.collapseNotesCount, this.performUpdateNotesCount); } @@ -339,7 +341,8 @@ export class NoteEditService implements OnApplicationShutdown { } // Check visibility - if (!await this.noteEntityService.isVisibleForMe(data.renote, user.id)) { + const visibilityCheck = await this.noteVisibilityService.checkNoteVisibilityAsync(data.renote, user.id); + if (!visibilityCheck.accessible) { throw new IdentifiableError('be9529e9-fe72-4de0-ae43-0b363c4938af', 'Cannot renote an invisible note'); } diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts index 95b464c56f..2b3defb189 100644 --- a/packages/backend/src/core/entities/NoteEntityService.ts +++ b/packages/backend/src/core/entities/NoteEntityService.ts @@ -19,7 +19,6 @@ import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js'; import { QueryService } from '@/core/QueryService.js'; import type { Config } from '@/config.js'; import { NoteVisibilityService } from '@/core/NoteVisibilityService.js'; -import type { PopulatedNote } from '@/core/NoteVisibilityService.js'; import type { NoteVisibilityData } from '@/core/NoteVisibilityService.js'; import type { OnModuleInit } from '@nestjs/common'; import type { CacheService } from '../CacheService.js'; @@ -653,29 +652,28 @@ export class NoteEntityService implements OnModuleInit { processErrors: note.processErrors, } : {}), - reply: opts.recurseReply && note.replyId ? this.pack(note.reply ?? opts._hint_?.notes.get(note.replyId) ?? note.replyId, me, { - detail: false, - skipHide: opts.skipHide, - withReactionAndUserPairCache: opts.withReactionAndUserPairCache, - _hint_: options?._hint_, + reply: opts.recurseReply && note.replyId ? this.pack(note.reply ?? opts._hint_?.notes.get(note.replyId) ?? note.replyId, me, { + detail: false, + skipHide: opts.skipHide, + withReactionAndUserPairCache: opts.withReactionAndUserPairCache, + _hint_: options?._hint_, - // Don't silence target of self-reply, since the outer note will already be silenced. - bypassSilence: bypassSilence || note.userId === note.replyUserId, - }) : undefined, + // Don't silence target of self-reply, since the outer note will already be silenced. + bypassSilence: bypassSilence || note.userId === note.replyUserId, + }) : undefined, - // The renote target needs to be packed with the reply, but we *must not* recurse any further. - // Pass detail=false and recurseReply=true to make sure we only include the right data. - renote: opts.recurseRenote && note.renoteId ? this.pack(note.renote ?? opts._hint_?.notes.get(note.renoteId) ?? note.renoteId, me, { - detail: false, - recurseReply: true, - skipHide: opts.skipHide, - withReactionAndUserPairCache: opts.withReactionAndUserPairCache, - _hint_: options?._hint_, + // The renote target needs to be packed with the reply, but we *must not* recurse any further. + // Pass detail=false and recurseReply=true to make sure we only include the right data. + renote: opts.recurseRenote && note.renoteId ? this.pack(note.renote ?? opts._hint_?.notes.get(note.renoteId) ?? note.renoteId, me, { + detail: false, + recurseReply: true, + skipHide: opts.skipHide, + withReactionAndUserPairCache: opts.withReactionAndUserPairCache, + _hint_: options?._hint_, - // Don't silence target of self-renote, since the outer note will already be silenced. - bypassSilence: bypassSilence || note.userId === note.renoteUserId, - }) : undefined, - } : {}), + // Don't silence target of self-renote, since the outer note will already be silenced. + bypassSilence: bypassSilence || note.userId === note.renoteUserId, + }) : undefined, }); this.noteVisibilityService.syncVisibility(packed); diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index cf61a297b0..4aece5353e 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -18,7 +18,6 @@ import { NoteCreateService } from '@/core/NoteCreateService.js'; import { DI } from '@/di-symbols.js'; import { isQuote, isRenote } from '@/misc/is-renote.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; -import { NoteVisibilityService } from '@/core/NoteVisibilityService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -262,7 +261,6 @@ export default class extends Endpoint { // eslint- private noteEntityService: NoteEntityService, private noteCreateService: NoteCreateService, - private readonly noteVisibilityService: NoteVisibilityService, ) { super(meta, paramDef, async (ps, me) => { if (ps.text && ps.text.length > this.config.maxNoteLength) { diff --git a/packages/backend/src/server/api/endpoints/notes/edit.ts b/packages/backend/src/server/api/endpoints/notes/edit.ts index 2794d7376a..276f2672d0 100644 --- a/packages/backend/src/server/api/endpoints/notes/edit.ts +++ b/packages/backend/src/server/api/endpoints/notes/edit.ts @@ -17,7 +17,6 @@ import { NoteEditService } from '@/core/NoteEditService.js'; import { DI } from '@/di-symbols.js'; import { isQuote, isRenote } from '@/misc/is-renote.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; -import { NoteVisibilityService } from '@/core/NoteVisibilityService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -312,7 +311,6 @@ export default class extends Endpoint { // eslint- private noteEntityService: NoteEntityService, private noteEditService: NoteEditService, - private readonly noteVisibilityService: NoteVisibilityService, ) { super(meta, paramDef, async (ps, me) => { if (ps.text && ps.text.length > this.config.maxNoteLength) {