mirror of
https://codeberg.org/yeentown/barkey.git
synced 2025-08-21 18:43:37 +00:00
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:
commit
b1467989a5
13 changed files with 221 additions and 35 deletions
|
@ -677,6 +677,9 @@ seems to do a decent job)
|
||||||
`packages/frontend/src/pages/timeline.vue`,
|
`packages/frontend/src/pages/timeline.vue`,
|
||||||
`packages/frontend/src/ui/deck/tl-column.vue`,
|
`packages/frontend/src/ui/deck/tl-column.vue`,
|
||||||
`packages/frontend/src/widgets/WidgetTimeline.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
|
* if there have been any changes to the federated user data (the
|
||||||
`renderPerson` function in
|
`renderPerson` function in
|
||||||
`packages/backend/src/core/activitypub/ApRendererService.ts`), make
|
`packages/backend/src/core/activitypub/ApRendererService.ts`), make
|
||||||
|
|
|
@ -19,6 +19,7 @@ import { TimeService } from '@/core/TimeService.js';
|
||||||
import { EnvService } from '@/core/EnvService.js';
|
import { EnvService } from '@/core/EnvService.js';
|
||||||
import { ApUtilityService } from '@/core/activitypub/ApUtilityService.js';
|
import { ApUtilityService } from '@/core/activitypub/ApUtilityService.js';
|
||||||
import { ApLogService } from '@/core/ApLogService.js';
|
import { ApLogService } from '@/core/ApLogService.js';
|
||||||
|
import { UpdateInstanceQueue } from '@/core/UpdateInstanceQueue.js';
|
||||||
import { AccountMoveService } from './AccountMoveService.js';
|
import { AccountMoveService } from './AccountMoveService.js';
|
||||||
import { AccountUpdateService } from './AccountUpdateService.js';
|
import { AccountUpdateService } from './AccountUpdateService.js';
|
||||||
import { AnnouncementService } from './AnnouncementService.js';
|
import { AnnouncementService } from './AnnouncementService.js';
|
||||||
|
@ -220,6 +221,7 @@ const $UserRenoteMutingService: Provider = { provide: 'UserRenoteMutingService',
|
||||||
const $UserSearchService: Provider = { provide: 'UserSearchService', useExisting: UserSearchService };
|
const $UserSearchService: Provider = { provide: 'UserSearchService', useExisting: UserSearchService };
|
||||||
const $UserSuspendService: Provider = { provide: 'UserSuspendService', useExisting: UserSuspendService };
|
const $UserSuspendService: Provider = { provide: 'UserSuspendService', useExisting: UserSuspendService };
|
||||||
const $UserAuthService: Provider = { provide: 'UserAuthService', useExisting: UserAuthService };
|
const $UserAuthService: Provider = { provide: 'UserAuthService', useExisting: UserAuthService };
|
||||||
|
const $UpdateInstanceQueue: Provider = { provide: 'UpdateInstanceQueue', useExisting: UpdateInstanceQueue };
|
||||||
const $VideoProcessingService: Provider = { provide: 'VideoProcessingService', useExisting: VideoProcessingService };
|
const $VideoProcessingService: Provider = { provide: 'VideoProcessingService', useExisting: VideoProcessingService };
|
||||||
const $UserWebhookService: Provider = { provide: 'UserWebhookService', useExisting: UserWebhookService };
|
const $UserWebhookService: Provider = { provide: 'UserWebhookService', useExisting: UserWebhookService };
|
||||||
const $SystemWebhookService: Provider = { provide: 'SystemWebhookService', useExisting: SystemWebhookService };
|
const $SystemWebhookService: Provider = { provide: 'SystemWebhookService', useExisting: SystemWebhookService };
|
||||||
|
@ -378,6 +380,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
|
||||||
UserSearchService,
|
UserSearchService,
|
||||||
UserSuspendService,
|
UserSuspendService,
|
||||||
UserAuthService,
|
UserAuthService,
|
||||||
|
UpdateInstanceQueue,
|
||||||
VideoProcessingService,
|
VideoProcessingService,
|
||||||
UserWebhookService,
|
UserWebhookService,
|
||||||
SystemWebhookService,
|
SystemWebhookService,
|
||||||
|
@ -532,6 +535,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
|
||||||
$UserSearchService,
|
$UserSearchService,
|
||||||
$UserSuspendService,
|
$UserSuspendService,
|
||||||
$UserAuthService,
|
$UserAuthService,
|
||||||
|
$UpdateInstanceQueue,
|
||||||
$VideoProcessingService,
|
$VideoProcessingService,
|
||||||
$UserWebhookService,
|
$UserWebhookService,
|
||||||
$SystemWebhookService,
|
$SystemWebhookService,
|
||||||
|
@ -687,6 +691,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
|
||||||
UserSearchService,
|
UserSearchService,
|
||||||
UserSuspendService,
|
UserSuspendService,
|
||||||
UserAuthService,
|
UserAuthService,
|
||||||
|
UpdateInstanceQueue,
|
||||||
VideoProcessingService,
|
VideoProcessingService,
|
||||||
UserWebhookService,
|
UserWebhookService,
|
||||||
SystemWebhookService,
|
SystemWebhookService,
|
||||||
|
@ -840,6 +845,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
|
||||||
$UserSearchService,
|
$UserSearchService,
|
||||||
$UserSuspendService,
|
$UserSuspendService,
|
||||||
$UserAuthService,
|
$UserAuthService,
|
||||||
|
$UpdateInstanceQueue,
|
||||||
$VideoProcessingService,
|
$VideoProcessingService,
|
||||||
$UserWebhookService,
|
$UserWebhookService,
|
||||||
$SystemWebhookService,
|
$SystemWebhookService,
|
||||||
|
|
|
@ -16,7 +16,7 @@ import type { Config } from '@/config.js';
|
||||||
import { StatusError } from '@/misc/status-error.js';
|
import { StatusError } from '@/misc/status-error.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.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 { ApUtilityService } from './activitypub/ApUtilityService.js';
|
||||||
import type { Response } from 'node-fetch';
|
import type { Response } from 'node-fetch';
|
||||||
import type { URL } from 'node:url';
|
import type { URL } from 'node:url';
|
||||||
|
@ -217,7 +217,7 @@ export class HttpRequestService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@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, {
|
const res = await this.send(url, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
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.
|
// The caller (ApResolverService) will verify the ID against the original / entry URL, which ensures that all three match.
|
||||||
this.apUtilityService.assertIdMatchesUrlAuthority(activity, res.url);
|
this.apUtilityService.assertIdMatchesUrlAuthority(activity, res.url);
|
||||||
|
|
||||||
return activity;
|
return activity as IObjectWithId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
|
52
packages/backend/src/core/UpdateInstanceQueue.ts
Normal file
52
packages/backend/src/core/UpdateInstanceQueue.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -32,7 +32,11 @@ import { AbuseReportService } from '@/core/AbuseReportService.js';
|
||||||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
||||||
import { fromTuple } from '@/misc/from-tuple.js';
|
import { fromTuple } from '@/misc/from-tuple.js';
|
||||||
import { IdentifiableError } from '@/misc/identifiable-error.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 { ApNoteService } from './models/ApNoteService.js';
|
||||||
import { ApLoggerService } from './ApLoggerService.js';
|
import { ApLoggerService } from './ApLoggerService.js';
|
||||||
import { ApDbResolverService } from './ApDbResolverService.js';
|
import { ApDbResolverService } from './ApDbResolverService.js';
|
||||||
|
@ -41,7 +45,7 @@ import { ApAudienceService } from './ApAudienceService.js';
|
||||||
import { ApPersonService } from './models/ApPersonService.js';
|
import { ApPersonService } from './models/ApPersonService.js';
|
||||||
import { ApQuestionService } from './models/ApQuestionService.js';
|
import { ApQuestionService } from './models/ApQuestionService.js';
|
||||||
import type { Resolver } from './ApResolverService.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()
|
@Injectable()
|
||||||
export class ApInboxService {
|
export class ApInboxService {
|
||||||
|
@ -88,7 +92,11 @@ export class ApInboxService {
|
||||||
private apQuestionService: ApQuestionService,
|
private apQuestionService: ApQuestionService,
|
||||||
private queueService: QueueService,
|
private queueService: QueueService,
|
||||||
private globalEventService: GlobalEventService,
|
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;
|
this.logger = this.apLoggerService.logger;
|
||||||
}
|
}
|
||||||
|
@ -310,18 +318,19 @@ export class ApInboxService {
|
||||||
const targetUri = getApId(activityObject);
|
const targetUri = getApId(activityObject);
|
||||||
if (targetUri.startsWith('bear:')) return 'skip: bearcaps url not supported.';
|
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}`);
|
this.logger.error(`Resolution failed: ${e}`);
|
||||||
throw e;
|
throw e;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isPost(target)) return await this.announceNote(actor, activity, target);
|
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)}`;
|
return `skip: unknown object type ${getApType(target)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@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);
|
const uri = getApId(activity);
|
||||||
|
|
||||||
if (actor.isSuspended) {
|
if (actor.isSuspended) {
|
||||||
|
@ -343,7 +352,9 @@ export class ApInboxService {
|
||||||
// Announce対象をresolve
|
// Announce対象をresolve
|
||||||
let renote;
|
let renote;
|
||||||
try {
|
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';
|
if (renote == null) return 'announce target is null';
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// 対象が4xxならスキップ
|
// 対象が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
|
@bindThis
|
||||||
private async block(actor: MiRemoteUser, activity: IBlock): Promise<string> {
|
private async block(actor: MiRemoteUser, activity: IBlock): Promise<string> {
|
||||||
// ※ activity.objectにブロック対象があり、それは存在するローカルユーザーのはず
|
// ※ activity.objectにブロック対象があり、それは存在するローカルユーザーのはず
|
||||||
|
|
|
@ -17,7 +17,7 @@ import { LoggerService } from '@/core/LoggerService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js';
|
import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js';
|
||||||
import type { IObject } from './type.js';
|
import type { IObject, IObjectWithId } from './type.js';
|
||||||
|
|
||||||
type Request = {
|
type Request = {
|
||||||
url: string;
|
url: string;
|
||||||
|
@ -185,7 +185,7 @@ export class ApRequestService {
|
||||||
* @param followAlternate
|
* @param followAlternate
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@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 _followAlternate = followAlternate ?? true;
|
||||||
const keypair = await this.userKeypairService.getUserKeypair(user.id);
|
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.
|
// The caller (ApResolverService) will verify the ID against the original / entry URL, which ensures that all three match.
|
||||||
this.apUtilityService.assertIdMatchesUrlAuthority(activity, res.url);
|
this.apUtilityService.assertIdMatchesUrlAuthority(activity, res.url);
|
||||||
|
|
||||||
return activity;
|
return activity as IObjectWithId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,11 +19,11 @@ import { fromTuple } from '@/misc/from-tuple.js';
|
||||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||||
import { ApLogService, calculateDurationSince, extractObjectContext } from '@/core/ApLogService.js';
|
import { ApLogService, calculateDurationSince, extractObjectContext } from '@/core/ApLogService.js';
|
||||||
import { ApUtilityService } from '@/core/activitypub/ApUtilityService.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 { ApDbResolverService } from './ApDbResolverService.js';
|
||||||
import { ApRendererService } from './ApRendererService.js';
|
import { ApRendererService } from './ApRendererService.js';
|
||||||
import { ApRequestService } from './ApRequestService.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 {
|
export class Resolver {
|
||||||
private history: Set<string>;
|
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
|
@bindThis
|
||||||
public async resolve(value: string | IObject | [string | IObject]): Promise<IObject> {
|
public async resolve(value: string | IObject | [string | IObject]): Promise<IObject> {
|
||||||
// eslint-disable-next-line no-param-reassign
|
// 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 startTime = process.hrtime.bigint();
|
||||||
|
|
||||||
const log = await this.apLogService.createFetchLog({
|
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('#')) {
|
if (value.includes('#')) {
|
||||||
// URLs with fragment parts cannot be resolved correctly because
|
// URLs with fragment parts cannot be resolved correctly because
|
||||||
// the fragment part does not get transmitted over HTTP(S).
|
// the fragment part does not get transmitted over HTTP(S).
|
||||||
|
@ -141,7 +170,7 @@ export class Resolver {
|
||||||
this.history.add(value);
|
this.history.add(value);
|
||||||
|
|
||||||
if (this.utilityService.isSelfHost(host)) {
|
if (this.utilityService.isSelfHost(host)) {
|
||||||
return await this.resolveLocal(value);
|
return await this.resolveLocal(value) as IObjectWithId;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.utilityService.isFederationAllowedHost(host)) {
|
if (!this.utilityService.isFederationAllowedHost(host)) {
|
||||||
|
@ -153,8 +182,8 @@ export class Resolver {
|
||||||
}
|
}
|
||||||
|
|
||||||
const object = (this.user
|
const object = (this.user
|
||||||
? await this.apRequestService.signedGet(value, this.user) as IObject
|
? await this.apRequestService.signedGet(value, this.user)
|
||||||
: await this.httpRequestService.getActivityJson(value)) as IObject;
|
: await this.httpRequestService.getActivityJson(value));
|
||||||
|
|
||||||
if (log) {
|
if (log) {
|
||||||
const { object: objectOnly, context, contextHash } = extractObjectContext(object);
|
const { object: objectOnly, context, contextHash } = extractObjectContext(object);
|
||||||
|
@ -199,7 +228,7 @@ export class Resolver {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private resolveLocal(url: string): Promise<IObject> {
|
private resolveLocal(url: string): Promise<IObjectWithId> {
|
||||||
const parsed = this.apDbResolverService.parseUri(url);
|
const parsed = this.apDbResolverService.parseUri(url);
|
||||||
if (!parsed.local) throw new IdentifiableError('02b40cd0-fa92-4b0c-acc9-fb2ada952ab8', `resolveLocal - not a local URL: ${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 {
|
} else {
|
||||||
return this.apRendererService.renderNote(note, author);
|
return this.apRendererService.renderNote(note, author);
|
||||||
}
|
}
|
||||||
});
|
}) as Promise<IObjectWithId>;
|
||||||
case 'users':
|
case 'users':
|
||||||
return this.usersRepository.findOneByOrFail({ id: parsed.id })
|
return this.usersRepository.findOneByOrFail({ id: parsed.id })
|
||||||
.then(user => this.apRendererService.renderPerson(user as MiLocalUser));
|
.then(user => this.apRendererService.renderPerson(user as MiLocalUser));
|
||||||
|
@ -224,7 +253,7 @@ export class Resolver {
|
||||||
this.notesRepository.findOneByOrFail({ id: parsed.id }),
|
this.notesRepository.findOneByOrFail({ id: parsed.id }),
|
||||||
this.pollsRepository.findOneByOrFail({ noteId: 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':
|
case 'likes':
|
||||||
return this.noteReactionsRepository.findOneByOrFail({ id: parsed.id }).then(async reaction =>
|
return this.noteReactionsRepository.findOneByOrFail({ id: parsed.id }).then(async reaction =>
|
||||||
this.apRendererService.addContext(await this.apRendererService.renderLike(reaction, { uri: null })));
|
this.apRendererService.addContext(await this.apRendererService.renderLike(reaction, { uri: null })));
|
||||||
|
@ -290,7 +319,10 @@ export class ApResolverService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public createResolver(): Resolver {
|
public createResolver(opts?: {
|
||||||
|
// Override the recursion limit
|
||||||
|
recursionLimit?: number,
|
||||||
|
}): Resolver {
|
||||||
return new Resolver(
|
return new Resolver(
|
||||||
this.config,
|
this.config,
|
||||||
this.meta,
|
this.meta,
|
||||||
|
@ -308,6 +340,7 @@ export class ApResolverService {
|
||||||
this.loggerService,
|
this.loggerService,
|
||||||
this.apLogService,
|
this.apLogService,
|
||||||
this.apUtilityService,
|
this.apUtilityService,
|
||||||
|
opts?.recursionLimit,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,6 +39,10 @@ export interface IObject {
|
||||||
sensitive?: boolean;
|
sensitive?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IObjectWithId extends IObject {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get array of ActivityStreams Objects id
|
* Get array of ActivityStreams Objects id
|
||||||
*/
|
*/
|
||||||
|
@ -403,6 +407,13 @@ export interface IMove extends IActivity {
|
||||||
target: IObject | string;
|
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 isApObject = (object: string | IObject): object is IObject => typeof(object) === 'object';
|
||||||
export const isCreate = (object: IObject): object is ICreate => getApType(object) === 'Create';
|
export const isCreate = (object: IObject): object is ICreate => getApType(object) === 'Create';
|
||||||
export const isDelete = (object: IObject): object is IDelete => getApType(object) === 'Delete';
|
export const isDelete = (object: IObject): object is IDelete => getApType(object) === 'Delete';
|
||||||
|
|
|
@ -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)) {
|
if (Array.isArray(value)) {
|
||||||
return value[0];
|
return value[0];
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,13 +25,14 @@ import { JsonLdService } from '@/core/activitypub/JsonLdService.js';
|
||||||
import { ApInboxService } from '@/core/activitypub/ApInboxService.js';
|
import { ApInboxService } from '@/core/activitypub/ApInboxService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||||
import { CollapsedQueue } from '@/misc/collapsed-queue.js';
|
//import { CollapsedQueue } from '@/misc/collapsed-queue.js';
|
||||||
import { MiNote } from '@/models/Note.js';
|
//import { MiNote } from '@/models/Note.js';
|
||||||
import { MiMeta } from '@/models/Meta.js';
|
import { MiMeta } from '@/models/Meta.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { SkApInboxLog } from '@/models/_.js';
|
import { SkApInboxLog } from '@/models/_.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { ApLogService, calculateDurationSince } from '@/core/ApLogService.js';
|
import { ApLogService, calculateDurationSince } from '@/core/ApLogService.js';
|
||||||
|
import { UpdateInstanceQueue } from '@/core/UpdateInstanceQueue.js';
|
||||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||||
import type { InboxJobData } from '../types.js';
|
import type { InboxJobData } from '../types.js';
|
||||||
|
|
||||||
|
@ -43,7 +44,7 @@ type UpdateInstanceJob = {
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class InboxProcessorService implements OnApplicationShutdown {
|
export class InboxProcessorService implements OnApplicationShutdown {
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
private updateInstanceQueue: CollapsedQueue<MiNote['id'], UpdateInstanceJob>;
|
//private updateInstanceQueue: CollapsedQueue<MiNote['id'], UpdateInstanceJob>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.meta)
|
@Inject(DI.meta)
|
||||||
|
@ -64,9 +65,10 @@ export class InboxProcessorService implements OnApplicationShutdown {
|
||||||
private federationChart: FederationChart,
|
private federationChart: FederationChart,
|
||||||
private queueLoggerService: QueueLoggerService,
|
private queueLoggerService: QueueLoggerService,
|
||||||
private readonly apLogService: ApLogService,
|
private readonly apLogService: ApLogService,
|
||||||
|
private readonly updateInstanceQueue: UpdateInstanceQueue,
|
||||||
) {
|
) {
|
||||||
this.logger = this.queueLoggerService.logger.createSubLogger('inbox');
|
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
|
@bindThis
|
||||||
|
@ -232,7 +234,7 @@ export class InboxProcessorService implements OnApplicationShutdown {
|
||||||
const signerHost = this.utilityService.extractDbHost(authUser.user.uri!);
|
const signerHost = this.utilityService.extractDbHost(authUser.user.uri!);
|
||||||
const activityIdHost = this.utilityService.extractDbHost(activity.id);
|
const activityIdHost = this.utilityService.extractDbHost(activity.id);
|
||||||
if (signerHost !== activityIdHost) {
|
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 {
|
} else {
|
||||||
// Activity ID should only be string or undefined.
|
// Activity ID should only be string or undefined.
|
||||||
|
@ -333,9 +335,7 @@ export class InboxProcessorService implements OnApplicationShutdown {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async dispose(): Promise<void> {
|
public async dispose(): Promise<void> {}
|
||||||
await this.updateInstanceQueue.performAllNow();
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
async onApplicationShutdown(signal?: string) {
|
async onApplicationShutdown(signal?: string) {
|
||||||
|
|
|
@ -8,7 +8,7 @@ import type { ApDbResolverService } from '@/core/activitypub/ApDbResolverService
|
||||||
import type { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
import type { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||||
import type { ApRequestService } from '@/core/activitypub/ApRequestService.js';
|
import type { ApRequestService } from '@/core/activitypub/ApRequestService.js';
|
||||||
import { Resolver } from '@/core/activitypub/ApResolverService.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 { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||||
import type { InstanceActorService } from '@/core/InstanceActorService.js';
|
import type { InstanceActorService } from '@/core/InstanceActorService.js';
|
||||||
import type { LoggerService } from '@/core/LoggerService.js';
|
import type { LoggerService } from '@/core/LoggerService.js';
|
||||||
|
@ -25,6 +25,7 @@ import type {
|
||||||
} from '@/models/_.js';
|
} from '@/models/_.js';
|
||||||
import { ApLogService } from '@/core/ApLogService.js';
|
import { ApLogService } from '@/core/ApLogService.js';
|
||||||
import { ApUtilityService } from '@/core/activitypub/ApUtilityService.js';
|
import { ApUtilityService } from '@/core/activitypub/ApUtilityService.js';
|
||||||
|
import { fromTuple } from '@/misc/from-tuple.js';
|
||||||
|
|
||||||
type MockResponse = {
|
type MockResponse = {
|
||||||
type: string;
|
type: string;
|
||||||
|
@ -72,8 +73,11 @@ export class MockResolver extends Resolver {
|
||||||
return this.#remoteGetTrials;
|
return this.#remoteGetTrials;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async resolve(value: string | [string]): Promise<IObjectWithId>;
|
||||||
|
public async resolve(value: string | IObject | [string | IObject]): Promise<IObject>;
|
||||||
@bindThis
|
@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;
|
if (typeof value !== 'string') return value;
|
||||||
|
|
||||||
this.#remoteGetTrials.push(value);
|
this.#remoteGetTrials.push(value);
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
process.env.NODE_ENV = 'test';
|
process.env.NODE_ENV = 'test';
|
||||||
|
|
||||||
import * as assert from 'assert';
|
import * as assert from 'assert';
|
||||||
|
@ -27,10 +28,10 @@ import { DI } from '@/di-symbols.js';
|
||||||
import { secureRndstr } from '@/misc/secure-rndstr.js';
|
import { secureRndstr } from '@/misc/secure-rndstr.js';
|
||||||
import { DownloadService } from '@/core/DownloadService.js';
|
import { DownloadService } from '@/core/DownloadService.js';
|
||||||
import { genAidx } from '@/misc/id/aidx.js';
|
import { genAidx } from '@/misc/id/aidx.js';
|
||||||
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { MockResolver } from '../misc/mock-resolver.js';
|
import { MockResolver } from '../misc/mock-resolver.js';
|
||||||
import { UserKeypairService } from '@/core/UserKeypairService.js';
|
import { UserKeypairService } from '@/core/UserKeypairService.js';
|
||||||
import { MemoryKVCache, RedisKVCache } from '@/misc/cache.js';
|
import { MemoryKVCache, RedisKVCache } from '@/misc/cache.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
|
||||||
|
|
||||||
const host = 'https://host1.test';
|
const host = 'https://host1.test';
|
||||||
|
|
||||||
|
|
|
@ -41,6 +41,7 @@ export default [
|
||||||
'@typescript-eslint/prefer-nullish-coalescing': ['warn', {
|
'@typescript-eslint/prefer-nullish-coalescing': ['warn', {
|
||||||
ignorePrimitives: true,
|
ignorePrimitives: true,
|
||||||
}],
|
}],
|
||||||
|
'no-param-reassign': 'off',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
Loading…
Add table
Reference in a new issue