server: implement FEP-e232 style quotes #318

Manually merged
Johann150 merged 5 commits from federation-quote into main 2023-01-17 20:51:55 +00:00
4 changed files with 88 additions and 28 deletions

View file

@ -24,7 +24,7 @@ import { DbResolver } from '../db-resolver.js';
import { apLogger } from '../logger.js';
import { resolvePerson } from './person.js';
import { resolveImage } from './image.js';
import { extractApHashtags } from './tag.js';
import { extractApHashtags, extractQuoteUrl } from './tag.js';
import { extractPollFromQuestion } from './question.js';
import { extractApMentions } from './mention.js';
@ -154,10 +154,10 @@ export async function createNote(value: string | IObject, resolver: Resolver, si
})
: null;
// 引用
let quote: Note | undefined | null;
const quoteUrl = extractQuoteUrl(note.tag);
if (note._misskey_quote || note.quoteUri) {
if (quoteUrl || note._misskey_quote || note.quoteUri) {
const tryResolveNote = async (uri: string): Promise<{
status: 'ok';
res: Note | null;
@ -184,10 +184,16 @@ export async function createNote(value: string | IObject, resolver: Resolver, si
}
};
const uris = unique([note._misskey_quote, note.quoteUri].filter((x): x is string => typeof x === 'string'));
const results = await Promise.all(uris.map(uri => tryResolveNote(uri)));
quote = results.filter((x): x is { status: 'ok', res: Note | null } => x.status === 'ok').map(x => x.res).find(x => x);
const uris = unique([quoteUrl, note._misskey_quote, note.quoteUri].filter((x): x is string => typeof x === 'string'));
// check the urls sequentially and abort early to not do unnecessary HTTP requests
// picks the first one that works
for (const uri in uris) {
const res = await tryResolveNote(uri);
if (res.status === 'ok') {
quote = res.res;
break;
}
}
if (!quote) {
if (results.some(x => x.status === 'temperror')) {
throw new Error('quote resolve failed');

View file

@ -1,5 +1,5 @@
import { toArray } from '@/prelude/array.js';
import { IObject, isHashtag, IApHashtag } from '../type.js';
import { IObject, isHashtag, IApHashtag, isLink, ILink } from '../type.js';
export function extractApHashtags(tags: IObject | IObject[] | null | undefined) {
if (tags == null) return [];
@ -16,3 +16,34 @@ export function extractApHashtagObjects(tags: IObject | IObject[] | null | undef
if (tags == null) return [];
return toArray(tags).filter(isHashtag);
}
// implements FEP-e232: Object Links (2022-12-23 version)
export function extractQuoteUrl(tags: IObject | IObject[] | null | undefined): string | null {
if (tags == null) return null;
// filter out correct links
let quotes: ILink[] = toArray(tags)
.filter(isLink)
.filter(link =>
[
'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
'application/activity+json'
].includes(link.mediaType?.toLowerCase())
Johann150 marked this conversation as resolved
Review

i'd instead do toArray(tags).filter(hasRel) because the link relation is what is doing all the semantic heavy-lifting

i'd instead do `toArray(tags).filter(hasRel)` because the link relation is what is doing all the semantic heavy-lifting
Review

This part is to implement the FEP because it says that this media type is required.

This part is to implement the FEP because it says that this media type is required.
)
.filter(link =>
toArray(link.rel)
.some(rel =>
[
'https://misskey-hub.net/ns#_misskey_quote',
'http://fedibird.com/ns#quoteUri',
'https://www.w3.org/ns/activitystreams#quoteUrl',
].includes(rel)
)
);
if (quotes.length === 0) return null;
// Deduplicate by href.
// If there is more than one quote, we just pick the first/a random one.
quotes.filter((x, i, arr) => arr.findIndex(y => x.href === y.href) === i)[0].href;
}

View file

@ -111,6 +111,16 @@ export default async function renderNote(note: Note, dive = true, isTalk = false
...apemojis,
];
if (quote) {
tag.push({
type: 'Link',
mediaType: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
href: quote,
name: `RE: ${quote}`,
rel: 'https://misskey-hub.net/ns#_misskey_quote',
});
}
const asPoll = poll ? {
type: 'Question',
content: await toHtml(text, note.mentions),

View file

@ -45,7 +45,7 @@ export function getOneApId(value: ApObject): string {
/**
* Get ActivityStreams Object id
*/
export function getApId(value: string | IObject): string {
export function getApId(value: string | Object): string {
if (typeof value === 'string') return value;
if (typeof value.id === 'string') return value.id;
throw new Error('cannot detemine id');
@ -54,7 +54,7 @@ export function getApId(value: string | IObject): string {
/**
* Get ActivityStreams Object type
*/
export function getApType(value: IObject): string {
export function getApType(value: Object): string {
if (typeof value.type === 'string') return value.type;
if (Array.isArray(value.type) && typeof value.type[0] === 'string') return value.type[0];
throw new Error('cannot detect type');
@ -196,24 +196,6 @@ export const isPropertyValue = (object: IObject): object is IApPropertyValue =>
typeof object.name === 'string' &&
typeof (object as any).value === 'string';
export interface IApMention extends IObject {
type: 'Mention';
href: string;
}
export const isMention = (object: IObject): object is IApMention =>
getApType(object) === 'Mention' &&
typeof object.href === 'string';
export interface IApHashtag extends IObject {
type: 'Hashtag';
name: string;
}
export const isHashtag = (object: IObject): object is IApHashtag =>
getApType(object) === 'Hashtag' &&
typeof object.name === 'string';
export interface IApEmoji extends IObject {
type: 'Emoji';
updated: Date;
@ -293,3 +275,34 @@ export const isLike = (object: IObject): object is ILike => getApType(object) ==
export const isAnnounce = (object: IObject): object is IAnnounce => getApType(object) === 'Announce';
export const isBlock = (object: IObject): object is IBlock => getApType(object) === 'Block';
export const isFlag = (object: IObject): object is IFlag => getApType(object) === 'Flag';
export interface ILink {
href: string;
rel?: string | string[];
mediaType?: string;
name?: string;
}
export interface IApMention extends ILink {
type: 'Mention';
}
export interface IApHashtag extends ILink {
type: 'Hashtag';
name: string;
}
export const isLink = (object: Record<string, any>): object is ILink =>
typeof object.href === 'string'
&& (
object.rel == undefined
|| typeof object.rel === 'string'
|| (Array.isArray(object.rel) && object.rel.every(x => typeof x === 'string'))
)
&& (object.mediaType == undefined || typeof object.mediaType === 'string');
export const isMention = (object: Record<string, any>): object is IApMention =>
getApType(object) === 'Mention' && isLink(object);
export const isHashtag = (object: Record<string, any>): object is IApHashtag =>
getApType(object) === 'Hashtag'
&& isLink(object)
&& typeof object.name === 'string';