feat: Port code from Lysand Server and improve it

This commit is contained in:
Jesse Wierzbinski 2024-05-13 21:00:05 -10:00
parent ef76ee4a5c
commit bf8898acee
No known key found for this signature in database
18 changed files with 733 additions and 89 deletions

View file

@ -1,3 +1,3 @@
{ {
"conventionalCommits.scopes": ["docs"] "conventionalCommits.scopes": ["docs"]
} }

20
biome.json Normal file
View file

@ -0,0 +1,20 @@
{
"$schema": "https://biomejs.dev/schemas/1.6.4/schema.json",
"organizeImports": {
"enabled": true,
"ignore": ["node_modules", "dist"]
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
},
"ignore": ["node_modules", "dist"]
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 4,
"ignore": ["node_modules", "dist"]
}
}

17
build.ts Normal file
View file

@ -0,0 +1,17 @@
import dts from "bun-plugin-dts";
import ora from "ora";
const spinner = ora("Building...").start();
await Bun.build({
entrypoints: ["federation/index.ts"],
outdir: "federation/dist",
format: "esm",
minify: true,
sourcemap: "external",
splitting: true,
target: "browser",
plugins: [dts()],
});
spinner.succeed("Built federation module");

BIN
bun.lockb

Binary file not shown.

View file

@ -1,20 +0,0 @@
{
"$schema": "https://biomejs.dev/schemas/1.6.4/schema.json",
"organizeImports": {
"enabled": true,
"ignore": ["node_modules", "dist"]
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
},
"ignore": ["node_modules", "dist"]
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 4,
"ignore": ["node_modules", "dist"]
}
}

53
federation/index.ts Normal file
View file

@ -0,0 +1,53 @@
/**
* @file index.ts
* @fileoverview Main entrypoint and export for the module
* @module federation
* @see module:federation/schemas/base
*/
import type { z } from "zod";
import {
Action,
ActorPublicKeyData,
ContentFormat,
Dislike,
Entity,
Extension,
Follow,
FollowAccept,
FollowReject,
Like,
Note,
Patch,
Publication,
Report,
ServerMetadata,
Undo,
User,
VanityExtension,
Visibility,
} from "~/federation/schemas/base";
export type InferType<T extends z.AnyZodObject> = z.infer<T>;
export {
Entity,
ContentFormat,
Visibility,
Publication,
Note,
Patch,
ActorPublicKeyData,
VanityExtension,
User,
Action,
Like,
Undo,
Dislike,
Follow,
FollowAccept,
FollowReject,
Extension,
Report,
ServerMetadata,
};

View file

@ -1,63 +1,67 @@
{ {
"name": "@lysand-org/federation", "name": "@lysand-org/federation",
"displayName": "Lysand Federation", "displayName": "Lysand Federation",
"version": "3.0.0", "version": "3.0.0",
"author": { "author": {
"email": "jesse.wierzbinski@lysand.org", "email": "jesse.wierzbinski@lysand.org",
"name": "Jesse Wierzbinski (CPlusPatch)", "name": "Jesse Wierzbinski (CPlusPatch)",
"url": "https://cpluspatch.com" "url": "https://cpluspatch.com"
}, },
"readme": "README.md", "readme": "README.md",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/lysand-org/api.git", "url": "https://github.com/lysand-org/api.git",
"directory": "federation" "directory": "federation"
}, },
"bugs": { "bugs": {
"url": "https://github.com/lysand-org/api/issues" "url": "https://github.com/lysand-org/api/issues"
}, },
"license": "MIT", "license": "MIT",
"contributors": [ "contributors": [
{ {
"name": "Jesse Wierzbinski", "name": "Jesse Wierzbinski",
"email": "jesse.wierzbinski@lysand.org", "email": "jesse.wierzbinski@lysand.org",
"url": "https://cpluspatch.com" "url": "https://cpluspatch.com"
} }
], ],
"maintainers": [ "maintainers": [
{ {
"name": "Jesse Wierzbinski", "name": "Jesse Wierzbinski",
"email": "jesse.wierzbinski@lysand.org", "email": "jesse.wierzbinski@lysand.org",
"url": "https://cpluspatch.com" "url": "https://cpluspatch.com"
} }
], ],
"description": "Type definitions for Lysand Federation, with validators.", "description": "Type definitions for Lysand Federation, with validators.",
"categories": ["Other"], "categories": ["Other"],
"type": "module", "type": "module",
"engines": { "engines": {
"bun": ">=1.1.8" "bun": ">=1.1.8"
}, },
"exports": { "exports": {
"import": "./dist/index.js", ".": {
"types": "./dist/index.d.ts", "import": "./dist/index.js",
"default": "./dist/index.js" "default": "./dist/index.js"
}, }
"funding": { },
"type": "opencollective", "funding": {
"url": "https://opencollective.com/lysand" "type": "opencollective",
}, "url": "https://opencollective.com/lysand"
"homepage": "https://lysand.org", },
"keywords": [ "homepage": "https://lysand.org",
"lysand", "keywords": [
"federation", "lysand",
"api", "federation",
"typescript", "api",
"zod", "typescript",
"validation" "zod",
], "validation"
"packageManager": "bun@1.1.8", ],
"devDependencies": { "packageManager": "bun@1.1.8",
"@biomejs/biome": "^1.7.3" "dependencies": {
}, "@types/mime-types": "^2.1.4",
"trustedDependencies": ["@biomejs/biome"] "magic-regexp": "^0.8.0",
"mime-types": "^2.1.35",
"zod": "^3.23.8",
"zod-validation-error": "^3.3.0"
}
} }

