feat: Add the ability to parse markdown and replace tags asynchronously, and fetch the missing users and channels. (#131)

* chore: Update ulid to v3 to allow use in service workers

This commit was made without the use of generative AI.

Signed-off-by: Jacob Schlecht <dadadah@echoha.us>

* feat: Add async message parsing that calls fetch for service workers.

This commit was made without the use of generative AI.

Signed-off-by: Jacob Schlecht <dadadah@echoha.us>

* feat: add custom emoji text replacements to the markdown parser

This commit was made without the use of generative AI.

Signed-off-by: Jacob Schlecht <dadadah@echoha.us>

* feat: dedupe regex matches, wait for all requests at once

This commit was made without the use of generative AI.

Signed-off-by: Jacob Schlecht <dadadah@echoha.us>

* fix: run lint for Client.ts

This commit was made without the use of generative AI.

Signed-off-by: Jacob Schlecht <dadadah@echoha.us>

* refactor: Wait for all promises at once

This commit was made without the use of generative AI.

Signed-off-by: Jacob Schlecht <dadadah@echoha.us>

* Using spread operator

Signed-off-by: Christopher Hultin <chris.hultin@gmail.com>

* fix: run linter

This commit was made without the use of generative AI.

Signed-off-by: Jacob Schlecht <dadadah@echoha.us>

---------

Signed-off-by: Jacob Schlecht <dadadah@echoha.us>
Signed-off-by: Christopher Hultin <chris.hultin@gmail.com>
Co-authored-by: Christopher Hultin <chris.hultin@gmail.com>
This commit is contained in:
Jacob Schlecht
2026-03-02 08:26:31 -07:00
committed by GitHub
parent 746afa7839
commit 97d924bb3a
4 changed files with 111 additions and 8 deletions
+1 -1
View File
@@ -32,7 +32,7 @@
"json-with-bigint": "^3.4.4",
"solid-js": "^1.9.6",
"stoat-api": "0.8.9-4",
"ulid": "^2.4.0"
"ulid": "^3.0.2"
},
"devDependencies": {
"@mxssfd/typedoc-theme": "^1.1.7",
+5 -5
View File
@@ -27,8 +27,8 @@ importers:
specifier: 0.8.9-4
version: 0.8.9-4
ulid:
specifier: ^2.4.0
version: 2.4.0
specifier: ^3.0.2
version: 3.0.2
devDependencies:
'@mxssfd/typedoc-theme':
specifier: ^1.1.7
@@ -1046,8 +1046,8 @@ packages:
uc.micro@2.1.0:
resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==}
ulid@2.4.0:
resolution: {integrity: sha512-fIRiVTJNcSRmXKPZtGzFQv9WRrZ3M9eoptl/teFJvjOzmpU+/K/JH6HZ8deBfb5vMEpicJcLn7JmvdknlMq7Zg==}
ulid@3.0.2:
resolution: {integrity: sha512-yu26mwteFYzBAot7KVMqFGCVpsF6g8wXfJzQUHvu1no3+rRRSFcSV2nKeYvNPLD2J4b08jYBDhHUjeH0ygIl9w==}
hasBin: true
undici-types@6.21.0:
@@ -2132,7 +2132,7 @@ snapshots:
uc.micro@2.1.0: {}
ulid@2.4.0: {}
ulid@3.0.2: {}
undici-types@6.21.0: {}
+100 -2
View File
@@ -34,7 +34,12 @@ import type { HydratedMessage } from "./hydration/message.js";
import type { HydratedServer } from "./hydration/server.js";
import type { HydratedServerMember } from "./hydration/serverMember.js";
import type { HydratedUser } from "./hydration/user.js";
import { RE_CHANNELS, RE_MENTIONS, RE_SPOILER } from "./lib/regex.js";
import {
RE_CHANNELS,
RE_CUSTOM_EMOJI,
RE_MENTIONS,
RE_SPOILER,
} from "./lib/regex.js";
export type Session = { _id: string; token: string; user_id: string } | string;
@@ -242,7 +247,9 @@ export class Client extends AsyncEventEmitter<Events> {
baseURL: this.options.baseURL,
});
const [configured, setConfigured] = createSignal(configuration !== undefined);
const [configured, setConfigured] = createSignal(
configuration !== undefined,
);
this.configured = configured;
this.#setConfigured = setConfigured;
@@ -416,6 +423,97 @@ export class Client extends AsyncEventEmitter<Events> {
.replace(RE_SPOILER, "<spoiler>");
}
/**
* Prepare a markdown-based message to be displayed to the user as plain text. This method will fetch each user or channel if they are missing. Useful for serviceworkers.
* @param source Source markdown text
* @returns Modified plain text
*/
async markdownToTextFetch(source: string): Promise<string> {
// Get all user matches, create a map to dedupe
const userMatches = Object.fromEntries(
Array.from(source.matchAll(RE_MENTIONS), (match) => {
return [match[0], match[1]];
}),
);
// Get all channel matches, create a map to dedupe
const channelMatches = Object.fromEntries(
Array.from(source.matchAll(RE_CHANNELS), (match) => {
return [match[0], match[1]];
}),
);
// Get all custom emoji matches, create a map to dedupe
const customEmojiMatches = Object.fromEntries(
Array.from(source.matchAll(RE_CUSTOM_EMOJI), (match) => {
return [match[0], match[1]];
}),
);
// Send requests to replace user ids
const userReplacementPromises = Object.keys(userMatches).map(
async (key) => {
const substr = userMatches[key];
if (substr) {
const user = await this.users.fetch(substr);
if (user) {
return [key, `@${user.username}`];
}
}
return [key, key];
},
);
// Send requests to replace channel ids
const channelReplacementPromises = Object.keys(channelMatches).map(
async (key) => {
const substr = channelMatches[key];
if (substr) {
const channel = await this.channels.fetch(substr);
if (channel) {
return [key, `#${channel.displayName}`];
}
}
return [key, key];
},
);
// Send requests to replace custom emojis
const customEmojiReplacementPromises = Object.keys(customEmojiMatches).map(
async (key) => {
const substr = customEmojiMatches[key];
if (substr) {
const emoji = await this.emojis.fetch(substr);
if (emoji) {
return [key, `:${emoji.name}:`];
}
}
return [key, key];
},
);
// Await for all promises to get the strings to replace with.
const replacements = await Promise.all([
...userReplacementPromises,
...channelReplacementPromises,
...customEmojiReplacementPromises,
]);
const replacementsMap = Object.fromEntries(replacements);
return source
.replace(RE_MENTIONS, (match) => replacementsMap[match])
.replace(RE_CHANNELS, (match) => replacementsMap[match])
.replace(RE_CUSTOM_EMOJI, (match) => replacementsMap[match])
.replace(RE_SPOILER, "<spoiler>");
}
/**
* Proxy a file through January.
* @param url URL to proxy
+5
View File
@@ -8,6 +8,11 @@ export const RE_MENTIONS = /<@([0-9ABCDEFGHJKMNPQRSTVWXYZ]{26})>/g;
*/
export const RE_CHANNELS = /<#([0-9ABCDEFGHJKMNPQRSTVWXYZ]{26})>/g;
/**
* Regular expression for stripping custom emojis.
*/
export const RE_CUSTOM_EMOJI = /:([0-9ABCDEFGHJKMNPQRSTVWXYZ]{26}):/g;
/**
* Regular expression for spoilers.
*/