merge: Implement basic support for fep-e232 and fep-044f quotes (resolves #1097 and #1098) (!1098)

View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/1098

Closes #1097 and #1098

Approved-by: dakkar <dakkar@thenautilus.net>
Approved-by: Marie <github@yuugi.dev>
This commit is contained in:
Hazelnoot 2025-06-12 16:45:07 +00:00
commit ae7767cd73
4 changed files with 70 additions and 11 deletions

View file

@ -34,7 +34,7 @@ import { UtilityService } from '@/core/UtilityService.js';
import { JsonLdService } from './JsonLdService.js'; import { JsonLdService } from './JsonLdService.js';
import { ApMfmService } from './ApMfmService.js'; import { ApMfmService } from './ApMfmService.js';
import { CONTEXT } from './misc/contexts.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'; 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() @Injectable()
@ -419,7 +419,7 @@ export class ApRendererService {
inReplyTo = null; inReplyTo = null;
} }
let quote; let quote: string | undefined = undefined;
if (note.renoteId) { if (note.renoteId) {
const renote = await this.notesRepository.findOneBy({ id: 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 emojis = await this.getEmojis(note.emojis);
const apemojis = emojis.filter(emoji => !emoji.localOnly).map(emoji => this.renderEmoji(emoji)); const apemojis = emojis.filter(emoji => !emoji.localOnly).map(emoji => this.renderEmoji(emoji));
const tag = [ const tag: IObject[] = [
...hashtagTags, ...hashtagTags,
...mentionTags, ...mentionTags,
...apemojis, ...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 ? { const asPoll = poll ? {
type: 'Question', type: 'Question',
[poll.expiresAt && poll.expiresAt < new Date() ? 'closed' : 'endTime']: poll.expiresAt, [poll.expiresAt && poll.expiresAt < new Date() ? 'closed' : 'endTime']: poll.expiresAt,
@ -537,6 +547,8 @@ export class ApRendererService {
_misskey_quote: quote, _misskey_quote: quote,
quoteUrl: quote, quoteUrl: quote,
quoteUri: 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(), published: this.idService.parse(note.id).date.toISOString(),
to, to,
cc, cc,
@ -774,7 +786,7 @@ export class ApRendererService {
inReplyTo = null; inReplyTo = null;
} }
let quote; let quote: string | undefined = undefined;
if (note.renoteId) { if (note.renoteId) {
const renote = await this.notesRepository.findOneBy({ id: 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 emojis = await this.getEmojis(note.emojis);
const apemojis = emojis.filter(emoji => !emoji.localOnly).map(emoji => this.renderEmoji(emoji)); const apemojis = emojis.filter(emoji => !emoji.localOnly).map(emoji => this.renderEmoji(emoji));
const tag = [ const tag: IObject[] = [
...hashtagTags, ...hashtagTags,
...mentionTags, ...mentionTags,
...apemojis, ...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 ? { const asPoll = poll ? {
type: 'Question', type: 'Question',
[poll.expiresAt && poll.expiresAt < new Date() ? 'closed' : 'endTime']: poll.expiresAt, [poll.expiresAt && poll.expiresAt < new Date() ? 'closed' : 'endTime']: poll.expiresAt,
@ -886,6 +908,8 @@ export class ApRendererService {
_misskey_quote: quote, _misskey_quote: quote,
quoteUrl: quote, quoteUrl: quote,
quoteUri: 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(), published: this.idService.parse(note.id).date.toISOString(),
to, to,
cc, cc,

View file

@ -540,6 +540,10 @@ const extension_context_definition = {
quoteUrl: 'as:quoteUrl', quoteUrl: 'as:quoteUrl',
fedibird: 'http://fedibird.com/ns#', fedibird: 'http://fedibird.com/ns#',
quoteUri: 'fedibird:quoteUri', quoteUri: 'fedibird:quoteUri',
quote: {
'@id': 'https://w3id.org/fep/044f#quote',
'@type': '@id',
},
// Mastodon // Mastodon
toot: 'http://joinmastodon.org/ns#', toot: 'http://joinmastodon.org/ns#',
Emoji: 'toot:Emoji', Emoji: 'toot:Emoji',

View file

@ -27,7 +27,7 @@ import { checkHttps } from '@/misc/check-https.js';
import { IdentifiableError } from '@/misc/identifiable-error.js'; import { IdentifiableError } from '@/misc/identifiable-error.js';
import { isRetryableError } from '@/misc/is-retryable-error.js'; import { isRetryableError } from '@/misc/is-retryable-error.js';
import { renderInlineError } from '@/misc/render-inline-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 { ApLoggerService } from '../ApLoggerService.js';
import { ApMfmService } from '../ApMfmService.js'; import { ApMfmService } from '../ApMfmService.js';
import { ApDbResolverService } from '../ApDbResolverService.js'; import { ApDbResolverService } from '../ApDbResolverService.js';
@ -657,9 +657,29 @@ export class ApNoteService {
*/ */
private async getQuote(note: IPost, entryUri: string, resolver: Resolver): Promise<MiNote | null | undefined> { private async getQuote(note: IPost, entryUri: string, resolver: Resolver): Promise<MiNote | null | undefined> {
const quoteUris = new Set<string>(); const quoteUris = new Set<string>();
if (note._misskey_quote) quoteUris.add(note._misskey_quote); if (note._misskey_quote && typeof(note._misskey_quote as unknown) === 'string') quoteUris.add(note._misskey_quote);
if (note.quoteUrl) quoteUris.add(note.quoteUrl); if (note.quoteUrl && typeof(note.quoteUrl as unknown) === 'string') quoteUris.add(note.quoteUrl);
if (note.quoteUri) quoteUris.add(note.quoteUri); 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 // No quote, return undefined
if (quoteUris.size < 1) return undefined; if (quoteUris.size < 1) return undefined;

View file

@ -35,6 +35,7 @@ export interface IObject {
mediaType?: string; mediaType?: string;
url?: ApObject | string; url?: ApObject | string;
href?: string; href?: string;
rel?: string | string[];
tag?: IObject | IObject[]; tag?: IObject | IObject[];
sensitive?: boolean; sensitive?: boolean;
} }
@ -55,6 +56,16 @@ export function isAnonymousObject(object: IObject): object is IAnonymousObject {
return object.id === undefined; 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 * Get array of ActivityStreams Objects id
*/ */
@ -204,6 +215,7 @@ export interface IPost extends IObject {
_misskey_content?: string; _misskey_content?: string;
quoteUrl?: string; quoteUrl?: string;
quoteUri?: string; quoteUri?: string;
quote?: string;
updated?: string; updated?: string;
} }
@ -306,9 +318,8 @@ export const isPropertyValue = (object: IObject): object is IApPropertyValue =>
'value' in object && 'value' in object &&
typeof object.value === 'string'; typeof object.value === 'string';
export interface IApMention extends IObject { export interface IApMention extends ILink {
type: 'Mention'; type: 'Mention';
href: string;
name: string; name: string;
} }