308 lines
7.8 KiB
TypeScript
308 lines
7.8 KiB
TypeScript
// Shamelessly lifted from: https://gitlab.com/soapbox-pub/ditto/-/blob/main/src/schemas/activitypub.ts
|
|
|
|
import { z } from "zod";
|
|
|
|
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),
|
|
});
|
|
|
|
const activitySchema = z.discriminatedUnion("type", [
|
|
followSchema,
|
|
acceptSchema,
|
|
createNoteSchema,
|
|
announceNoteSchema,
|
|
updateActorSchema,
|
|
likeSchema,
|
|
emojiReactSchema,
|
|
deleteSchema,
|
|
]).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 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,
|
|
}; |