From 9a7418db3d7c2613938c8d4195887ae0e5de09b2 Mon Sep 17 00:00:00 2001 From: mei23 Date: Sat, 23 Mar 2024 11:23:48 +0900 Subject: [PATCH 1/4] parse --- src/misc/get-quote.ts | 62 ++++++++++++++++++++ src/remote/activitypub/type.ts | 10 ++++ test/get-quote.ts | 103 +++++++++++++++++++++++++++++++++ 3 files changed, 175 insertions(+) create mode 100644 src/misc/get-quote.ts create mode 100644 test/get-quote.ts diff --git a/src/misc/get-quote.ts b/src/misc/get-quote.ts new file mode 100644 index 000000000000..b626e7fb20c7 --- /dev/null +++ b/src/misc/get-quote.ts @@ -0,0 +1,62 @@ +import { removeNull, toArray } from '../prelude/array'; +import { IObject, IPost, isLink } from '../remote/activitypub/type'; + +export function getQuote(post: IPost) { + // Misskey + if (typeof post._misskey_quote === 'string') return { href: post._misskey_quote } + // Fedibird + if (typeof post.quoteUri === 'string') return { href: post.quoteUri } + // Biwakodon + if (typeof post.quoteUrl === 'string') return { href: post.quoteUrl } + + // FEP-e232: Object Links + // https://codeberg.org/fediverse/fep/src/branch/main/fep/e232/fep-e232.md + const fepe232Tags = parseFepE232Tags(toArray(post.tag)); + const fepe232Quote = fepe232Tags.filter(x => x.rel === 'https://misskey-hub.net/ns#_misskey_quote')[0]; + if (fepe232Quote) { + return { + href: fepe232Quote.href, + name: fepe232Quote.name, + }; + } + + return null; +} + +//#region FEP-e232 +type FepE232Tag = { + type: 'Link'; + mediaType: string; + href: string; + name?: string; + rel?: string; +}; + +function parseFepE232Tags(tags: IObject[]): FepE232Tag[] { + return removeNull(tags.map(x => parseFepE232Tag(x))); +} + +function parseFepE232Tag(tag: IObject): FepE232Tag | null { + if (!isLink(tag)) return null; + if (!validateContentType(tag.mediaType)) return null; + if (typeof tag.href !== 'string') return null; + + return { + type: tag.type, + mediaType: tag.mediaType, + href: tag.href, + name: typeof tag.name === 'string' ? tag.name : undefined, + rel: typeof tag.rel === 'string' ? tag.rel : undefined, + }; +} + +function validateContentType(contentType: unknown): contentType is string { + if (contentType == null) return false; + if (typeof contentType !== 'string') return false; + + const parts = contentType.split(/\s*;\s*/); + if (parts[0] === 'application/activity+json') return true; + if (parts[0] !== 'application/ld+json') return false; + return parts.slice(1).some(part => part.trim() === 'profile="https://www.w3.org/ns/activitystreams"'); +} +//#endregion diff --git a/src/remote/activitypub/type.ts b/src/remote/activitypub/type.ts index 64738c3cd207..a4f0afed5749 100644 --- a/src/remote/activitypub/type.ts +++ b/src/remote/activitypub/type.ts @@ -230,6 +230,16 @@ export const isHashtag = (object: IObject): object is IApHashtag => getApType(object) === 'Hashtag' && typeof object.name === 'string'; +export interface IApLink extends IObject { + type: 'Link'; + href?: string; + rel?: string; + mediaType?: string; + name?: string; +}; + +export const isLink = (object: IObject): object is IApLink => getApType(object) === 'Link'; + export interface IActor extends IObject { type: 'Person' | 'Service' | 'Organization' | 'Group' | 'Application'; name?: string; diff --git a/test/get-quote.ts b/test/get-quote.ts new file mode 100644 index 000000000000..82e76f0a17d3 --- /dev/null +++ b/test/get-quote.ts @@ -0,0 +1,103 @@ +import * as assert from 'assert'; +import { getQuote } from '../src/misc/get-quote'; + +describe('getQuote', () => { + it('_misskey_quote', () => { + assert.deepStrictEqual(getQuote({ + _misskey_quote: 'https://example.com/notes/quote', + } as any), { + href: 'https://example.com/notes/quote', + }); + }); + + it('quoteUri', () => { + assert.deepStrictEqual(getQuote({ + quoteUri: 'https://example.com/notes/quote', + } as any), { + href: 'https://example.com/notes/quote', + }); + }); + + it('quoteUrl', () => { + assert.deepStrictEqual(getQuote({ + quoteUrl: 'https://example.com/notes/quote', + } as any), { + href: 'https://example.com/notes/quote', + }); + }); + + it('FEP-e232', () => { + assert.deepStrictEqual(getQuote({ + tag: [ + { + type: 'Link', + mediaType: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + rel: 'https://misskey-hub.net/ns#_misskey_quote', + href: 'https://example.com/notes/quote', + name: 'RE: https://example.com/html/quote', + } + ], + } as any), { + href: 'https://example.com/notes/quote', + name: 'RE: https://example.com/html/quote', + }); + }); + + it('FEP-e232 application/activity+json', () => { + assert.deepStrictEqual(getQuote({ + tag: [ + { + type: 'Link', + mediaType: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + rel: 'https://misskey-hub.net/ns#_misskey_quote', + href: 'https://example.com/notes/quote', + name: 'RE: https://example.com/html/quote', + } + ], + } as any), { + href: 'https://example.com/notes/quote', + name: 'RE: https://example.com/html/quote', + }); + }); + + it('FEP-e232 invalid mediaType', () => { + assert.deepStrictEqual(getQuote({ + tag: [ + { + type: 'Link', + mediaType: 'text/html', + rel: 'https://misskey-hub.net/ns#_misskey_quote', + href: 'https://example.com/notes/quote', + name: 'RE: https://example.com/html/quote', + } + ], + } as any), null); + }); + + it('FEP-e232 no Link', () => { + assert.deepStrictEqual(getQuote({ + tag: [ + { + type: 'Link2', + mediaType: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + rel: 'https://misskey-hub.net/ns#_misskey_quote', + href: 'https://example.com/notes/quote', + name: 'RE: https://example.com/html/quote', + } + ], + } as any), null); + }); + + it('FEP-e232 no match rel', () => { + assert.deepStrictEqual(getQuote({ + tag: [ + { + type: 'Link', + mediaType: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + href: 'https://example.com/notes/quote', + name: 'RE: https://example.com/html/quote', + } + ], + } as any), null); + }); +}); From 23354888d29a873b404a96e29a9f586a6f75c27b Mon Sep 17 00:00:00 2001 From: mei23 Date: Sat, 23 Mar 2024 11:28:39 +0900 Subject: [PATCH 2/4] get and replace --- src/remote/activitypub/models/note.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/remote/activitypub/models/note.ts b/src/remote/activitypub/models/note.ts index 18205d3ea4ba..e526b762b686 100644 --- a/src/remote/activitypub/models/note.ts +++ b/src/remote/activitypub/models/note.ts @@ -23,6 +23,7 @@ import { parseAudience } from '../audience'; import DbResolver from '../db-resolver'; import { parseDate, parseDateWithLimit } from '../misc/date'; import { StatusError } from '../../../misc/fetch'; +import { getQuote } from '../../../misc/get-quote'; const logger = apLogger; @@ -113,8 +114,8 @@ export async function createNote(value: string | IObject, resolver?: Resolver | const reply = note.inReplyTo ? await resolveNote(getOneApId(note.inReplyTo), resolver) : null; // 引用 - const q = note._misskey_quote || note.quoteUri || note.quoteUrl; - const quote = q ? await resolveNote(q, resolver) : null; + const quoteInfo = getQuote(note); + const quote = quoteInfo?.href ? await resolveNote(quoteInfo?.href, resolver) : null; // 参照 const references = await fetchReferences(note, resolver).catch(() => []); @@ -127,6 +128,8 @@ export async function createNote(value: string | IObject, resolver?: Resolver | : note.content ? htmlToMfm(note.content, note.tag) : null; + if (text && quoteInfo?.name) text.replace(quoteInfo.name, ''); + // 投票 if (reply && reply.poll) { const tryCreateVote = async (name: string, index: number): Promise => { From c54407bc704b2cda8d1f34d9a71c6ecfb09a9120 Mon Sep 17 00:00:00 2001 From: mei23 Date: Sat, 23 Mar 2024 11:54:48 +0900 Subject: [PATCH 3/4] render --- src/remote/activitypub/renderer/note.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/remote/activitypub/renderer/note.ts b/src/remote/activitypub/renderer/note.ts index ce8044ad9bd9..835ba9af683c 100644 --- a/src/remote/activitypub/renderer/note.ts +++ b/src/remote/activitypub/renderer/note.ts @@ -106,10 +106,19 @@ export default async function renderNote(note: INote, dive = true): Promise const emojis = await getEmojis(note.emojis); const apemojis = emojis.map(emoji => renderEmoji(emoji)); + const fepE232Quote = { + type: 'Link', + mediaType: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + rel: 'https://misskey-hub.net/ns#_misskey_quote', + href: quote, + name: `RE: ${quote}`, + }; + const tag = [ ...hashtagTags, ...mentionTags, ...apemojis, + fepE232Quote, ]; const { From 8060e85574275f6a19d30c4e56e63a9636259c4e Mon Sep 17 00:00:00 2001 From: mei23 Date: Sat, 23 Mar 2024 14:57:02 +0900 Subject: [PATCH 4/4] w --- src/misc/get-quote.ts | 38 ++++++++++++++++++++++--- src/remote/activitypub/models/person.ts | 2 +- src/remote/activitypub/type.ts | 2 -- test/get-quote.ts | 8 ------ 4 files changed, 35 insertions(+), 15 deletions(-) diff --git a/src/misc/get-quote.ts b/src/misc/get-quote.ts index b626e7fb20c7..d77837a2b638 100644 --- a/src/misc/get-quote.ts +++ b/src/misc/get-quote.ts @@ -1,18 +1,37 @@ import { removeNull, toArray } from '../prelude/array'; import { IObject, IPost, isLink } from '../remote/activitypub/type'; -export function getQuote(post: IPost) { +/** + * rels treated as quote + */ +const relQuotes = new Set([ + 'https://misskey-hub.net/ns#_misskey_quote', + 'http://fedibird.com/ns#quoteUri', +]); + +/** + * Misskey like quote + */ +type Quote = { + /** Target AP object ID */ + href: string; + /** Fallback text */ + name?: string; +}; + +/** + * Get one Misskey like quote + */ +export function getQuote(post: IPost): Quote | null { // Misskey if (typeof post._misskey_quote === 'string') return { href: post._misskey_quote } // Fedibird if (typeof post.quoteUri === 'string') return { href: post.quoteUri } - // Biwakodon - if (typeof post.quoteUrl === 'string') return { href: post.quoteUrl } // FEP-e232: Object Links // https://codeberg.org/fediverse/fep/src/branch/main/fep/e232/fep-e232.md const fepe232Tags = parseFepE232Tags(toArray(post.tag)); - const fepe232Quote = fepe232Tags.filter(x => x.rel === 'https://misskey-hub.net/ns#_misskey_quote')[0]; + const fepe232Quote = fepe232Tags.filter(x => x.rel && relQuotes.has(x.rel))[0]; if (fepe232Quote) { return { href: fepe232Quote.href, @@ -23,6 +42,17 @@ export function getQuote(post: IPost) { return null; } +/** + * Get AP links (experimental) + */ +export function getApLinks(post: IPost) { + const fepe232Tags = parseFepE232Tags(toArray(post.tag)); + + // other attachements? + + return fepe232Tags; +} + //#region FEP-e232 type FepE232Tag = { type: 'Link'; diff --git a/src/remote/activitypub/models/person.ts b/src/remote/activitypub/models/person.ts index 1d7f5c7464d6..3e631cf63efc 100644 --- a/src/remote/activitypub/models/person.ts +++ b/src/remote/activitypub/models/person.ts @@ -607,7 +607,7 @@ export async function fetchOutbox(user: IUser) { // Note if (object.inReplyTo) { // skip reply - } else if (object._misskey_quote || object.quoteUri || object.quoteUrl) { + } else if (object._misskey_quote || object.quoteUri) { // skip quote } else { if (++itemCount > 10) break; diff --git a/src/remote/activitypub/type.ts b/src/remote/activitypub/type.ts index a4f0afed5749..91d52e6c43c7 100644 --- a/src/remote/activitypub/type.ts +++ b/src/remote/activitypub/type.ts @@ -141,7 +141,6 @@ export interface IPost extends IObject { mediaType?: string; }; _misskey_quote?: string; - quoteUrl?: string; quoteUri?: string; references: string | ICollection; } @@ -168,7 +167,6 @@ export interface IQuestion extends IObject { mediaType?: string; }; _misskey_quote?: string; - quoteUrl?: string; quoteUri?: string; oneOf?: IQuestionChoice[]; anyOf?: IQuestionChoice[]; diff --git a/test/get-quote.ts b/test/get-quote.ts index 82e76f0a17d3..4049980f861b 100644 --- a/test/get-quote.ts +++ b/test/get-quote.ts @@ -18,14 +18,6 @@ describe('getQuote', () => { }); }); - it('quoteUrl', () => { - assert.deepStrictEqual(getQuote({ - quoteUrl: 'https://example.com/notes/quote', - } as any), { - href: 'https://example.com/notes/quote', - }); - }); - it('FEP-e232', () => { assert.deepStrictEqual(getQuote({ tag: [