diff options
| author | dakkar <dakkar@thenautilus.net> | 2024-11-23 10:41:33 +0000 |
|---|---|---|
| committer | dakkar <dakkar@thenautilus.net> | 2024-11-23 10:41:33 +0000 |
| commit | 6c13dc04f2d1a7b192d42d5b2a02fddbeb3617c7 (patch) | |
| tree | 307fcabbac6985abb8d59fa5c16ce021d1c1c7c4 /packages/backend/src | |
| parent | fix some lints for frontend (diff) | |
| parent | merge: Move `cypress` to `optionalDependencies` (!697) (diff) | |
| download | sharkey-6c13dc04f2d1a7b192d42d5b2a02fddbeb3617c7.tar.gz sharkey-6c13dc04f2d1a7b192d42d5b2a02fddbeb3617c7.tar.bz2 sharkey-6c13dc04f2d1a7b192d42d5b2a02fddbeb3617c7.zip | |
Merge branch 'develop' into feature/2024.10
Diffstat (limited to 'packages/backend/src')
8 files changed, 86 insertions, 17 deletions
diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index d1ae13d706..87b2b620d7 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -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, 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, 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'; @@ -166,7 +166,7 @@ export class ApInboxService { } else if (isAnnounce(activity)) { return await this.announce(actor, activity, resolver); } else if (isLike(activity)) { - return await this.like(actor, activity); + return await this.like(actor, activity, resolver); } else if (isUndo(activity)) { return await this.undo(actor, activity, resolver); } else if (isBlock(activity)) { @@ -198,10 +198,13 @@ export class ApInboxService { } @bindThis - private async like(actor: MiRemoteUser, activity: ILike): Promise<string> { + private async like(actor: MiRemoteUser, activity: ILike, resolver?: Resolver): Promise<string> { const targetUri = getApId(activity.object); - const note = await this.apNoteService.fetchNote(targetUri); + const object = fromTuple(activity.object); + if (!object) return 'skip: activity has no object property'; + + const note = await this.apNoteService.resolveNote(object, { resolver }); if (!note) return `skip: target note not found ${targetUri}`; await this.apNoteService.extractEmojis(activity.tag ?? [], actor.host).catch(() => null); @@ -272,8 +275,12 @@ export class ApInboxService { } if (activity.target === actor.featured) { - const object = fromTuple(activity.object); - const note = await this.apNoteService.resolveNote(object, { resolver }); + const activityObject = fromTuple(activity.object); + if (isApObject(activityObject) && !isPost(activityObject)) { + return `unsupported featured object type: ${getApType(activityObject)}`; + } + + const note = await this.apNoteService.resolveNote(activityObject, { resolver }); if (note == null) return 'note not found'; await this.notePiningService.addPinned(actor, note.id); return; @@ -386,7 +393,7 @@ export class ApInboxService { } @bindThis - private async create(actor: MiRemoteUser, activity: ICreate, resolver?: Resolver): Promise<string | void> { + private async create(actor: MiRemoteUser, activity: ICreate | IUpdate, resolver?: Resolver): Promise<string | void> { const uri = getApId(activity); this.logger.info(`Create: ${uri}`); @@ -421,14 +428,14 @@ export class ApInboxService { }); if (isPost(object)) { - await this.createNote(resolver, actor, object, false, activity); + await this.createNote(resolver, actor, object, false); } else { return `Unknown type: ${getApType(object)}`; } } @bindThis - private async createNote(resolver: Resolver, actor: MiRemoteUser, note: IObject, silent = false, activity?: ICreate): Promise<string> { + private async createNote(resolver: Resolver, actor: MiRemoteUser, note: IObject, silent = false): Promise<string> { const uri = getApId(note); if (typeof note === 'object') { @@ -643,6 +650,10 @@ export class ApInboxService { if (activity.target === actor.featured) { const activityObject = fromTuple(activity.object); + if (isApObject(activityObject) && !isPost(activityObject)) { + return `unsupported featured object type: ${getApType(activityObject)}`; + } + const note = await this.apNoteService.resolveNote(activityObject, { resolver }); if (note == null) return 'note not found'; await this.notePiningService.removePinned(actor, note.id); @@ -787,7 +798,7 @@ export class ApInboxService { } @bindThis - private async update(actor: MiRemoteUser, activity: IUpdate, resolver?: Resolver): Promise<string> { + private async update(actor: MiRemoteUser, activity: IUpdate, resolver?: Resolver): Promise<string | void> { if (actor.uri !== activity.actor) { return 'skip: invalid actor'; } @@ -806,9 +817,19 @@ export class ApInboxService { await this.apPersonService.updatePerson(actor.uri, resolver, object); return 'ok: Person updated'; } else if (getApType(object) === 'Question') { + // If we get an Update(Question) for a note that doesn't exist, then create it instead + if (!await this.apNoteService.hasNote(object)) { + return await this.create(actor, activity, resolver); + } + await this.apQuestionService.updateQuestion(object, actor, resolver).catch(err => console.error(err)); 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 + if (!await this.apNoteService.hasNote(object)) { + return await this.create(actor, activity, resolver); + } + await this.apNoteService.updateNote(object, actor, resolver).catch(err => console.error(err)); return 'ok: Note updated'; } else { diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index 51dd357cc5..7d65944488 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -143,6 +143,15 @@ export class ApNoteService { } /** + * Returns true if the provided object / ID exists in the local database. + */ + @bindThis + public async hasNote(object: string | IObject | [string | IObject]): Promise<boolean> { + const uri = getApId(object); + return await this.notesRepository.existsBy({ uri }); + } + + /** * Noteを作成します。 */ @bindThis diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts index c5c0bb8c94..8b1218df47 100644 --- a/packages/backend/src/core/activitypub/type.ts +++ b/packages/backend/src/core/activitypub/type.ts @@ -343,6 +343,7 @@ export interface IMove extends IActivity { target: IObject | string; } +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'; export const isUpdate = (object: IObject): object is IUpdate => getApType(object) === 'Update'; diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts index 3ad4577706..d6504a798a 100644 --- a/packages/backend/src/core/entities/NoteEntityService.ts +++ b/packages/backend/src/core/entities/NoteEntityService.ts @@ -16,6 +16,7 @@ import { bindThis } from '@/decorators.js'; import { DebounceLoader } from '@/misc/loader.js'; import { IdService } from '@/core/IdService.js'; import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js'; +import { isPackedPureRenote } from '@/misc/is-renote.js'; import type { OnModuleInit } from '@nestjs/common'; import type { CacheService } from '../CacheService.js'; import type { CustomEmojiService } from '../CustomEmojiService.js'; @@ -180,10 +181,9 @@ export class NoteEntityService implements OnModuleInit { } else { // フォロワーかどうか // TODO: 当関数呼び出しごとにクエリが走るのは重そうだからなんとかする - const appearNote = packedNote.renote ?? packedNote; const isFollowing = await this.followingsRepository.exists({ where: { - followeeId: appearNote.userId, + followeeId: packedNote.userId, followerId: meId, }, }); @@ -193,6 +193,14 @@ export class NoteEntityService implements OnModuleInit { } } + // If this is a pure renote (boost), then we should *also* check the boosted note's visibility. + // Otherwise we can have empty notes on the timeline, which is not good. + // Notes are packed in depth-first order, so we can safely grab the "isHidden" property to avoid duplicated checks. + // This is pulled out to ensure that we check both the renote *and* the boosted note. + if (packedNote.renote?.isHidden && isPackedPureRenote(packedNote)) { + hide = true; + } + if (!hide && meId && packedNote.userId !== meId) { const isBlocked = (await this.cacheService.userBlockedCache.fetch(meId)).has(packedNote.userId); diff --git a/packages/backend/src/misc/is-renote.ts b/packages/backend/src/misc/is-renote.ts index 43193d12f5..d6872de46a 100644 --- a/packages/backend/src/misc/is-renote.ts +++ b/packages/backend/src/misc/is-renote.ts @@ -71,6 +71,14 @@ type PackedQuote = fileIds: NonNullable<Packed<'Note'>['fileIds']> }); +type PackedPureRenote = PackedRenote & { + text: NonNullable<Packed<'Note'>['text']>; + cw: NonNullable<Packed<'Note'>['cw']>; + replyId: NonNullable<Packed<'Note'>['replyId']>; + poll: NonNullable<Packed<'Note'>['poll']>; + fileIds: NonNullable<Packed<'Note'>['fileIds']>; +} + export function isRenotePacked(note: Packed<'Note'>): note is PackedRenote { return note.renoteId != null; } @@ -82,3 +90,7 @@ export function isQuotePacked(note: PackedRenote): note is PackedQuote { note.poll != null || (note.fileIds != null && note.fileIds.length > 0); } + +export function isPackedPureRenote(note: Packed<'Note'>): note is PackedPureRenote { + return isRenotePacked(note) && !isQuotePacked(note); +} diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts index 28a74bbb4a..dd183cd991 100644 --- a/packages/backend/src/queue/QueueProcessorService.ts +++ b/packages/backend/src/queue/QueueProcessorService.ts @@ -11,6 +11,7 @@ import { DI } from '@/di-symbols.js'; import type Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; import { CheckModeratorsActivityProcessorService } from '@/queue/processors/CheckModeratorsActivityProcessorService.js'; +import { StatusError } from '@/misc/status-error.js'; import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js'; import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js'; import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js'; @@ -134,7 +135,7 @@ export class QueueProcessorService implements OnApplicationShutdown { // 何故かeがundefinedで来ることがある if (!e) return '?'; - if (e instanceof Bull.UnrecoverableError || e.name === 'AbortError') { + if (e instanceof Bull.UnrecoverableError || e.name === 'AbortError' || e instanceof StatusError) { return `${e.name}: ${e.message}`; } @@ -148,12 +149,15 @@ export class QueueProcessorService implements OnApplicationShutdown { function renderJob(job?: Bull.Job) { if (!job) return '?'; - return { - name: job.name || undefined, + const info: Record<string, string> = { info: getJobInfo(job), - failedReason: job.failedReason || undefined, data: job.data, }; + + if (job.name) info.name = job.name; + if (job.failedReason) info.failedReason = job.failedReason; + + return info; } //#region system diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts index f5a356db55..9026331dff 100644 --- a/packages/backend/src/queue/processors/InboxProcessorService.ts +++ b/packages/backend/src/queue/processors/InboxProcessorService.ts @@ -7,6 +7,7 @@ import { URL } from 'node:url'; import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import httpSignature from '@peertube/http-signature'; import * as Bull from 'bullmq'; +import { AbortError } from 'node-fetch'; import type Logger from '@/logger.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js'; @@ -238,6 +239,19 @@ export class InboxProcessorService implements OnApplicationShutdown { return e.message; } } + + if (e instanceof StatusError) { + if (e.isRetryable) { + return `temporary error ${e.statusCode}`; + } else { + return `skip: permanent error ${e.statusCode}`; + } + } + + if (e instanceof AbortError) { + return 'request aborted'; + } + throw e; } return 'ok'; diff --git a/packages/backend/src/server/api/openapi/gen-spec.ts b/packages/backend/src/server/api/openapi/gen-spec.ts index fb5954fee0..82ee0f47d7 100644 --- a/packages/backend/src/server/api/openapi/gen-spec.ts +++ b/packages/backend/src/server/api/openapi/gen-spec.ts @@ -183,7 +183,7 @@ export function genOpenapiSpec(config: Config, includeSelfRef = false) { }, ...(endpoint.meta.limit ? { '429': { - description: 'To many requests', + description: 'Too many requests', content: { 'application/json': { schema: { |