mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 08:28:19 +01:00
refactor(api): ♻️ Make SDK and client package only use resources in their own package
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 1s
Build Docker Images / lint (push) Failing after 7s
Build Docker Images / check (push) Failing after 6s
Build Docker Images / tests (push) Failing after 6s
Deploy Docs to GitHub Pages / build (push) Failing after 0s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Mirror to Codeberg / Mirror (push) Failing after 0s
Nix Build / check (push) Failing after 1s
Test Publish / build (client) (push) Failing after 0s
Test Publish / build (sdk) (push) Failing after 0s
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 1s
Build Docker Images / lint (push) Failing after 7s
Build Docker Images / check (push) Failing after 6s
Build Docker Images / tests (push) Failing after 6s
Deploy Docs to GitHub Pages / build (push) Failing after 0s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Mirror to Codeberg / Mirror (push) Failing after 0s
Nix Build / check (push) Failing after 1s
Test Publish / build (client) (push) Failing after 0s
Test Publish / build (sdk) (push) Failing after 0s
This commit is contained in:
parent
c0060f1baf
commit
5dfcfc548f
50
.github/workflows/staging.yml
vendored
50
.github/workflows/staging.yml
vendored
|
|
@ -1,50 +0,0 @@
|
|||
name: Staging build bundle
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["staging"]
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Install NPM packages
|
||||
run: |
|
||||
bun install
|
||||
|
||||
- name: Build dist
|
||||
run: |
|
||||
bun run build
|
||||
|
||||
- name: Bundle
|
||||
run: |
|
||||
mkdir bundle
|
||||
cp -r dist bundle/
|
||||
cp -r config bundle/
|
||||
cp -r docs bundle/
|
||||
cp -r CODE_OF_CONDUCT.md bundle/
|
||||
cp -r CONTRIBUTING.md bundle/
|
||||
cp -r README.md bundle/
|
||||
cp -r flake.nix bundle/
|
||||
cp -r shell.nix bundle/
|
||||
cp -r flake.lock bundle/
|
||||
cp -r LICENSE bundle/
|
||||
cp -r SECURITY.md bundle/
|
||||
tar cfJ archive.tar.xz bundle/
|
||||
|
||||
- name: Upload
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: staging-dist
|
||||
path: archive.tar.xz
|
||||
33
.github/workflows/test-publish.yml
vendored
Normal file
33
.github/workflows/test-publish.yml
vendored
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
name: Test Publish
|
||||
|
||||
on:
|
||||
push:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
# For provenance generation
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
# Build job
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
package: ["sdk", "client"]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Install
|
||||
run: bun install
|
||||
|
||||
- name: Publish to NPM
|
||||
run: bun publish --dry-run
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Publish to JSR
|
||||
run: bunx jsr publish --allow-slow-types --allow-dirty --dry-run
|
||||
|
|
@ -52,17 +52,56 @@ export default apiRoute((app) =>
|
|||
"json",
|
||||
z
|
||||
.object({
|
||||
display_name: AccountSchema.shape.display_name.openapi({
|
||||
description: "The display name to use for the profile.",
|
||||
example: "Lexi",
|
||||
}),
|
||||
username: AccountSchema.shape.username.openapi({
|
||||
description: "The username to use for the profile.",
|
||||
example: "lexi",
|
||||
}),
|
||||
note: AccountSchema.shape.note.openapi({
|
||||
description: "The account bio. Markdown is supported.",
|
||||
}),
|
||||
display_name: AccountSchema.shape.display_name
|
||||
.openapi({
|
||||
description:
|
||||
"The display name to use for the profile.",
|
||||
example: "Lexi",
|
||||
})
|
||||
.max(
|
||||
config.validation.accounts
|
||||
.max_displayname_characters,
|
||||
)
|
||||
.refine(
|
||||
(s) =>
|
||||
!config.validation.filters.displayname.some(
|
||||
(filter) => filter.test(s),
|
||||
),
|
||||
"Display name contains blocked words",
|
||||
),
|
||||
username: AccountSchema.shape.username
|
||||
.openapi({
|
||||
description: "The username to use for the profile.",
|
||||
example: "lexi",
|
||||
})
|
||||
.max(config.validation.accounts.max_username_characters)
|
||||
.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",
|
||||
),
|
||||
note: AccountSchema.shape.note
|
||||
.openapi({
|
||||
description:
|
||||
"The account bio. Markdown is supported.",
|
||||
})
|
||||
.max(config.validation.accounts.max_bio_characters)
|
||||
.refine(
|
||||
(s) =>
|
||||
!config.validation.filters.bio.some((filter) =>
|
||||
filter.test(s),
|
||||
),
|
||||
"Bio contains blocked words",
|
||||
),
|
||||
avatar: z
|
||||
.string()
|
||||
.url()
|
||||
|
|
@ -150,10 +189,14 @@ export default apiRoute((app) =>
|
|||
fields_attributes: z
|
||||
.array(
|
||||
z.object({
|
||||
name: AccountSchema.shape.fields.element.shape
|
||||
.name,
|
||||
value: AccountSchema.shape.fields.element.shape
|
||||
.value,
|
||||
name: AccountSchema.shape.fields.element.shape.name.max(
|
||||
config.validation.accounts
|
||||
.max_field_name_characters,
|
||||
),
|
||||
value: AccountSchema.shape.fields.element.shape.value.max(
|
||||
config.validation.accounts
|
||||
.max_field_value_characters,
|
||||
),
|
||||
}),
|
||||
)
|
||||
.max(config.validation.accounts.max_field_count),
|
||||
|
|
|
|||
|
|
@ -113,7 +113,9 @@ export default apiRoute((app) => {
|
|||
"json",
|
||||
z
|
||||
.object({
|
||||
shortcode: CustomEmojiSchema.shape.shortcode,
|
||||
shortcode: CustomEmojiSchema.shape.shortcode.max(
|
||||
config.validation.emojis.max_shortcode_characters,
|
||||
),
|
||||
element: z
|
||||
.string()
|
||||
.url()
|
||||
|
|
@ -136,7 +138,12 @@ export default apiRoute((app) => {
|
|||
),
|
||||
),
|
||||
category: CustomEmojiSchema.shape.category.optional(),
|
||||
alt: CustomEmojiSchema.shape.description.optional(),
|
||||
alt: CustomEmojiSchema.shape.description
|
||||
.unwrap()
|
||||
.max(
|
||||
config.validation.emojis.max_description_characters,
|
||||
)
|
||||
.optional(),
|
||||
global: CustomEmojiSchema.shape.global.default(false),
|
||||
})
|
||||
.partial(),
|
||||
|
|
|
|||
|
|
@ -45,7 +45,9 @@ export default apiRoute((app) =>
|
|||
validator(
|
||||
"json",
|
||||
z.object({
|
||||
shortcode: CustomEmojiSchema.shape.shortcode,
|
||||
shortcode: CustomEmojiSchema.shape.shortcode.max(
|
||||
config.validation.emojis.max_shortcode_characters,
|
||||
),
|
||||
element: z
|
||||
.string()
|
||||
.url()
|
||||
|
|
@ -68,7 +70,10 @@ export default apiRoute((app) =>
|
|||
),
|
||||
),
|
||||
category: CustomEmojiSchema.shape.category.optional(),
|
||||
alt: CustomEmojiSchema.shape.description.optional(),
|
||||
alt: CustomEmojiSchema.shape.description
|
||||
.unwrap()
|
||||
.max(config.validation.emojis.max_description_characters)
|
||||
.optional(),
|
||||
global: CustomEmojiSchema.shape.global.default(false),
|
||||
}),
|
||||
handleZodError,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { resolver, validator } from "hono-openapi/zod";
|
|||
import { z } from "zod";
|
||||
import { apiRoute, auth, handleZodError } from "@/api";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
import { config } from "~/config";
|
||||
|
||||
export default apiRoute((app) => {
|
||||
app.get(
|
||||
|
|
@ -109,7 +110,10 @@ export default apiRoute((app) => {
|
|||
description:
|
||||
"The custom thumbnail of the media to be attached, encoded using multipart form data.",
|
||||
}),
|
||||
description: AttachmentSchema.shape.description,
|
||||
description: AttachmentSchema.shape.description
|
||||
.unwrap()
|
||||
.max(config.validation.media.max_description_characters)
|
||||
.optional(),
|
||||
focus: z.string().openapi({
|
||||
description:
|
||||
"Two floating points (x,y), comma-delimited, ranging from -1.0 to 1.0. Used for media cropping on clients.",
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { resolver, validator } from "hono-openapi/zod";
|
|||
import { z } from "zod";
|
||||
import { apiRoute, auth, handleZodError } from "@/api";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
import { config } from "~/config";
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.post(
|
||||
|
|
@ -67,7 +68,10 @@ export default apiRoute((app) =>
|
|||
description:
|
||||
"The custom thumbnail of the media to be attached, encoded using multipart form data.",
|
||||
}),
|
||||
description: AttachmentSchema.shape.description.optional(),
|
||||
description: AttachmentSchema.shape.description
|
||||
.unwrap()
|
||||
.max(config.validation.media.max_description_characters)
|
||||
.optional(),
|
||||
focus: z
|
||||
.string()
|
||||
.optional()
|
||||
|
|
|
|||
|
|
@ -25,10 +25,20 @@ import * as VersiaEntities from "~/packages/sdk/entities";
|
|||
|
||||
const schema = z
|
||||
.object({
|
||||
status: StatusSourceSchema.shape.text.optional().openapi({
|
||||
description:
|
||||
"The text content of the status. If media_ids is provided, this becomes optional. Attaching a poll is optional while status is provided.",
|
||||
}),
|
||||
status: StatusSourceSchema.shape.text
|
||||
.max(config.validation.notes.max_characters)
|
||||
.refine(
|
||||
(s) =>
|
||||
!config.validation.filters.note_content.some((filter) =>
|
||||
filter.test(s),
|
||||
),
|
||||
"Status contains blocked words",
|
||||
)
|
||||
.optional()
|
||||
.openapi({
|
||||
description:
|
||||
"The text content of the status. If media_ids is provided, this becomes optional. Attaching a poll is optional while status is provided.",
|
||||
}),
|
||||
/* Versia Server API Extension */
|
||||
content_type: z
|
||||
.enum(["text/plain", "text/html", "text/markdown"])
|
||||
|
|
@ -54,7 +64,11 @@ const schema = z
|
|||
}),
|
||||
language: StatusSchema.shape.language.optional(),
|
||||
"poll[options]": z
|
||||
.array(PollOption.shape.title)
|
||||
.array(
|
||||
PollOption.shape.title.max(
|
||||
config.validation.polls.max_option_characters,
|
||||
),
|
||||
)
|
||||
.max(config.validation.polls.max_options)
|
||||
.optional()
|
||||
.openapi({
|
||||
|
|
|
|||
|
|
@ -20,10 +20,20 @@ import * as VersiaEntities from "~/packages/sdk/entities";
|
|||
|
||||
const schema = z
|
||||
.object({
|
||||
status: StatusSourceSchema.shape.text.optional().openapi({
|
||||
description:
|
||||
"The text content of the status. If media_ids is provided, this becomes optional. Attaching a poll is optional while status is provided.",
|
||||
}),
|
||||
status: StatusSourceSchema.shape.text
|
||||
.max(config.validation.notes.max_characters)
|
||||
.refine(
|
||||
(s) =>
|
||||
!config.validation.filters.note_content.some((filter) =>
|
||||
filter.test(s),
|
||||
),
|
||||
"Status contains blocked words",
|
||||
)
|
||||
.optional()
|
||||
.openapi({
|
||||
description:
|
||||
"The text content of the status. If media_ids is provided, this becomes optional. Attaching a poll is optional while status is provided.",
|
||||
}),
|
||||
/* Versia Server API Extension */
|
||||
content_type: z
|
||||
.enum(["text/plain", "text/html", "text/markdown"])
|
||||
|
|
@ -49,7 +59,11 @@ const schema = z
|
|||
}),
|
||||
language: StatusSchema.shape.language.optional(),
|
||||
"poll[options]": z
|
||||
.array(PollOption.shape.title)
|
||||
.array(
|
||||
PollOption.shape.title.max(
|
||||
config.validation.polls.max_option_characters,
|
||||
),
|
||||
)
|
||||
.max(config.validation.polls.max_options)
|
||||
.optional()
|
||||
.openapi({
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { resolver, validator } from "hono-openapi/zod";
|
|||
import { z } from "zod";
|
||||
import { apiRoute, auth, handleZodError } from "@/api";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
import { config } from "~/config";
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.post(
|
||||
|
|
@ -76,7 +77,10 @@ export default apiRoute((app) =>
|
|||
description:
|
||||
"The custom thumbnail of the media to be attached, encoded using multipart form data.",
|
||||
}),
|
||||
description: AttachmentSchema.shape.description.optional(),
|
||||
description: AttachmentSchema.shape.description
|
||||
.unwrap()
|
||||
.max(config.validation.media.max_description_characters)
|
||||
.optional(),
|
||||
focus: z
|
||||
.string()
|
||||
.optional()
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import {
|
|||
Id,
|
||||
RolePermission,
|
||||
Search as SearchSchema,
|
||||
userAddressRegex,
|
||||
zBoolean,
|
||||
} from "@versia/client/schemas";
|
||||
import { db, Note, User } from "@versia/kit/db";
|
||||
|
|
@ -11,13 +12,7 @@ import { and, eq, inArray, isNull, sql } from "drizzle-orm";
|
|||
import { describeRoute } from "hono-openapi";
|
||||
import { resolver, validator } from "hono-openapi/zod";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
apiRoute,
|
||||
auth,
|
||||
handleZodError,
|
||||
parseUserAddress,
|
||||
userAddressValidator,
|
||||
} from "@/api";
|
||||
import { apiRoute, auth, handleZodError, parseUserAddress } from "@/api";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
import { searchManager } from "~/classes/search/search-manager";
|
||||
import { config } from "~/config.ts";
|
||||
|
|
@ -151,7 +146,7 @@ export default apiRoute((app) =>
|
|||
|
||||
if (!type || type === "accounts") {
|
||||
// Check if q is matching format username@domain.com or @username@domain.com
|
||||
const accountMatches = q?.trim().match(userAddressValidator);
|
||||
const accountMatches = q?.trim().match(userAddressRegex);
|
||||
if (accountMatches) {
|
||||
// Remove leading @ if it exists
|
||||
if (accountMatches[0].startsWith("@")) {
|
||||
|
|
|
|||
1
bun.lock
1
bun.lock
|
|
@ -92,6 +92,7 @@
|
|||
"dependencies": {
|
||||
"@badgateway/oauth2-client": "^3.0.0",
|
||||
"iso-639-1": "^3.1.5",
|
||||
"magic-regexp": "^0.10.0",
|
||||
"zod": "^3.24.2",
|
||||
"zod-openapi": "^4.2.4",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2,8 +2,7 @@ import { type BunFile, env, file } from "bun";
|
|||
import ISO6391 from "iso-639-1";
|
||||
import { types as mimeTypes } from "mime-types";
|
||||
import { generateVAPIDKeys } from "web-push";
|
||||
import { ZodError, z } from "zod";
|
||||
import { fromZodError } from "zod-validation-error";
|
||||
import { z } from "zod";
|
||||
import { ProxiableUrl } from "~/classes/media/url.ts";
|
||||
import { RolePermission } from "~/packages/client/schemas/permissions.ts";
|
||||
|
||||
|
|
@ -271,16 +270,6 @@ export const hmacKey = sensitiveString.transform(async (text, ctx) => {
|
|||
return text;
|
||||
});
|
||||
|
||||
try {
|
||||
console.info();
|
||||
} catch (e) {
|
||||
if (e instanceof ZodError) {
|
||||
throw fromZodError(e);
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
|
||||
export const ConfigSchema = z
|
||||
.strictObject({
|
||||
postgres: z
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
import type { CustomEmoji } from "@versia/client/schemas";
|
||||
import {
|
||||
type CustomEmoji,
|
||||
emojiWithColonsRegex,
|
||||
emojiWithIdentifiersRegex,
|
||||
} from "@versia/client/schemas";
|
||||
import { db, type Instance, Media } from "@versia/kit/db";
|
||||
import { Emojis, type Instances, type Medias } from "@versia/kit/tables";
|
||||
import { randomUUIDv7 } from "bun";
|
||||
|
|
@ -13,7 +17,6 @@ import {
|
|||
type SQL,
|
||||
} from "drizzle-orm";
|
||||
import type { z } from "zod";
|
||||
import { emojiValidatorWithColons, emojiValidatorWithIdentifiers } from "@/api";
|
||||
import * as VersiaEntities from "~/packages/sdk/entities/index.ts";
|
||||
import type { ImageContentFormatSchema } from "~/packages/sdk/schemas/index.ts";
|
||||
import { BaseInterface } from "./base.ts";
|
||||
|
|
@ -162,7 +165,7 @@ export class Emoji extends BaseInterface<typeof Emojis, EmojiType> {
|
|||
* @returns An array of emojis
|
||||
*/
|
||||
public static parseFromText(text: string): Promise<Emoji[]> {
|
||||
const matches = text.match(emojiValidatorWithColons);
|
||||
const matches = text.match(emojiWithColonsRegex);
|
||||
if (!matches || matches.length === 0) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
|
@ -213,9 +216,8 @@ export class Emoji extends BaseInterface<typeof Emojis, EmojiType> {
|
|||
instance: Instance,
|
||||
): Promise<Emoji> {
|
||||
// Extracts the shortcode from the emoji name (e.g. :shortcode: -> shortcode)
|
||||
const shortcode = [
|
||||
...emoji.name.matchAll(emojiValidatorWithIdentifiers),
|
||||
][0].groups.shortcode;
|
||||
const shortcode = [...emoji.name.matchAll(emojiWithIdentifiersRegex)][0]
|
||||
.groups.shortcode;
|
||||
|
||||
if (!shortcode) {
|
||||
throw new Error("Could not extract shortcode from emoji name");
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@
|
|||
"dependencies": {
|
||||
"@badgateway/oauth2-client": "^3.0.0",
|
||||
"iso-639-1": "^3.1.5",
|
||||
"magic-regexp": "^0.10.0",
|
||||
"zod": "^3.24.2",
|
||||
"zod-openapi": "^4.2.4"
|
||||
}
|
||||
|
|
|
|||
48
packages/client/regex.ts
Normal file
48
packages/client/regex.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import {
|
||||
anyOf,
|
||||
caseInsensitive,
|
||||
charIn,
|
||||
charNotIn,
|
||||
createRegExp,
|
||||
digit,
|
||||
exactly,
|
||||
global,
|
||||
letter,
|
||||
maybe,
|
||||
not,
|
||||
oneOrMore,
|
||||
} from "magic-regexp";
|
||||
|
||||
export const userAddressRegex = createRegExp(
|
||||
maybe("@"),
|
||||
oneOrMore(anyOf(letter.lowercase, digit, charIn("-_"))).groupedAs(
|
||||
"username",
|
||||
),
|
||||
maybe(
|
||||
exactly("@"),
|
||||
oneOrMore(anyOf(letter, digit, charIn("_-.:"))).groupedAs("domain"),
|
||||
),
|
||||
[global],
|
||||
);
|
||||
|
||||
export const emojiRegex = createRegExp(
|
||||
// A-Z a-z 0-9 _ -
|
||||
oneOrMore(letter.or(digit).or(charIn("_-"))),
|
||||
[caseInsensitive, global],
|
||||
);
|
||||
|
||||
export const emojiWithColonsRegex = createRegExp(
|
||||
exactly(":"),
|
||||
oneOrMore(letter.or(digit).or(charIn("_-"))),
|
||||
exactly(":"),
|
||||
[caseInsensitive, global],
|
||||
);
|
||||
|
||||
export const emojiWithIdentifiersRegex = createRegExp(
|
||||
exactly(
|
||||
exactly(not.letter.or(not.digit).or(charNotIn("_-"))).times(1),
|
||||
oneOrMore(letter.or(digit).or(charIn("_-"))).groupedAs("shortcode"),
|
||||
exactly(not.letter.or(not.digit).or(charNotIn("_-"))).times(1),
|
||||
),
|
||||
[caseInsensitive, global],
|
||||
);
|
||||
|
|
@ -1,3 +1,9 @@
|
|||
export {
|
||||
emojiRegex,
|
||||
emojiWithColonsRegex,
|
||||
emojiWithIdentifiersRegex,
|
||||
userAddressRegex,
|
||||
} from "./regex.ts";
|
||||
export { Account, Field, Source } from "./schemas/account.ts";
|
||||
export { AccountWarning } from "./schemas/account-warning.ts";
|
||||
export { Appeal } from "./schemas/appeal.ts";
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { z } from "zod";
|
||||
import { userAddressValidator } from "@/api.ts";
|
||||
import { config } from "~/config.ts";
|
||||
import { userAddressRegex } from "../regex.ts";
|
||||
import { iso631, zBoolean } from "./common.ts";
|
||||
import { CustomEmoji } from "./emoji.ts";
|
||||
import { Role } from "./versia.ts";
|
||||
|
|
@ -11,7 +10,6 @@ export const Field = 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",
|
||||
|
|
@ -23,7 +21,6 @@ export const Field = 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>",
|
||||
|
|
@ -87,14 +84,6 @@ export const Source = 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",
|
||||
|
|
@ -102,12 +91,9 @@ export const Source = z
|
|||
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.",
|
||||
}),
|
||||
fields: z.array(Field).openapi({
|
||||
description: "Metadata about the account.",
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
description:
|
||||
|
|
@ -135,25 +121,10 @@ const BaseAccount = 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.",
|
||||
|
|
@ -166,7 +137,7 @@ const BaseAccount = z
|
|||
.string()
|
||||
.min(1)
|
||||
.trim()
|
||||
.regex(userAddressValidator, "Invalid user address")
|
||||
.regex(userAddressRegex, "Invalid user address")
|
||||
.openapi({
|
||||
description:
|
||||
"The Webfinger account URI. Equal to username for local users, or username@domain for remote users.",
|
||||
|
|
@ -189,14 +160,6 @@ const BaseAccount = 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:",
|
||||
|
|
@ -207,15 +170,7 @@ const BaseAccount = z
|
|||
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>",
|
||||
|
|
@ -279,16 +234,13 @@ const BaseAccount = z
|
|||
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",
|
||||
},
|
||||
}),
|
||||
fields: z.array(Field).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.",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { z } from "zod";
|
||||
import { config } from "~/config.ts";
|
||||
import { Id } from "./common.ts";
|
||||
|
||||
export const Attachment = z
|
||||
|
|
@ -51,16 +50,11 @@ export const Attachment = z
|
|||
},
|
||||
},
|
||||
}),
|
||||
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",
|
||||
}),
|
||||
description: z.string().trim().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.",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { z } from "zod";
|
||||
import { emojiValidator } from "@/api.ts";
|
||||
import { config } from "~/config.ts";
|
||||
import { emojiRegex } from "../regex.ts";
|
||||
import { Id, zBoolean } from "./common.ts";
|
||||
|
||||
export const CustomEmoji = z
|
||||
|
|
@ -14,9 +13,8 @@ export const CustomEmoji = z
|
|||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(config.validation.emojis.max_shortcode_characters)
|
||||
.regex(
|
||||
emojiValidator,
|
||||
emojiRegex,
|
||||
"Shortcode must only contain letters (any case), numbers, dashes or underscores.",
|
||||
)
|
||||
.openapi({
|
||||
|
|
@ -76,7 +74,6 @@ export const CustomEmoji = z
|
|||
/* Versia Server API extension */
|
||||
description: z
|
||||
.string()
|
||||
.max(config.validation.emojis.max_description_characters)
|
||||
.nullable()
|
||||
.openapi({
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { z } from "zod";
|
||||
import pkg from "~/package.json";
|
||||
import { Account } from "./account.ts";
|
||||
import { iso631 } from "./common.ts";
|
||||
import { Rule } from "./rule.ts";
|
||||
|
|
@ -48,7 +47,7 @@ export const Instance = z
|
|||
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,
|
||||
example: "https://github.com/versia-pub/server",
|
||||
}),
|
||||
description: z.string().openapi({
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { z } from "zod";
|
||||
import { config } from "~/config.ts";
|
||||
import { Id } from "./common.ts";
|
||||
import { CustomEmoji } from "./emoji.ts";
|
||||
|
||||
|
|
@ -9,7 +8,6 @@ export const PollOption = z
|
|||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(config.validation.polls.max_option_characters)
|
||||
.openapi({
|
||||
description: "The text value of the poll option.",
|
||||
example: "yes",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { z } from "zod";
|
||||
import { config } from "~/config.ts";
|
||||
import { Account } from "./account.ts";
|
||||
import { Attachment } from "./attachment.ts";
|
||||
import { PreviewCard } from "./card.ts";
|
||||
|
|
@ -55,21 +54,10 @@ export const StatusSource = z
|
|||
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",
|
||||
}),
|
||||
text: z.string().trim().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.",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { z } from "zod";
|
||||
import { config } from "~/config.ts";
|
||||
import { Id } from "./common.ts";
|
||||
import { RolePermission } from "./permissions.ts";
|
||||
|
||||
|
|
@ -55,15 +54,10 @@ export const Role = z
|
|||
/* 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",
|
||||
}),
|
||||
name: z.string().min(1).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,
|
||||
|
|
|
|||
46
utils/api.ts
46
utils/api.ts
|
|
@ -14,14 +14,12 @@ import {
|
|||
anyOf,
|
||||
caseInsensitive,
|
||||
charIn,
|
||||
charNotIn,
|
||||
createRegExp,
|
||||
digit,
|
||||
exactly,
|
||||
global,
|
||||
letter,
|
||||
maybe,
|
||||
not,
|
||||
oneOrMore,
|
||||
} from "magic-regexp";
|
||||
import { type ParsedQs, parse } from "qs";
|
||||
|
|
@ -49,28 +47,6 @@ export const idValidator = createRegExp(
|
|||
[caseInsensitive],
|
||||
);
|
||||
|
||||
export const emojiValidator = createRegExp(
|
||||
// A-Z a-z 0-9 _ -
|
||||
oneOrMore(letter.or(digit).or(charIn("_-"))),
|
||||
[caseInsensitive, global],
|
||||
);
|
||||
|
||||
export const emojiValidatorWithColons = createRegExp(
|
||||
exactly(":"),
|
||||
oneOrMore(letter.or(digit).or(charIn("_-"))),
|
||||
exactly(":"),
|
||||
[caseInsensitive, global],
|
||||
);
|
||||
|
||||
export const emojiValidatorWithIdentifiers = createRegExp(
|
||||
exactly(
|
||||
exactly(not.letter.or(not.digit).or(charNotIn("_-"))).times(1),
|
||||
oneOrMore(letter.or(digit).or(charIn("_-"))).groupedAs("shortcode"),
|
||||
exactly(not.letter.or(not.digit).or(charNotIn("_-"))).times(1),
|
||||
),
|
||||
[caseInsensitive, global],
|
||||
);
|
||||
|
||||
export const mentionValidator = createRegExp(
|
||||
exactly("@"),
|
||||
oneOrMore(anyOf(letter.lowercase, digit, charIn("-_"))).groupedAs(
|
||||
|
|
@ -83,28 +59,6 @@ export const mentionValidator = createRegExp(
|
|||
[global],
|
||||
);
|
||||
|
||||
export const userAddressValidator = createRegExp(
|
||||
maybe("@"),
|
||||
oneOrMore(anyOf(letter.lowercase, digit, charIn("-_"))).groupedAs(
|
||||
"username",
|
||||
),
|
||||
maybe(
|
||||
exactly("@"),
|
||||
oneOrMore(anyOf(letter, digit, charIn("_-.:"))).groupedAs("domain"),
|
||||
),
|
||||
[global],
|
||||
);
|
||||
|
||||
export const userAddressValidatorRemote = createRegExp(
|
||||
maybe("@"),
|
||||
oneOrMore(anyOf(letter.lowercase, digit, charIn("-_"))).groupedAs(
|
||||
"username",
|
||||
),
|
||||
exactly("@"),
|
||||
oneOrMore(anyOf(letter, digit, charIn("_-.:"))).groupedAs("domain"),
|
||||
[global],
|
||||
);
|
||||
|
||||
export const webfingerMention = createRegExp(
|
||||
exactly("acct:"),
|
||||
oneOrMore(anyOf(letter, digit, charIn("-_"))).groupedAs("username"),
|
||||
|
|
|
|||
Loading…
Reference in a new issue