server: implement FEP-e232 style quotes #318
4 changed files with 88 additions and 28 deletions
|
@ -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');
|
||||
|
|
|
@ -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
|
||||
)
|
||||
.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;
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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';
|
||||
|
|
Loading…
Reference in a new issue
i'd instead do
toArray(tags).filter(hasRel)
because the link relation is what is doing all the semantic heavy-liftingThis part is to implement the FEP because it says that this media type is required.