diff options
| author | Hazelnoot <acomputerdog@gmail.com> | 2024-10-15 18:01:57 -0400 |
|---|---|---|
| committer | Hazelnoot <acomputerdog@gmail.com> | 2024-10-15 18:09:11 -0400 |
| commit | 8a34d8e9d25546f7ef42f072a69f9923d5ba2e84 (patch) | |
| tree | 4523856296102c786f724243375d198f3fada789 /packages/backend/src/core | |
| parent | Fix indentation on locales/generateDTS.js (diff) | |
| parent | merge: Refresh locales after any change, not just a version update (resolves ... (diff) | |
| download | sharkey-8a34d8e9d25546f7ef42f072a69f9923d5ba2e84.tar.gz sharkey-8a34d8e9d25546f7ef42f072a69f9923d5ba2e84.tar.bz2 sharkey-8a34d8e9d25546f7ef42f072a69f9923d5ba2e84.zip | |
Merge branch 'develop' into feature/2024.9.0
# Conflicts:
# locales/en-US.yml
# locales/ja-JP.yml
# packages/backend/src/core/NoteCreateService.ts
# packages/backend/src/core/NoteDeleteService.ts
# packages/backend/src/core/NoteEditService.ts
# packages/frontend-shared/js/config.ts
# packages/frontend/src/boot/common.ts
# packages/frontend/src/pages/following-feed.vue
# packages/misskey-js/src/autogen/endpoint.ts
Diffstat (limited to 'packages/backend/src/core')
| -rw-r--r-- | packages/backend/src/core/CoreModule.ts | 6 | ||||
| -rw-r--r-- | packages/backend/src/core/LatestNoteService.ts | 139 | ||||
| -rw-r--r-- | packages/backend/src/core/NoteCreateService.ts | 34 | ||||
| -rw-r--r-- | packages/backend/src/core/NoteDeleteService.ts | 62 | ||||
| -rw-r--r-- | packages/backend/src/core/NoteEditService.ts | 10 | ||||
| -rw-r--r-- | packages/backend/src/core/activitypub/ApDbResolverService.ts | 6 | ||||
| -rw-r--r-- | packages/backend/src/core/activitypub/ApInboxService.ts | 45 | ||||
| -rw-r--r-- | packages/backend/src/core/activitypub/ApRendererService.ts | 3 | ||||
| -rw-r--r-- | packages/backend/src/core/activitypub/ApResolverService.ts | 6 | ||||
| -rw-r--r-- | packages/backend/src/core/activitypub/type.ts | 13 | ||||
| -rw-r--r-- | packages/backend/src/core/entities/UserEntityService.ts | 8 |
11 files changed, 217 insertions, 115 deletions
diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts index a192c2f270..c083068392 100644 --- a/packages/backend/src/core/CoreModule.ts +++ b/packages/backend/src/core/CoreModule.ts @@ -43,6 +43,7 @@ import { ModerationLogService } from './ModerationLogService.js'; import { NoteCreateService } from './NoteCreateService.js'; import { NoteEditService } from './NoteEditService.js'; import { NoteDeleteService } from './NoteDeleteService.js'; +import { LatestNoteService } from './LatestNoteService.js'; import { NotePiningService } from './NotePiningService.js'; import { NoteReadService } from './NoteReadService.js'; import { NotificationService } from './NotificationService.js'; @@ -187,6 +188,7 @@ const $ModerationLogService: Provider = { provide: 'ModerationLogService', useEx const $NoteCreateService: Provider = { provide: 'NoteCreateService', useExisting: NoteCreateService }; const $NoteEditService: Provider = { provide: 'NoteEditService', useExisting: NoteEditService }; const $NoteDeleteService: Provider = { provide: 'NoteDeleteService', useExisting: NoteDeleteService }; +const $LatestNoteService: Provider = { provide: 'LatestNoteService', useExisting: LatestNoteService }; const $NotePiningService: Provider = { provide: 'NotePiningService', useExisting: NotePiningService }; const $NoteReadService: Provider = { provide: 'NoteReadService', useExisting: NoteReadService }; const $NotificationService: Provider = { provide: 'NotificationService', useExisting: NotificationService }; @@ -339,6 +341,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp NoteCreateService, NoteEditService, NoteDeleteService, + LatestNoteService, NotePiningService, NoteReadService, NotificationService, @@ -487,6 +490,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp $NoteCreateService, $NoteEditService, $NoteDeleteService, + $LatestNoteService, $NotePiningService, $NoteReadService, $NotificationService, @@ -636,6 +640,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp NoteCreateService, NoteEditService, NoteDeleteService, + LatestNoteService, NotePiningService, NoteReadService, NotificationService, @@ -783,6 +788,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp $NoteCreateService, $NoteEditService, $NoteDeleteService, + $LatestNoteService, $NotePiningService, $NoteReadService, $NotificationService, diff --git a/packages/backend/src/core/LatestNoteService.ts b/packages/backend/src/core/LatestNoteService.ts new file mode 100644 index 0000000000..c379805506 --- /dev/null +++ b/packages/backend/src/core/LatestNoteService.ts @@ -0,0 +1,139 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Not } from 'typeorm'; +import { MiNote } from '@/models/Note.js'; +import { isPureRenote } from '@/misc/is-renote.js'; +import { SkLatestNote } from '@/models/LatestNote.js'; +import { DI } from '@/di-symbols.js'; +import type { LatestNotesRepository, NotesRepository } from '@/models/_.js'; +import { LoggerService } from '@/core/LoggerService.js'; +import Logger from '@/logger.js'; + +@Injectable() +export class LatestNoteService { + private readonly logger: Logger; + + constructor( + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + @Inject(DI.latestNotesRepository) + private latestNotesRepository: LatestNotesRepository, + + loggerService: LoggerService, + ) { + this.logger = loggerService.getLogger('LatestNoteService'); + } + + handleUpdatedNoteBG(before: MiNote, after: MiNote): void { + this + .handleUpdatedNote(before, after) + .catch(err => this.logger.error('Unhandled exception while updating latest_note (after update):', err)); + } + + async handleUpdatedNote(before: MiNote, after: MiNote): Promise<void> { + // If the key didn't change, then there's nothing to update + if (SkLatestNote.areEquivalent(before, after)) return; + + // Simulate update as delete + create + await this.handleDeletedNote(before); + await this.handleCreatedNote(after); + } + + handleCreatedNoteBG(note: MiNote): void { + this + .handleCreatedNote(note) + .catch(err => this.logger.error('Unhandled exception while updating latest_note (after create):', err)); + } + + async handleCreatedNote(note: MiNote): Promise<void> { + // Ignore DMs. + // Followers-only posts are *included*, as this table is used to back the "following" feed. + if (note.visibility === 'specified') return; + + // Ignore pure renotes + if (isPureRenote(note)) return; + + // Compute the compound key of the entry to check + const key = SkLatestNote.keyFor(note); + + // Make sure that this isn't an *older* post. + // We can get older posts through replies, lookups, updates, etc. + const currentLatest = await this.latestNotesRepository.findOneBy(key); + if (currentLatest != null && currentLatest.noteId >= note.id) return; + + // Record this as the latest note for the given user + const latestNote = new SkLatestNote({ + ...key, + noteId: note.id, + }); + await this.latestNotesRepository.upsert(latestNote, ['userId', 'isPublic', 'isReply', 'isQuote']); + } + + handleDeletedNoteBG(note: MiNote): void { + this + .handleDeletedNote(note) + .catch(err => this.logger.error('Unhandled exception while updating latest_note (after delete):', err)); + } + + async handleDeletedNote(note: MiNote): Promise<void> { + // If it's a DM, then it can't possibly be the latest note so we can safely skip this. + if (note.visibility === 'specified') return; + + // If it's a pure renote, then it can't possibly be the latest note so we can safely skip this. + if (isPureRenote(note)) return; + + // Compute the compound key of the entry to check + const key = SkLatestNote.keyFor(note); + + // Check if the deleted note was possibly the latest for the user + const existingLatest = await this.latestNotesRepository.findOneBy(key); + if (existingLatest == null || existingLatest.noteId !== note.id) return; + + // Find the newest remaining note for the user. + // We exclude DMs and pure renotes. + const nextLatest = await this.notesRepository + .createQueryBuilder('note') + .select() + .where({ + userId: key.userId, + visibility: key.isPublic + ? 'public' + : Not('specified'), + replyId: key.isReply + ? Not(null) + : null, + renoteId: key.isQuote + ? Not(null) + : null, + }) + .andWhere(` + ( + note."renoteId" IS NULL + OR note.text IS NOT NULL + OR note.cw IS NOT NULL + OR note."replyId" IS NOT NULL + OR note."hasPoll" + OR note."fileIds" != '{}' + ) + `) + .orderBy({ id: 'DESC' }) + .getOne(); + if (!nextLatest) return; + + // Record it as the latest + const latestNote = new SkLatestNote({ + ...key, + noteId: nextLatest.id, + }); + + // When inserting the latest note, it's possible that another worker has "raced" the insert and already added a newer note. + // We must use orIgnore() to ensure that the query ignores conflicts, otherwise an exception may be thrown. + await this.latestNotesRepository + .createQueryBuilder('latest') + .insert() + .into(SkLatestNote) + .values(latestNote) + .orIgnore() + .execute(); + } +} diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 29f7dd917d..41c1e3f66f 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -14,7 +14,7 @@ import { extractHashtags } from '@/misc/extract-hashtags.js'; import type { IMentionedRemoteUsers } from '@/models/Note.js'; import { MiNote } from '@/models/Note.js'; import { LatestNote } from '@/models/LatestNote.js'; -import type { ChannelFollowingsRepository, ChannelsRepository, FollowingsRepository, InstancesRepository, LatestNotesRepository, MiFollowing, MiMeta, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserListMembershipsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; +import type { ChannelFollowingsRepository, ChannelsRepository, FollowingsRepository, InstancesRepository, MiFollowing, MiMeta, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserListMembershipsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; import type { MiDriveFile } from '@/models/DriveFile.js'; import type { MiApp } from '@/models/App.js'; import { concat } from '@/misc/prelude/array.js'; @@ -58,7 +58,7 @@ import { isReply } from '@/misc/is-reply.js'; import { trackPromise } from '@/misc/promise-tracker.js'; import { isUserRelated } from '@/misc/is-user-related.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; -import { isQuote, isRenote } from '@/misc/is-renote.js'; +import { LatestNoteService } from '@/core/LatestNoteService.js'; import { CollapsedQueue } from '@/misc/collapsed-queue.js'; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; @@ -172,9 +172,6 @@ export class NoteCreateService implements OnApplicationShutdown { @Inject(DI.notesRepository) private notesRepository: NotesRepository, - @Inject(DI.latestNotesRepository) - private latestNotesRepository: LatestNotesRepository, - @Inject(DI.mutingsRepository) private mutingsRepository: MutingsRepository, @@ -225,6 +222,7 @@ export class NoteCreateService implements OnApplicationShutdown { private utilityService: UtilityService, private userBlockingService: UserBlockingService, private cacheService: CacheService, + private latestNoteService: LatestNoteService, ) { this.updateNotesCountQueue = new CollapsedQueue(60 * 1000 * 5, this.collapseNotesCount, this.performUpdateNotesCount); } @@ -530,8 +528,6 @@ export class NoteCreateService implements OnApplicationShutdown { await this.notesRepository.insert(insert); } - await this.updateLatestNote(insert); - return insert; } catch (e) { // duplicate key error @@ -812,6 +808,9 @@ export class NoteCreateService implements OnApplicationShutdown { }); } + // Update the Latest Note index / following feed + this.latestNoteService.handleCreatedNoteBG(note); + // Register to search database if (!user.noindex) this.index(note); } @@ -1151,25 +1150,4 @@ export class NoteCreateService implements OnApplicationShutdown { public async onApplicationShutdown(signal?: string | undefined): Promise<void> { await this.dispose(); } - - private async updateLatestNote(note: MiNote) { - // Ignore DMs. - // Followers-only posts are *included*, as this table is used to back the "following" feed. - if (note.visibility === 'specified') return; - - // Ignore pure renotes - if (isRenote(note) && !isQuote(note)) return; - - // Make sure that this isn't an *older* post. - // We can get older posts through replies, lookups, etc. - const currentLatest = await this.latestNotesRepository.findOneBy({ userId: note.userId }); - if (currentLatest != null && currentLatest.noteId >= note.id) return; - - // Record this as the latest note for the given user - const latestNote = new LatestNote({ - userId: note.userId, - noteId: note.id, - }); - await this.latestNotesRepository.upsert(latestNote, ['userId']); - } } diff --git a/packages/backend/src/core/NoteDeleteService.ts b/packages/backend/src/core/NoteDeleteService.ts index 6ea400b03e..285db9f152 100644 --- a/packages/backend/src/core/NoteDeleteService.ts +++ b/packages/backend/src/core/NoteDeleteService.ts @@ -3,12 +3,11 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Brackets, In, Not } from 'typeorm'; +import { Brackets, In } from 'typeorm'; import { Injectable, Inject } from '@nestjs/common'; import type { MiUser, MiLocalUser, MiRemoteUser } from '@/models/User.js'; -import type { MiNote, IMentionedRemoteUsers } from '@/models/Note.js'; -import { LatestNote } from '@/models/LatestNote.js'; -import type { InstancesRepository, MiMeta, LatestNotesRepository, NotesRepository, UsersRepository } from '@/models/_.js'; +import { MiNote, IMentionedRemoteUsers } from '@/models/Note.js'; +import type { InstancesRepository, MiMeta, NotesRepository, UsersRepository } from '@/models/_.js'; import { RelayService } from '@/core/RelayService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { DI } from '@/di-symbols.js'; @@ -24,6 +23,7 @@ import { bindThis } from '@/decorators.js'; import { SearchService } from '@/core/SearchService.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; import { isQuote, isRenote } from '@/misc/is-renote.js'; +import { LatestNoteService } from '@/core/LatestNoteService.js'; @Injectable() export class NoteDeleteService { @@ -40,9 +40,6 @@ export class NoteDeleteService { @Inject(DI.notesRepository) private notesRepository: NotesRepository, - @Inject(DI.latestNotesRepository) - private latestNotesRepository: LatestNotesRepository, - @Inject(DI.instancesRepository) private instancesRepository: InstancesRepository, @@ -57,6 +54,7 @@ export class NoteDeleteService { private notesChart: NotesChart, private perUserNotesChart: PerUserNotesChart, private instanceChart: InstanceChart, + private latestNoteService: LatestNoteService, ) {} /** @@ -149,7 +147,7 @@ export class NoteDeleteService { userId: user.id, }); - await this.updateLatestNote(note); + this.latestNoteService.handleDeletedNoteBG(note); if (deleter && (note.userId !== deleter.id)) { const user = await this.usersRepository.findOneByOrFail({ id: note.userId }); @@ -232,52 +230,4 @@ export class NoteDeleteService { this.apDeliverManagerService.deliverToUser(user, content, remoteUser); } } - - private async updateLatestNote(note: MiNote) { - // If it's a DM, then it can't possibly be the latest note so we can safely skip this. - if (note.visibility === 'specified') return; - - // Check if the deleted note was possibly the latest for the user - const hasLatestNote = await this.latestNotesRepository.existsBy({ userId: note.userId }); - if (hasLatestNote) return; - - // Find the newest remaining note for the user. - // We exclude DMs and pure renotes. - const nextLatest = await this.notesRepository - .createQueryBuilder('note') - .select() - .where({ - userId: note.userId, - visibility: Not('specified'), - }) - .andWhere(` - ( - note."renoteId" IS NULL - OR note.text IS NOT NULL - OR note.cw IS NOT NULL - OR note."replyId" IS NOT NULL - OR note."hasPoll" - OR note."fileIds" != '{}' - ) - `) - .orderBy({ id: 'DESC' }) - .getOne(); - if (!nextLatest) return; - - // Record it as the latest - const latestNote = new LatestNote({ - userId: note.userId, - noteId: nextLatest.id, - }); - - // When inserting the latest note, it's possible that another worker has "raced" the insert and already added a newer note. - // We must use orIgnore() to ensure that the query ignores conflicts, otherwise an exception may be thrown. - await this.latestNotesRepository - .createQueryBuilder('latest') - .insert() - .into(LatestNote) - .values(latestNote) - .orIgnore() - .execute(); - } } diff --git a/packages/backend/src/core/NoteEditService.ts b/packages/backend/src/core/NoteEditService.ts index 9bccb6c756..4c2b88f8dc 100644 --- a/packages/backend/src/core/NoteEditService.ts +++ b/packages/backend/src/core/NoteEditService.ts @@ -50,6 +50,7 @@ import { isReply } from '@/misc/is-reply.js'; import { trackPromise } from '@/misc/promise-tracker.js'; import { isUserRelated } from '@/misc/is-user-related.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; +import { LatestNoteService } from '@/core/LatestNoteService.js'; import { CollapsedQueue } from '@/misc/collapsed-queue.js'; import { NoteCreateService } from '@/core/NoteCreateService.js'; @@ -217,7 +218,7 @@ export class NoteEditService implements OnApplicationShutdown { private utilityService: UtilityService, private userBlockingService: UserBlockingService, private cacheService: CacheService, - private noteCreateService: NoteCreateService, + private latestNoteService: LatestNoteService, ) { this.updateNotesCountQueue = new CollapsedQueue(60 * 1000 * 5, this.collapseNotesCount, this.performUpdateNotesCount); } @@ -563,7 +564,7 @@ export class NoteEditService implements OnApplicationShutdown { } setImmediate('post edited', { signal: this.#shutdownController.signal }).then( - () => this.postNoteEdited(note, user, data, silent, tags!, mentionedUsers!), + () => this.postNoteEdited(note, oldnote, user, data, silent, tags!, mentionedUsers!), () => { /* aborted, ignore this */ }, ); @@ -574,7 +575,7 @@ export class NoteEditService implements OnApplicationShutdown { } @bindThis - private async postNoteEdited(note: MiNote, user: { + private async postNoteEdited(note: MiNote, oldNote: MiNote, user: { id: MiUser['id']; username: MiUser['username']; host: MiUser['host']; @@ -771,6 +772,9 @@ export class NoteEditService implements OnApplicationShutdown { }); } + // Update the Latest Note index / following feed + this.latestNoteService.handleUpdatedNoteBG(oldNote, note); + // Register to search database if (!user.noindex) this.index(note); } diff --git a/packages/backend/src/core/activitypub/ApDbResolverService.ts b/packages/backend/src/core/activitypub/ApDbResolverService.ts index 062af39732..2cb558dbff 100644 --- a/packages/backend/src/core/activitypub/ApDbResolverService.ts +++ b/packages/backend/src/core/activitypub/ApDbResolverService.ts @@ -59,7 +59,7 @@ export class ApDbResolverService implements OnApplicationShutdown { } @bindThis - public parseUri(value: string | IObject): UriParseResult { + public parseUri(value: string | IObject | [string | IObject]): UriParseResult { const separator = '/'; const uri = new URL(getApId(value)); @@ -78,7 +78,7 @@ export class ApDbResolverService implements OnApplicationShutdown { * AP Note => Misskey Note in DB */ @bindThis - public async getNoteFromApId(value: string | IObject): Promise<MiNote | null> { + public async getNoteFromApId(value: string | IObject | [string | IObject]): Promise<MiNote | null> { const parsed = this.parseUri(value); if (parsed.local) { @@ -98,7 +98,7 @@ export class ApDbResolverService implements OnApplicationShutdown { * AP Person => Misskey User in DB */ @bindThis - public async getUserFromApId(value: string | IObject): Promise<MiLocalUser | MiRemoteUser | null> { + public async getUserFromApId(value: string | IObject | [string | IObject]): Promise<MiLocalUser | MiRemoteUser | null> { const parsed = this.parseUri(value); if (parsed.local) { diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index 9f300e9905..d54c9544c3 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -40,6 +40,7 @@ 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, IObject, IReject, IRemove, IUndo, IUpdate, IMove, IPost } from './type.js'; +import { fromTuple } from '@/misc/from-tuple.js'; @Injectable() export class ApInboxService { @@ -254,7 +255,8 @@ export class ApInboxService { } if (activity.target === actor.featured) { - const note = await this.apNoteService.resolveNote(activity.object); + const object = fromTuple(activity.object); + const note = await this.apNoteService.resolveNote(object); if (note == null) return 'note not found'; await this.notePiningService.addPinned(actor, note.id); return; @@ -271,11 +273,12 @@ export class ApInboxService { const resolver = this.apResolverService.createResolver(); - if (!activity.object) return 'skip: activity has no object property'; - const targetUri = getApId(activity.object); + const activityObject = fromTuple(activity.object); + if (!activityObject) return 'skip: activity has no object property'; + const targetUri = getApId(activityObject); if (targetUri.startsWith('bear:')) return 'skip: bearcaps url not supported.'; - const target = await resolver.resolve(activity.object).catch(e => { + const target = await resolver.resolve(activityObject).catch(e => { this.logger.error(`Resolution failed: ${e}`); return e; }); @@ -370,29 +373,30 @@ export class ApInboxService { this.logger.info(`Create: ${uri}`); - if (!activity.object) return 'skip: activity has no object property'; - const targetUri = getApId(activity.object); + const activityObject = fromTuple(activity.object); + if (!activityObject) return 'skip: activity has no object property'; + const targetUri = getApId(activityObject); if (targetUri.startsWith('bear:')) return 'skip: bearcaps url not supported.'; // copy audiences between activity <=> object. - if (typeof activity.object === 'object') { - const to = unique(concat([toArray(activity.to), toArray(activity.object.to)])); - const cc = unique(concat([toArray(activity.cc), toArray(activity.object.cc)])); + if (typeof activityObject === 'object') { + const to = unique(concat([toArray(activity.to), toArray(activityObject.to)])); + const cc = unique(concat([toArray(activity.cc), toArray(activityObject.cc)])); activity.to = to; activity.cc = cc; - activity.object.to = to; - activity.object.cc = cc; + activityObject.to = to; + activityObject.cc = cc; } // If there is no attributedTo, use Activity actor. - if (typeof activity.object === 'object' && !activity.object.attributedTo) { - activity.object.attributedTo = activity.actor; + if (typeof activityObject === 'object' && !activityObject.attributedTo) { + activityObject.attributedTo = activity.actor; } const resolver = this.apResolverService.createResolver(); - const object = await resolver.resolve(activity.object).catch(e => { + const object = await resolver.resolve(activityObject).catch(e => { this.logger.error(`Resolution failed: ${e}`); throw e; }); @@ -448,15 +452,15 @@ export class ApInboxService { // 削除対象objectのtype let formerType: string | undefined; - if (typeof activity.object === 'string') { + const activityObject = fromTuple(activity.object); + if (typeof activityObject === 'string') { // typeが不明だけど、どうせ消えてるのでremote resolveしない formerType = undefined; } else { - const object = activity.object; - if (isTombstone(object)) { - formerType = toSingle(object.formerType); + if (isTombstone(activityObject)) { + formerType = toSingle(activityObject.formerType); } else { - formerType = toSingle(object.type); + formerType = toSingle(activityObject.type); } } @@ -616,7 +620,8 @@ export class ApInboxService { } if (activity.target === actor.featured) { - const note = await this.apNoteService.resolveNote(activity.object); + const activityObject = fromTuple(activity.object); + const note = await this.apNoteService.resolveNote(activityObject); if (note == null) return 'note not found'; await this.notePiningService.removePinned(actor, note.id); return; diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index 107dfaa630..9e4ccc7019 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -200,7 +200,8 @@ export class ApRendererService { type: 'Flag', actor: this.userEntityService.genLocalUserUri(user.id), content, - object, + // This MUST be an array for Pleroma compatibility: https://activitypub.software/TransFem-org/Sharkey/-/issues/641#note_7301 + object: [object], }; } diff --git a/packages/backend/src/core/activitypub/ApResolverService.ts b/packages/backend/src/core/activitypub/ApResolverService.ts index f9411a1283..5d5c61ce2c 100644 --- a/packages/backend/src/core/activitypub/ApResolverService.ts +++ b/packages/backend/src/core/activitypub/ApResolverService.ts @@ -20,6 +20,7 @@ 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>; @@ -66,7 +67,10 @@ export class Resolver { } @bindThis - public async resolve(value: string | IObject): Promise<IObject> { + public async resolve(value: string | IObject | [string | IObject]): Promise<IObject> { + // eslint-disable-next-line no-param-reassign + value = fromTuple(value); + if (typeof value !== 'string') { return value; } diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts index bb9836fb4e..af5aba9c16 100644 --- a/packages/backend/src/core/activitypub/type.ts +++ b/packages/backend/src/core/activitypub/type.ts @@ -3,6 +3,8 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { fromTuple } from '@/misc/from-tuple.js'; + export type Obj = { [x: string]: any }; export type ApObject = IObject | string | (IObject | string)[]; @@ -53,10 +55,13 @@ export function getOneApId(value: ApObject): string { /** * Get ActivityStreams Object id */ -export function getApId(value: string | IObject): string { +export function getApId(value: string | IObject | [string | IObject]): string { + // eslint-disable-next-line no-param-reassign + value = fromTuple(value); + if (typeof value === 'string') return value; if (typeof value.id === 'string') return value.id; - throw new Error('cannot detemine id'); + throw new Error('cannot determine id'); } /** @@ -85,7 +90,9 @@ export function getApHrefNullable(value: string | IObject | undefined): string | export interface IActivity extends IObject { //type: 'Activity'; actor: IObject | string; - object: IObject | string; + // ActivityPub spec allows for arrays: https://www.w3.org/TR/activitystreams-vocabulary/#properties + // Misskey can only handle one value, so we use a tuple for that case. + object: IObject | string | [IObject | string] ; target?: IObject | string; /** LD-Signature */ signature?: { diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index bb05e8712f..703b07973e 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -375,6 +375,13 @@ export class UserEntityService implements OnModuleInit { } @bindThis + public async getHasPendingSentFollowRequest(userId: MiUser['id']): Promise<boolean> { + return this.followRequestsRepository.existsBy({ + followerId: userId, + }); + } + + @bindThis public getOnlineStatus(user: MiUser): 'unknown' | 'online' | 'active' | 'offline' { if (user.hideOnlineStatus) return 'unknown'; if (user.lastActiveDate == null) return 'unknown'; @@ -643,6 +650,7 @@ export class UserEntityService implements OnModuleInit { hasUnreadChannel: false, // 後方互換性のため hasUnreadNotification: notificationsInfo?.hasUnread, // 後方互換性のため hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id), + hasPendingSentFollowRequest: this.getHasPendingSentFollowRequest(user.id), unreadNotificationsCount: notificationsInfo?.unreadCount, mutedWords: profile!.mutedWords, hardMutedWords: profile!.hardMutedWords, |