diff --git a/packages/backend/src/core/ChatService.ts b/packages/backend/src/core/ChatService.ts index 5cd336a097..565141ea9c 100644 --- a/packages/backend/src/core/ChatService.ts +++ b/packages/backend/src/core/ChatService.ts @@ -28,6 +28,8 @@ import { CustomEmojiService } from '@/core/CustomEmojiService.js'; import { emojiRegex } from '@/misc/emoji-regex.js'; import { NotificationService } from '@/core/NotificationService.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; +import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js'; +import { trackPromise } from '@/misc/promise-tracker.js'; const MAX_ROOM_MEMBERS = 50; const MAX_REACTIONS_PER_MESSAGE = 100; @@ -81,6 +83,7 @@ export class ChatService { private chatEntityService: ChatEntityService, private idService: IdService, private globalEventService: GlobalEventService, + private apDeliverManagerService: ApDeliverManagerService, private apRendererService: ApRendererService, private queueService: QueueService, private pushNotificationService: PushNotificationService, @@ -236,6 +239,19 @@ export class ChatService { }, 3000); } + //#region AP deliver + if (this.userEntityService.isLocalUser(fromUser) && this.userEntityService.isRemoteUser(toUser)) { + (async () => { + const content = await this.apRendererService.renderChatMessage(inserted, false); + const activity = this.apRendererService.addContext(content); + + const dm = this.apDeliverManagerService.createDeliverManager(fromUser, activity); + dm.addDirectRecipe(toUser); + trackPromise(dm.execute()); + })(); + } + //#endregion + return packedMessage; } diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index e88f60b806..786f2078d5 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -457,6 +457,36 @@ export class ApInboxService { } } + @bindThis + private async chatMessage(resolver: Resolver, actor: MiRemoteUser, message: IObject): Promise { + const uri = getApId(message); + + if (typeof message === 'object') { + if (actor.uri !== message.attributedTo) { + return 'skip: actor.uri !== message.attributedTo'; + } + + if (typeof message.id === 'string') { + if (this.utilityService.extractDbHost(actor.uri) !== this.utilityService.extractDbHost(message.id)) { + return 'skip: host in actor.uri !== message.id'; + } + } else { + return 'skip: message.id is not a string'; + } + } + + try { + await this.chatService.createMessageViaAp(message, actor, resolver); + return 'ok'; + } catch (err) { + if (err instanceof StatusError && !err.isRetryable) { + return `skip ${err.statusCode}`; + } else { + throw err; + } + } + } + @bindThis private async delete(actor: MiRemoteUser, activity: IDelete): Promise { if (actor.uri !== activity.actor) { diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index 55521d6e3a..0bf6627491 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -23,7 +23,7 @@ import { MfmService, type Appender } from '@/core/MfmService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import type { MiUserKeypair } from '@/models/UserKeypair.js'; -import type { UsersRepository, UserProfilesRepository, NotesRepository, DriveFilesRepository, PollsRepository, MiMeta } from '@/models/_.js'; +import type { UsersRepository, UserProfilesRepository, NotesRepository, DriveFilesRepository, PollsRepository, MiMeta, MiChatMessage } from '@/models/_.js'; import { bindThis } from '@/decorators.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js'; import { IdService } from '@/core/IdService.js'; @@ -502,6 +502,31 @@ export class ApRendererService { }; } + @bindThis + public async renderChatMessage(message: MiChatMessage, dive = true): Promise { + const attributedTo = this.userEntityService.genLocalUserUri(message.fromUserId); + + const file = message.fileId ? await this.driveFilesRepository.findOneBy({ id: message.fileId }) : null; + + const emojis = await this.getEmojis(message.emojis); + const apemojis = emojis.filter(emoji => !emoji.localOnly).map(emoji => this.renderEmoji(emoji)); + + const tag = [ + ...apemojis, + ]; + + return { + id: `${this.config.url}/chat-messages/${message.id}`, + type: 'Misskey:ChatMessage', + attributedTo, + text: message.text, + published: this.idService.parse(message.id).date.toISOString(), + to: message.toUserId, + attachment: file ? [this.renderDocument(file)] : [], + tag, + }; + } + @bindThis public async renderPerson(user: MiLocalUser) { const id = this.userEntityService.genLocalUserUri(user.id); diff --git a/packages/backend/src/models/ChatMessage.ts b/packages/backend/src/models/ChatMessage.ts index 3d2b64268e..ca647efee9 100644 --- a/packages/backend/src/models/ChatMessage.ts +++ b/packages/backend/src/models/ChatMessage.ts @@ -55,6 +55,8 @@ export class MiChatMessage { }) public text: string | null; + // 連合用 + // ローカルはnull @Column('varchar', { length: 512, nullable: true, }) @@ -82,4 +84,22 @@ export class MiChatMessage { length: 1024, array: true, default: '{}', }) public reactions: string[]; + + // 連合用 + @Column('varchar', { + length: 128, array: true, default: '{}', + }) + public emojis: string[]; + + // 連合用 + @Column('boolean', { + default: false, + }) + public isDelivering: boolean; + + // 連合用 + @Column('boolean', { + default: false, + }) + public isDeliverFailed: boolean; }