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",
|
"json",
|
||||||
z
|
z
|
||||||
.object({
|
.object({
|
||||||
display_name: AccountSchema.shape.display_name.openapi({
|
display_name: AccountSchema.shape.display_name
|
||||||
description: "The display name to use for the profile.",
|
.openapi({
|
||||||
example: "Lexi",
|
description:
|
||||||
}),
|
"The display name to use for the profile.",
|
||||||
username: AccountSchema.shape.username.openapi({
|
example: "Lexi",
|
||||||
description: "The username to use for the profile.",
|
})
|
||||||
example: "lexi",
|
.max(
|
||||||
}),
|
config.validation.accounts
|
||||||
note: AccountSchema.shape.note.openapi({
|
.max_displayname_characters,
|
||||||
description: "The account bio. Markdown is supported.",
|
)
|
||||||
}),
|
.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
|
avatar: z
|
||||||
.string()
|
.string()
|
||||||
.url()
|
.url()
|
||||||
|
|
@ -150,10 +189,14 @@ export default apiRoute((app) =>
|
||||||
fields_attributes: z
|
fields_attributes: z
|
||||||
.array(
|
.array(
|
||||||
z.object({
|
z.object({
|
||||||
name: AccountSchema.shape.fields.element.shape
|
name: AccountSchema.shape.fields.element.shape.name.max(
|
||||||
.name,
|
config.validation.accounts
|
||||||
value: AccountSchema.shape.fields.element.shape
|
.max_field_name_characters,
|
||||||
.value,
|
),
|
||||||
|
value: AccountSchema.shape.fields.element.shape.value.max(
|
||||||
|
config.validation.accounts
|
||||||
|
.max_field_value_characters,
|
||||||
|
),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.max(config.validation.accounts.max_field_count),
|
.max(config.validation.accounts.max_field_count),
|
||||||
|
|
|
||||||
|
|
@ -113,7 +113,9 @@ export default apiRoute((app) => {
|
||||||
"json",
|
"json",
|
||||||
z
|
z
|
||||||
.object({
|
.object({
|
||||||
shortcode: CustomEmojiSchema.shape.shortcode,
|
shortcode: CustomEmojiSchema.shape.shortcode.max(
|
||||||
|
config.validation.emojis.max_shortcode_characters,
|
||||||
|
),
|
||||||
element: z
|
element: z
|
||||||
.string()
|
.string()
|
||||||
.url()
|
.url()
|
||||||
|
|
@ -136,7 +138,12 @@ export default apiRoute((app) => {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
category: CustomEmojiSchema.shape.category.optional(),
|
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),
|
global: CustomEmojiSchema.shape.global.default(false),
|
||||||
})
|
})
|
||||||
.partial(),
|
.partial(),
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,9 @@ export default apiRoute((app) =>
|
||||||
validator(
|
validator(
|
||||||
"json",
|
"json",
|
||||||
z.object({
|
z.object({
|
||||||
shortcode: CustomEmojiSchema.shape.shortcode,
|
shortcode: CustomEmojiSchema.shape.shortcode.max(
|
||||||
|
config.validation.emojis.max_shortcode_characters,
|
||||||
|
),
|
||||||
element: z
|
element: z
|
||||||
.string()
|
.string()
|
||||||
.url()
|
.url()
|
||||||
|
|
@ -68,7 +70,10 @@ export default apiRoute((app) =>
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
category: CustomEmojiSchema.shape.category.optional(),
|
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),
|
global: CustomEmojiSchema.shape.global.default(false),
|
||||||
}),
|
}),
|
||||||
handleZodError,
|
handleZodError,
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import { resolver, validator } from "hono-openapi/zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { apiRoute, auth, handleZodError } from "@/api";
|
import { apiRoute, auth, handleZodError } from "@/api";
|
||||||
import { ApiError } from "~/classes/errors/api-error";
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
|
import { config } from "~/config";
|
||||||
|
|
||||||
export default apiRoute((app) => {
|
export default apiRoute((app) => {
|
||||||
app.get(
|
app.get(
|
||||||
|
|
@ -109,7 +110,10 @@ export default apiRoute((app) => {
|
||||||
description:
|
description:
|
||||||
"The custom thumbnail of the media to be attached, encoded using multipart form data.",
|
"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({
|
focus: z.string().openapi({
|
||||||
description:
|
description:
|
||||||
"Two floating points (x,y), comma-delimited, ranging from -1.0 to 1.0. Used for media cropping on clients.",
|
"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 { z } from "zod";
|
||||||
import { apiRoute, auth, handleZodError } from "@/api";
|
import { apiRoute, auth, handleZodError } from "@/api";
|
||||||
import { ApiError } from "~/classes/errors/api-error";
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
|
import { config } from "~/config";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.post(
|
app.post(
|
||||||
|
|
@ -67,7 +68,10 @@ export default apiRoute((app) =>
|
||||||
description:
|
description:
|
||||||
"The custom thumbnail of the media to be attached, encoded using multipart form data.",
|
"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
|
focus: z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
|
|
|
||||||
|
|
@ -25,10 +25,20 @@ import * as VersiaEntities from "~/packages/sdk/entities";
|
||||||
|
|
||||||
const schema = z
|
const schema = z
|
||||||
.object({
|
.object({
|
||||||
status: StatusSourceSchema.shape.text.optional().openapi({
|
status: StatusSourceSchema.shape.text
|
||||||
description:
|
.max(config.validation.notes.max_characters)
|
||||||
"The text content of the status. If media_ids is provided, this becomes optional. Attaching a poll is optional while status is provided.",
|
.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 */
|
/* Versia Server API Extension */
|
||||||
content_type: z
|
content_type: z
|
||||||
.enum(["text/plain", "text/html", "text/markdown"])
|
.enum(["text/plain", "text/html", "text/markdown"])
|
||||||
|
|
@ -54,7 +64,11 @@ const schema = z
|
||||||
}),
|
}),
|
||||||
language: StatusSchema.shape.language.optional(),
|
language: StatusSchema.shape.language.optional(),
|
||||||
"poll[options]": z
|
"poll[options]": z
|
||||||
.array(PollOption.shape.title)
|
.array(
|
||||||
|
PollOption.shape.title.max(
|
||||||
|
config.validation.polls.max_option_characters,
|
||||||
|
),
|
||||||
|
)
|
||||||
.max(config.validation.polls.max_options)
|
.max(config.validation.polls.max_options)
|
||||||
.optional()
|
.optional()
|
||||||
.openapi({
|
.openapi({
|
||||||
|
|
|
||||||
|
|
@ -20,10 +20,20 @@ import * as VersiaEntities from "~/packages/sdk/entities";
|
||||||
|
|
||||||
const schema = z
|
const schema = z
|
||||||
.object({
|
.object({
|
||||||
status: StatusSourceSchema.shape.text.optional().openapi({
|
status: StatusSourceSchema.shape.text
|
||||||
description:
|
.max(config.validation.notes.max_characters)
|
||||||
"The text content of the status. If media_ids is provided, this becomes optional. Attaching a poll is optional while status is provided.",
|
.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 */
|
/* Versia Server API Extension */
|
||||||
content_type: z
|
content_type: z
|
||||||
.enum(["text/plain", "text/html", "text/markdown"])
|
.enum(["text/plain", "text/html", "text/markdown"])
|
||||||
|
|
@ -49,7 +59,11 @@ const schema = z
|
||||||
}),
|
}),
|
||||||
language: StatusSchema.shape.language.optional(),
|
language: StatusSchema.shape.language.optional(),
|
||||||
"poll[options]": z
|
"poll[options]": z
|
||||||
.array(PollOption.shape.title)
|
.array(
|
||||||
|
PollOption.shape.title.max(
|
||||||
|
config.validation.polls.max_option_characters,
|
||||||
|
),
|
||||||
|
)
|
||||||
.max(config.validation.polls.max_options)
|
.max(config.validation.polls.max_options)
|
||||||
.optional()
|
.optional()
|
||||||
.openapi({
|
.openapi({
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import { resolver, validator } from "hono-openapi/zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { apiRoute, auth, handleZodError } from "@/api";
|
import { apiRoute, auth, handleZodError } from "@/api";
|
||||||
import { ApiError } from "~/classes/errors/api-error";
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
|
import { config } from "~/config";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.post(
|
app.post(
|
||||||
|
|
@ -76,7 +77,10 @@ export default apiRoute((app) =>
|
||||||
description:
|
description:
|
||||||
"The custom thumbnail of the media to be attached, encoded using multipart form data.",
|
"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
|
focus: z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import {
|
||||||
Id,
|
Id,
|
||||||
RolePermission,
|
RolePermission,
|
||||||
Search as SearchSchema,
|
Search as SearchSchema,
|
||||||
|
userAddressRegex,
|
||||||
zBoolean,
|
zBoolean,
|
||||||
} from "@versia/client/schemas";
|
} from "@versia/client/schemas";
|
||||||
import { db, Note, User } from "@versia/kit/db";
|
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 { describeRoute } from "hono-openapi";
|
||||||
import { resolver, validator } from "hono-openapi/zod";
|
import { resolver, validator } from "hono-openapi/zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import {
|
import { apiRoute, auth, handleZodError, parseUserAddress } from "@/api";
|
||||||
apiRoute,
|
|
||||||
auth,
|
|
||||||
handleZodError,
|
|
||||||
parseUserAddress,
|
|
||||||
userAddressValidator,
|
|
||||||
} from "@/api";
|
|
||||||
import { ApiError } from "~/classes/errors/api-error";
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
import { searchManager } from "~/classes/search/search-manager";
|
import { searchManager } from "~/classes/search/search-manager";
|
||||||
import { config } from "~/config.ts";
|
import { config } from "~/config.ts";
|
||||||
|
|
@ -151,7 +146,7 @@ export default apiRoute((app) =>
|
||||||
|
|
||||||
if (!type || type === "accounts") {
|
if (!type || type === "accounts") {
|
||||||
// Check if q is matching format username@domain.com or @username@domain.com
|
// 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) {
|
if (accountMatches) {
|
||||||
// Remove leading @ if it exists
|
// Remove leading @ if it exists
|
||||||
if (accountMatches[0].startsWith("@")) {
|
if (accountMatches[0].startsWith("@")) {
|
||||||
|
|
|
||||||
1
bun.lock
1
bun.lock
|
|
@ -92,6 +92,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@badgateway/oauth2-client": "^3.0.0",
|
"@badgateway/oauth2-client": "^3.0.0",
|
||||||
"iso-639-1": "^3.1.5",
|
"iso-639-1": "^3.1.5",
|
||||||
|
"magic-regexp": "^0.10.0",
|
||||||
"zod": "^3.24.2",
|
"zod": "^3.24.2",
|
||||||
"zod-openapi": "^4.2.4",
|
"zod-openapi": "^4.2.4",
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,7 @@ import { type BunFile, env, file } from "bun";
|
||||||
import ISO6391 from "iso-639-1";
|
import ISO6391 from "iso-639-1";
|
||||||
import { types as mimeTypes } from "mime-types";
|
import { types as mimeTypes } from "mime-types";
|
||||||
import { generateVAPIDKeys } from "web-push";
|
import { generateVAPIDKeys } from "web-push";
|
||||||
import { ZodError, z } from "zod";
|
import { z } from "zod";
|
||||||
import { fromZodError } from "zod-validation-error";
|
|
||||||
import { ProxiableUrl } from "~/classes/media/url.ts";
|
import { ProxiableUrl } from "~/classes/media/url.ts";
|
||||||
import { RolePermission } from "~/packages/client/schemas/permissions.ts";
|
import { RolePermission } from "~/packages/client/schemas/permissions.ts";
|
||||||
|
|
||||||
|
|
@ -271,16 +270,6 @@ export const hmacKey = sensitiveString.transform(async (text, ctx) => {
|
||||||
return text;
|
return text;
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
|
||||||
console.info();
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof ZodError) {
|
|
||||||
throw fromZodError(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ConfigSchema = z
|
export const ConfigSchema = z
|
||||||
.strictObject({
|
.strictObject({
|
||||||
postgres: z
|
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 { db, type Instance, Media } from "@versia/kit/db";
|
||||||
import { Emojis, type Instances, type Medias } from "@versia/kit/tables";
|
import { Emojis, type Instances, type Medias } from "@versia/kit/tables";
|
||||||
import { randomUUIDv7 } from "bun";
|
import { randomUUIDv7 } from "bun";
|
||||||
|
|
@ -13,7 +17,6 @@ import {
|
||||||
type SQL,
|
type SQL,
|
||||||
} from "drizzle-orm";
|
} from "drizzle-orm";
|
||||||
import type { z } from "zod";
|
import type { z } from "zod";
|
||||||
import { emojiValidatorWithColons, emojiValidatorWithIdentifiers } from "@/api";
|
|
||||||
import * as VersiaEntities from "~/packages/sdk/entities/index.ts";
|
import * as VersiaEntities from "~/packages/sdk/entities/index.ts";
|
||||||
import type { ImageContentFormatSchema } from "~/packages/sdk/schemas/index.ts";
|
import type { ImageContentFormatSchema } from "~/packages/sdk/schemas/index.ts";
|
||||||
import { BaseInterface } from "./base.ts";
|
import { BaseInterface } from "./base.ts";
|
||||||
|
|
@ -162,7 +165,7 @@ export class Emoji extends BaseInterface<typeof Emojis, EmojiType> {
|
||||||
* @returns An array of emojis
|
* @returns An array of emojis
|
||||||
*/
|
*/
|
||||||
public static parseFromText(text: string): Promise<Emoji[]> {
|
public static parseFromText(text: string): Promise<Emoji[]> {
|
||||||
const matches = text.match(emojiValidatorWithColons);
|
const matches = text.match(emojiWithColonsRegex);
|
||||||
if (!matches || matches.length === 0) {
|
if (!matches || matches.length === 0) {
|
||||||
return Promise.resolve([]);
|
return Promise.resolve([]);
|
||||||
}
|
}
|
||||||
|
|
@ -213,9 +216,8 @@ export class Emoji extends BaseInterface<typeof Emojis, EmojiType> {
|
||||||
instance: Instance,
|
instance: Instance,
|
||||||
): Promise<Emoji> {
|
): Promise<Emoji> {
|
||||||
// Extracts the shortcode from the emoji name (e.g. :shortcode: -> shortcode)
|
// Extracts the shortcode from the emoji name (e.g. :shortcode: -> shortcode)
|
||||||
const shortcode = [
|
const shortcode = [...emoji.name.matchAll(emojiWithIdentifiersRegex)][0]
|
||||||
...emoji.name.matchAll(emojiValidatorWithIdentifiers),
|
.groups.shortcode;
|
||||||
][0].groups.shortcode;
|
|
||||||
|
|
||||||
if (!shortcode) {
|
if (!shortcode) {
|
||||||
throw new Error("Could not extract shortcode from emoji name");
|
throw new Error("Could not extract shortcode from emoji name");
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@badgateway/oauth2-client": "^3.0.0",
|
"@badgateway/oauth2-client": "^3.0.0",
|
||||||
"iso-639-1": "^3.1.5",
|
"iso-639-1": "^3.1.5",
|
||||||
|
"magic-regexp": "^0.10.0",
|
||||||
"zod": "^3.24.2",
|
"zod": "^3.24.2",
|
||||||
"zod-openapi": "^4.2.4"
|
"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 { Account, Field, Source } from "./schemas/account.ts";
|
||||||
export { AccountWarning } from "./schemas/account-warning.ts";
|
export { AccountWarning } from "./schemas/account-warning.ts";
|
||||||
export { Appeal } from "./schemas/appeal.ts";
|
export { Appeal } from "./schemas/appeal.ts";
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { userAddressValidator } from "@/api.ts";
|
import { userAddressRegex } from "../regex.ts";
|
||||||
import { config } from "~/config.ts";
|
|
||||||
import { iso631, zBoolean } from "./common.ts";
|
import { iso631, zBoolean } from "./common.ts";
|
||||||
import { CustomEmoji } from "./emoji.ts";
|
import { CustomEmoji } from "./emoji.ts";
|
||||||
import { Role } from "./versia.ts";
|
import { Role } from "./versia.ts";
|
||||||
|
|
@ -11,7 +10,6 @@ export const Field = z
|
||||||
.string()
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
.min(1)
|
.min(1)
|
||||||
.max(config.validation.accounts.max_field_name_characters)
|
|
||||||
.openapi({
|
.openapi({
|
||||||
description: "The key of a given field’s key-value pair.",
|
description: "The key of a given field’s key-value pair.",
|
||||||
example: "Freak level",
|
example: "Freak level",
|
||||||
|
|
@ -23,7 +21,6 @@ export const Field = z
|
||||||
.string()
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
.min(1)
|
.min(1)
|
||||||
.max(config.validation.accounts.max_field_value_characters)
|
|
||||||
.openapi({
|
.openapi({
|
||||||
description: "The value associated with the name key.",
|
description: "The value associated with the name key.",
|
||||||
example: "<p>High</p>",
|
example: "<p>High</p>",
|
||||||
|
|
@ -87,14 +84,6 @@ export const Source = z
|
||||||
.string()
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
.min(0)
|
.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({
|
.openapi({
|
||||||
description: "Profile bio, in plain-text instead of in HTML.",
|
description: "Profile bio, in plain-text instead of in HTML.",
|
||||||
example: "ermmm what the meow meow",
|
example: "ermmm what the meow meow",
|
||||||
|
|
@ -102,12 +91,9 @@ export const Source = z
|
||||||
url: "https://docs.joinmastodon.org/entities/Account/#source-note",
|
url: "https://docs.joinmastodon.org/entities/Account/#source-note",
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
fields: z
|
fields: z.array(Field).openapi({
|
||||||
.array(Field)
|
description: "Metadata about the account.",
|
||||||
.max(config.validation.accounts.max_field_count)
|
}),
|
||||||
.openapi({
|
|
||||||
description: "Metadata about the account.",
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
.openapi({
|
.openapi({
|
||||||
description:
|
description:
|
||||||
|
|
@ -135,25 +121,10 @@ const BaseAccount = z
|
||||||
.string()
|
.string()
|
||||||
.min(3)
|
.min(3)
|
||||||
.trim()
|
.trim()
|
||||||
.max(config.validation.accounts.max_username_characters)
|
|
||||||
.regex(
|
.regex(
|
||||||
/^[a-z0-9_-]+$/,
|
/^[a-z0-9_-]+$/,
|
||||||
"Username can only contain letters, numbers, underscores and hyphens",
|
"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({
|
.openapi({
|
||||||
description:
|
description:
|
||||||
"The username of the account, not including domain.",
|
"The username of the account, not including domain.",
|
||||||
|
|
@ -166,7 +137,7 @@ const BaseAccount = z
|
||||||
.string()
|
.string()
|
||||||
.min(1)
|
.min(1)
|
||||||
.trim()
|
.trim()
|
||||||
.regex(userAddressValidator, "Invalid user address")
|
.regex(userAddressRegex, "Invalid user address")
|
||||||
.openapi({
|
.openapi({
|
||||||
description:
|
description:
|
||||||
"The Webfinger account URI. Equal to username for local users, or username@domain for remote users.",
|
"The Webfinger account URI. Equal to username for local users, or username@domain for remote users.",
|
||||||
|
|
@ -189,14 +160,6 @@ const BaseAccount = z
|
||||||
.string()
|
.string()
|
||||||
.min(3)
|
.min(3)
|
||||||
.trim()
|
.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({
|
.openapi({
|
||||||
description: "The profile’s display name.",
|
description: "The profile’s display name.",
|
||||||
example: "Lexi :flower:",
|
example: "Lexi :flower:",
|
||||||
|
|
@ -207,15 +170,7 @@ const BaseAccount = z
|
||||||
note: z
|
note: z
|
||||||
.string()
|
.string()
|
||||||
.min(0)
|
.min(0)
|
||||||
.max(config.validation.accounts.max_bio_characters)
|
|
||||||
.trim()
|
.trim()
|
||||||
.refine(
|
|
||||||
(s) =>
|
|
||||||
!config.validation.filters.bio.some((filter) =>
|
|
||||||
filter.test(s),
|
|
||||||
),
|
|
||||||
"Bio contains blocked words",
|
|
||||||
)
|
|
||||||
.openapi({
|
.openapi({
|
||||||
description: "The profile’s bio or description.",
|
description: "The profile’s bio or description.",
|
||||||
example: "<p>ermmm what the meow meow</p>",
|
example: "<p>ermmm what the meow meow</p>",
|
||||||
|
|
@ -279,16 +234,13 @@ const BaseAccount = z
|
||||||
url: "https://docs.joinmastodon.org/entities/Account/#locked",
|
url: "https://docs.joinmastodon.org/entities/Account/#locked",
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
fields: z
|
fields: z.array(Field).openapi({
|
||||||
.array(Field)
|
description:
|
||||||
.max(config.validation.accounts.max_field_count)
|
"Additional metadata attached to a profile as name-value pairs.",
|
||||||
.openapi({
|
externalDocs: {
|
||||||
description:
|
url: "https://docs.joinmastodon.org/entities/Account/#fields",
|
||||||
"Additional metadata attached to a profile as name-value pairs.",
|
},
|
||||||
externalDocs: {
|
}),
|
||||||
url: "https://docs.joinmastodon.org/entities/Account/#fields",
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
emojis: z.array(CustomEmoji).openapi({
|
emojis: z.array(CustomEmoji).openapi({
|
||||||
description:
|
description:
|
||||||
"Custom emoji entities to be used when rendering the profile.",
|
"Custom emoji entities to be used when rendering the profile.",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { config } from "~/config.ts";
|
|
||||||
import { Id } from "./common.ts";
|
import { Id } from "./common.ts";
|
||||||
|
|
||||||
export const Attachment = z
|
export const Attachment = z
|
||||||
|
|
@ -51,16 +50,11 @@ export const Attachment = z
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
description: z
|
description: z.string().trim().nullable().openapi({
|
||||||
.string()
|
description:
|
||||||
.trim()
|
"Alternate text that describes what is in the media attachment, to be used for the visually impaired or when media attachments do not load.",
|
||||||
.max(config.validation.media.max_description_characters)
|
example: "test media description",
|
||||||
.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({
|
blurhash: z.string().nullable().openapi({
|
||||||
description:
|
description:
|
||||||
"A hash computed by the BlurHash algorithm, for generating colorful preview thumbnails when media has not been downloaded yet.",
|
"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 { z } from "zod";
|
||||||
import { emojiValidator } from "@/api.ts";
|
import { emojiRegex } from "../regex.ts";
|
||||||
import { config } from "~/config.ts";
|
|
||||||
import { Id, zBoolean } from "./common.ts";
|
import { Id, zBoolean } from "./common.ts";
|
||||||
|
|
||||||
export const CustomEmoji = z
|
export const CustomEmoji = z
|
||||||
|
|
@ -14,9 +13,8 @@ export const CustomEmoji = z
|
||||||
.string()
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
.min(1)
|
.min(1)
|
||||||
.max(config.validation.emojis.max_shortcode_characters)
|
|
||||||
.regex(
|
.regex(
|
||||||
emojiValidator,
|
emojiRegex,
|
||||||
"Shortcode must only contain letters (any case), numbers, dashes or underscores.",
|
"Shortcode must only contain letters (any case), numbers, dashes or underscores.",
|
||||||
)
|
)
|
||||||
.openapi({
|
.openapi({
|
||||||
|
|
@ -76,7 +74,6 @@ export const CustomEmoji = z
|
||||||
/* Versia Server API extension */
|
/* Versia Server API extension */
|
||||||
description: z
|
description: z
|
||||||
.string()
|
.string()
|
||||||
.max(config.validation.emojis.max_description_characters)
|
|
||||||
.nullable()
|
.nullable()
|
||||||
.openapi({
|
.openapi({
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import pkg from "~/package.json";
|
|
||||||
import { Account } from "./account.ts";
|
import { Account } from "./account.ts";
|
||||||
import { iso631 } from "./common.ts";
|
import { iso631 } from "./common.ts";
|
||||||
import { Rule } from "./rule.ts";
|
import { Rule } from "./rule.ts";
|
||||||
|
|
@ -48,7 +47,7 @@ export const Instance = z
|
||||||
source_url: z.string().url().openapi({
|
source_url: z.string().url().openapi({
|
||||||
description:
|
description:
|
||||||
"The URL for the source code of the software running on this instance, in keeping with AGPL license requirements.",
|
"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: z.string().openapi({
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { config } from "~/config.ts";
|
|
||||||
import { Id } from "./common.ts";
|
import { Id } from "./common.ts";
|
||||||
import { CustomEmoji } from "./emoji.ts";
|
import { CustomEmoji } from "./emoji.ts";
|
||||||
|
|
||||||
|
|
@ -9,7 +8,6 @@ export const PollOption = z
|
||||||
.string()
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
.min(1)
|
.min(1)
|
||||||
.max(config.validation.polls.max_option_characters)
|
|
||||||
.openapi({
|
.openapi({
|
||||||
description: "The text value of the poll option.",
|
description: "The text value of the poll option.",
|
||||||
example: "yes",
|
example: "yes",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { config } from "~/config.ts";
|
|
||||||
import { Account } from "./account.ts";
|
import { Account } from "./account.ts";
|
||||||
import { Attachment } from "./attachment.ts";
|
import { Attachment } from "./attachment.ts";
|
||||||
import { PreviewCard } from "./card.ts";
|
import { PreviewCard } from "./card.ts";
|
||||||
|
|
@ -55,21 +54,10 @@ export const StatusSource = z
|
||||||
description: "ID of the status in the database.",
|
description: "ID of the status in the database.",
|
||||||
example: "c7db92a4-e472-4e94-a115-7411ee934ba1",
|
example: "c7db92a4-e472-4e94-a115-7411ee934ba1",
|
||||||
}),
|
}),
|
||||||
text: z
|
text: z.string().trim().openapi({
|
||||||
.string()
|
description: "The plain text used to compose the status.",
|
||||||
.max(config.validation.notes.max_characters)
|
example: "this is a status that will be edited",
|
||||||
.trim()
|
}),
|
||||||
.refine(
|
|
||||||
(s) =>
|
|
||||||
!config.validation.filters.note_content.some((filter) =>
|
|
||||||
filter.test(s),
|
|
||||||
),
|
|
||||||
"Status contains blocked words",
|
|
||||||
)
|
|
||||||
.openapi({
|
|
||||||
description: "The plain text used to compose the status.",
|
|
||||||
example: "this is a status that will be edited",
|
|
||||||
}),
|
|
||||||
spoiler_text: z.string().trim().min(1).max(1024).openapi({
|
spoiler_text: z.string().trim().min(1).max(1024).openapi({
|
||||||
description:
|
description:
|
||||||
"The plain text used to compose the status’s subject or content warning.",
|
"The plain text used to compose the status’s subject or content warning.",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { config } from "~/config.ts";
|
|
||||||
import { Id } from "./common.ts";
|
import { Id } from "./common.ts";
|
||||||
import { RolePermission } from "./permissions.ts";
|
import { RolePermission } from "./permissions.ts";
|
||||||
|
|
||||||
|
|
@ -55,15 +54,10 @@ export const Role = z
|
||||||
/* Versia Server API extension */
|
/* Versia Server API extension */
|
||||||
export const NoteReaction = z
|
export const NoteReaction = z
|
||||||
.object({
|
.object({
|
||||||
name: z
|
name: z.string().min(1).trim().openapi({
|
||||||
.string()
|
description: "Custom Emoji shortcode or Unicode emoji.",
|
||||||
.min(1)
|
example: "blobfox_coffee",
|
||||||
.max(config.validation.emojis.max_shortcode_characters)
|
}),
|
||||||
.trim()
|
|
||||||
.openapi({
|
|
||||||
description: "Custom Emoji shortcode or Unicode emoji.",
|
|
||||||
example: "blobfox_coffee",
|
|
||||||
}),
|
|
||||||
count: z.number().int().nonnegative().openapi({
|
count: z.number().int().nonnegative().openapi({
|
||||||
description: "Number of users who reacted with this emoji.",
|
description: "Number of users who reacted with this emoji.",
|
||||||
example: 5,
|
example: 5,
|
||||||
|
|
|
||||||
46
utils/api.ts
46
utils/api.ts
|
|
@ -14,14 +14,12 @@ import {
|
||||||
anyOf,
|
anyOf,
|
||||||
caseInsensitive,
|
caseInsensitive,
|
||||||
charIn,
|
charIn,
|
||||||
charNotIn,
|
|
||||||
createRegExp,
|
createRegExp,
|
||||||
digit,
|
digit,
|
||||||
exactly,
|
exactly,
|
||||||
global,
|
global,
|
||||||
letter,
|
letter,
|
||||||
maybe,
|
maybe,
|
||||||
not,
|
|
||||||
oneOrMore,
|
oneOrMore,
|
||||||
} from "magic-regexp";
|
} from "magic-regexp";
|
||||||
import { type ParsedQs, parse } from "qs";
|
import { type ParsedQs, parse } from "qs";
|
||||||
|
|
@ -49,28 +47,6 @@ export const idValidator = createRegExp(
|
||||||
[caseInsensitive],
|
[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(
|
export const mentionValidator = createRegExp(
|
||||||
exactly("@"),
|
exactly("@"),
|
||||||
oneOrMore(anyOf(letter.lowercase, digit, charIn("-_"))).groupedAs(
|
oneOrMore(anyOf(letter.lowercase, digit, charIn("-_"))).groupedAs(
|
||||||
|
|
@ -83,28 +59,6 @@ export const mentionValidator = createRegExp(
|
||||||
[global],
|
[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(
|
export const webfingerMention = createRegExp(
|
||||||
exactly("acct:"),
|
exactly("acct:"),
|
||||||
oneOrMore(anyOf(letter, digit, charIn("-_"))).groupedAs("username"),
|
oneOrMore(anyOf(letter, digit, charIn("-_"))).groupedAs("username"),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue