server: parse quote tag syntax

Ref: FEP-e232
This commit is contained in:
Johann150 2023-01-05 22:19:34 +01:00
parent 9bdf24d3a5
commit 5893a44ff5
Signed by untrusted user: Johann150
GPG key ID: 9EE6577A2A06F8F1
3 changed files with 78 additions and 28 deletions

View file

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

View file

@ -1,5 +1,5 @@
import { toArray } from '@/prelude/array.js'; 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) { export function extractApHashtags(tags: IObject | IObject[] | null | undefined) {
if (tags == null) return []; if (tags == null) return [];
@ -16,3 +16,34 @@ export function extractApHashtagObjects(tags: IObject | IObject[] | null | undef
if (tags == null) return []; if (tags == null) return [];
return toArray(tags).filter(isHashtag); 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())
)
.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

@ -45,7 +45,7 @@ export function getOneApId(value: ApObject): string {
/** /**
* Get ActivityStreams Object id * 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 === 'string') return value;
if (typeof value.id === 'string') return value.id; if (typeof value.id === 'string') return value.id;
throw new Error('cannot detemine id'); throw new Error('cannot detemine id');
@ -54,7 +54,7 @@ export function getApId(value: string | IObject): string {
/** /**
* Get ActivityStreams Object type * 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 (typeof value.type === 'string') return value.type;
if (Array.isArray(value.type) && typeof value.type[0] === 'string') return value.type[0]; if (Array.isArray(value.type) && typeof value.type[0] === 'string') return value.type[0];
throw new Error('cannot detect type'); throw new Error('cannot detect type');
@ -196,24 +196,6 @@ export const isPropertyValue = (object: IObject): object is IApPropertyValue =>
typeof object.name === 'string' && typeof object.name === 'string' &&
typeof (object as any).value === '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 { export interface IApEmoji extends IObject {
type: 'Emoji'; type: 'Emoji';
updated: Date; 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 isAnnounce = (object: IObject): object is IAnnounce => getApType(object) === 'Announce';
export const isBlock = (object: IObject): object is IBlock => getApType(object) === 'Block'; export const isBlock = (object: IObject): object is IBlock => getApType(object) === 'Block';
export const isFlag = (object: IObject): object is IFlag => getApType(object) === 'Flag'; 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';