mirror of
https://github.com/versia-pub/server.git
synced 2026-03-13 13:59:16 +01:00
refactor(api): ♻️ Move all client schema code to new package
This commit is contained in:
parent
52602c3da7
commit
3fe07a79b8
128 changed files with 3904 additions and 169 deletions
141
packages/client/README.md
Normal file
141
packages/client/README.md
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
<p align="center">
|
||||
<a href="https://versia.pub"><img src="https://cdn.versia.pub/branding/logo-dark.svg" alt="Versia Logo" height="110"></a>
|
||||
</p>
|
||||
|
||||
<center><h1><code>@versia/client</code></h1></center>
|
||||
|
||||
TypeScript client API for Versia and Mastodon servers.
|
||||
|
||||
## Efficiency
|
||||
|
||||
The built output of the package is not even `32 KB` in size, making it a lightweight and efficient solution for your Versia needs. Installing the package adds around `5 MB` to your `node_modules` folder, but this does not affect the final bundle size.
|
||||
|
||||
Compilation (bundling/minifying) time is a few seconds, almost all of which is spent on type-checking. The actual compilation time is less than a tenth of a second.
|
||||
|
||||
## Usage
|
||||
|
||||
This application may be used in the same was as [`megalodon`](https://github.com/h3poteto/megalodon).
|
||||
|
||||
Initialize the client with the following code:
|
||||
|
||||
```typescript
|
||||
import { Client } from "@versia/client";
|
||||
|
||||
const baseUrl = new URL("https://versia.social");
|
||||
const accessToken = "...";
|
||||
|
||||
const client = new Client(baseUrl, accessToken);
|
||||
```
|
||||
|
||||
The client can then be used to interact with the server:
|
||||
|
||||
```typescript
|
||||
const { data: status } = await client.postStatus("Hey there!");
|
||||
```
|
||||
|
||||
```typescript
|
||||
const { data: posts } = await client.getHomeTimeline();
|
||||
```
|
||||
|
||||
Use your editor's IntelliSense to see all available methods and properties. JSDoc comments are always available. Method names are the same as with Megalodon, but with slight parameter changes in some cases.
|
||||
|
||||
All methods have a special `extra` parameter that can be used to pass additional parameters to the underlying HTTP request. This can be used to pass query parameters, headers, etc.:
|
||||
|
||||
```typescript
|
||||
// extra is a RequestInit, the same as the second parameter of native fetch
|
||||
const { data: posts } = await client.getHomeTimeline({
|
||||
headers: { "User-Agent": "MyApp/3" },
|
||||
signal: new AbortSignal(),
|
||||
});
|
||||
```
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
#### For Usage
|
||||
|
||||
See the [**Compatibility**](#compatibility) section for the supported environments. Any package manager can be used to install the packages.
|
||||
|
||||
#### For Development
|
||||
|
||||
- [**Bun**](https://bun.sh) version `1.1.8` or higher.
|
||||
- Either the [**Linux**](https://www.linux.org) or [**macOS**](https://www.apple.com/macos) operating systems. ([**Windows**](https://www.microsoft.com/windows) will work, but is not officially supported.)
|
||||
|
||||
### Compatibility
|
||||
|
||||
This library is built for JavaScript runtimes with the support for:
|
||||
|
||||
- [**ES Modules**](https://nodejs.org/api/esm.html)
|
||||
- [**ECMAScript 2020**](https://www.ecma-international.org/ecma-262/11.0/index.html)
|
||||
|
||||
#### Runtimes
|
||||
|
||||
- **Node.js**: 14.0+ is the minimum, but only Node.js 20.0+ (LTS) is officially supported.
|
||||
- **Deno**: Support is unknown. 1.0+ is expected to work.
|
||||
- **Bun**: Bun 1.1.8 is the minimum-supported version. As Bun is rapidly evolving, this may change. Previous versions may also work.
|
||||
|
||||
#### Browsers
|
||||
|
||||
Consequently, this library is compatible without any bundling in the following browser versions:
|
||||
|
||||
- **Chrome**: 80+
|
||||
- **Edge**: 80+
|
||||
- **Firefox**: 74+
|
||||
- **Safari**: 13.1+
|
||||
- **Opera**: 67+
|
||||
- **Internet Explorer**: None
|
||||
|
||||
If you are targeting older browsers, please don't, you are doing yourself a disservice.
|
||||
|
||||
Transpilation to non-ES Module environments is not officially supported, but should be simple with the use of a bundler like [**Parcel**](https://parceljs.org) or [**Rollup**](https://rollupjs.org).
|
||||
|
||||
### Installation
|
||||
|
||||
Package is distributed as a scoped package on the NPM registry and [JSR](https://jsr.io).
|
||||
|
||||
We strongly recommend using JSR over NPM for all your packages that are available on it.
|
||||
|
||||
```bash
|
||||
# NPM version
|
||||
deno add npm:@versia/client # For Deno
|
||||
npm install @versia/client # For NPM
|
||||
yarn add @versia/client # For Yarn
|
||||
pnpm add @versia/client # For PNPM
|
||||
bun add @versia/client # For Bun
|
||||
|
||||
# JSR version
|
||||
deno add @versia/client # For Deno
|
||||
npx jsr add @versia/client # For JSR
|
||||
yarn dlx jsr add @versia/client # For Yarn
|
||||
pnpm dlx jsr add @versia/client # For PNPM
|
||||
bunx jsr add @versia/client # For Bun
|
||||
```
|
||||
|
||||
#### From Source
|
||||
|
||||
If you want to install from source, you can clone [this repository](https://github.com/versia-pub/api) and run the following commands:
|
||||
|
||||
```bash
|
||||
bun install # Install dependencies
|
||||
|
||||
bun run build # Build the packages
|
||||
```
|
||||
|
||||
The built package will be in the `client/dist` folder.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
### Projects
|
||||
|
||||
- [**Bun**](https://bun.sh): Thanks to the Bun team for creating an amazing JavaScript runtime.
|
||||
- [**TypeScript**](https://www.typescriptlang.org): TypeScript is the backbone of this project.
|
||||
- [**Node.js**](https://nodejs.org): Node.js created the idea of JavaScript on the server.
|
||||
|
||||
### People
|
||||
|
||||
- [**April John**](https://github.com/cutestnekoaqua): Creator and maintainer of the Versia Server ActivityPub bridge.
|
||||
3
packages/client/index.ts
Normal file
3
packages/client/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
// biome-ignore lint/performance/noBarrelFile: <explanation>
|
||||
export { type Output, ResponseError } from "./versia/base.ts";
|
||||
export { Client } from "./versia/client.ts";
|
||||
9
packages/client/jsr.jsonc
Normal file
9
packages/client/jsr.jsonc
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"$schema": "https://jsr.io/schema/config-file.v1.json",
|
||||
"name": "@versia/client",
|
||||
"version": "0.2.0-alpha.1",
|
||||
"exports": {
|
||||
".": "./index.ts",
|
||||
"./schemas": "./schemas.ts"
|
||||
}
|
||||
}
|
||||
61
packages/client/package.json
Normal file
61
packages/client/package.json
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
{
|
||||
"name": "@versia/client-ng",
|
||||
"displayName": "Versia Client",
|
||||
"version": "0.2.0-alpha.1",
|
||||
"author": {
|
||||
"email": "jesse.wierzbinski@lysand.org",
|
||||
"name": "Jesse Wierzbinski (CPlusPatch)",
|
||||
"url": "https://cpluspatch.com"
|
||||
},
|
||||
"readme": "README.md",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/versia-pub/server.git",
|
||||
"directory": "packages/client"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/versia-pub/server/issues"
|
||||
},
|
||||
"license": "MIT",
|
||||
"contributors": [
|
||||
{
|
||||
"name": "Jesse Wierzbinski",
|
||||
"email": "jesse.wierzbinski@lysand.org",
|
||||
"url": "https://cpluspatch.com"
|
||||
}
|
||||
],
|
||||
"maintainers": [
|
||||
{
|
||||
"name": "Jesse Wierzbinski",
|
||||
"email": "jesse.wierzbinski@lysand.org",
|
||||
"url": "https://cpluspatch.com"
|
||||
}
|
||||
],
|
||||
"description": "Client for Mastodon and Versia API",
|
||||
"categories": ["Other"],
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"bun": ">=1.2.5"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./index.ts",
|
||||
"default": "./index.ts"
|
||||
},
|
||||
"./schemas": {
|
||||
"import": "./schemas.ts",
|
||||
"default": "./schemas.ts"
|
||||
}
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/lysand"
|
||||
},
|
||||
"homepage": "https://versia.pub",
|
||||
"keywords": ["versia", "mastodon", "api", "typescript", "rest"],
|
||||
"packageManager": "bun@1.2.5",
|
||||
"dependencies": {
|
||||
"@badgateway/oauth2-client": "^2.4.2",
|
||||
"@hono/zod-openapi": "^0.19.2"
|
||||
}
|
||||
}
|
||||
39
packages/client/schemas.ts
Normal file
39
packages/client/schemas.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
// biome-ignore lint/performance/noBarrelFile: <explanation>
|
||||
export { AccountWarning } from "./schemas/account-warning.ts";
|
||||
export { Account, Source, Field } from "./schemas/account.ts";
|
||||
export { Appeal } from "./schemas/appeal.ts";
|
||||
export { Application, CredentialApplication } from "./schemas/application.ts";
|
||||
export { Attachment } from "./schemas/attachment.ts";
|
||||
export { PreviewCard, PreviewCardAuthor } from "./schemas/card.ts";
|
||||
export { Context } from "./schemas/context.ts";
|
||||
export { CustomEmoji } from "./schemas/emoji.ts";
|
||||
export { ExtendedDescription } from "./schemas/extended-description.ts";
|
||||
export { FamiliarFollowers } from "./schemas/familiar-followers.ts";
|
||||
export {
|
||||
Filter,
|
||||
FilterKeyword,
|
||||
FilterResult,
|
||||
FilterStatus,
|
||||
} from "./schemas/filters.ts";
|
||||
export { InstanceV1 } from "./schemas/instance-v1.ts";
|
||||
export { Instance } from "./schemas/instance.ts";
|
||||
export { Marker } from "./schemas/marker.ts";
|
||||
export { Notification } from "./schemas/notification.ts";
|
||||
export { Poll, PollOption } from "./schemas/poll.ts";
|
||||
export { Preferences } from "./schemas/preferences.ts";
|
||||
export { PrivacyPolicy } from "./schemas/privacy-policy.ts";
|
||||
export {
|
||||
WebPushSubscription,
|
||||
WebPushSubscriptionInput,
|
||||
} from "./schemas/pushsubscription.ts";
|
||||
export { Relationship } from "./schemas/relationship.ts";
|
||||
export { Report } from "./schemas/report.ts";
|
||||
export { Rule } from "./schemas/rule.ts";
|
||||
export { Search } from "./schemas/search.ts";
|
||||
export { Status, Mention, StatusSource } from "./schemas/status.ts";
|
||||
export { Tag } from "./schemas/tag.ts";
|
||||
export { Token } from "./schemas/token.ts";
|
||||
export { TermsOfService } from "./schemas/tos.ts";
|
||||
export { Role, NoteReaction, SSOConfig } from "./schemas/versia.ts";
|
||||
|
||||
export { Id, iso631, zBoolean } from "./schemas/common.ts";
|
||||
57
packages/client/schemas/account-warning.ts
Normal file
57
packages/client/schemas/account-warning.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import { z } from "@hono/zod-openapi";
|
||||
import { Account } from "./account.ts";
|
||||
import { Appeal } from "./appeal.ts";
|
||||
import { Id } from "./common.ts";
|
||||
|
||||
export const AccountWarning = z
|
||||
.object({
|
||||
id: Id.openapi({
|
||||
description: "The ID of the account warning in the database.",
|
||||
example: "0968680e-fd64-4525-b818-6e1c46fbdb28",
|
||||
}),
|
||||
action: z
|
||||
.enum([
|
||||
"none",
|
||||
"disable",
|
||||
"mark_statuses_as_sensitive",
|
||||
"delete_statuses",
|
||||
"sensitive",
|
||||
"silence",
|
||||
"suspend",
|
||||
])
|
||||
.openapi({
|
||||
description:
|
||||
"Action taken against the account. 'none' = No action was taken, this is a simple warning; 'disable' = The account has been disabled; 'mark_statuses_as_sensitive' = Specific posts from the target account have been marked as sensitive; 'delete_statuses' = Specific statuses from the target account have been deleted; 'sensitive' = All posts from the target account are marked as sensitive; 'silence' = The target account has been limited; 'suspend' = The target account has been suspended.",
|
||||
example: "none",
|
||||
}),
|
||||
text: z.string().openapi({
|
||||
description: "Message from the moderator to the target account.",
|
||||
example: "Please adhere to our community guidelines.",
|
||||
}),
|
||||
status_ids: z
|
||||
.array(Id)
|
||||
.nullable()
|
||||
.openapi({
|
||||
description:
|
||||
"List of status IDs that are relevant to the warning. When action is mark_statuses_as_sensitive or delete_statuses, those are the affected statuses.",
|
||||
example: ["5ee59275-c308-4173-bb1f-58646204579b"],
|
||||
}),
|
||||
target_account: Account.openapi({
|
||||
description:
|
||||
"Account against which a moderation decision has been taken.",
|
||||
}),
|
||||
appeal: Appeal.nullable().openapi({
|
||||
description: "Appeal submitted by the target account, if any.",
|
||||
example: null,
|
||||
}),
|
||||
created_at: z.string().datetime().openapi({
|
||||
description: "When the event took place.",
|
||||
example: "2025-01-04T14:11:00Z",
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
description: "Moderation warning against a particular account.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/AccountWarning",
|
||||
},
|
||||
});
|
||||
429
packages/client/schemas/account.ts
Normal file
429
packages/client/schemas/account.ts
Normal file
|
|
@ -0,0 +1,429 @@
|
|||
import { userAddressValidator } from "@/api.ts";
|
||||
import { z } from "@hono/zod-openapi";
|
||||
import type { Account as ApiAccount } from "@versia/client/types";
|
||||
import { config } from "~/config.ts";
|
||||
import { iso631, zBoolean } from "./common.ts";
|
||||
import { CustomEmoji } from "./emoji.ts";
|
||||
import { Role } from "./versia.ts";
|
||||
|
||||
export const Field = z.object({
|
||||
name: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(config.validation.accounts.max_field_name_characters)
|
||||
.openapi({
|
||||
description: "The key of a given field’s key-value pair.",
|
||||
example: "Freak level",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Account/#name",
|
||||
},
|
||||
}),
|
||||
value: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(config.validation.accounts.max_field_value_characters)
|
||||
.openapi({
|
||||
description: "The value associated with the name key.",
|
||||
example: "<p>High</p>",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Account/#value",
|
||||
},
|
||||
}),
|
||||
verified_at: z
|
||||
.string()
|
||||
.datetime()
|
||||
.nullable()
|
||||
.openapi({
|
||||
description:
|
||||
"Timestamp of when the server verified a URL value for a rel=“me” link.",
|
||||
example: null,
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Account/#verified_at",
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
export const Source = z
|
||||
.object({
|
||||
privacy: z.enum(["public", "unlisted", "private", "direct"]).openapi({
|
||||
description:
|
||||
"The default post privacy to be used for new statuses.",
|
||||
example: "unlisted",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Account/#source-privacy",
|
||||
},
|
||||
}),
|
||||
sensitive: zBoolean.openapi({
|
||||
description:
|
||||
"Whether new statuses should be marked sensitive by default.",
|
||||
example: false,
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Account/#source-sensitive",
|
||||
},
|
||||
}),
|
||||
language: iso631.openapi({
|
||||
description: "The default posting language for new statuses.",
|
||||
example: "en",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Account/#source-language",
|
||||
},
|
||||
}),
|
||||
follow_requests_count: z
|
||||
.number()
|
||||
.int()
|
||||
.nonnegative()
|
||||
.optional()
|
||||
.openapi({
|
||||
description: "The number of pending follow requests.",
|
||||
example: 3,
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Account/#follow_requests_count",
|
||||
},
|
||||
}),
|
||||
note: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(0)
|
||||
.max(config.validation.accounts.max_bio_characters)
|
||||
.refine(
|
||||
(s) =>
|
||||
!config.validation.filters.bio.some((filter) =>
|
||||
filter.test(s),
|
||||
),
|
||||
"Bio contains blocked words",
|
||||
)
|
||||
.openapi({
|
||||
description: "Profile bio, in plain-text instead of in HTML.",
|
||||
example: "ermmm what the meow meow",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Account/#source-note",
|
||||
},
|
||||
}),
|
||||
fields: z
|
||||
.array(Field)
|
||||
.max(config.validation.accounts.max_field_count)
|
||||
.openapi({
|
||||
description: "Metadata about the account.",
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
description:
|
||||
"An extra attribute that contains source values to be used with API methods that verify credentials and update credentials.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Account/#source",
|
||||
},
|
||||
});
|
||||
|
||||
export const Account = z.object({
|
||||
id: z
|
||||
.string()
|
||||
.uuid()
|
||||
.openapi({
|
||||
description: "The account ID in the database.",
|
||||
example: "9e84842b-4db6-4a9b-969d-46ab408278da",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Account/#id",
|
||||
},
|
||||
}),
|
||||
username: z
|
||||
.string()
|
||||
.min(3)
|
||||
.trim()
|
||||
.max(config.validation.accounts.max_username_characters)
|
||||
.regex(
|
||||
/^[a-z0-9_-]+$/,
|
||||
"Username can only contain letters, numbers, underscores and hyphens",
|
||||
)
|
||||
.refine(
|
||||
(s) =>
|
||||
!config.validation.filters.username.some((filter) =>
|
||||
filter.test(s),
|
||||
),
|
||||
"Username contains blocked words",
|
||||
)
|
||||
.refine(
|
||||
(s) =>
|
||||
!config.validation.accounts.disallowed_usernames.some((u) =>
|
||||
u.test(s),
|
||||
),
|
||||
"Username is disallowed",
|
||||
)
|
||||
.openapi({
|
||||
description: "The username of the account, not including domain.",
|
||||
example: "lexi",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Account/#username",
|
||||
},
|
||||
}),
|
||||
acct: z
|
||||
.string()
|
||||
.min(1)
|
||||
.trim()
|
||||
.regex(userAddressValidator, "Invalid user address")
|
||||
.openapi({
|
||||
description:
|
||||
"The Webfinger account URI. Equal to username for local users, or username@domain for remote users.",
|
||||
example: "lexi@beta.versia.social",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Account/#acct",
|
||||
},
|
||||
}),
|
||||
url: z
|
||||
.string()
|
||||
.url()
|
||||
.openapi({
|
||||
description: "The location of the user’s profile page.",
|
||||
example: "https://beta.versia.social/@lexi",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Account/#url",
|
||||
},
|
||||
}),
|
||||
display_name: z
|
||||
.string()
|
||||
.min(3)
|
||||
.trim()
|
||||
.max(config.validation.accounts.max_displayname_characters)
|
||||
.refine(
|
||||
(s) =>
|
||||
!config.validation.filters.displayname.some((filter) =>
|
||||
filter.test(s),
|
||||
),
|
||||
"Display name contains blocked words",
|
||||
)
|
||||
.openapi({
|
||||
description: "The profile’s display name.",
|
||||
example: "Lexi :flower:",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Account/#display_name",
|
||||
},
|
||||
}),
|
||||
note: z
|
||||
.string()
|
||||
.min(0)
|
||||
.max(config.validation.accounts.max_bio_characters)
|
||||
.trim()
|
||||
.refine(
|
||||
(s) =>
|
||||
!config.validation.filters.bio.some((filter) => filter.test(s)),
|
||||
"Bio contains blocked words",
|
||||
)
|
||||
.openapi({
|
||||
description: "The profile’s bio or description.",
|
||||
example: "<p>ermmm what the meow meow</p>",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Account/#note",
|
||||
},
|
||||
}),
|
||||
avatar: z
|
||||
.string()
|
||||
.url()
|
||||
.openapi({
|
||||
description:
|
||||
"An image icon that is shown next to statuses and in the profile.",
|
||||
example:
|
||||
"https://cdn.versia.social/avatars/cff9aea0-0000-43fe-8b5e-e7c7ea69a488/lexi.webp",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Account/#avatar",
|
||||
},
|
||||
}),
|
||||
avatar_static: z
|
||||
.string()
|
||||
.url()
|
||||
.openapi({
|
||||
description:
|
||||
"A static version of the avatar. Equal to avatar if its value is a static image; different if avatar is an animated GIF.",
|
||||
example:
|
||||
"https://cdn.versia.social/avatars/cff9aea0-0000-43fe-8b5e-e7c7ea69a488/lexi.webp",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Account/#avatar_static",
|
||||
},
|
||||
}),
|
||||
header: z
|
||||
.string()
|
||||
.url()
|
||||
.openapi({
|
||||
description:
|
||||
"An image banner that is shown above the profile and in profile cards.",
|
||||
example:
|
||||
"https://cdn.versia.social/headers/a049f8e3-878c-4faa-ae4c-a6bcceddbd9d/femboy_2.webp",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Account/#header",
|
||||
},
|
||||
}),
|
||||
header_static: z
|
||||
.string()
|
||||
.url()
|
||||
.openapi({
|
||||
description:
|
||||
"A static version of the header. Equal to header if its value is a static image; different if header is an animated GIF.",
|
||||
example:
|
||||
"https://cdn.versia.social/headers/a049f8e3-878c-4faa-ae4c-a6bcceddbd9d/femboy_2.webp",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Account/#header_static",
|
||||
},
|
||||
}),
|
||||
locked: zBoolean.openapi({
|
||||
description: "Whether the account manually approves follow requests.",
|
||||
example: false,
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Account/#locked",
|
||||
},
|
||||
}),
|
||||
fields: z
|
||||
.array(Field)
|
||||
.max(config.validation.accounts.max_field_count)
|
||||
.openapi({
|
||||
description:
|
||||
"Additional metadata attached to a profile as name-value pairs.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Account/#fields",
|
||||
},
|
||||
}),
|
||||
emojis: z.array(CustomEmoji).openapi({
|
||||
description:
|
||||
"Custom emoji entities to be used when rendering the profile.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Account/#emojis",
|
||||
},
|
||||
}),
|
||||
bot: zBoolean.openapi({
|
||||
description:
|
||||
"Indicates that the account may perform automated actions, may not be monitored, or identifies as a robot.",
|
||||
example: false,
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Account/#bot",
|
||||
},
|
||||
}),
|
||||
group: z.literal(false).openapi({
|
||||
description: "Indicates that the account represents a Group actor.",
|
||||
example: false,
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Account/#group",
|
||||
},
|
||||
}),
|
||||
discoverable: zBoolean.nullable().openapi({
|
||||
description:
|
||||
"Whether the account has opted into discovery features such as the profile directory.",
|
||||
example: true,
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Account/#discoverable",
|
||||
},
|
||||
}),
|
||||
noindex: zBoolean
|
||||
.nullable()
|
||||
.optional()
|
||||
.openapi({
|
||||
description:
|
||||
"Whether the local user has opted out of being indexed by search engines.",
|
||||
example: false,
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Account/#noindex",
|
||||
},
|
||||
}),
|
||||
// FIXME: Use a proper type
|
||||
moved: z
|
||||
.lazy((): z.ZodType<ApiAccount> => Account as z.ZodType<ApiAccount>)
|
||||
.nullable()
|
||||
.optional()
|
||||
.openapi({
|
||||
description:
|
||||
"Indicates that the profile is currently inactive and that its user has moved to a new account.",
|
||||
example: null,
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Account/#moved",
|
||||
},
|
||||
}),
|
||||
suspended: zBoolean.optional().openapi({
|
||||
description:
|
||||
"An extra attribute returned only when an account is suspended.",
|
||||
example: false,
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Account/#suspended",
|
||||
},
|
||||
}),
|
||||
limited: zBoolean.optional().openapi({
|
||||
description:
|
||||
"An extra attribute returned only when an account is silenced. If true, indicates that the account should be hidden behind a warning screen.",
|
||||
example: false,
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Account/#limited",
|
||||
},
|
||||
}),
|
||||
created_at: z
|
||||
.string()
|
||||
.datetime()
|
||||
.openapi({
|
||||
description: "When the account was created.",
|
||||
example: "2024-10-15T22:00:00.000Z",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Account/#created_at",
|
||||
},
|
||||
}),
|
||||
// TODO
|
||||
last_status_at: z
|
||||
.literal(null)
|
||||
.openapi({
|
||||
description: "When the most recent status was posted.",
|
||||
example: null,
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Account/#last_status_at",
|
||||
},
|
||||
})
|
||||
.nullable(),
|
||||
statuses_count: z
|
||||
.number()
|
||||
.int()
|
||||
.nonnegative()
|
||||
.openapi({
|
||||
description: "How many statuses are attached to this account.",
|
||||
example: 42,
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Account/#statuses_count",
|
||||
},
|
||||
}),
|
||||
followers_count: z
|
||||
.number()
|
||||
.int()
|
||||
.nonnegative()
|
||||
.openapi({
|
||||
description: "The reported followers of this profile.",
|
||||
example: 6,
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Account/#followers_count",
|
||||
},
|
||||
}),
|
||||
following_count: z
|
||||
.number()
|
||||
.int()
|
||||
.nonnegative()
|
||||
.openapi({
|
||||
description: "The reported follows of this profile.",
|
||||
example: 23,
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Account/#following_count",
|
||||
},
|
||||
}),
|
||||
/* Versia Server API extension */
|
||||
uri: z.string().url().openapi({
|
||||
description:
|
||||
"The location of the user's Versia profile page, as opposed to the local representation.",
|
||||
example:
|
||||
"https://beta.versia.social/users/9e84842b-4db6-4a9b-969d-46ab408278da",
|
||||
}),
|
||||
source: Source.optional(),
|
||||
role: z
|
||||
.object({
|
||||
name: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
/* Versia Server API extension */
|
||||
roles: z.array(Role).openapi({
|
||||
description: "Roles assigned to the account.",
|
||||
}),
|
||||
mute_expires_at: z.string().datetime().nullable().openapi({
|
||||
description: "When a timed mute will expire, if applicable.",
|
||||
example: "2025-03-01T14:00:00.000Z",
|
||||
}),
|
||||
});
|
||||
21
packages/client/schemas/appeal.ts
Normal file
21
packages/client/schemas/appeal.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { z } from "@hono/zod-openapi";
|
||||
|
||||
export const Appeal = z
|
||||
.object({
|
||||
text: z.string().openapi({
|
||||
description:
|
||||
"Text of the appeal from the moderated account to the moderators.",
|
||||
example: "I believe this action was taken in error.",
|
||||
}),
|
||||
state: z.enum(["approved", "rejected", "pending"]).openapi({
|
||||
description:
|
||||
"State of the appeal. 'approved' = The appeal has been approved by a moderator, 'rejected' = The appeal has been rejected by a moderator, 'pending' = The appeal has been submitted, but neither approved nor rejected yet.",
|
||||
example: "pending",
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
description: "Appeal against a moderation action.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Appeal",
|
||||
},
|
||||
});
|
||||
84
packages/client/schemas/application.ts
Normal file
84
packages/client/schemas/application.ts
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
import { z } from "@hono/zod-openapi";
|
||||
|
||||
export const Application = z.object({
|
||||
name: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(200)
|
||||
.openapi({
|
||||
description: "The name of your application.",
|
||||
example: "Test Application",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Application/#name",
|
||||
},
|
||||
}),
|
||||
website: z
|
||||
.string()
|
||||
.nullable()
|
||||
.openapi({
|
||||
description: "The website associated with your application.",
|
||||
example: "https://app.example",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Application/#website",
|
||||
},
|
||||
}),
|
||||
scopes: z
|
||||
.array(z.string())
|
||||
.default(["read"])
|
||||
.openapi({
|
||||
description:
|
||||
"The scopes for your application. This is the registered scopes string split on whitespace.",
|
||||
example: ["read", "write", "push"],
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Application/#scopes",
|
||||
},
|
||||
}),
|
||||
redirect_uris: z
|
||||
.array(
|
||||
z
|
||||
.string()
|
||||
.url()
|
||||
.or(z.literal("urn:ietf:wg:oauth:2.0:oob"))
|
||||
.openapi({
|
||||
description: "URL or 'urn:ietf:wg:oauth:2.0:oob'",
|
||||
}),
|
||||
)
|
||||
.openapi({
|
||||
description:
|
||||
"The registered redirection URI(s) for your application.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Application/#redirect_uris",
|
||||
},
|
||||
}),
|
||||
redirect_uri: z.string().openapi({
|
||||
deprecated: true,
|
||||
description:
|
||||
"The registered redirection URI(s) for your application. May contain \\n characters when multiple redirect URIs are registered.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Application/#redirect_uri",
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
export const CredentialApplication = Application.extend({
|
||||
client_id: z.string().openapi({
|
||||
description: "Client ID key, to be used for obtaining OAuth tokens",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/CredentialApplication/#client_id",
|
||||
},
|
||||
}),
|
||||
client_secret: z.string().openapi({
|
||||
description: "Client secret key, to be used for obtaining OAuth tokens",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/CredentialApplication/#client_secret",
|
||||
},
|
||||
}),
|
||||
client_secret_expires_at: z.string().openapi({
|
||||
description:
|
||||
"When the client secret key will expire at, presently this always returns 0 indicating that OAuth Clients do not expire",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/CredentialApplication/#client_secret_expires_at",
|
||||
},
|
||||
}),
|
||||
});
|
||||
76
packages/client/schemas/attachment.ts
Normal file
76
packages/client/schemas/attachment.ts
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
import { z } from "@hono/zod-openapi";
|
||||
import { config } from "~/config.ts";
|
||||
import { Id } from "./common.ts";
|
||||
|
||||
export const Attachment = z
|
||||
.object({
|
||||
id: Id.openapi({
|
||||
description: "The ID of the attachment in the database.",
|
||||
example: "8c33d4c6-2292-4f4d-945d-261836e09647",
|
||||
}),
|
||||
type: z.enum(["unknown", "image", "gifv", "video", "audio"]).openapi({
|
||||
description:
|
||||
"The type of the attachment. 'unknown' = unsupported or unrecognized file type, 'image' = Static image, 'gifv' = Looping, soundless animation, 'video' = Video clip, 'audio' = Audio track.",
|
||||
example: "image",
|
||||
}),
|
||||
url: z.string().url().openapi({
|
||||
description: "The location of the original full-size attachment.",
|
||||
example:
|
||||
"https://files.mastodon.social/media_attachments/files/022/345/792/original/57859aede991da25.jpeg",
|
||||
}),
|
||||
preview_url: z.string().url().nullable().openapi({
|
||||
description:
|
||||
"The location of a scaled-down preview of the attachment.",
|
||||
example:
|
||||
"https://files.mastodon.social/media_attachments/files/022/345/792/small/57859aede991da25.jpeg",
|
||||
}),
|
||||
remote_url: z.string().url().nullable().openapi({
|
||||
description:
|
||||
"The location of the full-size original attachment on the remote website, or null if the attachment is local.",
|
||||
example: null,
|
||||
}),
|
||||
meta: z.record(z.any()).openapi({
|
||||
description:
|
||||
"Metadata. May contain subtrees like 'small' and 'original', and possibly a 'focus' object for smart thumbnail cropping.",
|
||||
example: {
|
||||
original: {
|
||||
width: 640,
|
||||
height: 480,
|
||||
size: "640x480",
|
||||
aspect: 1.3333333333333333,
|
||||
},
|
||||
small: {
|
||||
width: 461,
|
||||
height: 346,
|
||||
size: "461x346",
|
||||
aspect: 1.3323699421965318,
|
||||
},
|
||||
focus: {
|
||||
x: -0.27,
|
||||
y: 0.51,
|
||||
},
|
||||
},
|
||||
}),
|
||||
description: z
|
||||
.string()
|
||||
.trim()
|
||||
.max(config.validation.media.max_description_characters)
|
||||
.nullable()
|
||||
.openapi({
|
||||
description:
|
||||
"Alternate text that describes what is in the media attachment, to be used for the visually impaired or when media attachments do not load.",
|
||||
example: "test media description",
|
||||
}),
|
||||
blurhash: z.string().nullable().openapi({
|
||||
description:
|
||||
"A hash computed by the BlurHash algorithm, for generating colorful preview thumbnails when media has not been downloaded yet.",
|
||||
example: "UFBWY:8_0Jxv4mx]t8t64.%M-:IUWGWAt6M}",
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
description:
|
||||
"Represents a file or media attachment that can be added to a status.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Attachment",
|
||||
},
|
||||
});
|
||||
159
packages/client/schemas/card.ts
Normal file
159
packages/client/schemas/card.ts
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
import { z } from "@hono/zod-openapi";
|
||||
import { Account } from "./account.ts";
|
||||
|
||||
export const PreviewCardAuthor = z.object({
|
||||
name: z.string().openapi({
|
||||
description: "The original resource author’s name.",
|
||||
example: "The Doubleclicks",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/PreviewCardAuthor/#name",
|
||||
},
|
||||
}),
|
||||
url: z
|
||||
.string()
|
||||
.url()
|
||||
.openapi({
|
||||
description: "A link to the author of the original resource.",
|
||||
example: "https://www.youtube.com/user/thedoubleclicks",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/PreviewCardAuthor/#url",
|
||||
},
|
||||
}),
|
||||
account: Account.nullable().openapi({
|
||||
description: "The fediverse account of the author.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/PreviewCardAuthor/#account",
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
export const PreviewCard = z
|
||||
.object({
|
||||
url: z
|
||||
.string()
|
||||
.url()
|
||||
.openapi({
|
||||
description: "Location of linked resource.",
|
||||
example: "https://www.youtube.com/watch?v=OMv_EPMED8Y",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/PreviewCard/#url",
|
||||
},
|
||||
}),
|
||||
title: z
|
||||
.string()
|
||||
.min(1)
|
||||
.openapi({
|
||||
description: "Title of linked resource.",
|
||||
example: "♪ Brand New Friend (Christmas Song!)",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/PreviewCard/#title",
|
||||
},
|
||||
}),
|
||||
description: z.string().openapi({
|
||||
description: "Description of preview.",
|
||||
example: "",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/PreviewCard/#description",
|
||||
},
|
||||
}),
|
||||
type: z.enum(["link", "photo", "video"]).openapi({
|
||||
description: "The type of the preview card.",
|
||||
example: "video",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/PreviewCard/#type",
|
||||
},
|
||||
}),
|
||||
authors: z.array(PreviewCardAuthor).openapi({
|
||||
description:
|
||||
"Fediverse account of the authors of the original resource.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/PreviewCard/#authors",
|
||||
},
|
||||
}),
|
||||
provider_name: z.string().openapi({
|
||||
description: "The provider of the original resource.",
|
||||
example: "YouTube",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/PreviewCard/#provider_name",
|
||||
},
|
||||
}),
|
||||
provider_url: z
|
||||
.string()
|
||||
.url()
|
||||
.openapi({
|
||||
description: "A link to the provider of the original resource.",
|
||||
example: "https://www.youtube.com/",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/PreviewCard/#provider_url",
|
||||
},
|
||||
}),
|
||||
html: z.string().openapi({
|
||||
description: "HTML to be used for generating the preview card.",
|
||||
example:
|
||||
'<iframe width="480" height="270" src="https://www.youtube.com/embed/OMv_EPMED8Y?feature=oembed" frameborder="0" allowfullscreen=""></iframe>',
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/PreviewCard/#html",
|
||||
},
|
||||
}),
|
||||
width: z
|
||||
.number()
|
||||
.int()
|
||||
.openapi({
|
||||
description: "Width of preview, in pixels.",
|
||||
example: 480,
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/PreviewCard/#width",
|
||||
},
|
||||
}),
|
||||
height: z
|
||||
.number()
|
||||
.int()
|
||||
.openapi({
|
||||
description: "Height of preview, in pixels.",
|
||||
example: 270,
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/PreviewCard/#height",
|
||||
},
|
||||
}),
|
||||
image: z
|
||||
.string()
|
||||
.url()
|
||||
.nullable()
|
||||
.openapi({
|
||||
description: "Preview thumbnail.",
|
||||
example:
|
||||
"https://cdn.versia.social/preview_cards/images/014/179/145/original/9cf4b7cf5567b569.jpeg",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/PreviewCard/#image",
|
||||
},
|
||||
}),
|
||||
embed_url: z
|
||||
.string()
|
||||
.url()
|
||||
.openapi({
|
||||
description: "Used for photo embeds, instead of custom html.",
|
||||
example:
|
||||
"https://live.staticflickr.com/65535/49088768431_6a4322b3bb_b.jpg",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/PreviewCard/#embed_url",
|
||||
},
|
||||
}),
|
||||
blurhash: z
|
||||
.string()
|
||||
.nullable()
|
||||
.openapi({
|
||||
description:
|
||||
"A hash computed by the BlurHash algorithm, for generating colorful preview thumbnails when media has not been downloaded yet.",
|
||||
example: "UvK0HNkV,:s9xBR%njog0fo2W=WBS5ozofV@",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/PreviewCard/#blurhash",
|
||||
},
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
description:
|
||||
"Represents a rich preview card that is generated using OpenGraph tags from a URL.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/PreviewCard",
|
||||
},
|
||||
});
|
||||
11
packages/client/schemas/common.ts
Normal file
11
packages/client/schemas/common.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { z } from "@hono/zod-openapi";
|
||||
import ISO6391 from "iso-639-1";
|
||||
|
||||
export const Id = z.string().uuid();
|
||||
|
||||
export const iso631 = z.enum(ISO6391.getAllCodes() as [string, ...string[]]);
|
||||
|
||||
export const zBoolean = z
|
||||
.string()
|
||||
.transform((v) => ["true", "1", "on"].includes(v.toLowerCase()))
|
||||
.or(z.boolean());
|
||||
25
packages/client/schemas/context.ts
Normal file
25
packages/client/schemas/context.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { z } from "@hono/zod-openapi";
|
||||
import { Status } from "./status.ts";
|
||||
|
||||
export const Context = z
|
||||
.object({
|
||||
ancestors: z.array(Status).openapi({
|
||||
description: "Parents in the thread.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Context/#ancestors",
|
||||
},
|
||||
}),
|
||||
descendants: z.array(Status).openapi({
|
||||
description: "Children in the thread.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Context/#descendants",
|
||||
},
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
description:
|
||||
"Represents the tree around a given status. Used for reconstructing threads of statuses.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Context/#context",
|
||||
},
|
||||
});
|
||||
95
packages/client/schemas/emoji.ts
Normal file
95
packages/client/schemas/emoji.ts
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
import { emojiValidator } from "@/api.ts";
|
||||
import { z } from "@hono/zod-openapi";
|
||||
import { config } from "~/config.ts";
|
||||
import { Id, zBoolean } from "./common.ts";
|
||||
|
||||
export const CustomEmoji = z
|
||||
.object({
|
||||
/* Versia Server API extension */
|
||||
id: Id.openapi({
|
||||
description: "ID of the custom emoji in the database.",
|
||||
example: "af9ccd29-c689-477f-aa27-d7d95fd8fb05",
|
||||
}),
|
||||
shortcode: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(config.validation.emojis.max_shortcode_characters)
|
||||
.regex(
|
||||
emojiValidator,
|
||||
"Shortcode must only contain letters (any case), numbers, dashes or underscores.",
|
||||
)
|
||||
.openapi({
|
||||
description: "The name of the custom emoji.",
|
||||
example: "blobaww",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/CustomEmoji/#shortcode",
|
||||
},
|
||||
}),
|
||||
url: z
|
||||
.string()
|
||||
.url()
|
||||
.openapi({
|
||||
description: "A link to the custom emoji.",
|
||||
example:
|
||||
"https://cdn.versia.social/emojis/images/000/011/739/original/blobaww.png",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/CustomEmoji/#url",
|
||||
},
|
||||
}),
|
||||
static_url: z
|
||||
.string()
|
||||
.url()
|
||||
.openapi({
|
||||
description: "A link to a static copy of the custom emoji.",
|
||||
example:
|
||||
"https://cdn.versia.social/emojis/images/000/011/739/static/blobaww.png",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/CustomEmoji/#static_url",
|
||||
},
|
||||
}),
|
||||
visible_in_picker: z.boolean().openapi({
|
||||
description:
|
||||
"Whether this Emoji should be visible in the picker or unlisted.",
|
||||
example: true,
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/CustomEmoji/#visible_in_picker",
|
||||
},
|
||||
}),
|
||||
category: z
|
||||
.string()
|
||||
.trim()
|
||||
.max(64)
|
||||
.nullable()
|
||||
.openapi({
|
||||
description: "Used for sorting custom emoji in the picker.",
|
||||
example: "Blobs",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/CustomEmoji/#category",
|
||||
},
|
||||
}),
|
||||
/* Versia Server API extension */
|
||||
global: zBoolean.openapi({
|
||||
description: "Whether this emoji is visible to all users.",
|
||||
example: false,
|
||||
}),
|
||||
/* Versia Server API extension */
|
||||
description: z
|
||||
.string()
|
||||
.max(config.validation.emojis.max_description_characters)
|
||||
.nullable()
|
||||
.openapi({
|
||||
description:
|
||||
"Emoji description for users using screen readers.",
|
||||
example: "A cute blob.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/CustomEmoji/#description",
|
||||
},
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
description: "Represents a custom emoji.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/CustomEmoji",
|
||||
},
|
||||
});
|
||||
31
packages/client/schemas/extended-description.ts
Normal file
31
packages/client/schemas/extended-description.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { z } from "@hono/zod-openapi";
|
||||
|
||||
export const ExtendedDescription = z
|
||||
.object({
|
||||
updated_at: z
|
||||
.string()
|
||||
.datetime()
|
||||
.openapi({
|
||||
description:
|
||||
"A timestamp of when the extended description was last updated.",
|
||||
example: "2025-01-12T13:11:00Z",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/ExtendedDescription/#updated_at",
|
||||
},
|
||||
}),
|
||||
content: z.string().openapi({
|
||||
description:
|
||||
"The rendered HTML content of the extended description.",
|
||||
example: "<p>We love casting spells.</p>",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/ExtendedDescription/#content",
|
||||
},
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
description:
|
||||
"Represents an extended description for the instance, to be shown on its about page.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/ExtendedDescription",
|
||||
},
|
||||
});
|
||||
26
packages/client/schemas/familiar-followers.ts
Normal file
26
packages/client/schemas/familiar-followers.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { z } from "@hono/zod-openapi";
|
||||
import { Account } from "./account.ts";
|
||||
|
||||
export const FamiliarFollowers = z
|
||||
.object({
|
||||
id: Account.shape.id.openapi({
|
||||
description: "The ID of the Account in the database.",
|
||||
example: "48214efb-1f3c-459a-abfa-618a5aeb2f7a",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/FamiliarFollowers/#id",
|
||||
},
|
||||
}),
|
||||
accounts: z.array(Account).openapi({
|
||||
description: "Accounts you follow that also follow this account.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/FamiliarFollowers/#accounts",
|
||||
},
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
description:
|
||||
"Represents a subset of your follows who also follow some other user.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/FamiliarFollowers",
|
||||
},
|
||||
});
|
||||
180
packages/client/schemas/filters.ts
Normal file
180
packages/client/schemas/filters.ts
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
import { z } from "@hono/zod-openapi";
|
||||
import { Id, zBoolean } from "./common.ts";
|
||||
|
||||
export const FilterStatus = z
|
||||
.object({
|
||||
id: Id.openapi({
|
||||
description: "The ID of the FilterStatus in the database.",
|
||||
example: "3b19ed7c-0c4b-45e1-8c75-e21dfc8e86c3",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/FilterStatus/#id",
|
||||
},
|
||||
}),
|
||||
status_id: Id.openapi({
|
||||
description: "The ID of the Status that will be filtered.",
|
||||
example: "4f941ac8-295c-4c2d-9300-82c162ac8028",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/FilterStatus/#status_id",
|
||||
},
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
description:
|
||||
"Represents a status ID that, if matched, should cause the filter action to be taken.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/FilterStatus",
|
||||
},
|
||||
});
|
||||
|
||||
export const FilterKeyword = z
|
||||
.object({
|
||||
id: Id.openapi({
|
||||
description: "The ID of the FilterKeyword in the database.",
|
||||
example: "ca921e60-5b96-4686-90f3-d7cc420d7391",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/FilterKeyword/#id",
|
||||
},
|
||||
}),
|
||||
keyword: z.string().openapi({
|
||||
description: "The phrase to be matched against.",
|
||||
example: "badword",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/FilterKeyword/#keyword",
|
||||
},
|
||||
}),
|
||||
whole_word: zBoolean.openapi({
|
||||
description:
|
||||
"Should the filter consider word boundaries? See implementation guidelines for filters.",
|
||||
example: false,
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/FilterKeyword/#whole_word",
|
||||
},
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
description:
|
||||
"Represents a keyword that, if matched, should cause the filter action to be taken.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/FilterKeyword",
|
||||
},
|
||||
});
|
||||
|
||||
export const Filter = z
|
||||
.object({
|
||||
id: Id.openapi({
|
||||
description: "The ID of the Filter in the database.",
|
||||
example: "6b8fa22f-b128-43c2-9a1f-3c0499ef3a51",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Filter/#id",
|
||||
},
|
||||
}),
|
||||
title: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(255)
|
||||
.openapi({
|
||||
description: "A title given by the user to name the filter.",
|
||||
example: "Test filter",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Filter/#title",
|
||||
},
|
||||
}),
|
||||
context: z
|
||||
.array(
|
||||
z.enum([
|
||||
"home",
|
||||
"notifications",
|
||||
"public",
|
||||
"thread",
|
||||
"account",
|
||||
]),
|
||||
)
|
||||
.default([])
|
||||
.openapi({
|
||||
description:
|
||||
"The contexts in which the filter should be applied.",
|
||||
example: ["home"],
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Filter/#context",
|
||||
},
|
||||
}),
|
||||
expires_at: z
|
||||
.string()
|
||||
.nullable()
|
||||
.openapi({
|
||||
description: "When the filter should no longer be applied.",
|
||||
example: "2026-09-20T17:27:39.296Z",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Filter/#expires_at",
|
||||
},
|
||||
}),
|
||||
filter_action: z
|
||||
.enum(["warn", "hide"])
|
||||
.default("warn")
|
||||
.openapi({
|
||||
description:
|
||||
"The action to be taken when a status matches this filter.",
|
||||
example: "warn",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Filter/#filter_action",
|
||||
},
|
||||
}),
|
||||
keywords: z.array(FilterKeyword).openapi({
|
||||
description: "The keywords grouped under this filter.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Filter/#keywords",
|
||||
},
|
||||
}),
|
||||
statuses: z.array(FilterStatus).openapi({
|
||||
description: "The statuses grouped under this filter.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Filter/#statuses",
|
||||
},
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
description:
|
||||
"Represents a user-defined filter for determining which statuses should not be shown to the user.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Filter",
|
||||
},
|
||||
});
|
||||
|
||||
export const FilterResult = z
|
||||
.object({
|
||||
filter: Filter.openapi({
|
||||
description: "The filter that was matched.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/FilterResult/#filter",
|
||||
},
|
||||
}),
|
||||
keyword_matches: z
|
||||
.array(z.string())
|
||||
.nullable()
|
||||
.openapi({
|
||||
description: "The keyword within the filter that was matched.",
|
||||
example: ["badword"],
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/FilterResult/#keyword_matches",
|
||||
},
|
||||
}),
|
||||
status_matches: z
|
||||
.array(Id)
|
||||
.nullable()
|
||||
.openapi({
|
||||
description:
|
||||
"The status ID within the filter that was matched.",
|
||||
example: ["3819515a-5ceb-4078-8524-c939e38dcf8f"],
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/FilterResult/#status_matches",
|
||||
},
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
description:
|
||||
"Represents a filter whose keywords matched a given status.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/FilterResult",
|
||||
},
|
||||
});
|
||||
141
packages/client/schemas/instance-v1.ts
Normal file
141
packages/client/schemas/instance-v1.ts
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
import { z } from "@hono/zod-openapi";
|
||||
import { Instance } from "./instance.ts";
|
||||
import { SSOConfig } from "./versia.ts";
|
||||
|
||||
export const InstanceV1 = z
|
||||
.object({
|
||||
uri: Instance.shape.domain,
|
||||
title: Instance.shape.title,
|
||||
short_description: Instance.shape.description,
|
||||
description: z.string().openapi({
|
||||
description: "An HTML-permitted description of the site.",
|
||||
example: "<p>Join the world's smallest social network.</p>",
|
||||
}),
|
||||
email: Instance.shape.contact.shape.email,
|
||||
version: Instance.shape.version,
|
||||
/* Versia Server API extension */
|
||||
versia_version: Instance.shape.versia_version,
|
||||
urls: z
|
||||
.object({
|
||||
streaming_api:
|
||||
Instance.shape.configuration.shape.urls.shape.streaming,
|
||||
})
|
||||
.openapi({
|
||||
description: "URLs of interest for clients apps.",
|
||||
}),
|
||||
stats: z
|
||||
.object({
|
||||
user_count: z.number().openapi({
|
||||
description: "Total users on this instance.",
|
||||
example: 812303,
|
||||
}),
|
||||
status_count: z.number().openapi({
|
||||
description: "Total statuses on this instance.",
|
||||
example: 38151616,
|
||||
}),
|
||||
domain_count: z.number().openapi({
|
||||
description: "Total domains discovered by this instance.",
|
||||
example: 25255,
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
description:
|
||||
"Statistics about how much information the instance contains.",
|
||||
}),
|
||||
thumbnail: z.string().url().nullable().openapi({
|
||||
description: "Banner image for the website.",
|
||||
example:
|
||||
"https://files.mastodon.social/site_uploads/files/000/000/001/original/vlcsnap-2018-08-27-16h43m11s127.png",
|
||||
}),
|
||||
languages: Instance.shape.languages,
|
||||
registrations: Instance.shape.registrations.shape.enabled,
|
||||
approval_required: Instance.shape.registrations.shape.approval_required,
|
||||
invites_enabled: z.boolean().openapi({
|
||||
description: "Whether invites are enabled.",
|
||||
example: true,
|
||||
}),
|
||||
configuration: z
|
||||
.object({
|
||||
accounts: z
|
||||
.object({
|
||||
max_featured_tags:
|
||||
Instance.shape.configuration.shape.accounts.shape
|
||||
.max_featured_tags,
|
||||
})
|
||||
.openapi({
|
||||
description: "Limits related to accounts.",
|
||||
}),
|
||||
statuses: z
|
||||
.object({
|
||||
max_characters:
|
||||
Instance.shape.configuration.shape.statuses.shape
|
||||
.max_characters,
|
||||
max_media_attachments:
|
||||
Instance.shape.configuration.shape.statuses.shape
|
||||
.max_media_attachments,
|
||||
characters_reserved_per_url:
|
||||
Instance.shape.configuration.shape.statuses.shape
|
||||
.characters_reserved_per_url,
|
||||
})
|
||||
.openapi({
|
||||
description: "Limits related to authoring statuses.",
|
||||
}),
|
||||
media_attachments: z
|
||||
.object({
|
||||
supported_mime_types:
|
||||
Instance.shape.configuration.shape.media_attachments
|
||||
.shape.supported_mime_types,
|
||||
image_size_limit:
|
||||
Instance.shape.configuration.shape.media_attachments
|
||||
.shape.image_size_limit,
|
||||
image_matrix_limit:
|
||||
Instance.shape.configuration.shape.media_attachments
|
||||
.shape.image_matrix_limit,
|
||||
video_size_limit:
|
||||
Instance.shape.configuration.shape.media_attachments
|
||||
.shape.video_size_limit,
|
||||
video_frame_rate_limit:
|
||||
Instance.shape.configuration.shape.media_attachments
|
||||
.shape.video_frame_rate_limit,
|
||||
video_matrix_limit:
|
||||
Instance.shape.configuration.shape.media_attachments
|
||||
.shape.video_matrix_limit,
|
||||
})
|
||||
.openapi({
|
||||
description:
|
||||
"Hints for which attachments will be accepted.",
|
||||
}),
|
||||
polls: z
|
||||
.object({
|
||||
max_options:
|
||||
Instance.shape.configuration.shape.polls.shape
|
||||
.max_options,
|
||||
max_characters_per_option:
|
||||
Instance.shape.configuration.shape.polls.shape
|
||||
.max_characters_per_option,
|
||||
min_expiration:
|
||||
Instance.shape.configuration.shape.polls.shape
|
||||
.min_expiration,
|
||||
max_expiration:
|
||||
Instance.shape.configuration.shape.polls.shape
|
||||
.max_expiration,
|
||||
})
|
||||
.openapi({
|
||||
description: "Limits related to polls.",
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
description: "Configured values and limits for this website.",
|
||||
}),
|
||||
contact_account: Instance.shape.contact.shape.account,
|
||||
rules: Instance.shape.rules,
|
||||
/* Versia Server API extension */
|
||||
sso: SSOConfig,
|
||||
})
|
||||
.openapi({
|
||||
description:
|
||||
"Represents the software instance of Versia Server running on this domain.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/V1_Instance",
|
||||
},
|
||||
});
|
||||
382
packages/client/schemas/instance.ts
Normal file
382
packages/client/schemas/instance.ts
Normal file
|
|
@ -0,0 +1,382 @@
|
|||
import { z } from "@hono/zod-openapi";
|
||||
import pkg from "~/package.json";
|
||||
import { Account } from "./account.ts";
|
||||
import { iso631 } from "./common.ts";
|
||||
import { Rule } from "./rule.ts";
|
||||
import { SSOConfig } from "./versia.ts";
|
||||
|
||||
const InstanceIcon = z
|
||||
.object({
|
||||
src: z.string().url().openapi({
|
||||
description: "The URL of this icon.",
|
||||
example:
|
||||
"https://files.mastodon.social/site_uploads/files/000/000/003/36/accf17b0104f18e5.png",
|
||||
}),
|
||||
size: z.string().openapi({
|
||||
description:
|
||||
"The size of this icon (in the form of 12x34, where 12 is the width and 34 is the height of the icon).",
|
||||
example: "36x36",
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/InstanceIcon",
|
||||
},
|
||||
});
|
||||
|
||||
export const Instance = z
|
||||
.object({
|
||||
domain: z.string().openapi({
|
||||
description: "The domain name of the instance.",
|
||||
example: "versia.social",
|
||||
}),
|
||||
title: z.string().openapi({
|
||||
description: "The title of the website.",
|
||||
example: "Versia Social • Now with 100% more blobs!",
|
||||
}),
|
||||
version: z.string().openapi({
|
||||
description:
|
||||
"Mastodon version that the API is compatible with. Used for compatibility with Mastodon clients.",
|
||||
example: "4.3.0+glitch",
|
||||
}),
|
||||
/* Versia Server API extension */
|
||||
versia_version: z.string().openapi({
|
||||
description: "Versia Server version.",
|
||||
example: "0.8.0",
|
||||
}),
|
||||
source_url: z.string().url().openapi({
|
||||
description:
|
||||
"The URL for the source code of the software running on this instance, in keeping with AGPL license requirements.",
|
||||
example: pkg.repository.url,
|
||||
}),
|
||||
description: z.string().openapi({
|
||||
description:
|
||||
"A short, plain-text description defined by the admin.",
|
||||
example: "The flagship Versia Server instance. Join for free hugs!",
|
||||
}),
|
||||
usage: z
|
||||
.object({
|
||||
users: z
|
||||
.object({
|
||||
active_month: z.number().openapi({
|
||||
description:
|
||||
"The number of active users in the past 4 weeks.",
|
||||
example: 1_261,
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
description:
|
||||
"Usage data related to users on this instance.",
|
||||
}),
|
||||
})
|
||||
.openapi({ description: "Usage data for this instance." }),
|
||||
thumbnail: z
|
||||
.object({
|
||||
url: z.string().url().openapi({
|
||||
description: "The URL for the thumbnail image.",
|
||||
example:
|
||||
"https://files.mastodon.social/site_uploads/files/000/000/001/@1x/57c12f441d083cde.png",
|
||||
}),
|
||||
blurhash: z.string().optional().openapi({
|
||||
description:
|
||||
"A hash computed by the BlurHash algorithm, for generating colorful preview thumbnails when media has not been downloaded yet.",
|
||||
example: "UUKJMXv|x]t7^*t7Rjaz^jazRjaz",
|
||||
}),
|
||||
versions: z
|
||||
.object({
|
||||
"@1x": z.string().url().optional().openapi({
|
||||
description:
|
||||
"The URL for the thumbnail image at 1x resolution.",
|
||||
}),
|
||||
"@2x": z.string().url().optional().openapi({
|
||||
description:
|
||||
"The URL for the thumbnail image at 2x resolution.",
|
||||
}),
|
||||
})
|
||||
.optional()
|
||||
.openapi({
|
||||
description:
|
||||
"Links to scaled resolution images, for high DPI screens.",
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
description: "An image used to represent this instance.",
|
||||
}),
|
||||
icon: z.array(InstanceIcon).openapi({
|
||||
description:
|
||||
"The list of available size variants for this instance configured icon.",
|
||||
}),
|
||||
languages: z.array(iso631).openapi({
|
||||
description: "Primary languages of the website and its staff.",
|
||||
example: ["en"],
|
||||
}),
|
||||
configuration: z
|
||||
.object({
|
||||
urls: z
|
||||
.object({
|
||||
streaming: z.string().url().openapi({
|
||||
description:
|
||||
"The Websockets URL for connecting to the streaming API.",
|
||||
example: "wss://versia.social",
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
description: "URLs of interest for clients apps.",
|
||||
}),
|
||||
vapid: z
|
||||
.object({
|
||||
public_key: z.string().openapi({
|
||||
description:
|
||||
"The instance's VAPID public key, used for push notifications, the same as WebPushSubscription#server_key.",
|
||||
example:
|
||||
"BCkMmVdKDnKYwzVCDC99Iuc9GvId-x7-kKtuHnLgfF98ENiZp_aj-UNthbCdI70DqN1zUVis-x0Wrot2sBagkMc=",
|
||||
}),
|
||||
})
|
||||
.openapi({ description: "VAPID configuration." }),
|
||||
accounts: z
|
||||
.object({
|
||||
max_featured_tags: z.number().openapi({
|
||||
description:
|
||||
"The maximum number of featured tags allowed for each account.",
|
||||
example: 10,
|
||||
}),
|
||||
max_pinned_statuses: z.number().openapi({
|
||||
description:
|
||||
"The maximum number of pinned statuses for each account.",
|
||||
example: 4,
|
||||
}),
|
||||
/* Versia Server API extension */
|
||||
max_displayname_characters: z.number().openapi({
|
||||
description:
|
||||
"The maximum number of characters allowed in a display name.",
|
||||
example: 30,
|
||||
}),
|
||||
/* Versia Server API extension */
|
||||
max_username_characters: z.number().openapi({
|
||||
description:
|
||||
"The maximum number of characters allowed in a username.",
|
||||
example: 30,
|
||||
}),
|
||||
/* Versia Server API extension */
|
||||
max_note_characters: z.number().openapi({
|
||||
description:
|
||||
"The maximum number of characters allowed in an account's bio/note.",
|
||||
example: 500,
|
||||
}),
|
||||
/* Versia Server API extension */
|
||||
avatar_limit: z.number().openapi({
|
||||
description:
|
||||
"The maximum size of an avatar image, in bytes.",
|
||||
example: 1048576,
|
||||
}),
|
||||
/* Versia Server API extension */
|
||||
header_limit: z.number().openapi({
|
||||
description:
|
||||
"The maximum size of a header image, in bytes.",
|
||||
example: 2097152,
|
||||
}),
|
||||
/* Versia Server API extension */
|
||||
fields: z
|
||||
.object({
|
||||
max_fields: z.number().openapi({
|
||||
description:
|
||||
"The maximum number of fields allowed per account.",
|
||||
example: 4,
|
||||
}),
|
||||
max_name_characters: z.number().openapi({
|
||||
description:
|
||||
"The maximum number of characters allowed in a field name.",
|
||||
example: 30,
|
||||
}),
|
||||
max_value_characters: z.number().openapi({
|
||||
description:
|
||||
"The maximum number of characters allowed in a field value.",
|
||||
example: 100,
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
description:
|
||||
"Limits related to account fields.",
|
||||
}),
|
||||
})
|
||||
.openapi({ description: "Limits related to accounts." }),
|
||||
statuses: z
|
||||
.object({
|
||||
max_characters: z.number().openapi({
|
||||
description:
|
||||
"The maximum number of allowed characters per status.",
|
||||
example: 500,
|
||||
}),
|
||||
max_media_attachments: z.number().openapi({
|
||||
description:
|
||||
"The maximum number of media attachments that can be added to a status.",
|
||||
example: 4,
|
||||
}),
|
||||
characters_reserved_per_url: z.number().openapi({
|
||||
description:
|
||||
"Each URL in a status will be assumed to be exactly this many characters.",
|
||||
example: 23,
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
description: "Limits related to authoring statuses.",
|
||||
}),
|
||||
media_attachments: z
|
||||
.object({
|
||||
supported_mime_types: z.array(z.string()).openapi({
|
||||
description:
|
||||
"Contains MIME types that can be uploaded.",
|
||||
example: ["image/jpeg", "image/png", "image/gif"],
|
||||
}),
|
||||
description_limit: z.number().openapi({
|
||||
description:
|
||||
"The maximum size of a description, in characters.",
|
||||
example: 1500,
|
||||
}),
|
||||
image_size_limit: z.number().openapi({
|
||||
description:
|
||||
"The maximum size of any uploaded image, in bytes.",
|
||||
example: 10485760,
|
||||
}),
|
||||
image_matrix_limit: z.number().openapi({
|
||||
description:
|
||||
"The maximum number of pixels (width times height) for image uploads.",
|
||||
example: 16777216,
|
||||
}),
|
||||
video_size_limit: z.number().openapi({
|
||||
description:
|
||||
"The maximum size of any uploaded video, in bytes.",
|
||||
example: 41943040,
|
||||
}),
|
||||
video_frame_rate_limit: z.number().openapi({
|
||||
description:
|
||||
"The maximum frame rate for any uploaded video.",
|
||||
example: 60,
|
||||
}),
|
||||
video_matrix_limit: z.number().openapi({
|
||||
description:
|
||||
"The maximum number of pixels (width times height) for video uploads.",
|
||||
example: 2304000,
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
description:
|
||||
"Hints for which attachments will be accepted.",
|
||||
}),
|
||||
/* Versia Server API extension */
|
||||
emojis: z
|
||||
.object({
|
||||
emoji_size_limit: z.number().openapi({
|
||||
description:
|
||||
"The maximum size of an emoji image, in bytes.",
|
||||
example: 1048576,
|
||||
}),
|
||||
max_shortcode_characters: z.number().openapi({
|
||||
description:
|
||||
"The maximum number of characters allowed in an emoji shortcode.",
|
||||
example: 30,
|
||||
}),
|
||||
max_description_characters: z.number().openapi({
|
||||
description:
|
||||
"The maximum number of characters allowed in an emoji description.",
|
||||
example: 100,
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
description: "Limits related to custom emojis.",
|
||||
}),
|
||||
polls: z
|
||||
.object({
|
||||
max_options: z.number().openapi({
|
||||
description:
|
||||
"Each poll is allowed to have up to this many options.",
|
||||
example: 4,
|
||||
}),
|
||||
max_characters_per_option: z.number().openapi({
|
||||
description:
|
||||
"Each poll option is allowed to have this many characters.",
|
||||
example: 50,
|
||||
}),
|
||||
min_expiration: z.number().openapi({
|
||||
description:
|
||||
"The shortest allowed poll duration, in seconds.",
|
||||
example: 300,
|
||||
}),
|
||||
max_expiration: z.number().openapi({
|
||||
description:
|
||||
"The longest allowed poll duration, in seconds.",
|
||||
example: 2629746,
|
||||
}),
|
||||
})
|
||||
.openapi({ description: "Limits related to polls." }),
|
||||
translation: z
|
||||
.object({
|
||||
enabled: z.boolean().openapi({
|
||||
description:
|
||||
"Whether the Translations API is available on this instance.",
|
||||
example: true,
|
||||
}),
|
||||
})
|
||||
.openapi({ description: "Hints related to translation." }),
|
||||
})
|
||||
.openapi({
|
||||
description: "Configured values and limits for this website.",
|
||||
}),
|
||||
registrations: z
|
||||
.object({
|
||||
enabled: z.boolean().openapi({
|
||||
description: "Whether registrations are enabled.",
|
||||
example: false,
|
||||
}),
|
||||
approval_required: z.boolean().openapi({
|
||||
description:
|
||||
"Whether registrations require moderator approval.",
|
||||
example: false,
|
||||
}),
|
||||
message: z.string().nullable().openapi({
|
||||
description:
|
||||
"A custom message to be shown when registrations are closed.",
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
description: "Information about registering for this website.",
|
||||
}),
|
||||
api_versions: z
|
||||
.object({
|
||||
mastodon: z.number().openapi({
|
||||
description:
|
||||
"API version number that this server implements.",
|
||||
example: 1,
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
description:
|
||||
"Information about which version of the API is implemented by this server.",
|
||||
}),
|
||||
contact: z
|
||||
.object({
|
||||
email: z.string().email().openapi({
|
||||
description:
|
||||
"An email address that can be messaged regarding inquiries or issues.",
|
||||
example: "contact@versia.social",
|
||||
}),
|
||||
account: Account.nullable().openapi({
|
||||
description:
|
||||
"An account that can be contacted regarding inquiries or issues.",
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
description:
|
||||
"Hints related to contacting a representative of the website.",
|
||||
}),
|
||||
rules: z.array(Rule).openapi({
|
||||
description: "An itemized list of rules for this website.",
|
||||
}),
|
||||
/* Versia Server API extension */
|
||||
sso: SSOConfig,
|
||||
})
|
||||
.openapi({
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Instance",
|
||||
},
|
||||
});
|
||||
26
packages/client/schemas/marker.ts
Normal file
26
packages/client/schemas/marker.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { z } from "@hono/zod-openapi";
|
||||
import { Id } from "./common.ts";
|
||||
|
||||
export const Marker = z
|
||||
.object({
|
||||
last_read_id: Id.openapi({
|
||||
description: "The ID of the most recently viewed entity.",
|
||||
example: "ead15c9d-8eda-4b2c-9546-ecbf851f001c",
|
||||
}),
|
||||
version: z.number().openapi({
|
||||
description:
|
||||
"An incrementing counter, used for locking to prevent write conflicts.",
|
||||
example: 462,
|
||||
}),
|
||||
updated_at: z.string().datetime().openapi({
|
||||
description: "The timestamp of when the marker was set.",
|
||||
example: "2025-01-12T13:11:00Z",
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
description:
|
||||
"Represents the last read position within a user's timelines.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Marker",
|
||||
},
|
||||
});
|
||||
70
packages/client/schemas/notification.ts
Normal file
70
packages/client/schemas/notification.ts
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import { z } from "@hono/zod-openapi";
|
||||
import { AccountWarning } from "./account-warning.ts";
|
||||
import { Account } from "./account.ts";
|
||||
import { Id } from "./common.ts";
|
||||
import { Report } from "./report.ts";
|
||||
import { Status } from "./status.ts";
|
||||
|
||||
export const Notification = z
|
||||
.object({
|
||||
id: Id.openapi({
|
||||
description: "The ID of the notification in the database.",
|
||||
example: "6405f495-da55-4ad7-b5d6-9a773360fc07",
|
||||
}),
|
||||
type: z
|
||||
.enum([
|
||||
"mention",
|
||||
"status",
|
||||
"reblog",
|
||||
"follow",
|
||||
"follow_request",
|
||||
"favourite",
|
||||
"poll",
|
||||
"update",
|
||||
"admin.sign_up",
|
||||
"admin.report",
|
||||
"severed_relationships",
|
||||
"moderation_warning",
|
||||
])
|
||||
.openapi({
|
||||
description:
|
||||
"The type of event that resulted in the notification.",
|
||||
example: "mention",
|
||||
}),
|
||||
group_key: z.string().openapi({
|
||||
description:
|
||||
"Group key shared by similar notifications, to be used in the grouped notifications feature.",
|
||||
example: "ungrouped-34975861",
|
||||
}),
|
||||
created_at: z.string().datetime().openapi({
|
||||
description: "The timestamp of the notification.",
|
||||
example: "2025-01-12T13:11:00Z",
|
||||
}),
|
||||
account: Account.openapi({
|
||||
description:
|
||||
"The account that performed the action that generated the notification.",
|
||||
}),
|
||||
status: Status.optional().openapi({
|
||||
description:
|
||||
"Status that was the object of the notification. Attached when type of the notification is favourite, reblog, status, mention, poll, or update.",
|
||||
}),
|
||||
report: Report.optional().openapi({
|
||||
description:
|
||||
"Report that was the object of the notification. Attached when type of the notification is admin.report.",
|
||||
}),
|
||||
event: z.undefined().openapi({
|
||||
description:
|
||||
"Versia Server does not sever relationships, so this field is always empty.",
|
||||
}),
|
||||
moderation_warning: AccountWarning.optional().openapi({
|
||||
description:
|
||||
"Moderation warning that caused the notification. Attached when type of the notification is moderation_warning.",
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
description:
|
||||
"Represents a notification of an event relevant to the user.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Notification",
|
||||
},
|
||||
});
|
||||
138
packages/client/schemas/poll.ts
Normal file
138
packages/client/schemas/poll.ts
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
import { z } from "@hono/zod-openapi";
|
||||
import { config } from "~/config.ts";
|
||||
import { Id } from "./common.ts";
|
||||
import { CustomEmoji } from "./emoji.ts";
|
||||
|
||||
export const PollOption = z
|
||||
.object({
|
||||
title: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(config.validation.polls.max_option_characters)
|
||||
.openapi({
|
||||
description: "The text value of the poll option.",
|
||||
example: "yes",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Poll/#Option-title",
|
||||
},
|
||||
}),
|
||||
votes_count: z
|
||||
.number()
|
||||
.int()
|
||||
.nonnegative()
|
||||
.nullable()
|
||||
.openapi({
|
||||
description:
|
||||
"The total number of received votes for this option.",
|
||||
example: 6,
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Poll/#Option-votes_count",
|
||||
},
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Poll/#Option",
|
||||
},
|
||||
});
|
||||
|
||||
export const Poll = z
|
||||
.object({
|
||||
id: Id.openapi({
|
||||
description: "ID of the poll in the database.",
|
||||
example: "d87d230f-e401-4282-80c7-2044ab989662",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Poll/#id",
|
||||
},
|
||||
}),
|
||||
expires_at: z
|
||||
.string()
|
||||
.datetime()
|
||||
.nullable()
|
||||
.openapi({
|
||||
description: "When the poll ends.",
|
||||
example: "2025-01-07T14:11:00.000Z",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Poll/#expires_at",
|
||||
},
|
||||
}),
|
||||
expired: z.boolean().openapi({
|
||||
description: "Is the poll currently expired?",
|
||||
example: false,
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Poll/#expired",
|
||||
},
|
||||
}),
|
||||
multiple: z.boolean().openapi({
|
||||
description: "Does the poll allow multiple-choice answers?",
|
||||
example: false,
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Poll/#multiple",
|
||||
},
|
||||
}),
|
||||
votes_count: z
|
||||
.number()
|
||||
.int()
|
||||
.nonnegative()
|
||||
.openapi({
|
||||
description: "How many votes have been received.",
|
||||
example: 6,
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Poll/#votes_count",
|
||||
},
|
||||
}),
|
||||
voters_count: z
|
||||
.number()
|
||||
.int()
|
||||
.nonnegative()
|
||||
.nullable()
|
||||
.openapi({
|
||||
description:
|
||||
"How many unique accounts have voted on a multiple-choice poll.",
|
||||
example: 3,
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Poll/#voters_count",
|
||||
},
|
||||
}),
|
||||
options: z.array(PollOption).openapi({
|
||||
description: "Possible answers for the poll.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Poll/#options",
|
||||
},
|
||||
}),
|
||||
emojis: z.array(CustomEmoji).openapi({
|
||||
description: "Custom emoji to be used for rendering poll options.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Poll/#emojis",
|
||||
},
|
||||
}),
|
||||
voted: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.openapi({
|
||||
description:
|
||||
"When called with a user token, has the authorized user voted?",
|
||||
example: true,
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Poll/#voted",
|
||||
},
|
||||
}),
|
||||
own_votes: z
|
||||
.array(z.number().int())
|
||||
.optional()
|
||||
.openapi({
|
||||
description:
|
||||
"When called with a user token, which options has the authorized user chosen? Contains an array of index values for options.",
|
||||
example: [0],
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Poll/#own_votes",
|
||||
},
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
description: "Represents a poll attached to a status.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Poll",
|
||||
},
|
||||
});
|
||||
50
packages/client/schemas/preferences.ts
Normal file
50
packages/client/schemas/preferences.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import { z } from "@hono/zod-openapi";
|
||||
import { Source } from "./account.ts";
|
||||
|
||||
export const Preferences = z
|
||||
.object({
|
||||
"posting:default:visibility": Source.shape.privacy.openapi({
|
||||
description: "Default visibility for new posts.",
|
||||
example: "public",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Preferences/#posting-default-visibility",
|
||||
},
|
||||
}),
|
||||
"posting:default:sensitive": Source.shape.sensitive.openapi({
|
||||
description: "Default sensitivity flag for new posts.",
|
||||
example: false,
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Preferences/#posting-default-sensitive",
|
||||
},
|
||||
}),
|
||||
"posting:default:language": Source.shape.language.nullable().openapi({
|
||||
description: "Default language for new posts.",
|
||||
example: null,
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Preferences/#posting-default-language",
|
||||
},
|
||||
}),
|
||||
"reading:expand:media": z
|
||||
.enum(["default", "show_all", "hide_all"])
|
||||
.openapi({
|
||||
description:
|
||||
"Whether media attachments should be automatically displayed or blurred/hidden.",
|
||||
example: "default",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Preferences/#reading-expand-media",
|
||||
},
|
||||
}),
|
||||
"reading:expand:spoilers": z.boolean().openapi({
|
||||
description: "Whether CWs should be expanded by default.",
|
||||
example: false,
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Preferences/#reading-expand-spoilers",
|
||||
},
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
description: "Represents a user's preferences.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Preferences",
|
||||
},
|
||||
});
|
||||
29
packages/client/schemas/privacy-policy.ts
Normal file
29
packages/client/schemas/privacy-policy.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import { z } from "@hono/zod-openapi";
|
||||
|
||||
export const PrivacyPolicy = z
|
||||
.object({
|
||||
updated_at: z
|
||||
.string()
|
||||
.datetime()
|
||||
.openapi({
|
||||
description:
|
||||
"A timestamp of when the privacy policy was last updated.",
|
||||
example: "2025-01-12T13:11:00Z",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/PrivacyPolicy/#updated_at",
|
||||
},
|
||||
}),
|
||||
content: z.string().openapi({
|
||||
description: "The rendered HTML content of the privacy policy.",
|
||||
example: "<p><h1>Privacy Policy</h1><p>None, good luck.</p></p>",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/PrivacyPolicy/#content",
|
||||
},
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
description: "Represents the privacy policy of the instance.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/PrivacyPolicy",
|
||||
},
|
||||
});
|
||||
135
packages/client/schemas/pushsubscription.ts
Normal file
135
packages/client/schemas/pushsubscription.ts
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
import { z } from "@hono/zod-openapi";
|
||||
import { Id } from "./common.ts";
|
||||
|
||||
export const WebPushSubscription = z
|
||||
.object({
|
||||
id: Id.openapi({
|
||||
example: "24eb1891-accc-43b4-b213-478e37d525b4",
|
||||
description: "The ID of the Web Push subscription in the database.",
|
||||
}),
|
||||
endpoint: z.string().url().openapi({
|
||||
example: "https://yourdomain.example/listener",
|
||||
description: "Where push alerts will be sent to.",
|
||||
}),
|
||||
alerts: z
|
||||
.object({
|
||||
mention: z.boolean().optional().openapi({
|
||||
example: true,
|
||||
description: "Receive mention notifications?",
|
||||
}),
|
||||
favourite: z.boolean().optional().openapi({
|
||||
example: true,
|
||||
description: "Receive favourite notifications?",
|
||||
}),
|
||||
reblog: z.boolean().optional().openapi({
|
||||
example: true,
|
||||
description: "Receive reblog notifications?",
|
||||
}),
|
||||
follow: z.boolean().optional().openapi({
|
||||
example: true,
|
||||
description: "Receive follow notifications?",
|
||||
}),
|
||||
poll: z.boolean().optional().openapi({
|
||||
example: false,
|
||||
description: "Receive poll notifications?",
|
||||
}),
|
||||
follow_request: z.boolean().optional().openapi({
|
||||
example: false,
|
||||
description: "Receive follow request notifications?",
|
||||
}),
|
||||
status: z.boolean().optional().openapi({
|
||||
example: false,
|
||||
description:
|
||||
"Receive new subscribed account notifications?",
|
||||
}),
|
||||
update: z.boolean().optional().openapi({
|
||||
example: false,
|
||||
description: "Receive status edited notifications?",
|
||||
}),
|
||||
"admin.sign_up": z.boolean().optional().openapi({
|
||||
example: false,
|
||||
description:
|
||||
"Receive new user signup notifications? Must have a role with the appropriate permissions.",
|
||||
}),
|
||||
"admin.report": z.boolean().optional().openapi({
|
||||
example: false,
|
||||
description:
|
||||
"Receive new report notifications? Must have a role with the appropriate permissions.",
|
||||
}),
|
||||
})
|
||||
.default({})
|
||||
.openapi({
|
||||
example: {
|
||||
mention: true,
|
||||
favourite: true,
|
||||
reblog: true,
|
||||
follow: true,
|
||||
poll: false,
|
||||
follow_request: false,
|
||||
status: false,
|
||||
update: false,
|
||||
"admin.sign_up": false,
|
||||
"admin.report": false,
|
||||
},
|
||||
description:
|
||||
"Which alerts should be delivered to the endpoint.",
|
||||
}),
|
||||
server_key: z.string().openapi({
|
||||
example:
|
||||
"BCk-QqERU0q-CfYZjcuB6lnyyOYfJ2AifKqfeGIm7Z-HiTU5T9eTG5GxVA0_OH5mMlI4UkkDTpaZwozy0TzdZ2M=",
|
||||
description: "The streaming server’s VAPID key.",
|
||||
}),
|
||||
})
|
||||
.openapi({});
|
||||
|
||||
export const WebPushSubscriptionInput = z
|
||||
.object({
|
||||
subscription: z.object({
|
||||
endpoint: z.string().url().openapi({
|
||||
example: "https://yourdomain.example/listener",
|
||||
description: "Where push alerts will be sent to.",
|
||||
}),
|
||||
keys: z
|
||||
.object({
|
||||
p256dh: z.string().base64url().openapi({
|
||||
description:
|
||||
"User agent public key. Base64url encoded string of a public key from a ECDH keypair using the prime256v1 curve.",
|
||||
example:
|
||||
"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEoKCJeHCy69ywHcb3dAR/T8Sud5ljSFHJkuiR6it1ycqAjGTe5F1oZ0ef5QiMX/zdQ+d4jSKiO7RztIz+o/eGuQ==",
|
||||
}),
|
||||
auth: z.string().base64url().openapi({
|
||||
description:
|
||||
"Auth secret. Base64url encoded string of 16 bytes of random data.",
|
||||
example: "u67u09PXZW4ncK9l9mAXkA==",
|
||||
}),
|
||||
})
|
||||
.strict(),
|
||||
}),
|
||||
policy: z
|
||||
.enum(["all", "followed", "follower", "none"])
|
||||
.default("all")
|
||||
.openapi({
|
||||
description:
|
||||
"Specify whether to receive push notifications from all, followed, follower, or none users.",
|
||||
}),
|
||||
data: z
|
||||
.object({
|
||||
alerts: WebPushSubscription.shape.alerts,
|
||||
})
|
||||
.strict()
|
||||
.default({
|
||||
alerts: {
|
||||
mention: false,
|
||||
favourite: false,
|
||||
reblog: false,
|
||||
follow: false,
|
||||
poll: false,
|
||||
follow_request: false,
|
||||
status: false,
|
||||
update: false,
|
||||
"admin.sign_up": false,
|
||||
"admin.report": false,
|
||||
},
|
||||
}),
|
||||
})
|
||||
.strict();
|
||||
74
packages/client/schemas/relationship.ts
Normal file
74
packages/client/schemas/relationship.ts
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import { z } from "@hono/zod-openapi";
|
||||
import { Id, iso631 } from "./common.ts";
|
||||
|
||||
export const Relationship = z
|
||||
.object({
|
||||
id: Id.openapi({
|
||||
description: "The account ID.",
|
||||
example: "51f34c31-c8c6-4dc2-9df1-3704fcdde9b6",
|
||||
}),
|
||||
following: z.boolean().openapi({
|
||||
description: "Are you following this user?",
|
||||
example: true,
|
||||
}),
|
||||
showing_reblogs: z.boolean().openapi({
|
||||
description:
|
||||
"Are you receiving this user’s boosts in your home timeline?",
|
||||
example: true,
|
||||
}),
|
||||
notifying: z.boolean().openapi({
|
||||
description: "Have you enabled notifications for this user?",
|
||||
example: false,
|
||||
}),
|
||||
languages: z.array(iso631).openapi({
|
||||
description: "Which languages are you following from this user?",
|
||||
example: ["en"],
|
||||
}),
|
||||
followed_by: z.boolean().openapi({
|
||||
description: "Are you followed by this user?",
|
||||
example: true,
|
||||
}),
|
||||
blocking: z.boolean().openapi({
|
||||
description: "Are you blocking this user?",
|
||||
example: false,
|
||||
}),
|
||||
blocked_by: z.boolean().openapi({
|
||||
description: "Is this user blocking you?",
|
||||
example: false,
|
||||
}),
|
||||
muting: z.boolean().openapi({
|
||||
description: "Are you muting this user?",
|
||||
example: false,
|
||||
}),
|
||||
muting_notifications: z.boolean().openapi({
|
||||
description: "Are you muting notifications from this user?",
|
||||
example: false,
|
||||
}),
|
||||
requested: z.boolean().openapi({
|
||||
description: "Do you have a pending follow request for this user?",
|
||||
example: false,
|
||||
}),
|
||||
requested_by: z.boolean().openapi({
|
||||
description: "Has this user requested to follow you?",
|
||||
example: false,
|
||||
}),
|
||||
domain_blocking: z.boolean().openapi({
|
||||
description: "Are you blocking this user’s domain?",
|
||||
example: false,
|
||||
}),
|
||||
endorsed: z.boolean().openapi({
|
||||
description: "Are you featuring this user on your profile?",
|
||||
example: false,
|
||||
}),
|
||||
note: z.string().min(0).max(5000).trim().openapi({
|
||||
description: "This user’s profile bio",
|
||||
example: "they also like Kerbal Space Program",
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
description:
|
||||
"Represents the relationship between accounts, such as following / blocking / muting / etc.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Relationship",
|
||||
},
|
||||
});
|
||||
59
packages/client/schemas/report.ts
Normal file
59
packages/client/schemas/report.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import { z } from "@hono/zod-openapi";
|
||||
import { Account } from "./account.ts";
|
||||
import { Id } from "./common.ts";
|
||||
|
||||
export const Report = z
|
||||
.object({
|
||||
id: Id.openapi({
|
||||
description: "The ID of the report in the database.",
|
||||
example: "9b0cd757-324b-4ea6-beab-f6226e138886",
|
||||
}),
|
||||
action_taken: z.boolean().openapi({
|
||||
description: "Whether an action was taken yet.",
|
||||
example: false,
|
||||
}),
|
||||
action_taken_at: z.string().datetime().nullable().openapi({
|
||||
description: "When an action was taken against the report.",
|
||||
example: null,
|
||||
}),
|
||||
category: z.enum(["spam", "violation", "other"]).openapi({
|
||||
description:
|
||||
"The generic reason for the report. 'spam' = Unwanted or repetitive content, 'violation' = A specific rule was violated, 'other' = Some other reason.",
|
||||
example: "spam",
|
||||
}),
|
||||
comment: z.string().openapi({
|
||||
description: "The reason for the report.",
|
||||
example: "Spam account",
|
||||
}),
|
||||
forwarded: z.boolean().openapi({
|
||||
description: "Whether the report was forwarded to a remote domain.",
|
||||
example: false,
|
||||
}),
|
||||
created_at: z.string().datetime().openapi({
|
||||
description: "When the report was created.",
|
||||
example: "2024-12-31T23:59:59.999Z",
|
||||
}),
|
||||
status_ids: z
|
||||
.array(Id)
|
||||
.nullable()
|
||||
.openapi({
|
||||
description:
|
||||
"IDs of statuses that have been attached to this report for additional context.",
|
||||
example: ["1abf027c-af03-46ff-8d17-9ee799a17ca7"],
|
||||
}),
|
||||
rule_ids: z.array(z.string()).nullable().openapi({
|
||||
description:
|
||||
"IDs of the rules that have been cited as a violation by this report.",
|
||||
example: null,
|
||||
}),
|
||||
target_account: Account.openapi({
|
||||
description: "The account that was reported.",
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
description:
|
||||
"Reports filed against users and/or statuses, to be taken action on by moderators.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Report",
|
||||
},
|
||||
});
|
||||
23
packages/client/schemas/rule.ts
Normal file
23
packages/client/schemas/rule.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { z } from "@hono/zod-openapi";
|
||||
|
||||
export const Rule = z
|
||||
.object({
|
||||
id: z.string().openapi({
|
||||
description: "The identifier for the rule.",
|
||||
example: "1",
|
||||
}),
|
||||
text: z.string().openapi({
|
||||
description: "The rule to be followed.",
|
||||
example: "Do not spam pictures of skibidi toilet.",
|
||||
}),
|
||||
hint: z.string().optional().openapi({
|
||||
description: "Longer-form description of the rule.",
|
||||
example: "Please, we beg you.",
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
description: "Represents a rule that server users should follow.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Rule",
|
||||
},
|
||||
});
|
||||
23
packages/client/schemas/search.ts
Normal file
23
packages/client/schemas/search.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { z } from "@hono/zod-openapi";
|
||||
import { Account } from "./account.ts";
|
||||
import { Status } from "./status.ts";
|
||||
import { Tag } from "./tag.ts";
|
||||
|
||||
export const Search = z
|
||||
.object({
|
||||
accounts: z.array(Account).openapi({
|
||||
description: "Accounts which match the given query",
|
||||
}),
|
||||
statuses: z.array(Status).openapi({
|
||||
description: "Statuses which match the given query",
|
||||
}),
|
||||
hashtags: z.array(Tag).openapi({
|
||||
description: "Hashtags which match the given query",
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
description: "Represents the results of a search.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Search",
|
||||
},
|
||||
});
|
||||
366
packages/client/schemas/status.ts
Normal file
366
packages/client/schemas/status.ts
Normal file
|
|
@ -0,0 +1,366 @@
|
|||
import { z } from "@hono/zod-openapi";
|
||||
import type { Status as ApiNote } from "@versia/client/types";
|
||||
import { config } from "~/config.ts";
|
||||
import { Account } from "./account.ts";
|
||||
import { Attachment } from "./attachment.ts";
|
||||
import { PreviewCard } from "./card.ts";
|
||||
import { Id, iso631, zBoolean } from "./common.ts";
|
||||
import { CustomEmoji } from "./emoji.ts";
|
||||
import { FilterResult } from "./filters.ts";
|
||||
import { Poll } from "./poll.ts";
|
||||
import { Tag } from "./tag.ts";
|
||||
import { NoteReaction } from "./versia.ts";
|
||||
|
||||
export const Mention = z
|
||||
.object({
|
||||
id: Account.shape.id.openapi({
|
||||
description: "The account ID of the mentioned user.",
|
||||
example: "b9dcb548-bd4d-42af-8b48-3693e6d298e6",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Status/#Mention-id",
|
||||
},
|
||||
}),
|
||||
username: Account.shape.username.openapi({
|
||||
description: "The username of the mentioned user.",
|
||||
example: "lexi",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Status/#Mention-username",
|
||||
},
|
||||
}),
|
||||
url: Account.shape.url.openapi({
|
||||
description: "The location of the mentioned user’s profile.",
|
||||
example: "https://beta.versia.social/@lexi",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Status/#Mention-url",
|
||||
},
|
||||
}),
|
||||
acct: Account.shape.acct.openapi({
|
||||
description:
|
||||
"The webfinger acct: URI of the mentioned user. Equivalent to username for local users, or username@domain for remote users.",
|
||||
example: "lexi@beta.versia.social",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Status/#Mention-acct",
|
||||
},
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Status/#Mention",
|
||||
},
|
||||
});
|
||||
|
||||
export const StatusSource = z
|
||||
.object({
|
||||
id: Id.openapi({
|
||||
description: "ID of the status in the database.",
|
||||
example: "c7db92a4-e472-4e94-a115-7411ee934ba1",
|
||||
}),
|
||||
text: z
|
||||
.string()
|
||||
.max(config.validation.notes.max_characters)
|
||||
.trim()
|
||||
.refine(
|
||||
(s) =>
|
||||
!config.validation.filters.note_content.some((filter) =>
|
||||
filter.test(s),
|
||||
),
|
||||
"Status contains blocked words",
|
||||
)
|
||||
.openapi({
|
||||
description: "The plain text used to compose the status.",
|
||||
example: "this is a status that will be edited",
|
||||
}),
|
||||
spoiler_text: z.string().trim().min(1).max(1024).openapi({
|
||||
description:
|
||||
"The plain text used to compose the status’s subject or content warning.",
|
||||
example: "",
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/StatusSource",
|
||||
},
|
||||
});
|
||||
|
||||
export const Status = z.object({
|
||||
id: Id.openapi({
|
||||
description: "ID of the status in the database.",
|
||||
example: "2de861d3-a3dd-42ee-ba38-2c7d3f4af588",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Status/#id",
|
||||
},
|
||||
}),
|
||||
uri: z
|
||||
.string()
|
||||
.url()
|
||||
.openapi({
|
||||
description: "URI of the status used for federation.",
|
||||
example:
|
||||
"https://beta.versia.social/@lexi/2de861d3-a3dd-42ee-ba38-2c7d3f4af588",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Status/#uri",
|
||||
},
|
||||
}),
|
||||
url: z
|
||||
.string()
|
||||
.url()
|
||||
.nullable()
|
||||
.openapi({
|
||||
description: "A link to the status’s HTML representation.",
|
||||
example:
|
||||
"https://beta.versia.social/@lexi/2de861d3-a3dd-42ee-ba38-2c7d3f4af588",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Status/#url",
|
||||
},
|
||||
}),
|
||||
account: Account.openapi({
|
||||
description: "The account that authored this status.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Status/#account",
|
||||
},
|
||||
}),
|
||||
in_reply_to_id: Id.nullable().openapi({
|
||||
description: "ID of the status being replied to.",
|
||||
example: "c41c9fe9-919a-4d35-a921-d3e79a5c95f8",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Status/#in_reply_to_id",
|
||||
},
|
||||
}),
|
||||
in_reply_to_account_id: Account.shape.id.nullable().openapi({
|
||||
description:
|
||||
"ID of the account that authored the status being replied to.",
|
||||
example: "7b9b3ec6-1013-4cc6-8902-94ad00cf2ccc",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Status/#in_reply_to_account_id",
|
||||
},
|
||||
}),
|
||||
reblog: z
|
||||
// @ts-expect-error broken recursive types
|
||||
.lazy((): z.ZodType<ApiNote> => Status as z.ZodType<ApiNote>)
|
||||
.nullable()
|
||||
.openapi({
|
||||
description: "The status being reblogged.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Status/#reblog",
|
||||
},
|
||||
}),
|
||||
content: z.string().openapi({
|
||||
description: "HTML-encoded status content.",
|
||||
example: "<p>hello world</p>",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Status/#content",
|
||||
},
|
||||
}),
|
||||
created_at: z
|
||||
.string()
|
||||
.datetime()
|
||||
.openapi({
|
||||
description: "The date when this status was created.",
|
||||
example: "2025-01-07T14:11:00.000Z",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Status/#created_at",
|
||||
},
|
||||
}),
|
||||
edited_at: z
|
||||
.string()
|
||||
.datetime()
|
||||
.nullable()
|
||||
.openapi({
|
||||
description: "Timestamp of when the status was last edited.",
|
||||
example: "2025-01-07T14:11:00.000Z",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Status/#edited_at",
|
||||
},
|
||||
}),
|
||||
emojis: z.array(CustomEmoji).openapi({
|
||||
description: "Custom emoji to be used when rendering status content.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Status/#emojis",
|
||||
},
|
||||
}),
|
||||
replies_count: z
|
||||
.number()
|
||||
.int()
|
||||
.nonnegative()
|
||||
.openapi({
|
||||
description: "How many replies this status has received.",
|
||||
example: 1,
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Status/#replies_count",
|
||||
},
|
||||
}),
|
||||
reblogs_count: z
|
||||
.number()
|
||||
.int()
|
||||
.nonnegative()
|
||||
.openapi({
|
||||
description: "How many boosts this status has received.",
|
||||
example: 6,
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Status/#reblogs_count",
|
||||
},
|
||||
}),
|
||||
favourites_count: z
|
||||
.number()
|
||||
.int()
|
||||
.nonnegative()
|
||||
.openapi({
|
||||
description: "How many favourites this status has received.",
|
||||
example: 11,
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Status/#favourites_count",
|
||||
},
|
||||
}),
|
||||
reblogged: zBoolean.optional().openapi({
|
||||
description:
|
||||
"If the current token has an authorized user: Have you boosted this status?",
|
||||
example: false,
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Status/#reblogged",
|
||||
},
|
||||
}),
|
||||
favourited: zBoolean.optional().openapi({
|
||||
description:
|
||||
"If the current token has an authorized user: Have you favourited this status?",
|
||||
example: true,
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Status/#favourited",
|
||||
},
|
||||
}),
|
||||
muted: zBoolean.optional().openapi({
|
||||
description:
|
||||
"If the current token has an authorized user: Have you muted notifications for this status’s conversation?",
|
||||
example: false,
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Status/#muted",
|
||||
},
|
||||
}),
|
||||
sensitive: zBoolean.openapi({
|
||||
description: "Is this status marked as sensitive content?",
|
||||
example: false,
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Status/#sensitive",
|
||||
},
|
||||
}),
|
||||
spoiler_text: z.string().openapi({
|
||||
description:
|
||||
"Subject or summary line, below which status content is collapsed until expanded.",
|
||||
example: "lewd text",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Status/#spoiler_text",
|
||||
},
|
||||
}),
|
||||
visibility: z.enum(["public", "unlisted", "private", "direct"]).openapi({
|
||||
description: "Visibility of this status.",
|
||||
example: "public",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Status/#visibility",
|
||||
},
|
||||
}),
|
||||
media_attachments: z.array(Attachment).openapi({
|
||||
description: "Media that is attached to this status.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Status/#media_attachments",
|
||||
},
|
||||
}),
|
||||
mentions: z.array(Mention).openapi({
|
||||
description: "Mentions of users within the status content.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Status/#mentions",
|
||||
},
|
||||
}),
|
||||
tags: z.array(Tag).openapi({
|
||||
description: "Hashtags used within the status content.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Status/#tags",
|
||||
},
|
||||
}),
|
||||
card: PreviewCard.nullable().openapi({
|
||||
description: "Preview card for links included within status content.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Status/#card",
|
||||
},
|
||||
}),
|
||||
poll: Poll.nullable().openapi({
|
||||
description: "The poll attached to the status.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Status/#poll",
|
||||
},
|
||||
}),
|
||||
application: z
|
||||
.object({
|
||||
name: z.string().openapi({
|
||||
description:
|
||||
"The name of the application that posted this status.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Status/#application-name",
|
||||
},
|
||||
}),
|
||||
website: z
|
||||
.string()
|
||||
.url()
|
||||
.nullable()
|
||||
.openapi({
|
||||
description:
|
||||
"The website associated with the application that posted this status.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Status/#application-website",
|
||||
},
|
||||
}),
|
||||
})
|
||||
.optional()
|
||||
.openapi({
|
||||
description: "The application used to post this status.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Status/#application",
|
||||
},
|
||||
}),
|
||||
language: iso631.nullable().openapi({
|
||||
description: "Primary language of this status.",
|
||||
example: "en",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Status/#language",
|
||||
},
|
||||
}),
|
||||
text: z
|
||||
.string()
|
||||
.nullable()
|
||||
.openapi({
|
||||
description:
|
||||
"Plain-text source of a status. Returned instead of content when status is deleted, so the user may redraft from the source text without the client having to reverse-engineer the original text from the HTML content.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Status/#text",
|
||||
},
|
||||
}),
|
||||
pinned: zBoolean.optional().openapi({
|
||||
description:
|
||||
"If the current token has an authorized user: Have you pinned this status? Only appears if the status is pinnable.",
|
||||
example: true,
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Status/#pinned",
|
||||
},
|
||||
}),
|
||||
reactions: z.array(NoteReaction).openapi({}),
|
||||
quote: z
|
||||
// @ts-expect-error broken recursive types
|
||||
.lazy((): z.ZodType<ApiNote> => Status as z.ZodType<ApiNote>)
|
||||
.nullable(),
|
||||
bookmarked: zBoolean.optional().openapi({
|
||||
description:
|
||||
"If the current token has an authorized user: Have you bookmarked this status?",
|
||||
example: false,
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Status/#bookmarked",
|
||||
},
|
||||
}),
|
||||
filtered: z
|
||||
.array(FilterResult)
|
||||
.optional()
|
||||
.openapi({
|
||||
description:
|
||||
"If the current token has an authorized user: The filter and keywords that matched this status.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Status/#filtered",
|
||||
},
|
||||
}),
|
||||
});
|
||||
31
packages/client/schemas/tag.ts
Normal file
31
packages/client/schemas/tag.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { z } from "@hono/zod-openapi";
|
||||
|
||||
export const Tag = z
|
||||
.object({
|
||||
name: z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(128)
|
||||
.openapi({
|
||||
description: "The value of the hashtag after the # sign.",
|
||||
example: "versia",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Status/#Tag-name",
|
||||
},
|
||||
}),
|
||||
url: z
|
||||
.string()
|
||||
.url()
|
||||
.openapi({
|
||||
description: "A link to the hashtag on the instance.",
|
||||
example: "https://beta.versia.social/tags/versia",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Status/#Tag-url",
|
||||
},
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Status/#Tag",
|
||||
},
|
||||
});
|
||||
29
packages/client/schemas/token.ts
Normal file
29
packages/client/schemas/token.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import { z } from "@hono/zod-openapi";
|
||||
|
||||
export const Token = z
|
||||
.object({
|
||||
access_token: z.string().openapi({
|
||||
description: "An OAuth token to be used for authorization.",
|
||||
example: "ZA-Yj3aBD8U8Cm7lKUp-lm9O9BmDgdhHzDeqsY8tlL0",
|
||||
}),
|
||||
token_type: z.string().openapi({
|
||||
description: "The OAuth token type. Versia uses Bearer tokens.",
|
||||
example: "Bearer",
|
||||
}),
|
||||
scope: z.string().openapi({
|
||||
description:
|
||||
"The OAuth scopes granted by this token, space-separated.",
|
||||
example: "read write follow push",
|
||||
}),
|
||||
created_at: z.number().nonnegative().openapi({
|
||||
description: "When the token was generated. UNIX timestamp.",
|
||||
example: 1573979017,
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
description:
|
||||
"Represents an OAuth token used for authenticating with the API and performing actions.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/Token",
|
||||
},
|
||||
});
|
||||
16
packages/client/schemas/tos.ts
Normal file
16
packages/client/schemas/tos.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { z } from "@hono/zod-openapi";
|
||||
|
||||
export const TermsOfService = z
|
||||
.object({
|
||||
updated_at: z.string().datetime().openapi({
|
||||
description: "A timestamp of when the ToS was last updated.",
|
||||
example: "2025-01-12T13:11:00Z",
|
||||
}),
|
||||
content: z.string().openapi({
|
||||
description: "The rendered HTML content of the ToS.",
|
||||
example: "<p><h1>ToS</h1><p>None, have fun.</p></p>",
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
description: "Represents the ToS of the instance.",
|
||||
});
|
||||
107
packages/client/schemas/versia.ts
Normal file
107
packages/client/schemas/versia.ts
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
import { z } from "@hono/zod-openapi";
|
||||
import { RolePermission } from "@versia/client/types";
|
||||
import { config } from "~/config.ts";
|
||||
import { Id } from "./common.ts";
|
||||
|
||||
/* Versia Server API extension */
|
||||
export const Role = z
|
||||
.object({
|
||||
id: Id.openapi({}).openapi({
|
||||
description: "The role ID in the database.",
|
||||
example: "b4a7e0f0-8f6a-479b-910b-9265c070d5bd",
|
||||
}),
|
||||
name: z.string().min(1).max(128).trim().openapi({
|
||||
description: "The name of the role.",
|
||||
example: "Moderator",
|
||||
}),
|
||||
permissions: z
|
||||
.array(z.nativeEnum(RolePermission))
|
||||
.transform(
|
||||
// Deduplicate permissions
|
||||
(permissions) => Array.from(new Set(permissions)),
|
||||
)
|
||||
.default([])
|
||||
.openapi({
|
||||
description: "The permissions granted to the role.",
|
||||
example: [
|
||||
RolePermission.ManageEmojis,
|
||||
RolePermission.ManageAccounts,
|
||||
],
|
||||
}),
|
||||
priority: z.number().int().default(0).openapi({
|
||||
description:
|
||||
"Role priority. Higher priority roles allow overriding lower priority roles.",
|
||||
example: 100,
|
||||
}),
|
||||
description: z.string().min(0).max(1024).trim().optional().openapi({
|
||||
description: "Short role description.",
|
||||
example: "Allows managing emojis and accounts.",
|
||||
}),
|
||||
visible: z.boolean().default(true).openapi({
|
||||
description: "Whether the role should be shown in the UI.",
|
||||
}),
|
||||
icon: z.string().url().optional().openapi({
|
||||
description: "URL to the role icon.",
|
||||
example: "https://example.com/role-icon.png",
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
description:
|
||||
"Information about a role in the system, as well as its permissions.",
|
||||
});
|
||||
|
||||
/* Versia Server API extension */
|
||||
export const NoteReaction = z
|
||||
.object({
|
||||
name: z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(config.validation.emojis.max_shortcode_characters)
|
||||
.trim()
|
||||
.openapi({
|
||||
description: "Custom Emoji shortcode or Unicode emoji.",
|
||||
example: "blobfox_coffee",
|
||||
}),
|
||||
count: z.number().int().nonnegative().openapi({
|
||||
description: "Number of users who reacted with this emoji.",
|
||||
example: 5,
|
||||
}),
|
||||
me: z.boolean().optional().openapi({
|
||||
description:
|
||||
"Whether the current authenticated user reacted with this emoji.",
|
||||
example: true,
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
description: "Information about a reaction to a note.",
|
||||
});
|
||||
|
||||
/* Versia Server API extension */
|
||||
export const SSOConfig = z.object({
|
||||
forced: z.boolean().openapi({
|
||||
description:
|
||||
"If this is enabled, normal identifier/password login is disabled and login must be done through SSO.",
|
||||
example: false,
|
||||
}),
|
||||
providers: z
|
||||
.array(
|
||||
z.object({
|
||||
id: z.string().min(1).openapi({
|
||||
description: "The ID of the provider.",
|
||||
example: "google",
|
||||
}),
|
||||
name: z.string().min(1).openapi({
|
||||
description: "Human-readable provider name.",
|
||||
example: "Google",
|
||||
}),
|
||||
icon: z.string().url().optional().openapi({
|
||||
description: "URL to the provider icon.",
|
||||
example: "https://cdn.versia.social/google-icon.png",
|
||||
}),
|
||||
}),
|
||||
)
|
||||
.openapi({
|
||||
description:
|
||||
"An array of external OpenID Connect providers that users can link their accounts to.",
|
||||
}),
|
||||
});
|
||||
306
packages/client/versia/base.ts
Normal file
306
packages/client/versia/base.ts
Normal file
|
|
@ -0,0 +1,306 @@
|
|||
import { DEFAULT_UA } from "./constants.ts";
|
||||
|
||||
type HttpVerb = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
||||
type ConvertibleObject = {
|
||||
[key: string]:
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| File
|
||||
| undefined
|
||||
| null
|
||||
| ConvertibleObject[]
|
||||
| ConvertibleObject;
|
||||
};
|
||||
|
||||
/**
|
||||
* Output of a request. Contains the data and headers.
|
||||
* @template ReturnType The type of the data returned by the request.
|
||||
*/
|
||||
export interface Output<ReturnType> {
|
||||
data: ReturnType;
|
||||
ok: boolean;
|
||||
raw: Response;
|
||||
}
|
||||
|
||||
const objectToFormData = (
|
||||
obj: ConvertibleObject,
|
||||
formData = new FormData(),
|
||||
parentKey = "",
|
||||
): FormData => {
|
||||
if (obj === undefined || obj === null) {
|
||||
return formData;
|
||||
}
|
||||
|
||||
for (const key of Object.keys(obj)) {
|
||||
const value = obj[key];
|
||||
const fullKey = parentKey ? `${parentKey}[${key}]` : key;
|
||||
|
||||
if (value === undefined || value === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (value instanceof File) {
|
||||
formData.append(fullKey, value as Blob);
|
||||
} else if (Array.isArray(value)) {
|
||||
for (const [index, item] of value.entries()) {
|
||||
const arrayKey = `${fullKey}[${index}]`;
|
||||
if (item instanceof File) {
|
||||
formData.append(arrayKey, item as Blob);
|
||||
} else if (typeof item === "object") {
|
||||
objectToFormData(
|
||||
item as ConvertibleObject,
|
||||
formData,
|
||||
arrayKey,
|
||||
);
|
||||
} else {
|
||||
formData.append(arrayKey, String(item));
|
||||
}
|
||||
}
|
||||
} else if (typeof value === "object") {
|
||||
objectToFormData(value as ConvertibleObject, formData, fullKey);
|
||||
} else {
|
||||
formData.append(fullKey, String(value));
|
||||
}
|
||||
}
|
||||
|
||||
return formData;
|
||||
};
|
||||
|
||||
/**
|
||||
* Wrapper around Error, useful for detecting if an error
|
||||
* is due to a failed request.
|
||||
*
|
||||
* Throws if the request returns invalid or unexpected data.
|
||||
*/
|
||||
export class ResponseError<
|
||||
ReturnType = {
|
||||
error?: string;
|
||||
},
|
||||
> extends Error {
|
||||
public constructor(
|
||||
public response: Output<ReturnType>,
|
||||
message: string,
|
||||
) {
|
||||
super(message);
|
||||
this.name = "ResponseError";
|
||||
}
|
||||
}
|
||||
|
||||
export class BaseClient {
|
||||
public constructor(
|
||||
protected baseUrl: URL,
|
||||
private accessToken?: string,
|
||||
public globalCatch: (error: ResponseError) => void = () => {
|
||||
// Do nothing by default
|
||||
},
|
||||
) {}
|
||||
|
||||
public get url(): URL {
|
||||
return this.baseUrl;
|
||||
}
|
||||
|
||||
public get token(): string | undefined {
|
||||
return this.accessToken;
|
||||
}
|
||||
|
||||
private async request<ReturnType>(
|
||||
request: Request,
|
||||
): Promise<Output<ReturnType>> {
|
||||
const result = await fetch(request);
|
||||
const isJson = result.headers
|
||||
.get("Content-Type")
|
||||
?.includes("application/json");
|
||||
|
||||
if (!result.ok) {
|
||||
const error = isJson ? await result.json() : await result.text();
|
||||
throw new ResponseError(
|
||||
{
|
||||
data: error,
|
||||
ok: false,
|
||||
raw: result,
|
||||
},
|
||||
`Request failed (${result.status}): ${
|
||||
error.error || error.message || result.statusText
|
||||
}`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
data: isJson ? await result.json() : (await result.text()) || null,
|
||||
ok: true,
|
||||
raw: result,
|
||||
};
|
||||
}
|
||||
|
||||
private constructRequest(
|
||||
path: string,
|
||||
method: HttpVerb,
|
||||
body?: object | FormData,
|
||||
extra?: RequestInit,
|
||||
): Request {
|
||||
const headers = new Headers({
|
||||
"User-Agent": DEFAULT_UA,
|
||||
});
|
||||
|
||||
if (this.accessToken) {
|
||||
headers.set("Authorization", `Bearer ${this.accessToken}`);
|
||||
}
|
||||
if (body) {
|
||||
if (!(body instanceof FormData)) {
|
||||
headers.set("Content-Type", "application/json; charset=utf-8");
|
||||
} // else: let FormData set the content type, as it knows best (boundary, etc.)
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(extra?.headers || {})) {
|
||||
headers.set(key, value);
|
||||
}
|
||||
|
||||
return new Request(new URL(path, this.baseUrl).toString(), {
|
||||
method,
|
||||
headers,
|
||||
body: body
|
||||
? body instanceof FormData
|
||||
? body
|
||||
: JSON.stringify(body)
|
||||
: undefined,
|
||||
...extra,
|
||||
});
|
||||
}
|
||||
|
||||
public get<ReturnType>(
|
||||
path: string,
|
||||
extra?: RequestInit,
|
||||
): Promise<Output<ReturnType>> {
|
||||
return this.request<ReturnType>(
|
||||
this.constructRequest(path, "GET", undefined, extra),
|
||||
).catch((e) => {
|
||||
this.globalCatch(e);
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
|
||||
public post<ReturnType>(
|
||||
path: string,
|
||||
body?: object,
|
||||
extra?: RequestInit,
|
||||
): Promise<Output<ReturnType>> {
|
||||
return this.request<ReturnType>(
|
||||
this.constructRequest(path, "POST", body, extra),
|
||||
).catch((e) => {
|
||||
this.globalCatch(e);
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
|
||||
public postForm<ReturnType>(
|
||||
path: string,
|
||||
body: FormData | ConvertibleObject,
|
||||
extra?: RequestInit,
|
||||
): Promise<Output<ReturnType>> {
|
||||
return this.request<ReturnType>(
|
||||
this.constructRequest(
|
||||
path,
|
||||
"POST",
|
||||
body instanceof FormData ? body : objectToFormData(body),
|
||||
extra,
|
||||
),
|
||||
).catch((e) => {
|
||||
this.globalCatch(e);
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
|
||||
public put<ReturnType>(
|
||||
path: string,
|
||||
body?: object,
|
||||
extra?: RequestInit,
|
||||
): Promise<Output<ReturnType>> {
|
||||
return this.request<ReturnType>(
|
||||
this.constructRequest(path, "PUT", body, extra),
|
||||
).catch((e) => {
|
||||
this.globalCatch(e);
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
|
||||
public putForm<ReturnType>(
|
||||
path: string,
|
||||
body: FormData | ConvertibleObject,
|
||||
extra?: RequestInit,
|
||||
): Promise<Output<ReturnType>> {
|
||||
return this.request<ReturnType>(
|
||||
this.constructRequest(
|
||||
path,
|
||||
"PUT",
|
||||
body instanceof FormData ? body : objectToFormData(body),
|
||||
extra,
|
||||
),
|
||||
).catch((e) => {
|
||||
this.globalCatch(e);
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
|
||||
public patch<ReturnType>(
|
||||
path: string,
|
||||
body?: object,
|
||||
extra?: RequestInit,
|
||||
): Promise<Output<ReturnType>> {
|
||||
return this.request<ReturnType>(
|
||||
this.constructRequest(path, "PATCH", body, extra),
|
||||
).catch((e) => {
|
||||
this.globalCatch(e);
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
|
||||
public patchForm<ReturnType>(
|
||||
path: string,
|
||||
body: FormData | ConvertibleObject,
|
||||
extra?: RequestInit,
|
||||
): Promise<Output<ReturnType>> {
|
||||
return this.request<ReturnType>(
|
||||
this.constructRequest(
|
||||
path,
|
||||
"PATCH",
|
||||
body instanceof FormData ? body : objectToFormData(body),
|
||||
extra,
|
||||
),
|
||||
).catch((e) => {
|
||||
this.globalCatch(e);
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
|
||||
public delete<ReturnType>(
|
||||
path: string,
|
||||
body?: object,
|
||||
extra?: RequestInit,
|
||||
): Promise<Output<ReturnType>> {
|
||||
return this.request<ReturnType>(
|
||||
this.constructRequest(path, "DELETE", body, extra),
|
||||
).catch((e) => {
|
||||
this.globalCatch(e);
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
|
||||
public deleteForm<ReturnType>(
|
||||
path: string,
|
||||
body: FormData | ConvertibleObject,
|
||||
extra?: RequestInit,
|
||||
): Promise<Output<ReturnType>> {
|
||||
return this.request<ReturnType>(
|
||||
this.constructRequest(
|
||||
path,
|
||||
"DELETE",
|
||||
body instanceof FormData ? body : objectToFormData(body),
|
||||
extra,
|
||||
),
|
||||
).catch((e) => {
|
||||
this.globalCatch(e);
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
}
|
||||
3103
packages/client/versia/client.ts
Normal file
3103
packages/client/versia/client.ts
Normal file
File diff suppressed because it is too large
Load diff
5
packages/client/versia/constants.ts
Normal file
5
packages/client/versia/constants.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import pkg from "../package.json" with { type: "json" };
|
||||
|
||||
export const NO_REDIRECT = "urn:ietf:wg:oauth:2.0:oob";
|
||||
export const DEFAULT_SCOPE = ["read", "write", "follow"];
|
||||
export const DEFAULT_UA = `VersiaClient/${pkg.version} (+${pkg.homepage})`;
|
||||
Loading…
Add table
Add a link
Reference in a new issue