mirror of
https://github.com/stoatchat/javascript-client-sdk.git
synced 2026-07-18 16:34:27 -04:00
Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7fef0567c7 | |||
| c6b90b08ee | |||
| 297710dbbe | |||
| 2c3d0c5366 | |||
| ed6ee065fe | |||
| 4573e6b2bb | |||
| afdd11fc3b | |||
| 9da2032818 | |||
| 3453407ba8 | |||
| 0ae58c30be | |||
| 8e9b8fa71a | |||
| cfc2b5b433 | |||
| de9908896a | |||
| 58ae86c5c6 | |||
| 64d449d7d3 | |||
| 7de00aa7ce | |||
| a672fea424 | |||
| 46b20bd7f7 | |||
| 1c97bc2d5a | |||
| b2a49fae3c | |||
| 7cdf5b1bce | |||
| 4a1b1addff | |||
| 3c71fa73e8 | |||
| 64d8d2c657 | |||
| 97d924bb3a | |||
| 746afa7839 | |||
| 0f2cd21df8 | |||
| e1a9c8a856 | |||
| cdf406094b | |||
| 075832ff2e | |||
| 8d2def3ee3 | |||
| 393bd00fdd | |||
| dd74283320 | |||
| ec178587bf | |||
| a3611e18e3 | |||
| a6f49b9f35 | |||
| 1e1b738a82 | |||
| 1c8e4201cb | |||
| fcc8b0e195 | |||
| 2a4dac552c | |||
| f89ed331d2 | |||
| 5b8fef4b1a | |||
| 00f3ca68a4 |
@@ -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");
|
||||
}
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
{
|
||||
pkgs ? import <nixpkgs> { },
|
||||
}:
|
||||
|
||||
with pkgs;
|
||||
pkgs.mkShell {
|
||||
name = "stoatEnv";
|
||||
|
||||
buildInputs = [
|
||||
# Tools
|
||||
git
|
||||
gh
|
||||
deno
|
||||
|
||||
# Node
|
||||
nodejs
|
||||
nodejs.pkgs.pnpm
|
||||
];
|
||||
}
|
||||
+7
-1
@@ -5,9 +5,15 @@ import { defineConfig } from "eslint/config";
|
||||
import tseslint from "typescript-eslint";
|
||||
|
||||
export default defineConfig([
|
||||
{
|
||||
languageOptions: {
|
||||
parserOptions: { tsconfigRootDir: import.meta.dirname },
|
||||
},
|
||||
},
|
||||
eslint.configs.recommended,
|
||||
tseslint.configs.recommended,
|
||||
solid,
|
||||
prettier,
|
||||
{
|
||||
rules: {
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
@@ -15,9 +21,9 @@ export default defineConfig([
|
||||
{
|
||||
caughtErrors: "all",
|
||||
varsIgnorePattern: "^_",
|
||||
argsIgnorePattern: "^_",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
prettier,
|
||||
]);
|
||||
|
||||
+6
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "stoat.js",
|
||||
"version": "7.3.0",
|
||||
"version": "7.3.7",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./lib/index.js"
|
||||
@@ -29,11 +29,13 @@
|
||||
"@solid-primitives/map": "^0.7.1",
|
||||
"@solid-primitives/set": "^0.7.1",
|
||||
"@vladfrangu/async_event_emitter": "^2.4.6",
|
||||
"revolt-api": "0.8.9",
|
||||
"json-with-bigint": "^3.4.4",
|
||||
"solid-js": "^1.9.6",
|
||||
"ulid": "^2.4.0"
|
||||
"stoat-api": "0.14.0",
|
||||
"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",
|
||||
@@ -45,4 +47,4 @@
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "^8.32.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+47
-15
@@ -17,16 +17,22 @@ importers:
|
||||
'@vladfrangu/async_event_emitter':
|
||||
specifier: ^2.4.6
|
||||
version: 2.4.6
|
||||
revolt-api:
|
||||
specifier: 0.8.9
|
||||
version: 0.8.9
|
||||
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.14.0
|
||||
version: 0.14.0
|
||||
ulid:
|
||||
specifier: ^2.4.0
|
||||
version: 2.4.0
|
||||
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))
|
||||
@@ -41,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)
|
||||
@@ -123,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}
|
||||
@@ -457,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}
|
||||
@@ -718,6 +734,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==}
|
||||
|
||||
@@ -905,9 +924,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 +994,9 @@ packages:
|
||||
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
stoat-api@0.14.0:
|
||||
resolution: {integrity: sha512-Kg6PS8D6aKKhs6KALvdHe6ygtwCuiCyj9JDT/KDR0BRDzoP2W9LjAePYZZBoA9BITIeBU270ug/0qDpWMGYb/w==}
|
||||
|
||||
strip-json-comments@3.1.1:
|
||||
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -1040,8 +1059,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:
|
||||
@@ -1171,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':
|
||||
@@ -1518,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:
|
||||
@@ -1811,6 +1839,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:
|
||||
@@ -1965,8 +1995,6 @@ snapshots:
|
||||
|
||||
reusify@1.1.0: {}
|
||||
|
||||
revolt-api@0.8.9: {}
|
||||
|
||||
router@2.2.0:
|
||||
dependencies:
|
||||
debug: 4.4.0
|
||||
@@ -2062,6 +2090,10 @@ snapshots:
|
||||
|
||||
statuses@2.0.1: {}
|
||||
|
||||
stoat-api@0.14.0:
|
||||
dependencies:
|
||||
json-with-bigint: 3.4.4
|
||||
|
||||
strip-json-comments@3.1.1: {}
|
||||
|
||||
style-to-object@1.0.8:
|
||||
@@ -2122,7 +2154,7 @@ snapshots:
|
||||
|
||||
uc.micro@2.1.0: {}
|
||||
|
||||
ulid@2.4.0: {}
|
||||
ulid@3.0.2: {}
|
||||
|
||||
undici-types@6.21.0: {}
|
||||
|
||||
|
||||
+180
-9
@@ -1,16 +1,17 @@
|
||||
import type { Accessor, Setter } from "solid-js";
|
||||
import { batch, createSignal } from "solid-js";
|
||||
|
||||
import { ReactiveMap } from "@solid-primitives/map";
|
||||
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, Error, RevoltConfig, Role } from "stoat-api";
|
||||
|
||||
import type { Channel } from "./classes/Channel.js";
|
||||
import type { Emoji } from "./classes/Emoji.js";
|
||||
import type { Message } from "./classes/Message.js";
|
||||
import type { Server } from "./classes/Server.js";
|
||||
import type { ServerMember } from "./classes/ServerMember.js";
|
||||
import type { User } from "./classes/User.js";
|
||||
import type { User, UserLimits } from "./classes/User.js";
|
||||
import { AccountCollection } from "./collections/AccountCollection.js";
|
||||
import { BotCollection } from "./collections/BotCollection.js";
|
||||
import { ChannelCollection } from "./collections/ChannelCollection.js";
|
||||
@@ -27,14 +28,19 @@ import {
|
||||
EventClient,
|
||||
type EventClientOptions,
|
||||
} from "./events/EventClient.js";
|
||||
import { ProtocolV1, handleEvent } from "./events/v1.js";
|
||||
import { ProtocolV1, UserSlowmodes, handleEvent } from "./events/v1.js";
|
||||
import type { HydratedChannel } from "./hydration/channel.js";
|
||||
import type { HydratedEmoji } from "./hydration/emoji.js";
|
||||
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;
|
||||
|
||||
@@ -42,8 +48,7 @@ export type Session = { _id: string; token: string; user_id: string } | string;
|
||||
* Events provided by the client
|
||||
*/
|
||||
export type Events = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
error: [error: any];
|
||||
error: [error: Error];
|
||||
|
||||
connected: [];
|
||||
connecting: [];
|
||||
@@ -94,6 +99,8 @@ export type Events = {
|
||||
|
||||
emojiCreate: [emoji: Emoji];
|
||||
emojiDelete: [emoji: HydratedEmoji];
|
||||
|
||||
userSlowmodes: [];
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -176,6 +183,7 @@ export class Client extends AsyncEventEmitter<Events> {
|
||||
readonly serverMembers;
|
||||
readonly sessions;
|
||||
readonly users;
|
||||
readonly userSlowmodes;
|
||||
|
||||
readonly api: API;
|
||||
readonly options: ClientOptions;
|
||||
@@ -188,10 +196,13 @@ export class Client extends AsyncEventEmitter<Events> {
|
||||
readonly ready: Accessor<boolean>;
|
||||
#setReady: Setter<boolean>;
|
||||
|
||||
readonly configured: Accessor<boolean>;
|
||||
#setConfigured: Setter<boolean>;
|
||||
|
||||
readonly connectionFailureCount: Accessor<number>;
|
||||
#setConnectionFailureCount: Setter<number>;
|
||||
#reconnectTimeout: number | undefined;
|
||||
|
||||
#slowmodeTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
||||
/**
|
||||
* Create Stoat.js Client
|
||||
*/
|
||||
@@ -239,6 +250,14 @@ export class Client extends AsyncEventEmitter<Events> {
|
||||
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;
|
||||
@@ -258,6 +277,7 @@ export class Client extends AsyncEventEmitter<Events> {
|
||||
this.serverMembers = new ServerMemberCollection(this);
|
||||
this.sessions = new SessionCollection(this);
|
||||
this.users = new UserCollection(this);
|
||||
this.userSlowmodes = new ReactiveMap<string, UserSlowmodes>();
|
||||
|
||||
this.events = new EventClient(1, "json", this.options);
|
||||
this.events.on("error", (error) => this.emit("error", error));
|
||||
@@ -279,7 +299,7 @@ export class Client extends AsyncEventEmitter<Events> {
|
||||
this.#reconnectTimeout = setTimeout(
|
||||
() => this.connect(),
|
||||
this.options.retryDelayFunction(this.connectionFailureCount()) *
|
||||
1e3,
|
||||
1e3,
|
||||
) as never;
|
||||
|
||||
this.#setConnectionFailureCount((count) => count + 1);
|
||||
@@ -328,6 +348,7 @@ export class Client extends AsyncEventEmitter<Events> {
|
||||
async #fetchConfiguration(): Promise<void> {
|
||||
if (!this.configuration) {
|
||||
this.configuration = await this.api.get("/");
|
||||
this.#setConfigured(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -378,6 +399,18 @@ export class Client extends AsyncEventEmitter<Events> {
|
||||
this.connect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Log out of current session
|
||||
*
|
||||
* This function prepares the client for disposal by removing all event listeners and killing the events socket.
|
||||
*/
|
||||
async logout(): Promise<void> {
|
||||
await this.api.post("/auth/session/logout");
|
||||
this.events.removeAllListeners();
|
||||
this.removeAllListeners();
|
||||
this.events.disconnect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare a markdown-based message to be displayed to the user as plain text.
|
||||
* @param source Source markdown text
|
||||
@@ -403,6 +436,118 @@ export class Client extends AsyncEventEmitter<Events> {
|
||||
|
||||
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, "<spoiler>");
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare a markdown-based message to be displayed to the user as plain text. This method will fetch each user or channel if they are missing. Useful for serviceworkers.
|
||||
* @param source Source markdown text
|
||||
* @returns Modified plain text
|
||||
*/
|
||||
async markdownToTextFetch(source: string): Promise<string> {
|
||||
// Get all user matches, create a map to dedupe
|
||||
const userMatches = Object.fromEntries(
|
||||
Array.from(source.matchAll(RE_MENTIONS), (match) => {
|
||||
return [match[0], match[1]];
|
||||
}),
|
||||
);
|
||||
|
||||
// Get all channel matches, create a map to dedupe
|
||||
const channelMatches = Object.fromEntries(
|
||||
Array.from(source.matchAll(RE_CHANNELS), (match) => {
|
||||
return [match[0], match[1]];
|
||||
}),
|
||||
);
|
||||
|
||||
// Get all custom emoji matches, create a map to dedupe
|
||||
const customEmojiMatches = Object.fromEntries(
|
||||
Array.from(source.matchAll(RE_CUSTOM_EMOJI), (match) => {
|
||||
return [match[0], match[1]];
|
||||
}),
|
||||
);
|
||||
|
||||
// Send requests to replace user ids
|
||||
const userReplacementPromises = Object.keys(userMatches).map(
|
||||
async (key) => {
|
||||
const substr = userMatches[key];
|
||||
if (substr) {
|
||||
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];
|
||||
}
|
||||
}
|
||||
|
||||
return [key, key];
|
||||
},
|
||||
);
|
||||
|
||||
// Send requests to replace channel ids
|
||||
const channelReplacementPromises = Object.keys(channelMatches).map(
|
||||
async (key) => {
|
||||
const substr = channelMatches[key];
|
||||
if (substr) {
|
||||
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];
|
||||
}
|
||||
}
|
||||
|
||||
return [key, key];
|
||||
},
|
||||
);
|
||||
|
||||
// Send requests to replace custom emojis
|
||||
const customEmojiReplacementPromises = Object.keys(customEmojiMatches).map(
|
||||
async (key) => {
|
||||
const substr = customEmojiMatches[key];
|
||||
if (substr) {
|
||||
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];
|
||||
}
|
||||
}
|
||||
|
||||
return [key, key];
|
||||
},
|
||||
);
|
||||
|
||||
// Await for all promises to get the strings to replace with.
|
||||
const replacements = await Promise.all([
|
||||
...userReplacementPromises,
|
||||
...channelReplacementPromises,
|
||||
...customEmojiReplacementPromises,
|
||||
]);
|
||||
|
||||
const replacementsMap = Object.fromEntries(replacements);
|
||||
|
||||
return source
|
||||
.replace(RE_MENTIONS, (match) => replacementsMap[match])
|
||||
.replace(RE_CHANNELS, (match) => replacementsMap[match])
|
||||
.replace(RE_CUSTOM_EMOJI, (match) => replacementsMap[match])
|
||||
.replace(RE_SPOILER, "<spoiler>");
|
||||
}
|
||||
|
||||
@@ -449,4 +594,30 @@ export class Client extends AsyncEventEmitter<Events> {
|
||||
|
||||
return data.id;
|
||||
}
|
||||
|
||||
setSlowmode(channelId: string, data: UserSlowmodes): void {
|
||||
const existing = this.#slowmodeTimers.get(channelId);
|
||||
if (existing) clearTimeout(existing);
|
||||
|
||||
this.userSlowmodes.set(channelId, { ...data, receivedAt: Date.now() });
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
this.userSlowmodes.delete(channelId);
|
||||
this.#slowmodeTimers.delete(channelId);
|
||||
this.emit("userSlowmodes");
|
||||
}, data.retry_after * 1000);
|
||||
|
||||
this.#slowmodeTimers.set(channelId, timer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Backend enforced limits for the logged in user
|
||||
*/
|
||||
get limits(): UserLimits | undefined {
|
||||
if (!this.configured() || !this.user) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.user.limits;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
+11
-2
@@ -1,8 +1,9 @@
|
||||
import type { DataEditBot } from "revolt-api";
|
||||
import type { DataEditBot } from "stoat-api";
|
||||
import { decodeTime } from "ulid";
|
||||
|
||||
import type { BotCollection } from "../collections/BotCollection.js";
|
||||
import type { BotFlags } from "../hydration/bot.js";
|
||||
import { hydrate } from "../hydration/index.js";
|
||||
|
||||
import { PublicBot } from "./PublicBot.js";
|
||||
import type { User } from "./User.js";
|
||||
@@ -139,7 +140,15 @@ export class Bot {
|
||||
* @param data Changes
|
||||
*/
|
||||
async edit(data: DataEditBot): Promise<void> {
|
||||
await this.#collection.client.api.patch(`/bots/${this.id as ""}`, data);
|
||||
this.#collection.updateUnderlyingObject(
|
||||
this.id,
|
||||
hydrate(
|
||||
"bot",
|
||||
await this.#collection.client.api.patch(`/bots/${this.id as ""}`, data),
|
||||
this.#collection.client,
|
||||
false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+79
-18
@@ -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,
|
||||
@@ -11,8 +12,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";
|
||||
import { decodeTime, ulid } from "ulid";
|
||||
|
||||
import { ChannelCollection } from "../collections/index.js";
|
||||
@@ -29,6 +30,7 @@ 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 { VoiceParticipant, VoiceStatus } from "./VoiceParticipant.js";
|
||||
|
||||
/**
|
||||
* Channel Class
|
||||
@@ -39,6 +41,8 @@ export class Channel {
|
||||
|
||||
_typingTimers: Record<string, number> = {};
|
||||
|
||||
voiceParticipants = new ReactiveMap<string, VoiceParticipant>();
|
||||
|
||||
/**
|
||||
* Construct Channel
|
||||
* @param collection Collection
|
||||
@@ -173,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;
|
||||
}
|
||||
|
||||
@@ -229,21 +233,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<string, { a: number; d: number }> | undefined {
|
||||
get rolePermissions(): Record<string, { a: bigint; d: bigint }> | undefined {
|
||||
return this.#collection.getUnderlyingObject(this.id).rolePermissions;
|
||||
}
|
||||
|
||||
@@ -291,7 +295,6 @@ export class Channel {
|
||||
if (
|
||||
!this.lastMessageId ||
|
||||
this.type === "SavedMessages" ||
|
||||
this.type === "VoiceChannel" ||
|
||||
this.#collection.client.options.channelExclusiveMuted(this)
|
||||
)
|
||||
return false;
|
||||
@@ -314,8 +317,7 @@ export class Channel {
|
||||
* Get mentions in this channel for user.
|
||||
*/
|
||||
get mentions(): ReactiveSet<string> | undefined {
|
||||
if (this.type === "SavedMessages" || this.type === "VoiceChannel")
|
||||
return undefined;
|
||||
if (this.type === "SavedMessages") return undefined;
|
||||
|
||||
return this.#collection.client.channelUnreads.get(this.id)
|
||||
?.messageMentionIds;
|
||||
@@ -327,7 +329,11 @@ 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 (
|
||||
this.type === "DirectMessage" ||
|
||||
this.type === "Group" ||
|
||||
typeof this.#collection.getUnderlyingObject(this.id).voice === "object"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -350,16 +356,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,
|
||||
),
|
||||
)
|
||||
@@ -369,7 +375,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);
|
||||
}
|
||||
|
||||
@@ -473,7 +479,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<void> {
|
||||
await this.#collection.client.api.delete(`/channels/${this.id as ""}`, {
|
||||
@@ -694,7 +700,7 @@ export class Channel {
|
||||
|
||||
/**
|
||||
* Create an invite to the channel
|
||||
* @requires `TextChannel`, `VoiceChannel`
|
||||
* @requires `TextChannel`
|
||||
* @returns Newly created invite code
|
||||
*/
|
||||
async createInvite(): Promise<Invite> {
|
||||
@@ -780,7 +786,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",
|
||||
@@ -792,6 +798,37 @@ export class Channel {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get slowmode value for the channel
|
||||
*/
|
||||
get slowmode(): number {
|
||||
return this.#collection.getUnderlyingObject(this.id).slowmode ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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?: 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,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start typing in this channel
|
||||
* @requires `DirectMessage`, `Group`, `TextChannel`
|
||||
@@ -813,4 +850,28 @@ export class Channel {
|
||||
channel: this.id,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The voice status of a channel. Screenshare supersedes video, video
|
||||
* supersedes voice, and voice supersedes none. This getter takes the highest
|
||||
* priority status from all participants in the channel.
|
||||
*/
|
||||
get voiceStatus(): VoiceStatus {
|
||||
if (!this.isVoice || this.voiceParticipants.size === 0) {
|
||||
return "none";
|
||||
}
|
||||
|
||||
let highest: VoiceStatus = "voice";
|
||||
for (const participant of this.voiceParticipants.values()) {
|
||||
const status = participant.voiceStatus;
|
||||
if (status === "screenshare") {
|
||||
return "screenshare";
|
||||
}
|
||||
if (status === "video") {
|
||||
highest = "video";
|
||||
}
|
||||
}
|
||||
|
||||
return highest;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DataEditWebhook } from "revolt-api";
|
||||
import { DataEditWebhook } from "stoat-api";
|
||||
|
||||
import type { ChannelWebhookCollection } from "../collections/ChannelWebhookCollection.js";
|
||||
import { hydrate } from "../hydration/index.js";
|
||||
|
||||
@@ -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";
|
||||
|
||||
+1
-1
@@ -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";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Invite } from "revolt-api";
|
||||
import type { Invite } from "stoat-api";
|
||||
|
||||
import type { Client } from "../Client.js";
|
||||
|
||||
|
||||
+4
-4
@@ -6,7 +6,7 @@ import type {
|
||||
MFAResponse,
|
||||
MultiFactorStatus,
|
||||
MFATicket as TicketType,
|
||||
} from "revolt-api";
|
||||
} from "stoat-api";
|
||||
|
||||
import type { Client } from "../Client.js";
|
||||
|
||||
@@ -31,14 +31,14 @@ export class MFA {
|
||||
* Whether authenticator app is enabled
|
||||
*/
|
||||
get authenticatorEnabled(): boolean {
|
||||
return this.#store[0].totp_mfa;
|
||||
return this.#store[0].totp_mfa ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether recovery codes are enabled
|
||||
*/
|
||||
get recoveryEnabled(): boolean {
|
||||
return this.#store[0].recovery_active;
|
||||
return this.#store[0].recovery_active ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -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],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
@@ -271,6 +271,13 @@ export class Message {
|
||||
return this.masquerade?.colour ?? this.member?.roleColour;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the role colour for this message
|
||||
*/
|
||||
get iconRole(): ServerRole | null | undefined {
|
||||
return this.member?.iconRole;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the avatar URL for this message
|
||||
*/
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
+77
-21
@@ -13,9 +13,8 @@ import type {
|
||||
DataEditRole,
|
||||
DataEditServer,
|
||||
Override,
|
||||
OverrideField,
|
||||
Role,
|
||||
} from "revolt-api";
|
||||
} from "stoat-api";
|
||||
import { decodeTime } from "ulid";
|
||||
|
||||
import type { ServerCollection } from "../collections/ServerCollection.js";
|
||||
@@ -35,6 +34,7 @@ import { ServerBan } from "./ServerBan.js";
|
||||
import { ServerMember } from "./ServerMember.js";
|
||||
import { ServerRole } from "./ServerRole.js";
|
||||
import { User } from "./User.js";
|
||||
import { VoiceStatus } from "./VoiceParticipant.js";
|
||||
|
||||
/**
|
||||
* Server Class
|
||||
@@ -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);
|
||||
}
|
||||
@@ -167,7 +168,7 @@ export class Server {
|
||||
/**
|
||||
* Default permissions
|
||||
*/
|
||||
get defaultPermissions(): number {
|
||||
get defaultPermissions(): bigint {
|
||||
return this.#collection.getUnderlyingObject(this.id).defaultPermissions;
|
||||
}
|
||||
|
||||
@@ -269,14 +270,7 @@ export class Server {
|
||||
* ranking roles. This is dictated by the "rank" property
|
||||
* which is smaller for higher priority roles.
|
||||
*/
|
||||
get orderedRoles(): {
|
||||
name: string;
|
||||
permissions: OverrideField;
|
||||
colour?: string | null;
|
||||
hoist?: boolean;
|
||||
rank?: number;
|
||||
id: string;
|
||||
}[] {
|
||||
get orderedRoles(): ServerRole[] {
|
||||
const roles = this.roles;
|
||||
return roles
|
||||
? [...roles.values()].sort((a, b) => (a.rank || 0) - (b.rank || 0))
|
||||
@@ -337,7 +331,8 @@ export class Server {
|
||||
/**
|
||||
* Permission the currently authenticated user has against this server
|
||||
*/
|
||||
get permission(): number {
|
||||
get permission(): bigint {
|
||||
if (!this.$exists) return 0n;
|
||||
return calculatePermission(this.#collection.client, this);
|
||||
}
|
||||
|
||||
@@ -648,11 +643,10 @@ export class Server {
|
||||
|
||||
#synced: undefined | "partial" | "full";
|
||||
|
||||
/**
|
||||
* Optimised member fetch route
|
||||
* @param excludeOffline
|
||||
*/
|
||||
async syncMembers(excludeOffline?: boolean): Promise<void> {
|
||||
async syncMembers(
|
||||
excludeOffline?: boolean,
|
||||
excludeOfflineUserCap?: number,
|
||||
): Promise<void> {
|
||||
if (this.#synced && (this.#synced === "full" || excludeOffline)) return;
|
||||
|
||||
const data = await this.#collection.client.api.get(
|
||||
@@ -661,7 +655,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) {
|
||||
@@ -795,4 +827,28 @@ export class Server {
|
||||
async deleteEmoji(emojiId: string): Promise<void> {
|
||||
await this.#collection.client.api.delete(`/custom/emoji/${emojiId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* The voice status of a server. Screenshare supersedes video, video
|
||||
* supersedes voice, and voice supersedes none. This getter takes the highest
|
||||
* priority status from all channels in the server.
|
||||
*/
|
||||
get voiceStatus(): VoiceStatus {
|
||||
let highest: VoiceStatus = "none";
|
||||
|
||||
for (const chan of this.channels) {
|
||||
const status = chan.voiceStatus;
|
||||
if (status === "screenshare") {
|
||||
return "screenshare";
|
||||
}
|
||||
if (status === "video") {
|
||||
highest = "video";
|
||||
}
|
||||
if (status === "voice" && highest === "none") {
|
||||
highest = "voice";
|
||||
}
|
||||
}
|
||||
|
||||
return highest;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
+28
-13
@@ -2,8 +2,7 @@ import type {
|
||||
DataBanCreate,
|
||||
DataMemberEdit,
|
||||
MemberCompositeKey,
|
||||
Role,
|
||||
} from "revolt-api";
|
||||
} from "stoat-api";
|
||||
|
||||
import type { ServerMemberCollection } from "../collections/ServerMemberCollection.js";
|
||||
import {
|
||||
@@ -15,6 +14,7 @@ import { Permission } from "../permissions/definitions.js";
|
||||
import type { Channel } from "./Channel.js";
|
||||
import type { File } from "./File.js";
|
||||
import type { Server } from "./Server.js";
|
||||
import { ServerRole } from "./ServerRole.js";
|
||||
import type { User } from "./User.js";
|
||||
|
||||
/**
|
||||
@@ -93,6 +93,13 @@ export class ServerMember {
|
||||
return this.#collection.getUnderlyingObject(key(this.id)).avatar;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pronouns
|
||||
*/
|
||||
get pronouns(): string | undefined {
|
||||
return this.#collection.getUnderlyingObject(key(this.id)).pronouns;
|
||||
}
|
||||
|
||||
/**
|
||||
* List of role IDs
|
||||
*/
|
||||
@@ -110,22 +117,18 @@ export class ServerMember {
|
||||
/**
|
||||
* Ordered list of roles for this member, from lowest to highest priority.
|
||||
*/
|
||||
get orderedRoles(): (Partial<Role> & { id: string })[] {
|
||||
get orderedRoles(): ServerRole[] {
|
||||
const server = this.server!;
|
||||
return (
|
||||
this.roles
|
||||
?.map((id) => ({
|
||||
id,
|
||||
...server.roles?.get(id),
|
||||
}))
|
||||
.sort((a, b) => b.rank! - a.rank!) ?? []
|
||||
);
|
||||
return this.roles
|
||||
?.map((id) => server.roles.get(id)!)
|
||||
.filter((role) => role)
|
||||
.sort((a, b) => b.rank! - a.rank!);
|
||||
}
|
||||
|
||||
/**
|
||||
* Member's currently hoisted role.
|
||||
*/
|
||||
get hoistedRole(): Partial<Role> | null {
|
||||
get hoistedRole(): ServerRole | null {
|
||||
const roles = this.orderedRoles.filter((x) => x.hoist);
|
||||
if (roles.length > 0) {
|
||||
return roles[roles.length - 1];
|
||||
@@ -146,6 +149,18 @@ export class ServerMember {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Member's current role icon with name.
|
||||
*/
|
||||
get iconRole(): ServerRole | null | undefined {
|
||||
const roles = this.orderedRoles.filter((x) => x.icon);
|
||||
if (roles.length > 0) {
|
||||
return roles[roles.length - 1]!;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Member's ranking
|
||||
* Smaller values are ranked as higher priority
|
||||
@@ -168,7 +183,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,
|
||||
});
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import type { Role as APIRole, OverrideField } from "revolt-api";
|
||||
import type { Role as APIRole } from "stoat-api";
|
||||
|
||||
import type { Client } from "../Client.js";
|
||||
|
||||
import { File } from "./File.js";
|
||||
|
||||
/**
|
||||
* Server Role
|
||||
*/
|
||||
@@ -11,7 +13,11 @@ export class ServerRole {
|
||||
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
readonly permissions: OverrideField;
|
||||
readonly icon?: File;
|
||||
readonly permissions: {
|
||||
a: bigint;
|
||||
d: bigint;
|
||||
};
|
||||
readonly colour?: string;
|
||||
readonly hoist: boolean;
|
||||
readonly rank: number;
|
||||
@@ -29,7 +35,11 @@ export class ServerRole {
|
||||
|
||||
this.id = id;
|
||||
this.name = data.name;
|
||||
this.permissions = data.permissions;
|
||||
this.icon = data.icon ? new File(client, data.icon) : undefined;
|
||||
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;
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import type { SystemMessage as APISystemMessage } from "revolt-api";
|
||||
import type {
|
||||
Message as APIMessage,
|
||||
SystemMessage as APISystemMessage,
|
||||
} from "stoat-api";
|
||||
import { decodeTime } from "ulid";
|
||||
|
||||
import type { Client } from "../Client.js";
|
||||
|
||||
@@ -28,7 +32,11 @@ 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 +58,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 +282,40 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
+54
-6
@@ -1,13 +1,21 @@
|
||||
import type { User as APIUser, DataEditUser, Presence } from "revolt-api";
|
||||
import type {
|
||||
User as APIUser,
|
||||
UserLimits as APIUserLimits,
|
||||
DataEditUser,
|
||||
Presence,
|
||||
} from "stoat-api";
|
||||
import { decodeTime } from "ulid";
|
||||
|
||||
import type { UserCollection } from "../collections/UserCollection.js";
|
||||
import { hydrate } from "../hydration/index.js";
|
||||
import { U32_MAX, UserPermission } from "../permissions/definitions.js";
|
||||
|
||||
import type { Channel } from "./Channel.js";
|
||||
import type { File } from "./File.js";
|
||||
import { UserProfile } from "./UserProfile.js";
|
||||
|
||||
export type UserLimits = APIUserLimits;
|
||||
|
||||
/**
|
||||
* User Class
|
||||
*/
|
||||
@@ -47,6 +55,13 @@ export class User {
|
||||
return new Date(decodeTime(this.id));
|
||||
}
|
||||
|
||||
/**
|
||||
* How old this user is in milliseconds
|
||||
*/
|
||||
get age() {
|
||||
return new Date().getTime() - this.createdAt.getTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* Username
|
||||
*/
|
||||
@@ -78,6 +93,13 @@ export class User {
|
||||
return this.#collection.getUnderlyingObject(this.id).avatar;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pronouns
|
||||
*/
|
||||
get pronouns(): string | undefined {
|
||||
return this.#collection.getUnderlyingObject(this.id).pronouns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Badges
|
||||
*/
|
||||
@@ -225,11 +247,19 @@ export class User {
|
||||
* @param data Changes
|
||||
*/
|
||||
async edit(data: DataEditUser): Promise<void> {
|
||||
await this.#collection.client.api.patch(
|
||||
`/users/${
|
||||
this.id === this.#collection.client.user?.id ? "@me" : this.id
|
||||
}`,
|
||||
data,
|
||||
this.#collection.updateUnderlyingObject(
|
||||
this.id,
|
||||
hydrate(
|
||||
"user",
|
||||
await this.#collection.client.api.patch(
|
||||
`/users/${
|
||||
this.id === this.#collection.client.user?.id ? "@me" : this.id
|
||||
}`,
|
||||
data,
|
||||
),
|
||||
this.#collection.client,
|
||||
false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -325,4 +355,22 @@ export class User {
|
||||
`/users/${this.id as ""}/mutual`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Backend enforced limits for a user
|
||||
*/
|
||||
get limits(): UserLimits | undefined {
|
||||
if (!this.#collection.client.configured()) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
this.age <
|
||||
(this.#collection.client.configuration?.features.limits.global
|
||||
.new_user_hours ?? 72) *
|
||||
3600_0000
|
||||
) {
|
||||
return this.#collection.client.configuration?.features.limits.new_user;
|
||||
}
|
||||
return this.#collection.client.configuration?.features.limits.default;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import { Accessor, Setter, createSignal } from "solid-js";
|
||||
|
||||
import type { Client } from "../Client.js";
|
||||
import { UserVoiceState } from "../events/v1.js";
|
||||
|
||||
/**
|
||||
* The voice status of an entity. Screenshare supersedes video, video supersedes voice, and voice supersedes none.
|
||||
*/
|
||||
export type VoiceStatus = "none" | "voice" | "video" | "screenshare";
|
||||
|
||||
/**
|
||||
* Voice Participant
|
||||
*/
|
||||
export class VoiceParticipant {
|
||||
protected client: Client;
|
||||
readonly userId: string;
|
||||
readonly joinedAt: Date;
|
||||
|
||||
readonly isReceiving: Accessor<boolean>;
|
||||
readonly isPublishing: Accessor<boolean>;
|
||||
readonly isScreensharing: Accessor<boolean>;
|
||||
readonly isCamera: Accessor<boolean>;
|
||||
|
||||
#setReceiving: Setter<boolean>;
|
||||
#setPublishing: Setter<boolean>;
|
||||
#setScreensharing: Setter<boolean>;
|
||||
#setCamera: Setter<boolean>;
|
||||
|
||||
/**
|
||||
* 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<UserVoiceState>) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The voice status of a participant. Screenshare supersedes video, video supersedes voice, and voice supersedes none.
|
||||
*/
|
||||
get voiceStatus(): VoiceStatus {
|
||||
if (this.isScreensharing()) {
|
||||
return "screenshare";
|
||||
}
|
||||
if (this.isCamera()) {
|
||||
return "video";
|
||||
}
|
||||
return "voice";
|
||||
}
|
||||
}
|
||||
@@ -19,3 +19,4 @@ export * from "./SystemMessage.js";
|
||||
export * from "./User.js";
|
||||
export * from "./MFA.js";
|
||||
export * from "./UserProfile.js";
|
||||
export * from "./VoiceParticipant.js";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
@@ -71,10 +71,11 @@ export class BotCollection extends ClassCollection<Bot, HydratedBot> {
|
||||
* @returns The newly-created bot
|
||||
*/
|
||||
async createBot(name: string): Promise<Bot> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { batch } from "solid-js";
|
||||
|
||||
import type {
|
||||
Server as APIServer,
|
||||
Channel,
|
||||
DataCreateServer,
|
||||
} from "revolt-api";
|
||||
import type { Server as APIServer, Channel, DataCreateServer } from "stoat-api";
|
||||
|
||||
import { Server } from "../classes/Server.js";
|
||||
import type { HydratedServer } from "../hydration/server.js";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -2,7 +2,8 @@ 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 { JSONParse, JSONStringify } from "json-with-bigint";
|
||||
import type { Error } from "stoat-api";
|
||||
|
||||
import type { ProtocolV1 } from "./v1.js";
|
||||
|
||||
@@ -155,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(() => {
|
||||
@@ -179,7 +197,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 +232,7 @@ export class EventClient<
|
||||
send(event: EventProtocol<T>["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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+248
-102
@@ -4,6 +4,7 @@ import { batch } from "solid-js";
|
||||
import { ReactiveSet } from "@solid-primitives/set";
|
||||
import type {
|
||||
Channel,
|
||||
ChannelUnread,
|
||||
Emoji,
|
||||
Error,
|
||||
FieldsChannel,
|
||||
@@ -17,11 +18,12 @@ import type {
|
||||
Role,
|
||||
Server,
|
||||
User,
|
||||
} from "revolt-api";
|
||||
} from "stoat-api";
|
||||
|
||||
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";
|
||||
|
||||
/**
|
||||
@@ -42,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
|
||||
@@ -65,51 +67,51 @@ type ServerMessage =
|
||||
| { type: "Error"; data: Error }
|
||||
| { type: "Bulk"; v: ServerMessage[] }
|
||||
| { type: "Authenticated" }
|
||||
| ({ type: "Ready" } & ReadyData)
|
||||
| ({ type: "Ready" } & Partial<ReadyData>)
|
||||
| { type: "Ping"; data: number }
|
||||
| { type: "Pong"; data: number }
|
||||
| ({ type: "Message" } & Message)
|
||||
| {
|
||||
type: "MessageUpdate";
|
||||
id: string;
|
||||
channel: string;
|
||||
data: Partial<Message>;
|
||||
}
|
||||
type: "MessageUpdate";
|
||||
id: string;
|
||||
channel: string;
|
||||
data: Partial<Message>;
|
||||
}
|
||||
| {
|
||||
type: "MessageAppend";
|
||||
id: string;
|
||||
channel: string;
|
||||
append: Pick<Partial<Message>, "embeds">;
|
||||
}
|
||||
type: "MessageAppend";
|
||||
id: string;
|
||||
channel: string;
|
||||
append: Pick<Partial<Message>, "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<Channel>;
|
||||
clear?: FieldsChannel[];
|
||||
}
|
||||
type: "ChannelUpdate";
|
||||
id: string;
|
||||
data: Partial<Channel>;
|
||||
clear?: FieldsChannel[];
|
||||
}
|
||||
| { type: "ChannelDelete"; id: string }
|
||||
| { type: "ChannelGroupJoin"; id: string; user: string }
|
||||
| { type: "ChannelGroupLeave"; id: string; user: string }
|
||||
@@ -117,63 +119,95 @@ 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<Server>;
|
||||
clear?: FieldsServer[];
|
||||
}
|
||||
type: "ServerUpdate";
|
||||
id: string;
|
||||
data: Partial<Server>;
|
||||
clear?: FieldsServer[];
|
||||
}
|
||||
| { type: "ServerDelete"; id: string }
|
||||
| {
|
||||
type: "ServerMemberUpdate";
|
||||
id: MemberCompositeKey;
|
||||
data: Partial<Member>;
|
||||
clear?: FieldsMember[];
|
||||
}
|
||||
type: "ServerMemberUpdate";
|
||||
id: MemberCompositeKey;
|
||||
data: Partial<Member>;
|
||||
clear?: FieldsMember[];
|
||||
}
|
||||
| { type: "ServerMemberJoin"; id: string; user: string }
|
||||
| { type: "ServerMemberLeave"; id: string; user: string }
|
||||
| {
|
||||
type: "ServerRoleUpdate";
|
||||
id: string;
|
||||
role_id: string;
|
||||
data: Partial<Role>;
|
||||
}
|
||||
type: "ServerRoleUpdate";
|
||||
id: string;
|
||||
role_id: string;
|
||||
data: Partial<Role>;
|
||||
}
|
||||
| { type: "ServerRoleDelete"; id: string; role_id: string }
|
||||
| {
|
||||
type: "UserUpdate";
|
||||
id: string;
|
||||
data: Partial<User>;
|
||||
clear?: FieldsUser[];
|
||||
}
|
||||
type: "UserUpdate";
|
||||
id: string;
|
||||
data: Partial<User>;
|
||||
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: "VoiceChannelLeave";
|
||||
id: string;
|
||||
user: string;
|
||||
}
|
||||
| {
|
||||
type: "VoiceChannelMove";
|
||||
user: string;
|
||||
from: string;
|
||||
to: string;
|
||||
state: UserVoiceState;
|
||||
}
|
||||
| {
|
||||
type: "UserVoiceStateUpdate";
|
||||
id: string;
|
||||
channel_id: string;
|
||||
data: Partial<UserVoiceState>;
|
||||
}
|
||||
| {
|
||||
type: "UserMoveVoiceChannel";
|
||||
node: string;
|
||||
token: string;
|
||||
}
|
||||
| {
|
||||
type: "UserSlowmodes";
|
||||
slowmodes: UserSlowmodes[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Policy change type
|
||||
@@ -185,6 +219,36 @@ 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[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Channel slowmodes for the active user
|
||||
*/
|
||||
export type UserSlowmodes = {
|
||||
channel_id: string;
|
||||
duration: number;
|
||||
retry_after: number;
|
||||
receivedAt?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Initial synchronisation packet
|
||||
*/
|
||||
@@ -194,6 +258,11 @@ type ReadyData = {
|
||||
channels: Channel[];
|
||||
members: Member[];
|
||||
emojis: Emoji[];
|
||||
voice_states: ChannelVoiceState[];
|
||||
|
||||
user_settings: Record<string, unknown>;
|
||||
channel_unreads: ChannelUnread[];
|
||||
|
||||
policy_changes: PolicyChange[];
|
||||
};
|
||||
|
||||
@@ -221,28 +290,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 emoji of event.emojis) {
|
||||
client.emojis.getOrCreate(emoji._id, emoji);
|
||||
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),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (event.emojis) {
|
||||
for (const emoji of event.emojis) {
|
||||
client.emojis.getOrCreate(emoji._id, emoji);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -253,7 +347,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"),
|
||||
);
|
||||
@@ -280,7 +374,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) &&
|
||||
@@ -526,7 +624,7 @@ export async function handleEvent(
|
||||
{ ...event, type: "ChannelStopTyping" },
|
||||
setReady,
|
||||
),
|
||||
1000,
|
||||
4000,
|
||||
) as never;
|
||||
|
||||
client.emit(
|
||||
@@ -641,7 +739,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;
|
||||
@@ -700,6 +798,9 @@ export async function handleEvent(
|
||||
case "Timeout":
|
||||
changes["timeout"] = undefined;
|
||||
break;
|
||||
case "Pronouns":
|
||||
changes["pronouns"] = undefined;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -771,6 +872,9 @@ export async function handleEvent(
|
||||
text: undefined,
|
||||
};
|
||||
break;
|
||||
case "Pronouns":
|
||||
changes["pronouns"] = undefined;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -865,5 +969,47 @@ 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;
|
||||
}
|
||||
case "UserSlowmodes": {
|
||||
for (const slowmode of event.slowmodes) {
|
||||
client.setSlowmode(slowmode.channel_id, slowmode);
|
||||
}
|
||||
client.emit("userSlowmodes");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
+31
-12
@@ -1,5 +1,5 @@
|
||||
import { ReactiveSet } from "@solid-primitives/set";
|
||||
import type { Channel as APIChannel, OverrideField } from "revolt-api";
|
||||
import type { Channel as APIChannel } from "stoat-api";
|
||||
|
||||
import type { Client } from "../Client.js";
|
||||
import { File } from "../classes/File.js";
|
||||
@@ -23,14 +23,15 @@ export type HydratedChannel = {
|
||||
ownerId?: string;
|
||||
serverId?: string;
|
||||
|
||||
permissions?: number;
|
||||
defaultPermissions?: OverrideField;
|
||||
rolePermissions?: Record<string, OverrideField>;
|
||||
permissions?: bigint;
|
||||
defaultPermissions?: { a: bigint; d: bigint };
|
||||
rolePermissions?: Record<string, { a: bigint; d: bigint }>;
|
||||
nsfw: boolean;
|
||||
slowmode: number;
|
||||
|
||||
lastMessageId?: string;
|
||||
|
||||
voice: boolean;
|
||||
voice?: { maxUsers?: number };
|
||||
};
|
||||
|
||||
export const channelHydration: Hydrate<Merge<APIChannel>, HydratedChannel> = {
|
||||
@@ -44,6 +45,7 @@ export const channelHydration: Hydrate<Merge<APIChannel>, HydratedChannel> = {
|
||||
default_permissions: "defaultPermissions",
|
||||
role_permissions: "rolePermissions",
|
||||
last_message_id: "lastMessageId",
|
||||
slowmode: "slowmode",
|
||||
},
|
||||
functions: {
|
||||
id: (channel) => channel._id,
|
||||
@@ -57,15 +59,32 @@ export const channelHydration: Hydrate<Merge<APIChannel>, 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) => {
|
||||
console.info(channel);
|
||||
return typeof (channel as never as { voice: object }).voice === "object";
|
||||
},
|
||||
slowmode: (channel) => channel.slowmode ?? 0,
|
||||
voice: (channel) =>
|
||||
!!channel.voice ||
|
||||
channel.channel_type === "DirectMessage" ||
|
||||
channel.channel_type === "Group"
|
||||
? {
|
||||
maxUsers: channel.voice?.max_users || undefined,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
initialHydration: () => ({
|
||||
typingIds: new ReactiveSet(),
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
@@ -53,7 +53,7 @@ export const messageHydration: Hydrate<Merge<Message>, 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!),
|
||||
|
||||
@@ -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";
|
||||
@@ -27,7 +27,7 @@ export type HydratedServer = {
|
||||
|
||||
systemMessages?: SystemMessageChannels;
|
||||
roles: ReactiveMap<string, ServerRole>;
|
||||
defaultPermissions: number;
|
||||
defaultPermissions: bigint;
|
||||
|
||||
flags: ServerFlags;
|
||||
analytics: boolean;
|
||||
@@ -58,7 +58,7 @@ export const serverHydration: Hydrate<APIServer, HydratedServer> = {
|
||||
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!,
|
||||
|
||||
@@ -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";
|
||||
@@ -11,6 +11,7 @@ export type HydratedServerMember = {
|
||||
joinedAt: Date;
|
||||
nickname?: string;
|
||||
avatar?: File;
|
||||
pronouns?: string;
|
||||
roles: string[];
|
||||
timeout?: Date;
|
||||
};
|
||||
@@ -28,6 +29,7 @@ export const serverMemberHydration: Hydrate<
|
||||
joinedAt: (member) => new Date(member.joined_at),
|
||||
nickname: (member) => member.nickname!,
|
||||
avatar: (member, ctx) => new File(ctx as Client, member.avatar!),
|
||||
pronouns: (member) => member.pronouns!,
|
||||
roles: (member) => member.roles,
|
||||
timeout: (member) => new Date(member.timeout!),
|
||||
},
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
@@ -25,6 +25,7 @@ export type HydratedUser = {
|
||||
flags: UserFlags;
|
||||
|
||||
avatar?: File;
|
||||
pronouns?: string;
|
||||
status?: UserStatus;
|
||||
bot?: BotInformation;
|
||||
};
|
||||
@@ -49,6 +50,7 @@ export const userHydration: Hydrate<APIUser, HydratedUser> = {
|
||||
flags: (user) => user.flags!,
|
||||
|
||||
avatar: (user, ctx) => new File(ctx as Client, user.avatar!),
|
||||
pronouns: (user) => user.pronouns,
|
||||
status: (user) => user.status!,
|
||||
bot: (user) => user.bot!,
|
||||
},
|
||||
|
||||
+2
-2
@@ -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";
|
||||
@@ -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";
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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.
|
||||
@@ -99,11 +99,10 @@ 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;
|
||||
if (typeof server === "undefined") return 0n;
|
||||
|
||||
// 3. If server owner, just grant all permissions.
|
||||
if (server.ownerId === user?.id) {
|
||||
@@ -116,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));
|
||||
@@ -146,9 +145,11 @@ export function calculatePermission(
|
||||
perm = perm & BigInt(ALLOW_IN_TIMEOUT);
|
||||
}
|
||||
|
||||
return Number(perm);
|
||||
return perm;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0n;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,79 +14,83 @@ 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,
|
||||
/// Bypass slowmode
|
||||
BypassSlowmode: 2n ** 39n,
|
||||
|
||||
// * 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 +98,7 @@ export const Permission = {
|
||||
|
||||
// * Grant all permissions
|
||||
/// Safely grant all permissions
|
||||
GrantAllSafe: 0x000f_ffff_ffff_ffff,
|
||||
GrantAllSafe: 0x000f_ffff_ffff_ffffn,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -124,7 +128,9 @@ export const DEFAULT_PERMISSION =
|
||||
Permission.SendEmbeds +
|
||||
Permission.UploadFiles +
|
||||
Permission.Connect +
|
||||
Permission.Speak;
|
||||
Permission.Speak +
|
||||
Permission.Video +
|
||||
Permission.Listen;
|
||||
|
||||
/**
|
||||
* Permissions in saved messages channel
|
||||
|
||||
@@ -40,7 +40,7 @@ export class ObjectStorage<T> {
|
||||
id: string,
|
||||
type: keyof Hydrators,
|
||||
context: unknown,
|
||||
data?: unknown
|
||||
data?: unknown,
|
||||
): void {
|
||||
if (data) {
|
||||
data = { partial: false, ...data };
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ES6",
|
||||
"moduleResolution": "node",
|
||||
"module": "es6",
|
||||
"moduleResolution": "bundler",
|
||||
"rootDir": "./src",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
|
||||
Reference in New Issue
Block a user