diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index 46a78687f3..23a52a248a 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -34,7 +34,7 @@ import { UtilityService } from '@/core/UtilityService.js'; import { JsonLdService } from './JsonLdService.js'; import { ApMfmService } from './ApMfmService.js'; import { CONTEXT } from './misc/contexts.js'; -import { getApId, IOrderedCollection, IOrderedCollectionPage } from './type.js'; +import { getApId, ILink, IOrderedCollection, IOrderedCollectionPage } from './type.js'; import type { IAccept, IActivity, IAdd, IAnnounce, IApDocument, IApEmoji, IApHashtag, IApImage, IApMention, IBlock, ICreate, IDelete, IFlag, IFollow, IKey, ILike, IMove, IObject, IPost, IQuestion, IReject, IRemove, ITombstone, IUndo, IUpdate } from './type.js'; @Injectable() @@ -419,7 +419,7 @@ export class ApRendererService { inReplyTo = null; } - let quote; + let quote: string | undefined = undefined; if (note.renoteId) { const renote = await this.notesRepository.findOneBy({ id: note.renoteId }); @@ -500,12 +500,22 @@ export class ApRendererService { const emojis = await this.getEmojis(note.emojis); const apemojis = emojis.filter(emoji => !emoji.localOnly).map(emoji => this.renderEmoji(emoji)); - const tag = [ + const tag: IObject[] = [ ...hashtagTags, ...mentionTags, ...apemojis, ]; + // https://codeberg.org/fediverse/fep/src/branch/main/fep/e232/fep-e232.md + if (quote) { + tag.push({ + type: 'Link', + mediaType: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + rel: 'https://misskey-hub.net/ns#_misskey_quote', + href: quote, + } satisfies ILink); + } + const asPoll = poll ? { type: 'Question', [poll.expiresAt && poll.expiresAt < new Date() ? 'closed' : 'endTime']: poll.expiresAt, @@ -537,6 +547,8 @@ export class ApRendererService { _misskey_quote: quote, quoteUrl: quote, quoteUri: quote, + // https://codeberg.org/fediverse/fep/src/branch/main/fep/044f/fep-044f.md + quote: quote, published: this.idService.parse(note.id).date.toISOString(), to, cc, @@ -774,7 +786,7 @@ export class ApRendererService { inReplyTo = null; } - let quote; + let quote: string | undefined = undefined; if (note.renoteId) { const renote = await this.notesRepository.findOneBy({ id: note.renoteId }); @@ -852,12 +864,22 @@ export class ApRendererService { const emojis = await this.getEmojis(note.emojis); const apemojis = emojis.filter(emoji => !emoji.localOnly).map(emoji => this.renderEmoji(emoji)); - const tag = [ + const tag: IObject[] = [ ...hashtagTags, ...mentionTags, ...apemojis, ]; + // https://codeberg.org/fediverse/fep/src/branch/main/fep/e232/fep-e232.md + if (quote) { + tag.push({ + type: 'Link', + mediaType: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + rel: 'https://misskey-hub.net/ns#_misskey_quote', + href: quote, + } satisfies ILink); + } + const asPoll = poll ? { type: 'Question', [poll.expiresAt && poll.expiresAt < new Date() ? 'closed' : 'endTime']: poll.expiresAt, @@ -886,6 +908,8 @@ export class ApRendererService { _misskey_quote: quote, quoteUrl: quote, quoteUri: quote, + // https://codeberg.org/fediverse/fep/src/branch/main/fep/044f/fep-044f.md + quote: quote, published: this.idService.parse(note.id).date.toISOString(), to, cc, diff --git a/packages/backend/src/core/activitypub/misc/contexts.ts b/packages/backend/src/core/activitypub/misc/contexts.ts index cedd1d8dd5..fa003b1791 100644 --- a/packages/backend/src/core/activitypub/misc/contexts.ts +++ b/packages/backend/src/core/activitypub/misc/contexts.ts @@ -540,6 +540,10 @@ const extension_context_definition = { quoteUrl: 'as:quoteUrl', fedibird: 'http://fedibird.com/ns#', quoteUri: 'fedibird:quoteUri', + quote: { + '@id': 'https://w3id.org/fep/044f#quote', + '@type': '@id', + }, // Mastodon toot: 'http://joinmastodon.org/ns#', Emoji: 'toot:Emoji', diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index 57d4303982..2a28405121 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -27,7 +27,7 @@ import { checkHttps } from '@/misc/check-https.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; import { isRetryableError } from '@/misc/is-retryable-error.js'; import { renderInlineError } from '@/misc/render-inline-error.js'; -import { getOneApId, getApId, validPost, isEmoji, getApType, isApObject, isDocument, IApDocument } from '../type.js'; +import { getOneApId, getApId, validPost, isEmoji, getApType, isApObject, isDocument, IApDocument, isLink } from '../type.js'; import { ApLoggerService } from '../ApLoggerService.js'; import { ApMfmService } from '../ApMfmService.js'; import { ApDbResolverService } from '../ApDbResolverService.js'; @@ -657,9 +657,29 @@ export class ApNoteService { */ private async getQuote(note: IPost, entryUri: string, resolver: Resolver): Promise { const quoteUris = new Set(); - if (note._misskey_quote) quoteUris.add(note._misskey_quote); - if (note.quoteUrl) quoteUris.add(note.quoteUrl); - if (note.quoteUri) quoteUris.add(note.quoteUri); + if (note._misskey_quote && typeof(note._misskey_quote as unknown) === 'string') quoteUris.add(note._misskey_quote); + if (note.quoteUrl && typeof(note.quoteUrl as unknown) === 'string') quoteUris.add(note.quoteUrl); + if (note.quoteUri && typeof(note.quoteUri as unknown) === 'string') quoteUris.add(note.quoteUri); + + // https://codeberg.org/fediverse/fep/src/branch/main/fep/044f/fep-044f.md + if (note.quote && typeof(note.quote as unknown) === 'string') quoteUris.add(note.quote); + + // https://codeberg.org/fediverse/fep/src/branch/main/fep/e232/fep-e232.md + const tags = toArray(note.tag).filter(tag => typeof(tag) === 'object' && isLink(tag)); + for (const tag of tags) { + if (!tag.href || typeof (tag.href as unknown) !== 'string') continue; + + const mediaTypes = toArray(tag.mediaType); + if ( + !mediaTypes.includes('application/ld+json; profile="https://www.w3.org/ns/activitystreams"') && + !mediaTypes.includes('application/activity+json') + ) continue; + + const rels = toArray(tag.rel); + if (!rels.includes('https://misskey-hub.net/ns#_misskey_quote')) continue; + + quoteUris.add(tag.href); + } // No quote, return undefined if (quoteUris.size < 1) return undefined; diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts index cc7599d394..554420d670 100644 --- a/packages/backend/src/core/activitypub/type.ts +++ b/packages/backend/src/core/activitypub/type.ts @@ -35,6 +35,7 @@ export interface IObject { mediaType?: string; url?: ApObject | string; href?: string; + rel?: string | string[]; tag?: IObject | IObject[]; sensitive?: boolean; } @@ -55,6 +56,16 @@ export function isAnonymousObject(object: IObject): object is IAnonymousObject { return object.id === undefined; } +export interface ILink extends IObject { + '@context'?: string | string[] | Obj | Obj[]; + type: 'Link' | 'Mention'; + href: string; +} + +export const isLink = (object: IObject): object is ILink => + (getApType(object) === 'Link' || getApType(object) === 'Link') && + typeof object.href === 'string'; + /** * Get array of ActivityStreams Objects id */ @@ -204,6 +215,7 @@ export interface IPost extends IObject { _misskey_content?: string; quoteUrl?: string; quoteUri?: string; + quote?: string; updated?: string; } @@ -306,9 +318,8 @@ export const isPropertyValue = (object: IObject): object is IApPropertyValue => 'value' in object && typeof object.value === 'string'; -export interface IApMention extends IObject { +export interface IApMention extends ILink { type: 'Mention'; - href: string; name: string; }