This commit is contained in:
おさむのひと 2026-06-23 14:48:30 +09:00 committed by GitHub
commit 5bad514910
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 83 additions and 32 deletions

View file

@ -58,6 +58,11 @@ import { CollapsedQueue } from '@/misc/collapsed-queue.js';
import { CacheService } from '@/core/CacheService.js';
import { isQuote, isRenote } from '@/misc/is-renote.js';
// TODO: 他のUUIDも定数化が揃った段階で core/errors/note.ts 等に切り出す
export const NOTE_CREATE_ERRORS = {
PROHIBITED_WORDS: { id: '689ee33f-f97c-479a-ac49-1b9f8140af99' },
} as const;
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
class NotificationManager {
@ -485,7 +490,7 @@ export class NoteCreateService implements OnApplicationShutdown {
}, this.meta.prohibitedWords);
if (hasProhibitedWords) {
throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', 'Note contains prohibited words');
throw new IdentifiableError(NOTE_CREATE_ERRORS.PROHIBITED_WORDS.id, 'Note contains prohibited words');
}
const inSilencedInstance = this.utilityService.isSilencedHost(this.meta.silencedHosts, user.host);

View file

@ -22,7 +22,7 @@ import { bindThis } from '@/decorators.js';
import { LoggerService } from '@/core/LoggerService.js';
import type Logger from '@/logger.js';
import { SystemAccountService } from '@/core/SystemAccountService.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import { AP_RESOLVER_ERRORS, apResolverErr } from './errors.js';
import type { ICollection, IObject, IOrderedCollection } from './type.js';
import { isCollectionOrOrderedCollection } from './type.js';
import { ApDbResolverService } from './ApDbResolverService.js';
@ -91,7 +91,7 @@ export class Resolver {
if (isCollectionOrOrderedCollection(collection)) {
return collection;
} else {
throw new IdentifiableError('f100eccf-f347-43fb-9b45-96a0831fb635', `unrecognized collection type: ${collection.type}`);
throw apResolverErr(AP_RESOLVER_ERRORS.UNRECOGNIZED_COLLECTION_TYPE, `unrecognized collection type: ${collection.type}`);
}
}
@ -105,15 +105,15 @@ export class Resolver {
// URLs with fragment parts cannot be resolved correctly because
// the fragment part does not get transmitted over HTTP(S).
// Avoid strange behaviour by not trying to resolve these at all.
throw new IdentifiableError('b94fd5b1-0e3b-4678-9df2-dad4cd515ab2', `cannot resolve URL with fragment: ${value}`);
throw apResolverErr(AP_RESOLVER_ERRORS.URL_WITH_FRAGMENT, `cannot resolve URL with fragment: ${value}`);
}
if (this.history.has(value)) {
throw new IdentifiableError('0dc86cf6-7cd6-4e56-b1e6-5903d62d7ea5', 'cannot resolve already resolved one');
throw apResolverErr(AP_RESOLVER_ERRORS.ALREADY_RESOLVED, 'cannot resolve already resolved one');
}
if (this.history.size > this.recursionLimit) {
throw new IdentifiableError('d592da9f-822f-4d91-83d7-4ceefabcf3d2', `hit recursion limit: ${this.utilityService.extractDbHost(value)}`);
throw apResolverErr(AP_RESOLVER_ERRORS.RECURSION_LIMIT, `hit recursion limit: ${this.utilityService.extractDbHost(value)}`);
}
this.history.add(value);
@ -124,7 +124,7 @@ export class Resolver {
}
if (!this.utilityService.isFederationAllowedHost(host)) {
throw new IdentifiableError('09d79f9e-64f1-4316-9cfa-e75c4d091574', 'Instance is blocked');
throw apResolverErr(AP_RESOLVER_ERRORS.INSTANCE_BLOCKED, 'Instance is blocked');
}
if (this.meta.signToActivityPubGet && !this.user) {
@ -140,7 +140,7 @@ export class Resolver {
!(object['@context'] as unknown[]).includes('https://www.w3.org/ns/activitystreams') :
object['@context'] !== 'https://www.w3.org/ns/activitystreams'
) {
throw new IdentifiableError('72180409-793c-4973-868e-5a118eb5519b', 'invalid response');
throw apResolverErr(AP_RESOLVER_ERRORS.INVALID_RESPONSE, 'invalid response');
}
return object;
@ -149,7 +149,7 @@ export class Resolver {
@bindThis
private resolveLocal(url: string): Promise<IObject> {
const parsed = this.apDbResolverService.parseUri(url);
if (!parsed.local) throw new IdentifiableError('02b40cd0-fa92-4b0c-acc9-fb2ada952ab8', 'resolveLocal: not local');
if (!parsed.local) throw apResolverErr(AP_RESOLVER_ERRORS.NOT_LOCAL, 'resolveLocal: not local');
switch (parsed.type) {
case 'notes':
@ -178,7 +178,7 @@ export class Resolver {
case 'follows':
return this.followRequestsRepository.findOneBy({ id: parsed.id })
.then(async followRequest => {
if (followRequest == null) throw new IdentifiableError('a9d946e5-d276-47f8-95fb-f04230289bb0', 'resolveLocal: invalid follow request ID');
if (followRequest == null) throw apResolverErr(AP_RESOLVER_ERRORS.INVALID_FOLLOW_REQUEST_ID, 'resolveLocal: invalid follow request ID');
const [follower, followee] = await Promise.all([
this.usersRepository.findOneBy({
id: followRequest.followerId,
@ -190,12 +190,12 @@ export class Resolver {
}),
]);
if (follower == null || followee == null) {
throw new IdentifiableError('06ae3170-1796-4d93-a697-2611ea6d83b6', 'resolveLocal: follower or followee does not exist');
throw apResolverErr(AP_RESOLVER_ERRORS.FOLLOWER_OR_FOLLOWEE_NOT_FOUND, 'resolveLocal: follower or followee does not exist');
}
return this.apRendererService.addContext(this.apRendererService.renderFollow(follower as MiLocalUser | MiRemoteUser, followee as MiLocalUser | MiRemoteUser, url));
});
default:
throw new IdentifiableError('7a5d2fc0-94bc-4db6-b8b8-1bf24a2e23d0', `resolveLocal: type ${parsed.type} unhandled`);
throw apResolverErr(AP_RESOLVER_ERRORS.UNHANDLED_TYPE, `resolveLocal: type ${parsed.type} unhandled`);
}
}
}

View file

@ -0,0 +1,40 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { IdentifiableError } from '@/misc/identifiable-error.js';
type ApErrorDef = { readonly id: string };
function mkErr(def: ApErrorDef, message?: string): IdentifiableError {
return new IdentifiableError(def.id, message);
}
// Resolver
export const AP_RESOLVER_ERRORS = {
UNRECOGNIZED_COLLECTION_TYPE: { id: 'f100eccf-f347-43fb-9b45-96a0831fb635' },
URL_WITH_FRAGMENT: { id: 'b94fd5b1-0e3b-4678-9df2-dad4cd515ab2' },
ALREADY_RESOLVED: { id: '0dc86cf6-7cd6-4e56-b1e6-5903d62d7ea5' },
RECURSION_LIMIT: { id: 'd592da9f-822f-4d91-83d7-4ceefabcf3d2' },
INSTANCE_BLOCKED: { id: '09d79f9e-64f1-4316-9cfa-e75c4d091574' },
INVALID_RESPONSE: { id: '72180409-793c-4973-868e-5a118eb5519b' },
NOT_LOCAL: { id: '02b40cd0-fa92-4b0c-acc9-fb2ada952ab8' },
INVALID_FOLLOW_REQUEST_ID: { id: 'a9d946e5-d276-47f8-95fb-f04230289bb0' },
FOLLOWER_OR_FOLLOWEE_NOT_FOUND: { id: '06ae3170-1796-4d93-a697-2611ea6d83b6' },
UNHANDLED_TYPE: { id: '7a5d2fc0-94bc-4db6-b8b8-1bf24a2e23d0' },
} as const satisfies Record<string, ApErrorDef>;
// ApNoteService
export const AP_NOTE_ERRORS = {
INVALID_NOTE: { id: 'd450b8a9-48e4-4dab-ae36-f4db763fda7c' },
ACTOR_SUSPENDED: { id: '85ab9bd7-3a41-4530-959d-f07073900109' },
} as const satisfies Record<string, ApErrorDef>;
export function apResolverErr(def: typeof AP_RESOLVER_ERRORS[keyof typeof AP_RESOLVER_ERRORS], message?: string): IdentifiableError {
return mkErr(def, message);
}
export function apNoteErr(def: typeof AP_NOTE_ERRORS[keyof typeof AP_NOTE_ERRORS], message?: string): IdentifiableError {
return mkErr(def, message);
}

View file

@ -24,6 +24,8 @@ import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js';
import { checkHttps } from '@/misc/check-https.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import { NOTE_CREATE_ERRORS } from '@/core/NoteCreateService.js';
import { AP_NOTE_ERRORS, apNoteErr } from '../errors.js';
import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType } from '../type.js';
import { ApLoggerService } from '../ApLoggerService.js';
import { ApMfmService } from '../ApMfmService.js';
@ -85,27 +87,27 @@ export class ApNoteService {
const apType = getApType(object);
if (apType == null || !validPost.includes(apType)) {
return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: invalid object type ${apType ?? 'undefined'}`);
return apNoteErr(AP_NOTE_ERRORS.INVALID_NOTE, `invalid Note: invalid object type ${apType ?? 'undefined'}`);
}
if (object.id && this.utilityService.extractDbHost(object.id) !== expectHost) {
return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: id has different host. expected: ${expectHost}, actual: ${this.utilityService.extractDbHost(object.id)}`);
return apNoteErr(AP_NOTE_ERRORS.INVALID_NOTE, `invalid Note: id has different host. expected: ${expectHost}, actual: ${this.utilityService.extractDbHost(object.id)}`);
}
const actualHost = object.attributedTo && this.utilityService.extractDbHost(getOneApId(object.attributedTo));
if (object.attributedTo && actualHost !== expectHost) {
return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${actualHost}`);
return apNoteErr(AP_NOTE_ERRORS.INVALID_NOTE, `invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${actualHost}`);
}
if (object.published && !this.idService.isSafeT(new Date(object.published).valueOf())) {
return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', 'invalid Note: published timestamp is malformed');
return apNoteErr(AP_NOTE_ERRORS.INVALID_NOTE, 'invalid Note: published timestamp is malformed');
}
if (actor) {
const attribution = (object.attributedTo) ? getOneApId(object.attributedTo) : actor.uri;
if (attribution !== actor.uri) {
return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: attribution does not match the actor that send it. attribution: ${attribution}, actor: ${actor.uri}`);
return apNoteErr(AP_NOTE_ERRORS.INVALID_NOTE, `invalid Note: attribution does not match the actor that send it. attribution: ${attribution}, actor: ${actor.uri}`);
}
}
@ -174,7 +176,7 @@ export class ApNoteService {
// eslint-disable-next-line no-param-reassign
actor ??= await this.apPersonService.fetchPerson(uri) as MiRemoteUser | undefined;
if (actor && actor.isSuspended) {
throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', 'actor has been suspended');
throw apNoteErr(AP_NOTE_ERRORS.ACTOR_SUSPENDED, 'actor has been suspended');
}
const apMentionRawCount = new Set(this.apMentionService.extractApMentionObjects(note.tag).map(x => x.href)).size;
@ -202,7 +204,9 @@ export class ApNoteService {
*/
const hasProhibitedWords = this.noteCreateService.checkProhibitedWordsContain({ cw, text, pollChoices: poll?.choices });
if (hasProhibitedWords) {
throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', 'Note contains prohibited words');
// NoteCreateService側でも同じエラーコードを使用しているため、こちらでも同じIDを使用する.
// FIXME: checkProhibitedWordsContainの中にthrowを押し込んでもいいかもしれない
throw new IdentifiableError(NOTE_CREATE_ERRORS.PROHIBITED_WORDS.id, 'Note contains prohibited words');
}
//#endregion
@ -211,7 +215,7 @@ export class ApNoteService {
// 解決した投稿者が凍結されていたらスキップ
if (actor.isSuspended) {
throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', 'actor has been suspended');
throw apNoteErr(AP_NOTE_ERRORS.ACTOR_SUSPENDED, 'actor has been suspended');
}
const noteAudience = await this.apAudienceService.parseAudience(actor, note.to, note.cc, resolver);
@ -414,7 +418,7 @@ export class ApNoteService {
publicUrl: tag.icon.url,
updatedAt: new Date(),
// _misskey_license が存在しなければ `null`
license: (tag._misskey_license?.freeText ?? null)
license: (tag._misskey_license?.freeText ?? null),
});
const emoji = await this.emojisRepository.findOneBy({ host, name });
@ -437,7 +441,7 @@ export class ApNoteService {
updatedAt: new Date(),
aliases: [],
// _misskey_license が存在しなければ `null`
license: (tag._misskey_license?.freeText ?? null)
license: (tag._misskey_license?.freeText ?? null),
});
}));
}

