Copy some ActivityPub conversion code from Mostr
This commit is contained in:
parent
1923b65099
commit
0d4b9e416c
|
@ -0,0 +1,323 @@
|
|||
import { z } from '@/deps.ts';
|
||||
|
||||
const apId = z.string().url();
|
||||
const recipients = z.array(z.string()).catch([]);
|
||||
const published = () => z.string().datetime().catch(new Date().toISOString());
|
||||
|
||||
/** Validates individual items in an array, dropping any that aren't valid. */
|
||||
function filteredArray<T extends z.ZodTypeAny>(schema: T) {
|
||||
return z.any().array()
|
||||
.transform((arr) => (
|
||||
arr.map((item) => {
|
||||
const parsed = schema.safeParse(item);
|
||||
return parsed.success ? parsed.data : undefined;
|
||||
}).filter((item): item is z.infer<T> => Boolean(item))
|
||||
));
|
||||
}
|
||||
|
||||
const imageSchema = z.object({
|
||||
type: z.literal('Image').catch('Image'),
|
||||
url: z.string().url(),
|
||||
});
|
||||
|
||||
const attachmentSchema = z.object({
|
||||
type: z.literal('Document').catch('Document'),
|
||||
mediaType: z.string().optional().catch(undefined),
|
||||
url: z.string().url(),
|
||||
});
|
||||
|
||||
const mentionSchema = z.object({
|
||||
type: z.literal('Mention'),
|
||||
href: z.string().url(),
|
||||
name: z.string().optional().catch(undefined),
|
||||
});
|
||||
|
||||
const hashtagSchema = z.object({
|
||||
type: z.literal('Hashtag'),
|
||||
href: z.string().url(),
|
||||
name: z.string(),
|
||||
});
|
||||
|
||||
const emojiSchema = z.object({
|
||||
type: z.literal('Emoji'),
|
||||
icon: imageSchema,
|
||||
name: z.string(),
|
||||
});
|
||||
|
||||
const tagSchema = z.discriminatedUnion('type', [
|
||||
mentionSchema,
|
||||
hashtagSchema,
|
||||
emojiSchema,
|
||||
]);
|
||||
|
||||
const propertyValueSchema = z.object({
|
||||
type: z.literal('PropertyValue'),
|
||||
name: z.string(),
|
||||
value: z.string(),
|
||||
verified_at: z.string().nullish(),
|
||||
});
|
||||
|
||||
/** https://codeberg.org/fediverse/fep/src/branch/main/feps/fep-fffd.md */
|
||||
const proxySchema = z.object({
|
||||
protocol: z.string().url(),
|
||||
proxied: z.string(),
|
||||
authoritative: z.boolean().optional().catch(undefined),
|
||||
});
|
||||
|
||||
const personSchema = z.object({
|
||||
type: z.literal('Person'),
|
||||
id: apId,
|
||||
icon: imageSchema.optional().catch(undefined),
|
||||
image: imageSchema.optional().catch(undefined),
|
||||
name: z.string().catch(''),
|
||||
preferredUsername: z.string(),
|
||||
inbox: apId,
|
||||
followers: apId.optional().catch(undefined),
|
||||
following: apId.optional().catch(undefined),
|
||||
outbox: apId.optional().catch(undefined),
|
||||
summary: z.string().catch(''),
|
||||
attachment: filteredArray(propertyValueSchema).catch([]),
|
||||
tag: filteredArray(emojiSchema).catch([]),
|
||||
endpoints: z.object({
|
||||
sharedInbox: apId.optional(),
|
||||
}).optional().catch({}),
|
||||
publicKey: z.object({
|
||||
id: apId,
|
||||
owner: apId,
|
||||
publicKeyPem: z.string(),
|
||||
}).optional().catch(undefined),
|
||||
proxyOf: z.array(proxySchema).optional().catch(undefined),
|
||||
});
|
||||
|
||||
const applicationSchema = personSchema.merge(z.object({ type: z.literal('Application') }));
|
||||
const groupSchema = personSchema.merge(z.object({ type: z.literal('Group') }));
|
||||
const organizationSchema = personSchema.merge(z.object({ type: z.literal('Organization') }));
|
||||
const serviceSchema = personSchema.merge(z.object({ type: z.literal('Service') }));
|
||||
|
||||
const actorSchema = z.discriminatedUnion('type', [
|
||||
personSchema,
|
||||
applicationSchema,
|
||||
groupSchema,
|
||||
organizationSchema,
|
||||
serviceSchema,
|
||||
]);
|
||||
|
||||
const noteSchema = z.object({
|
||||
type: z.literal('Note'),
|
||||
id: apId,
|
||||
to: recipients,
|
||||
cc: recipients,
|
||||
content: z.string(),
|
||||
attachment: z.array(attachmentSchema).optional().catch(undefined),
|
||||
tag: filteredArray(tagSchema).catch([]),
|
||||
inReplyTo: apId.optional().catch(undefined),
|
||||
attributedTo: apId,
|
||||
published: published(),
|
||||
sensitive: z.boolean().optional().catch(undefined),
|
||||
summary: z.string().nullish().catch(undefined),
|
||||
quoteUrl: apId.optional().catch(undefined),
|
||||
source: z.object({
|
||||
content: z.string(),
|
||||
mediaType: z.literal('text/markdown'),
|
||||
}).optional().catch(undefined),
|
||||
proxyOf: z.array(proxySchema).optional().catch(undefined),
|
||||
});
|
||||
|
||||
const flexibleNoteSchema = noteSchema.extend({
|
||||
quoteURL: apId.optional().catch(undefined),
|
||||
quoteUri: apId.optional().catch(undefined),
|
||||
_misskey_quote: apId.optional().catch(undefined),
|
||||
}).transform((note) => {
|
||||
const { quoteUrl, quoteUri, quoteURL, _misskey_quote, ...rest } = note;
|
||||
return {
|
||||
quoteUrl: quoteUrl || quoteUri || quoteURL || _misskey_quote,
|
||||
...rest,
|
||||
};
|
||||
});
|
||||
|
||||
// https://github.com/colinhacks/zod/discussions/2100#discussioncomment-5109781
|
||||
const objectSchema = z.union([
|
||||
flexibleNoteSchema,
|
||||
personSchema,
|
||||
applicationSchema,
|
||||
groupSchema,
|
||||
organizationSchema,
|
||||
serviceSchema,
|
||||
]).pipe(
|
||||
z.discriminatedUnion('type', [
|
||||
noteSchema,
|
||||
personSchema,
|
||||
applicationSchema,
|
||||
groupSchema,
|
||||
organizationSchema,
|
||||
serviceSchema,
|
||||
]),
|
||||
);
|
||||
|
||||
const createNoteSchema = z.object({
|
||||
type: z.literal('Create'),
|
||||
id: apId,
|
||||
to: recipients,
|
||||
cc: recipients,
|
||||
actor: apId,
|
||||
object: noteSchema,
|
||||
published: published(),
|
||||
proxyOf: z.array(proxySchema).optional().catch(undefined),
|
||||
});
|
||||
|
||||
const announceNoteSchema = z.object({
|
||||
type: z.literal('Announce'),
|
||||
id: apId,
|
||||
to: recipients,
|
||||
cc: recipients,
|
||||
actor: apId,
|
||||
object: apId.or(noteSchema),
|
||||
published: published(),
|
||||
proxyOf: z.array(proxySchema).optional().catch(undefined),
|
||||
});
|
||||
|
||||
const followSchema = z.object({
|
||||
type: z.literal('Follow'),
|
||||
id: apId,
|
||||
to: recipients,
|
||||
cc: recipients,
|
||||
actor: apId,
|
||||
object: apId,
|
||||
proxyOf: z.array(proxySchema).optional().catch(undefined),
|
||||
});
|
||||
|
||||
const acceptSchema = z.object({
|
||||
type: z.literal('Accept'),
|
||||
id: apId,
|
||||
actor: apId,
|
||||
to: recipients,
|
||||
cc: recipients,
|
||||
object: apId.or(followSchema),
|
||||
});
|
||||
|
||||
const likeSchema = z.object({
|
||||
type: z.literal('Like'),
|
||||
id: apId,
|
||||
actor: apId,
|
||||
object: apId,
|
||||
to: recipients,
|
||||
cc: recipients,
|
||||
proxyOf: z.array(proxySchema).optional().catch(undefined),
|
||||
});
|
||||
|
||||
const emojiReactSchema = z.object({
|
||||
type: z.literal('EmojiReact'),
|
||||
id: apId,
|
||||
actor: apId,
|
||||
object: apId,
|
||||
content: z.string().refine((v) => /\p{Extended_Pictographic}/u.test(v)),
|
||||
to: recipients,
|
||||
cc: recipients,
|
||||
proxyOf: z.array(proxySchema).optional().catch(undefined),
|
||||
});
|
||||
|
||||
const deleteSchema = z.object({
|
||||
type: z.literal('Delete'),
|
||||
id: apId,
|
||||
actor: apId,
|
||||
object: apId,
|
||||
to: recipients,
|
||||
cc: recipients,
|
||||
proxyOf: z.array(proxySchema).optional().catch(undefined),
|
||||
});
|
||||
|
||||
const updateActorSchema = z.object({
|
||||
type: z.literal('Update'),
|
||||
id: apId,
|
||||
actor: apId,
|
||||
to: recipients,
|
||||
cc: recipients,
|
||||
object: actorSchema,
|
||||
proxyOf: z.array(proxySchema).optional().catch(undefined),
|
||||
});
|
||||
|
||||
/**
|
||||
* A custom Zap activity type we made up, based on:
|
||||
* https://github.com/nostr-protocol/nips/blob/master/57.md
|
||||
*/
|
||||
const zapSchema = z.object({
|
||||
type: z.literal('Zap'),
|
||||
id: apId,
|
||||
actor: apId,
|
||||
object: apId,
|
||||
to: recipients,
|
||||
cc: recipients,
|
||||
proxyOf: z.array(proxySchema).optional().catch(undefined),
|
||||
});
|
||||
|
||||
const activitySchema = z.discriminatedUnion('type', [
|
||||
followSchema,
|
||||
acceptSchema,
|
||||
createNoteSchema,
|
||||
announceNoteSchema,
|
||||
updateActorSchema,
|
||||
likeSchema,
|
||||
emojiReactSchema,
|
||||
deleteSchema,
|
||||
zapSchema,
|
||||
]).refine((activity) => {
|
||||
const ids: string[] = [activity.id];
|
||||
|
||||
if (activity.type === 'Create') {
|
||||
ids.push(
|
||||
activity.object.id,
|
||||
activity.object.attributedTo,
|
||||
);
|
||||
}
|
||||
|
||||
if (activity.type === 'Update') {
|
||||
ids.push(activity.object.id);
|
||||
}
|
||||
|
||||
const { origin: actorOrigin } = new URL(activity.actor);
|
||||
|
||||
// Object containment
|
||||
return ids.every((id) => {
|
||||
const { origin: idOrigin } = new URL(id);
|
||||
return idOrigin === actorOrigin;
|
||||
});
|
||||
});
|
||||
|
||||
type Activity = z.infer<typeof activitySchema>;
|
||||
type CreateNote = z.infer<typeof createNoteSchema>;
|
||||
type Announce = z.infer<typeof announceNoteSchema>;
|
||||
type Update = z.infer<typeof updateActorSchema>;
|
||||
type Object = z.infer<typeof objectSchema>;
|
||||
type Follow = z.infer<typeof followSchema>;
|
||||
type Accept = z.infer<typeof acceptSchema>;
|
||||
type Actor = z.infer<typeof actorSchema>;
|
||||
type Note = z.infer<typeof noteSchema>;
|
||||
type Mention = z.infer<typeof mentionSchema>;
|
||||
type Hashtag = z.infer<typeof hashtagSchema>;
|
||||
type Emoji = z.infer<typeof emojiSchema>;
|
||||
type Like = z.infer<typeof likeSchema>;
|
||||
type EmojiReact = z.infer<typeof emojiReactSchema>;
|
||||
type Delete = z.infer<typeof deleteSchema>;
|
||||
type Zap = z.infer<typeof zapSchema>;
|
||||
type Proxy = z.infer<typeof proxySchema>;
|
||||
|
||||
export { acceptSchema, activitySchema, actorSchema, emojiSchema, followSchema, imageSchema, noteSchema, objectSchema };
|
||||
export type {
|
||||
Accept,
|
||||
Activity,
|
||||
Actor,
|
||||
Announce,
|
||||
CreateNote,
|
||||
Delete,
|
||||
Emoji,
|
||||
EmojiReact,
|
||||
Follow,
|
||||
Hashtag,
|
||||
Like,
|
||||
Mention,
|
||||
Note,
|
||||
Object,
|
||||
Proxy,
|
||||
Update,
|
||||
Zap,
|
||||
};
|
|
@ -0,0 +1,41 @@
|
|||
import { Conf } from '@/config.ts';
|
||||
import { parseMetaContent } from '@/schema.ts';
|
||||
|
||||
import type { Event } from '@/event.ts';
|
||||
import type { Actor } from '@/schemas/activitypub.ts';
|
||||
|
||||
/** Nostr metadata event to ActivityPub actor. */
|
||||
async function toActor(event: Event<0>, username: string): Promise<Actor> {
|
||||
const content = parseMetaContent(event);
|
||||
|
||||
return {
|
||||
type: 'Person',
|
||||
id: Conf.local(`/users/${username}`),
|
||||
name: content?.name || '',
|
||||
preferredUsername: username,
|
||||
inbox: Conf.local(`/users/${username}/inbox`),
|
||||
followers: Conf.local(`/users/${username}/followers`),
|
||||
following: Conf.local(`/users/${username}/following`),
|
||||
outbox: Conf.local(`/users/${username}/outbox`),
|
||||
icon: content.picture
|
||||
? {
|
||||
type: 'Image',
|
||||
url: content.picture,
|
||||
}
|
||||
: undefined,
|
||||
image: content.banner
|
||||
? {
|
||||
type: 'Image',
|
||||
url: content.banner,
|
||||
}
|
||||
: undefined,
|
||||
summary: content.about ?? '',
|
||||
attachment: [],
|
||||
tag: [],
|
||||
endpoints: {
|
||||
sharedInbox: Conf.local('/inbox'),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export { toActor };
|
Loading…
Reference in New Issue