merge: Accept announce activities (resolves #797 and #460) (!916)

View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/916

Closes #797 and #460

Approved-by: Marie <github@yuugi.dev>
Approved-by: dakkar <dakkar@thenautilus.net>
This commit is contained in:
dakkar 2025-03-23 13:45:13 +00:00
commit b1467989a5
13 changed files with 221 additions and 35 deletions

View file

@ -677,6 +677,9 @@ seems to do a decent job)
`packages/frontend/src/pages/timeline.vue`,
`packages/frontend/src/ui/deck/tl-column.vue`,
`packages/frontend/src/widgets/WidgetTimeline.vue`)
* from `packages/backend/src/queue/processors/InboxProcessorService.ts`
to `packages/backend/src/core/UpdateInstanceQueue.ts`
where `updateInstanceQueue` is impacted
* if there have been any changes to the federated user data (the
`renderPerson` function in
`packages/backend/src/core/activitypub/ApRendererService.ts`), make

View file

@ -19,6 +19,7 @@ import { TimeService } from '@/core/TimeService.js';
import { EnvService } from '@/core/EnvService.js';
import { ApUtilityService } from '@/core/activitypub/ApUtilityService.js';
import { ApLogService } from '@/core/ApLogService.js';
import { UpdateInstanceQueue } from '@/core/UpdateInstanceQueue.js';
import { AccountMoveService } from './AccountMoveService.js';
import { AccountUpdateService } from './AccountUpdateService.js';
import { AnnouncementService } from './AnnouncementService.js';
@ -220,6 +221,7 @@ const $UserRenoteMutingService: Provider = { provide: 'UserRenoteMutingService',
const $UserSearchService: Provider = { provide: 'UserSearchService', useExisting: UserSearchService };
const $UserSuspendService: Provider = { provide: 'UserSuspendService', useExisting: UserSuspendService };
const $UserAuthService: Provider = { provide: 'UserAuthService', useExisting: UserAuthService };
const $UpdateInstanceQueue: Provider = { provide: 'UpdateInstanceQueue', useExisting: UpdateInstanceQueue };
const $VideoProcessingService: Provider = { provide: 'VideoProcessingService', useExisting: VideoProcessingService };
const $UserWebhookService: Provider = { provide: 'UserWebhookService', useExisting: UserWebhookService };
const $SystemWebhookService: Provider = { provide: 'SystemWebhookService', useExisting: SystemWebhookService };
@ -378,6 +380,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
UserSearchService,
UserSuspendService,
UserAuthService,
UpdateInstanceQueue,
VideoProcessingService,
UserWebhookService,
SystemWebhookService,
@ -532,6 +535,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
$UserSearchService,
$UserSuspendService,
$UserAuthService,
$UpdateInstanceQueue,
$VideoProcessingService,
$UserWebhookService,
$SystemWebhookService,
@ -687,6 +691,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
UserSearchService,
UserSuspendService,
UserAuthService,
UpdateInstanceQueue,
VideoProcessingService,
UserWebhookService,
SystemWebhookService,
@ -840,6 +845,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
$UserSearchService,
$UserSuspendService,
$UserAuthService,
$UpdateInstanceQueue,
$VideoProcessingService,
$UserWebhookService,
$SystemWebhookService,

View file

@ -16,7 +16,7 @@ import type { Config } from '@/config.js';
import { StatusError } from '@/misc/status-error.js';
import { bindThis } from '@/decorators.js';
import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js';
import { IObject } from '@/core/activitypub/type.js';
import type { IObject, IObjectWithId } from '@/core/activitypub/type.js';
import { ApUtilityService } from './activitypub/ApUtilityService.js';
import type { Response } from 'node-fetch';
import type { URL } from 'node:url';
@ -217,7 +217,7 @@ export class HttpRequestService {
}
@bindThis
public async getActivityJson(url: string, isLocalAddressAllowed = false): Promise<IObject> {
public async getActivityJson(url: string, isLocalAddressAllowed = false): Promise<IObjectWithId> {
const res = await this.send(url, {
method: 'GET',
headers: {
@ -237,7 +237,7 @@ export class HttpRequestService {
// The caller (ApResolverService) will verify the ID against the original / entry URL, which ensures that all three match.
this.apUtilityService.assertIdMatchesUrlAuthority(activity, res.url);
return activity;
return activity as IObjectWithId;
}
@bindThis

View file

@ -0,0 +1,52 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable, OnApplicationShutdown } from '@nestjs/common';
import { CollapsedQueue } from '@/misc/collapsed-queue.js';
import { bindThis } from '@/decorators.js';
import { MiNote } from '@/models/Note.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
type UpdateInstanceJob = {
latestRequestReceivedAt: Date,
shouldUnsuspend: boolean,
};
// Moved from InboxProcessorService to allow access from ApInboxService
@Injectable()
export class UpdateInstanceQueue extends CollapsedQueue<MiNote['id'], UpdateInstanceJob> implements OnApplicationShutdown {
constructor(
private readonly federatedInstanceService: FederatedInstanceService,
) {
super(process.env.NODE_ENV !== 'test' ? 60 * 1000 * 5 : 0, (id, job) => this.collapseUpdateInstanceJobs(id, job), (id, job) => this.performUpdateInstance(id, job));
}
@bindThis
private collapseUpdateInstanceJobs(oldJob: UpdateInstanceJob, newJob: UpdateInstanceJob) {
const latestRequestReceivedAt = oldJob.latestRequestReceivedAt < newJob.latestRequestReceivedAt
? newJob.latestRequestReceivedAt
: oldJob.latestRequestReceivedAt;
const shouldUnsuspend = oldJob.shouldUnsuspend || newJob.shouldUnsuspend;
return {
latestRequestReceivedAt,
shouldUnsuspend,
};
}
@bindThis
private async performUpdateInstance(id: string, job: UpdateInstanceJob) {
await this.federatedInstanceService.update(id, {
latestRequestReceivedAt: new Date(),
isNotResponding: false,
// もしサーバーが死んでるために配信が止まっていた場合には自動的に復活させてあげる
suspensionState: job.shouldUnsuspend ? 'none' : undefined,
});
}
@bindThis
async onApplicationShutdown() {
await this.performAllNow();
}
}

View file

@ -32,7 +32,11 @@ 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, getNullableApId, isAccept, isActor, isAdd, isAnnounce, isApObject, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isDislike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js';
import InstanceChart from '@/core/chart/charts/instance.js';
import FederationChart from '@/core/chart/charts/federation.js';
import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js';
import { UpdateInstanceQueue } from '@/core/UpdateInstanceQueue.js';
import { getApHrefNullable, getApId, getApIds, getApType, getNullableApId, isAccept, isActor, isAdd, isAnnounce, isApObject, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isDislike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost, isActivity, IObjectWithId } from './type.js';
import { ApNoteService } from './models/ApNoteService.js';
import { ApLoggerService } from './ApLoggerService.js';
import { ApDbResolverService } from './ApDbResolverService.js';
@ -41,7 +45,7 @@ import { ApAudienceService } from './ApAudienceService.js';
import { ApPersonService } from './models/ApPersonService.js';
import { ApQuestionService } from './models/ApQuestionService.js';
import type { Resolver } from './ApResolverService.js';
import type { IAccept, IAdd, IAnnounce, IBlock, ICreate, IDelete, IFlag, IFollow, ILike, IDislike, IObject, IReject, IRemove, IUndo, IUpdate, IMove, IPost } from './type.js';
import type { IAccept, IAdd, IAnnounce, IBlock, ICreate, IDelete, IFlag, IFollow, ILike, IDislike, IObject, IReject, IRemove, IUndo, IUpdate, IMove, IPost, IActivity } from './type.js';
@Injectable()
export class ApInboxService {
@ -88,7 +92,11 @@ export class ApInboxService {
private apQuestionService: ApQuestionService,
private queueService: QueueService,
private globalEventService: GlobalEventService,
private federatedInstanceService: FederatedInstanceService,
private readonly federatedInstanceService: FederatedInstanceService,
private readonly fetchInstanceMetadataService: FetchInstanceMetadataService,
private readonly instanceChart: InstanceChart,
private readonly federationChart: FederationChart,
private readonly updateInstanceQueue: UpdateInstanceQueue,
) {
this.logger = this.apLoggerService.logger;
}
@ -310,18 +318,19 @@ export class ApInboxService {
const targetUri = getApId(activityObject);
if (targetUri.startsWith('bear:')) return 'skip: bearcaps url not supported.';
const target = await resolver.resolve(activityObject).catch(e => {
const target = await resolver.secureResolve(activityObject, uri).catch(e => {
this.logger.error(`Resolution failed: ${e}`);
throw e;
});
if (isPost(target)) return await this.announceNote(actor, activity, target);
if (isActivity(target)) return await this.announceActivity(activity, target, resolver);
return `skip: unknown object type ${getApType(target)}`;
}
@bindThis
private async announceNote(actor: MiRemoteUser, activity: IAnnounce, target: IPost, resolver?: Resolver): Promise<string | void> {
private async announceNote(actor: MiRemoteUser, activity: IAnnounce, target: IPost & IObjectWithId, resolver?: Resolver): Promise<string | void> {
const uri = getApId(activity);
if (actor.isSuspended) {
@ -343,7 +352,9 @@ export class ApInboxService {
// Announce対象をresolve
let renote;
try {
renote = await this.apNoteService.resolveNote(target, { resolver });
// The target ID is verified by secureResolve, so we know it shares host authority with the actor who sent it.
// This means we can pass that ID to resolveNote and avoid an extra fetch, which will fail if the note is private.
renote = await this.apNoteService.resolveNote(target, { resolver, sentFrom: new URL(getApId(target)) });
if (renote == null) return 'announce target is null';
} catch (err) {
// 対象が4xxならスキップ
@ -383,6 +394,63 @@ export class ApInboxService {
}
}
private async announceActivity(announce: IAnnounce, activity: IActivity & IObjectWithId, resolver: Resolver): Promise<string | void> {
// Since this is a new activity, we need to get a new actor.
const actorId = getApId(activity.actor);
const actor = await this.apPersonService.resolvePerson(actorId, resolver);
// Ignore announce of our own activities
// 1. No URI/host on an MiUser == local user
// 2. Local URI on activity == local activity
if (!actor.uri || !actor.host || this.utilityService.isUriLocal(activity.id)) {
throw new Bull.UnrecoverableError(`Cannot announce a local activity: ${activity.id} (from ${announce.id})`);
}
// Make sure that actor matches activity host.
// Activity host is already verified by resolver when fetching the activity, so that is the source of truth.
const actorHost = this.utilityService.punyHostPSLDomain(actor.uri);
const activityHost = this.utilityService.punyHostPSLDomain(activity.id);
if (actorHost !== activityHost) {
throw new Bull.UnrecoverableError(`Actor host ${actorHost} does not activity host ${activityHost} in activity ${activity.id} (from ${announce.id})`);
}
// Update stats (adapted from InboxProcessorService)
this.federationChart.inbox(actor.host).then();
process.nextTick(async () => {
const i = await (this.meta.enableStatsForFederatedInstances
? this.federatedInstanceService.fetchOrRegister(actor.host)
: this.federatedInstanceService.fetch(actor.host));
if (i == null) return;
this.updateInstanceQueue.enqueue(i.id, {
latestRequestReceivedAt: new Date(),
shouldUnsuspend: i.suspensionState === 'autoSuspendedForNotResponding',
});
if (this.meta.enableChartsForFederatedInstances) {
this.instanceChart.requestReceived(i.host).then();
}
this.fetchInstanceMetadataService.fetchInstanceMetadata(i).then();
});
// Process it!
return await this.performOneActivity(actor, activity, resolver)
.finally(() => {
// Update user (adapted from performActivity)
if (actor.lastFetchedAt == null || Date.now() - actor.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) {
setImmediate(() => {
// Don't re-use the resolver, or it may throw recursion errors.
// Instead, create a new resolver with an appropriately-reduced recursion limit.
this.apPersonService.updatePerson(actor.uri, this.apResolverService.createResolver({
recursionLimit: resolver.getRecursionLimit() - resolver.getHistory().length,
}));
});
}
});
}
@bindThis
private async block(actor: MiRemoteUser, activity: IBlock): Promise<string> {
// ※ activity.objectにブロック対象があり、それは存在するローカルユーザーのはず

View file

@ -17,7 +17,7 @@ import { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.js';
import type Logger from '@/logger.js';
import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js';
import type { IObject } from './type.js';
import type { IObject, IObjectWithId } from './type.js';
type Request = {
url: string;
@ -185,7 +185,7 @@ export class ApRequestService {
* @param followAlternate
*/
@bindThis
public async signedGet(url: string, user: { id: MiUser['id'] }, followAlternate?: boolean): Promise<IObject> {
public async signedGet(url: string, user: { id: MiUser['id'] }, followAlternate?: boolean): Promise<IObjectWithId> {
const _followAlternate = followAlternate ?? true;
const keypair = await this.userKeypairService.getUserKeypair(user.id);
@ -273,6 +273,6 @@ export class ApRequestService {
// The caller (ApResolverService) will verify the ID against the original / entry URL, which ensures that all three match.
this.apUtilityService.assertIdMatchesUrlAuthority(activity, res.url);
return activity;
return activity as IObjectWithId;
}
}

View file

@ -19,11 +19,11 @@ import { fromTuple } from '@/misc/from-tuple.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import { ApLogService, calculateDurationSince, extractObjectContext } from '@/core/ApLogService.js';
import { ApUtilityService } from '@/core/activitypub/ApUtilityService.js';
import { getApId, getNullableApId, isCollectionOrOrderedCollection } from './type.js';
import { getApId, getNullableApId, IObjectWithId, 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 type { IObject, ICollection, IOrderedCollection, ApObject } from './type.js';
export class Resolver {
private history: Set<string>;
@ -76,6 +76,35 @@ export class Resolver {
}
}
/**
* Securely resolves an AP object or URL that has been sent from another instance.
* An input object is trusted if and only if its ID matches the authority of sentFromUri.
* In all other cases, the object is re-fetched from remote by input string or object ID.
*/
@bindThis
public async secureResolve(input: ApObject, sentFromUri: string): Promise<IObjectWithId> {
// Unpack arrays to get the value element.
const value = fromTuple(input);
if (value == null) {
throw new IdentifiableError('20058164-9de1-4573-8715-425753a21c1d', 'Cannot resolve null input');
}
// This will throw if the input has no ID, which is good because we can't verify an anonymous object anyway.
const id = getApId(value);
// Check if we can use the provided object as-is.
// Our security requires that the object ID matches the host authority that sent it, otherwise it can't be trusted.
// A mismatch isn't necessarily malicious, it just means we can't use the object we were given.
if (typeof(value) === 'object' && this.apUtilityService.haveSameAuthority(id, sentFromUri)) {
return value as IObjectWithId;
}
// If the checks didn't pass, then we must fetch the object and use that.
return await this.resolve(id);
}
public async resolve(value: string | [string]): Promise<IObjectWithId>;
public async resolve(value: string | IObject | [string | IObject]): Promise<IObject>;
@bindThis
public async resolve(value: string | IObject | [string | IObject]): Promise<IObject> {
// eslint-disable-next-line no-param-reassign
@ -93,7 +122,7 @@ export class Resolver {
}
}
private async _resolveLogged(requestUri: string, host: string): Promise<IObject> {
private async _resolveLogged(requestUri: string, host: string): Promise<IObjectWithId> {
const startTime = process.hrtime.bigint();
const log = await this.apLogService.createFetchLog({
@ -122,7 +151,7 @@ export class Resolver {
}
}
private async _resolve(value: string, host: string, log?: SkApFetchLog): Promise<IObject> {
private async _resolve(value: string, host: string, log?: SkApFetchLog): Promise<IObjectWithId> {
if (value.includes('#')) {
// URLs with fragment parts cannot be resolved correctly because
// the fragment part does not get transmitted over HTTP(S).
@ -141,7 +170,7 @@ export class Resolver {
this.history.add(value);
if (this.utilityService.isSelfHost(host)) {
return await this.resolveLocal(value);
return await this.resolveLocal(value) as IObjectWithId;
}
if (!this.utilityService.isFederationAllowedHost(host)) {
@ -153,8 +182,8 @@ export class Resolver {
}
const object = (this.user
? await this.apRequestService.signedGet(value, this.user) as IObject
: await this.httpRequestService.getActivityJson(value)) as IObject;
? await this.apRequestService.signedGet(value, this.user)
: await this.httpRequestService.getActivityJson(value));
if (log) {
const { object: objectOnly, context, contextHash } = extractObjectContext(object);
@ -199,7 +228,7 @@ export class Resolver {
}
@bindThis
private resolveLocal(url: string): Promise<IObject> {
private resolveLocal(url: string): Promise<IObjectWithId> {
const parsed = this.apDbResolverService.parseUri(url);
if (!parsed.local) throw new IdentifiableError('02b40cd0-fa92-4b0c-acc9-fb2ada952ab8', `resolveLocal - not a local URL: ${url}`);
@ -214,7 +243,7 @@ export class Resolver {
} else {
return this.apRendererService.renderNote(note, author);
}
});
}) as Promise<IObjectWithId>;
case 'users':
return this.usersRepository.findOneByOrFail({ id: parsed.id })
.then(user => this.apRendererService.renderPerson(user as MiLocalUser));
@ -224,7 +253,7 @@ export class Resolver {
this.notesRepository.findOneByOrFail({ id: parsed.id }),
this.pollsRepository.findOneByOrFail({ noteId: parsed.id }),
])
.then(([note, poll]) => this.apRendererService.renderQuestion({ id: note.userId }, note, poll));
.then(([note, poll]) => this.apRendererService.renderQuestion({ id: note.userId }, note, poll)) as Promise<IObjectWithId>;
case 'likes':
return this.noteReactionsRepository.findOneByOrFail({ id: parsed.id }).then(async reaction =>
this.apRendererService.addContext(await this.apRendererService.renderLike(reaction, { uri: null })));
@ -290,7 +319,10 @@ export class ApResolverService {
}
@bindThis
public createResolver(): Resolver {
public createResolver(opts?: {
// Override the recursion limit
recursionLimit?: number,
}): Resolver {
return new Resolver(
this.config,
this.meta,
@ -308,6 +340,7 @@ export class ApResolverService {
this.loggerService,
this.apLogService,
this.apUtilityService,
opts?.recursionLimit,
);
}
}

View file

@ -39,6 +39,10 @@ export interface IObject {
sensitive?: boolean;
}
export interface IObjectWithId extends IObject {
id: string;
}
/**
* Get array of ActivityStreams Objects id
*/
@ -403,6 +407,13 @@ export interface IMove extends IActivity {
target: IObject | string;
}
export const validActivityTypes = ['Announce', 'Create', 'Update', 'Delete', 'Undo', 'Follow', 'Accept', 'Reject', 'Add', 'Remove', 'Like', 'Dislike', 'EmojiReaction', 'EmojiReact', 'Flag', 'Block', 'Move'];
export const isActivity = (object: IObject): object is IActivity => {
const type = getApType(object);
return type != null && validActivityTypes.includes(type);
};
export const isApObject = (object: string | IObject): object is IObject => typeof(object) === 'object';
export const isCreate = (object: IObject): object is ICreate => getApType(object) === 'Create';
export const isDelete = (object: IObject): object is IDelete => getApType(object) === 'Delete';

View file

@ -1,4 +1,11 @@
export function fromTuple<T>(value: T | [T]): T {
/*
* SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export function fromTuple<T>(value: T | [T]): T;
export function fromTuple<T>(value: T | [T] | T[]): T | undefined;
export function fromTuple<T>(value: T | [T] | T[]): T | undefined {
if (Array.isArray(value)) {
return value[0];
}

View file

@ -25,13 +25,14 @@ import { JsonLdService } from '@/core/activitypub/JsonLdService.js';
import { ApInboxService } from '@/core/activitypub/ApInboxService.js';
import { bindThis } from '@/decorators.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import { CollapsedQueue } from '@/misc/collapsed-queue.js';
import { MiNote } from '@/models/Note.js';
//import { CollapsedQueue } from '@/misc/collapsed-queue.js';
//import { MiNote } from '@/models/Note.js';
import { MiMeta } from '@/models/Meta.js';
import { DI } from '@/di-symbols.js';
import { SkApInboxLog } from '@/models/_.js';
import type { Config } from '@/config.js';
import { ApLogService, calculateDurationSince } from '@/core/ApLogService.js';
import { UpdateInstanceQueue } from '@/core/UpdateInstanceQueue.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
import type { InboxJobData } from '../types.js';
@ -43,7 +44,7 @@ type UpdateInstanceJob = {
@Injectable()
export class InboxProcessorService implements OnApplicationShutdown {
private logger: Logger;
private updateInstanceQueue: CollapsedQueue<MiNote['id'], UpdateInstanceJob>;
//private updateInstanceQueue: CollapsedQueue<MiNote['id'], UpdateInstanceJob>;
constructor(
@Inject(DI.meta)
@ -64,9 +65,10 @@ export class InboxProcessorService implements OnApplicationShutdown {
private federationChart: FederationChart,
private queueLoggerService: QueueLoggerService,
private readonly apLogService: ApLogService,
private readonly updateInstanceQueue: UpdateInstanceQueue,
) {
this.logger = this.queueLoggerService.logger.createSubLogger('inbox');
this.updateInstanceQueue = new CollapsedQueue(process.env.NODE_ENV !== 'test' ? 60 * 1000 * 5 : 0, this.collapseUpdateInstanceJobs, this.performUpdateInstance);
//this.updateInstanceQueue = new CollapsedQueue(process.env.NODE_ENV !== 'test' ? 60 * 1000 * 5 : 0, this.collapseUpdateInstanceJobs, this.performUpdateInstance);
}
@bindThis
@ -232,7 +234,7 @@ export class InboxProcessorService implements OnApplicationShutdown {
const signerHost = this.utilityService.extractDbHost(authUser.user.uri!);
const activityIdHost = this.utilityService.extractDbHost(activity.id);
if (signerHost !== activityIdHost) {
throw new Bull.UnrecoverableError(`skip: signerHost(${signerHost}) !== activity.id host(${activityIdHost}`);
throw new Bull.UnrecoverableError(`skip: signerHost(${signerHost}) !== activity.id host(${activityIdHost})`);
}
} else {
// Activity ID should only be string or undefined.
@ -333,9 +335,7 @@ export class InboxProcessorService implements OnApplicationShutdown {
}
@bindThis
public async dispose(): Promise<void> {
await this.updateInstanceQueue.performAllNow();
}
public async dispose(): Promise<void> {}
@bindThis
async onApplicationShutdown(signal?: string) {

View file

@ -8,7 +8,7 @@ import type { ApDbResolverService } from '@/core/activitypub/ApDbResolverService
import type { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import type { ApRequestService } from '@/core/activitypub/ApRequestService.js';
import { Resolver } from '@/core/activitypub/ApResolverService.js';
import type { IObject } from '@/core/activitypub/type.js';
import type { IObject, IObjectWithId } from '@/core/activitypub/type.js';
import type { HttpRequestService } from '@/core/HttpRequestService.js';
import type { InstanceActorService } from '@/core/InstanceActorService.js';
import type { LoggerService } from '@/core/LoggerService.js';
@ -25,6 +25,7 @@ import type {
} from '@/models/_.js';
import { ApLogService } from '@/core/ApLogService.js';
import { ApUtilityService } from '@/core/activitypub/ApUtilityService.js';
import { fromTuple } from '@/misc/from-tuple.js';
type MockResponse = {
type: string;
@ -72,8 +73,11 @@ export class MockResolver extends Resolver {
return this.#remoteGetTrials;
}
public async resolve(value: string | [string]): Promise<IObjectWithId>;
public async resolve(value: string | IObject | [string | IObject]): Promise<IObject>;
@bindThis
public async resolve(value: string | IObject): Promise<IObject> {
public async resolve(value: string | IObject | [string | IObject]): Promise<IObject> {
value = fromTuple(value);
if (typeof value !== 'string') return value;
this.#remoteGetTrials.push(value);

View file

@ -2,6 +2,7 @@
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
@ -27,10 +28,10 @@ import { DI } from '@/di-symbols.js';
import { secureRndstr } from '@/misc/secure-rndstr.js';
import { DownloadService } from '@/core/DownloadService.js';
import { genAidx } from '@/misc/id/aidx.js';
import { IdService } from '@/core/IdService.js';
import { MockResolver } from '../misc/mock-resolver.js';
import { UserKeypairService } from '@/core/UserKeypairService.js';
import { MemoryKVCache, RedisKVCache } from '@/misc/cache.js';
import { IdService } from '@/core/IdService.js';
const host = 'https://host1.test';

View file

@ -41,6 +41,7 @@ export default [
'@typescript-eslint/prefer-nullish-coalescing': ['warn', {
ignorePrimitives: true,
}],
'no-param-reassign': 'off',
},
},
];