mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 08:28:19 +01:00
refactor(federation): 🔥 Remove old types and federation code
This commit is contained in:
parent
5fd6a4e43d
commit
093337dd4f
|
|
@ -1,251 +0,0 @@
|
|||
export interface ContentFormat {
|
||||
[contentType: string]: {
|
||||
content: string;
|
||||
description?: string;
|
||||
size?: number;
|
||||
hash?: {
|
||||
md5?: string;
|
||||
sha1?: string;
|
||||
sha256?: string;
|
||||
sha512?: string;
|
||||
[key: string]: string | undefined;
|
||||
};
|
||||
blurhash?: string;
|
||||
fps?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
duration?: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Emoji {
|
||||
name: string;
|
||||
alt?: string;
|
||||
url: ContentFormat;
|
||||
}
|
||||
|
||||
export interface Collections<T> {
|
||||
first: string;
|
||||
last: string;
|
||||
total_count: number;
|
||||
author: string;
|
||||
next?: string;
|
||||
prev?: string;
|
||||
items: T[];
|
||||
}
|
||||
|
||||
export interface ActorPublicKeyData {
|
||||
public_key: string;
|
||||
actor: string;
|
||||
}
|
||||
|
||||
export interface Entity {
|
||||
id: string;
|
||||
created_at: string;
|
||||
uri: string;
|
||||
type: string;
|
||||
extensions?: {
|
||||
"org.lysand:custom_emojis"?: {
|
||||
emojis: Emoji[];
|
||||
};
|
||||
[key: string]: object | undefined;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Publication extends Entity {
|
||||
type: "Note" | "Patch";
|
||||
author: string;
|
||||
content?: ContentFormat;
|
||||
attachments?: ContentFormat[];
|
||||
replies_to?: string;
|
||||
quotes?: string;
|
||||
mentions?: string[];
|
||||
subject?: string;
|
||||
is_sensitive?: boolean;
|
||||
visibility: Visibility;
|
||||
extensions?: Entity["extensions"] & {
|
||||
"org.lysand:reactions"?: {
|
||||
reactions: string;
|
||||
};
|
||||
"org.lysand:polls"?: {
|
||||
poll: {
|
||||
options: ContentFormat[];
|
||||
votes: number[];
|
||||
multiple_choice?: boolean;
|
||||
expires_at: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export enum Visibility {
|
||||
Public = "public",
|
||||
Unlisted = "unlisted",
|
||||
Followers = "followers",
|
||||
Direct = "direct",
|
||||
}
|
||||
|
||||
export interface Note extends Publication {
|
||||
type: "Note";
|
||||
}
|
||||
|
||||
export interface Patch extends Publication {
|
||||
type: "Patch";
|
||||
patched_id: string;
|
||||
patched_at: string;
|
||||
}
|
||||
|
||||
export interface User extends Entity {
|
||||
type: "User";
|
||||
id: string;
|
||||
uri: string;
|
||||
created_at: string;
|
||||
display_name?: string;
|
||||
username: string;
|
||||
avatar?: ContentFormat;
|
||||
header?: ContentFormat;
|
||||
indexable: boolean;
|
||||
public_key: ActorPublicKeyData;
|
||||
bio?: ContentFormat;
|
||||
fields?: Field[];
|
||||
featured: string;
|
||||
followers: string;
|
||||
following: string;
|
||||
likes: string;
|
||||
dislikes: string;
|
||||
inbox: string;
|
||||
outbox: string;
|
||||
extensions?: Entity["extensions"] & {
|
||||
"org.lysand:vanity"?: VanityExtension;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Field {
|
||||
key: ContentFormat;
|
||||
value: ContentFormat;
|
||||
}
|
||||
|
||||
export interface Action extends Entity {
|
||||
type:
|
||||
| "Like"
|
||||
| "Dislike"
|
||||
| "Follow"
|
||||
| "FollowAccept"
|
||||
| "FollowReject"
|
||||
| "Announce"
|
||||
| "Undo";
|
||||
author: string;
|
||||
}
|
||||
|
||||
export interface Like extends Action {
|
||||
type: "Like";
|
||||
object: string;
|
||||
}
|
||||
|
||||
export interface Undo extends Action {
|
||||
type: "Undo";
|
||||
object: string;
|
||||
}
|
||||
|
||||
export interface Dislike extends Action {
|
||||
type: "Dislike";
|
||||
object: string;
|
||||
}
|
||||
|
||||
export interface Follow extends Action {
|
||||
type: "Follow";
|
||||
followee: string;
|
||||
}
|
||||
|
||||
export interface FollowAccept extends Action {
|
||||
type: "FollowAccept";
|
||||
follower: string;
|
||||
}
|
||||
|
||||
export interface FollowReject extends Action {
|
||||
type: "FollowReject";
|
||||
follower: string;
|
||||
}
|
||||
|
||||
export interface Announce extends Action {
|
||||
type: "Announce";
|
||||
object: string;
|
||||
}
|
||||
|
||||
// Specific extension types will extend from this
|
||||
export interface Extension extends Entity {
|
||||
type: "Extension";
|
||||
extension_type: string;
|
||||
}
|
||||
|
||||
export interface Reaction extends Extension {
|
||||
extension_type: "org.lysand:reactions/Reaction";
|
||||
object: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface Poll extends Extension {
|
||||
extension_type: "org.lysand:polls/Poll";
|
||||
options: ContentFormat[];
|
||||
votes: number[];
|
||||
multiple_choice?: boolean;
|
||||
expires_at: string;
|
||||
}
|
||||
|
||||
export interface Vote extends Extension {
|
||||
extension_type: "org.lysand:polls/Vote";
|
||||
poll: string;
|
||||
option: number;
|
||||
}
|
||||
|
||||
export interface VoteResult extends Extension {
|
||||
extension_type: "org.lysand:polls/VoteResult";
|
||||
poll: string;
|
||||
votes: number[];
|
||||
}
|
||||
|
||||
export interface Report extends Extension {
|
||||
extension_type: "org.lysand:reports/Report";
|
||||
objects: string[];
|
||||
reason: string;
|
||||
comment?: string;
|
||||
}
|
||||
|
||||
export interface VanityExtension {
|
||||
avatar_overlay?: ContentFormat;
|
||||
avatar_mask?: ContentFormat;
|
||||
background?: ContentFormat;
|
||||
audio?: ContentFormat;
|
||||
pronouns?: {
|
||||
[language: string]: (ShortPronoun | LongPronoun)[];
|
||||
};
|
||||
birthday?: string;
|
||||
location?: string;
|
||||
activitypub?: string;
|
||||
}
|
||||
|
||||
export type ShortPronoun = string;
|
||||
|
||||
export interface LongPronoun {
|
||||
subject: string;
|
||||
object: string;
|
||||
dependent_possessive: string;
|
||||
independent_possessive: string;
|
||||
reflexive: string;
|
||||
}
|
||||
|
||||
export interface ServerMetadata {
|
||||
type: "ServerMetadata";
|
||||
name: string;
|
||||
version: string;
|
||||
description?: string;
|
||||
website?: string;
|
||||
moderators?: string[];
|
||||
admins?: string[];
|
||||
logo?: ContentFormat;
|
||||
banner?: ContentFormat;
|
||||
supported_extensions: string[];
|
||||
extensions?: {
|
||||
[key: string]: object | undefined;
|
||||
};
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"name": "lysand-types",
|
||||
"version": "2.0.0",
|
||||
"description": "A collection of types for the Lysand protocol",
|
||||
"main": "index.ts"
|
||||
}
|
||||
|
|
@ -1,282 +0,0 @@
|
|||
import type * as Lysand from "lysand-types";
|
||||
import { fromZodError } from "zod-validation-error";
|
||||
import { schemas } from "./schemas";
|
||||
|
||||
const types = [
|
||||
"Note",
|
||||
"User",
|
||||
"Reaction",
|
||||
"Poll",
|
||||
"Vote",
|
||||
"VoteResult",
|
||||
"Report",
|
||||
"ServerMetadata",
|
||||
"Like",
|
||||
"Dislike",
|
||||
"Follow",
|
||||
"FollowAccept",
|
||||
"FollowReject",
|
||||
"Announce",
|
||||
"Undo",
|
||||
];
|
||||
|
||||
/**
|
||||
* Validates an incoming Lysand object using Zod, and returns the object if it is valid.
|
||||
*/
|
||||
export class EntityValidator {
|
||||
constructor(private entity: Lysand.Entity) {}
|
||||
|
||||
/**
|
||||
* Validates the entity.
|
||||
*/
|
||||
validate<ExpectedType>() {
|
||||
// Check if type is valid
|
||||
if (!this.entity.type) {
|
||||
throw new Error("Entity type is required");
|
||||
}
|
||||
|
||||
const schema = this.matchSchema(this.getType());
|
||||
|
||||
const output = schema.safeParse(this.entity);
|
||||
|
||||
if (!output.success) {
|
||||
throw fromZodError(output.error);
|
||||
}
|
||||
|
||||
return output.data as ExpectedType;
|
||||
}
|
||||
|
||||
getType() {
|
||||
// Check if type is valid, return TypeScript type
|
||||
if (!this.entity.type) {
|
||||
throw new Error("Entity type is required");
|
||||
}
|
||||
|
||||
if (!types.includes(this.entity.type)) {
|
||||
throw new Error(`Unknown entity type: ${this.entity.type}`);
|
||||
}
|
||||
|
||||
return this.entity.type as (typeof types)[number];
|
||||
}
|
||||
|
||||
matchSchema(type: string) {
|
||||
switch (type) {
|
||||
case "Note":
|
||||
return schemas.Note;
|
||||
case "User":
|
||||
return schemas.User;
|
||||
case "Reaction":
|
||||
return schemas.Reaction;
|
||||
case "Poll":
|
||||
return schemas.Poll;
|
||||
case "Vote":
|
||||
return schemas.Vote;
|
||||
case "VoteResult":
|
||||
return schemas.VoteResult;
|
||||
case "Report":
|
||||
return schemas.Report;
|
||||
case "ServerMetadata":
|
||||
return schemas.ServerMetadata;
|
||||
case "Like":
|
||||
return schemas.Like;
|
||||
case "Dislike":
|
||||
return schemas.Dislike;
|
||||
case "Follow":
|
||||
return schemas.Follow;
|
||||
case "FollowAccept":
|
||||
return schemas.FollowAccept;
|
||||
case "FollowReject":
|
||||
return schemas.FollowReject;
|
||||
case "Announce":
|
||||
return schemas.Announce;
|
||||
case "Undo":
|
||||
return schemas.Undo;
|
||||
default:
|
||||
throw new Error(`Unknown entity type: ${type}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class SignatureValidator {
|
||||
constructor(
|
||||
private public_key: CryptoKey,
|
||||
private signature: string,
|
||||
private date: string,
|
||||
private method: string,
|
||||
private url: URL,
|
||||
private body: string,
|
||||
) {}
|
||||
|
||||
static async fromStringKey(
|
||||
public_key: string,
|
||||
signature: string,
|
||||
date: string,
|
||||
method: string,
|
||||
url: URL,
|
||||
body: string,
|
||||
) {
|
||||
return new SignatureValidator(
|
||||
await crypto.subtle.importKey(
|
||||
"spki",
|
||||
Buffer.from(public_key, "base64"),
|
||||
"Ed25519",
|
||||
false,
|
||||
["verify"],
|
||||
),
|
||||
signature,
|
||||
date,
|
||||
method,
|
||||
url,
|
||||
body,
|
||||
);
|
||||
}
|
||||
async validate() {
|
||||
const signature = this.signature
|
||||
.split("signature=")[1]
|
||||
.replace(/"/g, "");
|
||||
|
||||
const digest = await crypto.subtle.digest(
|
||||
"SHA-256",
|
||||
new TextEncoder().encode(this.body),
|
||||
);
|
||||
|
||||
const expectedSignedString =
|
||||
`(request-target): ${this.method.toLowerCase()} ${
|
||||
this.url.pathname
|
||||
}\n` +
|
||||
`host: ${this.url.host}\n` +
|
||||
`date: ${this.date}\n` +
|
||||
`digest: SHA-256=${Buffer.from(new Uint8Array(digest)).toString(
|
||||
"base64",
|
||||
)}\n`;
|
||||
|
||||
// Check if signed string is valid
|
||||
const isValid = await crypto.subtle.verify(
|
||||
"Ed25519",
|
||||
this.public_key,
|
||||
Buffer.from(signature, "base64"),
|
||||
new TextEncoder().encode(expectedSignedString),
|
||||
);
|
||||
|
||||
return isValid;
|
||||
}
|
||||
}
|
||||
|
||||
export class SignatureConstructor {
|
||||
constructor(
|
||||
private private_key: CryptoKey,
|
||||
private url: URL,
|
||||
private authorUri: URL,
|
||||
) {}
|
||||
|
||||
static async fromStringKey(private_key: string, url: URL, authorUri: URL) {
|
||||
return new SignatureConstructor(
|
||||
await crypto.subtle.importKey(
|
||||
"pkcs8",
|
||||
Buffer.from(private_key, "base64"),
|
||||
"Ed25519",
|
||||
false,
|
||||
["sign"],
|
||||
),
|
||||
url,
|
||||
authorUri,
|
||||
);
|
||||
}
|
||||
|
||||
async sign(method: string, body: string) {
|
||||
const digest = await crypto.subtle.digest(
|
||||
"SHA-256",
|
||||
new TextEncoder().encode(body),
|
||||
);
|
||||
|
||||
const date = new Date();
|
||||
|
||||
const signature = await crypto.subtle.sign(
|
||||
"Ed25519",
|
||||
this.private_key,
|
||||
new TextEncoder().encode(
|
||||
`(request-target): ${method.toLowerCase()} ${
|
||||
this.url.pathname
|
||||
}\n` +
|
||||
`host: ${this.url.host}\n` +
|
||||
`date: ${date.toISOString()}\n` +
|
||||
`digest: SHA-256=${Buffer.from(
|
||||
new Uint8Array(digest),
|
||||
).toString("base64")}\n`,
|
||||
),
|
||||
);
|
||||
|
||||
const signatureBase64 = Buffer.from(new Uint8Array(signature)).toString(
|
||||
"base64",
|
||||
);
|
||||
|
||||
return {
|
||||
date: date.toISOString(),
|
||||
signature: `keyId="${this.authorUri.toString()}",algorithm="ed25519",headers="(request-target) host date digest",signature="${signatureBase64}"`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extends native fetch with object signing
|
||||
* Make sure to format your JSON in Canonical JSON format!
|
||||
* @param url URL to fetch
|
||||
* @param options Standard Web Fetch API options
|
||||
* @param privateKey Author private key in base64
|
||||
* @param authorUri Author URI
|
||||
* @param baseUrl Base URL of this server
|
||||
* @returns Fetch response
|
||||
*/
|
||||
export const signedFetch = async (
|
||||
url: string | URL,
|
||||
options: RequestInit,
|
||||
privateKey: string,
|
||||
authorUri: string | URL,
|
||||
baseUrl: string | URL,
|
||||
) => {
|
||||
const urlObj = new URL(url);
|
||||
const authorUriObj = new URL(authorUri);
|
||||
|
||||
const signature = await SignatureConstructor.fromStringKey(
|
||||
privateKey,
|
||||
urlObj,
|
||||
authorUriObj,
|
||||
);
|
||||
|
||||
const { date, signature: signatureHeader } = await signature.sign(
|
||||
options.method ?? "GET",
|
||||
options.body?.toString() || "",
|
||||
);
|
||||
|
||||
return fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
Date: date,
|
||||
Origin: new URL(baseUrl).origin,
|
||||
Signature: signatureHeader,
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
Accept: "application/json",
|
||||
...options.headers,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// Export all schemas as a single object
|
||||
export default {
|
||||
Note: schemas.Note,
|
||||
User: schemas.User,
|
||||
Reaction: schemas.Reaction,
|
||||
Poll: schemas.Poll,
|
||||
Vote: schemas.Vote,
|
||||
VoteResult: schemas.VoteResult,
|
||||
Report: schemas.Report,
|
||||
ServerMetadata: schemas.ServerMetadata,
|
||||
Like: schemas.Like,
|
||||
Dislike: schemas.Dislike,
|
||||
Follow: schemas.Follow,
|
||||
FollowAccept: schemas.FollowAccept,
|
||||
FollowReject: schemas.FollowReject,
|
||||
Announce: schemas.Announce,
|
||||
Undo: schemas.Undo,
|
||||
Entity: schemas.Entity,
|
||||
};
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"name": "lysand-utils",
|
||||
"version": "0.0.0",
|
||||
"main": "index.ts",
|
||||
"dependencies": { "zod": "^3.22.4", "zod-validation-error": "^3.2.0" }
|
||||
}
|
||||
|
|
@ -1,285 +0,0 @@
|
|||
import { emojiValidator } from "@api";
|
||||
import {
|
||||
charIn,
|
||||
createRegExp,
|
||||
digit,
|
||||
exactly,
|
||||
letter,
|
||||
oneOrMore,
|
||||
} from "magic-regexp";
|
||||
import { types } from "mime-types";
|
||||
import { z } from "zod";
|
||||
|
||||
const ContentFormat = z.record(
|
||||
z.enum(Object.values(types) as [string, ...string[]]),
|
||||
z.object({
|
||||
content: z.string(),
|
||||
description: z.string().optional(),
|
||||
size: z.number().int().nonnegative().optional(),
|
||||
hash: z.record(z.string(), z.string()).optional(),
|
||||
blurhash: z.string().optional(),
|
||||
fps: z.number().int().nonnegative().optional(),
|
||||
width: z.number().int().nonnegative().optional(),
|
||||
height: z.number().int().nonnegative().optional(),
|
||||
duration: z.number().nonnegative().optional(),
|
||||
}),
|
||||
);
|
||||
|
||||
const Entity = z.object({
|
||||
id: z.string().uuid(),
|
||||
created_at: z.string(),
|
||||
uri: z.string().url(),
|
||||
type: z.string(),
|
||||
extensions: z.object({
|
||||
"org.lysand:custom_emojis": z.object({
|
||||
emojis: z.array(
|
||||
z.object({
|
||||
name: z.string().regex(emojiValidator),
|
||||
url: ContentFormat,
|
||||
}),
|
||||
),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
const Visibility = z.enum(["public", "unlisted", "private", "direct"]);
|
||||
|
||||
const Publication = Entity.extend({
|
||||
type: z.enum(["Note", "Patch"]),
|
||||
author: z.string().url(),
|
||||
content: ContentFormat.optional(),
|
||||
attachments: z.array(ContentFormat).optional(),
|
||||
replies_to: z.string().url().optional(),
|
||||
quotes: z.string().url().optional(),
|
||||
mentions: z.array(z.string().url()).optional(),
|
||||
subject: z.string().optional(),
|
||||
is_sensitive: z.boolean().optional(),
|
||||
visibility: Visibility,
|
||||
extensions: Entity.shape.extensions.extend({
|
||||
"org.lysand:reactions": z
|
||||
.object({
|
||||
reactions: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
"org.lysand:polls": z
|
||||
.object({
|
||||
poll: z.object({
|
||||
options: z.array(ContentFormat),
|
||||
votes: z.array(z.number().int().nonnegative()),
|
||||
multiple_choice: z.boolean().optional(),
|
||||
expires_at: z.string(),
|
||||
}),
|
||||
})
|
||||
.optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
const Note = Publication.extend({
|
||||
type: z.literal("Note"),
|
||||
});
|
||||
|
||||
const Patch = Publication.extend({
|
||||
type: z.literal("Patch"),
|
||||
patched_id: z.string().uuid(),
|
||||
patched_at: z.string(),
|
||||
});
|
||||
|
||||
const ActorPublicKeyData = z.object({
|
||||
public_key: z.string(),
|
||||
actor: z.string().url(),
|
||||
});
|
||||
|
||||
const VanityExtension = z.object({
|
||||
avatar_overlay: ContentFormat.optional(),
|
||||
avatar_mask: ContentFormat.optional(),
|
||||
background: ContentFormat.optional(),
|
||||
audio: ContentFormat.optional(),
|
||||
pronouns: z.record(
|
||||
z.string(),
|
||||
z.array(
|
||||
z.union([
|
||||
z.object({
|
||||
subject: z.string(),
|
||||
object: z.string(),
|
||||
dependent_possessive: z.string(),
|
||||
independent_possessive: z.string(),
|
||||
reflexive: z.string(),
|
||||
}),
|
||||
z.string(),
|
||||
]),
|
||||
),
|
||||
),
|
||||
birthday: z.string().optional(),
|
||||
location: z.string().optional(),
|
||||
activitypub: z.string().optional(),
|
||||
});
|
||||
|
||||
const User = Entity.extend({
|
||||
type: z.literal("User"),
|
||||
display_name: z.string().optional(),
|
||||
username: z.string(),
|
||||
avatar: ContentFormat.optional(),
|
||||
header: ContentFormat.optional(),
|
||||
indexable: z.boolean(),
|
||||
public_key: ActorPublicKeyData,
|
||||
bio: ContentFormat.optional(),
|
||||
fields: z
|
||||
.array(
|
||||
z.object({
|
||||
name: ContentFormat,
|
||||
value: ContentFormat,
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
featured: z.string().url(),
|
||||
followers: z.string().url(),
|
||||
following: z.string().url(),
|
||||
likes: z.string().url(),
|
||||
dislikes: z.string().url(),
|
||||
inbox: z.string().url(),
|
||||
outbox: z.string().url(),
|
||||
extensions: Entity.shape.extensions.extend({
|
||||
"org.lysand:vanity": VanityExtension.optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
const Action = Entity.extend({
|
||||
type: z.union([
|
||||
z.literal("Like"),
|
||||
z.literal("Dislike"),
|
||||
z.literal("Follow"),
|
||||
z.literal("FollowAccept"),
|
||||
z.literal("FollowReject"),
|
||||
z.literal("Announce"),
|
||||
z.literal("Undo"),
|
||||
]),
|
||||
author: z.string().url(),
|
||||
});
|
||||
|
||||
const Like = Action.extend({
|
||||
type: z.literal("Like"),
|
||||
object: z.string().url(),
|
||||
});
|
||||
|
||||
const Undo = Action.extend({
|
||||
type: z.literal("Undo"),
|
||||
object: z.string().url(),
|
||||
});
|
||||
|
||||
const Dislike = Action.extend({
|
||||
type: z.literal("Dislike"),
|
||||
object: z.string().url(),
|
||||
});
|
||||
|
||||
const Follow = Action.extend({
|
||||
type: z.literal("Follow"),
|
||||
followee: z.string().url(),
|
||||
});
|
||||
|
||||
const FollowAccept = Action.extend({
|
||||
type: z.literal("FollowAccept"),
|
||||
follower: z.string().url(),
|
||||
});
|
||||
|
||||
const FollowReject = Action.extend({
|
||||
type: z.literal("FollowReject"),
|
||||
follower: z.string().url(),
|
||||
});
|
||||
|
||||
const Announce = Action.extend({
|
||||
type: z.literal("Announce"),
|
||||
object: z.string().url(),
|
||||
});
|
||||
|
||||
const Extension = Entity.extend({
|
||||
type: z.literal("Extension"),
|
||||
extension_type: z.string().regex(
|
||||
createRegExp(
|
||||
// org namespace, then colon, then alphanumeric/_/-, then extension name
|
||||
exactly(
|
||||
oneOrMore(
|
||||
exactly(letter.lowercase.or(digit).or(charIn("_-."))),
|
||||
),
|
||||
exactly(":"),
|
||||
oneOrMore(exactly(letter.lowercase.or(digit).or(charIn("_-")))),
|
||||
exactly("/"),
|
||||
oneOrMore(exactly(letter.or(digit).or(charIn("_-")))),
|
||||
),
|
||||
),
|
||||
"extension_type must be in the format '<namespaced_url>:extension_name/Extension_type', e.g. 'org.lysand:reactions/Reaction'. Notably, only the type can have uppercase letters.",
|
||||
),
|
||||
});
|
||||
|
||||
const Reaction = Extension.extend({
|
||||
extension_type: z.literal("org.lysand:reactions/Reaction"),
|
||||
object: z.string().url(),
|
||||
content: z.string(),
|
||||
});
|
||||
|
||||
const Poll = Extension.extend({
|
||||
extension_type: z.literal("org.lysand:polls/Poll"),
|
||||
options: z.array(ContentFormat),
|
||||
votes: z.array(z.number().int().nonnegative()),
|
||||
multiple_choice: z.boolean().optional(),
|
||||
expires_at: z.string(),
|
||||
});
|
||||
|
||||
const Vote = Extension.extend({
|
||||
extension_type: z.literal("org.lysand:polls/Vote"),
|
||||
poll: z.string().url(),
|
||||
option: z.number(),
|
||||
});
|
||||
|
||||
const VoteResult = Extension.extend({
|
||||
extension_type: z.literal("org.lysand:polls/VoteResult"),
|
||||
poll: z.string().url(),
|
||||
votes: z.array(z.number().int().nonnegative()),
|
||||
});
|
||||
|
||||
const Report = Extension.extend({
|
||||
extension_type: z.literal("org.lysand:reports/Report"),
|
||||
objects: z.array(z.string().url()),
|
||||
reason: z.string(),
|
||||
comment: z.string().optional(),
|
||||
});
|
||||
|
||||
const ServerMetadata = Entity.extend({
|
||||
type: z.literal("ServerMetadata"),
|
||||
name: z.string(),
|
||||
version: z.string(),
|
||||
description: z.string().optional(),
|
||||
website: z.string().optional(),
|
||||
moderators: z.array(z.string()).optional(),
|
||||
admins: z.array(z.string()).optional(),
|
||||
logo: ContentFormat.optional(),
|
||||
banner: ContentFormat.optional(),
|
||||
supported_extensions: z.array(z.string()),
|
||||
extensions: z.record(z.string(), z.any()).optional(),
|
||||
});
|
||||
|
||||
export const schemas = {
|
||||
Entity,
|
||||
ContentFormat,
|
||||
Visibility,
|
||||
Publication,
|
||||
Note,
|
||||
Patch,
|
||||
ActorPublicKeyData,
|
||||
VanityExtension,
|
||||
User,
|
||||
Action,
|
||||
Like,
|
||||
Undo,
|
||||
Dislike,
|
||||
Follow,
|
||||
FollowAccept,
|
||||
FollowReject,
|
||||
Announce,
|
||||
Extension,
|
||||
Reaction,
|
||||
Poll,
|
||||
Vote,
|
||||
VoteResult,
|
||||
Report,
|
||||
ServerMetadata,
|
||||
};
|
||||
Loading…
Reference in a new issue