mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 08:28:19 +01:00
chore(federation): 👽 Finish initial Versia Working Draft 4 update
This commit is contained in:
parent
c3fa867e74
commit
42e198ca0e
|
|
@ -41,9 +41,7 @@ export default class FederationUserFetch extends BaseCommand<
|
||||||
// Check instance exists, if not, create it
|
// Check instance exists, if not, create it
|
||||||
await Instance.resolve(`https://${host}`);
|
await Instance.resolve(`https://${host}`);
|
||||||
|
|
||||||
const requester = await User.getServerActor();
|
const manager = await User.getFederationRequester();
|
||||||
|
|
||||||
const manager = await requester.getFederationRequester();
|
|
||||||
|
|
||||||
const uri = await User.webFinger(manager, username, host);
|
const uri = await User.webFinger(manager, username, host);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,9 +41,7 @@ export default class FederationUserFinger extends BaseCommand<
|
||||||
// Check instance exists, if not, create it
|
// Check instance exists, if not, create it
|
||||||
await Instance.resolve(`https://${host}`);
|
await Instance.resolve(`https://${host}`);
|
||||||
|
|
||||||
const requester = await User.getServerActor();
|
const manager = await User.getFederationRequester();
|
||||||
|
|
||||||
const manager = await requester.getFederationRequester();
|
|
||||||
|
|
||||||
const uri = await User.webFinger(manager, username, host);
|
const uri = await User.webFinger(manager, username, host);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { emojiValidatorWithColons } from "@/api";
|
import { emojiValidatorWithColons, emojiValidatorWithIdentifiers } from "@/api";
|
||||||
import { proxyUrl } from "@/response";
|
import { proxyUrl } from "@/response";
|
||||||
import type { Emoji as ApiEmoji } from "@lysand-org/client/types";
|
import type { Emoji as ApiEmoji } from "@lysand-org/client/types";
|
||||||
import type { CustomEmojiExtension } from "@versia/federation/types";
|
import type { CustomEmojiExtension } from "@versia/federation/types";
|
||||||
|
|
@ -191,7 +191,7 @@ export class Emoji extends BaseInterface<typeof Emojis, EmojiWithInstance> {
|
||||||
|
|
||||||
public toVersia(): CustomEmojiExtension["emojis"][0] {
|
public toVersia(): CustomEmojiExtension["emojis"][0] {
|
||||||
return {
|
return {
|
||||||
name: this.data.shortcode,
|
name: `:${this.data.shortcode}:`,
|
||||||
url: {
|
url: {
|
||||||
[this.data.contentType]: {
|
[this.data.contentType]: {
|
||||||
content: this.data.url,
|
content: this.data.url,
|
||||||
|
|
@ -206,8 +206,17 @@ export class Emoji extends BaseInterface<typeof Emojis, EmojiWithInstance> {
|
||||||
emoji: CustomEmojiExtension["emojis"][0],
|
emoji: CustomEmojiExtension["emojis"][0],
|
||||||
instanceId: string | null,
|
instanceId: string | null,
|
||||||
): Promise<Emoji> {
|
): Promise<Emoji> {
|
||||||
|
// Extracts the shortcode from the emoji name (e.g. :shortcode: -> shortcode)
|
||||||
|
const shortcode = [
|
||||||
|
...emoji.name.matchAll(emojiValidatorWithIdentifiers),
|
||||||
|
][0].groups.shortcode;
|
||||||
|
|
||||||
|
if (!shortcode) {
|
||||||
|
throw new Error("Could not extract shortcode from emoji name");
|
||||||
|
}
|
||||||
|
|
||||||
return Emoji.insert({
|
return Emoji.insert({
|
||||||
shortcode: emoji.name,
|
shortcode,
|
||||||
url: Object.entries(emoji.url)[0][1].content,
|
url: Object.entries(emoji.url)[0][1].content,
|
||||||
alt: Object.entries(emoji.url)[0][1].description || undefined,
|
alt: Object.entries(emoji.url)[0][1].description || undefined,
|
||||||
contentType: Object.keys(emoji.url)[0],
|
contentType: Object.keys(emoji.url)[0],
|
||||||
|
|
|
||||||
|
|
@ -139,7 +139,7 @@ export class Instance extends BaseInterface<typeof Instances> {
|
||||||
const wellKnownUrl = new URL("/.well-known/versia", origin);
|
const wellKnownUrl = new URL("/.well-known/versia", origin);
|
||||||
const logger = getLogger("federation");
|
const logger = getLogger("federation");
|
||||||
|
|
||||||
const requester = await User.getServerActor().getFederationRequester();
|
const requester = await User.getFederationRequester();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { ok, raw, data } = await requester
|
const { ok, raw, data } = await requester
|
||||||
|
|
@ -195,7 +195,7 @@ export class Instance extends BaseInterface<typeof Instances> {
|
||||||
// Go to endpoint, then follow the links to the actual metadata
|
// Go to endpoint, then follow the links to the actual metadata
|
||||||
|
|
||||||
const logger = getLogger("federation");
|
const logger = getLogger("federation");
|
||||||
const requester = await User.getServerActor().getFederationRequester();
|
const requester = await User.getFederationRequester();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
|
|
|
||||||
|
|
@ -602,8 +602,7 @@ export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
|
||||||
throw new Error(`Invalid URI to parse ${uri}`);
|
throw new Error(`Invalid URI to parse ${uri}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const requester =
|
const requester = await User.getFederationRequester();
|
||||||
await User.getServerActor().getFederationRequester();
|
|
||||||
|
|
||||||
const { data } = await requester.get(uri, {
|
const { data } = await requester.get(uri, {
|
||||||
// @ts-expect-error Bun extension
|
// @ts-expect-error Bun extension
|
||||||
|
|
@ -636,7 +635,7 @@ export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
|
||||||
const emojis: Emoji[] = [];
|
const emojis: Emoji[] = [];
|
||||||
const logger = getLogger("federation");
|
const logger = getLogger("federation");
|
||||||
|
|
||||||
for (const emoji of note.extensions?.["org.lysand:custom_emojis"]
|
for (const emoji of note.extensions?.["pub.versia:custom_emojis"]
|
||||||
?.emojis ?? []) {
|
?.emojis ?? []) {
|
||||||
const resolvedEmoji = await Emoji.fetchFromRemote(emoji).catch(
|
const resolvedEmoji = await Emoji.fetchFromRemote(emoji).catch(
|
||||||
(e) => {
|
(e) => {
|
||||||
|
|
@ -948,7 +947,7 @@ export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
|
||||||
// TODO: Refactor as part of groups
|
// TODO: Refactor as part of groups
|
||||||
group: status.visibility === "public" ? "public" : "followers",
|
group: status.visibility === "public" ? "public" : "followers",
|
||||||
extensions: {
|
extensions: {
|
||||||
"org.lysand:custom_emojis": {
|
"pub.versia:custom_emojis": {
|
||||||
emojis: status.emojis.map((emoji) =>
|
emojis: status.emojis.map((emoji) =>
|
||||||
new Emoji(emoji).toVersia(),
|
new Emoji(emoji).toVersia(),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -135,56 +135,6 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static getServerActor(): User {
|
|
||||||
return new User({
|
|
||||||
id: "00000000-0000-0000-0000-000000000000",
|
|
||||||
username: "actor",
|
|
||||||
avatar: "",
|
|
||||||
createdAt: "2024-01-01T00:00:00.000Z",
|
|
||||||
displayName: "Server Actor",
|
|
||||||
note: "This is a system actor used for server-to-server communication. It is not a real user.",
|
|
||||||
updatedAt: "2024-01-01T00:00:00.000Z",
|
|
||||||
instanceId: null,
|
|
||||||
publicKey: config.instance.keys.public,
|
|
||||||
source: {
|
|
||||||
fields: [],
|
|
||||||
language: null,
|
|
||||||
note: "",
|
|
||||||
privacy: "public",
|
|
||||||
sensitive: false,
|
|
||||||
},
|
|
||||||
fields: [],
|
|
||||||
isAdmin: false,
|
|
||||||
isBot: false,
|
|
||||||
isLocked: false,
|
|
||||||
isDiscoverable: false,
|
|
||||||
endpoints: {
|
|
||||||
dislikes: "",
|
|
||||||
featured: "",
|
|
||||||
likes: "",
|
|
||||||
followers: "",
|
|
||||||
following: "",
|
|
||||||
inbox: "",
|
|
||||||
outbox: "",
|
|
||||||
},
|
|
||||||
disableAutomoderation: false,
|
|
||||||
email: "",
|
|
||||||
emailVerificationToken: "",
|
|
||||||
emojis: [],
|
|
||||||
followerCount: 0,
|
|
||||||
followingCount: 0,
|
|
||||||
header: "",
|
|
||||||
instance: null,
|
|
||||||
password: "",
|
|
||||||
passwordResetToken: "",
|
|
||||||
privateKey: config.instance.keys.private,
|
|
||||||
roles: [],
|
|
||||||
sanctions: [],
|
|
||||||
statusCount: 0,
|
|
||||||
uri: "/users/actor",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static getUri(id: string, uri: string | null, baseUrl: string) {
|
static getUri(id: string, uri: string | null, baseUrl: string) {
|
||||||
return uri || new URL(`/users/${id}`, baseUrl).toString();
|
return uri || new URL(`/users/${id}`, baseUrl).toString();
|
||||||
}
|
}
|
||||||
|
|
@ -433,7 +383,7 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
uri: string,
|
uri: string,
|
||||||
instance: Instance,
|
instance: Instance,
|
||||||
): Promise<User> {
|
): Promise<User> {
|
||||||
const requester = await User.getServerActor().getFederationRequester();
|
const requester = await User.getFederationRequester();
|
||||||
const { data: json } = await requester.get<Partial<VersiaUser>>(uri, {
|
const { data: json } = await requester.get<Partial<VersiaUser>>(uri, {
|
||||||
// @ts-expect-error Bun extension
|
// @ts-expect-error Bun extension
|
||||||
proxy: config.http.proxy.address,
|
proxy: config.http.proxy.address,
|
||||||
|
|
@ -749,6 +699,20 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to get the appropriate Versia SDK requester with the instance's private key
|
||||||
|
*
|
||||||
|
* @returns The requester
|
||||||
|
*/
|
||||||
|
static async getFederationRequester(): Promise<FederationRequester> {
|
||||||
|
const signatureConstructor = await SignatureConstructor.fromStringKey(
|
||||||
|
config.instance.keys.private,
|
||||||
|
config.http.base_url,
|
||||||
|
);
|
||||||
|
|
||||||
|
return new FederationRequester(signatureConstructor);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper to get the appropriate Versia SDK requester with this user's private key
|
* Helper to get the appropriate Versia SDK requester with this user's private key
|
||||||
*
|
*
|
||||||
|
|
@ -947,6 +911,7 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
).toString(),
|
).toString(),
|
||||||
indexable: false,
|
indexable: false,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
|
manually_approves_followers: this.data.isLocked,
|
||||||
avatar: urlToContentFormat(this.getAvatarUrl(config)) ?? undefined,
|
avatar: urlToContentFormat(this.getAvatarUrl(config)) ?? undefined,
|
||||||
header: urlToContentFormat(this.getHeaderUrl(config)) ?? undefined,
|
header: urlToContentFormat(this.getHeaderUrl(config)) ?? undefined,
|
||||||
display_name: user.displayName,
|
display_name: user.displayName,
|
||||||
|
|
@ -960,7 +925,7 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
algorithm: "ed25519",
|
algorithm: "ed25519",
|
||||||
},
|
},
|
||||||
extensions: {
|
extensions: {
|
||||||
"org.lysand:custom_emojis": {
|
"pub.versia:custom_emojis": {
|
||||||
emojis: user.emojis.map((emoji) =>
|
emojis: user.emojis.map((emoji) =>
|
||||||
new Emoji(emoji).toVersia(),
|
new Emoji(emoji).toVersia(),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -76,9 +76,7 @@ export default apiRoute((app) =>
|
||||||
|
|
||||||
const [username, domain] = accountMatches[0].split("@");
|
const [username, domain] = accountMatches[0].split("@");
|
||||||
|
|
||||||
const requester = user ?? User.getServerActor();
|
const manager = await (user ?? User).getFederationRequester();
|
||||||
|
|
||||||
const manager = await requester.getFederationRequester();
|
|
||||||
|
|
||||||
const uri = await User.webFinger(manager, username, domain);
|
const uri = await User.webFinger(manager, username, domain);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -87,9 +87,7 @@ export default apiRoute((app) =>
|
||||||
const accounts: User[] = [];
|
const accounts: User[] = [];
|
||||||
|
|
||||||
if (resolve && username && host) {
|
if (resolve && username && host) {
|
||||||
const requester = self ?? User.getServerActor();
|
const manager = await (self ?? User).getFederationRequester();
|
||||||
|
|
||||||
const manager = await requester.getFederationRequester();
|
|
||||||
|
|
||||||
const uri = await User.webFinger(manager, username, host);
|
const uri = await User.webFinger(manager, username, host);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -128,10 +128,9 @@ export default apiRoute((app) =>
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resolve) {
|
if (resolve) {
|
||||||
const requester = self ?? User.getServerActor();
|
const manager = await (
|
||||||
|
self ?? User
|
||||||
const manager =
|
).getFederationRequester();
|
||||||
await requester.getFederationRequester();
|
|
||||||
|
|
||||||
const uri = await User.webFinger(
|
const uri = await User.webFinger(
|
||||||
manager,
|
manager,
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,10 @@ export default apiRoute((app) =>
|
||||||
return context.json({ error: "Object not found" }, 404);
|
return context.json({ error: "Object not found" }, 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!foundAuthor) {
|
||||||
|
return context.json({ error: "Author not found" }, 404);
|
||||||
|
}
|
||||||
|
|
||||||
if (foundAuthor?.isRemote()) {
|
if (foundAuthor?.isRemote()) {
|
||||||
return context.json(
|
return context.json(
|
||||||
{ error: "Cannot view objects from remote instances" },
|
{ error: "Cannot view objects from remote instances" },
|
||||||
|
|
@ -92,9 +96,11 @@ export default apiRoute((app) =>
|
||||||
reqUrl.protocol = "https:";
|
reqUrl.protocol = "https:";
|
||||||
}
|
}
|
||||||
|
|
||||||
const author = foundAuthor ?? User.getServerActor();
|
const { headers } = await foundAuthor.sign(
|
||||||
|
apiObject,
|
||||||
const { headers } = await author.sign(apiObject, reqUrl, "GET");
|
reqUrl,
|
||||||
|
"GET",
|
||||||
|
);
|
||||||
|
|
||||||
return response(objectString, 200, {
|
return response(objectString, 200, {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
import { apiRoute, applyConfig, handleZodError } from "@/api";
|
import { apiRoute, applyConfig, handleZodError } from "@/api";
|
||||||
import { redirect, response } from "@/response";
|
import { redirect } from "@/response";
|
||||||
import { zValidator } from "@hono/zod-validator";
|
import { zValidator } from "@hono/zod-validator";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { config } from "~/packages/config-manager";
|
|
||||||
import { User } from "~/packages/database-interface/user";
|
import { User } from "~/packages/database-interface/user";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
|
|
@ -19,7 +18,7 @@ export const meta = applyConfig({
|
||||||
|
|
||||||
export const schemas = {
|
export const schemas = {
|
||||||
param: z.object({
|
param: z.object({
|
||||||
uuid: z.string().uuid().or(z.literal("actor")),
|
uuid: z.string().uuid(),
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -31,10 +30,7 @@ export default apiRoute((app) =>
|
||||||
async (context) => {
|
async (context) => {
|
||||||
const { uuid } = context.req.valid("param");
|
const { uuid } = context.req.valid("param");
|
||||||
|
|
||||||
const user =
|
const user = await User.fromId(uuid);
|
||||||
uuid === "actor"
|
|
||||||
? User.getServerActor()
|
|
||||||
: await User.fromId(uuid);
|
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return context.json({ error: "User not found" }, 404);
|
return context.json({ error: "User not found" }, 404);
|
||||||
|
|
@ -55,24 +51,15 @@ export default apiRoute((app) =>
|
||||||
return redirect(user.toApi().url);
|
return redirect(user.toApi().url);
|
||||||
}
|
}
|
||||||
|
|
||||||
const userString = JSON.stringify(user.toVersia());
|
const userJson = user.toVersia();
|
||||||
|
|
||||||
// If base_url uses https and request uses http, rewrite request to use https
|
const { headers } = await user.sign(
|
||||||
// This fixes reverse proxy errors
|
userJson,
|
||||||
const reqUrl = new URL(context.req.url);
|
context.req.url,
|
||||||
if (
|
"GET",
|
||||||
new URL(config.http.base_url).protocol === "https:" &&
|
);
|
||||||
reqUrl.protocol === "http:"
|
|
||||||
) {
|
|
||||||
reqUrl.protocol = "https:";
|
|
||||||
}
|
|
||||||
|
|
||||||
const { headers } = await user.sign(user.toVersia(), reqUrl, "GET");
|
return context.json(userJson, 200, headers.toJSON());
|
||||||
|
|
||||||
return response(userString, 200, {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
...headers.toJSON(),
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { apiRoute, applyConfig, handleZodError } from "@/api";
|
import { apiRoute, applyConfig, handleZodError } from "@/api";
|
||||||
import { zValidator } from "@hono/zod-validator";
|
import { zValidator } from "@hono/zod-validator";
|
||||||
|
import type { Entity } from "@versia/federation/types";
|
||||||
import { and, count, eq, inArray } from "drizzle-orm";
|
import { and, count, eq, inArray } from "drizzle-orm";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db } from "~/drizzle/db";
|
import { db } from "~/drizzle/db";
|
||||||
|
|
@ -79,7 +80,7 @@ export default apiRoute((app) =>
|
||||||
)
|
)
|
||||||
)[0].count;
|
)[0].count;
|
||||||
|
|
||||||
return context.json({
|
const json = {
|
||||||
first: new URL(
|
first: new URL(
|
||||||
`/users/${uuid}/outbox?page=1`,
|
`/users/${uuid}/outbox?page=1`,
|
||||||
config.http.base_url,
|
config.http.base_url,
|
||||||
|
|
@ -90,8 +91,7 @@ export default apiRoute((app) =>
|
||||||
)}`,
|
)}`,
|
||||||
config.http.base_url,
|
config.http.base_url,
|
||||||
).toString(),
|
).toString(),
|
||||||
total_items: totalNotes,
|
total: totalNotes,
|
||||||
// Server actor
|
|
||||||
author: author.getUri(),
|
author: author.getUri(),
|
||||||
next:
|
next:
|
||||||
notes.length === NOTES_PER_PAGE
|
notes.length === NOTES_PER_PAGE
|
||||||
|
|
@ -99,16 +99,25 @@ export default apiRoute((app) =>
|
||||||
`/users/${uuid}/outbox?page=${pageNumber + 1}`,
|
`/users/${uuid}/outbox?page=${pageNumber + 1}`,
|
||||||
config.http.base_url,
|
config.http.base_url,
|
||||||
).toString()
|
).toString()
|
||||||
: undefined,
|
: null,
|
||||||
prev:
|
previous:
|
||||||
pageNumber > 1
|
pageNumber > 1
|
||||||
? new URL(
|
? new URL(
|
||||||
`/users/${uuid}/outbox?page=${pageNumber - 1}`,
|
`/users/${uuid}/outbox?page=${pageNumber - 1}`,
|
||||||
config.http.base_url,
|
config.http.base_url,
|
||||||
).toString()
|
).toString()
|
||||||
: undefined,
|
: null,
|
||||||
items: notes.map((note) => note.toVersia()),
|
items: notes.map((note) => note.toVersia()),
|
||||||
});
|
};
|
||||||
|
|
||||||
|
const { headers } = await author.sign(
|
||||||
|
// @ts-expect-error To fix when I add collections to versia-api
|
||||||
|
json as Entity,
|
||||||
|
context.req.url,
|
||||||
|
"GET",
|
||||||
|
);
|
||||||
|
|
||||||
|
return context.json(json, 200, headers.toJSON());
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -79,9 +79,7 @@ export default apiRoute((app) =>
|
||||||
let activityPubUrl = "";
|
let activityPubUrl = "";
|
||||||
|
|
||||||
if (config.federation.bridge.enabled) {
|
if (config.federation.bridge.enabled) {
|
||||||
const requester = await User.getServerActor();
|
const manager = await User.getFederationRequester();
|
||||||
|
|
||||||
const manager = await requester.getFederationRequester();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
activityPubUrl = await manager.webFinger(
|
activityPubUrl = await manager.webFinger(
|
||||||
|
|
|
||||||
15
utils/api.ts
15
utils/api.ts
|
|
@ -9,12 +9,14 @@ 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 { parse } from "qs";
|
import { parse } from "qs";
|
||||||
|
|
@ -67,17 +69,26 @@ export const idValidator = createRegExp(
|
||||||
|
|
||||||
export const emojiValidator = createRegExp(
|
export const emojiValidator = createRegExp(
|
||||||
// A-Z a-z 0-9 _ -
|
// A-Z a-z 0-9 _ -
|
||||||
oneOrMore(letter.or(digit).or(exactly("_")).or(exactly("-"))),
|
oneOrMore(letter.or(digit).or(charIn("_-"))),
|
||||||
[caseInsensitive, global],
|
[caseInsensitive, global],
|
||||||
);
|
);
|
||||||
|
|
||||||
export const emojiValidatorWithColons = createRegExp(
|
export const emojiValidatorWithColons = createRegExp(
|
||||||
exactly(":"),
|
exactly(":"),
|
||||||
oneOrMore(letter.or(digit).or(exactly("_")).or(exactly("-"))),
|
oneOrMore(letter.or(digit).or(charIn("_-"))),
|
||||||
exactly(":"),
|
exactly(":"),
|
||||||
[caseInsensitive, global],
|
[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(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue