mirror of
https://github.com/versia-pub/server.git
synced 2026-03-13 22:09:16 +01:00
refactor(federation): ♻️ Rewrite federation SDK
This commit is contained in:
parent
ad1dc13a51
commit
d638610361
72 changed files with 2137 additions and 738 deletions
16
packages/federation/schemas/collection.ts
Normal file
16
packages/federation/schemas/collection.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { z } from "zod";
|
||||
import { url, u64 } from "./common.ts";
|
||||
|
||||
export const CollectionSchema = z.strictObject({
|
||||
author: url.nullable(),
|
||||
first: url,
|
||||
last: url,
|
||||
total: u64,
|
||||
next: url.nullable(),
|
||||
previous: url.nullable(),
|
||||
items: z.array(z.any()),
|
||||
});
|
||||
|
||||
export const URICollectionSchema = CollectionSchema.extend({
|
||||
items: z.array(url),
|
||||
});
|
||||
17
packages/federation/schemas/common.ts
Normal file
17
packages/federation/schemas/common.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { z } from "zod";
|
||||
|
||||
export const f64 = z
|
||||
.number()
|
||||
.nonnegative()
|
||||
.max(2 ** 64 - 1);
|
||||
|
||||
export const u64 = z
|
||||
.number()
|
||||
.int()
|
||||
.nonnegative()
|
||||
.max(2 ** 64 - 1);
|
||||
|
||||
export const url = z
|
||||
.string()
|
||||
.url()
|
||||
.transform((z) => new URL(z));
|
||||
117
packages/federation/schemas/contentformat.ts
Normal file
117
packages/federation/schemas/contentformat.ts
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
import { types } from "mime-types";
|
||||
import { z } from "zod";
|
||||
import { f64, u64 } from "./common.ts";
|
||||
|
||||
const hashSizes = {
|
||||
sha256: 64,
|
||||
sha512: 128,
|
||||
"sha3-256": 64,
|
||||
"sha3-512": 128,
|
||||
"blake2b-256": 64,
|
||||
"blake2b-512": 128,
|
||||
"blake3-256": 64,
|
||||
"blake3-512": 128,
|
||||
md5: 32,
|
||||
sha1: 40,
|
||||
sha224: 56,
|
||||
sha384: 96,
|
||||
"sha3-224": 56,
|
||||
"sha3-384": 96,
|
||||
"blake2s-256": 64,
|
||||
"blake2s-512": 128,
|
||||
"blake3-224": 56,
|
||||
"blake3-384": 96,
|
||||
};
|
||||
const allMimeTypes = Object.values(types) as [string, ...string[]];
|
||||
const textMimeTypes = Object.values(types).filter((v) =>
|
||||
v.startsWith("text/"),
|
||||
) as [string, ...string[]];
|
||||
const nonTextMimeTypes = Object.values(types).filter(
|
||||
(v) => !v.startsWith("text/"),
|
||||
) as [string, ...string[]];
|
||||
const imageMimeTypes = Object.values(types).filter((v) =>
|
||||
v.startsWith("image/"),
|
||||
) as [string, ...string[]];
|
||||
const videoMimeTypes = Object.values(types).filter((v) =>
|
||||
v.startsWith("video/"),
|
||||
) as [string, ...string[]];
|
||||
const audioMimeTypes = Object.values(types).filter((v) =>
|
||||
v.startsWith("audio/"),
|
||||
) as [string, ...string[]];
|
||||
|
||||
export const ContentFormatSchema = z.record(
|
||||
z.enum(allMimeTypes),
|
||||
z.strictObject({
|
||||
content: z.string().or(z.string().url()),
|
||||
remote: z.boolean(),
|
||||
description: z.string().nullish(),
|
||||
size: u64.nullish(),
|
||||
hash: z
|
||||
.strictObject(
|
||||
Object.fromEntries(
|
||||
Object.entries(hashSizes).map(([k, v]) => [
|
||||
k,
|
||||
z.string().length(v).nullish(),
|
||||
]),
|
||||
),
|
||||
)
|
||||
.nullish(),
|
||||
thumbhash: z.string().nullish(),
|
||||
width: u64.nullish(),
|
||||
height: u64.nullish(),
|
||||
duration: f64.nullish(),
|
||||
fps: u64.nullish(),
|
||||
}),
|
||||
);
|
||||
|
||||
export const TextContentFormatSchema = z.record(
|
||||
z.enum(textMimeTypes),
|
||||
ContentFormatSchema.valueSchema
|
||||
.pick({
|
||||
content: true,
|
||||
remote: true,
|
||||
})
|
||||
.extend({
|
||||
content: z.string(),
|
||||
remote: z.literal(false),
|
||||
}),
|
||||
);
|
||||
|
||||
export const NonTextContentFormatSchema = z.record(
|
||||
z.enum(nonTextMimeTypes),
|
||||
ContentFormatSchema.valueSchema
|
||||
.pick({
|
||||
content: true,
|
||||
remote: true,
|
||||
description: true,
|
||||
size: true,
|
||||
hash: true,
|
||||
thumbhash: true,
|
||||
width: true,
|
||||
height: true,
|
||||
})
|
||||
.extend({
|
||||
content: z.string().url(),
|
||||
remote: z.literal(true),
|
||||
}),
|
||||
);
|
||||
|
||||
export const ImageContentFormatSchema = z.record(
|
||||
z.enum(imageMimeTypes),
|
||||
NonTextContentFormatSchema.valueSchema,
|
||||
);
|
||||
|
||||
export const VideoContentFormatSchema = z.record(
|
||||
z.enum(videoMimeTypes),
|
||||
NonTextContentFormatSchema.valueSchema.extend({
|
||||
duration: ContentFormatSchema.valueSchema.shape.duration,
|
||||
fps: ContentFormatSchema.valueSchema.shape.fps,
|
||||
}),
|
||||
);
|
||||
|
||||
export const AudioContentFormatSchema = z.record(
|
||||
z.enum(audioMimeTypes),
|
||||
NonTextContentFormatSchema.valueSchema.extend({
|
||||
duration: ContentFormatSchema.valueSchema.shape.duration,
|
||||
}),
|
||||
);
|
||||
11
packages/federation/schemas/delete.ts
Normal file
11
packages/federation/schemas/delete.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { z } from "zod";
|
||||
import { url } from "./common.ts";
|
||||
import { EntitySchema } from "./entity.ts";
|
||||
|
||||
export const DeleteSchema = EntitySchema.extend({
|
||||
uri: z.null().optional(),
|
||||
type: z.literal("Delete"),
|
||||
author: url.nullable(),
|
||||
deleted_type: z.string(),
|
||||
deleted: url,
|
||||
});
|
||||
23
packages/federation/schemas/entity.ts
Normal file
23
packages/federation/schemas/entity.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { z } from "zod";
|
||||
import { isISOString } from "../regex.ts";
|
||||
import { url } from "./common.ts";
|
||||
import { CustomEmojiExtensionSchema } from "./extensions/emojis.ts";
|
||||
|
||||
export const ExtensionPropertySchema = z
|
||||
.object({
|
||||
"pub.versia:custom_emojis":
|
||||
CustomEmojiExtensionSchema.optional().nullable(),
|
||||
})
|
||||
.catchall(z.any());
|
||||
|
||||
export const EntitySchema = z.strictObject({
|
||||
// biome-ignore lint/style/useNamingConvention:
|
||||
$schema: z.string().url().nullish(),
|
||||
id: z.string().max(512),
|
||||
created_at: z
|
||||
.string()
|
||||
.refine((v) => isISOString(v), "must be a valid ISO8601 datetime"),
|
||||
uri: url,
|
||||
type: z.string(),
|
||||
extensions: ExtensionPropertySchema.nullish(),
|
||||
});
|
||||
25
packages/federation/schemas/extensions/emojis.ts
Normal file
25
packages/federation/schemas/extensions/emojis.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
/**
|
||||
* Custom emojis extension.
|
||||
* @module federation/schemas/extensions/custom_emojis
|
||||
* @see module:federation/schemas/base
|
||||
* @see https://versia.pub/extensions/custom-emojis
|
||||
*/
|
||||
import { z } from "zod";
|
||||
import { emojiRegex } from "../../regex.ts";
|
||||
import { ImageContentFormatSchema } from "../contentformat.ts";
|
||||
|
||||
export const CustomEmojiExtensionSchema = z.strictObject({
|
||||
emojis: z.array(
|
||||
z.strictObject({
|
||||
name: z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(256)
|
||||
.regex(
|
||||
emojiRegex,
|
||||
"Emoji name must be alphanumeric, underscores, or dashes, and surrounded by identifiers",
|
||||
),
|
||||
url: ImageContentFormatSchema,
|
||||
}),
|
||||
),
|
||||
});
|
||||
41
packages/federation/schemas/extensions/groups.ts
Normal file
41
packages/federation/schemas/extensions/groups.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import { z } from "zod";
|
||||
import { url } from "../common.ts";
|
||||
import { TextContentFormatSchema } from "../contentformat.ts";
|
||||
import { EntitySchema } from "../entity.ts";
|
||||
|
||||
export const GroupSchema = EntitySchema.extend({
|
||||
type: z.literal("pub.versia:groups/Group"),
|
||||
name: TextContentFormatSchema.nullish(),
|
||||
description: TextContentFormatSchema.nullish(),
|
||||
open: z.boolean().nullish(),
|
||||
members: url,
|
||||
notes: url.nullish(),
|
||||
});
|
||||
|
||||
export const GroupSubscribeSchema = EntitySchema.extend({
|
||||
type: z.literal("pub.versia:groups/Subscribe"),
|
||||
uri: z.null().optional(),
|
||||
subscriber: url,
|
||||
group: url,
|
||||
});
|
||||
|
||||
export const GroupUnsubscribeSchema = EntitySchema.extend({
|
||||
type: z.literal("pub.versia:groups/Unsubscribe"),
|
||||
uri: z.null().optional(),
|
||||
subscriber: url,
|
||||
group: url,
|
||||
});
|
||||
|
||||
export const GroupSubscribeAcceptSchema = EntitySchema.extend({
|
||||
type: z.literal("pub.versia:groups/SubscribeAccept"),
|
||||
uri: z.null().optional(),
|
||||
subscriber: url,
|
||||
group: url,
|
||||
});
|
||||
|
||||
export const GroupSubscribeRejectSchema = EntitySchema.extend({
|
||||
type: z.literal("pub.versia:groups/SubscribeReject"),
|
||||
uri: z.null().optional(),
|
||||
subscriber: url,
|
||||
group: url,
|
||||
});
|
||||
15
packages/federation/schemas/extensions/likes.ts
Normal file
15
packages/federation/schemas/extensions/likes.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { z } from "zod";
|
||||
import { url } from "../common.ts";
|
||||
import { EntitySchema } from "../entity.ts";
|
||||
|
||||
export const LikeSchema = EntitySchema.extend({
|
||||
type: z.literal("pub.versia:likes/Like"),
|
||||
author: url,
|
||||
liked: url,
|
||||
});
|
||||
|
||||
export const DislikeSchema = EntitySchema.extend({
|
||||
type: z.literal("pub.versia:likes/Dislike"),
|
||||
author: url,
|
||||
disliked: url,
|
||||
});
|
||||
15
packages/federation/schemas/extensions/migration.ts
Normal file
15
packages/federation/schemas/extensions/migration.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { z } from "zod";
|
||||
import { url } from "../common.ts";
|
||||
import { EntitySchema } from "../entity.ts";
|
||||
|
||||
export const MigrationSchema = EntitySchema.extend({
|
||||
type: z.literal("pub.versia:migration/Migration"),
|
||||
uri: z.null().optional(),
|
||||
author: url,
|
||||
destination: url,
|
||||
});
|
||||
|
||||
export const MigrationExtensionSchema = z.strictObject({
|
||||
previous: url,
|
||||
new: url.nullish(),
|
||||
});
|
||||
22
packages/federation/schemas/extensions/polls.ts
Normal file
22
packages/federation/schemas/extensions/polls.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { z } from "zod";
|
||||
import { isISOString } from "../../regex.ts";
|
||||
import { url, u64 } from "../common.ts";
|
||||
import { TextContentFormatSchema } from "../contentformat.ts";
|
||||
import { EntitySchema } from "../entity.ts";
|
||||
|
||||
export const VoteSchema = EntitySchema.extend({
|
||||
type: z.literal("pub.versia:polls/Vote"),
|
||||
author: url,
|
||||
poll: url,
|
||||
option: u64,
|
||||
});
|
||||
|
||||
export const PollExtensionSchema = z.strictObject({
|
||||
options: z.array(TextContentFormatSchema),
|
||||
votes: z.array(u64),
|
||||
multiple_choice: z.boolean(),
|
||||
expires_at: z
|
||||
.string()
|
||||
.refine((v) => isISOString(v), "must be a valid ISO8601 datetime")
|
||||
.nullish(),
|
||||
});
|
||||
10
packages/federation/schemas/extensions/reactions.ts
Normal file
10
packages/federation/schemas/extensions/reactions.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { z } from "zod";
|
||||
import { url } from "../common.ts";
|
||||
import { EntitySchema } from "../entity.ts";
|
||||
|
||||
export const ReactionSchema = EntitySchema.extend({
|
||||
type: z.literal("pub.versia:reactions/Reaction"),
|
||||
author: url,
|
||||
object: url,
|
||||
content: z.string().min(1).max(256),
|
||||
});
|
||||
15
packages/federation/schemas/extensions/reports.ts
Normal file
15
packages/federation/schemas/extensions/reports.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { z } from "zod";
|
||||
import { url } from "../common.ts";
|
||||
import { EntitySchema } from "../entity.ts";
|
||||
|
||||
export const ReportSchema = EntitySchema.extend({
|
||||
type: z.literal("pub.versia:reports/Report"),
|
||||
uri: z.null().optional(),
|
||||
author: url.nullish(),
|
||||
reported: z.array(url),
|
||||
tags: z.array(z.string()),
|
||||
comment: z
|
||||
.string()
|
||||
.max(2 ** 16)
|
||||
.nullish(),
|
||||
});
|
||||
9
packages/federation/schemas/extensions/share.ts
Normal file
9
packages/federation/schemas/extensions/share.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { z } from "zod";
|
||||
import { url } from "../common.ts";
|
||||
import { EntitySchema } from "../entity.ts";
|
||||
|
||||
export const ShareSchema = EntitySchema.extend({
|
||||
type: z.literal("pub.versia:share/Share"),
|
||||
author: url,
|
||||
shared: url,
|
||||
});
|
||||
46
packages/federation/schemas/extensions/vanity.ts
Normal file
46
packages/federation/schemas/extensions/vanity.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
/**
|
||||
* Vanity extension schema.
|
||||
* @module federation/schemas/extensions/vanity
|
||||
* @see module:federation/schemas/base
|
||||
* @see https://versia.pub/extensions/vanity
|
||||
*/
|
||||
|
||||
import { z } from "zod";
|
||||
import { ianaTimezoneRegex, isISOString } from "../../regex.ts";
|
||||
import { url } from "../common.ts";
|
||||
import {
|
||||
AudioContentFormatSchema,
|
||||
ImageContentFormatSchema,
|
||||
} from "../contentformat.ts";
|
||||
|
||||
export const VanityExtensionSchema = z.strictObject({
|
||||
avatar_overlays: z.array(ImageContentFormatSchema).nullish(),
|
||||
avatar_mask: ImageContentFormatSchema.nullish(),
|
||||
background: ImageContentFormatSchema.nullish(),
|
||||
audio: AudioContentFormatSchema.nullish(),
|
||||
pronouns: z.record(
|
||||
z.string(),
|
||||
z.array(
|
||||
z.union([
|
||||
z.strictObject({
|
||||
subject: z.string(),
|
||||
object: z.string(),
|
||||
dependent_possessive: z.string(),
|
||||
independent_possessive: z.string(),
|
||||
reflexive: z.string(),
|
||||
}),
|
||||
z.string(),
|
||||
]),
|
||||
),
|
||||
),
|
||||
birthday: z
|
||||
.string()
|
||||
.refine((v) => isISOString(v), "must be a valid ISO8601 datetime")
|
||||
.nullish(),
|
||||
location: z.string().nullish(),
|
||||
aliases: z.array(url).nullish(),
|
||||
timezone: z
|
||||
.string()
|
||||
.regex(ianaTimezoneRegex, "must be a valid IANA timezone")
|
||||
.nullish(),
|
||||
});
|
||||
31
packages/federation/schemas/follow.ts
Normal file
31
packages/federation/schemas/follow.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { z } from "zod";
|
||||
import { url } from "./common.ts";
|
||||
import { EntitySchema } from "./entity.ts";
|
||||
|
||||
export const FollowSchema = EntitySchema.extend({
|
||||
type: z.literal("Follow"),
|
||||
uri: z.null().optional(),
|
||||
author: url,
|
||||
followee: url,
|
||||
});
|
||||
|
||||
export const FollowAcceptSchema = EntitySchema.extend({
|
||||
type: z.literal("FollowAccept"),
|
||||
uri: z.null().optional(),
|
||||
author: url,
|
||||
follower: url,
|
||||
});
|
||||
|
||||
export const FollowRejectSchema = EntitySchema.extend({
|
||||
type: z.literal("FollowReject"),
|
||||
uri: z.null().optional(),
|
||||
author: url,
|
||||
follower: url,
|
||||
});
|
||||
|
||||
export const UnfollowSchema = EntitySchema.extend({
|
||||
type: z.literal("Unfollow"),
|
||||
uri: z.null().optional(),
|
||||
author: url,
|
||||
followee: url,
|
||||
});
|
||||
27
packages/federation/schemas/index.ts
Normal file
27
packages/federation/schemas/index.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
// biome-ignore lint/performance/noBarrelFile: <explanation>
|
||||
export { UserSchema } from "./user.ts";
|
||||
export { NoteSchema } from "./note.ts";
|
||||
export { EntitySchema } from "./entity.ts";
|
||||
export { DeleteSchema } from "./delete.ts";
|
||||
export { InstanceMetadataSchema } from "./instance.ts";
|
||||
export {
|
||||
ContentFormatSchema,
|
||||
ImageContentFormatSchema,
|
||||
AudioContentFormatSchema,
|
||||
NonTextContentFormatSchema,
|
||||
TextContentFormatSchema,
|
||||
VideoContentFormatSchema,
|
||||
} from "./contentformat.ts";
|
||||
export {
|
||||
FollowSchema,
|
||||
FollowAcceptSchema,
|
||||
FollowRejectSchema,
|
||||
UnfollowSchema,
|
||||
} from "./follow.ts";
|
||||
export { CollectionSchema, URICollectionSchema } from "./collection.ts";
|
||||
export { LikeSchema, DislikeSchema } from "./extensions/likes.ts";
|
||||
export { VoteSchema } from "./extensions/polls.ts";
|
||||
export { ReactionSchema } from "./extensions/reactions.ts";
|
||||
export { ReportSchema } from "./extensions/reports.ts";
|
||||
export { ShareSchema } from "./extensions/share.ts";
|
||||
export { WebFingerSchema } from "./webfinger.ts";
|
||||
41
packages/federation/schemas/instance.ts
Normal file
41
packages/federation/schemas/instance.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import { z } from "zod";
|
||||
import { extensionRegex, semverRegex } from "../regex.ts";
|
||||
import { url } from "./common.ts";
|
||||
import { ImageContentFormatSchema } from "./contentformat.ts";
|
||||
import { EntitySchema } from "./entity.ts";
|
||||
|
||||
export const InstanceMetadataSchema = EntitySchema.extend({
|
||||
type: z.literal("InstanceMetadata"),
|
||||
id: z.null().optional(),
|
||||
uri: z.null().optional(),
|
||||
name: z.string().min(1),
|
||||
software: z.strictObject({
|
||||
name: z.string().min(1),
|
||||
version: z.string().min(1),
|
||||
}),
|
||||
compatibility: z.strictObject({
|
||||
versions: z.array(
|
||||
z.string().regex(semverRegex, "must be a valid SemVer version"),
|
||||
),
|
||||
extensions: z.array(
|
||||
z
|
||||
.string()
|
||||
.min(1)
|
||||
.regex(
|
||||
extensionRegex,
|
||||
"must be in the format 'namespaced_url:extension_name', e.g. 'pub.versia:reactions'",
|
||||
),
|
||||
),
|
||||
}),
|
||||
description: z.string().nullish(),
|
||||
host: z.string(),
|
||||
shared_inbox: url.nullish(),
|
||||
public_key: z.strictObject({
|
||||
key: z.string().min(1),
|
||||
algorithm: z.literal("ed25519"),
|
||||
}),
|
||||
moderators: url.nullish(),
|
||||
admins: url.nullish(),
|
||||
logo: ImageContentFormatSchema.nullish(),
|
||||
banner: ImageContentFormatSchema.nullish(),
|
||||
});
|
||||
67
packages/federation/schemas/note.ts
Normal file
67
packages/federation/schemas/note.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import { z } from "zod";
|
||||
import { url } from "./common.ts";
|
||||
import {
|
||||
NonTextContentFormatSchema,
|
||||
TextContentFormatSchema,
|
||||
} from "./contentformat.ts";
|
||||
import { EntitySchema } from "./entity.ts";
|
||||
import { PollExtensionSchema } from "./extensions/polls.ts";
|
||||
|
||||
export const NoteSchema = EntitySchema.extend({
|
||||
type: z.literal("Note"),
|
||||
attachments: z.array(NonTextContentFormatSchema).nullish(),
|
||||
author: url,
|
||||
category: z
|
||||
.enum([
|
||||
"microblog",
|
||||
"forum",
|
||||
"blog",
|
||||
"image",
|
||||
"video",
|
||||
"audio",
|
||||
"messaging",
|
||||
])
|
||||
.nullish(),
|
||||
content: TextContentFormatSchema.nullish(),
|
||||
collections: z
|
||||
.strictObject({
|
||||
replies: url,
|
||||
quotes: url,
|
||||
"pub.versia:reactions/Reactions": url.nullish(),
|
||||
"pub.versia:share/Shares": url.nullish(),
|
||||
"pub.versia:likes/Likes": url.nullish(),
|
||||
"pub.versia:likes/Dislikes": url.nullish(),
|
||||
})
|
||||
.catchall(url),
|
||||
device: z
|
||||
.strictObject({
|
||||
name: z.string(),
|
||||
version: z.string().nullish(),
|
||||
url: url.nullish(),
|
||||
})
|
||||
.nullish(),
|
||||
group: url.or(z.enum(["public", "followers"])).nullish(),
|
||||
is_sensitive: z.boolean().nullish(),
|
||||
mentions: z.array(url).nullish(),
|
||||
previews: z
|
||||
.array(
|
||||
z.strictObject({
|
||||
link: url,
|
||||
title: z.string(),
|
||||
description: z.string().nullish(),
|
||||
image: url.nullish(),
|
||||
icon: url.nullish(),
|
||||
}),
|
||||
)
|
||||
.nullish(),
|
||||
quotes: url.nullish(),
|
||||
replies_to: url.nullish(),
|
||||
subject: z.string().nullish(),
|
||||
extensions: EntitySchema.shape.extensions
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.extend({
|
||||
"pub.versia:polls": PollExtensionSchema.nullish(),
|
||||
})
|
||||
.nullish(),
|
||||
});
|
||||
60
packages/federation/schemas/user.ts
Normal file
60
packages/federation/schemas/user.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import { z } from "zod";
|
||||
import { url } from "./common.ts";
|
||||
import {
|
||||
ImageContentFormatSchema,
|
||||
TextContentFormatSchema,
|
||||
} from "./contentformat.ts";
|
||||
import { EntitySchema } from "./entity.ts";
|
||||
import { MigrationExtensionSchema } from "./extensions/migration.ts";
|
||||
import { VanityExtensionSchema } from "./extensions/vanity.ts";
|
||||
|
||||
export const PublicKeyDataSchema = z.strictObject({
|
||||
key: z.string().min(1),
|
||||
actor: url,
|
||||
algorithm: z.literal("ed25519"),
|
||||
});
|
||||
|
||||
export const UserSchema = EntitySchema.extend({
|
||||
type: z.literal("User"),
|
||||
avatar: ImageContentFormatSchema.nullish(),
|
||||
bio: TextContentFormatSchema.nullish(),
|
||||
display_name: z.string().nullish(),
|
||||
fields: z
|
||||
.array(
|
||||
z.strictObject({
|
||||
key: TextContentFormatSchema,
|
||||
value: TextContentFormatSchema,
|
||||
}),
|
||||
)
|
||||
.nullish(),
|
||||
username: z
|
||||
.string()
|
||||
.min(1)
|
||||
.regex(
|
||||
/^[a-zA-Z0-9_-]+$/,
|
||||
"must be alphanumeric, and may contain _ or -",
|
||||
),
|
||||
header: ImageContentFormatSchema.nullish(),
|
||||
public_key: PublicKeyDataSchema,
|
||||
manually_approves_followers: z.boolean().nullish(),
|
||||
indexable: z.boolean().nullish(),
|
||||
inbox: url,
|
||||
collections: z
|
||||
.object({
|
||||
featured: url,
|
||||
followers: url,
|
||||
following: url,
|
||||
outbox: url,
|
||||
"pub.versia:likes/Likes": url.nullish(),
|
||||
"pub.versia:likes/Dislikes": url.nullish(),
|
||||
})
|
||||
.catchall(url),
|
||||
extensions: EntitySchema.shape.extensions
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.extend({
|
||||
"pub.versia:vanity": VanityExtensionSchema.nullish(),
|
||||
"pub.versia:migration": MigrationExtensionSchema.nullish(),
|
||||
})
|
||||
.nullish(),
|
||||
});
|
||||
19
packages/federation/schemas/webfinger.ts
Normal file
19
packages/federation/schemas/webfinger.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { z } from "zod";
|
||||
import { url } from "./common.ts";
|
||||
|
||||
export const WebFingerSchema = z.object({
|
||||
subject: url,
|
||||
aliases: z.array(url).optional(),
|
||||
properties: z.record(url, z.string().or(z.null())).optional(),
|
||||
links: z
|
||||
.array(
|
||||
z.object({
|
||||
rel: z.string(),
|
||||
type: z.string().optional(),
|
||||
href: url.optional(),
|
||||
titles: z.record(z.string(), z.string()).optional(),
|
||||
properties: z.record(url, z.string().or(z.null())).optional(),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue