forked from FoundKeyGang/FoundKey
Implement FEP-e232 qoutes
Changelog: Added
This commit is contained in:
commit
e3fd371f4a
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 { 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');
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -111,6 +111,16 @@ export default async function renderNote(note: Note, dive = true, isTalk = false
|
||||||
...apemojis,
|
...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 ? {
|
const asPoll = poll ? {
|
||||||
type: 'Question',
|
type: 'Question',
|
||||||
content: await toHtml(text, note.mentions),
|
content: await toHtml(text, note.mentions),
|
||||||
|
|
|
@ -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';
|
||||||
|
|
Loading…
Reference in a new issue