From ca2ea37cef4103e8a7e638b0de5afc1bd4fc2f75 Mon Sep 17 00:00:00 2001 From: Taureon <45183108+Taureon@users.noreply.github.com> Date: Sat, 14 Mar 2026 09:53:44 +0100 Subject: [PATCH 01/31] feat: create method ChannelWebhook.sendMessage --- src/classes/ChannelWebhook.ts | 40 ++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/src/classes/ChannelWebhook.ts b/src/classes/ChannelWebhook.ts index fee8e17c..0fc45f68 100644 --- a/src/classes/ChannelWebhook.ts +++ b/src/classes/ChannelWebhook.ts @@ -1,10 +1,12 @@ -import { DataEditWebhook } from "revolt-api"; +import { DataEditWebhook, DataMessageSend } from "revolt-api"; import type { ChannelWebhookCollection } from "../collections/ChannelWebhookCollection.js"; import { hydrate } from "../hydration/index.js"; import type { Channel } from "./Channel.js"; import type { File } from "./File.js"; +import { Message } from "./Message.js"; +import { ulid } from "ulid"; /** * Channel Webhook Class @@ -101,4 +103,40 @@ export class ChannelWebhook { this.#collection.delete(this.id); } + + /** + * Send a message through this webhook + * @param data Either the message as a string or message sending route data + * @returns Sent message + */ + async sendMessage( + data: string | DataMessageSend, + idempotencyKey: string = ulid(), + ): Promise { + const msg: DataMessageSend = + typeof data === "string" ? { content: data } : data; + + // Mark as silent message + if (msg.content?.startsWith("@silent ")) { + msg.content = msg.content.substring(8); + msg.flags ||= 1; + msg.flags |= 1; + } + + const message = await this.#collection.client.api.post( + `/webhooks/${this.id as ""}/${this.token as ""}`, + msg, + { + headers: { + "Idempotency-Key": idempotencyKey, + }, + }, + ); + + return this.#collection.client.messages.getOrCreate( + message._id, + message, + true, + ); + } } From 33154b287967248d2d5ebc6e0de99f2eb7273a0d Mon Sep 17 00:00:00 2001 From: izzy Date: Wed, 8 Oct 2025 21:55:49 +0100 Subject: [PATCH 02/31] fix: handle group/DM as voice channel Signed-off-by: Taureon --- src/classes/Channel.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/classes/Channel.ts b/src/classes/Channel.ts index f5dd3a84..80e376b0 100644 --- a/src/classes/Channel.ts +++ b/src/classes/Channel.ts @@ -327,7 +327,7 @@ export class Channel { * NB. subject to change as vc(2) goes to production */ get isVoice(): boolean { - return this.#collection.getUnderlyingObject(this.id).voice; + return this.type === 'Group' || this.type === 'DirectMessage' || this.#collection.getUnderlyingObject(this.id).voice; } /** From 895be07bdc613c9acc593da23829e3374e9d3084 Mon Sep 17 00:00:00 2001 From: izzy Date: Fri, 10 Oct 2025 15:44:22 +0100 Subject: [PATCH 03/31] chore: remove triage workflows Signed-off-by: Taureon --- .github/workflows/triage_issue.yml | 54 -------------------- .github/workflows/triage_pr.yml | 79 ------------------------------ 2 files changed, 133 deletions(-) delete mode 100644 .github/workflows/triage_issue.yml delete mode 100644 .github/workflows/triage_pr.yml diff --git a/.github/workflows/triage_issue.yml b/.github/workflows/triage_issue.yml deleted file mode 100644 index ecc69f59..00000000 --- a/.github/workflows/triage_issue.yml +++ /dev/null @@ -1,54 +0,0 @@ -name: Add Issue to Board - -on: - issues: - types: [opened] - -jobs: - track_issue: - runs-on: ubuntu-latest - steps: - - name: Get project data - env: - GITHUB_TOKEN: ${{ secrets.PAT }} - run: | - gh api graphql -f query=' - query { - organization(login: "revoltchat"){ - projectV2(number: 3) { - id - fields(first:20) { - nodes { - ... on ProjectV2SingleSelectField { - id - name - options { - id - name - } - } - } - } - } - } - }' > project_data.json - - echo 'PROJECT_ID='$(jq '.data.organization.projectV2.id' project_data.json) >> $GITHUB_ENV - echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV - echo 'TODO_OPTION_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .options[] | select(.name=="Todo") |.id' project_data.json) >> $GITHUB_ENV - - - name: Add issue to project - env: - GITHUB_TOKEN: ${{ secrets.PAT }} - ISSUE_ID: ${{ github.event.issue.node_id }} - run: | - item_id="$( gh api graphql -f query=' - mutation($project:ID!, $issue:ID!) { - addProjectV2ItemById(input: {projectId: $project, contentId: $issue}) { - item { - id - } - } - }' -f project=$PROJECT_ID -f issue=$ISSUE_ID --jq '.data.addProjectV2ItemById.item.id')" - - echo 'ITEM_ID='$item_id >> $GITHUB_ENV diff --git a/.github/workflows/triage_pr.yml b/.github/workflows/triage_pr.yml deleted file mode 100644 index 3010d2e5..00000000 --- a/.github/workflows/triage_pr.yml +++ /dev/null @@ -1,79 +0,0 @@ -name: Add PR to Board - -on: - pull_request_target: - types: [opened, synchronize, ready_for_review, review_requested] - -jobs: - track_pr: - runs-on: ubuntu-latest - steps: - - name: Get project data - env: - GITHUB_TOKEN: ${{ secrets.PAT }} - run: | - gh api graphql -f query=' - query { - organization(login: "revoltchat"){ - projectV2(number: 5) { - id - fields(first:20) { - nodes { - ... on ProjectV2SingleSelectField { - id - name - options { - id - name - } - } - } - } - } - } - }' > project_data.json - - echo 'PROJECT_ID='$(jq '.data.organization.projectV2.id' project_data.json) >> $GITHUB_ENV - echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV - echo 'INCOMING_OPTION_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .options[] | select(.name=="🆕 Untriaged") |.id' project_data.json) >> $GITHUB_ENV - - - name: Add PR to project - env: - GITHUB_TOKEN: ${{ secrets.PAT }} - PR_ID: ${{ github.event.pull_request.node_id }} - run: | - item_id="$( gh api graphql -f query=' - mutation($project:ID!, $pr:ID!) { - addProjectV2ItemById(input: {projectId: $project, contentId: $pr}) { - item { - id - } - } - }' -f project=$PROJECT_ID -f pr=$PR_ID --jq '.data.addProjectV2ItemById.item.id')" - - echo 'ITEM_ID='$item_id >> $GITHUB_ENV - - - name: Set fields - env: - GITHUB_TOKEN: ${{ secrets.PAT }} - run: | - gh api graphql -f query=' - mutation ( - $project: ID! - $item: ID! - $status_field: ID! - $status_value: String! - ) { - set_status: updateProjectV2ItemFieldValue(input: { - projectId: $project - itemId: $item - fieldId: $status_field - value: { - singleSelectOptionId: $status_value - } - }) { - projectV2Item { - id - } - } - }' -f project=$PROJECT_ID -f item=$ITEM_ID -f status_field=$STATUS_FIELD_ID -f status_value=${{ env.INCOMING_OPTION_ID }} --silent From 89fc0345a638b53b2828fefa0ebd57b411dcb036 Mon Sep 17 00:00:00 2001 From: izzy Date: Fri, 10 Oct 2025 16:36:13 +0100 Subject: [PATCH 04/31] chore: fill optional argument for `unreact` Signed-off-by: Taureon --- src/classes/Message.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/classes/Message.ts b/src/classes/Message.ts index c637cd98..a6f1a8f3 100644 --- a/src/classes/Message.ts +++ b/src/classes/Message.ts @@ -395,7 +395,7 @@ export class Message { * @param emoji Unicode or emoji ID * @param deleteAll Remove all reactions */ - async unreact(emoji: string, deleteAll: boolean): Promise { + async unreact(emoji: string, deleteAll = false): Promise { return await this.#collection.client.api.delete( `/channels/${this.channelId as ""}/messages/${this.id as ""}/reactions/${ emoji as "" From 2ba30ffef9dcc50388f06c619499be5be48e78fa Mon Sep 17 00:00:00 2001 From: izzy Date: Thu, 16 Oct 2025 17:28:42 +0100 Subject: [PATCH 05/31] chore: increase ack rate Signed-off-by: Taureon --- src/classes/Channel.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/classes/Channel.ts b/src/classes/Channel.ts index 80e376b0..b791aaa1 100644 --- a/src/classes/Channel.ts +++ b/src/classes/Channel.ts @@ -173,8 +173,8 @@ export class Channel { get recipient(): User | undefined { return this.type === "DirectMessage" ? this.recipients?.find( - (user) => user?.id !== this.#collection.client.user!.id, - ) + (user) => user?.id !== this.#collection.client.user!.id, + ) : undefined; } @@ -769,11 +769,10 @@ export class Channel { performAck(); } - // We need to use setTimeout here for both Node.js and browser. - this.#ackTimeout = setTimeout(performAck, 5000) as unknown as number; + this.#ackTimeout = setTimeout(performAck, 1500) as unknown as number; if (!this.#ackLimit) { - this.#ackLimit = +new Date() + 15e3; + this.#ackLimit = +new Date() + 4e3; } } From 76ff1912f97234934b4941260ce0e61366e6e40f Mon Sep 17 00:00:00 2001 From: izzy Date: Thu, 16 Oct 2025 17:28:53 +0100 Subject: [PATCH 06/31] fix: ensure `lastMessageId` is updated on incoming messages Signed-off-by: Taureon --- src/events/v1.ts | 174 ++++++++++++++++++++++++----------------------- 1 file changed, 88 insertions(+), 86 deletions(-) diff --git a/src/events/v1.ts b/src/events/v1.ts index 069bc729..1764bb6e 100644 --- a/src/events/v1.ts +++ b/src/events/v1.ts @@ -42,21 +42,21 @@ export type ProtocolV1 = { type ClientMessage = | { type: "Authenticate"; token: string } | { - type: "BeginTyping"; - channel: string; - } + type: "BeginTyping"; + channel: string; + } | { - type: "EndTyping"; - channel: string; - } + type: "EndTyping"; + channel: string; + } | { - type: "Ping"; - data: number; - } + type: "Ping"; + data: number; + } | { - type: "Pong"; - data: number; - }; + type: "Pong"; + data: number; + }; /** * Messages sent from the server @@ -70,46 +70,46 @@ type ServerMessage = | { type: "Pong"; data: number } | ({ type: "Message" } & Message) | { - type: "MessageUpdate"; - id: string; - channel: string; - data: Partial; - } + type: "MessageUpdate"; + id: string; + channel: string; + data: Partial; + } | { - type: "MessageAppend"; - id: string; - channel: string; - append: Pick, "embeds">; - } + type: "MessageAppend"; + id: string; + channel: string; + append: Pick, "embeds">; + } | { type: "MessageDelete"; id: string; channel: string } | { - type: "MessageReact"; - id: string; - channel_id: string; - user_id: string; - emoji_id: string; - } + type: "MessageReact"; + id: string; + channel_id: string; + user_id: string; + emoji_id: string; + } | { - type: "MessageUnreact"; - id: string; - channel_id: string; - user_id: string; - emoji_id: string; - } + type: "MessageUnreact"; + id: string; + channel_id: string; + user_id: string; + emoji_id: string; + } | { - type: "MessageRemoveReaction"; - id: string; - channel_id: string; - emoji_id: string; - } + type: "MessageRemoveReaction"; + id: string; + channel_id: string; + emoji_id: string; + } | { type: "BulkMessageDelete"; channel: string; ids: string[] } | ({ type: "ChannelCreate" } & Channel) | { - type: "ChannelUpdate"; - id: string; - data: Partial; - clear?: FieldsChannel[]; - } + type: "ChannelUpdate"; + id: string; + data: Partial; + clear?: FieldsChannel[]; + } | { type: "ChannelDelete"; id: string } | { type: "ChannelGroupJoin"; id: string; user: string } | { type: "ChannelGroupLeave"; id: string; user: string } @@ -117,62 +117,62 @@ type ServerMessage = | { type: "ChannelStopTyping"; id: string; user: string } | { type: "ChannelAck"; id: string; user: string; message_id: string } | { - type: "ServerCreate"; - id: string; - server: Server; - channels: Channel[]; - } + type: "ServerCreate"; + id: string; + server: Server; + channels: Channel[]; + } | { - type: "ServerUpdate"; - id: string; - data: Partial; - clear?: FieldsServer[]; - } + type: "ServerUpdate"; + id: string; + data: Partial; + clear?: FieldsServer[]; + } | { type: "ServerDelete"; id: string } | { - type: "ServerMemberUpdate"; - id: MemberCompositeKey; - data: Partial; - clear?: FieldsMember[]; - } + type: "ServerMemberUpdate"; + id: MemberCompositeKey; + data: Partial; + clear?: FieldsMember[]; + } | { type: "ServerMemberJoin"; id: string; user: string } | { type: "ServerMemberLeave"; id: string; user: string } | { - type: "ServerRoleUpdate"; - id: string; - role_id: string; - data: Partial; - } + type: "ServerRoleUpdate"; + id: string; + role_id: string; + data: Partial; + } | { type: "ServerRoleDelete"; id: string; role_id: string } | { - type: "UserUpdate"; - id: string; - data: Partial; - clear?: FieldsUser[]; - } + type: "UserUpdate"; + id: string; + data: Partial; + clear?: FieldsUser[]; + } | { type: "UserRelationship"; user: User; status: RelationshipStatus } | { type: "UserPresence"; id: string; online: boolean } | { - type: "UserSettingsUpdate"; - id: string; - update: { [key: string]: [number, string] }; - } + type: "UserSettingsUpdate"; + id: string; + update: { [key: string]: [number, string] }; + } | { type: "UserPlatformWipe"; user_id: string; flags: number } | ({ type: "EmojiCreate" } & Emoji) | { type: "EmojiDelete"; id: string } | ({ - type: "Auth"; - } & ( + type: "Auth"; + } & ( | { - event_type: "DeleteSession"; - user_id: string; - session_id: string; - } + event_type: "DeleteSession"; + user_id: string; + session_id: string; + } | { - event_type: "DeleteAllSessions"; - user_id: string; - exclude_session_id: string; - } + event_type: "DeleteAllSessions"; + user_id: string; + exclude_session_id: string; + } )); /** @@ -277,13 +277,15 @@ export async function handleEvent( client.messages.getOrCreate(event._id, event, true); + const channel = client.channels.get(event.channel); + if (!channel) return; + + client.channels.updateUnderlyingObject(channel.id, "lastMessageId", event._id); + if ( event.mentions?.includes(client.user!.id) && client.options.syncUnreads ) { - const channel = client.channels.get(event.channel); - if (!channel) return; - const unread = client.channelUnreads.for(channel); unread.messageMentionIds.add(event._id); client.channels.updateUnderlyingObject( From c45732e22701be110f29e4d4835395ce5f96a8eb Mon Sep 17 00:00:00 2001 From: izzy Date: Thu, 16 Oct 2025 18:01:45 +0100 Subject: [PATCH 07/31] refactor: revolt.js -> stoat.js (publish 7.3.0) Signed-off-by: Taureon --- LICENSE | 2 +- README.md | 14 +++++++------- package.json | 10 +++++----- src/Client.ts | 10 +++++----- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/LICENSE b/LICENSE index e88e9efc..9366aadf 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021-2023 Paul Makles +Copyright (c) 2021-2025 Pawel Makles Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index e6d4f146..96299cb0 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# revolt.js +# stoat.js -![revolt.js](https://img.shields.io/npm/v/revolt.js) ![revolt-api](https://img.shields.io/npm/v/revolt-api?label=Revolt%20API) +![stoat.js](https://img.shields.io/npm/v/stoat.js) ![stoat-api](https://img.shields.io/npm/v/stoat-api?label=Stoat%20API) -**revolt.js** is a JavaScript library for interacting with the entire Revolt API. +**stoat.js** is a JavaScript library for interacting with the Stoat API ## Requirements @@ -14,7 +14,7 @@ To use this module, you must be using at least: ## Example Usage ```javascript -import { Client } from "revolt.js"; +import { Client } from "stoat.js"; let client = new Client(); @@ -46,15 +46,15 @@ function MyApp() { } ``` -## Revolt API Types +## Stoat API Types > [!WARNING] > It is advised you do not use this unless necessary. If you find somewhere that isn't covered by the library, please open an issue as this library aims to transform all objects. -All `revolt-api` types are re-exported from this library under `API`. +All `stoat-api` types are re-exported from this library under `API`. ```typescript -import { API } from "revolt.js"; +import { API } from "stoat.js"; // API.Channel; // API.[..]; diff --git a/package.json b/package.json index ce9d1422..207284c1 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,13 @@ { - "name": "revolt.js", - "version": "7.2.0", + "name": "stoat.js", + "version": "7.3.0", "type": "module", "exports": { ".": "./lib/index.js" }, "types": "lib/index.d.ts", - "repository": "https://github.com/revoltchat/revolt.js", - "author": "Paul Makles ", + "repository": "https://github.com/stoatchat/javascript-client-sdk", + "author": "insert ", "license": "MIT", "scripts": { "build": "tsc", @@ -23,7 +23,7 @@ "README.md", "lib" ], - "description": "Library for interacting with the Revolt API.", + "description": "Library for interacting with the Stoat API", "packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39", "dependencies": { "@solid-primitives/map": "^0.7.1", diff --git a/src/Client.ts b/src/Client.ts index f11846b5..134a5df7 100644 --- a/src/Client.ts +++ b/src/Client.ts @@ -162,7 +162,7 @@ export type ClientOptions = Partial & { }; /** - * Revolt.js Clients + * Stoat.js Clients */ export class Client extends AsyncEventEmitter { readonly account; @@ -193,13 +193,13 @@ export class Client extends AsyncEventEmitter { #reconnectTimeout: number | undefined; /** - * Create Revolt.js Client + * Create Stoat.js Client */ constructor(options?: Partial, configuration?: RevoltConfig) { super(); this.options = { - baseURL: "https://api.revolt.chat", + baseURL: "https://stoat.chat/api", partials: false, eagerFetching: true, syncUnreads: false, @@ -279,7 +279,7 @@ export class Client extends AsyncEventEmitter { this.#reconnectTimeout = setTimeout( () => this.connect(), this.options.retryDelayFunction(this.connectionFailureCount()) * - 1e3, + 1e3, ) as never; this.#setConnectionFailureCount((count) => count + 1); @@ -317,7 +317,7 @@ export class Client extends AsyncEventEmitter { this.events.disconnect(); this.#setReady(false); this.events.connect( - this.configuration?.ws ?? "wss://ws.revolt.chat", + this.configuration?.ws ?? "wss://stoat.chat/events", typeof this.#session === "string" ? this.#session : this.#session!.token, ); } From 5c64973b98399b91a2930c1b0e85e50319f3bde1 Mon Sep 17 00:00:00 2001 From: izzy Date: Thu, 16 Oct 2025 18:40:31 +0100 Subject: [PATCH 08/31] chore: bump stoat-api to 0.8.9-3 Signed-off-by: Taureon --- package.json | 4 ++-- pnpm-lock.yaml | 16 ++++++++-------- src/Client.ts | 4 ++-- src/classes/BannedUser.ts | 2 +- src/classes/Bot.ts | 2 +- src/classes/Channel.ts | 13 ++++++------- src/classes/ChannelWebhook.ts | 2 +- src/classes/Emoji.ts | 2 +- src/classes/File.ts | 2 +- src/classes/Invite.ts | 2 +- src/classes/MFA.ts | 2 +- src/classes/Message.ts | 2 +- src/classes/MessageEmbed.ts | 2 +- src/classes/PublicBot.ts | 2 +- src/classes/PublicInvite.ts | 2 +- src/classes/Server.ts | 2 +- src/classes/ServerBan.ts | 2 +- src/classes/ServerMember.ts | 2 +- src/classes/ServerRole.ts | 2 +- src/classes/SystemMessage.ts | 2 +- src/classes/User.ts | 2 +- src/classes/UserProfile.ts | 2 +- src/collections/AccountCollection.ts | 2 +- src/collections/BotCollection.ts | 2 +- src/collections/ChannelCollection.ts | 2 +- src/collections/ChannelUnreadCollection.ts | 2 +- src/collections/ChannelWebhookCollection.ts | 2 +- src/collections/EmojiCollection.ts | 2 +- src/collections/MessageCollection.ts | 2 +- src/collections/ServerCollection.ts | 2 +- src/collections/ServerMemberCollection.ts | 2 +- src/collections/SessionCollection.ts | 2 +- src/collections/UserCollection.ts | 2 +- src/events/EventClient.ts | 2 +- src/events/v1.ts | 2 +- src/hydration/bot.ts | 4 ++-- src/hydration/channel.ts | 2 +- src/hydration/channelUnread.ts | 2 +- src/hydration/channelWebhook.ts | 2 +- src/hydration/emoji.ts | 2 +- src/hydration/message.ts | 2 +- src/hydration/server.ts | 2 +- src/hydration/serverMember.ts | 2 +- src/hydration/session.ts | 2 +- src/hydration/user.ts | 2 +- src/index.ts | 2 +- src/permissions/calculator.ts | 3 +-- 47 files changed, 62 insertions(+), 64 deletions(-) diff --git a/package.json b/package.json index 207284c1..f80736f3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "stoat.js", - "version": "7.3.0", + "version": "7.3.1", "type": "module", "exports": { ".": "./lib/index.js" @@ -29,7 +29,7 @@ "@solid-primitives/map": "^0.7.1", "@solid-primitives/set": "^0.7.1", "@vladfrangu/async_event_emitter": "^2.4.6", - "revolt-api": "0.8.9", + "stoat-api": "0.8.9-3", "solid-js": "^1.9.6", "ulid": "^2.4.0" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2c4dd6f4..33dc9bfd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,12 +17,12 @@ importers: '@vladfrangu/async_event_emitter': specifier: ^2.4.6 version: 2.4.6 - revolt-api: - specifier: 0.8.9 - version: 0.8.9 solid-js: specifier: ^1.9.6 version: 1.9.6 + stoat-api: + specifier: 0.8.9-3 + version: 0.8.9-3 ulid: specifier: ^2.4.0 version: 2.4.0 @@ -905,9 +905,6 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - revolt-api@0.8.9: - resolution: {integrity: sha512-+VDJlgj/WBJvuTbkrmTUWjpCAU4DjN50akDdsDonZxy1Xx2eNXCzKDXhxof8wfOwhMU8HtXBFR5NgZvrBW6XZA==} - router@2.2.0: resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} engines: {node: '>= 18'} @@ -978,6 +975,9 @@ packages: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} + stoat-api@0.8.9-3: + resolution: {integrity: sha512-cuw6+5HUQScBxjtA11bELCCBUaio2eESUfAZHYi9qj29VFlAalJ63GEZKF6U8h1UydqcQ7YNx/Q9TU5D/rWNhA==} + strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -1965,8 +1965,6 @@ snapshots: reusify@1.1.0: {} - revolt-api@0.8.9: {} - router@2.2.0: dependencies: debug: 4.4.0 @@ -2062,6 +2060,8 @@ snapshots: statuses@2.0.1: {} + stoat-api@0.8.9-3: {} + strip-json-comments@3.1.1: {} style-to-object@1.0.8: diff --git a/src/Client.ts b/src/Client.ts index 134a5df7..34d8091e 100644 --- a/src/Client.ts +++ b/src/Client.ts @@ -2,8 +2,8 @@ import type { Accessor, Setter } from "solid-js"; import { batch, createSignal } from "solid-js"; import { AsyncEventEmitter } from "@vladfrangu/async_event_emitter"; -import { API } from "revolt-api"; -import type { DataLogin, RevoltConfig, Role } from "revolt-api"; +import { API } from "stoat-api"; +import type { DataLogin, RevoltConfig, Role } from "stoat-api"; import type { Channel } from "./classes/Channel.js"; import type { Emoji } from "./classes/Emoji.js"; diff --git a/src/classes/BannedUser.ts b/src/classes/BannedUser.ts index 4283cc71..30b86a5a 100644 --- a/src/classes/BannedUser.ts +++ b/src/classes/BannedUser.ts @@ -1,4 +1,4 @@ -import type { BannedUser as APIBannedUser } from "revolt-api"; +import type { BannedUser as APIBannedUser } from "stoat-api"; import type { Client } from "../Client.js"; diff --git a/src/classes/Bot.ts b/src/classes/Bot.ts index 9942a400..503adb12 100644 --- a/src/classes/Bot.ts +++ b/src/classes/Bot.ts @@ -1,4 +1,4 @@ -import type { DataEditBot } from "revolt-api"; +import type { DataEditBot } from "stoat-api"; import { decodeTime } from "ulid"; import type { BotCollection } from "../collections/BotCollection.js"; diff --git a/src/classes/Channel.ts b/src/classes/Channel.ts index b791aaa1..e4ac8a42 100644 --- a/src/classes/Channel.ts +++ b/src/classes/Channel.ts @@ -11,8 +11,8 @@ import type { DataMessageSend, Invite, Override, -} from "revolt-api"; -import type { APIRoutes } from "revolt-api/lib/routes"; +} from "stoat-api"; +import type { APIRoutes } from "stoat-api/lib/routes"; import { decodeTime, ulid } from "ulid"; import { ChannelCollection } from "../collections/index.js"; @@ -291,7 +291,6 @@ export class Channel { if ( !this.lastMessageId || this.type === "SavedMessages" || - this.type === "VoiceChannel" || this.#collection.client.options.channelExclusiveMuted(this) ) return false; @@ -314,7 +313,7 @@ export class Channel { * Get mentions in this channel for user. */ get mentions(): ReactiveSet | undefined { - if (this.type === "SavedMessages" || this.type === "VoiceChannel") + if (this.type === "SavedMessages") return undefined; return this.#collection.client.channelUnreads.get(this.id) @@ -473,7 +472,7 @@ export class Channel { /** * Delete or leave a channel * @param leaveSilently Whether to not send a message on leave - * @requires `DirectMessage`, `Group`, `TextChannel`, `VoiceChannel` + * @requires `DirectMessage`, `Group`, `TextChannel` */ async delete(leaveSilently?: boolean): Promise { await this.#collection.client.api.delete(`/channels/${this.id as ""}`, { @@ -694,7 +693,7 @@ export class Channel { /** * Create an invite to the channel - * @requires `TextChannel`, `VoiceChannel` + * @requires `TextChannel` * @returns Newly created invite code */ async createInvite(): Promise { @@ -780,7 +779,7 @@ export class Channel { * Set role permissions * @param role_id Role Id, set to 'default' to affect all users * @param permissions Permission value - * @requires `Group`, `TextChannel`, `VoiceChannel` + * @requires `Group`, `TextChannel` */ async setPermissions( role_id = "default", diff --git a/src/classes/ChannelWebhook.ts b/src/classes/ChannelWebhook.ts index 0fc45f68..76e2703e 100644 --- a/src/classes/ChannelWebhook.ts +++ b/src/classes/ChannelWebhook.ts @@ -1,4 +1,4 @@ -import { DataEditWebhook, DataMessageSend } from "revolt-api"; +import { DataEditWebhook, DataMessageSend } from "stoat-api"; import type { ChannelWebhookCollection } from "../collections/ChannelWebhookCollection.js"; import { hydrate } from "../hydration/index.js"; diff --git a/src/classes/Emoji.ts b/src/classes/Emoji.ts index efbcec47..e5bf66a5 100644 --- a/src/classes/Emoji.ts +++ b/src/classes/Emoji.ts @@ -1,4 +1,4 @@ -import type { EmojiParent } from "revolt-api"; +import type { EmojiParent } from "stoat-api"; import { decodeTime } from "ulid"; import type { EmojiCollection } from "../collections/EmojiCollection.js"; diff --git a/src/classes/File.ts b/src/classes/File.ts index fd3fb652..cf1e25d2 100644 --- a/src/classes/File.ts +++ b/src/classes/File.ts @@ -1,4 +1,4 @@ -import type { File as APIFile, Metadata } from "revolt-api"; +import type { File as APIFile, Metadata } from "stoat-api"; import type { Client } from "../Client.js"; diff --git a/src/classes/Invite.ts b/src/classes/Invite.ts index 6fc03161..4e579348 100644 --- a/src/classes/Invite.ts +++ b/src/classes/Invite.ts @@ -1,4 +1,4 @@ -import type { Invite } from "revolt-api"; +import type { Invite } from "stoat-api"; import type { Client } from "../Client.js"; diff --git a/src/classes/MFA.ts b/src/classes/MFA.ts index 72255332..f916b4ff 100644 --- a/src/classes/MFA.ts +++ b/src/classes/MFA.ts @@ -6,7 +6,7 @@ import type { MFAResponse, MultiFactorStatus, MFATicket as TicketType, -} from "revolt-api"; +} from "stoat-api"; import type { Client } from "../Client.js"; diff --git a/src/classes/Message.ts b/src/classes/Message.ts index a6f1a8f3..0d8b6bc5 100644 --- a/src/classes/Message.ts +++ b/src/classes/Message.ts @@ -6,7 +6,7 @@ import type { DataEditMessage, DataMessageSend, Masquerade, -} from "revolt-api"; +} from "stoat-api"; import { decodeTime } from "ulid"; import type { Client } from "../Client.js"; diff --git a/src/classes/MessageEmbed.ts b/src/classes/MessageEmbed.ts index 5b6b77c9..800f4a12 100644 --- a/src/classes/MessageEmbed.ts +++ b/src/classes/MessageEmbed.ts @@ -1,4 +1,4 @@ -import type { Embed, ImageSize, Special } from "revolt-api"; +import type { Embed, ImageSize, Special } from "stoat-api"; import type { Client } from "../Client.js"; diff --git a/src/classes/PublicBot.ts b/src/classes/PublicBot.ts index 3653d3f9..ef9b0c1f 100644 --- a/src/classes/PublicBot.ts +++ b/src/classes/PublicBot.ts @@ -1,4 +1,4 @@ -import type { File as APIFile, PublicBot as APIPublicBot } from "revolt-api"; +import type { File as APIFile, PublicBot as APIPublicBot } from "stoat-api"; import type { Client } from "../Client.js"; diff --git a/src/classes/PublicInvite.ts b/src/classes/PublicInvite.ts index da3aac40..f64cd632 100644 --- a/src/classes/PublicInvite.ts +++ b/src/classes/PublicInvite.ts @@ -1,6 +1,6 @@ import { batch } from "solid-js"; -import type { Invite, InviteResponse } from "revolt-api"; +import type { Invite, InviteResponse } from "stoat-api"; import type { Client } from "../Client.js"; import type { ServerFlags } from "../hydration/server.js"; diff --git a/src/classes/Server.ts b/src/classes/Server.ts index 82567057..eba16cb5 100644 --- a/src/classes/Server.ts +++ b/src/classes/Server.ts @@ -15,7 +15,7 @@ import type { Override, OverrideField, Role, -} from "revolt-api"; +} from "stoat-api"; import { decodeTime } from "ulid"; import type { ServerCollection } from "../collections/ServerCollection.js"; diff --git a/src/classes/ServerBan.ts b/src/classes/ServerBan.ts index 807f15a5..f2150596 100644 --- a/src/classes/ServerBan.ts +++ b/src/classes/ServerBan.ts @@ -2,7 +2,7 @@ import type { BannedUser as APIBannedUser, ServerBan as APIServerBan, MemberCompositeKey, -} from "revolt-api"; +} from "stoat-api"; import type { Client } from "../Client.js"; diff --git a/src/classes/ServerMember.ts b/src/classes/ServerMember.ts index ec4d2559..14796d26 100644 --- a/src/classes/ServerMember.ts +++ b/src/classes/ServerMember.ts @@ -3,7 +3,7 @@ import type { DataMemberEdit, MemberCompositeKey, Role, -} from "revolt-api"; +} from "stoat-api"; import type { ServerMemberCollection } from "../collections/ServerMemberCollection.js"; import { diff --git a/src/classes/ServerRole.ts b/src/classes/ServerRole.ts index d1fa8b39..7447fbc6 100644 --- a/src/classes/ServerRole.ts +++ b/src/classes/ServerRole.ts @@ -1,4 +1,4 @@ -import type { Role as APIRole, OverrideField } from "revolt-api"; +import type { Role as APIRole, OverrideField } from "stoat-api"; import type { Client } from "../Client.js"; diff --git a/src/classes/SystemMessage.ts b/src/classes/SystemMessage.ts index 922a2ce4..4b053344 100644 --- a/src/classes/SystemMessage.ts +++ b/src/classes/SystemMessage.ts @@ -1,4 +1,4 @@ -import type { SystemMessage as APISystemMessage } from "revolt-api"; +import type { SystemMessage as APISystemMessage } from "stoat-api"; import type { Client } from "../Client.js"; diff --git a/src/classes/User.ts b/src/classes/User.ts index 2ce6cee9..296650f5 100644 --- a/src/classes/User.ts +++ b/src/classes/User.ts @@ -1,4 +1,4 @@ -import type { User as APIUser, DataEditUser, Presence } from "revolt-api"; +import type { User as APIUser, DataEditUser, Presence } from "stoat-api"; import { decodeTime } from "ulid"; import type { UserCollection } from "../collections/UserCollection.js"; diff --git a/src/classes/UserProfile.ts b/src/classes/UserProfile.ts index a2a2b7b1..06d7cd85 100644 --- a/src/classes/UserProfile.ts +++ b/src/classes/UserProfile.ts @@ -1,4 +1,4 @@ -import type { UserProfile as APIUserProfile } from "revolt-api"; +import type { UserProfile as APIUserProfile } from "stoat-api"; import type { Client } from "../Client.js"; diff --git a/src/collections/AccountCollection.ts b/src/collections/AccountCollection.ts index 1f49675e..b695a355 100644 --- a/src/collections/AccountCollection.ts +++ b/src/collections/AccountCollection.ts @@ -1,4 +1,4 @@ -import type { DataCreateAccount, WebPushSubscription } from "revolt-api"; +import type { DataCreateAccount, WebPushSubscription } from "stoat-api"; import type { Client } from "../Client.js"; import { MFA } from "../classes/MFA.js"; diff --git a/src/collections/BotCollection.ts b/src/collections/BotCollection.ts index 2b0e2ff5..ba3f62a1 100644 --- a/src/collections/BotCollection.ts +++ b/src/collections/BotCollection.ts @@ -1,6 +1,6 @@ import { batch } from "solid-js"; -import type { Bot as APIBot, OwnedBotsResponse } from "revolt-api"; +import type { Bot as APIBot, OwnedBotsResponse } from "stoat-api"; import { Bot } from "../classes/Bot.js"; import { PublicBot } from "../classes/PublicBot.js"; diff --git a/src/collections/ChannelCollection.ts b/src/collections/ChannelCollection.ts index 407f5359..a3ba4747 100644 --- a/src/collections/ChannelCollection.ts +++ b/src/collections/ChannelCollection.ts @@ -1,4 +1,4 @@ -import type { Channel as APIChannel } from "revolt-api"; +import type { Channel as APIChannel } from "stoat-api"; import { Channel } from "../classes/Channel.js"; import { User } from "../classes/User.js"; diff --git a/src/collections/ChannelUnreadCollection.ts b/src/collections/ChannelUnreadCollection.ts index c783ccf7..280555b8 100644 --- a/src/collections/ChannelUnreadCollection.ts +++ b/src/collections/ChannelUnreadCollection.ts @@ -1,6 +1,6 @@ import { batch } from "solid-js"; -import type { ChannelUnread as APIChannelUnread } from "revolt-api"; +import type { ChannelUnread as APIChannelUnread } from "stoat-api"; import { ChannelUnread } from "../classes/ChannelUnread.js"; import { Channel } from "../classes/index.js"; diff --git a/src/collections/ChannelWebhookCollection.ts b/src/collections/ChannelWebhookCollection.ts index 3d1d9e13..2fcee225 100644 --- a/src/collections/ChannelWebhookCollection.ts +++ b/src/collections/ChannelWebhookCollection.ts @@ -1,4 +1,4 @@ -import type { Webhook } from "revolt-api"; +import type { Webhook } from "stoat-api"; import { ChannelWebhook } from "../classes/ChannelWebhook.js"; import type { HydratedChannelWebhook } from "../hydration/channelWebhook.js"; diff --git a/src/collections/EmojiCollection.ts b/src/collections/EmojiCollection.ts index eb700f54..906516b8 100644 --- a/src/collections/EmojiCollection.ts +++ b/src/collections/EmojiCollection.ts @@ -1,4 +1,4 @@ -import type { Emoji as APIEmoji } from "revolt-api"; +import type { Emoji as APIEmoji } from "stoat-api"; import { Emoji } from "../classes/Emoji.js"; import type { HydratedEmoji } from "../hydration/emoji.js"; diff --git a/src/collections/MessageCollection.ts b/src/collections/MessageCollection.ts index efd351de..55210113 100644 --- a/src/collections/MessageCollection.ts +++ b/src/collections/MessageCollection.ts @@ -1,4 +1,4 @@ -import type { Message as APIMessage } from "revolt-api"; +import type { Message as APIMessage } from "stoat-api"; import { Message } from "../classes/Message.js"; import type { HydratedMessage } from "../hydration/message.js"; diff --git a/src/collections/ServerCollection.ts b/src/collections/ServerCollection.ts index e7c30c0e..819f5691 100644 --- a/src/collections/ServerCollection.ts +++ b/src/collections/ServerCollection.ts @@ -4,7 +4,7 @@ import type { Server as APIServer, Channel, DataCreateServer, -} from "revolt-api"; +} from "stoat-api"; import { Server } from "../classes/Server.js"; import type { HydratedServer } from "../hydration/server.js"; diff --git a/src/collections/ServerMemberCollection.ts b/src/collections/ServerMemberCollection.ts index d421c2b8..a559979d 100644 --- a/src/collections/ServerMemberCollection.ts +++ b/src/collections/ServerMemberCollection.ts @@ -1,4 +1,4 @@ -import type { Member, MemberCompositeKey } from "revolt-api"; +import type { Member, MemberCompositeKey } from "stoat-api"; import { ServerMember } from "../classes/ServerMember.js"; import type { HydratedServerMember } from "../hydration/serverMember.js"; diff --git a/src/collections/SessionCollection.ts b/src/collections/SessionCollection.ts index 59274b98..acae155c 100644 --- a/src/collections/SessionCollection.ts +++ b/src/collections/SessionCollection.ts @@ -1,6 +1,6 @@ import { batch } from "solid-js"; -import type { SessionInfo } from "revolt-api"; +import type { SessionInfo } from "stoat-api"; import { Session } from "../classes/Session.js"; import type { HydratedSession } from "../hydration/session.js"; diff --git a/src/collections/UserCollection.ts b/src/collections/UserCollection.ts index 4ae2c23e..a5e70740 100644 --- a/src/collections/UserCollection.ts +++ b/src/collections/UserCollection.ts @@ -1,4 +1,4 @@ -import type { User as APIUser } from "revolt-api"; +import type { User as APIUser } from "stoat-api"; import type { Client } from "../Client.js"; import { User } from "../classes/User.js"; diff --git a/src/events/EventClient.ts b/src/events/EventClient.ts index f9b9fced..8b8f288c 100644 --- a/src/events/EventClient.ts +++ b/src/events/EventClient.ts @@ -2,7 +2,7 @@ import type { Accessor, Setter } from "solid-js"; import { createSignal } from "solid-js"; import { AsyncEventEmitter } from "@vladfrangu/async_event_emitter"; -import type { Error } from "revolt-api"; +import type { Error } from "stoat-api"; import type { ProtocolV1 } from "./v1.js"; diff --git a/src/events/v1.ts b/src/events/v1.ts index 1764bb6e..6d20c087 100644 --- a/src/events/v1.ts +++ b/src/events/v1.ts @@ -17,7 +17,7 @@ import type { Role, Server, User, -} from "revolt-api"; +} from "stoat-api"; import type { Client } from "../Client.js"; import { MessageEmbed } from "../classes/MessageEmbed.js"; diff --git a/src/hydration/bot.ts b/src/hydration/bot.ts index 663d49a8..99fbe2e6 100644 --- a/src/hydration/bot.ts +++ b/src/hydration/bot.ts @@ -1,4 +1,4 @@ -import type { Bot as APIBot } from "revolt-api"; +import type { Bot as APIBot } from "stoat-api"; import type { Hydrate } from "./index.js"; @@ -41,4 +41,4 @@ export const botHydration: Hydrate = { /** * Flags attributed to users */ -export enum BotFlags {} +export enum BotFlags { } diff --git a/src/hydration/channel.ts b/src/hydration/channel.ts index 6a861948..c0e18ea6 100644 --- a/src/hydration/channel.ts +++ b/src/hydration/channel.ts @@ -1,5 +1,5 @@ import { ReactiveSet } from "@solid-primitives/set"; -import type { Channel as APIChannel, OverrideField } from "revolt-api"; +import type { Channel as APIChannel, OverrideField } from "stoat-api"; import type { Client } from "../Client.js"; import { File } from "../classes/File.js"; diff --git a/src/hydration/channelUnread.ts b/src/hydration/channelUnread.ts index b30f503c..c5e2643f 100644 --- a/src/hydration/channelUnread.ts +++ b/src/hydration/channelUnread.ts @@ -1,5 +1,5 @@ import { ReactiveSet } from "@solid-primitives/set"; -import type { ChannelUnread } from "revolt-api"; +import type { ChannelUnread } from "stoat-api"; import type { Merge } from "../lib/merge.js"; diff --git a/src/hydration/channelWebhook.ts b/src/hydration/channelWebhook.ts index ecf3abfb..654a8c00 100644 --- a/src/hydration/channelWebhook.ts +++ b/src/hydration/channelWebhook.ts @@ -1,4 +1,4 @@ -import type { Webhook } from "revolt-api"; +import type { Webhook } from "stoat-api"; import type { Client } from "../Client.js"; import { File } from "../classes/File.js"; diff --git a/src/hydration/emoji.ts b/src/hydration/emoji.ts index 90e0564f..634cedb9 100644 --- a/src/hydration/emoji.ts +++ b/src/hydration/emoji.ts @@ -1,4 +1,4 @@ -import type { Emoji as APIEmoji, EmojiParent } from "revolt-api"; +import type { Emoji as APIEmoji, EmojiParent } from "stoat-api"; import type { Merge } from "../lib/merge.js"; diff --git a/src/hydration/message.ts b/src/hydration/message.ts index 6c454bfa..81e3a6d6 100644 --- a/src/hydration/message.ts +++ b/src/hydration/message.ts @@ -1,6 +1,6 @@ import { ReactiveMap } from "@solid-primitives/map"; import { ReactiveSet } from "@solid-primitives/set"; -import type { Interactions, Masquerade, Message } from "revolt-api"; +import type { Interactions, Masquerade, Message } from "stoat-api"; import type { Client } from "../Client.js"; import { File } from "../classes/File.js"; diff --git a/src/hydration/server.ts b/src/hydration/server.ts index acc66633..e5128ad6 100644 --- a/src/hydration/server.ts +++ b/src/hydration/server.ts @@ -4,7 +4,7 @@ import type { Server as APIServer, Category, SystemMessageChannels, -} from "revolt-api"; +} from "stoat-api"; import type { Client } from "../Client.js"; import { File } from "../classes/File.js"; diff --git a/src/hydration/serverMember.ts b/src/hydration/serverMember.ts index ce3137ae..cfb230a7 100644 --- a/src/hydration/serverMember.ts +++ b/src/hydration/serverMember.ts @@ -1,4 +1,4 @@ -import type { Member as APIMember, MemberCompositeKey } from "revolt-api"; +import type { Member as APIMember, MemberCompositeKey } from "stoat-api"; import type { Client } from "../Client.js"; import { File } from "../classes/File.js"; diff --git a/src/hydration/session.ts b/src/hydration/session.ts index d16221e4..4d975c15 100644 --- a/src/hydration/session.ts +++ b/src/hydration/session.ts @@ -1,4 +1,4 @@ -import type { SessionInfo as APISession } from "revolt-api"; +import type { SessionInfo as APISession } from "stoat-api"; import type { Hydrate } from "./index.js"; diff --git a/src/hydration/user.ts b/src/hydration/user.ts index 952b5290..c7f2e50c 100644 --- a/src/hydration/user.ts +++ b/src/hydration/user.ts @@ -3,7 +3,7 @@ import type { BotInformation, RelationshipStatus, UserStatus, -} from "revolt-api"; +} from "stoat-api"; import type { Client } from "../Client.js"; import { File } from "../classes/File.js"; diff --git a/src/index.ts b/src/index.ts index 6c32bbd1..a8cd6c01 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -export * as API from "revolt-api"; +export * as API from "stoat-api"; export { Client } from "./Client.js"; export type { ClientOptions, Session as PrivateSession } from "./Client.js"; export * from "./classes/index.js"; diff --git a/src/permissions/calculator.ts b/src/permissions/calculator.ts index 2daab6ef..914bb851 100644 --- a/src/permissions/calculator.ts +++ b/src/permissions/calculator.ts @@ -99,8 +99,7 @@ export function calculatePermission( return target.permissions ?? DEFAULT_PERMISSION_DIRECT_MESSAGE; } } - case "TextChannel": - case "VoiceChannel": { + case "TextChannel": { // 2. Get server. const server = target.server; if (typeof server === "undefined") return 0; From db4b4913c7507ad7c6a0ba3e7ca909ef7731c105 Mon Sep 17 00:00:00 2001 From: izzy Date: Wed, 22 Oct 2025 17:50:33 +0100 Subject: [PATCH 09/31] chore: switch to bigint handling for permissions Signed-off-by: Taureon --- package.json | 5 +-- pnpm-lock.yaml | 20 ++++++++--- src/classes/Channel.ts | 16 ++++----- src/classes/Server.ts | 20 +++++------ src/classes/ServerMember.ts | 6 ++-- src/classes/ServerRole.ts | 12 +++++-- src/events/EventClient.ts | 22 ++++++------ src/events/v1.ts | 2 +- src/hydration/channel.ts | 21 +++++++---- src/hydration/message.ts | 2 +- src/hydration/server.ts | 4 +-- src/permissions/calculator.ts | 20 ++++++----- src/permissions/definitions.ts | 66 +++++++++++++++++----------------- tsconfig.json | 4 +-- 14 files changed, 125 insertions(+), 95 deletions(-) diff --git a/package.json b/package.json index f80736f3..dbb7bab7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "stoat.js", - "version": "7.3.1", + "version": "7.3.2", "type": "module", "exports": { ".": "./lib/index.js" @@ -29,8 +29,9 @@ "@solid-primitives/map": "^0.7.1", "@solid-primitives/set": "^0.7.1", "@vladfrangu/async_event_emitter": "^2.4.6", - "stoat-api": "0.8.9-3", + "json-with-bigint": "^3.4.4", "solid-js": "^1.9.6", + "stoat-api": "0.8.9-4", "ulid": "^2.4.0" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 33dc9bfd..25752324 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,12 +17,15 @@ importers: '@vladfrangu/async_event_emitter': specifier: ^2.4.6 version: 2.4.6 + json-with-bigint: + specifier: ^3.4.4 + version: 3.4.4 solid-js: specifier: ^1.9.6 version: 1.9.6 stoat-api: - specifier: 0.8.9-3 - version: 0.8.9-3 + specifier: 0.8.9-4 + version: 0.8.9-4 ulid: specifier: ^2.4.0 version: 2.4.0 @@ -718,6 +721,9 @@ packages: json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + json-with-bigint@3.4.4: + resolution: {integrity: sha512-AhpYAAaZsPjU7smaBomDt1SOQshi9rEm6BlTbfVwsG1vNmeHKtEedJi62sHZzJTyKNtwzmNnrsd55kjwJ7054A==} + kebab-case@1.0.2: resolution: {integrity: sha512-7n6wXq4gNgBELfDCpzKc+mRrZFs7D+wgfF5WRFLNAr4DA/qtr9Js8uOAVAfHhuLMfAcQ0pRKqbpjx+TcJVdE1Q==} @@ -975,8 +981,8 @@ packages: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} - stoat-api@0.8.9-3: - resolution: {integrity: sha512-cuw6+5HUQScBxjtA11bELCCBUaio2eESUfAZHYi9qj29VFlAalJ63GEZKF6U8h1UydqcQ7YNx/Q9TU5D/rWNhA==} + stoat-api@0.8.9-4: + resolution: {integrity: sha512-6N4kfE1x+j/XYVaBuvYqzbWxjgqbxSarvNjMv8GEcBaqbkiyPzUwsCj12NJsNNp4uyZmsqoUYUj2jIq018uqiA==} strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} @@ -1811,6 +1817,8 @@ snapshots: json-stable-stringify-without-jsonify@1.0.1: {} + json-with-bigint@3.4.4: {} + kebab-case@1.0.2: {} keyv@4.5.4: @@ -2060,7 +2068,9 @@ snapshots: statuses@2.0.1: {} - stoat-api@0.8.9-3: {} + stoat-api@0.8.9-4: + dependencies: + json-with-bigint: 3.4.4 strip-json-comments@3.1.1: {} diff --git a/src/classes/Channel.ts b/src/classes/Channel.ts index e4ac8a42..bf55f25a 100644 --- a/src/classes/Channel.ts +++ b/src/classes/Channel.ts @@ -12,7 +12,7 @@ import type { Invite, Override, } from "stoat-api"; -import type { APIRoutes } from "stoat-api/lib/routes"; +import type { APIRoutes } from "stoat-api"; import { decodeTime, ulid } from "ulid"; import { ChannelCollection } from "../collections/index.js"; @@ -229,21 +229,21 @@ export class Channel { /** * Permissions allowed for users in this group */ - get permissions(): number | undefined { + get permissions(): bigint | undefined { return this.#collection.getUnderlyingObject(this.id).permissions; } /** * Default permissions for this server channel */ - get defaultPermissions(): { a: number; d: number } | undefined { + get defaultPermissions(): { a: bigint; d: bigint } | undefined { return this.#collection.getUnderlyingObject(this.id).defaultPermissions; } /** * Role permissions for this server channel */ - get rolePermissions(): Record | undefined { + get rolePermissions(): Record | undefined { return this.#collection.getUnderlyingObject(this.id).rolePermissions; } @@ -349,16 +349,16 @@ export class Channel { get potentiallyRestrictedChannel(): string | boolean | undefined { if (!this.serverId) return false; return ( - bitwiseAndEq(this.defaultPermissions?.d ?? 0, Permission.ViewChannel) || + bitwiseAndEq(this.defaultPermissions?.d ?? 0n, Permission.ViewChannel) || !bitwiseAndEq(this.server!.defaultPermissions, Permission.ViewChannel) || [...(this.server?.roles.keys() ?? [])].find( (role) => bitwiseAndEq( - this.rolePermissions?.[role]?.d ?? 0, + this.rolePermissions?.[role]?.d ?? 0n, Permission.ViewChannel, ) || bitwiseAndEq( - this.server?.roles.get(role)?.permissions.d ?? 0, + this.server?.roles.get(role)?.permissions.d ?? 0n, Permission.ViewChannel, ), ) @@ -368,7 +368,7 @@ export class Channel { /** * Permission the currently authenticated user has against this channel */ - get permission(): number { + get permission(): bigint { return calculatePermission(this.#collection.client, this); } diff --git a/src/classes/Server.ts b/src/classes/Server.ts index eba16cb5..55725839 100644 --- a/src/classes/Server.ts +++ b/src/classes/Server.ts @@ -167,7 +167,7 @@ export class Server { /** * Default permissions */ - get defaultPermissions(): number { + get defaultPermissions(): bigint { return this.#collection.getUnderlyingObject(this.id).defaultPermissions; } @@ -271,7 +271,7 @@ export class Server { */ get orderedRoles(): { name: string; - permissions: OverrideField; + permissions: { a: bigint, d: bigint }; colour?: string | null; hoist?: boolean; rank?: number; @@ -337,7 +337,7 @@ export class Server { /** * Permission the currently authenticated user has against this server */ - get permission(): number { + get permission(): bigint { return calculatePermission(this.#collection.client, this); } @@ -514,12 +514,11 @@ export class Server { */ async kickUser(user: string | User | ServerMember): Promise { return await this.#collection.client.api.delete( - `/servers/${this.id as ""}/members/${ - typeof user === "string" - ? user - : user instanceof User - ? user.id - : user.id.user + `/servers/${this.id as ""}/members/${typeof user === "string" + ? user + : user instanceof User + ? user.id + : user.id.user }`, ); } @@ -723,8 +722,7 @@ export class Server { query: string, ): Promise<{ members: ServerMember[]; users: User[] }> { const data = (await this.#collection.client.api.get( - `/servers/${ - this.id as "" + `/servers/${this.id as "" }/members_experimental_query?experimental_api=true&query=${encodeURIComponent( query, )}` as never, diff --git a/src/classes/ServerMember.ts b/src/classes/ServerMember.ts index 14796d26..d25a615d 100644 --- a/src/classes/ServerMember.ts +++ b/src/classes/ServerMember.ts @@ -110,7 +110,7 @@ export class ServerMember { /** * Ordered list of roles for this member, from lowest to highest priority. */ - get orderedRoles(): (Partial & { id: string })[] { + get orderedRoles(): (Partial & { permissions: { a: bigint, d: bigint } }> & { id: string })[] { const server = this.server!; return ( this.roles @@ -125,7 +125,7 @@ export class ServerMember { /** * Member's currently hoisted role. */ - get hoistedRole(): Partial | null { + get hoistedRole(): Partial & { permissions: { a: bigint, d: bigint } }> | null { const roles = this.orderedRoles.filter((x) => x.hoist); if (roles.length > 0) { return roles[roles.length - 1]; @@ -168,7 +168,7 @@ export class ServerMember { * @param target Target object to check permissions against * @returns Permissions that this member has */ - getPermissions(target: Server | Channel): number { + getPermissions(target: Server | Channel): bigint { return calculatePermission(this.#collection.client, target, { member: this, }); diff --git a/src/classes/ServerRole.ts b/src/classes/ServerRole.ts index 7447fbc6..b8f5dbbe 100644 --- a/src/classes/ServerRole.ts +++ b/src/classes/ServerRole.ts @@ -1,4 +1,4 @@ -import type { Role as APIRole, OverrideField } from "stoat-api"; +import type { Role as APIRole } from "stoat-api"; import type { Client } from "../Client.js"; @@ -11,7 +11,10 @@ export class ServerRole { readonly id: string; readonly name: string; - readonly permissions: OverrideField; + readonly permissions: { + a: bigint, + d: bigint + }; readonly colour?: string; readonly hoist: boolean; readonly rank: number; @@ -29,7 +32,10 @@ export class ServerRole { this.id = id; this.name = data.name; - this.permissions = data.permissions; + this.permissions = { + a: BigInt(data.permissions.a), + d: BigInt(data.permissions.d) + }; this.colour = data.colour ?? undefined; this.hoist = data.hoist || false; this.rank = data.rank ?? 0; diff --git a/src/events/EventClient.ts b/src/events/EventClient.ts index 8b8f288c..93deebaa 100644 --- a/src/events/EventClient.ts +++ b/src/events/EventClient.ts @@ -6,6 +6,8 @@ import type { Error } from "stoat-api"; import type { ProtocolV1 } from "./v1.js"; +import { JSONParse, JSONStringify } from 'json-with-bigint'; + /** * Available protocols to connect with */ @@ -94,7 +96,7 @@ export class EventClient< #connectTimeoutReference: number | undefined; #lastError: // eslint-disable-next-line @typescript-eslint/no-explicit-any - { type: "socket"; data: any } | { type: "revolt"; data: Error } | undefined; + { type: "socket"; data: any } | { type: "revolt"; data: Error } | undefined; /** * Create a new event client. @@ -179,7 +181,7 @@ export class EventClient< if (this.#transportFormat === "json") { if (typeof event.data === "string") { - this.handle(JSON.parse(event.data)); + this.handle(JSONParse(event.data)); } } }; @@ -214,7 +216,7 @@ export class EventClient< send(event: EventProtocol["client"]): void { if (this.options.debug) console.debug("[C->S]", event); if (!this.#socket) throw "Socket closed, trying to send."; - this.#socket.send(JSON.stringify(event)); + this.#socket.send(JSONStringify(event)); } /** @@ -273,14 +275,14 @@ export class EventClient< */ get lastError(): | { - type: "socket"; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - data: any; - } + type: "socket"; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + data: any; + } | { - type: "revolt"; - data: Error; - } + type: "revolt"; + data: Error; + } | undefined { return this.#lastError; } diff --git a/src/events/v1.ts b/src/events/v1.ts index 6d20c087..282cb4ca 100644 --- a/src/events/v1.ts +++ b/src/events/v1.ts @@ -641,7 +641,7 @@ export async function handleEvent( let role = {}; const roles = server.roles; if (roles.has(event.role_id)) { - role = roles.get(event.role_id) as Role; + role = roles.get(event.role_id) as never; roles.delete(event.role_id); } else if (!client.servers.isPartial(event.id)) { return; diff --git a/src/hydration/channel.ts b/src/hydration/channel.ts index c0e18ea6..26895d35 100644 --- a/src/hydration/channel.ts +++ b/src/hydration/channel.ts @@ -23,9 +23,9 @@ export type HydratedChannel = { ownerId?: string; serverId?: string; - permissions?: number; - defaultPermissions?: OverrideField; - rolePermissions?: Record; + permissions?: bigint; + defaultPermissions?: { a: bigint, d: bigint }; + rolePermissions?: Record; nsfw: boolean; lastMessageId?: string; @@ -57,9 +57,18 @@ export const channelHydration: Hydrate, HydratedChannel> = { userId: (channel) => channel.user, ownerId: (channel) => channel.owner, serverId: (channel) => channel.server, - permissions: (channel) => channel.permissions!, - defaultPermissions: (channel) => channel.default_permissions!, - rolePermissions: (channel) => channel.role_permissions, + permissions: (channel) => BigInt(channel.permissions!), + defaultPermissions: (channel) => ({ + a: BigInt(channel.default_permissions?.a ?? 0), + d: BigInt(channel.default_permissions?.d ?? 0), + }), + rolePermissions: (channel) => Object.fromEntries( + Object.entries(channel.role_permissions ?? {}) + .map(([k, v]) => [k, { + a: BigInt(v.a), + d: BigInt(v.d) + }]) + ), nsfw: (channel) => channel.nsfw || false, lastMessageId: (channel) => channel.last_message_id!, voice: (channel) => { diff --git a/src/hydration/message.ts b/src/hydration/message.ts index 81e3a6d6..1674b102 100644 --- a/src/hydration/message.ts +++ b/src/hydration/message.ts @@ -1,6 +1,6 @@ import { ReactiveMap } from "@solid-primitives/map"; import { ReactiveSet } from "@solid-primitives/set"; -import type { Interactions, Masquerade, Message } from "stoat-api"; +import type { Embed, Interactions, Masquerade, Message } from "stoat-api"; import type { Client } from "../Client.js"; import { File } from "../classes/File.js"; diff --git a/src/hydration/server.ts b/src/hydration/server.ts index e5128ad6..87a4475a 100644 --- a/src/hydration/server.ts +++ b/src/hydration/server.ts @@ -27,7 +27,7 @@ export type HydratedServer = { systemMessages?: SystemMessageChannels; roles: ReactiveMap; - defaultPermissions: number; + defaultPermissions: bigint; flags: ServerFlags; analytics: boolean; @@ -58,7 +58,7 @@ export const serverHydration: Hydrate = { new ServerRole(ctx as Client, server._id, id, server.roles![id]), ]), ), - defaultPermissions: (server) => server.default_permissions, + defaultPermissions: (server) => BigInt(server.default_permissions), icon: (server, ctx) => new File(ctx as Client, server.icon!), banner: (server, ctx) => new File(ctx as Client, server.banner!), flags: (server) => server.flags!, diff --git a/src/permissions/calculator.ts b/src/permissions/calculator.ts index 914bb851..1d88ca54 100644 --- a/src/permissions/calculator.ts +++ b/src/permissions/calculator.ts @@ -13,9 +13,9 @@ import { * @param a Input A * @param b Inputs (OR'd together) */ -export function bitwiseAndEq(a: number, ...b: number[]): boolean { - const value = b.reduce((prev, cur) => prev | BigInt(cur), 0n); - return (value & BigInt(a)) === value; +export function bitwiseAndEq(a: bigint, ...b: bigint[]): boolean { + const value = b.reduce((prev, cur) => prev | cur, 0n); + return (value & a) === value; } /** @@ -32,7 +32,7 @@ export function calculatePermission( */ member?: ServerMember; }, -): number { +): bigint { const user = options?.member ? options?.member.user : client.user; if (user?.privileged) { return Permission.GrantAllSafe; @@ -50,7 +50,7 @@ export function calculatePermission( server: target.id, }) ?? { roles: null, timeout: null }; - if (!member) return 0; + if (!member) return 0n; // 3. Apply allows from default_permissions. let perm = BigInt(target.defaultPermissions); @@ -72,7 +72,7 @@ export function calculatePermission( perm = perm & BigInt(ALLOW_IN_TIMEOUT); } - return Number(perm); + return perm; } } else { // 1. Check channel type. @@ -102,7 +102,7 @@ export function calculatePermission( case "TextChannel": { // 2. Get server. const server = target.server; - if (typeof server === "undefined") return 0; + if (typeof server === "undefined") return 0n; // 3. If server owner, just grant all permissions. if (server.ownerId === user?.id) { @@ -115,7 +115,7 @@ export function calculatePermission( server: server.id, }) ?? { roles: null, timeout: null }; - if (!member) return 0; + if (!member) return 0n; // 5. Calculate server base permissions. let perm = BigInt(calculatePermission(client, server, options)); @@ -145,9 +145,11 @@ export function calculatePermission( perm = perm & BigInt(ALLOW_IN_TIMEOUT); } - return Number(perm); + return perm; } } } + + return 0n; } } diff --git a/src/permissions/definitions.ts b/src/permissions/definitions.ts index c6834b36..e970f7b4 100644 --- a/src/permissions/definitions.ts +++ b/src/permissions/definitions.ts @@ -14,79 +14,81 @@ export const UserPermission = { export const Permission = { // * Generic permissions /// Manage the channel or channels on the server - ManageChannel: 2 ** 0, + ManageChannel: 2n ** 0n, /// Manage the server - ManageServer: 2 ** 1, + ManageServer: 2n ** 1n, /// Manage permissions on servers or channels - ManagePermissions: 2 ** 2, + ManagePermissions: 2n ** 2n, /// Manage roles on server - ManageRole: 2 ** 3, + ManageRole: 2n ** 3n, /// Manage server customisation (includes emoji) - ManageCustomisation: 2 ** 4, + ManageCustomisation: 2n ** 4n, // % 1 bits reserved // * Member permissions /// Kick other members below their ranking - KickMembers: 2 ** 6, + KickMembers: 2n ** 6n, /// Ban other members below their ranking - BanMembers: 2 ** 7, + BanMembers: 2n ** 7n, /// Timeout other members below their ranking - TimeoutMembers: 2 ** 8, + TimeoutMembers: 2n ** 8n, /// Assign roles to members below their ranking - AssignRoles: 2 ** 9, + AssignRoles: 2n ** 9n, /// Change own nickname - ChangeNickname: 2 ** 10, + ChangeNickname: 2n ** 10n, /// Change or remove other's nicknames below their ranking - ManageNicknames: 2 ** 11, + ManageNicknames: 2n ** 11n, /// Change own avatar - ChangeAvatar: 2 ** 12, + ChangeAvatar: 2n ** 12n, /// Remove other's avatars below their ranking - RemoveAvatars: 2 ** 13, + RemoveAvatars: 2n ** 13n, // % 7 bits reserved // * Channel permissions /// View a channel - ViewChannel: 2 ** 20, + ViewChannel: 2n ** 20n, /// Read a channel's past message history - ReadMessageHistory: 2 ** 21, + ReadMessageHistory: 2n ** 21n, /// Send a message in a channel - SendMessage: 2 ** 22, + SendMessage: 2n ** 22n, /// Delete messages in a channel - ManageMessages: 2 ** 23, + ManageMessages: 2n ** 23n, /// Manage webhook entries on a channel - ManageWebhooks: 2 ** 24, + ManageWebhooks: 2n ** 24n, /// Create invites to this channel - InviteOthers: 2 ** 25, + InviteOthers: 2n ** 25n, /// Send embedded content in this channel - SendEmbeds: 2 ** 26, + SendEmbeds: 2n ** 26n, /// Send attachments and media in this channel - UploadFiles: 2 ** 27, + UploadFiles: 2n ** 27n, /// Masquerade messages using custom nickname and avatar - Masquerade: 2 ** 28, + Masquerade: 2n ** 28n, /// React to messages with emoji - React: 2 ** 29, + React: 2n ** 29n, // * Voice permissions /// Connect to a voice channel - Connect: 2 ** 30, + Connect: 2n ** 30n, /// Speak in a voice call - Speak: 2 ** 31, + Speak: 2n ** 31n, /// Share video in a voice call - Video: 2 ** 32, + Video: 2n ** 32n, /// Mute other members with lower ranking in a voice call - MuteMembers: 2 ** 33, + MuteMembers: 2n ** 33n, /// Deafen other members with lower ranking in a voice call - DeafenMembers: 2 ** 34, + DeafenMembers: 2n ** 34n, /// Move members between voice channels - MoveMembers: 2 ** 35, + MoveMembers: 2n ** 35n, + /// Move members between voice channels + Listen: 2n ** 36n, // * Mention permissions /// Mention @everyone or @online - MentionEveryone: 2 ** 37, + MentionEveryone: 2n ** 37n, /// Mention a role - MentionRoles: 2 ** 38, + MentionRoles: 2n ** 38n, // * Misc. permissions // % Bits 39 to 52: free area @@ -94,7 +96,7 @@ export const Permission = { // * Grant all permissions /// Safely grant all permissions - GrantAllSafe: 0x000f_ffff_ffff_ffff, + GrantAllSafe: 0x000f_ffff_ffff_ffffn, }; /** diff --git a/tsconfig.json b/tsconfig.json index a39ec41d..e72d721d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,8 +1,8 @@ { "compilerOptions": { "target": "ES2022", - "module": "ES6", - "moduleResolution": "node", + "module": "es6", + "moduleResolution": "bundler", "rootDir": "./src", "declaration": true, "declarationMap": true, From 1fcfc69c8d9ade3ff688ed6e070b871980a3d0cb Mon Sep 17 00:00:00 2001 From: izzy Date: Wed, 22 Oct 2025 18:06:16 +0100 Subject: [PATCH 10/31] chore: add Video and Listen to default permission set Signed-off-by: Taureon --- package.json | 2 +- src/permissions/definitions.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index dbb7bab7..4d311367 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "stoat.js", - "version": "7.3.2", + "version": "7.3.3", "type": "module", "exports": { ".": "./lib/index.js" diff --git a/src/permissions/definitions.ts b/src/permissions/definitions.ts index e970f7b4..4ecf373d 100644 --- a/src/permissions/definitions.ts +++ b/src/permissions/definitions.ts @@ -126,7 +126,9 @@ export const DEFAULT_PERMISSION = Permission.SendEmbeds + Permission.UploadFiles + Permission.Connect + - Permission.Speak; + Permission.Speak + + Permission.Video + + Permission.Listen; /** * Permissions in saved messages channel From 1dbf2bf32fa78aaaebb7685cddfcc75ac9c4ddf7 Mon Sep 17 00:00:00 2001 From: izzy Date: Wed, 22 Oct 2025 20:25:58 +0100 Subject: [PATCH 11/31] feat: add VoiceParticipant and voice state management Signed-off-by: Taureon --- src/classes/Channel.ts | 6 +- src/classes/VoiceParticipant.ts | 72 ++++++++++++++++++++ src/classes/index.ts | 1 + src/events/EventClient.ts | 26 ++++++-- src/events/v1.ts | 112 +++++++++++++++++++++++++++++++- src/hydration/channel.ts | 35 +++++----- 6 files changed, 229 insertions(+), 23 deletions(-) create mode 100644 src/classes/VoiceParticipant.ts diff --git a/src/classes/Channel.ts b/src/classes/Channel.ts index bf55f25a..1ff18d38 100644 --- a/src/classes/Channel.ts +++ b/src/classes/Channel.ts @@ -29,6 +29,8 @@ import type { Message } from "./Message.js"; import type { Server } from "./Server.js"; import type { ServerMember } from "./ServerMember.js"; import type { User } from "./User.js"; +import { ReactiveMap } from "@solid-primitives/map"; +import { VoiceParticipant } from "./VoiceParticipant.js"; /** * Channel Class @@ -39,6 +41,8 @@ export class Channel { _typingTimers: Record = {}; + voiceParticipants = new ReactiveMap(); + /** * Construct Channel * @param collection Collection @@ -326,7 +330,7 @@ export class Channel { * NB. subject to change as vc(2) goes to production */ get isVoice(): boolean { - return this.type === 'Group' || this.type === 'DirectMessage' || this.#collection.getUnderlyingObject(this.id).voice; + return typeof this.#collection.getUnderlyingObject(this.id).voice === 'object'; } /** diff --git a/src/classes/VoiceParticipant.ts b/src/classes/VoiceParticipant.ts new file mode 100644 index 00000000..b97172ac --- /dev/null +++ b/src/classes/VoiceParticipant.ts @@ -0,0 +1,72 @@ + +import { Accessor, createSignal, Setter } from "solid-js"; +import type { Client } from "../Client.js"; +import { UserVoiceState } from "../events/v1.js"; + +/** + * Voice Participant + */ +export class VoiceParticipant { + protected client: Client; + readonly userId: string; + readonly joinedAt: Date; + + readonly isReceiving: Accessor; + readonly isPublishing: Accessor; + readonly isScreensharing: Accessor; + readonly isCamera: Accessor; + + #setReceiving: Setter; + #setPublishing: Setter; + #setScreensharing: Setter; + #setCamera: Setter; + + /** + * Construct Server Ban + * @param client Client + * @param data Data + */ + constructor(client: Client, data: UserVoiceState) { + this.client = client; + this.userId = data.id; + this.joinedAt = new Date(data.joined_at); + + const [isReceiving, setReceiving] = createSignal(data.is_receiving); + this.isReceiving = isReceiving; + this.#setReceiving = setReceiving; + + const [isPublishing, setPublishing] = createSignal(data.is_publishing); + this.isPublishing = isPublishing; + this.#setPublishing = setPublishing; + + const [isScreensharing, setScreensharing] = createSignal(data.screensharing); + this.isScreensharing = isScreensharing; + this.#setScreensharing = setScreensharing; + + const [isCamera, setCamera] = createSignal(data.camera); + this.isCamera = isCamera; + this.#setCamera = setCamera; + } + + /** + * Update the state + * @param data Data + */ + update(data: Partial) { + if (typeof data.is_receiving === 'boolean') { + this.#setReceiving(data.is_receiving); + } + + if (typeof data.is_publishing === 'boolean') { + this.#setPublishing(data.is_publishing); + } + + if (typeof data.screensharing === 'boolean') { + this.#setScreensharing(data.screensharing); + } + + if (typeof data.camera === 'boolean') { + this.#setCamera(data.camera); + } + } +} diff --git a/src/classes/index.ts b/src/classes/index.ts index 9bd7e417..07c8af39 100644 --- a/src/classes/index.ts +++ b/src/classes/index.ts @@ -19,3 +19,4 @@ export * from "./SystemMessage.js"; export * from "./User.js"; export * from "./MFA.js"; export * from "./UserProfile.js"; +export * from "./VoiceParticipant.js"; diff --git a/src/events/EventClient.ts b/src/events/EventClient.ts index 93deebaa..e6f37f12 100644 --- a/src/events/EventClient.ts +++ b/src/events/EventClient.ts @@ -2,12 +2,11 @@ import type { Accessor, Setter } from "solid-js"; import { createSignal } from "solid-js"; import { AsyncEventEmitter } from "@vladfrangu/async_event_emitter"; +import { JSONParse, JSONStringify } from "json-with-bigint"; import type { Error } from "stoat-api"; import type { ProtocolV1 } from "./v1.js"; -import { JSONParse, JSONStringify } from 'json-with-bigint'; - /** * Available protocols to connect with */ @@ -157,9 +156,26 @@ export class EventClient< this.options.pongTimeout * 1e3, ) as never; - this.#socket = new WebSocket( - `${uri}?version=${this.#protocolVersion}&format=${this.#transportFormat}&token=${token}`, - ); + const url = new URL(uri); + url.searchParams.set("version", this.#protocolVersion.toString()); + url.searchParams.set("format", this.#transportFormat); + url.searchParams.set("token", token); + + // todo: pass-through ts as a configuration option + // todo: then remove /settings/fetch from web client + // todo: do the same for unreads + // url.searchParams.append("ready", "users"); + // url.searchParams.append("ready", "servers"); + // url.searchParams.append("ready", "channels"); + // url.searchParams.append("ready", "members"); + // url.searchParams.append("ready", "emojis"); + // url.searchParams.append("ready", "voice_states"); + // url.searchParams.append("ready", "user_settings[ordering]"); + // url.searchParams.append("ready", "user_settings[notifications]"); + // url.searchParams.append("ready", "unreads or something"); + // url.searchParams.append("ready", "policy_changes"); + + this.#socket = new WebSocket(url); this.#socket.onopen = () => { this.#heartbeatIntervalReference = setInterval(() => { diff --git a/src/events/v1.ts b/src/events/v1.ts index 282cb4ca..9f4239e1 100644 --- a/src/events/v1.ts +++ b/src/events/v1.ts @@ -4,6 +4,7 @@ import { batch } from "solid-js"; import { ReactiveSet } from "@solid-primitives/set"; import type { Channel, + ChannelUnread, Emoji, Error, FieldsChannel, @@ -22,6 +23,7 @@ import type { import type { Client } from "../Client.js"; import { MessageEmbed } from "../classes/MessageEmbed.js"; import { ServerRole } from "../classes/ServerRole.js"; +import { VoiceParticipant } from "../classes/VoiceParticipant.js"; import { hydrate } from "../hydration/index.js"; /** @@ -173,7 +175,35 @@ type ServerMessage = user_id: string; exclude_session_id: string; } - )); + )) + | { + type: "VoiceChannelJoin"; + id: string; + state: UserVoiceState; + } + | { + type: "VoiceChannelLeave"; + id: string; + user: string; + } + | { + type: "VoiceChannelMove"; + user: string; + from: string; + to: string; + state: UserVoiceState; + } + | { + type: "UserVoiceStateUpdate"; + id: string; + channel_id: string; + data: Partial; + } + | { + type: "UserMoveVoiceChannel"; + node: string; + token: string; + }; /** * Policy change type @@ -185,6 +215,26 @@ type PolicyChange = { url: string; }; +/** + * Voice state for a user + */ +export type UserVoiceState = { + id: string; + joined_at: number; + is_receiving: boolean; + is_publishing: boolean; + screensharing: boolean; + camera: boolean; +}; + +/** + * Voice state for a channel + */ +type ChannelVoiceState = { + id: string; + participants: UserVoiceState[]; +}; + /** * Initial synchronisation packet */ @@ -194,6 +244,11 @@ type ReadyData = { channels: Channel[]; members: Member[]; emojis: Emoji[]; + voice_states: ChannelVoiceState[]; + + user_settings: Record; + channel_unreads: ChannelUnread[]; + policy_changes: PolicyChange[]; }; @@ -241,6 +296,20 @@ export async function handleEvent( client.channels.getOrCreate(channel._id, channel); } + for (const state of event.voice_states) { + const channel = client.channels.get(state.id); + if (channel) { + channel.voiceParticipants.clear(); + + for (const participant of state.participants) { + channel.voiceParticipants.set( + participant.id, + new VoiceParticipant(client, participant), + ); + } + } + } + for (const emoji of event.emojis) { client.emojis.getOrCreate(emoji._id, emoji); } @@ -280,7 +349,11 @@ export async function handleEvent( const channel = client.channels.get(event.channel); if (!channel) return; - client.channels.updateUnderlyingObject(channel.id, "lastMessageId", event._id); + client.channels.updateUnderlyingObject( + channel.id, + "lastMessageId", + event._id, + ); if ( event.mentions?.includes(client.user!.id) && @@ -865,5 +938,40 @@ export async function handleEvent( // TODO: implement DeleteSession and DeleteAllSessions break; } + case "VoiceChannelJoin": { + const channel = client.channels.getOrPartial(event.id); + if (channel) { + channel.voiceParticipants.set( + event.state.id, + new VoiceParticipant(client, event.state), + ); + // todo: event + } + break; + } + case "VoiceChannelLeave": { + const channel = client.channels.getOrPartial(event.id); + if (channel) { + channel.voiceParticipants.delete(event.user); + // todo: event + } + break; + } + case "VoiceChannelMove": { + // todo + break; + } + case "UserVoiceStateUpdate": { + const channel = client.channels.getOrPartial(event.channel_id); + if (channel) { + channel.voiceParticipants.get(event.id)?.update(event.data); + // todo: event + } + break; + } + case "UserMoveVoiceChannel": { + // todo + break; + } } } diff --git a/src/hydration/channel.ts b/src/hydration/channel.ts index 26895d35..ed7c2a92 100644 --- a/src/hydration/channel.ts +++ b/src/hydration/channel.ts @@ -1,11 +1,13 @@ import { ReactiveSet } from "@solid-primitives/set"; -import type { Channel as APIChannel, OverrideField } from "stoat-api"; +import type { Channel as APIChannel } from "stoat-api"; import type { Client } from "../Client.js"; import { File } from "../classes/File.js"; import type { Merge } from "../lib/merge.js"; import type { Hydrate } from "./index.js"; +import { VoiceParticipant } from "../classes/VoiceParticipant.js"; +import { ReactiveMap } from "@solid-primitives/map"; export type HydratedChannel = { id: string; @@ -24,13 +26,13 @@ export type HydratedChannel = { serverId?: string; permissions?: bigint; - defaultPermissions?: { a: bigint, d: bigint }; - rolePermissions?: Record; + defaultPermissions?: { a: bigint; d: bigint }; + rolePermissions?: Record; nsfw: boolean; lastMessageId?: string; - voice: boolean; + voice?: { maxUsers?: number }; }; export const channelHydration: Hydrate, HydratedChannel> = { @@ -62,19 +64,22 @@ export const channelHydration: Hydrate, HydratedChannel> = { a: BigInt(channel.default_permissions?.a ?? 0), d: BigInt(channel.default_permissions?.d ?? 0), }), - rolePermissions: (channel) => Object.fromEntries( - Object.entries(channel.role_permissions ?? {}) - .map(([k, v]) => [k, { - a: BigInt(v.a), - d: BigInt(v.d) - }]) - ), + rolePermissions: (channel) => + Object.fromEntries( + Object.entries(channel.role_permissions ?? {}).map(([k, v]) => [ + k, + { + a: BigInt(v.a), + d: BigInt(v.d), + }, + ]), + ), nsfw: (channel) => channel.nsfw || false, lastMessageId: (channel) => channel.last_message_id!, - voice: (channel) => { - console.info(channel); - return typeof (channel as never as { voice: object }).voice === "object"; - }, + voice: (channel) => + !!channel.voice || channel.channel_type === 'DirectMessage' || channel.channel_type === 'Group' ? ({ + maxUsers: channel.voice?.max_users || undefined, + }) : undefined, }, initialHydration: () => ({ typingIds: new ReactiveSet(), From 8321f5e452bab1a4583d1215d0ccf9628e0a6cbf Mon Sep 17 00:00:00 2001 From: izzy Date: Wed, 22 Oct 2025 21:04:14 +0100 Subject: [PATCH 12/31] feat: add Channel#joinCall Signed-off-by: Taureon --- src/classes/Channel.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/classes/Channel.ts b/src/classes/Channel.ts index 1ff18d38..dd4af61d 100644 --- a/src/classes/Channel.ts +++ b/src/classes/Channel.ts @@ -795,6 +795,22 @@ export class Channel { ); } + /** + * Join a call + * @param node Target node + * @param forceDisconnect Whether to disconnect existing call + * @param recipients Ring targets + * @returns LiveKit URL and Token + */ + async joinCall(node = undefined, forceDisconnect = true, recipients: (User | string)[]) { + return await this.#collection.client.api.post( + `/channels/${this.id as ''}/join_call`, { + node, + recipients: recipients.map(entry => typeof entry === 'string' ? entry : entry.id), + force_disconnect: forceDisconnect + }); + } + /** * Start typing in this channel * @requires `DirectMessage`, `Group`, `TextChannel` From e84fafa4ee8a8d17943620b2826cdd56b8c292c5 Mon Sep 17 00:00:00 2001 From: izzy Date: Wed, 22 Oct 2025 21:07:19 +0100 Subject: [PATCH 13/31] fix: incorrect signature for Channel#joinCall Signed-off-by: Taureon --- src/classes/Channel.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/classes/Channel.ts b/src/classes/Channel.ts index dd4af61d..073780dd 100644 --- a/src/classes/Channel.ts +++ b/src/classes/Channel.ts @@ -802,7 +802,7 @@ export class Channel { * @param recipients Ring targets * @returns LiveKit URL and Token */ - async joinCall(node = undefined, forceDisconnect = true, recipients: (User | string)[]) { + async joinCall(node?: string, forceDisconnect = true, recipients: (User | string)[] = []) { return await this.#collection.client.api.post( `/channels/${this.id as ''}/join_call`, { node, From 987a33c0672a458902abc9daaa752c319ea57b1d Mon Sep 17 00:00:00 2001 From: Aeledfyr <45501007+Aeledfyr@users.noreply.github.com> Date: Wed, 22 Oct 2025 15:27:16 -0500 Subject: [PATCH 14/31] fix: increase typing timeout to 4s (#119) The frontend only sends typing updates every 2.5-3s, so the 1s timeout caused the typing indicator to flicker. Signed-off-by: Aeledfyr Signed-off-by: Taureon --- src/events/v1.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/events/v1.ts b/src/events/v1.ts index 9f4239e1..32821dd3 100644 --- a/src/events/v1.ts +++ b/src/events/v1.ts @@ -599,7 +599,7 @@ export async function handleEvent( { ...event, type: "ChannelStopTyping" }, setReady, ), - 1000, + 4000, ) as never; client.emit( From b34b1de03ed10624985faf8321bedb985deaf30e Mon Sep 17 00:00:00 2001 From: Angelo Kontaxis Date: Wed, 22 Oct 2025 21:28:09 +0100 Subject: [PATCH 15/31] feat: add call_started system message (#120) Signed-off-by: Taureon --- src/classes/SystemMessage.ts | 41 ++++++++++++++++++++++++++++++++++-- src/hydration/message.ts | 4 ++-- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/src/classes/SystemMessage.ts b/src/classes/SystemMessage.ts index 4b053344..0bfac303 100644 --- a/src/classes/SystemMessage.ts +++ b/src/classes/SystemMessage.ts @@ -1,9 +1,10 @@ -import type { SystemMessage as APISystemMessage } from "stoat-api"; +import type { SystemMessage as APISystemMessage, Message as APIMessage } from "stoat-api"; import type { Client } from "../Client.js"; import type { User } from "./User.js"; import { Message } from "./index.js"; +import { decodeTime } from "ulid"; /** * System Message @@ -28,7 +29,7 @@ export abstract class SystemMessage { * @param embed Data * @returns System Message */ - static from(client: Client, message: APISystemMessage): SystemMessage { + static from(client: Client, parent: APIMessage, message: APISystemMessage): SystemMessage { switch (message.type) { case "text": return new TextSystemMessage(client, message); @@ -50,6 +51,8 @@ export abstract class SystemMessage { case "message_pinned": case "message_unpinned": return new MessagePinnedSystemMessage(client, message); + case "call_started": + return new CallStartedSystemMessage(client, parent, message); default: return new TextSystemMessage(client, { type: "text", @@ -272,3 +275,37 @@ export class MessagePinnedSystemMessage extends SystemMessage { return this.client!.users.get(this.byId); } } + +/** + * Call Started System Message + */ +export class CallStartedSystemMessage extends SystemMessage { + readonly byId: string; + readonly startedAt: Date; + readonly finishedAt: Date | null; + /** + * Construct System Message + * @param client Client + * @param parent Message + * @param systemMessage System Message + */ + constructor( + client: Client, + parent: APIMessage, + systemMessage: APISystemMessage & { + type: "call_started"; + }, + ) { + super(client, systemMessage.type); + this.byId = systemMessage.by; + this.startedAt = new Date(decodeTime(parent._id)); + this.finishedAt = systemMessage.finished_at != null ? new Date(systemMessage.finished_at) : null; + } + + /** + * User that started the call + */ + get by(): User | undefined { + return this.client!.users.get(this.byId); + } +} diff --git a/src/hydration/message.ts b/src/hydration/message.ts index 1674b102..6cd84c3f 100644 --- a/src/hydration/message.ts +++ b/src/hydration/message.ts @@ -53,7 +53,7 @@ export const messageHydration: Hydrate, HydratedMessage> = { : undefined, content: (message) => message.content!, systemMessage: (message, ctx) => - SystemMessage.from(ctx as Client, message.system!), + SystemMessage.from(ctx as Client, message, message.system!), attachments: (message, ctx) => message.attachments!.map((file) => new File(ctx as Client, file)), editedAt: (message) => new Date(message.edited!), @@ -98,4 +98,4 @@ export enum MessageFlags { * This cannot be true if MentionsEveryone is true */ MentionsOnline = 3, -} +} \ No newline at end of file From 8bb64f792f9269858b53f9528fdbe76626c877b7 Mon Sep 17 00:00:00 2001 From: izzy Date: Thu, 23 Oct 2025 11:38:02 +0100 Subject: [PATCH 16/31] fix: don't include recipients array if not necessary Signed-off-by: Taureon --- package.json | 2 +- src/classes/Channel.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 4d311367..1e3f0863 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "stoat.js", - "version": "7.3.3", + "version": "7.3.4", "type": "module", "exports": { ".": "./lib/index.js" diff --git a/src/classes/Channel.ts b/src/classes/Channel.ts index 073780dd..17bc5aca 100644 --- a/src/classes/Channel.ts +++ b/src/classes/Channel.ts @@ -802,11 +802,11 @@ export class Channel { * @param recipients Ring targets * @returns LiveKit URL and Token */ - async joinCall(node?: string, forceDisconnect = true, recipients: (User | string)[] = []) { + async joinCall(node?: string, forceDisconnect = true, recipients?: (User | string)[]) { return await this.#collection.client.api.post( `/channels/${this.id as ''}/join_call`, { node, - recipients: recipients.map(entry => typeof entry === 'string' ? entry : entry.id), + recipients: recipients?.map(entry => typeof entry === 'string' ? entry : entry.id), force_disconnect: forceDisconnect }); } From 6698c9d16073acdf183f260702a56e36a1215a30 Mon Sep 17 00:00:00 2001 From: izzy Date: Thu, 23 Oct 2025 13:34:31 +0100 Subject: [PATCH 17/31] fix: consider DMs as voice channels Signed-off-by: Taureon --- src/classes/Channel.ts | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/src/classes/Channel.ts b/src/classes/Channel.ts index 17bc5aca..ea087113 100644 --- a/src/classes/Channel.ts +++ b/src/classes/Channel.ts @@ -1,5 +1,6 @@ import { batch } from "solid-js"; +import { ReactiveMap } from "@solid-primitives/map"; import type { ReactiveSet } from "@solid-primitives/set"; import type { Channel as APIChannel, @@ -29,7 +30,6 @@ import type { Message } from "./Message.js"; import type { Server } from "./Server.js"; import type { ServerMember } from "./ServerMember.js"; import type { User } from "./User.js"; -import { ReactiveMap } from "@solid-primitives/map"; import { VoiceParticipant } from "./VoiceParticipant.js"; /** @@ -317,8 +317,7 @@ export class Channel { * Get mentions in this channel for user. */ get mentions(): ReactiveSet | undefined { - if (this.type === "SavedMessages") - return undefined; + if (this.type === "SavedMessages") return undefined; return this.#collection.client.channelUnreads.get(this.id) ?.messageMentionIds; @@ -330,7 +329,11 @@ export class Channel { * NB. subject to change as vc(2) goes to production */ get isVoice(): boolean { - return typeof this.#collection.getUnderlyingObject(this.id).voice === 'object'; + return ( + this.type === "DirectMessage" || + this.type === "Group" || + typeof this.#collection.getUnderlyingObject(this.id).voice === "object" + ); } /** @@ -797,18 +800,26 @@ export class Channel { /** * Join a call - * @param node Target node + * @param node Target node * @param forceDisconnect Whether to disconnect existing call * @param recipients Ring targets * @returns LiveKit URL and Token */ - async joinCall(node?: string, forceDisconnect = true, recipients?: (User | string)[]) { + async joinCall( + node?: string, + forceDisconnect = true, + recipients?: (User | string)[], + ) { return await this.#collection.client.api.post( - `/channels/${this.id as ''}/join_call`, { - node, - recipients: recipients?.map(entry => typeof entry === 'string' ? entry : entry.id), - force_disconnect: forceDisconnect - }); + `/channels/${this.id as ""}/join_call`, + { + node, + recipients: recipients?.map((entry) => + typeof entry === "string" ? entry : entry.id, + ), + force_disconnect: forceDisconnect, + }, + ); } /** From 1e7aa530d4956e6e8a04f9c712ee55bd93615c01 Mon Sep 17 00:00:00 2001 From: izzy Date: Sun, 16 Nov 2025 14:22:33 +0000 Subject: [PATCH 18/31] fix: create user object when creating bot Signed-off-by: Taureon --- package.json | 2 +- src/collections/BotCollection.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 1e3f0863..c3c9b88e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "stoat.js", - "version": "7.3.4", + "version": "7.3.5", "type": "module", "exports": { ".": "./lib/index.js" diff --git a/src/collections/BotCollection.ts b/src/collections/BotCollection.ts index ba3f62a1..846d5cfc 100644 --- a/src/collections/BotCollection.ts +++ b/src/collections/BotCollection.ts @@ -71,10 +71,11 @@ export class BotCollection extends ClassCollection { * @returns The newly-created bot */ async createBot(name: string): Promise { - const bot = await this.client.api.post(`/bots/create`, { + const { user, ...bot } = await this.client.api.post(`/bots/create`, { name, }); + this.client.users.getOrCreate(user._id, user); return this.getOrCreate(bot._id, bot); } } From 96d704d7a4a1aa910c80ca7532ce1ad1b7b0da80 Mon Sep 17 00:00:00 2001 From: Amy <138383945+amycatgirl@users.noreply.github.com> Date: Thu, 1 Jan 2026 13:50:43 -0400 Subject: [PATCH 19/31] feat: add nix shell (#121) Signed-off-by: Amy Signed-off-by: Taureon --- default.nix | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 default.nix diff --git a/default.nix b/default.nix new file mode 100644 index 00000000..52387226 --- /dev/null +++ b/default.nix @@ -0,0 +1,19 @@ +{ + pkgs ? import { }, +}: + +with pkgs; +pkgs.mkShell { + name = "stoatEnv"; + + buildInputs = [ + # Tools + git + gh + deno + + # Node + nodejs + nodejs.pkgs.pnpm + ]; +} From fcb26af2220e45f0ba5ff9540ef028e0cc66e924 Mon Sep 17 00:00:00 2001 From: Amy <138383945+amycatgirl@users.noreply.github.com> Date: Thu, 1 Jan 2026 13:51:30 -0400 Subject: [PATCH 20/31] fix: missing undefined check on policy_changes (#122) * feat: add nix shell Signed-off-by: Amy * fix: missing undefined check in policy_changes Signed-off-by: Amy --------- Signed-off-by: Amy Signed-off-by: Taureon --- src/events/v1.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/events/v1.ts b/src/events/v1.ts index 32821dd3..9a23a75b 100644 --- a/src/events/v1.ts +++ b/src/events/v1.ts @@ -322,7 +322,7 @@ export async function handleEvent( setReady(true); client.emit("ready"); - if (event.policy_changes.length) { + if (event.policy_changes?.length) { client.emit("policyChanges", event.policy_changes, async () => client.api.post("/policy/acknowledge"), ); From e2ffcca3a02e76a3d76993c9c7ca7c2fa62ea89e Mon Sep 17 00:00:00 2001 From: Amy <138383945+amycatgirl@users.noreply.github.com> Date: Fri, 16 Jan 2026 14:28:35 -0400 Subject: [PATCH 21/31] fix: make ReadyData partial to match event documentation (#123) Signed-off-by: Amy Signed-off-by: Taureon --- src/events/v1.ts | 271 ++++++++++++++++++++++++----------------------- 1 file changed, 141 insertions(+), 130 deletions(-) diff --git a/src/events/v1.ts b/src/events/v1.ts index 9a23a75b..eb51b2f4 100644 --- a/src/events/v1.ts +++ b/src/events/v1.ts @@ -44,21 +44,21 @@ export type ProtocolV1 = { type ClientMessage = | { type: "Authenticate"; token: string } | { - type: "BeginTyping"; - channel: string; - } + type: "BeginTyping"; + channel: string; + } | { - type: "EndTyping"; - channel: string; - } + type: "EndTyping"; + channel: string; + } | { - type: "Ping"; - data: number; - } + type: "Ping"; + data: number; + } | { - type: "Pong"; - data: number; - }; + type: "Pong"; + data: number; + }; /** * Messages sent from the server @@ -67,51 +67,51 @@ type ServerMessage = | { type: "Error"; data: Error } | { type: "Bulk"; v: ServerMessage[] } | { type: "Authenticated" } - | ({ type: "Ready" } & ReadyData) + | ({ type: "Ready" } & Partial) | { type: "Ping"; data: number } | { type: "Pong"; data: number } | ({ type: "Message" } & Message) | { - type: "MessageUpdate"; - id: string; - channel: string; - data: Partial; - } + type: "MessageUpdate"; + id: string; + channel: string; + data: Partial; + } | { - type: "MessageAppend"; - id: string; - channel: string; - append: Pick, "embeds">; - } + type: "MessageAppend"; + id: string; + channel: string; + append: Pick, "embeds">; + } | { type: "MessageDelete"; id: string; channel: string } | { - type: "MessageReact"; - id: string; - channel_id: string; - user_id: string; - emoji_id: string; - } + type: "MessageReact"; + id: string; + channel_id: string; + user_id: string; + emoji_id: string; + } | { - type: "MessageUnreact"; - id: string; - channel_id: string; - user_id: string; - emoji_id: string; - } + type: "MessageUnreact"; + id: string; + channel_id: string; + user_id: string; + emoji_id: string; + } | { - type: "MessageRemoveReaction"; - id: string; - channel_id: string; - emoji_id: string; - } + type: "MessageRemoveReaction"; + id: string; + channel_id: string; + emoji_id: string; + } | { type: "BulkMessageDelete"; channel: string; ids: string[] } | ({ type: "ChannelCreate" } & Channel) | { - type: "ChannelUpdate"; - id: string; - data: Partial; - clear?: FieldsChannel[]; - } + type: "ChannelUpdate"; + id: string; + data: Partial; + clear?: FieldsChannel[]; + } | { type: "ChannelDelete"; id: string } | { type: "ChannelGroupJoin"; id: string; user: string } | { type: "ChannelGroupLeave"; id: string; user: string } @@ -119,91 +119,91 @@ type ServerMessage = | { type: "ChannelStopTyping"; id: string; user: string } | { type: "ChannelAck"; id: string; user: string; message_id: string } | { - type: "ServerCreate"; - id: string; - server: Server; - channels: Channel[]; - } + type: "ServerCreate"; + id: string; + server: Server; + channels: Channel[]; + } | { - type: "ServerUpdate"; - id: string; - data: Partial; - clear?: FieldsServer[]; - } + type: "ServerUpdate"; + id: string; + data: Partial; + clear?: FieldsServer[]; + } | { type: "ServerDelete"; id: string } | { - type: "ServerMemberUpdate"; - id: MemberCompositeKey; - data: Partial; - clear?: FieldsMember[]; - } + type: "ServerMemberUpdate"; + id: MemberCompositeKey; + data: Partial; + clear?: FieldsMember[]; + } | { type: "ServerMemberJoin"; id: string; user: string } | { type: "ServerMemberLeave"; id: string; user: string } | { - type: "ServerRoleUpdate"; - id: string; - role_id: string; - data: Partial; - } + type: "ServerRoleUpdate"; + id: string; + role_id: string; + data: Partial; + } | { type: "ServerRoleDelete"; id: string; role_id: string } | { - type: "UserUpdate"; - id: string; - data: Partial; - clear?: FieldsUser[]; - } + type: "UserUpdate"; + id: string; + data: Partial; + clear?: FieldsUser[]; + } | { type: "UserRelationship"; user: User; status: RelationshipStatus } | { type: "UserPresence"; id: string; online: boolean } | { - type: "UserSettingsUpdate"; - id: string; - update: { [key: string]: [number, string] }; - } + type: "UserSettingsUpdate"; + id: string; + update: { [key: string]: [number, string] }; + } | { type: "UserPlatformWipe"; user_id: string; flags: number } | ({ type: "EmojiCreate" } & Emoji) | { type: "EmojiDelete"; id: string } | ({ - type: "Auth"; - } & ( + type: "Auth"; + } & ( | { - event_type: "DeleteSession"; - user_id: string; - session_id: string; - } + event_type: "DeleteSession"; + user_id: string; + session_id: string; + } | { - event_type: "DeleteAllSessions"; - user_id: string; - exclude_session_id: string; - } + event_type: "DeleteAllSessions"; + user_id: string; + exclude_session_id: string; + } )) | { - type: "VoiceChannelJoin"; - id: string; - state: UserVoiceState; - } + type: "VoiceChannelJoin"; + id: string; + state: UserVoiceState; + } | { - type: "VoiceChannelLeave"; - id: string; - user: string; - } + type: "VoiceChannelLeave"; + id: string; + user: string; + } | { - type: "VoiceChannelMove"; - user: string; - from: string; - to: string; - state: UserVoiceState; - } + type: "VoiceChannelMove"; + user: string; + from: string; + to: string; + state: UserVoiceState; + } | { - type: "UserVoiceStateUpdate"; - id: string; - channel_id: string; - data: Partial; - } + type: "UserVoiceStateUpdate"; + id: string; + channel_id: string; + data: Partial; + } | { - type: "UserMoveVoiceChannel"; - node: string; - token: string; - }; + type: "UserMoveVoiceChannel"; + node: string; + token: string; + }; /** * Policy change type @@ -276,42 +276,53 @@ export async function handleEvent( } case "Ready": { batch(() => { - for (const user of event.users) { - const u = client.users.getOrCreate(user._id, user); + if (event.users) { + for (const user of event.users) { + const u = client.users.getOrCreate(user._id, user); - if (u.relationship === "User") { - client.user = u; + if (u.relationship === "User") { + client.user = u; + } } } - for (const server of event.servers) { - client.servers.getOrCreate(server._id, server); + if (event.servers) { + for (const server of event.servers) { + client.servers.getOrCreate(server._id, server); + } } - for (const member of event.members) { - client.serverMembers.getOrCreate(member._id, member); + if (event.members) { + for (const member of event.members) { + client.serverMembers.getOrCreate(member._id, member); + } } - for (const channel of event.channels) { - client.channels.getOrCreate(channel._id, channel); + if (event.channels) { + for (const channel of event.channels) { + client.channels.getOrCreate(channel._id, channel); + } } - for (const state of event.voice_states) { - const channel = client.channels.get(state.id); - if (channel) { - channel.voiceParticipants.clear(); - - for (const participant of state.participants) { - channel.voiceParticipants.set( - participant.id, - new VoiceParticipant(client, participant), - ); + if (event.voice_states) { + for (const state of event.voice_states) { + const channel = client.channels.get(state.id); + if (channel) { + channel.voiceParticipants.clear(); + + for (const participant of state.participants) { + channel.voiceParticipants.set( + participant.id, + new VoiceParticipant(client, participant), + ); + } } } } - - for (const emoji of event.emojis) { - client.emojis.getOrCreate(emoji._id, emoji); + if (event.emojis) { + for (const emoji of event.emojis) { + client.emojis.getOrCreate(emoji._id, emoji); + } } }); From dc34ea29af0584c5f27902a12a509d77a07d952e Mon Sep 17 00:00:00 2001 From: izzy Date: Fri, 16 Jan 2026 20:33:47 +0000 Subject: [PATCH 22/31] chore: bump version to 7.3.6 Signed-off-by: izzy Signed-off-by: Taureon --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index c3c9b88e..ee0de040 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "stoat.js", - "version": "7.3.5", + "version": "7.3.6", "type": "module", "exports": { ".": "./lib/index.js" @@ -46,4 +46,4 @@ "typescript": "^5.8.3", "typescript-eslint": "^8.32.0" } -} \ No newline at end of file +} From a266663d0bc8ed6a61a2738d294ce13535124a0c Mon Sep 17 00:00:00 2001 From: izzy Date: Thu, 12 Feb 2026 21:45:40 +0000 Subject: [PATCH 23/31] chore: aggressively optimise large servers by just not loading many members Signed-off-by: Taureon --- package.json | 2 +- src/classes/Server.ts | 65 ++++++++++++++++++++++++++++++++++--------- 2 files changed, 53 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index ee0de040..6b1e6d8c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "stoat.js", - "version": "7.3.6", + "version": "7.3.7", "type": "module", "exports": { ".": "./lib/index.js" diff --git a/src/classes/Server.ts b/src/classes/Server.ts index 55725839..72218942 100644 --- a/src/classes/Server.ts +++ b/src/classes/Server.ts @@ -271,7 +271,7 @@ export class Server { */ get orderedRoles(): { name: string; - permissions: { a: bigint, d: bigint }; + permissions: { a: bigint; d: bigint }; colour?: string | null; hoist?: boolean; rank?: number; @@ -514,11 +514,12 @@ export class Server { */ async kickUser(user: string | User | ServerMember): Promise { return await this.#collection.client.api.delete( - `/servers/${this.id as ""}/members/${typeof user === "string" - ? user - : user instanceof User - ? user.id - : user.id.user + `/servers/${this.id as ""}/members/${ + typeof user === "string" + ? user + : user instanceof User + ? user.id + : user.id.user }`, ); } @@ -647,11 +648,10 @@ export class Server { #synced: undefined | "partial" | "full"; - /** - * Optimised member fetch route - * @param excludeOffline - */ - async syncMembers(excludeOffline?: boolean): Promise { + async syncMembers( + excludeOffline?: boolean, + excludeOfflineUserCap?: number, + ): Promise { if (this.#synced && (this.#synced === "full" || excludeOffline)) return; const data = await this.#collection.client.api.get( @@ -660,7 +660,45 @@ export class Server { ); batch(() => { - if (excludeOffline) { + if (excludeOffline && excludeOfflineUserCap) { + // quick fix to cap users + let count = 0; + + for ( + let i = 0; + i < data.users.length && count < excludeOfflineUserCap; + i++ + ) { + const user = data.users[i]; + if (user.online && data.members[i].roles?.length) { + this.#collection.client.users.getOrCreate(user._id, user); + this.#collection.client.serverMembers.getOrCreate( + data.members[i]._id, + data.members[i], + ); + + count++; + } + } + + for ( + let i = 0; + i < data.users.length && count < excludeOfflineUserCap; + i++ + ) { + const user = data.users[i]; + if (user.online && !data.members[i].roles?.length) { + this.#collection.client.users.getOrCreate(user._id, user); + this.#collection.client.serverMembers.getOrCreate( + data.members[i]._id, + data.members[i], + ); + + count++; + } + } + // end quick fix + } else if (excludeOffline) { for (let i = 0; i < data.users.length; i++) { const user = data.users[i]; if (user.online) { @@ -722,7 +760,8 @@ export class Server { query: string, ): Promise<{ members: ServerMember[]; users: User[] }> { const data = (await this.#collection.client.api.get( - `/servers/${this.id as "" + `/servers/${ + this.id as "" }/members_experimental_query?experimental_api=true&query=${encodeURIComponent( query, )}` as never, From db388824f950974d7dc2a772050b4d44d3280a60 Mon Sep 17 00:00:00 2001 From: Christopher Hultin Date: Sat, 28 Feb 2026 12:18:54 -0700 Subject: [PATCH 24/31] fix: lint issues (#133) Signed-off-by: Chris Hultin Signed-off-by: Taureon --- src/Client.ts | 2 +- src/classes/Channel.ts | 4 ++-- src/classes/MFA.ts | 2 +- src/classes/ServerMember.ts | 8 ++++++-- src/classes/ServerRole.ts | 6 +++--- src/classes/SystemMessage.ts | 18 ++++++++++++++---- src/classes/VoiceParticipant.ts | 14 ++++++++------ src/collections/ServerCollection.ts | 6 +----- src/events/EventClient.ts | 14 +++++++------- src/hydration/bot.ts | 2 +- src/hydration/channel.ts | 14 +++++++++----- src/hydration/message.ts | 2 +- src/index.ts | 2 +- src/storage/ObjectStorage.ts | 2 +- 14 files changed, 56 insertions(+), 40 deletions(-) diff --git a/src/Client.ts b/src/Client.ts index 34d8091e..a179fc6e 100644 --- a/src/Client.ts +++ b/src/Client.ts @@ -279,7 +279,7 @@ export class Client extends AsyncEventEmitter { this.#reconnectTimeout = setTimeout( () => this.connect(), this.options.retryDelayFunction(this.connectionFailureCount()) * - 1e3, + 1e3, ) as never; this.#setConnectionFailureCount((count) => count + 1); diff --git a/src/classes/Channel.ts b/src/classes/Channel.ts index ea087113..db83df5e 100644 --- a/src/classes/Channel.ts +++ b/src/classes/Channel.ts @@ -177,8 +177,8 @@ export class Channel { get recipient(): User | undefined { return this.type === "DirectMessage" ? this.recipients?.find( - (user) => user?.id !== this.#collection.client.user!.id, - ) + (user) => user?.id !== this.#collection.client.user!.id, + ) : undefined; } diff --git a/src/classes/MFA.ts b/src/classes/MFA.ts index f916b4ff..1223c965 100644 --- a/src/classes/MFA.ts +++ b/src/classes/MFA.ts @@ -61,7 +61,7 @@ export class MFA { return new MFATicket( this.#client, await this.#client.api.put("/auth/mfa/ticket", params), - this.#store[1] + this.#store[1], ); } diff --git a/src/classes/ServerMember.ts b/src/classes/ServerMember.ts index d25a615d..46fac46a 100644 --- a/src/classes/ServerMember.ts +++ b/src/classes/ServerMember.ts @@ -110,7 +110,9 @@ export class ServerMember { /** * Ordered list of roles for this member, from lowest to highest priority. */ - get orderedRoles(): (Partial & { permissions: { a: bigint, d: bigint } }> & { id: string })[] { + get orderedRoles(): (Partial< + Omit & { permissions: { a: bigint; d: bigint } } + > & { id: string })[] { const server = this.server!; return ( this.roles @@ -125,7 +127,9 @@ export class ServerMember { /** * Member's currently hoisted role. */ - get hoistedRole(): Partial & { permissions: { a: bigint, d: bigint } }> | null { + get hoistedRole(): Partial< + Omit & { permissions: { a: bigint; d: bigint } } + > | null { const roles = this.orderedRoles.filter((x) => x.hoist); if (roles.length > 0) { return roles[roles.length - 1]; diff --git a/src/classes/ServerRole.ts b/src/classes/ServerRole.ts index b8f5dbbe..fe133aa2 100644 --- a/src/classes/ServerRole.ts +++ b/src/classes/ServerRole.ts @@ -12,8 +12,8 @@ export class ServerRole { readonly id: string; readonly name: string; readonly permissions: { - a: bigint, - d: bigint + a: bigint; + d: bigint; }; readonly colour?: string; readonly hoist: boolean; @@ -34,7 +34,7 @@ export class ServerRole { this.name = data.name; this.permissions = { a: BigInt(data.permissions.a), - d: BigInt(data.permissions.d) + d: BigInt(data.permissions.d), }; this.colour = data.colour ?? undefined; this.hoist = data.hoist || false; diff --git a/src/classes/SystemMessage.ts b/src/classes/SystemMessage.ts index 0bfac303..8cfd0a2a 100644 --- a/src/classes/SystemMessage.ts +++ b/src/classes/SystemMessage.ts @@ -1,10 +1,13 @@ -import type { SystemMessage as APISystemMessage, Message as APIMessage } from "stoat-api"; +import type { + Message as APIMessage, + SystemMessage as APISystemMessage, +} from "stoat-api"; +import { decodeTime } from "ulid"; import type { Client } from "../Client.js"; import type { User } from "./User.js"; import { Message } from "./index.js"; -import { decodeTime } from "ulid"; /** * System Message @@ -29,7 +32,11 @@ export abstract class SystemMessage { * @param embed Data * @returns System Message */ - static from(client: Client, parent: APIMessage, message: APISystemMessage): SystemMessage { + static from( + client: Client, + parent: APIMessage, + message: APISystemMessage, + ): SystemMessage { switch (message.type) { case "text": return new TextSystemMessage(client, message); @@ -299,7 +306,10 @@ export class CallStartedSystemMessage extends SystemMessage { super(client, systemMessage.type); this.byId = systemMessage.by; this.startedAt = new Date(decodeTime(parent._id)); - this.finishedAt = systemMessage.finished_at != null ? new Date(systemMessage.finished_at) : null; + this.finishedAt = + systemMessage.finished_at != null + ? new Date(systemMessage.finished_at) + : null; } /** diff --git a/src/classes/VoiceParticipant.ts b/src/classes/VoiceParticipant.ts index b97172ac..6e01cede 100644 --- a/src/classes/VoiceParticipant.ts +++ b/src/classes/VoiceParticipant.ts @@ -1,5 +1,5 @@ +import { Accessor, Setter, createSignal } from "solid-js"; -import { Accessor, createSignal, Setter } from "solid-js"; import type { Client } from "../Client.js"; import { UserVoiceState } from "../events/v1.js"; @@ -39,7 +39,9 @@ export class VoiceParticipant { this.isPublishing = isPublishing; this.#setPublishing = setPublishing; - const [isScreensharing, setScreensharing] = createSignal(data.screensharing); + const [isScreensharing, setScreensharing] = createSignal( + data.screensharing, + ); this.isScreensharing = isScreensharing; this.#setScreensharing = setScreensharing; @@ -53,19 +55,19 @@ export class VoiceParticipant { * @param data Data */ update(data: Partial) { - if (typeof data.is_receiving === 'boolean') { + if (typeof data.is_receiving === "boolean") { this.#setReceiving(data.is_receiving); } - if (typeof data.is_publishing === 'boolean') { + if (typeof data.is_publishing === "boolean") { this.#setPublishing(data.is_publishing); } - if (typeof data.screensharing === 'boolean') { + if (typeof data.screensharing === "boolean") { this.#setScreensharing(data.screensharing); } - if (typeof data.camera === 'boolean') { + if (typeof data.camera === "boolean") { this.#setCamera(data.camera); } } diff --git a/src/collections/ServerCollection.ts b/src/collections/ServerCollection.ts index 819f5691..1870c225 100644 --- a/src/collections/ServerCollection.ts +++ b/src/collections/ServerCollection.ts @@ -1,10 +1,6 @@ import { batch } from "solid-js"; -import type { - Server as APIServer, - Channel, - DataCreateServer, -} from "stoat-api"; +import type { Server as APIServer, Channel, DataCreateServer } from "stoat-api"; import { Server } from "../classes/Server.js"; import type { HydratedServer } from "../hydration/server.js"; diff --git a/src/events/EventClient.ts b/src/events/EventClient.ts index e6f37f12..dd1e3866 100644 --- a/src/events/EventClient.ts +++ b/src/events/EventClient.ts @@ -291,14 +291,14 @@ export class EventClient< */ get lastError(): | { - type: "socket"; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - data: any; - } + type: "socket"; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + data: any; + } | { - type: "revolt"; - data: Error; - } + type: "revolt"; + data: Error; + } | undefined { return this.#lastError; } diff --git a/src/hydration/bot.ts b/src/hydration/bot.ts index 99fbe2e6..0d63b176 100644 --- a/src/hydration/bot.ts +++ b/src/hydration/bot.ts @@ -41,4 +41,4 @@ export const botHydration: Hydrate = { /** * Flags attributed to users */ -export enum BotFlags { } +export enum BotFlags {} diff --git a/src/hydration/channel.ts b/src/hydration/channel.ts index ed7c2a92..9013fefe 100644 --- a/src/hydration/channel.ts +++ b/src/hydration/channel.ts @@ -1,13 +1,13 @@ +import { ReactiveMap } from "@solid-primitives/map"; import { ReactiveSet } from "@solid-primitives/set"; import type { Channel as APIChannel } from "stoat-api"; import type { Client } from "../Client.js"; import { File } from "../classes/File.js"; +import { VoiceParticipant } from "../classes/VoiceParticipant.js"; import type { Merge } from "../lib/merge.js"; import type { Hydrate } from "./index.js"; -import { VoiceParticipant } from "../classes/VoiceParticipant.js"; -import { ReactiveMap } from "@solid-primitives/map"; export type HydratedChannel = { id: string; @@ -77,9 +77,13 @@ export const channelHydration: Hydrate, HydratedChannel> = { nsfw: (channel) => channel.nsfw || false, lastMessageId: (channel) => channel.last_message_id!, voice: (channel) => - !!channel.voice || channel.channel_type === 'DirectMessage' || channel.channel_type === 'Group' ? ({ - maxUsers: channel.voice?.max_users || undefined, - }) : undefined, + !!channel.voice || + channel.channel_type === "DirectMessage" || + channel.channel_type === "Group" + ? { + maxUsers: channel.voice?.max_users || undefined, + } + : undefined, }, initialHydration: () => ({ typingIds: new ReactiveSet(), diff --git a/src/hydration/message.ts b/src/hydration/message.ts index 6cd84c3f..4475dee8 100644 --- a/src/hydration/message.ts +++ b/src/hydration/message.ts @@ -98,4 +98,4 @@ export enum MessageFlags { * This cannot be true if MentionsEveryone is true */ MentionsOnline = 3, -} \ No newline at end of file +} diff --git a/src/index.ts b/src/index.ts index a8cd6c01..6eab7f1f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,4 +8,4 @@ export { BotFlags } from "./hydration/bot.js"; export { ServerFlags } from "./hydration/server.js"; export { UserBadges, UserFlags } from "./hydration/user.js"; export * from "./lib/regex.js"; -export * from './permissions/definitions.js'; +export * from "./permissions/definitions.js"; diff --git a/src/storage/ObjectStorage.ts b/src/storage/ObjectStorage.ts index 366389c1..91dd50ea 100644 --- a/src/storage/ObjectStorage.ts +++ b/src/storage/ObjectStorage.ts @@ -40,7 +40,7 @@ export class ObjectStorage { id: string, type: keyof Hydrators, context: unknown, - data?: unknown + data?: unknown, ): void { if (data) { data = { partial: false, ...data }; From b1e0cbff0582c48d900122c0ff8069a14d425761 Mon Sep 17 00:00:00 2001 From: Jacob Schlecht Date: Sat, 28 Feb 2026 12:24:27 -0700 Subject: [PATCH 25/31] feat: Enable asynchronous configuration loading and configuration signal (#127) Signed-off-by: Jacob Schlecht Signed-off-by: Taureon --- src/Client.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Client.ts b/src/Client.ts index a179fc6e..94a3d613 100644 --- a/src/Client.ts +++ b/src/Client.ts @@ -188,6 +188,9 @@ export class Client extends AsyncEventEmitter { readonly ready: Accessor; #setReady: Setter; + readonly configured: Accessor; + #setConfigured: Setter; + readonly connectionFailureCount: Accessor; #setConnectionFailureCount: Setter; #reconnectTimeout: number | undefined; @@ -239,6 +242,12 @@ export class Client extends AsyncEventEmitter { baseURL: this.options.baseURL, }); + const [configured, setConfigured] = createSignal(configuration !== undefined); + this.configured = configured; + this.#setConfigured = setConfigured; + + this.#fetchConfiguration(); + const [ready, setReady] = createSignal(false); this.ready = ready; this.#setReady = setReady; @@ -328,6 +337,7 @@ export class Client extends AsyncEventEmitter { async #fetchConfiguration(): Promise { if (!this.configuration) { this.configuration = await this.api.get("/"); + this.#setConfigured(true); } } From da5ea0693c5aec3271082cea1702d5e087e96a58 Mon Sep 17 00:00:00 2001 From: Jacob Schlecht Date: Mon, 2 Mar 2026 08:26:31 -0700 Subject: [PATCH 26/31] 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 * 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 * 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 * 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 * fix: run lint for Client.ts This commit was made without the use of generative AI. Signed-off-by: Jacob Schlecht * refactor: Wait for all promises at once This commit was made without the use of generative AI. Signed-off-by: Jacob Schlecht * Using spread operator Signed-off-by: Christopher Hultin * fix: run linter This commit was made without the use of generative AI. Signed-off-by: Jacob Schlecht --------- Signed-off-by: Jacob Schlecht Signed-off-by: Christopher Hultin Co-authored-by: Christopher Hultin Signed-off-by: Taureon --- package.json | 2 +- pnpm-lock.yaml | 10 ++--- src/Client.ts | 102 ++++++++++++++++++++++++++++++++++++++++++++++- src/lib/regex.ts | 5 +++ 4 files changed, 111 insertions(+), 8 deletions(-) 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. */ From 8d0aa6798dc38856ab12fdbdd84c54735c91d291 Mon Sep 17 00:00:00 2001 From: z-nexx Date: Mon, 2 Mar 2026 16:26:49 +0100 Subject: [PATCH 27/31] fix: correct event name in example (#130) correct event name Signed-off-by: z-nexx Signed-off-by: Taureon --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 96299cb0..d23b9b53 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ client.on("ready", async () => console.info(`Logged in as ${client.user.username}!`), ); -client.on("messageCreate", async (message) => { +client.on("message", async (message) => { if (message.content === "hello") { message.channel.sendMessage("world"); } From 020acc83e1bbf0251a0bf5f24851b8175e7fc7f2 Mon Sep 17 00:00:00 2001 From: Jacob Schlecht Date: Mon, 2 Mar 2026 20:47:27 -0700 Subject: [PATCH 28/31] fix: markdownToTextFetch will now return results even if fetches fail (#135) This commit was made without the use of generative AI. Signed-off-by: Jacob Schlecht Signed-off-by: Taureon --- src/Client.ts | 36 ++++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/src/Client.ts b/src/Client.ts index 1c9a810a..86b2693d 100644 --- a/src/Client.ts +++ b/src/Client.ts @@ -455,10 +455,14 @@ export class Client extends AsyncEventEmitter { async (key) => { const substr = userMatches[key]; if (substr) { - const user = await this.users.fetch(substr); - - if (user) { - return [key, `@${user.username}`]; + try { + const user = await this.users.fetch(substr); + if (user) { + return [key, `@${user.username}`]; + } + } catch { + // If the fetch fails, just show the match as a default + return [key, key]; } } @@ -471,10 +475,14 @@ export class Client extends AsyncEventEmitter { async (key) => { const substr = channelMatches[key]; if (substr) { - const channel = await this.channels.fetch(substr); - - if (channel) { - return [key, `#${channel.displayName}`]; + try { + const channel = await this.channels.fetch(substr); + if (channel) { + return [key, `#${channel.displayName}`]; + } + } catch { + // If the fetch fails, just show the match as a default + return [key, key]; } } @@ -487,10 +495,14 @@ export class Client extends AsyncEventEmitter { async (key) => { const substr = customEmojiMatches[key]; if (substr) { - const emoji = await this.emojis.fetch(substr); - - if (emoji) { - return [key, `:${emoji.name}:`]; + try { + const emoji = await this.emojis.fetch(substr); + if (emoji) { + return [key, `:${emoji.name}:`]; + } + } catch { + // If the fetch fails, just show the match as a default + return [key, key]; } } From e88c8e09440554b699ce3ac17ef7bad84bf9dec0 Mon Sep 17 00:00:00 2001 From: Jacob Schlecht Date: Mon, 2 Mar 2026 20:47:39 -0700 Subject: [PATCH 29/31] feat: Add emoji parsing to sync markdownToText (#136) This commit was made without the use of generative AI. Signed-off-by: Jacob Schlecht Signed-off-by: Taureon --- src/Client.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Client.ts b/src/Client.ts index 86b2693d..3119525c 100644 --- a/src/Client.ts +++ b/src/Client.ts @@ -420,6 +420,15 @@ export class Client extends AsyncEventEmitter { return sub; }) + .replace(RE_CUSTOM_EMOJI, (sub: string, id: string) => { + const emoji = this.emojis.get(id as string); + + if (emoji) { + return `:${emoji.name}:`; + } + + return sub; + }) .replace(RE_SPOILER, ""); } From 9657673c57e8ad8ee4458edcd933fee0bdd91aa4 Mon Sep 17 00:00:00 2001 From: Jacob Schlecht Date: Mon, 2 Mar 2026 21:14:24 -0700 Subject: [PATCH 30/31] fix: linter fix to make pr checks pass (#137) * fix: linter fix to make pr checks pass This commit was made without the use of generative AI. Signed-off-by: Jacob Schlecht * fix: add missing eslint library This commit was made without the use of generative AI. Signed-off-by: Jacob Schlecht --------- Signed-off-by: Jacob Schlecht Signed-off-by: Taureon --- eslint.config.js | 3 ++- package.json | 1 + pnpm-lock.yaml | 26 ++++++++++++++++++++++++-- src/events/EventClient.ts | 2 +- 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index de573f89..8a5a53e6 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -8,6 +8,7 @@ export default defineConfig([ eslint.configs.recommended, tseslint.configs.recommended, solid, + prettier, { rules: { "@typescript-eslint/no-unused-vars": [ @@ -15,9 +16,9 @@ export default defineConfig([ { caughtErrors: "all", varsIgnorePattern: "^_", + argsIgnorePattern: "^_", }, ], }, }, - prettier, ]); diff --git a/package.json b/package.json index 34430209..695d9ca9 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "ulid": "^3.0.2" }, "devDependencies": { + "@eslint/js": "^9.39.1", "@mxssfd/typedoc-theme": "^1.1.7", "@trivago/prettier-plugin-sort-imports": "^5.2.2", "@types/node": "^22.15.17", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a38c4bfd..7bfd8b5b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -30,6 +30,9 @@ importers: specifier: ^3.0.2 version: 3.0.2 devDependencies: + '@eslint/js': + specifier: ^9.39.1 + version: 9.39.3 '@mxssfd/typedoc-theme': specifier: ^1.1.7 version: 1.1.7(typedoc@0.27.9(typescript@5.8.3)) @@ -44,7 +47,7 @@ importers: version: 9.26.0 eslint-plugin-prettier: specifier: ^5.4.0 - version: 5.4.0(eslint@9.26.0)(prettier@3.5.3) + version: 5.4.0(eslint-config-prettier@10.1.8(eslint@9.26.0))(eslint@9.26.0)(prettier@3.5.3) eslint-plugin-solid: specifier: ^0.14.5 version: 0.14.5(eslint@9.26.0)(typescript@5.8.3) @@ -126,6 +129,10 @@ packages: resolution: {integrity: sha512-I9XlJawFdSMvWjDt6wksMCrgns5ggLNfFwFvnShsleWruvXM514Qxk8V246efTw+eo9JABvVz+u3q2RiAowKxQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/js@9.39.3': + resolution: {integrity: sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/object-schema@2.1.6': resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -460,6 +467,12 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} + eslint-config-prettier@10.1.8: + resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + eslint-plugin-prettier@5.4.0: resolution: {integrity: sha512-BvQOvUhkVQM1i63iMETK9Hjud9QhqBnbtT1Zc642p9ynzBuCe5pybkOnvqZIBypXmMlsGcnU4HZ8sCTPfpAexA==} engines: {node: ^14.18.0 || >=16.0.0} @@ -1177,6 +1190,8 @@ snapshots: '@eslint/js@9.26.0': {} + '@eslint/js@9.39.3': {} + '@eslint/object-schema@2.1.6': {} '@eslint/plugin-kit@0.2.8': @@ -1524,12 +1539,19 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-plugin-prettier@5.4.0(eslint@9.26.0)(prettier@3.5.3): + eslint-config-prettier@10.1.8(eslint@9.26.0): + dependencies: + eslint: 9.26.0 + optional: true + + eslint-plugin-prettier@5.4.0(eslint-config-prettier@10.1.8(eslint@9.26.0))(eslint@9.26.0)(prettier@3.5.3): dependencies: eslint: 9.26.0 prettier: 3.5.3 prettier-linter-helpers: 1.0.0 synckit: 0.11.4 + optionalDependencies: + eslint-config-prettier: 10.1.8(eslint@9.26.0) eslint-plugin-solid@0.14.5(eslint@9.26.0)(typescript@5.8.3): dependencies: diff --git a/src/events/EventClient.ts b/src/events/EventClient.ts index dd1e3866..b77eff57 100644 --- a/src/events/EventClient.ts +++ b/src/events/EventClient.ts @@ -95,7 +95,7 @@ export class EventClient< #connectTimeoutReference: number | undefined; #lastError: // eslint-disable-next-line @typescript-eslint/no-explicit-any - { type: "socket"; data: any } | { type: "revolt"; data: Error } | undefined; + { type: "socket"; data: any } | { type: "revolt"; data: Error } | undefined; /** * Create a new event client. From 30f8b9e5b1c250cdd1c75178c403d5d21999d3d1 Mon Sep 17 00:00:00 2001 From: Mihai <45673304+mihaicm93@users.noreply.github.com> Date: Tue, 3 Mar 2026 05:17:03 +0100 Subject: [PATCH 31/31] Fix/server deletion lockup (#134) * fix: Server deletion client lockup Signed-off-by: mihai <45673304+mihaicm93@users.noreply.github.com> * fix: lint errors Signed-off-by: mihai <45673304+mihaicm93@users.noreply.github.com> * fix lint error Signed-off-by: mihai <45673304+mihaicm93@users.noreply.github.com> --------- Signed-off-by: mihai <45673304+mihaicm93@users.noreply.github.com> Co-authored-by: Christopher Hultin Signed-off-by: Taureon --- src/classes/Server.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/classes/Server.ts b/src/classes/Server.ts index 72218942..a63d6e25 100644 --- a/src/classes/Server.ts +++ b/src/classes/Server.ts @@ -136,9 +136,10 @@ export class Server { * Channels */ get channels(): Channel[] { - return [ - ...this.#collection.getUnderlyingObject(this.id).channelIds.values(), - ] + const channelIds = this.#collection.getUnderlyingObject(this.id).channelIds; + if (!channelIds) return []; + + return [...channelIds.values()] .map((id) => this.#collection.client.channels.get(id)!) .filter((x) => x); } @@ -338,6 +339,7 @@ export class Server { * Permission the currently authenticated user has against this server */ get permission(): bigint { + if (!this.$exists) return 0n; return calculatePermission(this.#collection.client, this); }