View file

@ -19,6 +19,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import { AP_RESOLVER_ERRORS } from '@/core/activitypub/errors.js';
import { FetchAllowSoftFailMask } from '@/core/activitypub/misc/check-against-url.js';
import { ApiError } from '../../error.js';
@ -154,23 +155,23 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (err instanceof IdentifiableError) {
switch (err.id) {
// resolve
case 'b94fd5b1-0e3b-4678-9df2-dad4cd515ab2':
case AP_RESOLVER_ERRORS.URL_WITH_FRAGMENT.id:
throw new ApiError(meta.errors.uriInvalid);
case '0dc86cf6-7cd6-4e56-b1e6-5903d62d7ea5':
case 'd592da9f-822f-4d91-83d7-4ceefabcf3d2':
case AP_RESOLVER_ERRORS.ALREADY_RESOLVED.id:
case AP_RESOLVER_ERRORS.RECURSION_LIMIT.id:
throw new ApiError(meta.errors.requestFailed);
case '09d79f9e-64f1-4316-9cfa-e75c4d091574':
case AP_RESOLVER_ERRORS.INSTANCE_BLOCKED.id:
throw new ApiError(meta.errors.federationNotAllowed);
case '72180409-793c-4973-868e-5a118eb5519b':
case AP_RESOLVER_ERRORS.INVALID_RESPONSE.id:
throw new ApiError(meta.errors.responseInvalid);
// resolveLocal
case '02b40cd0-fa92-4b0c-acc9-fb2ada952ab8':
case AP_RESOLVER_ERRORS.NOT_LOCAL.id:
throw new ApiError(meta.errors.uriInvalid);
case 'a9d946e5-d276-47f8-95fb-f04230289bb0':
case '06ae3170-1796-4d93-a697-2611ea6d83b6':
case AP_RESOLVER_ERRORS.INVALID_FOLLOW_REQUEST_ID.id:
case AP_RESOLVER_ERRORS.FOLLOWER_OR_FOLLOWEE_NOT_FOUND.id:
throw new ApiError(meta.errors.noSuchObject);
case '7a5d2fc0-94bc-4db6-b8b8-1bf24a2e23d0':
case AP_RESOLVER_ERRORS.UNHANDLED_TYPE.id:
throw new ApiError(meta.errors.responseInvalid);
}
}

View file

@ -11,6 +11,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { NoteCreateService } from '@/core/NoteCreateService.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import { NOTE_CREATE_ERRORS } from '@/core/NoteCreateService.js';
import { ApiError } from '../../error.js';
export const meta = {
@ -249,7 +250,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
} catch (err) {
// TODO: 他のErrorもここでキャッチしてエラーメッセージを当てるようにしたい
if (err instanceof IdentifiableError) {
if (err.id === '689ee33f-f97c-479a-ac49-1b9f8140af99') {
if (err.id === NOTE_CREATE_ERRORS.PROHIBITED_WORDS.id) {
throw new ApiError(meta.errors.containsProhibitedWords);
} else if (err.id === '9f466dab-c856-48cd-9e65-ff90ff750580') {
throw new ApiError(meta.errors.containsTooManyMentions);