diff --git a/package.json b/package.json index 6b1e6d8c..34430209 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 25752324..a38c4bfd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: {} diff --git a/src/Client.ts b/src/Client.ts index 94a3d613..1c9a810a 100644 --- a/src/Client.ts +++ b/src/Client.ts @@ -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 { 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 { .replace(RE_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 { + // 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, ""); + } + /** * Proxy a file through January. * @param url URL to proxy diff --git a/src/lib/regex.ts b/src/lib/regex.ts index 7bbe0821..82212261 100644 --- a/src/lib/regex.ts +++ b/src/lib/regex.ts @@ -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. */