mirror of
https://github.com/stoatchat/javascript-client-sdk.git
synced 2026-07-18 16:34:27 -04:00
Compare commits
34 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 |
@@ -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,
|
||||
]);
|
||||
|
||||
+5
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "stoat.js",
|
||||
"version": "7.3.4",
|
||||
"version": "7.3.7",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./lib/index.js"
|
||||
@@ -31,10 +31,11 @@
|
||||
"@vladfrangu/async_event_emitter": "^2.4.6",
|
||||
"json-with-bigint": "^3.4.4",
|
||||
"solid-js": "^1.9.6",
|
||||
"stoat-api": "0.8.9-4",
|
||||
"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",
|
||||
@@ -46,4 +47,4 @@
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "^8.32.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+34
-12
@@ -24,12 +24,15 @@ importers:
|
||||
specifier: ^1.9.6
|
||||
version: 1.9.6
|
||||
stoat-api:
|
||||
specifier: 0.8.9-4
|
||||
version: 0.8.9-4
|
||||
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))
|
||||
@@ -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}
|
||||
@@ -981,8 +994,8 @@ packages:
|
||||
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
stoat-api@0.8.9-4:
|
||||
resolution: {integrity: sha512-6N4kfE1x+j/XYVaBuvYqzbWxjgqbxSarvNjMv8GEcBaqbkiyPzUwsCj12NJsNNp4uyZmsqoUYUj2jIq018uqiA==}
|
||||
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==}
|
||||
@@ -1046,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:
|
||||
@@ -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:
|
||||
@@ -2068,7 +2090,7 @@ snapshots:
|
||||
|
||||
statuses@2.0.1: {}
|
||||
|
||||
stoat-api@0.8.9-4:
|
||||
stoat-api@0.14.0:
|
||||
dependencies:
|
||||
json-with-bigint: 3.4.4
|
||||
|
||||
@@ -2132,7 +2154,7 @@ snapshots:
|
||||
|
||||
uc.micro@2.1.0: {}
|
||||
|
||||
ulid@2.4.0: {}
|
||||
ulid@3.0.2: {}
|
||||
|
||||
undici-types@6.21.0: {}
|
||||
|
||||
|
||||
+179
-8
@@ -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 "stoat-api";
|
||||
import type { DataLogin, RevoltConfig, Role } 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;
|
||||
}
|
||||
}
|
||||
|
||||
+10
-1
@@ -3,6 +3,7 @@ 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+56
-14
@@ -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,8 +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 { ReactiveMap } from "@solid-primitives/map";
|
||||
import { VoiceParticipant } from "./VoiceParticipant.js";
|
||||
import { VoiceParticipant, VoiceStatus } from "./VoiceParticipant.js";
|
||||
|
||||
/**
|
||||
* Channel Class
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -317,8 +317,7 @@ export class Channel {
|
||||
* Get mentions in this channel for user.
|
||||
*/
|
||||
get mentions(): ReactiveSet<string> | 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"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -795,20 +798,35 @@ 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 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,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -832,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;
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -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],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
+82
-24
@@ -13,7 +13,6 @@ import type {
|
||||
DataEditRole,
|
||||
DataEditServer,
|
||||
Override,
|
||||
OverrideField,
|
||||
Role,
|
||||
} from "stoat-api";
|
||||
import { decodeTime } from "ulid";
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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: { a: bigint, d: bigint };
|
||||
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))
|
||||
@@ -338,6 +332,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);
|
||||
}
|
||||
|
||||
@@ -514,11 +509,12 @@ export class Server {
|
||||
*/
|
||||
async kickUser(user: string | User | ServerMember): Promise<void> {
|
||||
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 +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(
|
||||
@@ -660,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) {
|
||||
@@ -722,7 +755,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,
|
||||
@@ -793,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;
|
||||
}
|
||||
}
|
||||
|
||||
+26
-11
@@ -2,7 +2,6 @@ import type {
|
||||
DataBanCreate,
|
||||
DataMemberEdit,
|
||||
MemberCompositeKey,
|
||||
Role,
|
||||
} from "stoat-api";
|
||||
|
||||
import type { ServerMemberCollection } from "../collections/ServerMemberCollection.js";
|
||||
@@ -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<Omit<Role, 'permissions'> & { permissions: { a: bigint, d: bigint } }> & { 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<Omit<Role, 'permissions'> & { permissions: { a: bigint, d: bigint } }> | 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
|
||||
|
||||
@@ -2,6 +2,8 @@ import type { Role as APIRole } from "stoat-api";
|
||||
|
||||
import type { Client } from "../Client.js";
|
||||
|
||||
import { File } from "./File.js";
|
||||
|
||||
/**
|
||||
* Server Role
|
||||
*/
|
||||
@@ -11,9 +13,10 @@ export class ServerRole {
|
||||
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
readonly icon?: File;
|
||||
readonly permissions: {
|
||||
a: bigint,
|
||||
d: bigint
|
||||
a: bigint;
|
||||
d: bigint;
|
||||
};
|
||||
readonly colour?: string;
|
||||
readonly hoist: boolean;
|
||||
@@ -32,9 +35,10 @@ export class ServerRole {
|
||||
|
||||
this.id = id;
|
||||
this.name = data.name;
|
||||
this.icon = data.icon ? new File(client, data.icon) : undefined;
|
||||
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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+54
-6
@@ -1,13 +1,21 @@
|
||||
import type { User as APIUser, DataEditUser, Presence } from "stoat-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,8 +1,13 @@
|
||||
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";
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
@@ -39,7 +44,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,20 +60,33 @@ export class VoiceParticipant {
|
||||
* @param data Data
|
||||
*/
|
||||
update(data: Partial<UserVoiceState>) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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";
|
||||
|
||||
@@ -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.
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+177
-139
@@ -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<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 }
|
||||
@@ -119,91 +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: "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<UserVoiceState>;
|
||||
}
|
||||
type: "UserVoiceStateUpdate";
|
||||
id: string;
|
||||
channel_id: string;
|
||||
data: Partial<UserVoiceState>;
|
||||
}
|
||||
| {
|
||||
type: "UserMoveVoiceChannel";
|
||||
node: string;
|
||||
token: string;
|
||||
};
|
||||
type: "UserMoveVoiceChannel";
|
||||
node: string;
|
||||
token: string;
|
||||
}
|
||||
| {
|
||||
type: "UserSlowmodes";
|
||||
slowmodes: UserSlowmodes[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Policy change type
|
||||
@@ -235,6 +239,16 @@ type ChannelVoiceState = {
|
||||
participants: UserVoiceState[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Channel slowmodes for the active user
|
||||
*/
|
||||
export type UserSlowmodes = {
|
||||
channel_id: string;
|
||||
duration: number;
|
||||
retry_after: number;
|
||||
receivedAt?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Initial synchronisation packet
|
||||
*/
|
||||
@@ -276,42 +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;
|
||||
}
|
||||
}
|
||||
|
||||
for (const server of event.servers) {
|
||||
client.servers.getOrCreate(server._id, server);
|
||||
}
|
||||
|
||||
for (const member of event.members) {
|
||||
client.serverMembers.getOrCreate(member._id, member);
|
||||
}
|
||||
|
||||
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 (u.relationship === "User") {
|
||||
client.user = u;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const emoji of event.emojis) {
|
||||
client.emojis.getOrCreate(emoji._id, emoji);
|
||||
if (event.servers) {
|
||||
for (const server of event.servers) {
|
||||
client.servers.getOrCreate(server._id, server);
|
||||
}
|
||||
}
|
||||
|
||||
if (event.members) {
|
||||
for (const member of event.members) {
|
||||
client.serverMembers.getOrCreate(member._id, member);
|
||||
}
|
||||
}
|
||||
|
||||
if (event.channels) {
|
||||
for (const channel of event.channels) {
|
||||
client.channels.getOrCreate(channel._id, channel);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -322,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"),
|
||||
);
|
||||
@@ -773,6 +798,9 @@ export async function handleEvent(
|
||||
case "Timeout":
|
||||
changes["timeout"] = undefined;
|
||||
break;
|
||||
case "Pronouns":
|
||||
changes["pronouns"] = undefined;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -844,6 +872,9 @@ export async function handleEvent(
|
||||
text: undefined,
|
||||
};
|
||||
break;
|
||||
case "Pronouns":
|
||||
changes["pronouns"] = undefined;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -973,5 +1004,12 @@ export async function handleEvent(
|
||||
// todo
|
||||
break;
|
||||
}
|
||||
case "UserSlowmodes": {
|
||||
for (const slowmode of event.slowmodes) {
|
||||
client.setSlowmode(slowmode.channel_id, slowmode);
|
||||
}
|
||||
client.emit("userSlowmodes");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,4 +41,4 @@ export const botHydration: Hydrate<APIBot, HydratedBot> = {
|
||||
/**
|
||||
* Flags attributed to users
|
||||
*/
|
||||
export enum BotFlags { }
|
||||
export enum BotFlags {}
|
||||
|
||||
@@ -6,8 +6,6 @@ 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;
|
||||
@@ -29,6 +27,7 @@ export type HydratedChannel = {
|
||||
defaultPermissions?: { a: bigint; d: bigint };
|
||||
rolePermissions?: Record<string, { a: bigint; d: bigint }>;
|
||||
nsfw: boolean;
|
||||
slowmode: number;
|
||||
|
||||
lastMessageId?: string;
|
||||
|
||||
@@ -46,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,
|
||||
@@ -76,10 +76,15 @@ export const channelHydration: Hydrate<Merge<APIChannel>, HydratedChannel> = {
|
||||
),
|
||||
nsfw: (channel) => channel.nsfw || false,
|
||||
lastMessageId: (channel) => channel.last_message_id!,
|
||||
slowmode: (channel) => channel.slowmode ?? 0,
|
||||
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(),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ReactiveMap } from "@solid-primitives/map";
|
||||
import { ReactiveSet } from "@solid-primitives/set";
|
||||
import type { Embed, Interactions, Masquerade, Message } from "stoat-api";
|
||||
import type { Interactions, Masquerade, Message } from "stoat-api";
|
||||
|
||||
import type { Client } from "../Client.js";
|
||||
import { File } from "../classes/File.js";
|
||||
@@ -98,4 +98,4 @@ export enum MessageFlags {
|
||||
* This cannot be true if MentionsEveryone is true
|
||||
*/
|
||||
MentionsOnline = 3,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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!),
|
||||
},
|
||||
|
||||
@@ -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!,
|
||||
},
|
||||
|
||||
+1
-1
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -67,6 +67,8 @@ export const Permission = {
|
||||
Masquerade: 2n ** 28n,
|
||||
/// React to messages with emoji
|
||||
React: 2n ** 29n,
|
||||
/// Bypass slowmode
|
||||
BypassSlowmode: 2n ** 39n,
|
||||
|
||||
// * Voice permissions
|
||||
/// Connect to a voice 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 };
|
||||
|
||||
Reference in New Issue
Block a user