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 { 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,

View file

@ -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',

View file

@ -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<MiNote | null | undefined> {
const quoteUris = new Set<string>();
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;

View file

@ -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;
}