mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 16:38: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