mirror of
				https://codeberg.org/yeentown/barkey.git
				synced 2025-11-04 07:24:13 +00:00 
			
		
		
		
	Merge branch 'develop' into feature/2024.10
This commit is contained in:
		
						commit
						66dd12fb8a
					
				
					 15 changed files with 167 additions and 122 deletions
				
			
		| 
						 | 
				
			
			@ -5,6 +5,7 @@
 | 
			
		|||
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import { IsNull, Not } from 'typeorm';
 | 
			
		||||
import { UnrecoverableError } from 'bullmq';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import type { FollowingsRepository } from '@/models/_.js';
 | 
			
		||||
import type { MiLocalUser, MiRemoteUser, MiUser } from '@/models/User.js';
 | 
			
		||||
| 
						 | 
				
			
			@ -128,7 +129,7 @@ class DeliverManager {
 | 
			
		|||
 | 
			
		||||
			for (const following of followers) {
 | 
			
		||||
				const inbox = following.followerSharedInbox ?? following.followerInbox;
 | 
			
		||||
				if (inbox === null) throw new Error('inbox is null');
 | 
			
		||||
				if (inbox === null) throw new UnrecoverableError(`inbox is null: following ${following.id}`);
 | 
			
		||||
				inboxes.set(inbox, following.followerSharedInbox != null);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -32,7 +32,7 @@ import { AbuseReportService } from '@/core/AbuseReportService.js';
 | 
			
		|||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
 | 
			
		||||
import { fromTuple } from '@/misc/from-tuple.js';
 | 
			
		||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
 | 
			
		||||
import { getApHrefNullable, getApId, getApIds, getApType, isAccept, isActor, isAdd, isAnnounce, isApObject, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js';
 | 
			
		||||
import { getApHrefNullable, getApId, getApIds, getApType, getNullableApId, isAccept, isActor, isAdd, isAnnounce, isApObject, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js';
 | 
			
		||||
import { ApNoteService } from './models/ApNoteService.js';
 | 
			
		||||
import { ApLoggerService } from './ApLoggerService.js';
 | 
			
		||||
import { ApDbResolverService } from './ApDbResolverService.js';
 | 
			
		||||
| 
						 | 
				
			
			@ -342,7 +342,7 @@ export class ApInboxService {
 | 
			
		|||
				// 対象が4xxならスキップ
 | 
			
		||||
				if (err instanceof StatusError) {
 | 
			
		||||
					if (!err.isRetryable) {
 | 
			
		||||
						return `Ignored announce target ${target.id} - ${err.statusCode}`;
 | 
			
		||||
						return `skip: ignored announce target ${target.id} - ${err.statusCode}`;
 | 
			
		||||
					}
 | 
			
		||||
					return `Error in announce target ${target.id} - ${err.statusCode}`;
 | 
			
		||||
				}
 | 
			
		||||
| 
						 | 
				
			
			@ -430,7 +430,7 @@ export class ApInboxService {
 | 
			
		|||
		if (isPost(object)) {
 | 
			
		||||
			await this.createNote(resolver, actor, object, false);
 | 
			
		||||
		} else {
 | 
			
		||||
			return `Unknown type: ${getApType(object)}`;
 | 
			
		||||
			return `skip: Unsupported type for Create: ${getApType(object)} ${getNullableApId(object)}`;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -462,7 +462,7 @@ export class ApInboxService {
 | 
			
		|||
			return 'ok';
 | 
			
		||||
		} catch (err) {
 | 
			
		||||
			if (err instanceof StatusError && !err.isRetryable) {
 | 
			
		||||
				return `skip ${err.statusCode}`;
 | 
			
		||||
				return `skip: ${err.statusCode}`;
 | 
			
		||||
			} else {
 | 
			
		||||
				throw err;
 | 
			
		||||
			}
 | 
			
		||||
| 
						 | 
				
			
			@ -549,7 +549,7 @@ export class ApInboxService {
 | 
			
		|||
			const note = await this.apDbResolverService.getNoteFromApId(uri);
 | 
			
		||||
 | 
			
		||||
			if (note == null) {
 | 
			
		||||
				return 'message not found';
 | 
			
		||||
				return 'skip: ignoring deleted note on both ends';
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (note.userId !== actor.id) {
 | 
			
		||||
| 
						 | 
				
			
			@ -688,7 +688,7 @@ export class ApInboxService {
 | 
			
		|||
		if (isAnnounce(object)) return await this.undoAnnounce(actor, object);
 | 
			
		||||
		if (isAccept(object)) return await this.undoAccept(actor, object);
 | 
			
		||||
 | 
			
		||||
		return `skip: unknown object type ${getApType(object)}`;
 | 
			
		||||
		return `skip: unknown activity type ${getApType(object)}`;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
| 
						 | 
				
			
			@ -822,7 +822,7 @@ export class ApInboxService {
 | 
			
		|||
				return await this.create(actor, activity, resolver);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			await this.apQuestionService.updateQuestion(object, actor, resolver).catch(err => console.error(err));
 | 
			
		||||
			await this.apQuestionService.updateQuestion(object, actor, resolver);
 | 
			
		||||
			return 'ok: Question updated';
 | 
			
		||||
		} else if (isPost(object)) {
 | 
			
		||||
			// If we get an Update(Note) for a note that doesn't exist, then create it instead
 | 
			
		||||
| 
						 | 
				
			
			@ -830,10 +830,10 @@ export class ApInboxService {
 | 
			
		|||
				return await this.create(actor, activity, resolver);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			await this.apNoteService.updateNote(object, actor, resolver).catch(err => console.error(err));
 | 
			
		||||
			await this.apNoteService.updateNote(object, actor, resolver);
 | 
			
		||||
			return 'ok: Note updated';
 | 
			
		||||
		} else {
 | 
			
		||||
			return `skip: Unknown type: ${getApType(object)}`;
 | 
			
		||||
			return `skip: Unsupported type for Update: ${getApType(object)} ${getNullableApId(object)}`;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,6 +7,7 @@ import { createPublicKey, randomUUID } from 'node:crypto';
 | 
			
		|||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import { In } from 'typeorm';
 | 
			
		||||
import * as mfm from '@transfem-org/sfm-js';
 | 
			
		||||
import { UnrecoverableError } from 'bullmq';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import type { Config } from '@/config.js';
 | 
			
		||||
import type { MiPartialLocalUser, MiLocalUser, MiPartialRemoteUser, MiRemoteUser, MiUser } from '@/models/User.js';
 | 
			
		||||
| 
						 | 
				
			
			@ -30,6 +31,7 @@ import { IdService } from '@/core/IdService.js';
 | 
			
		|||
import { JsonLdService } from './JsonLdService.js';
 | 
			
		||||
import { ApMfmService } from './ApMfmService.js';
 | 
			
		||||
import { CONTEXT } from './misc/contexts.js';
 | 
			
		||||
import { getApId } from './type.js';
 | 
			
		||||
import type { IAccept, IActivity, IAdd, IAnnounce, IApDocument, IApEmoji, IApHashtag, IApImage, IApMention, IBlock, ICreate, IDelete, IFlag, IFollow, IKey, ILike, IMove, IObject, IPost, IQuestion, IReject, IRemove, ITombstone, IUndo, IUpdate } from './type.js';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
| 
						 | 
				
			
			@ -106,7 +108,7 @@ export class ApRendererService {
 | 
			
		|||
			to = [`${attributedTo}/followers`];
 | 
			
		||||
			cc = [];
 | 
			
		||||
		} else {
 | 
			
		||||
			throw new Error('renderAnnounce: cannot render non-public note');
 | 
			
		||||
			throw new UnrecoverableError(`renderAnnounce: cannot render non-public note: ${getApId(object)}`);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,6 +5,7 @@
 | 
			
		|||
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import { IsNull, Not } from 'typeorm';
 | 
			
		||||
import { UnrecoverableError } from 'bullmq';
 | 
			
		||||
import type { MiLocalUser, MiRemoteUser } from '@/models/User.js';
 | 
			
		||||
import { InstanceActorService } from '@/core/InstanceActorService.js';
 | 
			
		||||
import type { NotesRepository, PollsRepository, NoteReactionsRepository, UsersRepository, FollowRequestsRepository, MiMeta } from '@/models/_.js';
 | 
			
		||||
| 
						 | 
				
			
			@ -15,12 +16,12 @@ import { UtilityService } from '@/core/UtilityService.js';
 | 
			
		|||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
import { LoggerService } from '@/core/LoggerService.js';
 | 
			
		||||
import type Logger from '@/logger.js';
 | 
			
		||||
import { fromTuple } from '@/misc/from-tuple.js';
 | 
			
		||||
import { isCollectionOrOrderedCollection } from './type.js';
 | 
			
		||||
import { ApDbResolverService } from './ApDbResolverService.js';
 | 
			
		||||
import { ApRendererService } from './ApRendererService.js';
 | 
			
		||||
import { ApRequestService } from './ApRequestService.js';
 | 
			
		||||
import type { IObject, ICollection, IOrderedCollection } from './type.js';
 | 
			
		||||
import { fromTuple } from '@/misc/from-tuple.js';
 | 
			
		||||
 | 
			
		||||
export class Resolver {
 | 
			
		||||
	private history: Set<string>;
 | 
			
		||||
| 
						 | 
				
			
			@ -67,7 +68,7 @@ export class Resolver {
 | 
			
		|||
		if (isCollectionOrOrderedCollection(collection)) {
 | 
			
		||||
			return collection;
 | 
			
		||||
		} else {
 | 
			
		||||
			throw new Error(`unrecognized collection type: ${collection.type}`);
 | 
			
		||||
			throw new UnrecoverableError(`unrecognized collection type: ${collection.type}`);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -84,15 +85,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 Error(`cannot resolve URL with fragment: ${value}`);
 | 
			
		||||
			throw new UnrecoverableError(`cannot resolve URL with fragment: ${value}`);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (this.history.has(value)) {
 | 
			
		||||
			throw new Error('cannot resolve already resolved one');
 | 
			
		||||
			throw new Error(`cannot resolve already resolved URL: ${value}`);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (this.history.size > this.recursionLimit) {
 | 
			
		||||
			throw new Error(`hit recursion limit: ${this.utilityService.extractDbHost(value)}`);
 | 
			
		||||
			throw new Error(`hit recursion limit: ${value}`);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		this.history.add(value);
 | 
			
		||||
| 
						 | 
				
			
			@ -103,7 +104,7 @@ export class Resolver {
 | 
			
		|||
		}
 | 
			
		||||
 | 
			
		||||
		if (!this.utilityService.isFederationAllowedHost(host)) {
 | 
			
		||||
			throw new Error('Instance is blocked');
 | 
			
		||||
			throw new UnrecoverableError(`instance is blocked: ${value}`);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (this.config.signToActivityPubGet && !this.user) {
 | 
			
		||||
| 
						 | 
				
			
			@ -119,7 +120,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 Error('invalid response');
 | 
			
		||||
			throw new UnrecoverableError(`invalid AP object ${value}: does not have ActivityStreams context`);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// HttpRequestService / ApRequestService have already checked that
 | 
			
		||||
| 
						 | 
				
			
			@ -127,11 +128,11 @@ export class Resolver {
 | 
			
		|||
		// object after redirects; here we double-check that no redirects
 | 
			
		||||
		// bounced between hosts
 | 
			
		||||
		if (object.id == null) {
 | 
			
		||||
			throw new Error('invalid AP object: missing id');
 | 
			
		||||
			throw new UnrecoverableError(`invalid AP object ${value}: missing id`);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (this.utilityService.punyHost(object.id) !== this.utilityService.punyHost(value)) {
 | 
			
		||||
			throw new Error(`invalid AP object ${value}: id ${object.id} has different host`);
 | 
			
		||||
			throw new UnrecoverableError(`invalid AP object ${value}: id ${object.id} has different host`);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return object;
 | 
			
		||||
| 
						 | 
				
			
			@ -140,7 +141,7 @@ export class Resolver {
 | 
			
		|||
	@bindThis
 | 
			
		||||
	private resolveLocal(url: string): Promise<IObject> {
 | 
			
		||||
		const parsed = this.apDbResolverService.parseUri(url);
 | 
			
		||||
		if (!parsed.local) throw new Error('resolveLocal: not local');
 | 
			
		||||
		if (!parsed.local) throw new UnrecoverableError(`resolveLocal - not a local URL: ${url}`);
 | 
			
		||||
 | 
			
		||||
		switch (parsed.type) {
 | 
			
		||||
			case 'notes':
 | 
			
		||||
| 
						 | 
				
			
			@ -169,7 +170,7 @@ export class Resolver {
 | 
			
		|||
			case 'follows':
 | 
			
		||||
				return this.followRequestsRepository.findOneBy({ id: parsed.id })
 | 
			
		||||
					.then(async followRequest => {
 | 
			
		||||
						if (followRequest == null) throw new Error('resolveLocal: invalid follow request ID');
 | 
			
		||||
						if (followRequest == null) throw new UnrecoverableError(`resolveLocal - invalid follow request ID ${parsed.id}: ${url}`);
 | 
			
		||||
						const [follower, followee] = await Promise.all([
 | 
			
		||||
							this.usersRepository.findOneBy({
 | 
			
		||||
								id: followRequest.followerId,
 | 
			
		||||
| 
						 | 
				
			
			@ -181,12 +182,12 @@ export class Resolver {
 | 
			
		|||
							}),
 | 
			
		||||
						]);
 | 
			
		||||
						if (follower == null || followee == null) {
 | 
			
		||||
							throw new Error('resolveLocal: follower or followee does not exist');
 | 
			
		||||
							throw new Error(`resolveLocal - follower or followee does not exist: ${url}`);
 | 
			
		||||
						}
 | 
			
		||||
						return this.apRendererService.addContext(this.apRendererService.renderFollow(follower as MiLocalUser | MiRemoteUser, followee as MiLocalUser | MiRemoteUser, url));
 | 
			
		||||
					});
 | 
			
		||||
			default:
 | 
			
		||||
				throw new Error(`resolveLocal: type ${parsed.type} unhandled`);
 | 
			
		||||
				throw new UnrecoverableError(`resolveLocal: type ${parsed.type} unhandled: ${url}`);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,6 +5,7 @@
 | 
			
		|||
 | 
			
		||||
import * as crypto from 'node:crypto';
 | 
			
		||||
import { Injectable } from '@nestjs/common';
 | 
			
		||||
import { UnrecoverableError } from 'bullmq';
 | 
			
		||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
 | 
			
		||||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
import { CONTEXT, PRELOADED_CONTEXTS } from './misc/contexts.js';
 | 
			
		||||
| 
						 | 
				
			
			@ -109,7 +110,7 @@ class JsonLd {
 | 
			
		|||
	@bindThis
 | 
			
		||||
	private getLoader() {
 | 
			
		||||
		return async (url: string): Promise<RemoteDocument> => {
 | 
			
		||||
			if (!/^https?:\/\//.test(url)) throw new Error(`Invalid URL ${url}`);
 | 
			
		||||
			if (!/^https?:\/\//.test(url)) throw new UnrecoverableError(`Invalid URL: ${url}`);
 | 
			
		||||
 | 
			
		||||
			if (this.preLoad) {
 | 
			
		||||
				if (url in PRELOADED_CONTEXTS) {
 | 
			
		||||
| 
						 | 
				
			
			@ -148,7 +149,7 @@ class JsonLd {
 | 
			
		|||
			},
 | 
			
		||||
		).then(res => {
 | 
			
		||||
			if (!res.ok) {
 | 
			
		||||
				throw new Error(`${res.status} ${res.statusText}`);
 | 
			
		||||
				throw new Error(`JSON-LD fetch failed with ${res.status} ${res.statusText}: ${url}`);
 | 
			
		||||
			} else {
 | 
			
		||||
				return res.json();
 | 
			
		||||
			}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,6 +3,7 @@
 | 
			
		|||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { UnrecoverableError } from 'bullmq';
 | 
			
		||||
import type { IObject } from '../type.js';
 | 
			
		||||
 | 
			
		||||
function getHrefsFrom(one: IObject | string | undefined | (IObject | string | undefined)[]): (string | undefined)[] {
 | 
			
		||||
| 
						 | 
				
			
			@ -25,6 +26,6 @@ export function assertActivityMatchesUrls(activity: IObject, urls: string[]) {
 | 
			
		|||
		.map(u => new URL(u as string).href);
 | 
			
		||||
 | 
			
		||||
	if (!actualUrls.some(u => expectedUrls.has(u))) {
 | 
			
		||||
		throw new Error(`bad Activity: neither id(${activity.id}) nor url(${JSON.stringify(activity.url)}) match location(${urls})`);
 | 
			
		||||
		throw new UnrecoverableError(`bad Activity: neither id(${activity.id}) nor url(${JSON.stringify(activity.url)}) match location(${urls})`);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,7 +9,7 @@ export function validateContentTypeSetAsActivityPub(response: Response): void {
 | 
			
		|||
	const contentType = (response.headers.get('content-type') ?? '').toLowerCase();
 | 
			
		||||
 | 
			
		||||
	if (contentType === '') {
 | 
			
		||||
		throw new Error('Validate content type of AP response: No content-type header');
 | 
			
		||||
		throw new Error(`invalid content type of AP response - no content-type header: ${response.url}`);
 | 
			
		||||
	}
 | 
			
		||||
	if (
 | 
			
		||||
		contentType.startsWith('application/activity+json') ||
 | 
			
		||||
| 
						 | 
				
			
			@ -17,7 +17,7 @@ export function validateContentTypeSetAsActivityPub(response: Response): void {
 | 
			
		|||
	) {
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
	throw new Error('Validate content type of AP response: Content type is not application/activity+json or application/ld+json');
 | 
			
		||||
	throw new Error(`invalid content type of AP response - content type is not application/activity+json or application/ld+json: ${response.url}`);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const plusJsonSuffixRegex = /^\s*(application|text)\/[a-zA-Z0-9\.\-\+]+\+json\s*(;|$)/;
 | 
			
		||||
| 
						 | 
				
			
			@ -26,7 +26,7 @@ export function validateContentTypeSetAsJsonLD(response: Response): void {
 | 
			
		|||
	const contentType = (response.headers.get('content-type') ?? '').toLowerCase();
 | 
			
		||||
 | 
			
		||||
	if (contentType === '') {
 | 
			
		||||
		throw new Error('Validate content type of JSON LD: No content-type header');
 | 
			
		||||
		throw new Error(`invalid content type of JSON LD - no content-type header: ${response.url}`);
 | 
			
		||||
	}
 | 
			
		||||
	if (
 | 
			
		||||
		contentType.startsWith('application/ld+json') ||
 | 
			
		||||
| 
						 | 
				
			
			@ -35,5 +35,5 @@ export function validateContentTypeSetAsJsonLD(response: Response): void {
 | 
			
		|||
	) {
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
	throw new Error('Validate content type of JSON LD: Content type is not application/ld+json or application/json');
 | 
			
		||||
	throw new Error(`invalid content type of JSON LD - content type is not application/ld+json or application/json: ${response.url}`);
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,6 +15,7 @@ import { bindThis } from '@/decorators.js';
 | 
			
		|||
import { checkHttps } from '@/misc/check-https.js';
 | 
			
		||||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
 | 
			
		||||
import type { Config } from '@/config.js';
 | 
			
		||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
 | 
			
		||||
import { ApResolverService } from '../ApResolverService.js';
 | 
			
		||||
import { ApLoggerService } from '../ApLoggerService.js';
 | 
			
		||||
import { isDocument, type IObject } from '../type.js';
 | 
			
		||||
| 
						 | 
				
			
			@ -47,7 +48,7 @@ export class ApImageService {
 | 
			
		|||
	public async createImage(actor: MiRemoteUser, value: string | IObject): Promise<MiDriveFile | null> {
 | 
			
		||||
		// 投稿者が凍結されていたらスキップ
 | 
			
		||||
		if (actor.isSuspended) {
 | 
			
		||||
			throw new Error('actor has been suspended');
 | 
			
		||||
			throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', `actor has been suspended: ${actor.uri}`);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const image = await this.apResolverService.createResolver().resolve(value);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,6 +5,7 @@
 | 
			
		|||
 | 
			
		||||
import { forwardRef, Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import { In } from 'typeorm';
 | 
			
		||||
import { UnrecoverableError } from 'bullmq';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import type { UsersRepository, PollsRepository, EmojisRepository, NotesRepository, MiMeta } from '@/models/_.js';
 | 
			
		||||
import type { Config } from '@/config.js';
 | 
			
		||||
| 
						 | 
				
			
			@ -177,22 +178,22 @@ export class ApNoteService {
 | 
			
		|||
		this.logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`);
 | 
			
		||||
 | 
			
		||||
		if (note.id == null) {
 | 
			
		||||
			throw new Error('Refusing to create note without id');
 | 
			
		||||
			throw new UnrecoverableError(`Refusing to create note without id: ${entryUri}`);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (!checkHttps(note.id)) {
 | 
			
		||||
			throw new Error('unexpected schema of note.id: ' + note.id);
 | 
			
		||||
			throw new UnrecoverableError(`unexpected schema of note.id ${note.id} in ${entryUri}`);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const url = getOneApHrefNullable(note.url);
 | 
			
		||||
 | 
			
		||||
		if (url != null) {
 | 
			
		||||
			if (!checkHttps(url)) {
 | 
			
		||||
				throw new Error('unexpected schema of note url: ' + url);
 | 
			
		||||
				throw new UnrecoverableError(`unexpected schema of note.url ${url} in ${entryUri}`);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (this.utilityService.punyHost(url) !== this.utilityService.punyHost(note.id)) {
 | 
			
		||||
				throw new Error(`note url & uri host mismatch: note url: ${url}, note uri: ${note.id}`);
 | 
			
		||||
				throw new Error(`note url <> uri host mismatch: ${url} <> ${note.id} in ${entryUri}`);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -200,7 +201,7 @@ export class ApNoteService {
 | 
			
		|||
 | 
			
		||||
		// 投稿者をフェッチ
 | 
			
		||||
		if (note.attributedTo == null) {
 | 
			
		||||
			throw new Error('invalid note.attributedTo: ' + note.attributedTo);
 | 
			
		||||
			throw new UnrecoverableError(`invalid note.attributedTo ${note.attributedTo} in ${entryUri}`);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const uri = getOneApId(note.attributedTo);
 | 
			
		||||
| 
						 | 
				
			
			@ -209,7 +210,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 new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', `actor ${uri} has been suspended: ${entryUri}`);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const apMentions = await this.apMentionService.extractApMentions(note.tag, resolver);
 | 
			
		||||
| 
						 | 
				
			
			@ -236,7 +237,7 @@ 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');
 | 
			
		||||
			throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', `Note contains prohibited words: ${entryUri}`);
 | 
			
		||||
		}
 | 
			
		||||
		//#endregion
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -245,7 +246,7 @@ export class ApNoteService {
 | 
			
		|||
 | 
			
		||||
		// 解決した投稿者が凍結されていたらスキップ
 | 
			
		||||
		if (actor.isSuspended) {
 | 
			
		||||
			throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', 'actor has been suspended');
 | 
			
		||||
			throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', `actor has been suspended: ${entryUri}`);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const noteAudience = await this.apAudienceService.parseAudience(actor, note.to, note.cc, resolver);
 | 
			
		||||
| 
						 | 
				
			
			@ -275,13 +276,13 @@ export class ApNoteService {
 | 
			
		|||
				.then(x => {
 | 
			
		||||
					if (x == null) {
 | 
			
		||||
						this.logger.warn('Specified inReplyTo, but not found');
 | 
			
		||||
						throw new Error('inReplyTo not found');
 | 
			
		||||
						throw new Error(`could not fetch inReplyTo ${note.inReplyTo} for note ${entryUri}`);
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					return x;
 | 
			
		||||
				})
 | 
			
		||||
				.catch(async err => {
 | 
			
		||||
					this.logger.warn(`Error in inReplyTo ${note.inReplyTo} - ${err.statusCode ?? err}`);
 | 
			
		||||
					this.logger.warn(`error ${err.statusCode ?? err} fetching inReplyTo ${note.inReplyTo} for note ${entryUri}`);
 | 
			
		||||
					throw err;
 | 
			
		||||
				})
 | 
			
		||||
			: null;
 | 
			
		||||
| 
						 | 
				
			
			@ -290,16 +291,25 @@ export class ApNoteService {
 | 
			
		|||
		let quote: MiNote | undefined | null = null;
 | 
			
		||||
 | 
			
		||||
		if (note._misskey_quote ?? note.quoteUrl ?? note.quoteUri) {
 | 
			
		||||
			const tryResolveNote = async (uri: string): Promise<
 | 
			
		||||
			const tryResolveNote = async (uri: unknown): Promise<
 | 
			
		||||
				| { status: 'ok'; res: MiNote }
 | 
			
		||||
				| { status: 'permerror' | 'temperror' }
 | 
			
		||||
			> => {
 | 
			
		||||
				if (typeof uri !== 'string' || !/^https?:/.test(uri)) return { status: 'permerror' };
 | 
			
		||||
				if (typeof uri !== 'string' || !/^https?:/.test(uri)) {
 | 
			
		||||
					this.logger.warn(`Failed to resolve quote ${uri} for note ${entryUri}: URI is invalid`);
 | 
			
		||||
					return { status: 'permerror' };
 | 
			
		||||
				}
 | 
			
		||||
				try {
 | 
			
		||||
					const res = await this.resolveNote(uri, { resolver });
 | 
			
		||||
					if (res == null) return { status: 'permerror' };
 | 
			
		||||
					if (res == null) {
 | 
			
		||||
						this.logger.warn(`Failed to resolve quote ${uri} for note ${entryUri}: resolution error`);
 | 
			
		||||
						return { status: 'permerror' };
 | 
			
		||||
					}
 | 
			
		||||
					return { status: 'ok', res };
 | 
			
		||||
				} catch (e) {
 | 
			
		||||
					const error = e instanceof Error ? `${e.name}: ${e.message}` : String(e);
 | 
			
		||||
					this.logger.warn(`Failed to resolve quote ${uri} for note ${entryUri}: ${error}`);
 | 
			
		||||
 | 
			
		||||
					return {
 | 
			
		||||
						status: (e instanceof StatusError && !e.isRetryable) ? 'permerror' : 'temperror',
 | 
			
		||||
					};
 | 
			
		||||
| 
						 | 
				
			
			@ -312,7 +322,7 @@ export class ApNoteService {
 | 
			
		|||
			quote = results.filter((x): x is { status: 'ok', res: MiNote } => x.status === 'ok').map(x => x.res).at(0);
 | 
			
		||||
			if (!quote) {
 | 
			
		||||
				if (results.some(x => x.status === 'temperror')) {
 | 
			
		||||
					throw new Error('quote resolve failed');
 | 
			
		||||
					throw new Error(`temporary error resolving quote for ${entryUri}`);
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
| 
						 | 
				
			
			@ -372,7 +382,7 @@ export class ApNoteService {
 | 
			
		|||
			this.logger.info('The note is already inserted while creating itself, reading again');
 | 
			
		||||
			const duplicate = await this.fetchNote(value);
 | 
			
		||||
			if (!duplicate) {
 | 
			
		||||
				throw new Error('The note creation failed with duplication error even when there is no duplication');
 | 
			
		||||
				throw new Error(`The note creation failed with duplication error even when there is no duplication: ${entryUri}`);
 | 
			
		||||
			}
 | 
			
		||||
			return duplicate;
 | 
			
		||||
		}
 | 
			
		||||
| 
						 | 
				
			
			@ -383,18 +393,17 @@ export class ApNoteService {
 | 
			
		|||
	 */
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async updateNote(value: string | IObject, actor?: MiRemoteUser, resolver?: Resolver, silent = false): Promise<MiNote | null> {
 | 
			
		||||
		const noteUri = typeof value === 'string' ? value : value.id;
 | 
			
		||||
		if (noteUri == null) throw new Error('uri is null');
 | 
			
		||||
		const noteUri = getApId(value);
 | 
			
		||||
 | 
			
		||||
		// URIがこのサーバーを指しているならスキップ
 | 
			
		||||
		if (noteUri.startsWith(this.config.url + '/')) throw new Error('uri points local');
 | 
			
		||||
		if (noteUri.startsWith(this.config.url + '/')) throw new UnrecoverableError(`uri points local: ${noteUri}`);
 | 
			
		||||
 | 
			
		||||
		//#region このサーバーに既に登録されているか
 | 
			
		||||
		const UpdatedNote = await this.notesRepository.findOneBy({ uri: noteUri });
 | 
			
		||||
		if (UpdatedNote == null) throw new Error('Note is not registered');
 | 
			
		||||
		const updatedNote = await this.notesRepository.findOneBy({ uri: noteUri });
 | 
			
		||||
		if (updatedNote == null) throw new Error(`Note is not registered (no note): ${noteUri}`);
 | 
			
		||||
 | 
			
		||||
		const user = await this.usersRepository.findOneBy({ id: UpdatedNote.userId }) as MiRemoteUser | null;
 | 
			
		||||
		if (user == null) throw new Error('Note is not registered');
 | 
			
		||||
		const user = await this.usersRepository.findOneBy({ id: updatedNote.userId }) as MiRemoteUser | null;
 | 
			
		||||
		if (user == null) throw new Error(`Note is not registered (no user): ${noteUri}`);
 | 
			
		||||
 | 
			
		||||
		// eslint-disable-next-line no-param-reassign
 | 
			
		||||
		if (resolver == null) resolver = this.apResolverService.createResolver();
 | 
			
		||||
| 
						 | 
				
			
			@ -421,29 +430,29 @@ export class ApNoteService {
 | 
			
		|||
		this.logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`);
 | 
			
		||||
 | 
			
		||||
		if (note.id == null) {
 | 
			
		||||
			throw new Error('Refusing to update note without id');
 | 
			
		||||
			throw new UnrecoverableError(`Refusing to update note without id: ${noteUri}`);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (!checkHttps(note.id)) {
 | 
			
		||||
			throw new Error('unexpected schema of note.id: ' + note.id);
 | 
			
		||||
			throw new UnrecoverableError(`unexpected schema of note.id ${note.id} in ${noteUri}`);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const url = getOneApHrefNullable(note.url);
 | 
			
		||||
 | 
			
		||||
		if (url != null) {
 | 
			
		||||
			if (!checkHttps(url)) {
 | 
			
		||||
				throw new Error('unexpected schema of note url: ' + url);
 | 
			
		||||
				throw new UnrecoverableError(`unexpected schema of note.url ${url} in ${noteUri}`);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (this.utilityService.punyHost(url) !== this.utilityService.punyHost(note.id)) {
 | 
			
		||||
				throw new Error(`note url & uri host mismatch: note url: ${url}, note uri: ${note.id}`);
 | 
			
		||||
				throw new UnrecoverableError(`note url <> id host mismatch: ${url} <> ${note.id} in ${noteUri}`);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		this.logger.info(`Creating the Note: ${note.id}`);
 | 
			
		||||
 | 
			
		||||
		if (actor.isSuspended) {
 | 
			
		||||
			throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', 'actor has been suspended');
 | 
			
		||||
			throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', `actor ${actor.id} has been suspended: ${noteUri}`);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const apMentions = await this.apMentionService.extractApMentions(note.tag, resolver);
 | 
			
		||||
| 
						 | 
				
			
			@ -470,7 +479,7 @@ export class ApNoteService {
 | 
			
		|||
		 */
 | 
			
		||||
		const hasProhibitedWords = await this.noteCreateService.checkProhibitedWordsContain({ cw, text, pollChoices: poll?.choices });
 | 
			
		||||
		if (hasProhibitedWords) {
 | 
			
		||||
			throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', 'Note contains prohibited words');
 | 
			
		||||
			throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', `Note contains prohibited words: ${noteUri}`);
 | 
			
		||||
		}
 | 
			
		||||
		//#endregion
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -501,13 +510,13 @@ export class ApNoteService {
 | 
			
		|||
				.then(x => {
 | 
			
		||||
					if (x == null) {
 | 
			
		||||
						this.logger.warn('Specified inReplyTo, but not found');
 | 
			
		||||
						throw new Error('inReplyTo not found');
 | 
			
		||||
						throw new Error(`could not fetch inReplyTo ${note.inReplyTo}: ${entryUri}`);
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					return x;
 | 
			
		||||
				})
 | 
			
		||||
				.catch(async err => {
 | 
			
		||||
					this.logger.warn(`Error in inReplyTo ${note.inReplyTo} - ${err.statusCode ?? err}`);
 | 
			
		||||
					this.logger.warn(`error ${err.statusCode ?? err} fetching inReplyTo ${note.inReplyTo}: ${entryUri}`);
 | 
			
		||||
					throw err;
 | 
			
		||||
				})
 | 
			
		||||
			: null;
 | 
			
		||||
| 
						 | 
				
			
			@ -516,16 +525,25 @@ export class ApNoteService {
 | 
			
		|||
		let quote: MiNote | undefined | null = null;
 | 
			
		||||
 | 
			
		||||
		if (note._misskey_quote ?? note.quoteUrl ?? note.quoteUri) {
 | 
			
		||||
			const tryResolveNote = async (uri: string): Promise<
 | 
			
		||||
			const tryResolveNote = async (uri: unknown): Promise<
 | 
			
		||||
				| { status: 'ok'; res: MiNote }
 | 
			
		||||
				| { status: 'permerror' | 'temperror' }
 | 
			
		||||
			> => {
 | 
			
		||||
				if (typeof uri !== 'string' || !/^https?:/.test(uri)) return { status: 'permerror' };
 | 
			
		||||
				if (typeof uri !== 'string' || !/^https?:/.test(uri)) {
 | 
			
		||||
					this.logger.warn(`Failed to resolve quote ${uri} for note ${entryUri}: URI is invalid`);
 | 
			
		||||
					return { status: 'permerror' };
 | 
			
		||||
				}
 | 
			
		||||
				try {
 | 
			
		||||
					const res = await this.resolveNote(uri, { resolver });
 | 
			
		||||
					if (res == null) return { status: 'permerror' };
 | 
			
		||||
					if (res == null) {
 | 
			
		||||
						this.logger.warn(`Failed to resolve quote ${uri} for note ${entryUri}: resolution error`);
 | 
			
		||||
						return { status: 'permerror' };
 | 
			
		||||
					}
 | 
			
		||||
					return { status: 'ok', res };
 | 
			
		||||
				} catch (e) {
 | 
			
		||||
					const error = e instanceof Error ? `${e.name}: ${e.message}` : String(e);
 | 
			
		||||
					this.logger.warn(`Failed to resolve quote ${uri} for note ${entryUri}: ${error}`);
 | 
			
		||||
 | 
			
		||||
					return {
 | 
			
		||||
						status: (e instanceof StatusError && !e.isRetryable) ? 'permerror' : 'temperror',
 | 
			
		||||
					};
 | 
			
		||||
| 
						 | 
				
			
			@ -538,7 +556,7 @@ export class ApNoteService {
 | 
			
		|||
			quote = results.filter((x): x is { status: 'ok', res: MiNote } => x.status === 'ok').map(x => x.res).at(0);
 | 
			
		||||
			if (!quote) {
 | 
			
		||||
				if (results.some(x => x.status === 'temperror')) {
 | 
			
		||||
					throw new Error('quote resolve failed');
 | 
			
		||||
					throw new Error(`temporary error resolving quote for ${entryUri}`);
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
| 
						 | 
				
			
			@ -573,7 +591,7 @@ export class ApNoteService {
 | 
			
		|||
		const apEmojis = emojis.map(emoji => emoji.name);
 | 
			
		||||
 | 
			
		||||
		try {
 | 
			
		||||
			return await this.noteEditService.edit(actor, UpdatedNote.id, {
 | 
			
		||||
			return await this.noteEditService.edit(actor, updatedNote.id, {
 | 
			
		||||
				createdAt: note.published ? new Date(note.published) : null,
 | 
			
		||||
				files,
 | 
			
		||||
				reply,
 | 
			
		||||
| 
						 | 
				
			
			@ -598,7 +616,7 @@ export class ApNoteService {
 | 
			
		|||
			this.logger.info('The note is already inserted while creating itself, reading again');
 | 
			
		||||
			const duplicate = await this.fetchNote(value);
 | 
			
		||||
			if (!duplicate) {
 | 
			
		||||
				throw new Error('The note creation failed with duplication error even when there is no duplication');
 | 
			
		||||
				throw new Error(`The note creation failed with duplication error even when there is no duplication: ${noteUri}`);
 | 
			
		||||
			}
 | 
			
		||||
			return duplicate;
 | 
			
		||||
		}
 | 
			
		||||
| 
						 | 
				
			
			@ -615,7 +633,7 @@ export class ApNoteService {
 | 
			
		|||
		const uri = getApId(value);
 | 
			
		||||
 | 
			
		||||
		if (!this.utilityService.isFederationAllowedUri(uri)) {
 | 
			
		||||
			throw new StatusError('blocked host', 451);
 | 
			
		||||
			throw new StatusError(`blocked host: ${uri}`, 451, 'blocked host');
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const unlock = await this.appLockService.getApLock(uri);
 | 
			
		||||
| 
						 | 
				
			
			@ -627,7 +645,7 @@ export class ApNoteService {
 | 
			
		|||
			//#endregion
 | 
			
		||||
 | 
			
		||||
			if (this.utilityService.isUriLocal(uri)) {
 | 
			
		||||
				throw new StatusError('cannot resolve local note', 400, 'cannot resolve local note');
 | 
			
		||||
				throw new StatusError(`cannot resolve local note: ${uri}`, 400, 'cannot resolve local note');
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// リモートサーバーからフェッチしてきて登録
 | 
			
		||||
| 
						 | 
				
			
			@ -675,7 +693,7 @@ export class ApNoteService {
 | 
			
		|||
					});
 | 
			
		||||
 | 
			
		||||
					const emoji = await this.emojisRepository.findOneBy({ host, name });
 | 
			
		||||
					if (emoji == null) throw new Error('emoji update failed');
 | 
			
		||||
					if (emoji == null) throw new Error(`emoji update failed: ${name}:${host}`);
 | 
			
		||||
					return emoji;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,6 +8,7 @@ import promiseLimit from 'promise-limit';
 | 
			
		|||
import { DataSource } from 'typeorm';
 | 
			
		||||
import { ModuleRef } from '@nestjs/core';
 | 
			
		||||
import { AbortError } from 'node-fetch';
 | 
			
		||||
import { UnrecoverableError } from 'bullmq';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import type { FollowingsRepository, InstancesRepository, MiMeta, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/_.js';
 | 
			
		||||
import type { Config } from '@/config.js';
 | 
			
		||||
| 
						 | 
				
			
			@ -140,26 +141,27 @@ export class ApPersonService implements OnModuleInit {
 | 
			
		|||
		const expectHost = this.utilityService.punyHost(uri);
 | 
			
		||||
 | 
			
		||||
		if (!isActor(x)) {
 | 
			
		||||
			throw new Error(`invalid Actor type '${x.type}'`);
 | 
			
		||||
			throw new UnrecoverableError(`invalid Actor type '${x.type}' in ${uri}`);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (!(typeof x.id === 'string' && x.id.length > 0)) {
 | 
			
		||||
			throw new Error('invalid Actor: wrong id');
 | 
			
		||||
			throw new UnrecoverableError(`invalid Actor ${uri} - wrong id type`);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (!(typeof x.inbox === 'string' && x.inbox.length > 0)) {
 | 
			
		||||
			throw new Error('invalid Actor: wrong inbox');
 | 
			
		||||
			throw new UnrecoverableError(`invalid Actor ${uri} - wrong inbox type`);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (this.utilityService.punyHost(x.inbox) !== expectHost) {
 | 
			
		||||
			throw new Error('invalid Actor: inbox has different host');
 | 
			
		||||
		const inboxHost = this.utilityService.punyHost(x.inbox);
 | 
			
		||||
		if (inboxHost !== expectHost) {
 | 
			
		||||
			throw new UnrecoverableError(`invalid Actor ${uri} - wrong inbox ${inboxHost}`);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const sharedInboxObject = x.sharedInbox ?? (x.endpoints ? x.endpoints.sharedInbox : undefined);
 | 
			
		||||
		if (sharedInboxObject != null) {
 | 
			
		||||
			const sharedInbox = getApId(sharedInboxObject);
 | 
			
		||||
			if (!(typeof sharedInbox === 'string' && sharedInbox.length > 0 && this.utilityService.punyHost(sharedInbox) === expectHost)) {
 | 
			
		||||
				throw new Error('invalid Actor: wrong shared inbox');
 | 
			
		||||
				throw new UnrecoverableError(`invalid Actor ${uri} - wrong shared inbox ${sharedInbox}`);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -169,16 +171,16 @@ export class ApPersonService implements OnModuleInit {
 | 
			
		|||
				const collectionUri = getApId(xCollection);
 | 
			
		||||
				if (typeof collectionUri === 'string' && collectionUri.length > 0) {
 | 
			
		||||
					if (this.utilityService.punyHost(collectionUri) !== expectHost) {
 | 
			
		||||
						throw new Error(`invalid Actor: ${collection} has different host`);
 | 
			
		||||
						throw new UnrecoverableError(`invalid Actor ${uri} - wrong ${collection} ${collectionUri}`);
 | 
			
		||||
					}
 | 
			
		||||
				} else if (collectionUri != null) {
 | 
			
		||||
					throw new Error(`invalid Actor: wrong ${collection}`);
 | 
			
		||||
					throw new UnrecoverableError(`invalid Actor ${uri}: wrong ${collection} type`);
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (!(typeof x.preferredUsername === 'string' && x.preferredUsername.length > 0 && x.preferredUsername.length <= 128 && /^\w([\w-.]*\w)?$/.test(x.preferredUsername))) {
 | 
			
		||||
			throw new Error('invalid Actor: wrong username');
 | 
			
		||||
			throw new UnrecoverableError(`invalid Actor ${uri} - wrong username`);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// These fields are only informational, and some AP software allows these
 | 
			
		||||
| 
						 | 
				
			
			@ -186,7 +188,7 @@ export class ApPersonService implements OnModuleInit {
 | 
			
		|||
		// we can at least see these users and their activities.
 | 
			
		||||
		if (x.name) {
 | 
			
		||||
			if (!(typeof x.name === 'string' && x.name.length > 0)) {
 | 
			
		||||
				throw new Error('invalid Actor: wrong name');
 | 
			
		||||
				throw new UnrecoverableError(`invalid Actor ${uri} - wrong name`);
 | 
			
		||||
			}
 | 
			
		||||
			x.name = truncate(x.name, nameLength);
 | 
			
		||||
		} else if (x.name === '') {
 | 
			
		||||
| 
						 | 
				
			
			@ -195,24 +197,24 @@ export class ApPersonService implements OnModuleInit {
 | 
			
		|||
		}
 | 
			
		||||
		if (x.summary) {
 | 
			
		||||
			if (!(typeof x.summary === 'string' && x.summary.length > 0)) {
 | 
			
		||||
				throw new Error('invalid Actor: wrong summary');
 | 
			
		||||
				throw new UnrecoverableError(`invalid Actor ${uri} - wrong summary`);
 | 
			
		||||
			}
 | 
			
		||||
			x.summary = truncate(x.summary, summaryLength);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const idHost = this.utilityService.punyHost(x.id);
 | 
			
		||||
		if (idHost !== expectHost) {
 | 
			
		||||
			throw new Error('invalid Actor: id has different host');
 | 
			
		||||
			throw new UnrecoverableError(`invalid Actor ${uri} - wrong id ${x.id}`);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (x.publicKey) {
 | 
			
		||||
			if (typeof x.publicKey.id !== 'string') {
 | 
			
		||||
				throw new Error('invalid Actor: publicKey.id is not a string');
 | 
			
		||||
				throw new UnrecoverableError(`invalid Actor ${uri} - wrong publicKey.id type`);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			const publicKeyIdHost = this.utilityService.punyHost(x.publicKey.id);
 | 
			
		||||
			if (publicKeyIdHost !== expectHost) {
 | 
			
		||||
				throw new Error('invalid Actor: publicKey.id has different host');
 | 
			
		||||
				throw new UnrecoverableError(`invalid Actor ${uri} - wrong publicKey.id ${x.publicKey.id}`);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -304,18 +306,18 @@ export class ApPersonService implements OnModuleInit {
 | 
			
		|||
	 */
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async createPerson(uri: string, resolver?: Resolver): Promise<MiRemoteUser> {
 | 
			
		||||
		if (typeof uri !== 'string') throw new Error('uri is not string');
 | 
			
		||||
		if (typeof uri !== 'string') throw new UnrecoverableError(`uri is not string: ${uri}`);
 | 
			
		||||
 | 
			
		||||
		const host = this.utilityService.punyHost(uri);
 | 
			
		||||
		if (host === this.utilityService.toPuny(this.config.host)) {
 | 
			
		||||
			throw new StatusError('cannot resolve local user', 400, 'cannot resolve local user');
 | 
			
		||||
			throw new StatusError(`cannot resolve local user: ${uri}`, 400, 'cannot resolve local user');
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// eslint-disable-next-line no-param-reassign
 | 
			
		||||
		if (resolver == null) resolver = this.apResolverService.createResolver();
 | 
			
		||||
 | 
			
		||||
		const object = await resolver.resolve(uri);
 | 
			
		||||
		if (object.id == null) throw new Error('invalid object.id: ' + object.id);
 | 
			
		||||
		if (object.id == null) throw new UnrecoverableError(`null object.id in ${uri}`);
 | 
			
		||||
 | 
			
		||||
		const person = this.validateActor(object, uri);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -347,16 +349,16 @@ export class ApPersonService implements OnModuleInit {
 | 
			
		|||
		const url = getOneApHrefNullable(person.url);
 | 
			
		||||
 | 
			
		||||
		if (person.id == null) {
 | 
			
		||||
			throw new Error('Refusing to create person without id');
 | 
			
		||||
			throw new UnrecoverableError(`Refusing to create person without id: ${uri}`);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (url != null) {
 | 
			
		||||
			if (!checkHttps(url)) {
 | 
			
		||||
				throw new Error('unexpected schema of person url: ' + url);
 | 
			
		||||
				throw new UnrecoverableError(`unexpected schema of person url ${url} in ${uri}`);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (this.utilityService.punyHost(url) !== this.utilityService.punyHost(person.id)) {
 | 
			
		||||
				throw new Error(`person url <> uri host mismatch: ${url} <> ${person.id}`);
 | 
			
		||||
				throw new UnrecoverableError(`person url <> uri host mismatch: ${url} <> ${person.id} in ${uri}`);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -451,7 +453,7 @@ export class ApPersonService implements OnModuleInit {
 | 
			
		|||
			if (isDuplicateKeyValueError(e)) {
 | 
			
		||||
				// /users/@a => /users/:id のように入力がaliasなときにエラーになることがあるのを対応
 | 
			
		||||
				const u = await this.usersRepository.findOneBy({ uri: person.id });
 | 
			
		||||
				if (u == null) throw new Error('already registered');
 | 
			
		||||
				if (u == null) throw new UnrecoverableError(`already registered a user with conflicting data: ${uri}`);
 | 
			
		||||
 | 
			
		||||
				user = u as MiRemoteUser;
 | 
			
		||||
			} else {
 | 
			
		||||
| 
						 | 
				
			
			@ -460,7 +462,7 @@ export class ApPersonService implements OnModuleInit {
 | 
			
		|||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (user == null) throw new Error('failed to create user: user is null');
 | 
			
		||||
		if (user == null) throw new Error(`failed to create user - user is null: ${uri}`);
 | 
			
		||||
 | 
			
		||||
		// Register to the cache
 | 
			
		||||
		this.cacheService.uriPersonCache.set(user.uri, user);
 | 
			
		||||
| 
						 | 
				
			
			@ -511,7 +513,7 @@ export class ApPersonService implements OnModuleInit {
 | 
			
		|||
	 */
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async updatePerson(uri: string, resolver?: Resolver | null, hint?: IObject, movePreventUris: string[] = []): Promise<string | void> {
 | 
			
		||||
		if (typeof uri !== 'string') throw new Error('uri is not string');
 | 
			
		||||
		if (typeof uri !== 'string') throw new UnrecoverableError('uri is not string');
 | 
			
		||||
 | 
			
		||||
		// URIがこのサーバーを指しているならスキップ
 | 
			
		||||
		if (this.utilityService.isUriLocal(uri)) return;
 | 
			
		||||
| 
						 | 
				
			
			@ -564,16 +566,16 @@ export class ApPersonService implements OnModuleInit {
 | 
			
		|||
		const url = getOneApHrefNullable(person.url);
 | 
			
		||||
 | 
			
		||||
		if (person.id == null) {
 | 
			
		||||
			throw new Error('Refusing to update person without id');
 | 
			
		||||
			throw new UnrecoverableError(`Refusing to update person without id: ${uri}`);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (url != null) {
 | 
			
		||||
			if (!checkHttps(url)) {
 | 
			
		||||
				throw new Error('unexpected schema of person url: ' + url);
 | 
			
		||||
				throw new UnrecoverableError(`unexpected schema of person url ${url} in ${uri}`);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (this.utilityService.punyHost(url) !== this.utilityService.punyHost(person.id)) {
 | 
			
		||||
				throw new Error(`person url <> uri host mismatch: ${url} <> ${person.id}`);
 | 
			
		||||
				throw new UnrecoverableError(`person url <> uri host mismatch: ${url} <> ${person.id} in ${uri}`);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -684,7 +686,7 @@ export class ApPersonService implements OnModuleInit {
 | 
			
		|||
				});
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return 'skip';
 | 
			
		||||
		return 'skip: too soon to migrate accounts';
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
| 
						 | 
				
			
			@ -743,7 +745,7 @@ export class ApPersonService implements OnModuleInit {
 | 
			
		|||
		});
 | 
			
		||||
		if (!collection) return;
 | 
			
		||||
 | 
			
		||||
		if (!isCollectionOrOrderedCollection(collection)) throw new Error('Object is not Collection or OrderedCollection');
 | 
			
		||||
		if (!isCollectionOrOrderedCollection(collection)) throw new UnrecoverableError(`featured ${user.featured} is not Collection or OrderedCollection in ${user.uri}`);
 | 
			
		||||
 | 
			
		||||
		// Resolve to Object(may be Note) arrays
 | 
			
		||||
		const unresolvedItems = isCollection(collection) ? collection.items : collection.orderedItems;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,6 +4,7 @@
 | 
			
		|||
 */
 | 
			
		||||
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import { UnrecoverableError } from 'bullmq';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import type { UsersRepository, NotesRepository, PollsRepository } from '@/models/_.js';
 | 
			
		||||
import type { Config } from '@/config.js';
 | 
			
		||||
| 
						 | 
				
			
			@ -11,7 +12,7 @@ import type { IPoll } from '@/models/Poll.js';
 | 
			
		|||
import type { MiRemoteUser } from '@/models/User.js';
 | 
			
		||||
import type Logger from '@/logger.js';
 | 
			
		||||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
import { getOneApId, isQuestion } from '../type.js';
 | 
			
		||||
import { getApId, getApType, getNullableApId, getOneApId, isQuestion } from '../type.js';
 | 
			
		||||
import { UtilityService } from '@/core/UtilityService.js';
 | 
			
		||||
import { ApLoggerService } from '../ApLoggerService.js';
 | 
			
		||||
import { ApResolverService } from '../ApResolverService.js';
 | 
			
		||||
| 
						 | 
				
			
			@ -48,10 +49,10 @@ export class ApQuestionService {
 | 
			
		|||
		if (resolver == null) resolver = this.apResolverService.createResolver();
 | 
			
		||||
 | 
			
		||||
		const question = await resolver.resolve(source);
 | 
			
		||||
		if (!isQuestion(question)) throw new Error('invalid type');
 | 
			
		||||
		if (!isQuestion(question)) throw new UnrecoverableError(`invalid type ${getApType(question)}: ${getNullableApId(question)}`);
 | 
			
		||||
 | 
			
		||||
		const multiple = question.oneOf === undefined;
 | 
			
		||||
		if (multiple && question.anyOf === undefined) throw new Error('invalid question');
 | 
			
		||||
		if (multiple && question.anyOf === undefined) throw new UnrecoverableError(`invalid question - neither oneOf nor anyOf is defined: ${getNullableApId(question)}`);
 | 
			
		||||
 | 
			
		||||
		const expiresAt = question.endTime ? new Date(question.endTime) : question.closed ? new Date(question.closed) : null;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -72,21 +73,20 @@ export class ApQuestionService {
 | 
			
		|||
	 */
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async updateQuestion(value: string | IObject, actor?: MiRemoteUser, resolver?: Resolver): Promise<boolean> {
 | 
			
		||||
		const uri = typeof value === 'string' ? value : value.id;
 | 
			
		||||
		if (uri == null) throw new Error('uri is null');
 | 
			
		||||
		const uri = getApId(value);
 | 
			
		||||
 | 
			
		||||
		// URIがこのサーバーを指しているならスキップ
 | 
			
		||||
		if (this.utilityService.isUriLocal(uri)) throw new Error('uri points local');
 | 
			
		||||
		if (this.utilityService.isUriLocal(uri)) throw new Error(`uri points local: ${uri}`);
 | 
			
		||||
 | 
			
		||||
		//#region このサーバーに既に登録されているか
 | 
			
		||||
		const note = await this.notesRepository.findOneBy({ uri });
 | 
			
		||||
		if (note == null) throw new Error('Question is not registered');
 | 
			
		||||
		if (note == null) throw new Error(`Question is not registered (no note): ${uri}`);
 | 
			
		||||
 | 
			
		||||
		const poll = await this.pollsRepository.findOneBy({ noteId: note.id });
 | 
			
		||||
		if (poll == null) throw new Error('Question is not registered');
 | 
			
		||||
		if (poll == null) throw new Error(`Question is not registered (no poll): ${uri}`);
 | 
			
		||||
 | 
			
		||||
		const user = await this.usersRepository.findOneBy({ id: poll.userId });
 | 
			
		||||
		if (user == null) throw new Error('Question is not registered');
 | 
			
		||||
		if (user == null) throw new Error(`Question is not registered (no user): ${uri}`);
 | 
			
		||||
		//#endregion
 | 
			
		||||
 | 
			
		||||
		// resolve new Question object
 | 
			
		||||
| 
						 | 
				
			
			@ -95,25 +95,25 @@ export class ApQuestionService {
 | 
			
		|||
		const question = await resolver.resolve(value);
 | 
			
		||||
		this.logger.debug(`fetched question: ${JSON.stringify(question, null, 2)}`);
 | 
			
		||||
 | 
			
		||||
		if (!isQuestion(question)) throw new Error('object is not a Question');
 | 
			
		||||
		if (!isQuestion(question)) throw new UnrecoverableError(`object ${getApType(question)} is not a Question: ${uri}`);
 | 
			
		||||
 | 
			
		||||
		const attribution = (question.attributedTo) ? getOneApId(question.attributedTo) : user.uri;
 | 
			
		||||
		const attributionMatchesExisting = attribution === user.uri;
 | 
			
		||||
		const actorMatchesAttribution = (actor) ? attribution === actor.uri : true;
 | 
			
		||||
 | 
			
		||||
		if (!attributionMatchesExisting || !actorMatchesAttribution) {
 | 
			
		||||
			throw new Error('Refusing to ingest update for poll by different user');
 | 
			
		||||
			throw new UnrecoverableError(`Refusing to ingest update for poll by different user: ${uri}`);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const apChoices = question.oneOf ?? question.anyOf;
 | 
			
		||||
		if (apChoices == null) throw new Error('invalid apChoices: ' + apChoices);
 | 
			
		||||
		if (apChoices == null) throw new UnrecoverableError(`poll has no choices: ${uri}`);
 | 
			
		||||
 | 
			
		||||
		let changed = false;
 | 
			
		||||
 | 
			
		||||
		for (const choice of poll.choices) {
 | 
			
		||||
			const oldCount = poll.votes[poll.choices.indexOf(choice)];
 | 
			
		||||
			const newCount = apChoices.filter(ap => ap.name === choice).at(0)?.replies?.totalItems;
 | 
			
		||||
			if (newCount == null || !(Number.isInteger(newCount) && newCount >= 0)) throw new Error('invalid newCount: ' + newCount);
 | 
			
		||||
			if (newCount == null || !(Number.isInteger(newCount) && newCount >= 0)) throw new UnrecoverableError(`invalid newCount: ${newCount} in ${uri}`);
 | 
			
		||||
 | 
			
		||||
			if (oldCount <= newCount) {
 | 
			
		||||
				changed = true;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,6 +3,7 @@
 | 
			
		|||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { UnrecoverableError } from 'bullmq';
 | 
			
		||||
import { fromTuple } from '@/misc/from-tuple.js';
 | 
			
		||||
 | 
			
		||||
export type Obj = { [x: string]: any };
 | 
			
		||||
| 
						 | 
				
			
			@ -64,7 +65,19 @@ export function getApId(value: string | IObject | [string | IObject]): string {
 | 
			
		|||
 | 
			
		||||
	if (typeof value === 'string') return value;
 | 
			
		||||
	if (typeof value.id === 'string') return value.id;
 | 
			
		||||
	throw new Error('cannot determine id');
 | 
			
		||||
	throw new UnrecoverableError('cannot determine id');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Get ActivityStreams Object id, or null if not present
 | 
			
		||||
 */
 | 
			
		||||
export function getNullableApId(value: string | IObject | [string | IObject]): string | null {
 | 
			
		||||
	// eslint-disable-next-line no-param-reassign
 | 
			
		||||
	value = fromTuple(value);
 | 
			
		||||
 | 
			
		||||
	if (typeof value === 'string') return value;
 | 
			
		||||
	if (typeof value.id === 'string') return value.id;
 | 
			
		||||
	return null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -225,7 +225,11 @@ export class InboxProcessorService implements OnApplicationShutdown {
 | 
			
		|||
		try {
 | 
			
		||||
			const result = await this.apInboxService.performActivity(authUser.user, activity);
 | 
			
		||||
			if (result && !result.startsWith('ok')) {
 | 
			
		||||
				this.logger.warn(`inbox activity ignored (maybe): id=${activity.id} reason=${result}`);
 | 
			
		||||
				if (result.startsWith('skip:')) {
 | 
			
		||||
					this.logger.info(`inbox activity ignored: id=${activity.id} reason=${result}`);
 | 
			
		||||
				} else {
 | 
			
		||||
					this.logger.warn(`inbox activity failed: id=${activity.id} reason=${result}`);
 | 
			
		||||
				}
 | 
			
		||||
				return result;
 | 
			
		||||
			}
 | 
			
		||||
		} catch (e) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,7 +7,7 @@
 | 
			
		|||
  "scripts": {
 | 
			
		||||
    "build": "tsc -p ./",
 | 
			
		||||
    "doc": "typedoc --out ../docs ./src",
 | 
			
		||||
    "test": "NODE_ENV=test jest -u --maxWorkers=3"
 | 
			
		||||
    "test": "cross-env NODE_ENV=test jest -u --maxWorkers=3"
 | 
			
		||||
  },
 | 
			
		||||
  "engines": {
 | 
			
		||||
    "node": ">=15.0.0"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -49,13 +49,14 @@ describe('detector', () => {
 | 
			
		|||
    })
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  describe('wildebeest', () => {
 | 
			
		||||
    const url = 'https://wildebeest.mirror-kt.dev'
 | 
			
		||||
    it('should be mastodon', async () => {
 | 
			
		||||
      const wildebeest = await detector(url)
 | 
			
		||||
      expect(wildebeest).toEqual('mastodon')
 | 
			
		||||
    })
 | 
			
		||||
  })
 | 
			
		||||
	// This domain no longer resolves, and resolution failures apparently crash jest
 | 
			
		||||
  // describe('wildebeest', () => {
 | 
			
		||||
  //   const url = 'https://wildebeest.mirror-kt.dev'
 | 
			
		||||
  //   it('should be mastodon', async () => {
 | 
			
		||||
  //     const wildebeest = await detector(url)
 | 
			
		||||
  //     expect(wildebeest).toEqual('mastodon')
 | 
			
		||||
  //   })
 | 
			
		||||
  // })
 | 
			
		||||
 | 
			
		||||
  describe('unknown', () => {
 | 
			
		||||
    const url = 'https://google.com'
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		
		Reference in a new issue