diff --git a/api/api/v1/accounts/:id/follow.ts b/api/api/v1/accounts/:id/follow.ts index dc443f53..3b3edc07 100644 --- a/api/api/v1/accounts/:id/follow.ts +++ b/api/api/v1/accounts/:id/follow.ts @@ -2,7 +2,7 @@ import { apiRoute, auth, withUserParam } from "@/api"; import { createRoute, z } from "@hono/zod-openapi"; import { Relationship } from "@versia/kit/db"; import { RolePermissions } from "@versia/kit/tables"; -import ISO6391 from "iso-639-1"; +import { iso631 } from "~/classes/schemas/common"; const schemas = { param: z.object({ @@ -12,9 +12,7 @@ const schemas = { .object({ reblogs: z.coerce.boolean().optional(), notify: z.coerce.boolean().optional(), - languages: z - .array(z.enum(ISO6391.getAllCodes() as [string, ...string[]])) - .optional(), + languages: z.array(iso631).optional(), }) .optional() .default({ reblogs: true, notify: false, languages: [] }), diff --git a/classes/schemas/account-warning.ts b/classes/schemas/account-warning.ts new file mode 100644 index 00000000..4bdd730d --- /dev/null +++ b/classes/schemas/account-warning.ts @@ -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", + }, + }); diff --git a/classes/schemas/account.ts b/classes/schemas/account.ts index fed37940..818a2aa8 100644 --- a/classes/schemas/account.ts +++ b/classes/schemas/account.ts @@ -1,8 +1,8 @@ import { z } from "@hono/zod-openapi"; import type { Account as ApiAccount } from "@versia/client/types"; -import ISO6391 from "iso-639-1"; import { config } from "~/packages/config-manager"; import { zBoolean } from "~/packages/config-manager/config.type"; +import { iso631 } from "./common.ts"; import { CustomEmoji } from "./emoji.ts"; import { Role } from "./versia.ts"; @@ -63,15 +63,13 @@ export const Source = z url: "https://docs.joinmastodon.org/entities/Account/#source-sensitive", }, }), - language: z - .enum(ISO6391.getAllCodes() as [string, ...string[]]) - .openapi({ - description: "The default posting language for new statuses.", - example: "en", - externalDocs: { - url: "https://docs.joinmastodon.org/entities/Account/#source-language", - }, - }), + 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() diff --git a/classes/schemas/appeal.ts b/classes/schemas/appeal.ts new file mode 100644 index 00000000..8c10bd6a --- /dev/null +++ b/classes/schemas/appeal.ts @@ -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", + }, + }); diff --git a/classes/schemas/attachment.ts b/classes/schemas/attachment.ts new file mode 100644 index 00000000..9200632c --- /dev/null +++ b/classes/schemas/attachment.ts @@ -0,0 +1,70 @@ +import { z } from "@hono/zod-openapi"; +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().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", + }, + }); diff --git a/classes/schemas/card.ts b/classes/schemas/card.ts index fd65c403..0b367b78 100644 --- a/classes/schemas/card.ts +++ b/classes/schemas/card.ts @@ -27,125 +27,133 @@ export const PreviewCardAuthor = z.object({ }), }); -export const PreviewCard = z.object({ - url: z - .string() - .url() - .openapi({ - description: "Location of linked resource.", - example: "https://www.youtube.com/watch?v=OMv_EPMED8Y", +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/#url", + url: "https://docs.joinmastodon.org/entities/PreviewCard/#description", }, }), - title: z - .string() - .min(1) - .openapi({ - description: "Title of linked resource.", - example: "♪ Brand New Friend (Christmas Song!)", + type: z.enum(["link", "photo", "video"]).openapi({ + description: "The type of the preview card.", + example: "video", externalDocs: { - url: "https://docs.joinmastodon.org/entities/PreviewCard/#title", + url: "https://docs.joinmastodon.org/entities/PreviewCard/#type", }, }), - 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: - '', - 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({ + authors: z.array(PreviewCardAuthor).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@", + "Fediverse account of the authors of the original resource.", externalDocs: { - url: "https://docs.joinmastodon.org/entities/PreviewCard/#blurhash", + 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: + '', + 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", + }, + }); diff --git a/classes/schemas/common.ts b/classes/schemas/common.ts index ddb0cfcb..d3a40c18 100644 --- a/classes/schemas/common.ts +++ b/classes/schemas/common.ts @@ -1,3 +1,6 @@ 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[]]); diff --git a/classes/schemas/extended-description.ts b/classes/schemas/extended-description.ts new file mode 100644 index 00000000..261dbf1e --- /dev/null +++ b/classes/schemas/extended-description.ts @@ -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: "
We love casting spells.
", + 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", + }, + }); diff --git a/classes/schemas/familiar-followers.ts b/classes/schemas/familiar-followers.ts new file mode 100644 index 00000000..d4de1c7b --- /dev/null +++ b/classes/schemas/familiar-followers.ts @@ -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", + }, + }); diff --git a/classes/schemas/filters.ts b/classes/schemas/filters.ts index 3059e868..9ac25a10 100644 --- a/classes/schemas/filters.ts +++ b/classes/schemas/filters.ts @@ -1,129 +1,171 @@ import { z } from "@hono/zod-openapi"; import { Id } 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", - }, - }), -}); - -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: z.boolean().openapi({ +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: - "Should the filter consider word boundaries? See implementation guidelines for filters.", - example: false, + "Represents a status ID that, if matched, should cause the filter action to be taken.", externalDocs: { - url: "https://docs.joinmastodon.org/entities/FilterKeyword/#whole_word", + url: "https://docs.joinmastodon.org/entities/FilterStatus", }, - }), -}); + }); -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().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"])) - .openapi({ - description: "The contexts in which the filter should be applied.", - example: ["home"], +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/Filter/#context", + url: "https://docs.joinmastodon.org/entities/FilterKeyword/#id", }, }), - expires_at: z - .string() - .nullable() - .openapi({ - description: "When the filter should no longer be applied.", - example: "2026-09-20T17:27:39.296Z", + keyword: z.string().openapi({ + description: "The phrase to be matched against.", + example: "badword", externalDocs: { - url: "https://docs.joinmastodon.org/entities/Filter/#expires_at", + url: "https://docs.joinmastodon.org/entities/FilterKeyword/#keyword", }, }), - filter_action: z.enum(["warn", "hide"]).openapi({ + whole_word: z.boolean().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: - "The action to be taken when a status matches this filter.", - example: "warn", + "Represents a keyword that, if matched, should cause the filter action to be taken.", externalDocs: { - url: "https://docs.joinmastodon.org/entities/Filter/#filter_action", + url: "https://docs.joinmastodon.org/entities/FilterKeyword", }, - }), - 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", - }, - }), -}); + }); -export const FilterResult = z.object({ - filter: Filter.openapi({ - description: "The filter that was matched.", +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().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", + ]), + ) + .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"]).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/FilterResult/#filter", + url: "https://docs.joinmastodon.org/entities/Filter", }, - }), - keyword_matches: z - .array(z.string()) - .nullable() - .openapi({ - description: "The keyword within the filter that was matched.", - example: ["badword"], + }); + +export const FilterResult = z + .object({ + filter: Filter.openapi({ + description: "The filter that was matched.", externalDocs: { - url: "https://docs.joinmastodon.org/entities/FilterResult/#keyword_matches", + url: "https://docs.joinmastodon.org/entities/FilterResult/#filter", }, }), - 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", - }, - }), -}); + 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", + }, + }); diff --git a/classes/schemas/instance-v1.ts b/classes/schemas/instance-v1.ts new file mode 100644 index 00000000..88906383 --- /dev/null +++ b/classes/schemas/instance-v1.ts @@ -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: "Join the world's smallest social network.
", + }), + 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", + }, + }); diff --git a/classes/schemas/instance.ts b/classes/schemas/instance.ts new file mode 100644 index 00000000..a9d535c4 --- /dev/null +++ b/classes/schemas/instance.ts @@ -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", + }, + }); diff --git a/classes/schemas/marker.ts b/classes/schemas/marker.ts new file mode 100644 index 00000000..e9a6cdb7 --- /dev/null +++ b/classes/schemas/marker.ts @@ -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", + }, + }); diff --git a/classes/schemas/notification.ts b/classes/schemas/notification.ts new file mode 100644 index 00000000..9104fae0 --- /dev/null +++ b/classes/schemas/notification.ts @@ -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", + }, + }); diff --git a/classes/schemas/poll.ts b/classes/schemas/poll.ts new file mode 100644 index 00000000..75465830 --- /dev/null +++ b/classes/schemas/poll.ts @@ -0,0 +1,132 @@ +import { z } from "@hono/zod-openapi"; +import { Id } from "./common.ts"; +import { CustomEmoji } from "./emoji.ts"; + +export const PollOption = z + .object({ + title: z.string().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", + }, + }); diff --git a/classes/schemas/preferences.ts b/classes/schemas/preferences.ts new file mode 100644 index 00000000..65e104c1 --- /dev/null +++ b/classes/schemas/preferences.ts @@ -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", + }, + }); diff --git a/classes/schemas/privacy-policy.ts b/classes/schemas/privacy-policy.ts new file mode 100644 index 00000000..dee471ec --- /dev/null +++ b/classes/schemas/privacy-policy.ts @@ -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: "None, good luck.
", + 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", + }, + }); diff --git a/classes/schemas/relationship.ts b/classes/schemas/relationship.ts new file mode 100644 index 00000000..8d2f8889 --- /dev/null +++ b/classes/schemas/relationship.ts @@ -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().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", + }, + }); diff --git a/classes/schemas/report.ts b/classes/schemas/report.ts new file mode 100644 index 00000000..4d4049ec --- /dev/null +++ b/classes/schemas/report.ts @@ -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", + }, + }); diff --git a/classes/schemas/rule.ts b/classes/schemas/rule.ts new file mode 100644 index 00000000..0a8db603 --- /dev/null +++ b/classes/schemas/rule.ts @@ -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", + }, + }); diff --git a/classes/schemas/search.ts b/classes/schemas/search.ts new file mode 100644 index 00000000..e3286479 --- /dev/null +++ b/classes/schemas/search.ts @@ -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", + }, + }); diff --git a/classes/schemas/status.ts b/classes/schemas/status.ts index 9a7bf0a1..88ff73cd 100644 --- a/classes/schemas/status.ts +++ b/classes/schemas/status.ts @@ -1,13 +1,14 @@ import { z } from "@hono/zod-openapi"; import type { Status as ApiNote } from "@versia/client/types"; import { Media } from "@versia/kit/db"; -import ISO6391 from "iso-639-1"; import { zBoolean } from "~/packages/config-manager/config.type.ts"; import { Account } from "./account.ts"; import { PreviewCard } from "./card.ts"; -import { Id } from "./common.ts"; +import { Id, iso631 } 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 @@ -48,155 +49,6 @@ export const Mention = z }, }); -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", - }, - }); - -export const PollOption = z - .object({ - title: z.string().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: zBoolean.openapi({ - description: "Is the poll currently expired?", - example: false, - externalDocs: { - url: "https://docs.joinmastodon.org/entities/Poll/#expired", - }, - }), - multiple: zBoolean.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: zBoolean.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", - }, - }), -}); - export const Status = z.object({ id: Id.openapi({ description: "ID of the status in the database.", @@ -429,16 +281,13 @@ export const Status = z.object({ url: "https://docs.joinmastodon.org/entities/Status/#application", }, }), - language: z - .enum(ISO6391.getAllCodes() as [string, ...string[]]) - .nullable() - .openapi({ - description: "Primary language of this status.", - example: "en", - externalDocs: { - url: "https://docs.joinmastodon.org/entities/Status/#language", - }, - }), + 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() diff --git a/classes/schemas/tag.ts b/classes/schemas/tag.ts new file mode 100644 index 00000000..4cc9eac6 --- /dev/null +++ b/classes/schemas/tag.ts @@ -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", + }, + }); diff --git a/classes/schemas/token.ts b/classes/schemas/token.ts new file mode 100644 index 00000000..11c942dd --- /dev/null +++ b/classes/schemas/token.ts @@ -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", + }, + }); diff --git a/classes/schemas/versia.ts b/classes/schemas/versia.ts index fd44db0c..ef8d5cd1 100644 --- a/classes/schemas/versia.ts +++ b/classes/schemas/versia.ts @@ -1,7 +1,7 @@ import { z } from "@hono/zod-openapi"; import { RolePermission } from "@versia/client/types"; -import { Id } from "./common.ts"; import { config } from "~/packages/config-manager/index.ts"; +import { Id } from "./common.ts"; /* Versia Server API extension */ export const Role = z @@ -75,3 +75,33 @@ export const NoteReaction = z .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.", + }), +});