188
federation/schemas/base.ts Normal file
View file

@ -0,0 +1,188 @@
import { z } from "zod";
import { ContentFormat } from "./content_format";
import { CustomEmojiExtension } from "./extensions/custom_emojis";
import { VanityExtension } from "./extensions/vanity";
import { extensionTypeRegex } from "./regex";
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": CustomEmojiExtension.optional(),
}),
});
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 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 Extension = Entity.extend({
type: z.literal("Extension"),
extension_type: z
.string()
.regex(
extensionTypeRegex,
"extension_type must be in the format 'namespaced_url:extension_name/ExtensionType', e.g. 'org.lysand:reactions/Reaction'. Notably, only the type can have uppercase letters.",
),
});
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 {
Entity,
Visibility,
Publication,
Note,
Patch,
ActorPublicKeyData,
VanityExtension,
User,
Action,
Like,
Undo,
Dislike,
Follow,
FollowAccept,
FollowReject,
Extension,
Report,
ServerMetadata,
ContentFormat,
CustomEmojiExtension,
};

View file

@ -0,0 +1,17 @@
import { types } from "mime-types";
import { z } from "zod";
export 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(),
}),
);

View file

@ -0,0 +1,50 @@
/**
* Custom emojis extension.
* @module federation/schemas/extensions/custom_emojis
* @see module:federation/schemas/base
* @see https://lysand.org/extensions/custom-emojis
*/
import { z } from "zod";
import { ContentFormat } from "../content_format";
import { emojiRegex } from "../regex";
/**
* @description Used to validate the properties the extension's custom field
* @see https://lysand.org/extensions/custom-emojis
* @example
* {
* // ...
* "extensions": {
* "org.lysand:custom_emojis": {
* "emojis": [
* {
* "name": "happy_face",
* "url": {
* "image/png": {
* "content": "https://cdn.example.com/emojis/happy_face.png",
* "content_type": "image/png"
* }
* }
* },
* // ...
* ]
* }
* }
* // ...
* }
*/
export const CustomEmojiExtension = z.object({
emojis: z.array(
z.object({
name: z
.string()
.min(1)
.max(256)
.regex(
emojiRegex,
"Emoji name must be alphanumeric, underscores, or dashes.",
),
url: ContentFormat,
}),
),
});

View file

@ -0,0 +1,92 @@
/**
* Polls extension
* @module federation/schemas/extensions/polls
* @see module:federation/schemas/base
* @see https://lysand.org/extensions/polls
*/
import { z } from "zod";
import { Extension } from "../base";
import { ContentFormat } from "../content_format";
/**
* @description Poll extension entity
* @see https://lysand.org/extensions/polls
* @example
* {
* "type": "Extension",
* "id": "d6eb84ea-cd13-43f9-9c54-01244da8e5e3",
* "extension_type": "org.lysand:polls/Poll",
* "uri": "https://example.com/polls/d6eb84ea-cd13-43f9-9c54-01244da8e5e3",
* "options": [
* {
* "text/plain": {
* "content": "Red"
* }
* },
* {
* "text/plain": {
* "content": "Blue"
* }
* },
* {
* "text/plain": {
* "content": "Green"
* }
* }
* ],
* "votes": [
* 9,
* 5,
* 0
* ],
* "multiple_choice": false,
* "expires_at": "2021-01-04T00:00:00.000Z"
* }
*/
export 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(),
});
/**
* @description Vote extension entity
* @see https://lysand.org/extensions/polls
* @example
* {
* "type": "Extension",
* "id": "31c4de70-e266-4f61-b0f7-3767d3ccf565",
* "created_at": "2021-01-01T00:00:00.000Z",
* "uri": "https://example.com/votes/31c4de70-e266-4f61-b0f7-3767d3ccf565",
* "extension_type": "org.lysand:polls/Vote",
* "poll": "https://example.com/polls/31c4de70-e266-4f61-b0f7-3767d3ccf565",
* "option": 1
* }
*/
export const Vote = Extension.extend({
extension_type: z.literal("org.lysand:polls/Vote"),
poll: z.string().url(),
option: z.number(),
});
/**
* @description Vote result extension entity
* @see https://lysand.org/extensions/polls
* @example
* {
* "type": "Extension",
* "id": "c6d5755b-f42c-418f-ab53-2ee3705d6628",
* "created_at": "2021-01-01T00:00:00.000Z",
* "uri": "https://example.com/polls/c6d5755b-f42c-418f-ab53-2ee3705d6628/result",
* "extension_type": "org.lysand:polls/VoteResult",
* "poll": "https://example.com/polls/c6d5755b-f42c-418f-ab53-2ee3705d6628",
* "votes": [9, 5, 0]
* }
*/
export const VoteResult = Extension.extend({
extension_type: z.literal("org.lysand:polls/VoteResult"),
poll: z.string().url(),
votes: z.array(z.number().int().nonnegative()),
});

View file

@ -0,0 +1,28 @@
/**
* Reactions extension
* @module federation/schemas/extensions/reactions
* @see module:federation/schemas/base
* @see https://lysand.org/extensions/reactions
*/
import { z } from "zod";
import { Extension } from "../base";
/**
* @description Reaction extension entity
* @see https://lysand.org/extensions/reactions
* @example
* {
* "type": "Extension",
* "id": "d6eb84ea-cd13-43f9-9c54-01244da8e5e3",
* "created_at": "2021-01-01T00:00:00.000Z",
* "uri": "https://example.com/reactions/d6eb84ea-cd13-43f9-9c54-01244da8e5e3",
* "extension_type": "org.lysand:reactions/Reaction",
* "object": "https://example.com/posts/d6eb84ea-cd13-43f9-9c54-01244da8e5e3",
* "content": "👍"
* }
*/
export const Reaction = Extension.extend({
extension_type: z.literal("org.lysand:reactions/Reaction"),
object: z.string().url(),
content: z.string(),
});

View file

@ -0,0 +1,93 @@
/**
* Vanity extension schema.
* @module federation/schemas/extensions/vanity
* @see module:federation/schemas/base
* @see https://lysand.org/extensions/vanity
*/
import { z } from "zod";
import { ContentFormat } from "../content_format";
/**
* @description Vanity extension entity
* @see https://lysand.org/extensions/vanity
* @example
* {
* // ...
* "type": "User",
* // ...
* "extensions": {
* "org.lysand:vanity": {
* "avatar_overlay": {
* "image/png": {
* "content": "https://cdn.example.com/ab5081cf-b11f-408f-92c2-7c246f290593/cat_ears.png",
* "content_type": "image/png"
* }
* },
* "avatar_mask": {
* "image/png": {
* "content": "https://cdn.example.com/d8c42be1-d0f7-43ef-b4ab-5f614e1beba4/rounded_square.jpeg",
* "content_type": "image/jpeg"
* }
* },
* "background": {
* "image/png": {
* "content": "https://cdn.example.com/6492ddcd-311e-4921-9567-41b497762b09/untitled-file-0019822.png",
* "content_type": "image/png"
* }
* },
* "audio": {
* "audio/mpeg": {
* "content": "https://cdn.example.com/4da2f0d4-4728-4819-83e4-d614e4c5bebc/michael-jackson-thriller.mp3",
* "content_type": "audio/mpeg"
* }
* },
* "pronouns": {
* "en-us": [
* "he/him",
* {
* "subject": "they",
* "object": "them",
* "dependent_possessive": "their",
* "independent_possessive": "theirs",
* "reflexive": "themself"
* },
* ]
* },
* "birthday": "1998-04-12",
* "location": "+40.6894-074.0447/",
* "activitypub": [
* "@erikuden@mastodon.de"
* ],
* "aliases": [
* "https://burger.social/accounts/349ee237-c672-41c1-aadc-677e185f795a",
* "https://social.lysand.org/users/f565ef02-035d-4974-ba5e-f62a8558331d"
* ]
* }
* }
* }
*/
export 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(),
});

View file

@ -0,0 +1,40 @@
/**
* Regular expressions for matching various strings.
* @module federation/schemas/regex
* @see module:federation/schemas/base
*/
import {
caseInsensitive,
charIn,
createRegExp,
digit,
exactly,
global,
letter,
oneOrMore,
} from "magic-regexp";
/**
* Regular expression for matching emojis.
*/
export const emojiRegex = createRegExp(
// A-Z a-z 0-9 _ -
oneOrMore(letter.or(digit).or(exactly("_")).or(exactly("-"))),
[caseInsensitive, global],
);
/**
* Regular expression for matching an extension_type
* @example org.lysand:custom_emojis/Emoji
*/
export const extensionTypeRegex = 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("_-")))),
),
);

View file

@ -0,0 +1,12 @@
import { describe, expect, it } from "bun:test";
import { Note } from "../schemas/base";
describe("Package testing", () => {
it("should not validate a bad Note", () => {
const badObject = {
IamBad: "Note",
};
expect(Note.parseAsync(badObject)).rejects.toThrow();
});
});

8
federation/tsconfig.json Normal file
View file

@ -0,0 +1,8 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"paths": {
"~/*": ["./*"]
}
}
}

View file

@ -1,9 +1,19 @@
{ {
"name": "lysand-api", "name": "lysand-api",
"private": true, "private": true,
"workspaces": ["federation"], "workspaces": ["federation"],
"scripts": { "scripts": {
"lint": "bunx @biomejs/biome check .", "lint": "bunx @biomejs/biome check .",
"build": "echo 'Not implemented :(' && exit 1" "build": "bun run build.ts"
} },
"devDependencies": {
"@biomejs/biome": "^1.7.3",
"bun-plugin-dts": "^0.2.3",
"bun-types": "^1.1.8"
},
"trustedDependencies": ["@biomejs/biome"],
"dependencies": {
"chalk": "^5.3.0",
"ora": "^8.0.1"
}
} }

32
tsconfig.json Normal file
View file

@ -0,0 +1,32 @@
{
"compilerOptions": {
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "esnext",
"target": "esnext",
"moduleResolution": "Bundler",
"moduleDetection": "force",
"allowImportingTsExtensions": true,
"noEmit": true,
"composite": true,
"strict": true,
"downlevelIteration": true,
"skipLibCheck": true,
"noImplicitAny": true,
"jsx": "preserve",
"allowSyntheticDefaultImports": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"forceConsistentCasingInFileNames": true,
"allowJs": true,
"emitDecoratorMetadata": false,
"experimentalDecorators": true,
"verbatimModuleSyntax": true,
"types": [
"bun-types" // add Bun global
],
"paths": {
"~/*": ["./*"]
}
},
"include": ["*.ts", "*.d.ts", "**/*.ts", "**/*.d.ts"]
}