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

This commit is contained in:
Jesse Wierzbinski 2025-05-13 11:51:59 +02:00
parent c0060f1baf
commit 5dfcfc548f
No known key found for this signature in database
25 changed files with 256 additions and 260 deletions

View file

@ -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
View 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

View file

@ -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),

View file

@ -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(),

View file

@ -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,

View file

@ -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.",

View file

@ -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()

View file

@ -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({

View file

@ -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({

View file

@ -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()

View file

@ -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("@")) {

View file

@ -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",
},

View file

@ -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

View file

@ -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");

View file

@ -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
View 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],
);

View file

@ -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";

View file

@ -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 fields 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 profiles 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 profiles 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.",

View file

@ -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.",

View file

@ -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:

View file

@ -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:

View file

@ -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",

View file

@ -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 statuss subject or content warning.",

View file

@ -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,

View file

@ -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"),