mirror of
https://github.com/versia-pub/api.git
synced 2025-12-06 16:38:20 +01:00
feat: ✨ Port code from Lysand Server and improve it
This commit is contained in:
parent
ef76ee4a5c
commit
bf8898acee
20
biome.json
Normal file
20
biome.json
Normal 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
17
build.ts
Normal 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");
|
||||||
|
|
@ -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
53
federation/index.ts
Normal 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,
|
||||||
|
};
|
||||||
|
|
@ -38,9 +38,10 @@
|
||||||
"bun": ">=1.1.8"
|
"bun": ">=1.1.8"
|
||||||
},
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
|
".": {
|
||||||
"import": "./dist/index.js",
|
"import": "./dist/index.js",
|
||||||
"types": "./dist/index.d.ts",
|
|
||||||
"default": "./dist/index.js"
|
"default": "./dist/index.js"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
|
|
@ -56,8 +57,11 @@
|
||||||
"validation"
|
"validation"
|
||||||
],
|
],
|
||||||
"packageManager": "bun@1.1.8",
|
"packageManager": "bun@1.1.8",
|
||||||
"devDependencies": {
|
"dependencies": {
|
||||||
"@biomejs/biome": "^1.7.3"
|
"@types/mime-types": "^2.1.4",
|
||||||
},
|
"magic-regexp": "^0.8.0",
|
||||||
"trustedDependencies": ["@biomejs/biome"]
|
"mime-types": "^2.1.35",
|
||||||
|
"zod": "^3.23.8",
|
||||||
|
"zod-validation-error": "^3.3.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
188
federation/schemas/base.ts
Normal file
188
federation/schemas/base.ts
Normal 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,
|
||||||
|
};
|
||||||
17
federation/schemas/content_format.ts
Normal file
17
federation/schemas/content_format.ts
Normal 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(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
50
federation/schemas/extensions/custom_emojis.ts
Normal file
50
federation/schemas/extensions/custom_emojis.ts
Normal 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,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
});
|
||||||
92
federation/schemas/extensions/polls.ts
Normal file
92
federation/schemas/extensions/polls.ts
Normal 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()),
|
||||||
|
});
|
||||||
28
federation/schemas/extensions/reactions.ts
Normal file
28
federation/schemas/extensions/reactions.ts
Normal 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(),
|
||||||
|
});
|
||||||
93
federation/schemas/extensions/vanity.ts
Normal file
93
federation/schemas/extensions/vanity.ts
Normal 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(),
|
||||||
|
});
|
||||||
40
federation/schemas/regex.ts
Normal file
40
federation/schemas/regex.ts
Normal 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("_-")))),
|
||||||
|
),
|
||||||
|
);
|
||||||
12
federation/tests/index.test.ts
Normal file
12
federation/tests/index.test.ts
Normal 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
8
federation/tsconfig.json
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"extends": "../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"paths": {
|
||||||
|
"~/*": ["./*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
package.json
12
package.json
|
|
@ -4,6 +4,16 @@
|
||||||
"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
32
tsconfig.json
Normal 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"]
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue