mirror of
https://github.com/stoatchat/javascript-client-sdk.git
synced 2026-07-21 04:25:27 -04:00
merge: pull request #70 from revoltchat/insert/feat/store-rewrite
This commit is contained in:
@@ -1,4 +0,0 @@
|
||||
API_URL=https://api.revolt.chat
|
||||
EMAIL=1@example.com
|
||||
PASSWORD=uNiquePassword!2
|
||||
BOT_TOKEN=abc
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"root": true,
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": ["solid", "spellcheck"],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:solid/typescript",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"prettier"
|
||||
],
|
||||
"rules": {
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"spellcheck/spell-checker": [
|
||||
"warn",
|
||||
{
|
||||
"lang": "en_GB",
|
||||
"strings": false,
|
||||
"identifiers": false,
|
||||
"templates": false,
|
||||
"skipWords": ["uri", "webhook", "unreads"],
|
||||
"minLength": 3
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
"varsIgnorePattern": "^_"
|
||||
}
|
||||
],
|
||||
"no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
"varsIgnorePattern": "^_"
|
||||
}
|
||||
],
|
||||
"require-jsdoc": [
|
||||
"warn",
|
||||
{
|
||||
"require": {
|
||||
"FunctionDeclaration": true,
|
||||
"MethodDefinition": true,
|
||||
"ClassDeclaration": true,
|
||||
"ArrowFunctionExpression": true,
|
||||
"FunctionExpression": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
+22
-17
@@ -2,27 +2,32 @@ name: Publish Documentation
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
docs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Checkout repository and submodules
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: pnpm/action-setup@v2.2.4
|
||||
with:
|
||||
version: 7
|
||||
- name: Use Node.js 18
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: "pnpm"
|
||||
- name: Install packages
|
||||
run: pnpm install
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 16
|
||||
- name: Build
|
||||
run: pnpm run docs
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
|
||||
- name: Build
|
||||
run: yarn run docs
|
||||
|
||||
- name: Deploy
|
||||
uses: JamesIves/github-pages-deploy-action@4.1.5
|
||||
with:
|
||||
branch: docs
|
||||
folder: docs
|
||||
- name: Deploy
|
||||
uses: JamesIves/github-pages-deploy-action@4.1.5
|
||||
with:
|
||||
branch: docs
|
||||
folder: docs
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
name: Lint
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [16.x, 18.x]
|
||||
|
||||
steps:
|
||||
- name: Checkout repository and submodules
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: pnpm/action-setup@v2.2.4
|
||||
with:
|
||||
version: 7
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: "pnpm"
|
||||
- name: Install packages
|
||||
run: pnpm install
|
||||
|
||||
- name: Typecheck
|
||||
run: pnpm typecheck
|
||||
- name: Code Formatting
|
||||
run: pnpm fmt:check
|
||||
- name: Lint
|
||||
run: pnpm lint
|
||||
@@ -1,24 +0,0 @@
|
||||
name: Typecheck
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [12.x, 14.x, 16.x, 18.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'yarn'
|
||||
- run: yarn install
|
||||
- run: yarn run typecheck
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
# build
|
||||
node_modules
|
||||
.env
|
||||
dist
|
||||
docs
|
||||
esm
|
||||
todo
|
||||
/lib
|
||||
|
||||
# yarn
|
||||
.pnp.*
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"plugins": ["@trivago/prettier-plugin-sort-imports"],
|
||||
"importOrder": [
|
||||
"^solid",
|
||||
"<THIRD_PARTY_MODULES>",
|
||||
"^\\.\\.",
|
||||
"^[./]"
|
||||
],
|
||||
"importOrderSeparation": true,
|
||||
"importOrderSortSpecifiers": true
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
module.exports = {
|
||||
tabWidth: 4,
|
||||
trailingComma: "all",
|
||||
jsxBracketSameLine: true,
|
||||
};
|
||||
Vendored
-783
File diff suppressed because one or more lines are too long
@@ -1,2 +0,0 @@
|
||||
yarnPath: .yarn/releases/yarn-3.2.3.cjs
|
||||
nodeLinker: node-modules
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Paul Makles
|
||||
Copyright (c) 2021-2023 Paul Makles
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,95 +1,61 @@
|
||||
# revolt.js
|
||||
|
||||
> **Warning**
|
||||
> revolt.js is currently being rewritten, it's pretty much ready for use for most applications but is still not entirely feature complete.
|
||||
>
|
||||
> You can find the [version 6 README here](https://github.com/revoltchat/revolt.js/tree/v6).
|
||||
|
||||
 
|
||||
|
||||
**revolt.js** is a direct implementation of the entire Revolt API and provides a way to authenticate and start communicating with Revolt servers.
|
||||
**revolt.js** is a JavaScript library for interacting with the entire Revolt API.
|
||||
|
||||
## Example Usage (Javascript / ES6)
|
||||
## Example Usage
|
||||
|
||||
```javascript
|
||||
// esm / typescript
|
||||
import { Client } from "revolt.js";
|
||||
|
||||
let client = new Client();
|
||||
|
||||
client.on("ready", async () =>
|
||||
console.info(`Logged in as ${client.user.username}!`),
|
||||
);
|
||||
|
||||
client.on("message", async (message) => {
|
||||
if (message.content === "hello") {
|
||||
message.channel.sendMessage("world");
|
||||
}
|
||||
});
|
||||
|
||||
client.loginBot("..");
|
||||
```
|
||||
|
||||
If you are using Node, you must specify `--experimental-specifier-resolution=node`.
|
||||
|
||||
For example, `node --experimental-specifier-resolution=node index.js`.
|
||||
|
||||
## Example Usage (CommonJS)
|
||||
|
||||
```javascript
|
||||
// ...or commonjs
|
||||
const { Client } = require("revolt.js");
|
||||
|
||||
let client = new Client();
|
||||
|
||||
client.on("ready", async () =>
|
||||
console.info(`Logged in as ${client.user.username}!`),
|
||||
console.info(`Logged in as ${client.user.username}!`)
|
||||
);
|
||||
|
||||
client.on("message", async (message) => {
|
||||
if (message.content === "hello") {
|
||||
message.channel.sendMessage("world");
|
||||
}
|
||||
client.on("messageCreate", async (message) => {
|
||||
if (message.content === "hello") {
|
||||
message.channel.sendMessage("world");
|
||||
}
|
||||
});
|
||||
|
||||
client.loginBot("..");
|
||||
```
|
||||
|
||||
## Example Usage (Typescript)
|
||||
## Reactivity with Signals & Solid.js Primitives
|
||||
|
||||
```typescript
|
||||
import { Client } from "revolt.js";
|
||||
All objects have reactivity built-in and can be dropped straight into any Solid.js project.
|
||||
|
||||
let client = new Client();
|
||||
```tsx
|
||||
const client = new Client();
|
||||
// initialise the client
|
||||
|
||||
client.on("ready", async () =>
|
||||
console.info(`Logged in as ${client.user!.username}!`),
|
||||
);
|
||||
|
||||
client.on("message", async (message) => {
|
||||
if (message.content === "hello") {
|
||||
message.channel!.sendMessage("world");
|
||||
}
|
||||
});
|
||||
|
||||
client.loginBot("..");
|
||||
```
|
||||
|
||||
## MobX
|
||||
|
||||
MobX is used behind the scenes so you can subscribe to any change as you normally would, e.g. with `mobx-react(-lite)` or mobx's utility functions.
|
||||
|
||||
```typescript
|
||||
import { autorun } from 'mobx';
|
||||
|
||||
[..]
|
||||
|
||||
client.once('ready', () => {
|
||||
autorun(() => {
|
||||
console.log(`Current username is ${client.user!.username}!`);
|
||||
});
|
||||
});
|
||||
function MyApp() {
|
||||
return (
|
||||
<h1>Your username is: {client.user?.username ?? "[logging in...]"}</h1>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Revolt API Types
|
||||
|
||||
All `revolt-api` types are re-exported from this library under `API`.
|
||||
|
||||
> **Warning**
|
||||
> It is advised you do not use this unless necessary, if you find somewhere that isn't covered by the library, please open an issue as this library aims to transform all objects.
|
||||
|
||||
```typescript
|
||||
import { API } from 'revolt.js';
|
||||
import { API } from "revolt.js";
|
||||
|
||||
// API.Channel;
|
||||
// API.[..];
|
||||
|
||||
+53
-92
@@ -1,94 +1,55 @@
|
||||
{
|
||||
"name": "revolt.js",
|
||||
"version": "6.0.20",
|
||||
"main": "dist/index.js",
|
||||
"typings": "dist/index.d.ts",
|
||||
"module": "esm/index.js",
|
||||
"repository": "https://github.com/revoltchat/revolt.js",
|
||||
"author": "Paul Makles <insrt.uk>",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@insertish/exponential-backoff": "3.1.0-patch.2",
|
||||
"@insertish/isomorphic-ws": "^4.0.1",
|
||||
"axios": "^0.21.4",
|
||||
"eventemitter3": "^4.0.7",
|
||||
"lodash.defaultsdeep": "^4.6.1",
|
||||
"lodash.flatten": "^4.4.0",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"long": "^5.2.0",
|
||||
"mobx": "^6.3.2",
|
||||
"revolt-api": "0.5.17",
|
||||
"ulid": "^2.3.0",
|
||||
"ws": "^8.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/events": "^3.0.0",
|
||||
"@types/lodash": "^4.14.168",
|
||||
"@types/lodash.defaultsdeep": "^4.6.6",
|
||||
"@types/lodash.flatten": "^4.4.6",
|
||||
"@types/lodash.isequal": "^4.5.5",
|
||||
"@types/node": "^14.14.31",
|
||||
"@types/ws": "^7.2.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.3.1",
|
||||
"@typescript-eslint/parser": "^5.3.1",
|
||||
"dotenv": "^8.2.0",
|
||||
"eslint": "^8.2.0",
|
||||
"in-publish": "^2.0.1",
|
||||
"jsdoc-babel": "^0.5.0",
|
||||
"node-fetch": "^2.6.1",
|
||||
"prettier": "^2.4.1",
|
||||
"rimraf": "^3.0.2",
|
||||
"tsc-watch": "^4.1.0",
|
||||
"typedoc": "^0.21.4",
|
||||
"typescript": "^4.4.4"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": [
|
||||
"mobx"
|
||||
],
|
||||
"extends": [
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:mobx/recommended"
|
||||
],
|
||||
"ignorePatterns": [
|
||||
"build/"
|
||||
],
|
||||
"rules": {
|
||||
"radix": "off",
|
||||
"no-spaced-func": "off",
|
||||
"react/no-danger": "off",
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
"varsIgnorePattern": "^_"
|
||||
}
|
||||
],
|
||||
"no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
"varsIgnorePattern": "^_"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "tsc-watch --onSuccess \"node --experimental-specifier-resolution=node dist/tester\"",
|
||||
"prepublish": "in-publish && tsc || echo Skipping build.",
|
||||
"build": "rimraf dist esm && tsc && tsc -p tsconfig.cjs.json",
|
||||
"build:watch": "tsc-watch",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"docs": "typedoc --readme README.md src/",
|
||||
"lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'",
|
||||
"fmt": "prettier --write 'src/**/*.{js,jsx,ts,tsx}'"
|
||||
},
|
||||
"files": [
|
||||
"README.md",
|
||||
"dist",
|
||||
"esm"
|
||||
],
|
||||
"description": "Library for interacting with the Revolt API.",
|
||||
"packageManager": "yarn@3.2.3"
|
||||
"name": "revolt.js",
|
||||
"version": "7.0.0-beta.1",
|
||||
"main": "lib/cjs/index.js",
|
||||
"module": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"repository": "https://github.com/revoltchat/revolt.js",
|
||||
"author": "Paul Makles <insrt.uk>",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"build:watch": "tsc-watch --onSuccess \"node .\"",
|
||||
"lint": "eslint --ext .ts,.tsx src/",
|
||||
"lint:fix": "eslint --fix --ext .ts,.tsx src/",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"docs": "typedoc --plugin @mxssfd/typedoc-theme --theme my-theme --readme README.md src/",
|
||||
"fmt": "prettier --write 'src/**/*.{js,jsx,ts,tsx}'",
|
||||
"fmt:check": "prettier --check 'src/**/*.{js,jsx,ts,tsx}'"
|
||||
},
|
||||
"files": [
|
||||
"README.md",
|
||||
"lib"
|
||||
],
|
||||
"description": "Library for interacting with the Revolt API.",
|
||||
"packageManager": "pnpm@7.14.2",
|
||||
"dependencies": {
|
||||
"@mxssfd/typedoc-theme": "^1.1.1",
|
||||
"@solid-primitives/map": "^0.4.3",
|
||||
"@solid-primitives/set": "^0.4.3",
|
||||
"eventemitter3": "^5.0.0",
|
||||
"isomorphic-ws": "^5.0.0",
|
||||
"long": "^5.2.1",
|
||||
"revolt-api": "^0.5.17",
|
||||
"solid-js": "^1.7.2",
|
||||
"typedoc": "^0.24.1",
|
||||
"ulid": "^2.3.0",
|
||||
"ws": "^8.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@trivago/prettier-plugin-sort-imports": "^4.1.1",
|
||||
"@types/node": "^18.15.11",
|
||||
"@types/ws": "^8.5.4",
|
||||
"@typescript-eslint/eslint-plugin": "^5.57.1",
|
||||
"@typescript-eslint/parser": "^5.57.1",
|
||||
"dotenv": "^16.0.3",
|
||||
"eslint": "^8.37.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-solid": "^0.12.0",
|
||||
"eslint-plugin-spellcheck": "^0.0.20",
|
||||
"prettier": "^2.8.7",
|
||||
"tsc-watch": "^6.0.0",
|
||||
"typed-emitter": "^2.1.0",
|
||||
"typescript": "^5.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+2056
File diff suppressed because it is too large
Load Diff
+337
-507
@@ -1,544 +1,374 @@
|
||||
import { Accessor, Setter, createSignal } from "solid-js";
|
||||
|
||||
import EventEmitter from "eventemitter3";
|
||||
import defaultsDeep from "lodash.defaultsdeep";
|
||||
import { action, makeObservable, observable } from "mobx";
|
||||
import type {
|
||||
DataCreateAccount,
|
||||
DataLogin,
|
||||
DataOnboard,
|
||||
InviteResponse,
|
||||
} from "revolt-api";
|
||||
import type { RevoltConfig, Metadata } from "revolt-api";
|
||||
import { API, MemberCompositeKey, Role } from "revolt-api";
|
||||
import { API, DataCreateAccount, Role } from "revolt-api";
|
||||
import type { DataLogin, RevoltConfig } from "revolt-api";
|
||||
|
||||
import Bots from "./maps/Bots";
|
||||
import Channels, { Channel } from "./maps/Channels";
|
||||
import Members, { Member } from "./maps/Members";
|
||||
import Messages, { Message } from "./maps/Messages";
|
||||
import Servers, { Server } from "./maps/Servers";
|
||||
import Users, { User } from "./maps/Users";
|
||||
import Emojis, { Emoji } from "./maps/Emojis";
|
||||
import { Channel, Emoji, Message, Server, ServerMember, User } from "./classes";
|
||||
import {
|
||||
BotCollection,
|
||||
ChannelCollection,
|
||||
ChannelUnreadCollection,
|
||||
EmojiCollection,
|
||||
MessageCollection,
|
||||
ServerCollection,
|
||||
ServerMemberCollection,
|
||||
UserCollection,
|
||||
} from "./collections";
|
||||
import {
|
||||
ConnectionState,
|
||||
EventClient,
|
||||
EventClientOptions,
|
||||
handleEventV1,
|
||||
} from "./events";
|
||||
import {
|
||||
HydratedChannel,
|
||||
HydratedEmoji,
|
||||
HydratedMessage,
|
||||
HydratedServer,
|
||||
HydratedServerMember,
|
||||
HydratedUser,
|
||||
} from "./hydration";
|
||||
import { RE_CHANNELS, RE_MENTIONS, RE_SPOILER } from "./lib/regex";
|
||||
|
||||
import { WebSocketClient } from "./websocket/client";
|
||||
import { ClientboundNotification } from "./websocket/notifications";
|
||||
export type Session = { _id: string; token: string; user_id: string } | string;
|
||||
|
||||
import { defaultConfig } from "./config";
|
||||
import { Nullable } from "./util/null";
|
||||
import Unreads from "./util/Unreads";
|
||||
/**
|
||||
* Events provided by the client
|
||||
*/
|
||||
type Events = {
|
||||
error(error: Error): void;
|
||||
|
||||
connected(): void;
|
||||
connecting(): void;
|
||||
disconnected(): void;
|
||||
ready(): void;
|
||||
logout(): void;
|
||||
|
||||
messageCreate(message: Message): void;
|
||||
messageUpdate(message: Message, previousMessage: HydratedMessage): void;
|
||||
messageDelete(message: HydratedMessage): void;
|
||||
messageDeleteBulk(messages: HydratedMessage[], channel?: Channel): void;
|
||||
messageReactionAdd(message: Message, userId: string, emoji: string): void;
|
||||
messageReactionRemove(message: Message, userId: string, emoji: string): void;
|
||||
messageReactionRemoveEmoji(message: Message, emoji: string): void;
|
||||
|
||||
channelCreate(channel: Channel): void;
|
||||
channelUpdate(channel: Channel, previousChannel: HydratedChannel): void;
|
||||
channelDelete(channel: HydratedChannel): void;
|
||||
channelGroupJoin(channel: Channel, user: User): void;
|
||||
channelGroupLeave(channel: Channel, user?: User): void;
|
||||
channelStartTyping(channel: Channel, user?: User): void;
|
||||
channelStopTyping(channel: Channel, user?: User): void;
|
||||
channelAcknowledged(channel: Channel, messageId: string): void;
|
||||
|
||||
serverCreate(server: Server): void;
|
||||
serverUpdate(server: Server, previousServer: HydratedServer): void;
|
||||
serverDelete(server: HydratedServer): void;
|
||||
serverRoleUpdate(server: Server, roleId: string, previousRole: Role): void;
|
||||
serverRoleDelete(server: Server, roleId: string, role: Role): void;
|
||||
|
||||
serverMemberUpdate(
|
||||
member: ServerMember,
|
||||
previousMember: HydratedServerMember
|
||||
): void;
|
||||
serverMemberJoin(member: ServerMember): void;
|
||||
serverMemberLeave(member: HydratedServerMember): void;
|
||||
|
||||
userUpdate(user: User, previousUser: HydratedUser): void;
|
||||
// ^ userRelationshipChanged(user: User, previousRelationship: RelationshipStatus): void;
|
||||
// ^ userPresenceChanged(user: User, previousPresence: boolean): void;
|
||||
userSettingsUpdate(
|
||||
id: string,
|
||||
update: Record<string, [number, string]>
|
||||
): void;
|
||||
|
||||
emojiCreate(emoji: Emoji): void;
|
||||
emojiDelete(emoji: HydratedEmoji): void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Client options object
|
||||
*/
|
||||
export interface ClientOptions {
|
||||
apiURL: string;
|
||||
debug: boolean;
|
||||
export type ClientOptions = Partial<EventClientOptions> & {
|
||||
/**
|
||||
* Base URL of the API server
|
||||
*/
|
||||
baseURL: string;
|
||||
|
||||
cache: boolean;
|
||||
unreads: boolean;
|
||||
/**
|
||||
* Whether to allow partial objects to emit from events
|
||||
* @default false
|
||||
*/
|
||||
partials: boolean;
|
||||
|
||||
heartbeat: number;
|
||||
autoReconnect: boolean;
|
||||
/**
|
||||
* Whether to eagerly fetch users and members for incoming events
|
||||
* @default true
|
||||
* @deprecated
|
||||
*/
|
||||
eagerFetching: boolean;
|
||||
|
||||
/**
|
||||
* Automatically reconnect the client if no
|
||||
* `pong` is received after X seconds of sending a `ping`.
|
||||
* This is a temporary fix for an issue where the client
|
||||
* would randomly stop receiving websocket messages.
|
||||
*/
|
||||
pongTimeout?: number;
|
||||
/**
|
||||
* Whether to automatically sync unreads information
|
||||
* @default false
|
||||
*/
|
||||
syncUnreads: boolean;
|
||||
|
||||
/**
|
||||
* If `pongTimeout` is set, this decides what to do when
|
||||
* the timeout is triggered. Default is `RECONNECT`.
|
||||
*/
|
||||
onPongTimeout?: "EXIT" | "RECONNECT";
|
||||
/**
|
||||
* Whether to reconnect when disconnected
|
||||
* @default true
|
||||
*/
|
||||
autoReconnect: boolean;
|
||||
|
||||
ackRateLimiter: boolean;
|
||||
}
|
||||
/**
|
||||
* Retry delay function
|
||||
* @param retryCount Count
|
||||
* @returns Delay in seconds
|
||||
* @default (2^x-1) ±20%
|
||||
*/
|
||||
retryDelayFunction(retryCount: number): number;
|
||||
|
||||
export declare interface Client {
|
||||
on(event: "connected", listener: () => void): this;
|
||||
on(event: "connecting", listener: () => void): this;
|
||||
on(event: "dropped", listener: () => void): this;
|
||||
on(event: "ready", listener: () => void): this;
|
||||
on(event: "logout", listener: () => void): this;
|
||||
on(
|
||||
event: "packet",
|
||||
listener: (packet: ClientboundNotification) => void,
|
||||
): this;
|
||||
|
||||
on(event: "message", listener: (message: Message) => void): this;
|
||||
on(event: "message/update", listener: (message: Message) => void): this;
|
||||
on(
|
||||
event: "message/delete",
|
||||
listener: (id: string, message?: Message) => void,
|
||||
): this;
|
||||
|
||||
// General purpose event
|
||||
on(
|
||||
event: "message/updated",
|
||||
listener: (message: Message, packet: ClientboundNotification) => void,
|
||||
): this;
|
||||
|
||||
on(event: "channel/create", listener: (channel: Channel) => void): this;
|
||||
on(event: "channel/update", listener: (channel: Channel) => void): this;
|
||||
on(
|
||||
event: "channel/delete",
|
||||
listener: (id: string, channel?: Channel) => void,
|
||||
): this;
|
||||
|
||||
on(event: "server/update", listener: (server: Server) => void): this;
|
||||
on(
|
||||
event: "server/delete",
|
||||
listener: (id: string, server?: Server) => void,
|
||||
): this;
|
||||
|
||||
on(
|
||||
event: "role/update",
|
||||
listener: (roleId: string, role: Role, serverId: string) => void,
|
||||
): this;
|
||||
on(
|
||||
event: "role/delete",
|
||||
listener: (id: string, serverId: string) => void,
|
||||
): this;
|
||||
|
||||
on(event: "member/join", listener: (member: Member) => void): this;
|
||||
on(event: "member/update", listener: (member: Member) => void): this;
|
||||
on(event: "member/leave", listener: (id: MemberCompositeKey) => void): this;
|
||||
|
||||
on(event: "user/relationship", listener: (user: User) => void): this;
|
||||
|
||||
on(event: "emoji/create", listener: (emoji: Emoji) => void): this;
|
||||
on(
|
||||
event: "emoji/delete",
|
||||
listener: (id: string, emoji?: Emoji) => void,
|
||||
): this;
|
||||
}
|
||||
/**
|
||||
* Check whether a channel is muted
|
||||
* @param channel Channel
|
||||
* @return Whether it is muted
|
||||
* @default false
|
||||
*/
|
||||
channelIsMuted(channel: Channel): boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Regular expression for mentions.
|
||||
* Revolt.js Clients
|
||||
*/
|
||||
export const RE_MENTIONS = /<@([A-z0-9]{26})>/g;
|
||||
export class Client extends EventEmitter<Events> {
|
||||
readonly bots;
|
||||
readonly channels;
|
||||
readonly channelUnreads;
|
||||
readonly emojis;
|
||||
readonly messages;
|
||||
readonly servers;
|
||||
readonly serverMembers;
|
||||
readonly users;
|
||||
|
||||
/**
|
||||
* Regular expression for spoilers.
|
||||
*/
|
||||
export const RE_SPOILER = /!!.+!!/g;
|
||||
readonly api: API;
|
||||
readonly options: ClientOptions;
|
||||
readonly events: EventClient<1>;
|
||||
|
||||
export type FileArgs = [
|
||||
options?: {
|
||||
max_side?: number;
|
||||
size?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
},
|
||||
allowAnimation?: boolean,
|
||||
fallback?: string,
|
||||
];
|
||||
configuration: RevoltConfig | undefined;
|
||||
#session: Session | undefined;
|
||||
user: User | undefined;
|
||||
|
||||
export type Session = { token: string };
|
||||
readonly ready: Accessor<boolean>;
|
||||
#setReady: Setter<boolean>;
|
||||
|
||||
export class Client extends EventEmitter {
|
||||
heartbeat: number;
|
||||
readonly connectionFailureCount: Accessor<number>;
|
||||
#setConnectionFailureCount: Setter<number>;
|
||||
#reconnectTimeout: number | undefined;
|
||||
|
||||
api: API;
|
||||
session?: Session | string;
|
||||
user: Nullable<User> = null;
|
||||
/**
|
||||
* Create Revolt.js Client
|
||||
*/
|
||||
constructor(options?: Partial<ClientOptions>) {
|
||||
super();
|
||||
|
||||
options: ClientOptions;
|
||||
websocket: WebSocketClient;
|
||||
configuration?: RevoltConfig;
|
||||
this.options = {
|
||||
baseURL: "https://api.revolt.chat",
|
||||
partials: false,
|
||||
eagerFetching: true,
|
||||
syncUnreads: false,
|
||||
autoReconnect: true,
|
||||
/**
|
||||
* Retry delay function
|
||||
* @param retryCount Count
|
||||
* @returns Delay in seconds
|
||||
*/
|
||||
retryDelayFunction(retryCount) {
|
||||
return (Math.pow(2, retryCount) - 1) * (0.8 + Math.random() * 0.4);
|
||||
},
|
||||
/**
|
||||
* Check whether a channel is muted
|
||||
* @param channel Channel
|
||||
* @return Whether it is muted
|
||||
*/
|
||||
channelIsMuted() {
|
||||
return false;
|
||||
},
|
||||
...options,
|
||||
};
|
||||
|
||||
users: Users;
|
||||
channels: Channels;
|
||||
servers: Servers;
|
||||
members: Members;
|
||||
messages: Messages;
|
||||
bots: Bots;
|
||||
emojis: Emojis;
|
||||
this.api = new API({
|
||||
baseURL: this.options.baseURL,
|
||||
});
|
||||
|
||||
unreads?: Unreads;
|
||||
const [ready, setReady] = createSignal(false);
|
||||
this.ready = ready;
|
||||
this.#setReady = setReady;
|
||||
|
||||
constructor(options: Partial<ClientOptions> = {}) {
|
||||
super();
|
||||
const [connectionFailureCount, setConnectionFailureCount] = createSignal(0);
|
||||
this.connectionFailureCount = connectionFailureCount;
|
||||
this.#setConnectionFailureCount = setConnectionFailureCount;
|
||||
|
||||
this.users = new Users(this);
|
||||
this.channels = new Channels(this);
|
||||
this.servers = new Servers(this);
|
||||
this.members = new Members(this);
|
||||
this.messages = new Messages(this);
|
||||
this.bots = new Bots(this);
|
||||
this.emojis = new Emojis(this);
|
||||
this.bots = new BotCollection(this);
|
||||
this.channels = new ChannelCollection(this);
|
||||
this.channelUnreads = new ChannelUnreadCollection(this);
|
||||
this.emojis = new EmojiCollection(this);
|
||||
this.messages = new MessageCollection(this);
|
||||
this.users = new UserCollection(this);
|
||||
this.servers = new ServerCollection(this);
|
||||
this.serverMembers = new ServerMemberCollection(this);
|
||||
|
||||
makeObservable(
|
||||
this,
|
||||
{
|
||||
user: observable,
|
||||
users: observable,
|
||||
channels: observable,
|
||||
servers: observable,
|
||||
members: observable,
|
||||
messages: observable,
|
||||
emojis: observable,
|
||||
reset: action,
|
||||
},
|
||||
{
|
||||
proxy: false,
|
||||
},
|
||||
);
|
||||
this.events = new EventClient(1, "json", this.options);
|
||||
this.events.on("error", (error) => this.emit("error", error));
|
||||
this.events.on("state", (state) => {
|
||||
switch (state) {
|
||||
case ConnectionState.Connected:
|
||||
this.#setConnectionFailureCount(0);
|
||||
this.emit("connected");
|
||||
break;
|
||||
case ConnectionState.Connecting:
|
||||
this.emit("connecting");
|
||||
break;
|
||||
case ConnectionState.Disconnected:
|
||||
this.emit("disconnected");
|
||||
if (this.options.autoReconnect) {
|
||||
this.#reconnectTimeout = setTimeout(
|
||||
() => this.connect(),
|
||||
this.options.retryDelayFunction(this.connectionFailureCount()) *
|
||||
1e3
|
||||
) as never;
|
||||
|
||||
this.options = defaultsDeep(options, defaultConfig);
|
||||
if (this.options.cache) throw "Cache is not supported yet.";
|
||||
this.#setConnectionFailureCount((count) => count + 1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
if (this.options.unreads) {
|
||||
this.unreads = new Unreads(this);
|
||||
this.events.on("event", (event) =>
|
||||
handleEventV1(this, event, this.#setReady)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to Revolt
|
||||
*/
|
||||
connect() {
|
||||
clearTimeout(this.#reconnectTimeout);
|
||||
this.events.disconnect();
|
||||
this.#setReady(false);
|
||||
this.events.connect(
|
||||
"wss://ws.revolt.chat",
|
||||
typeof this.#session === "string" ? this.#session : this.#session!.token
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the configuration of the server if it has not been already fetched.
|
||||
*/
|
||||
async #fetchConfiguration() {
|
||||
if (!this.configuration) {
|
||||
this.configuration = await this.api.get("/");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update API object to use authentication.
|
||||
*/
|
||||
#updateHeaders() {
|
||||
(this.api as API) = new API({
|
||||
baseURL: this.options.baseURL,
|
||||
authentication: {
|
||||
revolt: this.#session,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Log in with auth data, creating a new session in the process.
|
||||
* @param details Login data object
|
||||
* @returns An on-boarding function if on-boarding is required, undefined otherwise
|
||||
*/
|
||||
async login(details: DataLogin) {
|
||||
await this.#fetchConfiguration();
|
||||
const data = await this.api.post("/auth/session/login", details);
|
||||
if (data.result === "Success") {
|
||||
this.#session = data;
|
||||
// TODO: return await this.connect();
|
||||
} else {
|
||||
throw "MFA not implemented!";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Use an existing session to log into Revolt
|
||||
* @param session Session data object
|
||||
* @returns An on-boarding function if on-boarding is required, undefined otherwise
|
||||
*/
|
||||
async useExistingSession(session: Session) {
|
||||
await this.#fetchConfiguration();
|
||||
this.#session = session;
|
||||
this.#updateHeaders();
|
||||
this.connect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Log in as a bot
|
||||
* @param token Bot token
|
||||
*/
|
||||
async loginBot(token: string) {
|
||||
await this.#fetchConfiguration();
|
||||
this.#session = token;
|
||||
this.#updateHeaders();
|
||||
this.connect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register for a new account
|
||||
* @param data Registration data object
|
||||
* @returns A promise containing a registration response object
|
||||
*/
|
||||
register(data: DataCreateAccount) {
|
||||
return this.api.post("/auth/account/create", data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare a markdown-based message to be displayed to the user as plain text.
|
||||
* @param source Source markdown text
|
||||
* @returns Modified plain text
|
||||
*/
|
||||
markdownToText(source: string) {
|
||||
return source
|
||||
.replace(RE_MENTIONS, (sub: string, id: string) => {
|
||||
const user = this.users.get(id as string);
|
||||
|
||||
if (user) {
|
||||
return `@${user.username}`;
|
||||
}
|
||||
|
||||
this.api = new API({ baseURL: this.apiURL });
|
||||
this.websocket = new WebSocketClient(this);
|
||||
this.heartbeat = this.options.heartbeat;
|
||||
return sub;
|
||||
})
|
||||
.replace(RE_CHANNELS, (sub: string, id: string) => {
|
||||
const channel = this.channels.get(id as string);
|
||||
|
||||
this.proxyFile = this.proxyFile.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* ? Configuration.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get the current API base URL.
|
||||
*/
|
||||
get apiURL() {
|
||||
return this.options.apiURL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether debug mode is turned on.
|
||||
*/
|
||||
get debug() {
|
||||
return this.options.debug;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether revolt.js should auto-reconnect
|
||||
*/
|
||||
get autoReconnect() {
|
||||
return this.options.autoReconnect;
|
||||
}
|
||||
|
||||
/**
|
||||
* ? Authentication and connection.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fetches the configuration of the server.
|
||||
*
|
||||
* @remarks
|
||||
* Unlike `fetchConfiguration`, this function also fetches the
|
||||
* configuration if it has already been fetched before.
|
||||
*/
|
||||
async connect() {
|
||||
this.configuration = await this.api.get("/");
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the configuration of the server if it has not been already fetched.
|
||||
*/
|
||||
async fetchConfiguration() {
|
||||
if (!this.configuration) await this.connect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update API object to use authentication.
|
||||
*/
|
||||
private $updateHeaders() {
|
||||
this.api = new API({
|
||||
baseURL: this.apiURL,
|
||||
authentication: {
|
||||
revolt: this.session,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Log in with auth data, creating a new session in the process.
|
||||
* @param details Login data object
|
||||
* @returns An onboarding function if onboarding is required, undefined otherwise
|
||||
*/
|
||||
async login(details: DataLogin) {
|
||||
await this.fetchConfiguration();
|
||||
const data = await this.api.post("/auth/session/login", details);
|
||||
if (data.result === "Success") {
|
||||
this.session = data;
|
||||
return await this.$connect();
|
||||
} else {
|
||||
throw "MFA not implemented!";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Use an existing session to log into Revolt.
|
||||
* @param session Session data object
|
||||
* @returns An onboarding function if onboarding is required, undefined otherwise
|
||||
*/
|
||||
async useExistingSession(session: Session) {
|
||||
await this.fetchConfiguration();
|
||||
this.session = session;
|
||||
return await this.$connect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Log in as a bot.
|
||||
* @param token Bot token
|
||||
*/
|
||||
async loginBot(token: string) {
|
||||
await this.fetchConfiguration();
|
||||
this.session = token;
|
||||
this.$updateHeaders();
|
||||
return await this.websocket.connect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check onboarding status and connect to notifications service.
|
||||
* @returns
|
||||
*/
|
||||
private async $connect() {
|
||||
this.$updateHeaders();
|
||||
const { onboarding } = await this.api.get("/onboard/hello");
|
||||
if (onboarding) {
|
||||
return (username: string, loginAfterSuccess?: boolean) =>
|
||||
this.completeOnboarding({ username }, loginAfterSuccess);
|
||||
if (channel) {
|
||||
return `#${channel.displayName}`;
|
||||
}
|
||||
|
||||
await this.websocket.connect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Finish onboarding for a user, for example by providing a username.
|
||||
* @param data Onboarding data object
|
||||
* @param loginAfterSuccess Defines whether to automatically log in and connect after onboarding finishes
|
||||
*/
|
||||
async completeOnboarding(data: DataOnboard, loginAfterSuccess?: boolean) {
|
||||
await this.api.post("/onboard/complete", data);
|
||||
if (loginAfterSuccess) {
|
||||
await this.websocket.connect();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ? Miscellaneous API routes.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fetch information about a given invite code.
|
||||
* @param code The invite code.
|
||||
* @returns Invite information.
|
||||
*/
|
||||
async fetchInvite(code: string) {
|
||||
return await this.api.get(`/invites/${code as ""}`);
|
||||
}
|
||||
|
||||
async joinInvite(code: string): Promise<Server>;
|
||||
async joinInvite(invite: InviteResponse): Promise<Server>;
|
||||
|
||||
/**
|
||||
* Use an invite.
|
||||
* @param invite Invite
|
||||
* @returns Data provided by invite.
|
||||
*/
|
||||
async joinInvite(invite: string | InviteResponse) {
|
||||
const code = typeof invite === "string" ? invite : invite.code;
|
||||
|
||||
if (typeof invite === "object") {
|
||||
switch (invite.type) {
|
||||
case "Group": {
|
||||
const group = this.channels.get(invite.channel_id);
|
||||
if (group) return group;
|
||||
break;
|
||||
}
|
||||
case "Server": {
|
||||
const server = this.servers.get(invite.server_id);
|
||||
if (server) return server;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const response = await this.api.post(`/invites/${code}`);
|
||||
if (response.type === "Server") {
|
||||
return await this.servers.fetch(
|
||||
response.server._id,
|
||||
response.server,
|
||||
response.channels,
|
||||
);
|
||||
} else {
|
||||
throw "Unsupported invite type.";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an invite.
|
||||
* @param code The invite code.
|
||||
*/
|
||||
async deleteInvite(code: string) {
|
||||
await this.api.delete(`/invites/${code as ""}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch user settings for current user.
|
||||
* @param keys Settings keys to fetch, leave blank to fetch full object.
|
||||
* @returns Key-value object of settings.
|
||||
*/
|
||||
async syncFetchSettings(keys: string[]) {
|
||||
return await this.api.post("/sync/settings/fetch", { keys });
|
||||
}
|
||||
|
||||
/**
|
||||
* Set user settings for current user.
|
||||
* @param data Data to set as an object. Any non-string values will be automatically serialised.
|
||||
* @param timestamp Timestamp to use for the current revision.
|
||||
*/
|
||||
async syncSetSettings(
|
||||
data: { [key: string]: object | string },
|
||||
timestamp?: number,
|
||||
) {
|
||||
const requestData: { [key: string]: string } = {};
|
||||
for (const key of Object.keys(data)) {
|
||||
const value = data[key];
|
||||
requestData[key] =
|
||||
typeof value === "string" ? value : JSON.stringify(value);
|
||||
}
|
||||
|
||||
await this.api.post(`/sync/settings/set`, {
|
||||
...requestData,
|
||||
timestamp,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch user unreads for current user.
|
||||
* @returns Array of channel unreads.
|
||||
*/
|
||||
async syncFetchUnreads() {
|
||||
return await this.api.get("/sync/unreads");
|
||||
}
|
||||
|
||||
/**
|
||||
* ? Utility functions.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Log out of Revolt. Disconnect the WebSocket, request a session invalidation and reset the client.
|
||||
*/
|
||||
async logout(avoidRequest?: boolean) {
|
||||
this.user = null;
|
||||
this.emit("logout");
|
||||
!avoidRequest && (await this.api.post("/auth/session/logout"));
|
||||
this.reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the client by setting properties to their original value or deleting them entirely.
|
||||
* Disconnects the current WebSocket.
|
||||
*/
|
||||
reset() {
|
||||
this.user = null;
|
||||
this.websocket.disconnect();
|
||||
delete this.session;
|
||||
|
||||
this.users = new Users(this);
|
||||
this.channels = new Channels(this);
|
||||
this.servers = new Servers(this);
|
||||
this.members = new Members(this);
|
||||
this.messages = new Messages(this);
|
||||
this.emojis = new Emojis(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register for a new account.
|
||||
* @param data Registration data object
|
||||
* @returns A promise containing a registration response object
|
||||
*/
|
||||
register(data: DataCreateAccount) {
|
||||
return this.api.post("/auth/account/create", data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare a markdown-based message to be displayed to the user as plain text.
|
||||
* @param source Source markdown text
|
||||
* @returns Modified plain text
|
||||
*/
|
||||
markdownToText(source: string) {
|
||||
return source
|
||||
.replace(RE_MENTIONS, (sub: string, id: string) => {
|
||||
const user = this.users.get(id as string);
|
||||
|
||||
if (user) {
|
||||
return `@${user.username}`;
|
||||
}
|
||||
|
||||
return sub;
|
||||
})
|
||||
.replace(RE_SPOILER, "<spoiler>");
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxy a file through January.
|
||||
* @param url URL to proxy
|
||||
* @returns Proxied media URL
|
||||
*/
|
||||
proxyFile(url: string): string | undefined {
|
||||
if (this.configuration?.features.january.enabled) {
|
||||
return `${
|
||||
this.configuration.features.january.url
|
||||
}/proxy?url=${encodeURIComponent(url)}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a URL to a given file with given options.
|
||||
* @param attachment Partial of attachment object
|
||||
* @param options Optional query parameters to modify object
|
||||
* @param allowAnimation Returns GIF if applicable, no operations occur on image
|
||||
* @param fallback Fallback URL
|
||||
* @returns Generated URL or nothing
|
||||
*/
|
||||
generateFileURL(
|
||||
attachment?: {
|
||||
tag: string;
|
||||
_id: string;
|
||||
content_type?: string;
|
||||
metadata?: Metadata;
|
||||
},
|
||||
...args: FileArgs
|
||||
) {
|
||||
const [options, allowAnimation, fallback] = args;
|
||||
|
||||
const autumn = this.configuration?.features.autumn;
|
||||
if (!autumn?.enabled) return fallback;
|
||||
if (!attachment) return fallback;
|
||||
|
||||
const { tag, _id, content_type, metadata } = attachment;
|
||||
|
||||
// ! FIXME: These limits should be done on Autumn.
|
||||
if (metadata?.type === "Image") {
|
||||
if (
|
||||
Math.min(metadata.width, metadata.height) <= 0 ||
|
||||
(content_type === "image/gif" &&
|
||||
Math.max(metadata.width, metadata.height) >= 1024)
|
||||
)
|
||||
return fallback;
|
||||
}
|
||||
|
||||
let query = "";
|
||||
if (options) {
|
||||
if (!allowAnimation || content_type !== "image/gif") {
|
||||
query =
|
||||
"?" +
|
||||
Object.keys(options)
|
||||
.map((k) => `${k}=${options[k as keyof FileArgs[0]]}`)
|
||||
.join("&");
|
||||
}
|
||||
}
|
||||
|
||||
return `${autumn.url}/${tag}/${_id}${query}`;
|
||||
return sub;
|
||||
})
|
||||
.replace(RE_SPOILER, "<spoiler>");
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxy a file through January.
|
||||
* @param url URL to proxy
|
||||
* @returns Proxied media URL
|
||||
*/
|
||||
proxyFile(url: string): string | undefined {
|
||||
if (this.configuration?.features.january.enabled) {
|
||||
return `${
|
||||
this.configuration.features.january.url
|
||||
}/proxy?url=${encodeURIComponent(url)}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
import { DataEditBot } from "revolt-api";
|
||||
import { decodeTime } from "ulid";
|
||||
|
||||
import { BotCollection } from "../collections";
|
||||
|
||||
/**
|
||||
* Bot Class
|
||||
*/
|
||||
export class Bot {
|
||||
readonly #collection: BotCollection;
|
||||
readonly id: string;
|
||||
|
||||
/**
|
||||
* Construct Bot
|
||||
* @param collection Collection
|
||||
* @param id Id
|
||||
*/
|
||||
constructor(collection: BotCollection, id: string) {
|
||||
this.#collection = collection;
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert to string
|
||||
* @returns String
|
||||
*/
|
||||
toString() {
|
||||
return `<@${this.id}>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Time when this user created their account
|
||||
*/
|
||||
get createdAt() {
|
||||
return new Date(decodeTime(this.id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Corresponding user
|
||||
*/
|
||||
get user() {
|
||||
return this.#collection.client.users.get(this.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Owner's Id
|
||||
*/
|
||||
get ownerId() {
|
||||
return this.#collection.getUnderlyingObject(this.id).ownerId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Owner
|
||||
*/
|
||||
get owner() {
|
||||
return this.#collection.client.users.get(this.ownerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bot Token
|
||||
*/
|
||||
get token() {
|
||||
return this.#collection.getUnderlyingObject(this.id).token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this bot can be invited by anyone
|
||||
*/
|
||||
get public() {
|
||||
return this.#collection.getUnderlyingObject(this.id).public;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this bot has analytics enabled
|
||||
*/
|
||||
get analytics() {
|
||||
return this.#collection.getUnderlyingObject(this.id).analytics;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this bot shows up on Discover
|
||||
*/
|
||||
get discoverable() {
|
||||
return this.#collection.getUnderlyingObject(this.id).discoverable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interactions URL
|
||||
*/
|
||||
get interactionsUrl() {
|
||||
return this.#collection.getUnderlyingObject(this.id).interactionsUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Link to terms of service
|
||||
*/
|
||||
get termsOfServiceUrl() {
|
||||
return this.#collection.getUnderlyingObject(this.id).termsOfServiceUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Link to privacy policy
|
||||
*/
|
||||
get privacyPolicyUrl() {
|
||||
return this.#collection.getUnderlyingObject(this.id).privacyPolicyUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bot Flags
|
||||
*/
|
||||
get flags() {
|
||||
return this.#collection.getUnderlyingObject(this.id).flags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit a bot
|
||||
* @param data Changes
|
||||
*/
|
||||
async edit(data: DataEditBot) {
|
||||
await this.#collection.client.api.patch(`/bots/${this.id as ""}`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a bot
|
||||
*/
|
||||
async delete() {
|
||||
await this.#collection.client.api.delete(`/bots/${this.id as ""}`);
|
||||
this.#collection.delete(this.id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,660 @@
|
||||
import type {
|
||||
Member as ApiMember,
|
||||
Message as ApiMessage,
|
||||
User as ApiUser,
|
||||
DataEditChannel,
|
||||
DataMessageSend,
|
||||
OptionsMessageSearch,
|
||||
Override,
|
||||
} from "revolt-api";
|
||||
import { APIRoutes } from "revolt-api/dist/routes";
|
||||
import { decodeTime, ulid } from "ulid";
|
||||
|
||||
import { Message } from "..";
|
||||
import { ChannelCollection } from "../collections";
|
||||
import { bitwiseAndEq, calculatePermission } from "../permissions/calculator";
|
||||
import { Permission } from "../permissions/definitions";
|
||||
|
||||
/**
|
||||
* Channel Class
|
||||
*/
|
||||
export class Channel {
|
||||
readonly #collection: ChannelCollection;
|
||||
readonly id: string;
|
||||
|
||||
/**
|
||||
* Construct Channel
|
||||
* @param collection Collection
|
||||
* @param id Channel Id
|
||||
*/
|
||||
constructor(collection: ChannelCollection, id: string) {
|
||||
this.#collection = collection;
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write to string as a channel mention
|
||||
* @returns Formatted String
|
||||
*/
|
||||
toString() {
|
||||
return `<#${this.id}>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Time when this server was created
|
||||
*/
|
||||
get createdAt() {
|
||||
return new Date(decodeTime(this.id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Channel type
|
||||
*/
|
||||
get type() {
|
||||
return this.#collection.getUnderlyingObject(this.id).channelType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Absolute pathname to this channel in the client
|
||||
*/
|
||||
get path() {
|
||||
if (this.serverId) {
|
||||
return `/server/${this.serverId}/channel/${this.id}`;
|
||||
} else {
|
||||
return `/channel/${this.id}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* URL to this channel
|
||||
*/
|
||||
get url() {
|
||||
return this.#collection.client.configuration?.app + this.path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Channel name
|
||||
*/
|
||||
get name() {
|
||||
return this.#collection.getUnderlyingObject(this.id).name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display name
|
||||
*/
|
||||
get displayName() {
|
||||
return this.type === "SavedMessages"
|
||||
? this.user?.username
|
||||
: this.type === "DirectMessage"
|
||||
? this.recipient?.username
|
||||
: this.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Channel description
|
||||
*/
|
||||
get description() {
|
||||
return this.#collection.getUnderlyingObject(this.id).description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Channel icon
|
||||
*/
|
||||
get icon() {
|
||||
return this.#collection.getUnderlyingObject(this.id).icon;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the conversation is active
|
||||
*/
|
||||
get active() {
|
||||
return this.#collection.getUnderlyingObject(this.id).active;
|
||||
}
|
||||
|
||||
/**
|
||||
* User ids of people currently typing in channel
|
||||
*/
|
||||
get typingIds() {
|
||||
return this.#collection.getUnderlyingObject(this.id).typingIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Users currently trying in channel
|
||||
*/
|
||||
get typing() {
|
||||
return [...this.typingIds.values()].map(
|
||||
(id) => this.#collection.client.users.get(id)!
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* User ids of recipients of the group
|
||||
*/
|
||||
get recipientIds() {
|
||||
return this.#collection.getUnderlyingObject(this.id).recipientIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recipients of the group
|
||||
*/
|
||||
get recipients() {
|
||||
return [
|
||||
...this.#collection.getUnderlyingObject(this.id).recipientIds.values(),
|
||||
].map((id) => this.#collection.client.users.get(id)!);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find recipient of this DM
|
||||
*/
|
||||
get recipient() {
|
||||
return this.type === "DirectMessage"
|
||||
? this.recipients.find(
|
||||
(user) => user.id !== this.#collection.client.user!.id
|
||||
)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* User ID
|
||||
*/
|
||||
get userId() {
|
||||
return this.#collection.getUnderlyingObject(this.id).userId!;
|
||||
}
|
||||
|
||||
/**
|
||||
* User this channel belongs to
|
||||
*/
|
||||
get user() {
|
||||
return this.#collection.client.users.get(
|
||||
this.#collection.getUnderlyingObject(this.id).userId!
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Owner ID
|
||||
*/
|
||||
get ownerId() {
|
||||
return this.#collection.getUnderlyingObject(this.id).ownerId!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Owner of the group
|
||||
*/
|
||||
get owner() {
|
||||
return this.#collection.client.users.get(
|
||||
this.#collection.getUnderlyingObject(this.id).ownerId!
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Server ID
|
||||
*/
|
||||
get serverId() {
|
||||
return this.#collection.getUnderlyingObject(this.id).serverId!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Server this channel is in
|
||||
*/
|
||||
get server() {
|
||||
return this.#collection.client.servers.get(
|
||||
this.#collection.getUnderlyingObject(this.id).serverId!
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Permissions allowed for users in this group
|
||||
*/
|
||||
get permissions() {
|
||||
return this.#collection.getUnderlyingObject(this.id).permissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default permissions for this server channel
|
||||
*/
|
||||
get defaultPermissions() {
|
||||
return this.#collection.getUnderlyingObject(this.id).defaultPermissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Role permissions for this server channel
|
||||
*/
|
||||
get rolePermissions() {
|
||||
return this.#collection.getUnderlyingObject(this.id).rolePermissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this channel is marked as mature
|
||||
*/
|
||||
get mature() {
|
||||
return this.#collection.getUnderlyingObject(this.id).nsfw;
|
||||
}
|
||||
|
||||
/**
|
||||
* ID of the last message sent in this channel
|
||||
*/
|
||||
get lastMessageId() {
|
||||
return this.#collection.getUnderlyingObject(this.id).lastMessageId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Last message sent in this channel
|
||||
*/
|
||||
get lastMessage() {
|
||||
return this.#collection.client.messages.get(this.lastMessageId!);
|
||||
}
|
||||
|
||||
/**
|
||||
* Time when the last message was sent
|
||||
*/
|
||||
get lastMessageAt() {
|
||||
return this.lastMessageId
|
||||
? new Date(decodeTime(this.lastMessageId))
|
||||
: undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Time when the channel was last updated (either created or a message was sent)
|
||||
*/
|
||||
get updatedAt() {
|
||||
return this.lastMessageAt ?? this.createdAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get whether this channel is unread.
|
||||
*/
|
||||
get unread() {
|
||||
if (
|
||||
!this.lastMessageId ||
|
||||
this.type === "SavedMessages" ||
|
||||
this.type === "VoiceChannel" ||
|
||||
this.#collection.client.options.channelIsMuted(this)
|
||||
)
|
||||
return false;
|
||||
|
||||
return (
|
||||
(
|
||||
this.#collection.client.channelUnreads.get(this.id)?.lastMessageId ??
|
||||
"0"
|
||||
).localeCompare(this.lastMessageId) === -1
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get mentions in this channel for user.
|
||||
*/
|
||||
get mentions() {
|
||||
if (this.type === "SavedMessages" || this.type === "VoiceChannel")
|
||||
return undefined;
|
||||
|
||||
return this.#collection.client.channelUnreads.get(this.id)
|
||||
?.messageMentionIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* URL to the channel icon
|
||||
*/
|
||||
get iconURL() {
|
||||
return (
|
||||
this.icon?.createFileURL({ max_side: 256 }) ?? this.recipient?.avatarURL
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* URL to a small variant of the channel icon
|
||||
*/
|
||||
get smallIconURL() {
|
||||
return this.icon?.createFileURL({ max_side: 64 });
|
||||
}
|
||||
|
||||
/**
|
||||
* URL to the animated channel icon
|
||||
*/
|
||||
get animatedIconURL() {
|
||||
return (
|
||||
this.icon?.createFileURL({ max_side: 256 }, true) ??
|
||||
this.recipient?.animatedAvatarURL
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Permission the currently authenticated user has against this channel
|
||||
*/
|
||||
get permission() {
|
||||
return calculatePermission(this.#collection.client, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether we have a given permission in a channel
|
||||
* @param permission Permission Names
|
||||
* @returns Whether we have this permission
|
||||
*/
|
||||
havePermission(...permission: (keyof typeof Permission)[]) {
|
||||
return bitwiseAndEq(
|
||||
this.permission,
|
||||
...permission.map((x) => Permission[x])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a channel's members.
|
||||
* @requires `Group`
|
||||
* @returns An array of the channel's members.
|
||||
*/
|
||||
async fetchMembers() {
|
||||
const members = await this.#collection.client.api.get(
|
||||
`/channels/${this.id as ""}/members`
|
||||
);
|
||||
|
||||
return members.map((user) =>
|
||||
this.#collection.client.users.getOrCreate(user._id, user)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit a channel
|
||||
* @param data Changes
|
||||
*/
|
||||
async edit(data: DataEditChannel) {
|
||||
await this.#collection.client.api.patch(`/channels/${this.id as ""}`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete or leave a channel
|
||||
* @param leaveSilently Whether to not send a message on leave
|
||||
* @requires `DirectMessage`, `Group`, `TextChannel`, `VoiceChannel`
|
||||
*/
|
||||
async delete(leaveSilently?: boolean) {
|
||||
await this.#collection.client.api.delete(`/channels/${this.id as ""}`, {
|
||||
leave_silently: leaveSilently,
|
||||
});
|
||||
|
||||
if (this.type === "DirectMessage") {
|
||||
this.#collection.updateUnderlyingObject(this.id, "active", false);
|
||||
return;
|
||||
}
|
||||
|
||||
this.#collection.delete(this.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a user to a group
|
||||
* @param user_id ID of the target user
|
||||
* @requires `Group`
|
||||
*/
|
||||
async addMember(user_id: string) {
|
||||
return await this.#collection.client.api.put(
|
||||
`/channels/${this.id as ""}/recipients/${user_id as ""}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a user from a group
|
||||
* @param user_id ID of the target user
|
||||
* @requires `Group`
|
||||
*/
|
||||
async removeMember(user_id: string) {
|
||||
return await this.#collection.client.api.delete(
|
||||
`/channels/${this.id as ""}/recipients/${user_id as ""}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message
|
||||
* @param data Either the message as a string or message sending route data
|
||||
* @requires `SavedMessages`, `DirectMessage`, `Group`, `TextChannel`
|
||||
* @returns Sent message
|
||||
*/
|
||||
async sendMessage(
|
||||
data: string | DataMessageSend,
|
||||
idempotencyKey: string = ulid()
|
||||
) {
|
||||
const msg: DataMessageSend =
|
||||
typeof data === "string" ? { content: data } : data;
|
||||
|
||||
const message = await this.#collection.client.api.post(
|
||||
`/channels/${this.id as ""}/messages`,
|
||||
msg,
|
||||
{
|
||||
headers: {
|
||||
"Idempotency-Key": idempotencyKey,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return this.#collection.client.messages.getOrCreate(
|
||||
message._id,
|
||||
message,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a message by its ID
|
||||
* @param messageId ID of the target message
|
||||
* @requires `SavedMessages`, `DirectMessage`, `Group`, `TextChannel`
|
||||
* @returns Message
|
||||
*/
|
||||
async fetchMessage(messageId: string) {
|
||||
const message = await this.#collection.client.api.get(
|
||||
`/channels/${this.id as ""}/messages/${messageId as ""}`
|
||||
);
|
||||
|
||||
return this.#collection.client.messages.getOrCreate(message._id, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch multiple messages from a channel
|
||||
* @param params Message fetching route data
|
||||
* @requires `SavedMessages`, `DirectMessage`, `Group`, `TextChannel`
|
||||
* @returns Messages
|
||||
*/
|
||||
async fetchMessages(
|
||||
params?: Omit<
|
||||
(APIRoutes & {
|
||||
method: "get";
|
||||
path: "/channels/{target}/messages";
|
||||
})["params"],
|
||||
"include_users"
|
||||
>
|
||||
) {
|
||||
const messages = (await this.#collection.client.api.get(
|
||||
`/channels/${this.id as ""}/messages`,
|
||||
{ ...params }
|
||||
)) as ApiMessage[];
|
||||
|
||||
return messages.map((message) =>
|
||||
this.#collection.client.messages.getOrCreate(message._id, message)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch multiple messages from a channel including the users that sent them
|
||||
* @param params Message fetching route data
|
||||
* @requires `SavedMessages`, `DirectMessage`, `Group`, `TextChannel`
|
||||
* @returns Object including messages and users
|
||||
*/
|
||||
async fetchMessagesWithUsers(
|
||||
params?: Omit<
|
||||
(APIRoutes & {
|
||||
method: "get";
|
||||
path: "/channels/{target}/messages";
|
||||
})["params"],
|
||||
"include_users"
|
||||
>
|
||||
) {
|
||||
const data = (await this.#collection.client.api.get(
|
||||
`/channels/${this.id as ""}/messages`,
|
||||
{ ...params, include_users: true }
|
||||
)) as { messages: ApiMessage[]; users: ApiUser[]; members?: ApiMember[] };
|
||||
|
||||
return {
|
||||
messages: data.messages.map((message) =>
|
||||
this.#collection.client.messages.getOrCreate(message._id, message)
|
||||
),
|
||||
users: data.users.map((user) =>
|
||||
this.#collection.client.users.getOrCreate(user._id, user)
|
||||
),
|
||||
members: data.members?.map((member) =>
|
||||
this.#collection.client.serverMembers.getOrCreate(member._id, member)
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for messages
|
||||
* @param params Message searching route data
|
||||
* @requires `SavedMessages`, `DirectMessage`, `Group`, `TextChannel`
|
||||
* @returns Messages
|
||||
*/
|
||||
async search(params: Omit<OptionsMessageSearch, "include_users">) {
|
||||
const messages = (await this.#collection.client.api.post(
|
||||
`/channels/${this.id as ""}/search`,
|
||||
params
|
||||
)) as ApiMessage[];
|
||||
|
||||
return messages.map((message) =>
|
||||
this.#collection.client.messages.getOrCreate(message._id, message)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for messages including the users that sent them
|
||||
* @param params Message searching route data
|
||||
* @requires `SavedMessages`, `DirectMessage`, `Group`, `TextChannel`
|
||||
* @returns Object including messages and users
|
||||
*/
|
||||
async searchWithUsers(params: Omit<OptionsMessageSearch, "include_users">) {
|
||||
const data = (await this.#collection.client.api.post(
|
||||
`/channels/${this.id as ""}/search`,
|
||||
{
|
||||
...params,
|
||||
include_users: true,
|
||||
}
|
||||
)) as { messages: ApiMessage[]; users: ApiUser[]; members?: ApiMember[] };
|
||||
|
||||
return {
|
||||
messages: data.messages.map((message) =>
|
||||
this.#collection.client.messages.getOrCreate(message._id, message)
|
||||
),
|
||||
users: data.users.map((user) =>
|
||||
this.#collection.client.users.getOrCreate(user._id, user)
|
||||
),
|
||||
members: data.members?.map((member) =>
|
||||
this.#collection.client.serverMembers.getOrCreate(member._id, member)
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete many messages by their IDs
|
||||
* @param ids List of message IDs
|
||||
* @requires `SavedMessages`, `DirectMessage`, `Group`, `TextChannel`
|
||||
*/
|
||||
async deleteMessages(ids: string[]) {
|
||||
await this.#collection.client.api.delete(
|
||||
`/channels/${this.id as ""}/messages/bulk`,
|
||||
{
|
||||
ids,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an invite to the channel
|
||||
* @requires `TextChannel`, `VoiceChannel`
|
||||
* @returns Newly created invite code
|
||||
*/
|
||||
async createInvite() {
|
||||
return await this.#collection.client.api.post(
|
||||
`/channels/${this.id as ""}/invites`
|
||||
);
|
||||
}
|
||||
|
||||
#ackTimeout?: number;
|
||||
#ackLimit?: number;
|
||||
|
||||
/**
|
||||
* Mark a channel as read
|
||||
* @param message Last read message or its ID
|
||||
* @param skipRateLimiter Whether to skip the internal rate limiter
|
||||
* @requires `SavedMessages`, `DirectMessage`, `Group`, `TextChannel`
|
||||
*/
|
||||
async ack(message?: Message | string, skipRateLimiter?: boolean) {
|
||||
const lastMessageId =
|
||||
(typeof message === "string" ? message : message?.id) ??
|
||||
this.lastMessageId ??
|
||||
ulid();
|
||||
|
||||
const unreads = this.#collection.client.channelUnreads;
|
||||
const channelUnread = unreads.get(this.id);
|
||||
if (channelUnread) {
|
||||
unreads.updateUnderlyingObject(this.id, {
|
||||
lastMessageId,
|
||||
});
|
||||
|
||||
if (channelUnread.messageMentionIds.size) {
|
||||
channelUnread.messageMentionIds.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the actual acknowledgement request
|
||||
*/
|
||||
const performAck = () => {
|
||||
this.#ackLimit = undefined;
|
||||
this.#collection.client.api.put(
|
||||
`/channels/${this.id}/ack/${lastMessageId as ""}`
|
||||
);
|
||||
};
|
||||
|
||||
if (skipRateLimiter) return performAck();
|
||||
|
||||
clearTimeout(this.#ackTimeout);
|
||||
if (this.#ackLimit && +new Date() > this.#ackLimit) {
|
||||
performAck();
|
||||
}
|
||||
|
||||
// We need to use setTimeout here for both Node.js and browser.
|
||||
this.#ackTimeout = setTimeout(performAck, 5000) as unknown as number;
|
||||
|
||||
if (!this.#ackLimit) {
|
||||
this.#ackLimit = +new Date() + 15e3;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set role permissions
|
||||
* @param role_id Role Id, set to 'default' to affect all users
|
||||
* @param permissions Permission value
|
||||
* @requires `Group`, `TextChannel`, `VoiceChannel`
|
||||
*/
|
||||
async setPermissions(role_id = "default", permissions: Override) {
|
||||
return await this.#collection.client.api.put(
|
||||
`/channels/${this.id as ""}/permissions/${role_id as ""}`,
|
||||
{ permissions }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start typing in this channel
|
||||
* @requires `DirectMessage`, `Group`, `TextChannel`
|
||||
*/
|
||||
startTyping() {
|
||||
this.#collection.client.events.send({
|
||||
type: "BeginTyping",
|
||||
channel: this.id,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop typing in this channel
|
||||
* @requires `DirectMessage`, `Group`, `TextChannel`
|
||||
*/
|
||||
stopTyping() {
|
||||
this.#collection.client.events.send({
|
||||
type: "EndTyping",
|
||||
channel: this.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { ChannelUnreadCollection } from "../collections";
|
||||
|
||||
/**
|
||||
* Channel Unread Class
|
||||
*/
|
||||
export class ChannelUnread {
|
||||
readonly #collection: ChannelUnreadCollection;
|
||||
readonly id: string;
|
||||
|
||||
/**
|
||||
* Construct Channel
|
||||
* @param collection Collection
|
||||
* @param id Channel Id
|
||||
*/
|
||||
constructor(collection: ChannelUnreadCollection, id: string) {
|
||||
this.#collection = collection;
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Last read message id
|
||||
*/
|
||||
get lastMessageId() {
|
||||
return this.#collection.getUnderlyingObject(this.id).lastMessageId;
|
||||
}
|
||||
|
||||
/**
|
||||
* List of message IDs that we were mentioned in
|
||||
*/
|
||||
get messageMentionIds() {
|
||||
return this.#collection.getUnderlyingObject(this.id).messageMentionIds;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import { decodeTime } from "ulid";
|
||||
|
||||
import { EmojiCollection } from "../collections";
|
||||
|
||||
/**
|
||||
* Emoji Class
|
||||
*/
|
||||
export class Emoji {
|
||||
readonly #collection: EmojiCollection;
|
||||
readonly id: string;
|
||||
|
||||
/**
|
||||
* Construct Emoji
|
||||
* @param collection Collection
|
||||
* @param id Emoji Id
|
||||
*/
|
||||
constructor(collection: EmojiCollection, id: string) {
|
||||
this.#collection = collection;
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert to string
|
||||
* @returns String
|
||||
*/
|
||||
toString() {
|
||||
return `:${this.id}:`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Time when this emoji was created
|
||||
*/
|
||||
get createdAt() {
|
||||
return new Date(decodeTime(this.id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Information about the parent of this emoji
|
||||
*/
|
||||
get parent() {
|
||||
return this.#collection.getUnderlyingObject(this.id).parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creator of the emoji
|
||||
*/
|
||||
get creator() {
|
||||
return this.#collection.client.users.get(
|
||||
this.#collection.getUnderlyingObject(this.id).creatorId
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Name
|
||||
*/
|
||||
get name() {
|
||||
return this.#collection.getUnderlyingObject(this.id).name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the emoji is animated
|
||||
*/
|
||||
get animated() {
|
||||
return this.#collection.getUnderlyingObject(this.id).animated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the emoji is marked as mature
|
||||
*/
|
||||
get mature() {
|
||||
return this.#collection.getUnderlyingObject(this.id).nsfw;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete Emoji
|
||||
*/
|
||||
async delete() {
|
||||
await this.#collection.client.api.delete(`/custom/emoji/${this.id}`);
|
||||
|
||||
const emoji = this.#collection.getUnderlyingObject(this.id);
|
||||
if (emoji) {
|
||||
this.#collection.client.emit("emojiDelete", emoji);
|
||||
this.#collection.delete(this.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import { Metadata } from "revolt-api";
|
||||
|
||||
import { API, Client } from "..";
|
||||
|
||||
/**
|
||||
* Uploaded File
|
||||
*/
|
||||
export class File {
|
||||
#client: Client;
|
||||
|
||||
/**
|
||||
* File Id
|
||||
*/
|
||||
readonly id: string;
|
||||
|
||||
/**
|
||||
* File bucket
|
||||
*/
|
||||
readonly tag: string;
|
||||
|
||||
/**
|
||||
* Original filename
|
||||
*/
|
||||
readonly filename: string;
|
||||
|
||||
/**
|
||||
* Parsed metadata of the file
|
||||
*/
|
||||
readonly metadata: Metadata;
|
||||
|
||||
/**
|
||||
* Raw content type of this file
|
||||
*/
|
||||
readonly contentType: string;
|
||||
|
||||
/**
|
||||
* Size of the file (in bytes)
|
||||
*/
|
||||
readonly size: number;
|
||||
|
||||
/**
|
||||
* Construct File
|
||||
* @param client Client
|
||||
* @param file File
|
||||
*/
|
||||
constructor(client: Client, file: API.File) {
|
||||
this.#client = client;
|
||||
this.id = file._id;
|
||||
this.tag = file.tag;
|
||||
this.filename = file.filename;
|
||||
this.metadata = file.metadata;
|
||||
this.contentType = file.content_type;
|
||||
this.size = file.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Direct URL to the file
|
||||
*/
|
||||
get url() {
|
||||
return `${this.#client.configuration?.features.autumn.url}/${this.tag}/${
|
||||
this.id
|
||||
}/${this.filename}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Download URL for the file
|
||||
*/
|
||||
get downloadURL() {
|
||||
return `${this.#client.configuration?.features.autumn.url}/${
|
||||
this.tag
|
||||
}/download/${this.id}/${this.filename}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Human readable file size
|
||||
*/
|
||||
get humanReadableSize() {
|
||||
if (this.size > 1e6) {
|
||||
return `${(this.size / 1e6).toFixed(2)} MB`;
|
||||
} else if (this.size > 1e3) {
|
||||
return `${(this.size / 1e3).toFixed(2)} KB`;
|
||||
}
|
||||
|
||||
return `${this.size} B`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this file should have a spoiler
|
||||
*/
|
||||
get isSpoiler() {
|
||||
return this.filename.toLowerCase().startsWith("spoiler_");
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a URL to a given file with given options.
|
||||
* @param options Optional query parameters to modify object
|
||||
* @param allowAnimation Returns GIF if applicable, no operations occur on image
|
||||
* @returns Generated URL or nothing
|
||||
*/
|
||||
createFileURL(
|
||||
options?: {
|
||||
max_side?: number;
|
||||
size?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
},
|
||||
allowAnimation?: boolean
|
||||
) {
|
||||
const autumn = this.#client.configuration?.features.autumn;
|
||||
if (!autumn?.enabled) return;
|
||||
|
||||
// TODO: server-side
|
||||
if (this.metadata.type === "Image") {
|
||||
if (
|
||||
Math.min(this.metadata.width, this.metadata.height) <= 0 ||
|
||||
(this.contentType === "image/gif" &&
|
||||
Math.max(this.metadata.width, this.metadata.height) >= 4096)
|
||||
)
|
||||
return;
|
||||
}
|
||||
|
||||
let query = "";
|
||||
if (options) {
|
||||
if (!allowAnimation || this.contentType !== "image/gif") {
|
||||
query =
|
||||
"?" +
|
||||
Object.keys(options)
|
||||
.map((k) => `${k}=${options[k as keyof typeof options]}`)
|
||||
.join("&");
|
||||
}
|
||||
}
|
||||
|
||||
return `${autumn.url}/${this.tag}/${this.id}${query}`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import { API, Client } from "..";
|
||||
|
||||
/**
|
||||
* Channel Invite
|
||||
*/
|
||||
export abstract class ChannelInvite {
|
||||
protected client?: Client;
|
||||
readonly type: API.Invite["type"] | "None";
|
||||
|
||||
/**
|
||||
* Construct Channel Invite
|
||||
* @param client Client
|
||||
* @param type Type
|
||||
*/
|
||||
constructor(client?: Client, type: API.Invite["type"] | "None" = "None") {
|
||||
this.client = client;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an Invite from an API Invite
|
||||
* @param client Client
|
||||
* @param invite Data
|
||||
* @returns Invite
|
||||
*/
|
||||
static from(client: Client, invite: API.Invite): ChannelInvite {
|
||||
switch (invite.type) {
|
||||
case "Server":
|
||||
return new ServerInvite(client, invite);
|
||||
default:
|
||||
return new UnknownInvite(client);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invite of unknown type
|
||||
*/
|
||||
export class UnknownInvite extends ChannelInvite {}
|
||||
|
||||
/**
|
||||
* Server Invite
|
||||
*/
|
||||
export class ServerInvite extends ChannelInvite {
|
||||
readonly id: string;
|
||||
readonly creatorId: string;
|
||||
readonly serverId: string;
|
||||
readonly channelId: string;
|
||||
|
||||
/**
|
||||
* Construct Server Invite
|
||||
* @param client Client
|
||||
* @param invite Invite
|
||||
*/
|
||||
constructor(client: Client, invite: API.Invite & { type: "Server" }) {
|
||||
super(client, "Server");
|
||||
|
||||
this.id = invite._id;
|
||||
this.creatorId = invite.creator;
|
||||
this.serverId = invite.server;
|
||||
this.channelId = invite.server;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creator of the invite
|
||||
*/
|
||||
get creator() {
|
||||
return this.client!.users.get(this.creatorId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Server this invite points to
|
||||
*/
|
||||
get server() {
|
||||
return this.client!.servers.get(this.serverId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Channel this invite points to
|
||||
*/
|
||||
get channel() {
|
||||
return this.client!.channels.get(this.channelId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the invite
|
||||
*/
|
||||
async delete() {
|
||||
await this.client!.api.delete(`/invites/${this.id}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
import { DataEditMessage, DataMessageSend } from "revolt-api";
|
||||
import { decodeTime } from "ulid";
|
||||
|
||||
import { MessageCollection } from "../collections";
|
||||
|
||||
/**
|
||||
* Message Class
|
||||
*/
|
||||
export class Message {
|
||||
readonly #collection: MessageCollection;
|
||||
readonly id: string;
|
||||
|
||||
/**
|
||||
* Construct Message
|
||||
* @param collection Collection
|
||||
* @param id Message Id
|
||||
*/
|
||||
constructor(collection: MessageCollection, id: string) {
|
||||
this.#collection = collection;
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Time when this message was posted
|
||||
*/
|
||||
get createdAt() {
|
||||
return new Date(decodeTime(this.id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Absolute pathname to this message in the client
|
||||
*/
|
||||
get path() {
|
||||
return `${this.channel!.path}/${this.id}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* URL to this message
|
||||
*/
|
||||
get url() {
|
||||
return this.#collection.client.configuration?.app + this.path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Nonce value
|
||||
*/
|
||||
get nonce() {
|
||||
return this.#collection.getUnderlyingObject(this.id).nonce;
|
||||
}
|
||||
|
||||
/**
|
||||
* Id of channel this message was sent in
|
||||
*/
|
||||
get channelId() {
|
||||
return this.#collection.getUnderlyingObject(this.id).channelId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Channel this message was sent in
|
||||
*/
|
||||
get channel() {
|
||||
return this.#collection.client.channels.get(
|
||||
this.#collection.getUnderlyingObject(this.id).channelId
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Server this message was sent in
|
||||
*/
|
||||
get server() {
|
||||
return this.channel!.server;
|
||||
}
|
||||
|
||||
/**
|
||||
* Member this message was sent by
|
||||
*/
|
||||
get member() {
|
||||
return this.#collection.client.serverMembers.getByKey({
|
||||
server: this.channel!.serverId,
|
||||
user: this.authorId!,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Id of user this message was sent by
|
||||
*/
|
||||
get authorId() {
|
||||
return this.#collection.getUnderlyingObject(this.id).authorId;
|
||||
}
|
||||
|
||||
/**
|
||||
* User this message was sent by
|
||||
*/
|
||||
get author() {
|
||||
return this.#collection.client.users.get(
|
||||
this.#collection.getUnderlyingObject(this.id).authorId!
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Content
|
||||
*/
|
||||
get content() {
|
||||
return this.#collection.getUnderlyingObject(this.id).content;
|
||||
}
|
||||
|
||||
/**
|
||||
* System message content
|
||||
*/
|
||||
get systemMessage() {
|
||||
return this.#collection.getUnderlyingObject(this.id).systemMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attachments
|
||||
*/
|
||||
get attachments() {
|
||||
return this.#collection.getUnderlyingObject(this.id).attachments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Time at which this message was edited
|
||||
*/
|
||||
get editedAt() {
|
||||
return this.#collection.getUnderlyingObject(this.id).editedAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Embeds
|
||||
*/
|
||||
get embeds() {
|
||||
return this.#collection.getUnderlyingObject(this.id).embeds;
|
||||
}
|
||||
|
||||
/**
|
||||
* IDs of users this message mentions
|
||||
*/
|
||||
get mentionIds() {
|
||||
return this.#collection.getUnderlyingObject(this.id).mentionIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* IDs of messages this message replies to
|
||||
*/
|
||||
get replyIds() {
|
||||
return this.#collection.getUnderlyingObject(this.id).replyIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reactions
|
||||
*/
|
||||
get reactions() {
|
||||
return this.#collection.getUnderlyingObject(this.id).reactions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interactions
|
||||
*/
|
||||
get interactions() {
|
||||
return this.#collection.getUnderlyingObject(this.id).interactions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Masquerade
|
||||
*/
|
||||
get masquerade() {
|
||||
return this.#collection.getUnderlyingObject(this.id).masquerade;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the username for this message
|
||||
*/
|
||||
get username() {
|
||||
return (
|
||||
this.masquerade?.name ?? this.member?.nickname ?? this.author?.username
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the role colour for this message
|
||||
*/
|
||||
get roleColour() {
|
||||
return this.masquerade?.colour ?? this.member?.roleColour;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the avatar URL for this message
|
||||
*/
|
||||
get avatarURL() {
|
||||
return (
|
||||
this.masqueradeAvatarURL ??
|
||||
this.member?.avatarURL ??
|
||||
this.author?.avatarURL
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the animated avatar URL for this message
|
||||
*/
|
||||
get animatedAvatarURL() {
|
||||
return (
|
||||
this.masqueradeAvatarURL ??
|
||||
(this.member
|
||||
? this.member?.animatedAvatarURL
|
||||
: this.author?.animatedAvatarURL)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Avatar URL from the masquerade
|
||||
*/
|
||||
get masqueradeAvatarURL() {
|
||||
const avatar = this.masquerade?.avatar;
|
||||
return avatar ? this.#collection.client.proxyFile(avatar) : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit a message
|
||||
* @param data Message edit route data
|
||||
*/
|
||||
async edit(data: DataEditMessage) {
|
||||
return await this.#collection.client.api.patch(
|
||||
`/channels/${this.channelId as ""}/messages/${this.id as ""}`,
|
||||
data
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a message
|
||||
*/
|
||||
async delete() {
|
||||
return await this.#collection.client.api.delete(
|
||||
`/channels/${this.channelId as ""}/messages/${this.id as ""}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Acknowledge this message as read
|
||||
*/
|
||||
ack() {
|
||||
this.channel?.ack(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reply to Message
|
||||
*/
|
||||
reply(
|
||||
data:
|
||||
| string
|
||||
| (Omit<DataMessageSend, "nonce"> & {
|
||||
nonce?: string;
|
||||
}),
|
||||
mention = true
|
||||
) {
|
||||
const obj = typeof data === "string" ? { content: data } : data;
|
||||
return this.channel?.sendMessage({
|
||||
...obj,
|
||||
replies: [{ id: this.id, mention }],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all reactions from this message
|
||||
*/
|
||||
async clearReactions() {
|
||||
return await this.#collection.client.api.delete(
|
||||
`/channels/${this.channelId as ""}/messages/${this.id as ""}/reactions`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* React to a message
|
||||
* @param emoji Unicode or emoji ID
|
||||
*/
|
||||
async react(emoji: string) {
|
||||
return await this.#collection.client.api.put(
|
||||
`/channels/${this.channelId as ""}/messages/${this.id as ""}/reactions/${
|
||||
emoji as ""
|
||||
}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Un-react from a message
|
||||
* @param emoji Unicode or emoji ID
|
||||
*/
|
||||
async unreact(emoji: string) {
|
||||
return await this.#collection.client.api.delete(
|
||||
`/channels/${this.channelId as ""}/messages/${this.id as ""}/reactions/${
|
||||
emoji as ""
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
import { API, Client, File } from "..";
|
||||
|
||||
/**
|
||||
* Message Embed
|
||||
*/
|
||||
export abstract class MessageEmbed {
|
||||
protected client?: Client;
|
||||
readonly type: API.Embed["type"];
|
||||
|
||||
/**
|
||||
* Construct Embed
|
||||
* @param client Client
|
||||
* @param type Type
|
||||
*/
|
||||
constructor(client?: Client, type: API.Embed["type"] = "None") {
|
||||
this.client = client;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an Embed from an API Embed
|
||||
* @param client Client
|
||||
* @param embed Data
|
||||
* @returns Embed
|
||||
*/
|
||||
static from(client: Client, embed: API.Embed): MessageEmbed {
|
||||
switch (embed.type) {
|
||||
case "Image":
|
||||
return new ImageEmbed(client, embed);
|
||||
case "Video":
|
||||
return new VideoEmbed(client, embed);
|
||||
case "Website":
|
||||
return new WebsiteEmbed(client, embed);
|
||||
case "Text":
|
||||
return new TextEmbed(client, embed);
|
||||
default:
|
||||
return new UnknownEmbed(client);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Embed of unknown type
|
||||
*/
|
||||
export class UnknownEmbed extends MessageEmbed {}
|
||||
|
||||
/**
|
||||
* Image Embed
|
||||
*/
|
||||
export class ImageEmbed extends MessageEmbed {
|
||||
readonly url: string;
|
||||
readonly width: number;
|
||||
readonly height: number;
|
||||
readonly size: API.ImageSize;
|
||||
|
||||
/**
|
||||
* Construct Image Embed
|
||||
* @param client Client
|
||||
* @param embed Embed
|
||||
*/
|
||||
constructor(
|
||||
client: Client,
|
||||
embed: Omit<API.Embed & { type: "Image" }, "type">
|
||||
) {
|
||||
super(client, "Image");
|
||||
|
||||
this.url = embed.url;
|
||||
this.width = embed.width;
|
||||
this.height = embed.height;
|
||||
this.size = embed.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxied image URL
|
||||
*/
|
||||
get proxiedURL() {
|
||||
return this.client?.proxyFile(this.url);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Video Embed
|
||||
*/
|
||||
export class VideoEmbed extends MessageEmbed {
|
||||
readonly url: string;
|
||||
readonly width: number;
|
||||
readonly height: number;
|
||||
|
||||
/**
|
||||
* Construct Video Embed
|
||||
* @param client Client
|
||||
* @param embed Embed
|
||||
*/
|
||||
constructor(
|
||||
client: Client,
|
||||
embed: Omit<API.Embed & { type: "Video" }, "type">
|
||||
) {
|
||||
super(client, "Video");
|
||||
|
||||
this.url = embed.url;
|
||||
this.width = embed.width;
|
||||
this.height = embed.height;
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxied video URL
|
||||
*/
|
||||
get proxiedURL() {
|
||||
return this.client?.proxyFile(this.url);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Website Embed
|
||||
*/
|
||||
export class WebsiteEmbed extends MessageEmbed {
|
||||
readonly url?: string;
|
||||
readonly originalUrl?: string;
|
||||
readonly specialContent?: API.Special;
|
||||
readonly title?: string;
|
||||
readonly description?: string;
|
||||
readonly image?: ImageEmbed;
|
||||
readonly video?: VideoEmbed;
|
||||
readonly siteName?: string;
|
||||
readonly iconUrl?: string;
|
||||
readonly colour?: string;
|
||||
|
||||
/**
|
||||
* Construct Video Embed
|
||||
* @param client Client
|
||||
* @param embed Embed
|
||||
*/
|
||||
constructor(
|
||||
client: Client,
|
||||
embed: Omit<API.Embed & { type: "Website" }, "type">
|
||||
) {
|
||||
super(client, "Website");
|
||||
|
||||
this.url = embed.url!;
|
||||
this.originalUrl = embed.original_url!;
|
||||
this.specialContent = embed.special!;
|
||||
this.title = embed.title!;
|
||||
this.description = embed.description!;
|
||||
this.image = embed.image ? new ImageEmbed(client, embed.image) : undefined;
|
||||
this.video = embed.video ? new VideoEmbed(client, embed.video) : undefined;
|
||||
this.siteName = embed.site_name!;
|
||||
this.iconUrl = embed.icon_url!;
|
||||
this.colour = embed.colour!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxied icon URL
|
||||
*/
|
||||
get proxiedIconURL() {
|
||||
return this.iconUrl ? this.client?.proxyFile(this.iconUrl) : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* If special content is present, generate the embed URL
|
||||
*/
|
||||
get embedURL() {
|
||||
switch (this.specialContent?.type) {
|
||||
case "YouTube": {
|
||||
let timestamp = "";
|
||||
|
||||
if (this.specialContent.timestamp) {
|
||||
timestamp = `&start=${this.specialContent.timestamp}`;
|
||||
}
|
||||
|
||||
return `https://www.youtube-nocookie.com/embed/${this.specialContent.id}?modestbranding=1${timestamp}`;
|
||||
}
|
||||
case "Twitch":
|
||||
return `https://player.twitch.tv/?${this.specialContent.content_type.toLowerCase()}=${
|
||||
this.specialContent.id
|
||||
}&parent=${(window ?? {})?.location?.hostname}&autoplay=false`;
|
||||
case "Lightspeed":
|
||||
return `https://new.lightspeed.tv/embed/${this.specialContent.id}/stream`;
|
||||
case "Spotify":
|
||||
return `https://open.spotify.com/embed/${this.specialContent.content_type}/${this.specialContent.id}`;
|
||||
case "Soundcloud":
|
||||
return `https://w.soundcloud.com/player/?url=${encodeURIComponent(
|
||||
this.url!
|
||||
)}&color=%23FF7F50&auto_play=false&hide_related=false&show_comments=true&show_user=true&show_reposts=false&show_teaser=true&visual=true`;
|
||||
case "Bandcamp": {
|
||||
return `https://bandcamp.com/EmbeddedPlayer/${this.specialContent.content_type.toLowerCase()}=${
|
||||
this.specialContent.id
|
||||
}/size=large/bgcol=181a1b/linkcol=056cc4/tracklist=false/transparent=true/`;
|
||||
}
|
||||
case "Streamable": {
|
||||
return `https://streamable.com/e/${this.specialContent.id}?loop=0`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Text Embed
|
||||
*/
|
||||
export class TextEmbed extends MessageEmbed {
|
||||
readonly iconUrl?: string;
|
||||
readonly url?: string;
|
||||
readonly title?: string;
|
||||
readonly description?: string;
|
||||
readonly media?: File;
|
||||
readonly colour?: string;
|
||||
|
||||
/**
|
||||
* Construct Video Embed
|
||||
* @param client Client
|
||||
* @param embed Embed
|
||||
*/
|
||||
constructor(
|
||||
client: Client,
|
||||
embed: Omit<API.Embed & { type: "Text" }, "type">
|
||||
) {
|
||||
super(client, "Text");
|
||||
|
||||
this.iconUrl = embed.icon_url!;
|
||||
this.url = embed.url!;
|
||||
this.title = embed.title!;
|
||||
this.description = embed.description!;
|
||||
this.media = embed.media ? new File(client, embed.media) : undefined;
|
||||
this.colour = embed.colour!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxied icon URL
|
||||
*/
|
||||
get proxiedIconURL() {
|
||||
return this.iconUrl ? this.client?.proxyFile(this.iconUrl) : undefined;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { API, Channel, Client, File, Server } from "..";
|
||||
|
||||
/**
|
||||
* Public Bot Class
|
||||
*/
|
||||
export class PublicBot {
|
||||
#client: Client;
|
||||
|
||||
readonly id: string;
|
||||
readonly username: string;
|
||||
readonly avatar?: File;
|
||||
readonly description?: string;
|
||||
|
||||
/**
|
||||
* Construct Public Bot
|
||||
* @param client Client
|
||||
* @param data Data
|
||||
*/
|
||||
constructor(client: Client, data: API.PublicBot) {
|
||||
this.#client = client;
|
||||
this.id = data._id;
|
||||
this.username = data.username;
|
||||
this.avatar = data.avatar ? new File(client, data.avatar) : undefined;
|
||||
this.description = data.description!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the bot to a server
|
||||
* @param server Server
|
||||
*/
|
||||
addToServer(server: Server | string) {
|
||||
this.#client.api.post(`/bots/${this.id as ""}/invite`, {
|
||||
server: server instanceof Server ? server.id : server,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the bot to a group
|
||||
* @param group Group
|
||||
*/
|
||||
addToGroup(group: Channel | string) {
|
||||
// TODO: should use GroupChannel once that is added
|
||||
this.#client.api.post(`/bots/${this.id as ""}/invite`, {
|
||||
group: group instanceof Channel ? group.id : group,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import { API, Client, File } from "..";
|
||||
import { ServerFlags } from "../hydration/server";
|
||||
|
||||
/**
|
||||
* Public Channel Invite
|
||||
*/
|
||||
export abstract class PublicChannelInvite {
|
||||
protected client?: Client;
|
||||
readonly type: API.Invite["type"] | "None";
|
||||
|
||||
/**
|
||||
* Construct Channel Invite
|
||||
* @param client Client
|
||||
* @param type Type
|
||||
*/
|
||||
constructor(client?: Client, type: API.Invite["type"] | "None" = "None") {
|
||||
this.client = client;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an Invite from an API Invite Response
|
||||
* @param client Client
|
||||
* @param invite Data
|
||||
* @returns Invite
|
||||
*/
|
||||
static from(client: Client, invite: API.InviteResponse): PublicChannelInvite {
|
||||
switch (invite.type) {
|
||||
case "Server":
|
||||
return new ServerPublicInvite(client, invite);
|
||||
default:
|
||||
return new UnknownPublicInvite(client);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Public invite of unknown type
|
||||
*/
|
||||
export class UnknownPublicInvite extends PublicChannelInvite {}
|
||||
|
||||
/**
|
||||
* Public Server Invite
|
||||
*/
|
||||
export class ServerPublicInvite extends PublicChannelInvite {
|
||||
readonly code: string;
|
||||
readonly userName: string;
|
||||
readonly userAvatar?: File;
|
||||
|
||||
readonly serverId: string;
|
||||
readonly serverName: string;
|
||||
readonly serverIcon?: File;
|
||||
readonly serverBanner?: File;
|
||||
readonly serverFlags: ServerFlags;
|
||||
readonly memberCount: number;
|
||||
|
||||
readonly channelId: string;
|
||||
readonly channelName: string;
|
||||
readonly channelDescription?: string;
|
||||
|
||||
/**
|
||||
* Construct Server Invite
|
||||
* @param client Client
|
||||
* @param invite Invite
|
||||
*/
|
||||
constructor(client: Client, invite: API.InviteResponse & { type: "Server" }) {
|
||||
super(client, "Server");
|
||||
|
||||
this.code = invite.code;
|
||||
this.userName = invite.user_name;
|
||||
this.userAvatar = invite.user_avatar
|
||||
? new File(client, invite.user_avatar)
|
||||
: undefined;
|
||||
|
||||
this.serverId = invite.server_id;
|
||||
this.serverName = invite.server_name;
|
||||
this.serverIcon = invite.server_icon
|
||||
? new File(client, invite.server_icon)
|
||||
: undefined;
|
||||
this.serverBanner = invite.server_banner
|
||||
? new File(client, invite.server_banner)
|
||||
: undefined;
|
||||
this.serverFlags = invite.server_flags ?? 0;
|
||||
this.memberCount = invite.member_count;
|
||||
|
||||
this.channelId = invite.channel_id;
|
||||
this.channelName = invite.channel_name;
|
||||
this.channelDescription = invite.channel_description!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Join the server
|
||||
*/
|
||||
async join() {
|
||||
const existingServer = this.client!.servers.get(this.serverId);
|
||||
if (existingServer) return existingServer;
|
||||
|
||||
const { server, channels } = await this.client!.api.post(
|
||||
`/invites/${this.code as ""}`
|
||||
);
|
||||
|
||||
for (const channel of channels) {
|
||||
this.client!.channels.getOrCreate(channel._id, channel);
|
||||
}
|
||||
|
||||
return this.client!.servers.getOrCreate(server._id, server, true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,603 @@
|
||||
import type {
|
||||
Category,
|
||||
DataBanCreate,
|
||||
DataCreateChannel,
|
||||
DataCreateEmoji,
|
||||
DataEditRole,
|
||||
DataEditServer,
|
||||
Override,
|
||||
} from "revolt-api";
|
||||
import { decodeTime } from "ulid";
|
||||
|
||||
import { ServerMember, User } from "..";
|
||||
import { ServerCollection } from "../collections";
|
||||
import { bitwiseAndEq, calculatePermission } from "../permissions/calculator";
|
||||
import { Permission } from "../permissions/definitions";
|
||||
|
||||
import { Channel } from "./Channel";
|
||||
import { ChannelInvite } from "./Invite";
|
||||
import { ServerBan } from "./ServerBan";
|
||||
|
||||
/**
|
||||
* Server Class
|
||||
*/
|
||||
export class Server {
|
||||
readonly #collection: ServerCollection;
|
||||
readonly id: string;
|
||||
|
||||
/**
|
||||
* Construct Server
|
||||
* @param collection Collection
|
||||
* @param id Id
|
||||
*/
|
||||
constructor(collection: ServerCollection, id: string) {
|
||||
this.#collection = collection;
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert to string
|
||||
* @returns String
|
||||
*/
|
||||
toString() {
|
||||
return `<%${this.id}>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Time when this server was created
|
||||
*/
|
||||
get createdAt() {
|
||||
return new Date(decodeTime(this.id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Owner's user ID
|
||||
*/
|
||||
get ownerId() {
|
||||
return this.#collection.getUnderlyingObject(this.id).ownerId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Owner
|
||||
*/
|
||||
get owner() {
|
||||
return this.#collection.client.users.get(
|
||||
this.#collection.getUnderlyingObject(this.id).ownerId
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Name
|
||||
*/
|
||||
get name() {
|
||||
return this.#collection.getUnderlyingObject(this.id).name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Description
|
||||
*/
|
||||
get description() {
|
||||
return this.#collection.getUnderlyingObject(this.id).description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Icon
|
||||
*/
|
||||
get icon() {
|
||||
return this.#collection.getUnderlyingObject(this.id).icon;
|
||||
}
|
||||
|
||||
/**
|
||||
* Banner
|
||||
*/
|
||||
get banner() {
|
||||
return this.#collection.getUnderlyingObject(this.id).banner;
|
||||
}
|
||||
|
||||
/**
|
||||
* Channel IDs
|
||||
*/
|
||||
get channelIds() {
|
||||
return this.#collection.getUnderlyingObject(this.id).channelIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Channels
|
||||
*/
|
||||
get channels() {
|
||||
return [
|
||||
...this.#collection.getUnderlyingObject(this.id).channelIds.values(),
|
||||
]
|
||||
.map((id) => this.#collection.client.channels.get(id)!)
|
||||
.filter((x) => x);
|
||||
}
|
||||
|
||||
/**
|
||||
* Categories
|
||||
*/
|
||||
get categories() {
|
||||
return this.#collection.getUnderlyingObject(this.id).categories;
|
||||
}
|
||||
|
||||
/**
|
||||
* System message channels
|
||||
*/
|
||||
get systemMessages() {
|
||||
return this.#collection.getUnderlyingObject(this.id).systemMessages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Roles
|
||||
*/
|
||||
get roles() {
|
||||
return this.#collection.getUnderlyingObject(this.id).roles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default permissions
|
||||
*/
|
||||
get defaultPermissions() {
|
||||
return this.#collection.getUnderlyingObject(this.id).defaultPermissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Server flags
|
||||
*/
|
||||
get flags() {
|
||||
return this.#collection.getUnderlyingObject(this.id).flags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether analytics are enabled for this server
|
||||
*/
|
||||
get analytics() {
|
||||
return this.#collection.getUnderlyingObject(this.id).analytics;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this server is publicly discoverable
|
||||
*/
|
||||
get discoverable() {
|
||||
return this.#collection.getUnderlyingObject(this.id).discoverable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this server is marked as mature
|
||||
*/
|
||||
get mature() {
|
||||
return this.#collection.getUnderlyingObject(this.id).nsfw;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array of ordered categories with their respective channels.
|
||||
* Uncategorised channels are returned in `id="default"` category.
|
||||
*/
|
||||
get orderedChannels(): (Omit<Category, "channels"> & {
|
||||
channels: Channel[];
|
||||
})[] {
|
||||
const uncategorised = new Set(this.channels.map((channel) => channel.id));
|
||||
|
||||
const elements = [];
|
||||
let defaultCategory;
|
||||
|
||||
const categories = this.categories;
|
||||
if (categories) {
|
||||
for (const category of categories) {
|
||||
const channels = [];
|
||||
for (const key of category.channels) {
|
||||
if (uncategorised.delete(key)) {
|
||||
channels.push(this.#collection.client.channels.get(key)!);
|
||||
}
|
||||
}
|
||||
|
||||
const cat = {
|
||||
...category,
|
||||
channels,
|
||||
};
|
||||
|
||||
if (cat.id === "default") {
|
||||
if (channels.length === 0) continue;
|
||||
|
||||
defaultCategory = cat;
|
||||
}
|
||||
|
||||
elements.push(cat);
|
||||
}
|
||||
}
|
||||
|
||||
if (uncategorised.size > 0) {
|
||||
const channels = [...uncategorised].map(
|
||||
(key) => this.#collection.client.channels.get(key)!
|
||||
);
|
||||
|
||||
if (defaultCategory) {
|
||||
defaultCategory.channels = [...defaultCategory.channels, ...channels];
|
||||
} else {
|
||||
elements.unshift({
|
||||
id: "default",
|
||||
title: "Default",
|
||||
channels,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return elements;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default channel for this server
|
||||
*/
|
||||
get defaultChannel(): Channel | undefined {
|
||||
return this.orderedChannels.find((cat) => cat.channels.length)?.channels[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an ordered array of roles with their IDs attached.
|
||||
* The highest ranking roles will be first followed by lower
|
||||
* ranking roles. This is dictated by the "rank" property
|
||||
* which is smaller for higher priority roles.
|
||||
*/
|
||||
get orderedRoles() {
|
||||
const roles = this.roles;
|
||||
return roles
|
||||
? [...roles.entries()]
|
||||
.map(([id, role]) => ({ id, ...role }))
|
||||
.sort((a, b) => (a.rank || 0) - (b.rank || 0))
|
||||
: [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the server is currently unread
|
||||
* @returns Whether the server is unread
|
||||
*/
|
||||
get unread() {
|
||||
return this.channels.find((channel) => channel.unread);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all message IDs of unread messages
|
||||
* @returns Array of message IDs which are unread
|
||||
*/
|
||||
get mentions() {
|
||||
const arr = this.channels.map((channel) =>
|
||||
Array.from(channel.mentions?.values() ?? [])
|
||||
);
|
||||
|
||||
return ([] as string[]).concat(...arr);
|
||||
}
|
||||
|
||||
/**
|
||||
* URL to the server's icon
|
||||
*/
|
||||
get iconURL() {
|
||||
return this.icon?.createFileURL({ max_side: 256 });
|
||||
}
|
||||
|
||||
/**
|
||||
* URL to the server's animated icon
|
||||
*/
|
||||
get animatedIconURL() {
|
||||
return this.icon?.createFileURL({ max_side: 256 }, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* URL to the server's banner
|
||||
*/
|
||||
get bannerURL() {
|
||||
return this.banner?.createFileURL({ max_side: 256 }, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Own member object for this server
|
||||
*/
|
||||
get member() {
|
||||
return this.#collection.client.serverMembers.getByKey({
|
||||
server: this.id,
|
||||
user: this.#collection.client.user!.id,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Permission the currently authenticated user has against this server
|
||||
*/
|
||||
get permission() {
|
||||
return calculatePermission(this.#collection.client, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether we have a given permission in a server
|
||||
* @param permission Permission Names
|
||||
* @returns Whether we have this permission
|
||||
*/
|
||||
havePermission(...permission: (keyof typeof Permission)[]) {
|
||||
return bitwiseAndEq(
|
||||
this.permission,
|
||||
...permission.map((x) => Permission[x])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a channel
|
||||
* @param data Channel create route data
|
||||
* @returns The newly-created channel
|
||||
*/
|
||||
async createChannel(data: DataCreateChannel) {
|
||||
const channel = await this.#collection.client.api.post(
|
||||
`/servers/${this.id as ""}/channels`,
|
||||
data
|
||||
);
|
||||
|
||||
return this.#collection.client.channels.getOrCreate(channel._id, channel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit a server
|
||||
* @param data Changes
|
||||
*/
|
||||
async edit(data: DataEditServer) {
|
||||
await this.#collection.client.api.patch(`/servers/${this.id as ""}`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete or leave a server
|
||||
* @param leaveSilently Whether to not send a message on leave
|
||||
*/
|
||||
async delete(leaveSilently?: boolean) {
|
||||
await this.#collection.client.api.delete(`/servers/${this.id as ""}`, {
|
||||
leave_silently: leaveSilently,
|
||||
});
|
||||
|
||||
this.#collection.delete(this.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a server as read
|
||||
*/
|
||||
async ack() {
|
||||
await this.#collection.client.api.put(`/servers/${this.id}/ack`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ban user from this server
|
||||
* @param user User
|
||||
* @param options Ban options
|
||||
*/
|
||||
async banUser(
|
||||
user: string | User | ServerMember,
|
||||
options: DataBanCreate = {}
|
||||
) {
|
||||
const userId =
|
||||
user instanceof User
|
||||
? user.id
|
||||
: user instanceof ServerMember
|
||||
? user.id.user
|
||||
: user;
|
||||
|
||||
const ban = await this.#collection.client.api.put(
|
||||
`/servers/${this.id as ""}/bans/${userId as ""}`,
|
||||
options
|
||||
);
|
||||
|
||||
return new ServerBan(this.#collection.client, ban);
|
||||
}
|
||||
|
||||
/**
|
||||
* Kick user from this server
|
||||
* @param user User
|
||||
*/
|
||||
async kickUser(user: string | User | ServerMember) {
|
||||
const userId =
|
||||
user instanceof User
|
||||
? user.id
|
||||
: user instanceof ServerMember
|
||||
? user.id.user
|
||||
: user;
|
||||
|
||||
return await this.#collection.client.api.delete(
|
||||
`/servers/${this.id as ""}/members/${userId}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pardon user's ban
|
||||
* @param user User
|
||||
*/
|
||||
async unbanUser(user: string | User) {
|
||||
const userId = user instanceof User ? user.id : user;
|
||||
return await this.#collection.client.api.delete(
|
||||
`/servers/${this.id as ""}/bans/${userId}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a server's invites
|
||||
* @returns An array of the server's invites
|
||||
*/
|
||||
async fetchInvites() {
|
||||
const invites = await this.#collection.client.api.get(
|
||||
`/servers/${this.id as ""}/invites`
|
||||
);
|
||||
|
||||
return invites.map((invite) =>
|
||||
ChannelInvite.from(this.#collection.client, invite)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a server's bans
|
||||
* @returns An array of the server's bans.
|
||||
*/
|
||||
async fetchBans() {
|
||||
const { users, bans } = await this.#collection.client.api.get(
|
||||
`/servers/${this.id as ""}/bans`
|
||||
);
|
||||
|
||||
users.forEach((user) =>
|
||||
this.#collection.client.users.getOrCreate(user._id, user)
|
||||
);
|
||||
|
||||
return bans.map((ban) => new ServerBan(this.#collection.client, ban));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set role permissions
|
||||
* @param roleId Role Id, set to 'default' to affect all users
|
||||
* @param permissions Permission value
|
||||
*/
|
||||
async setPermissions(roleId = "default", permissions: Override | number) {
|
||||
return await this.#collection.client.api.put(
|
||||
`/servers/${this.id as ""}/permissions/${roleId as ""}`,
|
||||
{ permissions: permissions as Override }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create role
|
||||
* @param name Role name
|
||||
*/
|
||||
async createRole(name: string) {
|
||||
return await this.#collection.client.api.post(
|
||||
`/servers/${this.id as ""}/roles`,
|
||||
{
|
||||
name,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit a role
|
||||
* @param roleId Role ID
|
||||
* @param data Role editing route data
|
||||
*/
|
||||
async editRole(roleId: string, data: DataEditRole) {
|
||||
return await this.#collection.client.api.patch(
|
||||
`/servers/${this.id as ""}/roles/${roleId as ""}`,
|
||||
data
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete role
|
||||
* @param roleId Role ID
|
||||
*/
|
||||
async deleteRole(roleId: string) {
|
||||
return await this.#collection.client.api.delete(
|
||||
`/servers/${this.id as ""}/roles/${roleId as ""}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a server member
|
||||
* @param user User
|
||||
* @returns Server member object
|
||||
*/
|
||||
async fetchMember(user: User | string) {
|
||||
const userId = typeof user === "string" ? user : user.id;
|
||||
const existing = this.#collection.client.serverMembers.getByKey({
|
||||
server: this.id,
|
||||
user: userId,
|
||||
});
|
||||
|
||||
if (existing) return existing;
|
||||
|
||||
const member = await this.#collection.client.api.get(
|
||||
`/servers/${this.id as ""}/members/${userId as ""}`
|
||||
);
|
||||
|
||||
return this.#collection.client.serverMembers.getOrCreate(
|
||||
member._id,
|
||||
member
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimised member fetch route
|
||||
* @param excludeOffline
|
||||
*/
|
||||
async syncMembers(excludeOffline?: boolean) {
|
||||
const data = await this.#collection.client.api.get(
|
||||
`/servers/${this.id as ""}/members`,
|
||||
{ exclude_offline: excludeOffline }
|
||||
);
|
||||
|
||||
if (excludeOffline) {
|
||||
for (let i = 0; i < data.users.length; i++) {
|
||||
const user = data.users[i];
|
||||
if (user.online) {
|
||||
this.#collection.client.users.getOrCreate(user._id, user);
|
||||
this.#collection.client.serverMembers.getOrCreate(
|
||||
data.members[i]._id,
|
||||
data.members[i]
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < data.users.length; i++) {
|
||||
this.#collection.client.users.getOrCreate(
|
||||
data.users[i]._id,
|
||||
data.users[i]
|
||||
);
|
||||
this.#collection.client.serverMembers.getOrCreate(
|
||||
data.members[i]._id,
|
||||
data.members[i]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a server's members
|
||||
* @returns List of the server's members and their user objects
|
||||
*/
|
||||
async fetchMembers() {
|
||||
const data = await this.#collection.client.api.get(
|
||||
`/servers/${this.id as ""}/members`
|
||||
);
|
||||
|
||||
return {
|
||||
members: data.members.map((member) =>
|
||||
this.#collection.client.serverMembers.getOrCreate(member._id, member)
|
||||
),
|
||||
users: data.users.map((user) =>
|
||||
this.#collection.client.users.getOrCreate(user._id, user)
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an emoji on the server
|
||||
* @param autumnId Autumn Id
|
||||
* @param options Options
|
||||
*/
|
||||
async createEmoji(
|
||||
autumnId: string,
|
||||
options: Omit<DataCreateEmoji, "parent">
|
||||
) {
|
||||
const emoji = await this.#collection.client.api.put(
|
||||
`/custom/emoji/${autumnId as ""}`,
|
||||
{
|
||||
parent: {
|
||||
type: "Server",
|
||||
id: this.id,
|
||||
},
|
||||
...options,
|
||||
}
|
||||
);
|
||||
|
||||
return this.#collection.client.emojis.getOrCreate(emoji._id, emoji, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a server's emoji
|
||||
* @returns List of server emoji
|
||||
*/
|
||||
async fetchEmojis() {
|
||||
const emojis = await this.#collection.client.api.get(
|
||||
`/servers/${this.id as ""}/emojis`
|
||||
);
|
||||
|
||||
return emojis.map((emoji) =>
|
||||
this.#collection.client.emojis.getOrCreate(emoji._id, emoji)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { MemberCompositeKey } from "revolt-api";
|
||||
|
||||
import { API, Client } from "..";
|
||||
|
||||
/**
|
||||
* Server Ban
|
||||
*/
|
||||
export class ServerBan {
|
||||
protected client: Client;
|
||||
readonly id: MemberCompositeKey;
|
||||
readonly reason?: string;
|
||||
|
||||
/**
|
||||
* Construct Server Ban
|
||||
* @param client Client
|
||||
* @param data Data
|
||||
*/
|
||||
constructor(client: Client, data: API.ServerBan) {
|
||||
this.client = client;
|
||||
this.id = data._id;
|
||||
this.reason = data.reason!;
|
||||
}
|
||||
|
||||
/**
|
||||
* User
|
||||
*/
|
||||
get user() {
|
||||
return this.client.users.get(this.id.user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Server
|
||||
*/
|
||||
get server() {
|
||||
return this.client.servers.get(this.id.server);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove this server ban
|
||||
*/
|
||||
async pardon() {
|
||||
await this.client.api.delete(
|
||||
`/servers/${this.id.server as ""}/bans/${this.id.user as ""}`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
import type {
|
||||
DataBanCreate,
|
||||
DataMemberEdit,
|
||||
MemberCompositeKey,
|
||||
} from "revolt-api";
|
||||
|
||||
import { ServerMemberCollection } from "../collections";
|
||||
import { bitwiseAndEq, calculatePermission } from "../permissions/calculator";
|
||||
import { Permission } from "../permissions/definitions";
|
||||
|
||||
import { Channel } from "./Channel";
|
||||
import { Server } from "./Server";
|
||||
|
||||
/**
|
||||
* Deterministic conversion of member composite key to string ID
|
||||
* @param key Key
|
||||
* @returns String key
|
||||
*/
|
||||
function key(key: MemberCompositeKey) {
|
||||
return key.server + key.user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Server Member Class
|
||||
*/
|
||||
export class ServerMember {
|
||||
readonly #collection: ServerMemberCollection;
|
||||
readonly id: MemberCompositeKey;
|
||||
|
||||
/**
|
||||
* Construct Server Member
|
||||
* @param collection Collection
|
||||
* @param id Id
|
||||
*/
|
||||
constructor(collection: ServerMemberCollection, id: MemberCompositeKey) {
|
||||
this.#collection = collection;
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert to string
|
||||
* @returns String
|
||||
*/
|
||||
toString() {
|
||||
return `<@${this.id.user}>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Server this member belongs to
|
||||
*/
|
||||
get server() {
|
||||
return this.#collection.client.servers.get(this.id.server);
|
||||
}
|
||||
|
||||
/**
|
||||
* User corresponding to this member
|
||||
*/
|
||||
get user() {
|
||||
return this.#collection.client.users.get(this.id.user);
|
||||
}
|
||||
|
||||
/**
|
||||
* When this user joined the server
|
||||
*/
|
||||
get joinedAt() {
|
||||
return this.#collection.getUnderlyingObject(key(this.id)).joinedAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Nickname
|
||||
*/
|
||||
get nickname() {
|
||||
return this.#collection.getUnderlyingObject(key(this.id)).nickname;
|
||||
}
|
||||
|
||||
/**
|
||||
* Avatar
|
||||
*/
|
||||
get avatar() {
|
||||
return this.#collection.getUnderlyingObject(key(this.id)).avatar;
|
||||
}
|
||||
|
||||
/**
|
||||
* List of role IDs
|
||||
*/
|
||||
get roles() {
|
||||
return this.#collection.getUnderlyingObject(key(this.id)).roles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Time at which timeout expires
|
||||
*/
|
||||
get timeout() {
|
||||
return this.#collection.getUnderlyingObject(key(this.id)).timeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ordered list of roles for this member, from lowest to highest priority.
|
||||
*/
|
||||
get orderedRoles() {
|
||||
const server = this.server!;
|
||||
return (
|
||||
this.roles
|
||||
?.map((id) => ({
|
||||
id,
|
||||
...server.roles?.get(id),
|
||||
}))
|
||||
.sort((a, b) => b.rank! - a.rank!) ?? []
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Member's currently hoisted role.
|
||||
*/
|
||||
get hoistedRole() {
|
||||
const roles = this.orderedRoles.filter((x) => x.hoist);
|
||||
if (roles.length > 0) {
|
||||
return roles[roles.length - 1];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Member's current role colour.
|
||||
*/
|
||||
get roleColour() {
|
||||
const roles = this.orderedRoles.filter((x) => x.colour);
|
||||
if (roles.length > 0) {
|
||||
return roles[roles.length - 1].colour;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Member's ranking
|
||||
* Smaller values are ranked as higher priority
|
||||
*/
|
||||
get ranking() {
|
||||
if (this.id.user === this.server?.ownerId) {
|
||||
return -Infinity;
|
||||
}
|
||||
|
||||
const roles = this.orderedRoles;
|
||||
if (roles.length > 0) {
|
||||
return roles[roles.length - 1].rank!;
|
||||
} else {
|
||||
return Infinity;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the permissions that this member has against a certain object
|
||||
* @param target Target object to check permissions against
|
||||
* @returns Permissions that this member has
|
||||
*/
|
||||
getPermissions(target: Server | Channel) {
|
||||
return calculatePermission(this.#collection.client, target, {
|
||||
member: this,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a member has a certain permission against a certain object
|
||||
* @param target Target object to check permissions against
|
||||
* @param permission Permission names to check for
|
||||
* @returns Whether the member has this permission
|
||||
*/
|
||||
hasPermission(
|
||||
target: Server | Channel,
|
||||
...permission: (keyof typeof Permission)[]
|
||||
) {
|
||||
return bitwiseAndEq(
|
||||
this.getPermissions(target),
|
||||
...permission.map((x) => Permission[x])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the target member has a higher rank than this member.
|
||||
* @param target The member to compare against
|
||||
* @returns Whether this member is inferior to the target
|
||||
*/
|
||||
inferiorTo(target: ServerMember) {
|
||||
return target.ranking < this.ranking;
|
||||
}
|
||||
|
||||
/**
|
||||
* URL to the member's avatar
|
||||
*/
|
||||
get avatarURL() {
|
||||
return (
|
||||
this.avatar?.createFileURL({ max_side: 256 }) ?? this.user?.avatarURL
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* URL to the member's animated avatar
|
||||
*/
|
||||
get animatedAvatarURL() {
|
||||
return (
|
||||
this.avatar?.createFileURL({ max_side: 256 }, true) ??
|
||||
this.user?.animatedAvatarURL
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit a member
|
||||
* @param data Changes
|
||||
*/
|
||||
async edit(data: DataMemberEdit) {
|
||||
await this.#collection.client.api.patch(
|
||||
`/servers/${this.id.server as ""}/members/${this.id.user as ""}`,
|
||||
data
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ban this member from the server
|
||||
* @param options Ban options
|
||||
*/
|
||||
async ban(options: DataBanCreate) {
|
||||
this.server?.banUser(this, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Kick this member from the server
|
||||
*/
|
||||
async kick() {
|
||||
this.server?.kickUser(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
import { API, Client } from "..";
|
||||
|
||||
/**
|
||||
* System Message
|
||||
*/
|
||||
export abstract class SystemMessage {
|
||||
protected client?: Client;
|
||||
readonly type: API.SystemMessage["type"];
|
||||
|
||||
/**
|
||||
* Construct System Message
|
||||
* @param client Client
|
||||
* @param type Type
|
||||
*/
|
||||
constructor(client: Client, type: API.SystemMessage["type"]) {
|
||||
this.client = client;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an System Message from an API System Message
|
||||
* @param client Client
|
||||
* @param embed Data
|
||||
* @returns System Message
|
||||
*/
|
||||
static from(client: Client, message: API.SystemMessage): SystemMessage {
|
||||
switch (message.type) {
|
||||
case "text":
|
||||
return new TextSystemMessage(client, message);
|
||||
case "user_added":
|
||||
case "user_remove":
|
||||
return new UserModeratedSystemMessage(client, message);
|
||||
case "user_joined":
|
||||
case "user_left":
|
||||
case "user_kicked":
|
||||
case "user_banned":
|
||||
return new UserSystemMessage(client, message);
|
||||
case "channel_renamed":
|
||||
return new ChannelRenamedSystemMessage(client, message);
|
||||
case "channel_description_changed":
|
||||
case "channel_icon_changed":
|
||||
return new ChannelEditSystemMessage(client, message);
|
||||
case "channel_ownership_changed":
|
||||
return new ChannelOwnershipChangeSystemMessage(client, message);
|
||||
default:
|
||||
return new TextSystemMessage(client, {
|
||||
type: "text",
|
||||
content: `${(message as { type: string }).type} is not supported.`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Text System Message
|
||||
*/
|
||||
export class TextSystemMessage extends SystemMessage {
|
||||
readonly content: string;
|
||||
|
||||
/**
|
||||
* Construct System Message
|
||||
* @param client Client
|
||||
* @param systemMessage System Message
|
||||
*/
|
||||
constructor(
|
||||
client: Client,
|
||||
systemMessage: API.SystemMessage & { type: "text" }
|
||||
) {
|
||||
super(client, systemMessage.type);
|
||||
this.content = systemMessage.content;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* User System Message
|
||||
*/
|
||||
export class UserSystemMessage extends SystemMessage {
|
||||
readonly userId: string;
|
||||
|
||||
/**
|
||||
* Construct System Message
|
||||
* @param client Client
|
||||
* @param systemMessage System Message
|
||||
*/
|
||||
constructor(
|
||||
client: Client,
|
||||
systemMessage: API.SystemMessage & {
|
||||
type:
|
||||
| "user_added"
|
||||
| "user_remove"
|
||||
| "user_joined"
|
||||
| "user_left"
|
||||
| "user_kicked"
|
||||
| "user_banned";
|
||||
}
|
||||
) {
|
||||
super(client, systemMessage.type);
|
||||
this.userId = systemMessage.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* User this message concerns
|
||||
*/
|
||||
get user() {
|
||||
return this.client!.users.get(this.userId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* User Moderated System Message
|
||||
*/
|
||||
export class UserModeratedSystemMessage extends UserSystemMessage {
|
||||
readonly byId: string;
|
||||
|
||||
/**
|
||||
* Construct System Message
|
||||
* @param client Client
|
||||
* @param systemMessage System Message
|
||||
*/
|
||||
constructor(
|
||||
client: Client,
|
||||
systemMessage: API.SystemMessage & {
|
||||
type: "user_added" | "user_remove";
|
||||
}
|
||||
) {
|
||||
super(client, systemMessage);
|
||||
this.byId = systemMessage.by;
|
||||
}
|
||||
|
||||
/**
|
||||
* User this action was performed by
|
||||
*/
|
||||
get by() {
|
||||
console.info("deez!", this.byId);
|
||||
return this.client!.users.get(this.byId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Channel Edit System Message
|
||||
*/
|
||||
export class ChannelEditSystemMessage extends SystemMessage {
|
||||
readonly byId: string;
|
||||
|
||||
/**
|
||||
* Construct System Message
|
||||
* @param client Client
|
||||
* @param systemMessage System Message
|
||||
*/
|
||||
constructor(
|
||||
client: Client,
|
||||
systemMessage: API.SystemMessage & {
|
||||
type:
|
||||
| "channel_renamed"
|
||||
| "channel_description_changed"
|
||||
| "channel_icon_changed";
|
||||
}
|
||||
) {
|
||||
super(client, systemMessage.type);
|
||||
this.byId = systemMessage.by;
|
||||
}
|
||||
|
||||
/**
|
||||
* User this action was performed by
|
||||
*/
|
||||
get by() {
|
||||
return this.client!.users.get(this.byId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Channel Renamed System Message
|
||||
*/
|
||||
export class ChannelRenamedSystemMessage extends ChannelEditSystemMessage {
|
||||
readonly name: string;
|
||||
|
||||
/**
|
||||
* Construct System Message
|
||||
* @param client Client
|
||||
* @param systemMessage System Message
|
||||
*/
|
||||
constructor(
|
||||
client: Client,
|
||||
systemMessage: API.SystemMessage & {
|
||||
type: "channel_renamed";
|
||||
}
|
||||
) {
|
||||
super(client, systemMessage);
|
||||
this.name = systemMessage.name;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Channel Ownership Change System Message
|
||||
*/
|
||||
export class ChannelOwnershipChangeSystemMessage extends SystemMessage {
|
||||
readonly fromId: string;
|
||||
readonly toId: string;
|
||||
|
||||
/**
|
||||
* Construct System Message
|
||||
* @param client Client
|
||||
* @param systemMessage System Message
|
||||
*/
|
||||
constructor(
|
||||
client: Client,
|
||||
systemMessage: API.SystemMessage & {
|
||||
type: "channel_ownership_changed";
|
||||
}
|
||||
) {
|
||||
super(client, systemMessage.type);
|
||||
this.fromId = systemMessage.from;
|
||||
this.toId = systemMessage.to;
|
||||
}
|
||||
|
||||
/**
|
||||
* User giving away channel ownership
|
||||
*/
|
||||
get from() {
|
||||
return this.client!.users.get(this.fromId);
|
||||
}
|
||||
|
||||
/**
|
||||
* User receiving channel ownership
|
||||
*/
|
||||
get to() {
|
||||
return this.client!.users.get(this.toId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
import { DataEditUser } from "revolt-api";
|
||||
import { decodeTime } from "ulid";
|
||||
|
||||
import { UserCollection } from "../collections";
|
||||
import { U32_MAX, UserPermission } from "../permissions/definitions";
|
||||
|
||||
/**
|
||||
* User Class
|
||||
*/
|
||||
export class User {
|
||||
readonly #collection: UserCollection;
|
||||
readonly id: string;
|
||||
|
||||
/**
|
||||
* Construct User
|
||||
* @param collection Collection
|
||||
* @param id Id
|
||||
*/
|
||||
constructor(collection: UserCollection, id: string) {
|
||||
this.#collection = collection;
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write to string as a user mention
|
||||
* @returns Formatted String
|
||||
*/
|
||||
toString() {
|
||||
return `<@${this.id}>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Time when this user created their account
|
||||
*/
|
||||
get createdAt() {
|
||||
return new Date(decodeTime(this.id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Username
|
||||
*/
|
||||
get username() {
|
||||
return this.#collection.getUnderlyingObject(this.id).username;
|
||||
}
|
||||
|
||||
/**
|
||||
* Avatar
|
||||
*/
|
||||
get avatar() {
|
||||
return this.#collection.getUnderlyingObject(this.id).avatar;
|
||||
}
|
||||
|
||||
/**
|
||||
* Badges
|
||||
*/
|
||||
get badges() {
|
||||
return this.#collection.getUnderlyingObject(this.id).badges;
|
||||
}
|
||||
|
||||
/**
|
||||
* User Status
|
||||
*/
|
||||
get status() {
|
||||
return this.#collection.getUnderlyingObject(this.id).status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship with user
|
||||
*/
|
||||
get relationship() {
|
||||
return this.#collection.getUnderlyingObject(this.id).relationship;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the user is online
|
||||
*/
|
||||
get online() {
|
||||
return this.#collection.getUnderlyingObject(this.id).online;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the user is privileged
|
||||
*/
|
||||
get privileged() {
|
||||
return this.#collection.getUnderlyingObject(this.id).privileged;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flags
|
||||
*/
|
||||
get flags() {
|
||||
return this.#collection.getUnderlyingObject(this.id).flags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bot information
|
||||
*/
|
||||
get bot() {
|
||||
return this.#collection.getUnderlyingObject(this.id).bot;
|
||||
}
|
||||
|
||||
/**
|
||||
* URL to the user's default avatar
|
||||
*/
|
||||
get defaultAvatarURL() {
|
||||
return `${this.#collection.client.options.baseURL}/users/${
|
||||
this.id
|
||||
}/default_avatar`;
|
||||
}
|
||||
|
||||
/**
|
||||
* URL to the user's avatar
|
||||
*/
|
||||
get avatarURL() {
|
||||
return (
|
||||
this.avatar?.createFileURL({ max_side: 256 }) ?? this.defaultAvatarURL
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* URL to the user's animated avatar
|
||||
*/
|
||||
get animatedAvatarURL() {
|
||||
return (
|
||||
this.avatar?.createFileURL({ max_side: 256 }, true) ??
|
||||
this.defaultAvatarURL
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Permissions against this user
|
||||
*/
|
||||
get permission() {
|
||||
let permissions = 0;
|
||||
switch (this.relationship) {
|
||||
case "Friend":
|
||||
case "User":
|
||||
return U32_MAX;
|
||||
case "Blocked":
|
||||
case "BlockedOther":
|
||||
return UserPermission.Access;
|
||||
case "Incoming":
|
||||
case "Outgoing":
|
||||
permissions = UserPermission.Access;
|
||||
}
|
||||
|
||||
if (
|
||||
this.#collection.client.channels.find(
|
||||
(channel) =>
|
||||
(channel.type === "Group" || channel.type === "DirectMessage") &&
|
||||
channel.recipientIds.has(this.id)
|
||||
) ||
|
||||
this.#collection.client.serverMembers.find(
|
||||
(member) => member.id.user === this.id
|
||||
)
|
||||
) {
|
||||
if (this.#collection.client.user?.bot || this.bot) {
|
||||
permissions |= UserPermission.SendMessage;
|
||||
}
|
||||
|
||||
permissions |= UserPermission.Access | UserPermission.ViewProfile;
|
||||
}
|
||||
|
||||
return permissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit the user
|
||||
* @param data Changes
|
||||
*/
|
||||
async edit(data: DataEditUser) {
|
||||
await this.#collection.client.api.patch(
|
||||
`/users/${
|
||||
this.id === this.#collection.client.user?.id ? "@me" : this.id
|
||||
}`,
|
||||
data
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the username of the current user
|
||||
* @param username New username
|
||||
* @param password Current password
|
||||
*/
|
||||
async changeUsername(username: string, password: string) {
|
||||
return await this.#collection.client.api.patch("/users/@me/username", {
|
||||
username,
|
||||
password,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a DM with a user
|
||||
* @returns DM Channel
|
||||
*/
|
||||
async openDM() {
|
||||
let dm = [...this.#collection.client.channels.values()].find(
|
||||
(x) => x.type === "DirectMessage" && x.recipient == this
|
||||
);
|
||||
|
||||
if (dm) {
|
||||
if (!dm.active) {
|
||||
this.#collection.client.channels.updateUnderlyingObject(
|
||||
dm.id,
|
||||
"active",
|
||||
true
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const data = await this.#collection.client.api.get(
|
||||
`/users/${this.id as ""}/dm`
|
||||
);
|
||||
|
||||
dm = this.#collection.client.channels.getOrCreate(data._id, data)!;
|
||||
}
|
||||
|
||||
return dm;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a friend request to a user
|
||||
*/
|
||||
async addFriend() {
|
||||
const user = await this.#collection.client.api.post(`/users/friend`, {
|
||||
username: this.username,
|
||||
});
|
||||
|
||||
return this.#collection.getOrCreate(user._id, user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a user from the friend list
|
||||
*/
|
||||
async removeFriend() {
|
||||
await this.#collection.client.api.delete(`/users/${this.id as ""}/friend`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Block a user
|
||||
*/
|
||||
async blockUser() {
|
||||
await this.#collection.client.api.put(`/users/${this.id as ""}/block`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unblock a user
|
||||
*/
|
||||
async unblockUser() {
|
||||
await this.#collection.client.api.delete(`/users/${this.id as ""}/block`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the profile of a user
|
||||
* @returns The profile of the user
|
||||
*/
|
||||
async fetchProfile() {
|
||||
return await this.#collection.client.api.get(
|
||||
`/users/${this.id as ""}/profile`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the mutual connections of the current user and a target user
|
||||
* @returns The mutual connections of the current user and a target user
|
||||
*/
|
||||
async fetchMutual() {
|
||||
return await this.#collection.client.api.get(
|
||||
`/users/${this.id as ""}/mutual`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
export * from "./Bot";
|
||||
export * from "./Channel";
|
||||
export * from "./ChannelUnread";
|
||||
export * from "./Emoji";
|
||||
export * from "./File";
|
||||
export * from "./Invite";
|
||||
export * from "./Message";
|
||||
export * from "./MessageEmbed";
|
||||
export * from "./PublicBot";
|
||||
export * from "./PublicInvite";
|
||||
export * from "./Server";
|
||||
export * from "./ServerBan";
|
||||
export * from "./ServerMember";
|
||||
export * from "./SystemMessage";
|
||||
export * from "./User";
|
||||
@@ -0,0 +1,73 @@
|
||||
import { OwnedBotsResponse } from "revolt-api";
|
||||
|
||||
import { API, Bot, PublicBot } from "..";
|
||||
import { HydratedBot } from "../hydration/bot";
|
||||
|
||||
import { ClassCollection } from ".";
|
||||
|
||||
/**
|
||||
* Collection of Bots
|
||||
*/
|
||||
export class BotCollection extends ClassCollection<Bot, HydratedBot> {
|
||||
/**
|
||||
* Fetch bot by ID
|
||||
* @param id Id
|
||||
* @returns Bot
|
||||
*/
|
||||
async fetch(id: string): Promise<Bot> {
|
||||
const bot = this.get(id);
|
||||
if (bot) return bot;
|
||||
const data = await this.client.api.get(`/bots/${id as ""}`);
|
||||
this.client.users.getOrCreate(data.user._id, data.user);
|
||||
return this.getOrCreate(data.bot._id, data.bot);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch owned bots
|
||||
* @returns List of bots
|
||||
*/
|
||||
async fetchOwned(): Promise<Bot[]> {
|
||||
const data = (await this.client.api.get("/bots/@me")) as OwnedBotsResponse;
|
||||
data.users.forEach((user) => this.client.users.getOrCreate(user._id, user));
|
||||
return data.bots.map((bot) => this.getOrCreate(bot._id, bot));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch public bot by ID
|
||||
* @param id Id
|
||||
* @returns Public Bot
|
||||
*/
|
||||
async fetchPublic(id: string): Promise<PublicBot> {
|
||||
const data = await this.client.api.get(`/bots/${id as ""}/invite`);
|
||||
return new PublicBot(this.client, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create
|
||||
* @param id Id
|
||||
* @param data Data
|
||||
* @returns Bot
|
||||
*/
|
||||
getOrCreate(id: string, data: API.Bot) {
|
||||
if (this.has(id)) {
|
||||
return this.get(id)!;
|
||||
} else {
|
||||
const instance = new Bot(this, id);
|
||||
this.create(id, "bot", instance, this.client, data);
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a bot
|
||||
* @param name Bot name
|
||||
* @returns The newly-created bot
|
||||
*/
|
||||
async createBot(name: string) {
|
||||
const bot = await this.client.api.post(`/bots/create`, {
|
||||
name,
|
||||
});
|
||||
|
||||
return this.getOrCreate(bot._id, bot);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import { API, Channel, User } from "..";
|
||||
import { HydratedChannel } from "../hydration";
|
||||
|
||||
import { ClassCollection } from ".";
|
||||
|
||||
/**
|
||||
* Collection of Channels
|
||||
*/
|
||||
export class ChannelCollection extends ClassCollection<
|
||||
Channel,
|
||||
HydratedChannel
|
||||
> {
|
||||
/**
|
||||
* Delete an object
|
||||
* @param id Id
|
||||
*/
|
||||
override delete(id: string): void {
|
||||
const channel = this.get(id);
|
||||
channel?.server?.channelIds.delete(id);
|
||||
super.delete(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch channel by ID
|
||||
* @param id Id
|
||||
* @returns Channel
|
||||
*/
|
||||
async fetch(id: string): Promise<Channel> {
|
||||
const channel = this.get(id);
|
||||
if (channel) return channel;
|
||||
const data = await this.client.api.get(`/channels/${id as ""}`);
|
||||
return this.getOrCreate(data._id, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create
|
||||
* @param id Id
|
||||
* @param data Data
|
||||
* @param isNew Whether this object is new
|
||||
*/
|
||||
getOrCreate(id: string, data: API.Channel, isNew = false) {
|
||||
if (this.has(id)) {
|
||||
return this.get(id)!;
|
||||
} else {
|
||||
const instance = new Channel(this, id);
|
||||
this.create(id, "channel", instance, this.client, data);
|
||||
isNew && this.client.emit("channelCreate", instance);
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or return partial
|
||||
* @param id Id
|
||||
*/
|
||||
getOrPartial(id: string) {
|
||||
if (this.has(id)) {
|
||||
return this.get(id)!;
|
||||
} else if (this.client.options.partials) {
|
||||
const instance = new Channel(this, id);
|
||||
this.create(id, "channel", instance, this.client, {
|
||||
id,
|
||||
partial: true,
|
||||
});
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a group
|
||||
* @param name Group name
|
||||
* @param users Users to add
|
||||
* @returns The newly-created group
|
||||
*/
|
||||
async createGroup(name: string, users: (User | string)[]) {
|
||||
const group = await this.client.api.post(`/channels/create`, {
|
||||
name,
|
||||
users: users.map((user) => (user instanceof User ? user.id : user)),
|
||||
});
|
||||
|
||||
return this.getOrCreate(group._id, group, true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { API } from "..";
|
||||
import { ChannelUnread } from "../classes/ChannelUnread";
|
||||
import { HydratedChannelUnread } from "../hydration";
|
||||
|
||||
import { ClassCollection } from ".";
|
||||
|
||||
/**
|
||||
* Collection of Channel Unreads
|
||||
*/
|
||||
export class ChannelUnreadCollection extends ClassCollection<
|
||||
ChannelUnread,
|
||||
HydratedChannelUnread
|
||||
> {
|
||||
/**
|
||||
* Load unread information from server
|
||||
*/
|
||||
async sync() {
|
||||
const unreads = await this.client.api.get("/sync/unreads");
|
||||
this.reset();
|
||||
for (const unread of unreads) {
|
||||
this.getOrCreate(unread._id.channel, unread);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all unread data
|
||||
*/
|
||||
reset() {
|
||||
this.updateUnderlyingObject({});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create
|
||||
* @param id Id
|
||||
* @param data Data
|
||||
*/
|
||||
getOrCreate(id: string, data: API.ChannelUnread) {
|
||||
if (this.has(id)) {
|
||||
return this.get(id)!;
|
||||
} else {
|
||||
const instance = new ChannelUnread(this, id);
|
||||
this.create(id, "channelUnread", instance, this.client, data);
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
import { SetStoreFunction } from "solid-js/store";
|
||||
|
||||
import { ReactiveMap } from "@solid-primitives/map";
|
||||
|
||||
import { Client } from "..";
|
||||
import { Hydrators } from "../hydration";
|
||||
import { ObjectStorage } from "../storage/ObjectStorage";
|
||||
|
||||
/**
|
||||
* Abstract Collection type
|
||||
*/
|
||||
export abstract class Collection<T> {
|
||||
/**
|
||||
* Get an existing object
|
||||
* @param id Id
|
||||
* @returns Object
|
||||
*/
|
||||
abstract get(id: string): T | undefined;
|
||||
|
||||
/**
|
||||
* Check whether an id exists in the Collection
|
||||
* @param id Id
|
||||
* @returns Whether it exists
|
||||
*/
|
||||
abstract has(id: string): boolean;
|
||||
|
||||
/**
|
||||
* Delete an object
|
||||
* @param id Id
|
||||
*/
|
||||
abstract delete(id: string): void;
|
||||
|
||||
/**
|
||||
* Number of stored objects
|
||||
* @returns Size
|
||||
*/
|
||||
abstract size(): number;
|
||||
|
||||
/**
|
||||
* Iterable of keys in the map
|
||||
* @returns Iterable
|
||||
*/
|
||||
abstract keys(): IterableIterator<string>;
|
||||
|
||||
/**
|
||||
* Iterable of values in the map
|
||||
* @returns Iterable
|
||||
*/
|
||||
abstract values(): IterableIterator<T>;
|
||||
|
||||
/**
|
||||
* Iterable of key, value pairs in the map
|
||||
* @returns Iterable
|
||||
*/
|
||||
abstract entries(): IterableIterator<[string, T]>;
|
||||
|
||||
/**
|
||||
* Execute a provided function over each key, value pair in the map
|
||||
* @param cb Callback for each pair
|
||||
*/
|
||||
abstract forEach(
|
||||
cb: (value: T, key: string, map: ReactiveMap<string, T>) => void
|
||||
): void;
|
||||
|
||||
/**
|
||||
* List of values in the map
|
||||
* @returns List
|
||||
*/
|
||||
toList() {
|
||||
return [...this.values()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the collection by a given predicate
|
||||
* @param predicate Predicate to satisfy
|
||||
*/
|
||||
filter(predicate: (value: T, key: string) => boolean): T[] {
|
||||
const list: T[] = [];
|
||||
for (const [key, value] of this.entries()) {
|
||||
if (predicate(value, key)) {
|
||||
list.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map the collection using a given callback
|
||||
* @param cb Callback
|
||||
*/
|
||||
map<O>(cb: (value: T, key: string) => O): O[] {
|
||||
const list: O[] = [];
|
||||
for (const [key, value] of this.entries()) {
|
||||
list.push(cb(value, key));
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find some value based on a predicate
|
||||
* @param predicate Predicate to satisfy
|
||||
*/
|
||||
find(predicate: (value: T, key: string) => boolean): T | undefined {
|
||||
for (const [key, value] of this.entries()) {
|
||||
if (predicate(value, key)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Collection backed by a Solid.js Store
|
||||
*/
|
||||
export abstract class StoreCollection<T, V> extends Collection<T> {
|
||||
#storage = new ObjectStorage<V>();
|
||||
#objects = new ReactiveMap<string, T>();
|
||||
readonly getUnderlyingObject: (id: string) => V;
|
||||
readonly updateUnderlyingObject: SetStoreFunction<Record<string, V>>;
|
||||
|
||||
/**
|
||||
* Construct store backed collection
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
this.getUnderlyingObject = this.#storage.get;
|
||||
this.updateUnderlyingObject = this.#storage.set;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an existing object
|
||||
* @param id Id
|
||||
* @returns Object
|
||||
*/
|
||||
get(id: string): T | undefined {
|
||||
return this.#objects.get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether an id exists in the Collection
|
||||
* @param id Id
|
||||
* @returns Whether it exists
|
||||
*/
|
||||
has(id: string) {
|
||||
return this.#objects.has(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an object
|
||||
* @param id Id
|
||||
*/
|
||||
delete(id: string): void {
|
||||
this.#objects.delete(id);
|
||||
this.updateUnderlyingObject(id, undefined as never);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance of an object
|
||||
* @param id Id
|
||||
* @param type Type
|
||||
* @param instance Instance
|
||||
* @param context Context
|
||||
* @param data Data
|
||||
*/
|
||||
create(
|
||||
id: string,
|
||||
type: keyof Hydrators,
|
||||
instance: T,
|
||||
context: unknown,
|
||||
data?: unknown
|
||||
) {
|
||||
this.#storage.hydrate(id, type, context, data);
|
||||
this.#objects.set(id, instance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether an object is partially defined
|
||||
* @param id Id
|
||||
* @returns Whether it is a partial
|
||||
*/
|
||||
isPartial(id: string): boolean {
|
||||
return !!(this.getUnderlyingObject(id) as { partial: boolean }).partial;
|
||||
}
|
||||
|
||||
/**
|
||||
* Number of stored objects
|
||||
* @returns Size
|
||||
*/
|
||||
size() {
|
||||
return this.#objects.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterable of keys in the map
|
||||
* @returns Iterable
|
||||
*/
|
||||
keys() {
|
||||
return this.#objects.keys();
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterable of values in the map
|
||||
* @returns Iterable
|
||||
*/
|
||||
values() {
|
||||
return this.#objects.values();
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterable of key, value pairs in the map
|
||||
* @returns Iterable
|
||||
*/
|
||||
entries() {
|
||||
return this.#objects.entries();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a provided function over each key, value pair in the map
|
||||
* @param cb Callback for each pair
|
||||
* @returns Iterable
|
||||
*/
|
||||
forEach(cb: (value: T, key: string, map: ReactiveMap<string, T>) => void) {
|
||||
return this.#objects.forEach(cb);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic class collection backed by store
|
||||
*/
|
||||
export class ClassCollection<T, V> extends StoreCollection<T, V> {
|
||||
readonly client: Client;
|
||||
|
||||
/**
|
||||
* Create generic class collection
|
||||
* @param client Client
|
||||
*/
|
||||
constructor(client: Client) {
|
||||
super();
|
||||
this.client = client;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { API, Emoji } from "..";
|
||||
import { HydratedEmoji } from "../hydration";
|
||||
|
||||
import { ClassCollection } from ".";
|
||||
|
||||
/**
|
||||
* Collection of Emoji
|
||||
*/
|
||||
export class EmojiCollection extends ClassCollection<Emoji, HydratedEmoji> {
|
||||
/**
|
||||
* Fetch emoji by ID
|
||||
* @param id Id
|
||||
* @returns Emoji
|
||||
*/
|
||||
async fetch(id: string): Promise<Emoji> {
|
||||
const emoji = this.get(id);
|
||||
if (emoji) return emoji;
|
||||
const data = await this.client.api.get(`/custom/emoji/${id as ""}`);
|
||||
return this.getOrCreate(data._id, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create
|
||||
* @param id Id
|
||||
* @param data Data
|
||||
* @param isNew Whether this object is new
|
||||
*/
|
||||
getOrCreate(id: string, data: API.Emoji, isNew = false) {
|
||||
if (this.has(id)) {
|
||||
return this.get(id)!;
|
||||
} else {
|
||||
const instance = new Emoji(this, id);
|
||||
this.create(id, "emoji", instance, this.client, data);
|
||||
isNew && this.client.emit("emojiCreate", instance);
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or return partial
|
||||
* @param id Id
|
||||
*/
|
||||
getOrPartial(id: string) {
|
||||
if (this.has(id)) {
|
||||
return this.get(id)!;
|
||||
} else if (this.client.options.partials) {
|
||||
const instance = new Emoji(this, id);
|
||||
this.create(id, "emoji", instance, this.client, {
|
||||
id,
|
||||
});
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { API, Message } from "..";
|
||||
import { HydratedMessage } from "../hydration";
|
||||
|
||||
import { ClassCollection } from ".";
|
||||
|
||||
/**
|
||||
* Collection of Messages
|
||||
*/
|
||||
export class MessageCollection extends ClassCollection<
|
||||
Message,
|
||||
HydratedMessage
|
||||
> {
|
||||
/**
|
||||
* Fetch message by Id
|
||||
* @param channelId Channel Id
|
||||
* @param messageId Message Id
|
||||
* @returns Message
|
||||
*/
|
||||
async fetch(channelId: string, messageId: string): Promise<Message> {
|
||||
const message = this.get(messageId);
|
||||
if (message) return message;
|
||||
|
||||
const data = await this.client.api.get(
|
||||
`/channels/${channelId as ""}/messages/${messageId as ""}`
|
||||
);
|
||||
|
||||
return this.getOrCreate(data._id, data, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create
|
||||
* @param id Id
|
||||
* @param data Data
|
||||
* @param isNew Whether this object is new
|
||||
*/
|
||||
getOrCreate(id: string, data: API.Message, isNew = false) {
|
||||
if (this.has(id)) {
|
||||
return this.get(id)!;
|
||||
} else {
|
||||
const instance = new Message(this, id);
|
||||
this.create(id, "message", instance, this.client, data);
|
||||
isNew && this.client.emit("messageCreate", instance);
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or return partial
|
||||
* @param id Id
|
||||
*/
|
||||
getOrPartial(id: string) {
|
||||
if (this.has(id)) {
|
||||
return this.get(id)!;
|
||||
} else if (this.client.options.partials) {
|
||||
const instance = new Message(this, id);
|
||||
this.create(id, "message", instance, this.client, {
|
||||
id,
|
||||
partial: true,
|
||||
});
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Globally fetch messages
|
||||
* @requires Admin
|
||||
* @param query Message query
|
||||
*/
|
||||
async queryMessages(query: API.MessageQuery) {
|
||||
return this.client.api.post("/admin/messages", query);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { DataCreateServer } from "revolt-api";
|
||||
|
||||
import { API, Server } from "..";
|
||||
import { HydratedServer } from "../hydration";
|
||||
|
||||
import { ClassCollection } from ".";
|
||||
|
||||
/**
|
||||
* Collection of Servers
|
||||
*/
|
||||
export class ServerCollection extends ClassCollection<Server, HydratedServer> {
|
||||
/**
|
||||
* Fetch server by ID
|
||||
*
|
||||
* This will not fetch channels!
|
||||
* @param id Id
|
||||
* @returns Server
|
||||
*/
|
||||
async fetch(id: string): Promise<Server> {
|
||||
const server = this.get(id);
|
||||
if (server) return server;
|
||||
const data = await this.client.api.get(`/servers/${id as ""}`);
|
||||
return this.getOrCreate(data._id, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create
|
||||
* @param id Id
|
||||
* @param data Data
|
||||
* @param isNew Whether this object is new
|
||||
*/
|
||||
getOrCreate(id: string, data: API.Server, isNew = false) {
|
||||
if (this.has(id)) {
|
||||
return this.get(id)!;
|
||||
} else {
|
||||
const instance = new Server(this, id);
|
||||
this.create(id, "server", instance, this.client, data);
|
||||
isNew && this.client.emit("serverCreate", instance);
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or return partial
|
||||
* @param id Id
|
||||
*/
|
||||
getOrPartial(id: string) {
|
||||
if (this.has(id)) {
|
||||
return this.get(id)!;
|
||||
} else if (this.client.options.partials) {
|
||||
const instance = new Server(this, id);
|
||||
this.create(id, "server", instance, this.client, {
|
||||
id,
|
||||
partial: true,
|
||||
});
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a server
|
||||
* @param data Server options
|
||||
* @returns The newly-created server
|
||||
*/
|
||||
async createServer(data: DataCreateServer) {
|
||||
const { server, channels } = await this.client.api.post(
|
||||
`/servers/create`,
|
||||
data
|
||||
);
|
||||
|
||||
for (const channel of channels) {
|
||||
this.client.channels.getOrCreate(channel._id, channel);
|
||||
}
|
||||
|
||||
return this.getOrCreate(server._id, server, true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import { API, ServerMember } from "..";
|
||||
import { HydratedServerMember } from "../hydration";
|
||||
|
||||
import { ClassCollection } from ".";
|
||||
|
||||
/**
|
||||
* Collection of Server Members
|
||||
*/
|
||||
export class ServerMemberCollection extends ClassCollection<
|
||||
ServerMember,
|
||||
HydratedServerMember
|
||||
> {
|
||||
/**
|
||||
* Check if member exists by composite key
|
||||
* @param id Id
|
||||
* @returns Whether it exists
|
||||
*/
|
||||
hasByKey(id: API.MemberCompositeKey) {
|
||||
return super.has(id.server + id.user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get member by composite key
|
||||
* @param id Id
|
||||
* @returns Member
|
||||
*/
|
||||
getByKey(id: API.MemberCompositeKey) {
|
||||
return super.get(id.server + id.user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch server member by Id
|
||||
* @param serverId Server Id
|
||||
* @param userId User Id
|
||||
* @returns Message
|
||||
*/
|
||||
async fetch(serverId: string, userId: string): Promise<ServerMember> {
|
||||
const member = this.get(serverId + userId);
|
||||
if (member) return member;
|
||||
|
||||
const data = await this.client.api.get(
|
||||
`/servers/${serverId as ""}/members/${userId as ""}`
|
||||
);
|
||||
|
||||
return this.getOrCreate(data._id, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create
|
||||
* @param id Id
|
||||
* @param data Data
|
||||
*/
|
||||
getOrCreate(id: API.MemberCompositeKey, data: API.Member) {
|
||||
if (this.hasByKey(id)) {
|
||||
return this.getByKey(id)!;
|
||||
} else {
|
||||
const instance = new ServerMember(this, id);
|
||||
this.create(
|
||||
id.server + id.user,
|
||||
"serverMember",
|
||||
instance,
|
||||
this.client,
|
||||
data
|
||||
);
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or return partial
|
||||
* @param id Id
|
||||
*/
|
||||
getOrPartial(id: API.MemberCompositeKey) {
|
||||
if (this.hasByKey(id)) {
|
||||
return this.getByKey(id)!;
|
||||
} else if (this.client.options.partials) {
|
||||
const instance = new ServerMember(this, id);
|
||||
this.create(id.server + id.user, "serverMember", instance, this.client, {
|
||||
id,
|
||||
partial: true,
|
||||
});
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { API, User } from "..";
|
||||
import { HydratedUser } from "../hydration";
|
||||
|
||||
import { ClassCollection } from ".";
|
||||
|
||||
/**
|
||||
* Collection of Users
|
||||
*/
|
||||
export class UserCollection extends ClassCollection<User, HydratedUser> {
|
||||
/**
|
||||
* Fetch user by ID
|
||||
* @param id Id
|
||||
* @returns User
|
||||
*/
|
||||
async fetch(id: string): Promise<User> {
|
||||
const user = this.get(id);
|
||||
if (user) return user;
|
||||
const data = await this.client.api.get(`/users/${id as ""}`);
|
||||
return this.getOrCreate(data._id, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create
|
||||
* @param id Id
|
||||
* @param data Data
|
||||
* @param isNew Whether this object is new
|
||||
*/
|
||||
getOrCreate(id: string, data: API.User) {
|
||||
if (this.has(id)) {
|
||||
return this.get(id)!;
|
||||
} else {
|
||||
const instance = new User(this, id);
|
||||
this.create(id, "user", instance, this.client, data);
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or return partial
|
||||
* @param id Id
|
||||
*/
|
||||
getOrPartial(id: string) {
|
||||
if (this.has(id)) {
|
||||
return this.get(id)!;
|
||||
} else if (this.client.options.partials) {
|
||||
const instance = new User(this, id);
|
||||
this.create(id, "user", instance, this.client, {
|
||||
id,
|
||||
partial: true,
|
||||
});
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export * from "./Collection";
|
||||
|
||||
export { BotCollection } from "./BotCollection";
|
||||
export { ChannelCollection } from "./ChannelCollection";
|
||||
export { ChannelUnreadCollection } from "./ChannelUnreadCollection";
|
||||
export { EmojiCollection } from "./EmojiCollection";
|
||||
export { MessageCollection } from "./MessageCollection";
|
||||
export { ServerCollection } from "./ServerCollection";
|
||||
export { ServerMemberCollection } from "./ServerMemberCollection";
|
||||
export { UserCollection } from "./UserCollection";
|
||||
@@ -1,13 +0,0 @@
|
||||
export const LIBRARY_VERSION = "6.0.17";
|
||||
|
||||
export const defaultConfig = {
|
||||
apiURL: "https://api.revolt.chat",
|
||||
autoReconnect: true,
|
||||
heartbeat: 30,
|
||||
debug: false,
|
||||
|
||||
cache: false,
|
||||
unreads: false,
|
||||
|
||||
ackRateLimiter: true,
|
||||
};
|
||||
@@ -0,0 +1,229 @@
|
||||
import { Accessor, Setter, createSignal } from "solid-js";
|
||||
|
||||
import EventEmitter from "eventemitter3";
|
||||
import WebSocket from "isomorphic-ws";
|
||||
|
||||
import type { AvailableProtocols, EventProtocol } from ".";
|
||||
|
||||
/**
|
||||
* All possible event client states.
|
||||
*/
|
||||
export enum ConnectionState {
|
||||
Idle,
|
||||
Connecting,
|
||||
Connected,
|
||||
Disconnected,
|
||||
}
|
||||
|
||||
/**
|
||||
* Event client options object
|
||||
*/
|
||||
export interface EventClientOptions {
|
||||
/**
|
||||
* Whether to log events
|
||||
* @default false
|
||||
*/
|
||||
debug: boolean;
|
||||
|
||||
/**
|
||||
* Time in seconds between Ping packets sent to the server
|
||||
* @default 30
|
||||
*/
|
||||
heartbeatInterval: number;
|
||||
|
||||
/**
|
||||
* Maximum time in seconds between Ping and corresponding Pong
|
||||
* @default 10
|
||||
*/
|
||||
pongTimeout: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Events provided by the client.
|
||||
*/
|
||||
type Events<T extends AvailableProtocols, P extends EventProtocol<T>> = {
|
||||
error: (error: Error) => void;
|
||||
event: (event: P["server"]) => void;
|
||||
state: (state: ConnectionState) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Simple wrapper around the Revolt websocket service.
|
||||
*/
|
||||
export class EventClient<T extends AvailableProtocols> extends EventEmitter<
|
||||
Events<T, EventProtocol<T>>
|
||||
> {
|
||||
readonly options: EventClientOptions;
|
||||
|
||||
#protocolVersion: T;
|
||||
#transportFormat: "json" | "msgpack";
|
||||
|
||||
readonly ping: Accessor<number>;
|
||||
#setPing: Setter<number>;
|
||||
|
||||
readonly state: Accessor<ConnectionState>;
|
||||
#setStateSetter: Setter<ConnectionState>;
|
||||
|
||||
#socket: WebSocket | undefined;
|
||||
#heartbeatIntervalReference: number | undefined;
|
||||
#pongTimeoutReference: number | undefined;
|
||||
|
||||
/**
|
||||
* Create a new event client.
|
||||
* @param protocolVersion Target protocol version
|
||||
* @param transportFormat Communication format
|
||||
* @param options Configuration options
|
||||
*/
|
||||
constructor(
|
||||
protocolVersion: T,
|
||||
transportFormat: "json" = "json",
|
||||
options?: Partial<EventClientOptions>
|
||||
) {
|
||||
super();
|
||||
|
||||
this.#protocolVersion = protocolVersion;
|
||||
this.#transportFormat = transportFormat;
|
||||
|
||||
this.options = {
|
||||
heartbeatInterval: 30,
|
||||
pongTimeout: 10,
|
||||
debug: false,
|
||||
...options,
|
||||
};
|
||||
|
||||
const [state, setState] = createSignal(ConnectionState.Idle);
|
||||
this.state = state;
|
||||
this.#setStateSetter = setState;
|
||||
|
||||
const [ping, setPing] = createSignal(-1);
|
||||
this.ping = ping;
|
||||
this.#setPing = setPing;
|
||||
|
||||
this.disconnect = this.disconnect.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current state
|
||||
* @param state state
|
||||
*/
|
||||
private setState(state: ConnectionState) {
|
||||
this.#setStateSetter(state);
|
||||
this.emit("state", state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to the websocket service.
|
||||
* @param uri WebSocket URI
|
||||
* @param token Authentication token
|
||||
*/
|
||||
connect(uri: string, token: string) {
|
||||
this.disconnect();
|
||||
this.setState(ConnectionState.Connecting);
|
||||
|
||||
this.#socket = new WebSocket(
|
||||
`${uri}?version=${this.#protocolVersion}&format=${
|
||||
this.#transportFormat
|
||||
}&token=${token}`
|
||||
);
|
||||
|
||||
this.#socket.onopen = () => {
|
||||
this.#heartbeatIntervalReference = setInterval(() => {
|
||||
this.send({ type: "Ping", data: +new Date() });
|
||||
this.#pongTimeoutReference = setTimeout(
|
||||
() => this.disconnect(),
|
||||
this.options.pongTimeout * 1e3
|
||||
) as never;
|
||||
}, this.options.heartbeatInterval * 1e3) as never;
|
||||
};
|
||||
|
||||
this.#socket.onerror = (error) => {
|
||||
this.emit("error", error as never);
|
||||
};
|
||||
|
||||
this.#socket.onmessage = (event) => {
|
||||
if (this.#transportFormat === "json") {
|
||||
if (typeof event.data === "string") {
|
||||
this.handle(JSON.parse(event.data));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let closed = false;
|
||||
this.#socket.onclose = () => {
|
||||
if (closed) return;
|
||||
closed = true;
|
||||
this.disconnect();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect the websocket client.
|
||||
*/
|
||||
disconnect() {
|
||||
if (!this.#socket) return;
|
||||
clearInterval(this.#heartbeatIntervalReference);
|
||||
const socket = this.#socket;
|
||||
this.#socket = undefined;
|
||||
socket.close();
|
||||
this.setState(ConnectionState.Disconnected);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an event to the server.
|
||||
* @param event Event
|
||||
*/
|
||||
send(event: EventProtocol<T>["client"]) {
|
||||
this.options.debug && console.debug("[C->S]", event);
|
||||
if (!this.#socket) throw "Socket closed, trying to send.";
|
||||
this.#socket.send(JSON.stringify(event));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle events intended for client before passing them along.
|
||||
* @param event Event
|
||||
*/
|
||||
handle(event: EventProtocol<T>["server"]) {
|
||||
this.options.debug && console.debug("[S->C]", event);
|
||||
switch (event.type) {
|
||||
case "Ping":
|
||||
this.send({
|
||||
type: "Pong",
|
||||
data: event.data,
|
||||
});
|
||||
return;
|
||||
case "Pong":
|
||||
clearTimeout(this.#pongTimeoutReference);
|
||||
this.#setPing(+new Date() - event.data);
|
||||
this.options.debug && console.debug(`[ping] ${this.ping()}ms`);
|
||||
return;
|
||||
case "Error":
|
||||
this.emit("error", event as never);
|
||||
this.disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
switch (this.state()) {
|
||||
case ConnectionState.Connecting:
|
||||
if (event.type === "Authenticated") {
|
||||
// no-op
|
||||
} else if (event.type === "Ready") {
|
||||
this.emit("event", event);
|
||||
this.setState(ConnectionState.Connected);
|
||||
} else {
|
||||
throw `Unreachable code. Received ${event.type} in Connecting state.`;
|
||||
}
|
||||
break;
|
||||
case ConnectionState.Connected:
|
||||
if (event.type === "Authenticated" || event.type === "Ready") {
|
||||
throw `Unreachable code. Received ${event.type} in Connected state.`;
|
||||
} else {
|
||||
this.emit("event", event);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw `Unreachable code. Received ${
|
||||
event.type
|
||||
} in state ${this.state()}.`;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { ProtocolV1 } from "./v1";
|
||||
|
||||
export { handleEvent as handleEventV1 } from "./v1";
|
||||
|
||||
export * from "./EventClient";
|
||||
|
||||
/**
|
||||
* Available protocols to connect with
|
||||
*/
|
||||
export type AvailableProtocols = 1;
|
||||
|
||||
/**
|
||||
* Protocol mapping
|
||||
*/
|
||||
type Protocols = {
|
||||
1: ProtocolV1;
|
||||
};
|
||||
|
||||
/**
|
||||
* Select a protocol by its key
|
||||
*/
|
||||
export type EventProtocol<T extends AvailableProtocols> = Protocols[T];
|
||||
@@ -0,0 +1,779 @@
|
||||
import { Setter } from "solid-js";
|
||||
|
||||
import { ReactiveSet } from "@solid-primitives/set";
|
||||
import type {
|
||||
Channel,
|
||||
Embed,
|
||||
Emoji,
|
||||
FieldsChannel,
|
||||
FieldsMember,
|
||||
FieldsServer,
|
||||
FieldsUser,
|
||||
Member,
|
||||
MemberCompositeKey,
|
||||
Message,
|
||||
RelationshipStatus,
|
||||
Role,
|
||||
Server,
|
||||
User,
|
||||
} from "revolt-api";
|
||||
|
||||
import { Client } from "..";
|
||||
import { Merge } from "../lib/merge";
|
||||
|
||||
/**
|
||||
* Version 1 of the events protocol
|
||||
*/
|
||||
export type ProtocolV1 = {
|
||||
client: ClientMessage;
|
||||
server: ServerMessage;
|
||||
};
|
||||
|
||||
/**
|
||||
* Messages sent to the server
|
||||
*/
|
||||
type ClientMessage =
|
||||
| { type: "Authenticate"; token: string }
|
||||
| {
|
||||
type: "BeginTyping";
|
||||
channel: string;
|
||||
}
|
||||
| {
|
||||
type: "EndTyping";
|
||||
channel: string;
|
||||
}
|
||||
| {
|
||||
type: "Ping";
|
||||
data: number;
|
||||
}
|
||||
| {
|
||||
type: "Pong";
|
||||
data: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Messages sent from the server
|
||||
*/
|
||||
type ServerMessage =
|
||||
| ({ type: "Error" } & WebSocketError)
|
||||
| { type: "Bulk"; v: ServerMessage[] }
|
||||
| { type: "Authenticated" }
|
||||
| ({ type: "Ready" } & ReadyData)
|
||||
| { type: "Ping"; data: number }
|
||||
| { type: "Pong"; data: number }
|
||||
| ({ type: "Message" } & Message)
|
||||
| {
|
||||
type: "MessageUpdate";
|
||||
id: string;
|
||||
channel: string;
|
||||
data: Partial<Message>;
|
||||
}
|
||||
| {
|
||||
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: "MessageUnreact";
|
||||
id: string;
|
||||
channel_id: string;
|
||||
user_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: "ChannelDelete"; id: string }
|
||||
| { type: "ChannelGroupJoin"; id: string; user: string }
|
||||
| { type: "ChannelGroupLeave"; id: string; user: string }
|
||||
| { type: "ChannelStartTyping"; id: string; user: string }
|
||||
| { type: "ChannelStopTyping"; id: string; user: string }
|
||||
| { type: "ChannelAck"; id: string; user: string; message_id: string }
|
||||
| {
|
||||
type: "ServerCreate";
|
||||
id: string;
|
||||
server: Server;
|
||||
channels: Channel[];
|
||||
}
|
||||
| {
|
||||
type: "ServerUpdate";
|
||||
id: string;
|
||||
data: Partial<Server>;
|
||||
clear?: FieldsServer[];
|
||||
}
|
||||
| { type: "ServerDelete"; id: string }
|
||||
| {
|
||||
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: "ServerRoleDelete"; id: string; role_id: string }
|
||||
| {
|
||||
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: "UserPlatformWipe"; user_id: string; flags: number }
|
||||
| ({ type: "EmojiCreate" } & Emoji)
|
||||
| { type: "EmojiDelete"; id: string }
|
||||
| ({
|
||||
type: "Auth";
|
||||
} & (
|
||||
| {
|
||||
event_type: "DeleteSession";
|
||||
user_id: string;
|
||||
session_id: string;
|
||||
}
|
||||
| {
|
||||
event_type: "DeleteAllSessions";
|
||||
user_id: string;
|
||||
exclude_session_id: string;
|
||||
}
|
||||
));
|
||||
|
||||
/**
|
||||
* Initial synchronisation packet
|
||||
*/
|
||||
type ReadyData = {
|
||||
users: User[];
|
||||
servers: Server[];
|
||||
channels: Channel[];
|
||||
members: Member[];
|
||||
emojis: Emoji[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Websocket error packet
|
||||
*/
|
||||
type WebSocketError = {
|
||||
error:
|
||||
| "InternalError"
|
||||
| "InvalidSession"
|
||||
| "OnboardingNotFinished"
|
||||
| "AlreadyAuthenticated";
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle an event for the Client
|
||||
* @param client Client
|
||||
* @param event Event
|
||||
* @param setReady Signal state change
|
||||
*/
|
||||
export async function handleEvent(
|
||||
client: Client,
|
||||
event: ServerMessage,
|
||||
setReady: Setter<boolean>
|
||||
) {
|
||||
switch (event.type) {
|
||||
case "Bulk": {
|
||||
for (const item of event.v) {
|
||||
handleEvent(client, item, setReady);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "Ready": {
|
||||
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 emoji of event.emojis) {
|
||||
client.emojis.getOrCreate(emoji._id, emoji);
|
||||
}
|
||||
|
||||
if (client.options.syncUnreads) {
|
||||
await client.channelUnreads.sync();
|
||||
}
|
||||
|
||||
setReady(true);
|
||||
client.emit("ready");
|
||||
|
||||
break;
|
||||
}
|
||||
case "Message": {
|
||||
if (!client.messages.has(event._id)) {
|
||||
// TODO: this should not be necessary in future protocols:
|
||||
if (event.author && client.options.eagerFetching) {
|
||||
await client.users.fetch(event.author);
|
||||
const serverId = client.channels.get(event.channel)?.serverId;
|
||||
if (serverId)
|
||||
await client.serverMembers.fetch(serverId, event.author);
|
||||
}
|
||||
|
||||
client.messages.getOrCreate(event._id, event, true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "MessageUpdate": {
|
||||
const message = client.messages.getOrPartial(event.id);
|
||||
if (message) {
|
||||
const previousMessage = {
|
||||
...client.messages.getUnderlyingObject(event.id),
|
||||
};
|
||||
|
||||
client.messages.updateUnderlyingObject(event.id, {
|
||||
...(event.data as object),
|
||||
editedAt: new Date(),
|
||||
});
|
||||
|
||||
client.emit("messageUpdate", message, previousMessage);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "MessageAppend": {
|
||||
const message = client.messages.getOrPartial(event.id);
|
||||
if (message) {
|
||||
const previousMessage = {
|
||||
...client.messages.getUnderlyingObject(event.id),
|
||||
};
|
||||
|
||||
client.messages.updateUnderlyingObject(
|
||||
event.id,
|
||||
"embeds",
|
||||
(embeds) => [...(embeds ?? []), event.append.embeds ?? []] as Embed[]
|
||||
);
|
||||
|
||||
client.emit("messageUpdate", message, previousMessage);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "MessageDelete": {
|
||||
if (client.messages.getOrPartial(event.id)) {
|
||||
const message = client.messages.getUnderlyingObject(event.id);
|
||||
client.emit("messageDelete", message);
|
||||
client.messages.delete(event.id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "BulkMessageDelete": {
|
||||
client.emit(
|
||||
"messageDeleteBulk",
|
||||
event.ids
|
||||
.map((id) => {
|
||||
if (client.messages.has(id)) {
|
||||
const message = client.messages.getUnderlyingObject(id);
|
||||
client.messages.delete(id);
|
||||
return message!;
|
||||
}
|
||||
|
||||
return undefined!;
|
||||
})
|
||||
.filter((x) => x),
|
||||
client.channels.get(event.channel)
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "MessageReact": {
|
||||
const message = client.messages.getOrPartial(event.id);
|
||||
if (message) {
|
||||
const reactions = message.reactions;
|
||||
const set = reactions.get(event.emoji_id)!;
|
||||
if (set) {
|
||||
if (set.has(event.user_id)) return;
|
||||
set.add(event.user_id);
|
||||
} else {
|
||||
reactions.set(event.emoji_id, new ReactiveSet([event.user_id]));
|
||||
}
|
||||
|
||||
client.emit(
|
||||
"messageReactionAdd",
|
||||
message,
|
||||
event.user_id,
|
||||
event.emoji_id
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "MessageUnreact": {
|
||||
const message = client.messages.getOrPartial(event.id);
|
||||
if (message) {
|
||||
const set = message.reactions.get(event.emoji_id);
|
||||
if (set?.has(event.user_id)) {
|
||||
set.delete(event.user_id);
|
||||
} else if (!client.messages.isPartial(event.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
client.emit(
|
||||
"messageReactionRemove",
|
||||
message,
|
||||
event.user_id,
|
||||
event.emoji_id
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "MessageRemoveReaction": {
|
||||
const message = client.messages.getOrPartial(event.id);
|
||||
if (message) {
|
||||
const reactions = message.reactions;
|
||||
if (reactions.has(event.emoji_id)) {
|
||||
reactions.delete(event.emoji_id);
|
||||
} else if (!client.messages.isPartial(event.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
client.emit("messageReactionRemoveEmoji", message, event.emoji_id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "ChannelCreate": {
|
||||
if (!client.channels.has(event._id)) {
|
||||
client.channels.getOrCreate(event._id, event, true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "ChannelUpdate": {
|
||||
const channel = client.channels.getOrPartial(event.id);
|
||||
if (channel) {
|
||||
const previousChannel = {
|
||||
...client.channels.getUnderlyingObject(event.id),
|
||||
};
|
||||
|
||||
const changes = {
|
||||
...(event.data as Merge<Channel>),
|
||||
};
|
||||
|
||||
if (event.clear) {
|
||||
for (const remove of event.clear) {
|
||||
switch (remove) {
|
||||
case "Description":
|
||||
changes["description"] = undefined;
|
||||
break;
|
||||
case "DefaultPermissions":
|
||||
changes["default_permissions"] = undefined;
|
||||
break;
|
||||
case "Icon":
|
||||
changes["icon"] = undefined;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
client.channels.updateUnderlyingObject(event.id, changes as never);
|
||||
client.emit("channelUpdate", channel, previousChannel);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "ChannelDelete": {
|
||||
if (client.channels.getOrPartial(event.id)) {
|
||||
const channel = client.channels.getUnderlyingObject(event.id);
|
||||
client.emit("channelDelete", channel);
|
||||
client.channels.delete(event.id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "ChannelGroupJoin": {
|
||||
const channel = client.channels.getOrPartial(event.id);
|
||||
if (channel) {
|
||||
if (!channel.recipientIds.has(event.user)) {
|
||||
channel.recipientIds.add(event.user);
|
||||
} else if (!client.channels.isPartial(event.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
client.emit(
|
||||
"channelGroupJoin",
|
||||
channel,
|
||||
await client.users.fetch(event.user)
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "ChannelGroupLeave": {
|
||||
const channel = client.channels.getOrPartial(event.id);
|
||||
if (channel) {
|
||||
if (channel.recipientIds.has(event.user)) {
|
||||
channel.recipientIds.delete(event.user);
|
||||
} else if (!client.channels.isPartial(event.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
client.emit(
|
||||
"channelGroupLeave",
|
||||
channel,
|
||||
client.users.getOrPartial(event.user)!
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "ChannelStartTyping": {
|
||||
const channel = client.channels.getOrPartial(event.id);
|
||||
if (channel) {
|
||||
if (!channel.typingIds.has(event.user)) {
|
||||
channel.typingIds.add(event.user);
|
||||
} else if (!client.channels.isPartial(event.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
client.emit(
|
||||
"channelStartTyping",
|
||||
channel,
|
||||
client.users.getOrPartial(event.user)!
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "ChannelStopTyping": {
|
||||
const channel = client.channels.getOrPartial(event.id);
|
||||
if (channel) {
|
||||
if (channel.typingIds.has(event.user)) {
|
||||
channel.typingIds.delete(event.user);
|
||||
} else if (!client.channels.isPartial(event.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
client.emit(
|
||||
"channelStopTyping",
|
||||
channel,
|
||||
client.users.getOrPartial(event.user)!
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "ChannelAck": {
|
||||
const channel = client.channels.getOrPartial(event.id);
|
||||
if (channel) {
|
||||
client.emit("channelAcknowledged", channel, event.message_id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "ServerCreate": {
|
||||
if (!client.servers.has(event.server._id)) {
|
||||
for (const channel of event.channels) {
|
||||
client.channels.getOrCreate(channel._id, channel);
|
||||
}
|
||||
|
||||
client.servers.getOrCreate(event.server._id, event.server, true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "ServerUpdate": {
|
||||
const server = client.servers.getOrPartial(event.id);
|
||||
if (server) {
|
||||
const previousServer = {
|
||||
...client.servers.getUnderlyingObject(event.id),
|
||||
};
|
||||
|
||||
const changes = {
|
||||
...event.data,
|
||||
};
|
||||
|
||||
if (event.clear) {
|
||||
for (const remove of event.clear) {
|
||||
switch (remove) {
|
||||
case "Banner":
|
||||
changes["banner"] = undefined;
|
||||
break;
|
||||
case "Categories":
|
||||
changes["categories"] = undefined;
|
||||
break;
|
||||
case "SystemMessages":
|
||||
changes["system_messages"] = undefined;
|
||||
break;
|
||||
case "Description":
|
||||
changes["description"] = undefined;
|
||||
break;
|
||||
case "Icon":
|
||||
changes["icon"] = undefined;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
client.servers.updateUnderlyingObject(event.id, changes as never);
|
||||
client.emit("serverUpdate", server, previousServer);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "ServerDelete": {
|
||||
if (client.servers.getOrPartial(event.id)) {
|
||||
const server = client.servers.getUnderlyingObject(event.id);
|
||||
client.emit("serverDelete", server);
|
||||
client.servers.delete(event.id);
|
||||
|
||||
for (const channel of server.channelIds) {
|
||||
client.channels.delete(channel);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "ServerRoleUpdate": {
|
||||
const server = client.servers.getOrPartial(event.id);
|
||||
if (server) {
|
||||
const role = server.roles.get(event.role_id) ?? {};
|
||||
server.roles.set(event.role_id, {
|
||||
...role,
|
||||
...event.data,
|
||||
} as Role);
|
||||
|
||||
client.emit("serverRoleUpdate", server, event.role_id, role as never);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "ServerRoleDelete": {
|
||||
const server = client.servers.getOrPartial(event.id);
|
||||
if (server) {
|
||||
let role = {};
|
||||
const roles = server.roles;
|
||||
if (roles.has(event.role_id)) {
|
||||
role = roles.get(event.role_id) as Role;
|
||||
roles.delete(event.role_id);
|
||||
} else if (!client.servers.isPartial(event.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
client.emit("serverRoleDelete", server, event.role_id, role as never);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "ServerMemberJoin": {
|
||||
const id = {
|
||||
server: event.id,
|
||||
user: event.user,
|
||||
};
|
||||
|
||||
if (!client.serverMembers.hasByKey(id)) {
|
||||
client.emit(
|
||||
"serverMemberJoin",
|
||||
client.serverMembers.getOrCreate(id, {
|
||||
_id: id,
|
||||
joined_at: new Date().toUTCString(),
|
||||
})
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "ServerMemberUpdate": {
|
||||
const member = client.serverMembers.getOrPartial(event.id);
|
||||
if (member) {
|
||||
const previousMember = {
|
||||
...client.serverMembers.getUnderlyingObject(
|
||||
event.id.server + event.id.user
|
||||
),
|
||||
};
|
||||
|
||||
const changes = {
|
||||
...event.data,
|
||||
};
|
||||
|
||||
if (event.clear) {
|
||||
for (const remove of event.clear) {
|
||||
switch (remove) {
|
||||
case "Nickname":
|
||||
changes["nickname"] = undefined;
|
||||
break;
|
||||
case "Avatar":
|
||||
changes["avatar"] = undefined;
|
||||
break;
|
||||
case "Roles":
|
||||
changes["roles"] = undefined;
|
||||
break;
|
||||
case "Timeout":
|
||||
changes["timeout"] = undefined;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
client.serverMembers.updateUnderlyingObject(
|
||||
event.id.server + event.id.user,
|
||||
changes as never
|
||||
);
|
||||
|
||||
client.emit("serverMemberUpdate", member, previousMember);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "ServerMemberLeave": {
|
||||
const id = {
|
||||
server: event.id,
|
||||
user: event.user,
|
||||
};
|
||||
|
||||
if (client.serverMembers.getOrPartial(id)) {
|
||||
const member = client.serverMembers.getUnderlyingObject(
|
||||
id.server + id.user
|
||||
);
|
||||
client.emit("serverMemberLeave", member);
|
||||
client.serverMembers.delete(id.server + id.user);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "UserUpdate": {
|
||||
const user = client.users.getOrPartial(event.id);
|
||||
if (user) {
|
||||
const previousUser = {
|
||||
...client.users.getUnderlyingObject(event.id),
|
||||
};
|
||||
|
||||
const changes = {
|
||||
...event.data,
|
||||
};
|
||||
|
||||
if (event.clear) {
|
||||
for (const remove of event.clear) {
|
||||
switch (remove) {
|
||||
case "Avatar":
|
||||
changes["avatar"] = undefined;
|
||||
break;
|
||||
case "StatusPresence":
|
||||
changes["status"] = {
|
||||
...(previousUser.status ?? {}),
|
||||
...(changes["status"] ?? {}),
|
||||
presence: undefined,
|
||||
};
|
||||
break;
|
||||
case "StatusText":
|
||||
changes["status"] = {
|
||||
...(previousUser.status ?? {}),
|
||||
...(changes["status"] ?? {}),
|
||||
text: undefined,
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
client.users.updateUnderlyingObject(event.id, changes as never);
|
||||
client.emit("userUpdate", user, previousUser);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "UserRelationship": {
|
||||
handleEvent(
|
||||
client,
|
||||
{
|
||||
type: "UserUpdate",
|
||||
id: event.user._id,
|
||||
data: {
|
||||
relationship: event.user.relationship!,
|
||||
},
|
||||
},
|
||||
setReady
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "UserPresence": {
|
||||
handleEvent(
|
||||
client,
|
||||
{
|
||||
type: "UserUpdate",
|
||||
id: event.id,
|
||||
data: {
|
||||
online: event.online,
|
||||
},
|
||||
},
|
||||
setReady
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "UserSettingsUpdate": {
|
||||
client.emit("userSettingsUpdate", event.id, event.update);
|
||||
break;
|
||||
}
|
||||
case "UserPlatformWipe": {
|
||||
handleEvent(
|
||||
client,
|
||||
{
|
||||
type: "BulkMessageDelete",
|
||||
channel: "0",
|
||||
ids: client.messages
|
||||
.toList()
|
||||
.filter((message) => message.authorId === event.user_id)
|
||||
.map((message) => message.id),
|
||||
},
|
||||
setReady
|
||||
);
|
||||
|
||||
handleEvent(
|
||||
client,
|
||||
{
|
||||
type: "UserUpdate",
|
||||
id: event.user_id,
|
||||
data: {
|
||||
username: `Deleted User`,
|
||||
online: false,
|
||||
flags: event.flags,
|
||||
badges: 0,
|
||||
relationship: "None",
|
||||
},
|
||||
clear: ["Avatar", "StatusPresence", "StatusText"],
|
||||
},
|
||||
setReady
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
case "EmojiCreate": {
|
||||
if (!client.emojis.has(event._id)) {
|
||||
client.emojis.getOrCreate(event._id, event, true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "EmojiDelete": {
|
||||
if (client.emojis.getOrPartial(event.id)) {
|
||||
const emoji = client.emojis.getUnderlyingObject(event.id);
|
||||
client.emit("emojiDelete", emoji);
|
||||
client.emojis.delete(event.id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "Auth": {
|
||||
// TODO: implement DeleteSession and DeleteAllSessions
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
-1
@@ -1 +0,0 @@
|
||||
type Tail<T extends unknown[]> = T extends [infer _A, ...infer R] ? R : never;
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Bot as ApiBot } from "revolt-api";
|
||||
|
||||
import { Hydrate } from ".";
|
||||
|
||||
export type HydratedBot = {
|
||||
id: string;
|
||||
ownerId: string;
|
||||
token: string;
|
||||
public: boolean;
|
||||
analytics: boolean;
|
||||
discoverable: boolean;
|
||||
interactionsUrl?: string;
|
||||
termsOfServiceUrl?: string;
|
||||
privacyPolicyUrl?: string;
|
||||
flags: BotFlags;
|
||||
};
|
||||
|
||||
export const botHydration: Hydrate<ApiBot, HydratedBot> = {
|
||||
keyMapping: {
|
||||
_id: "id",
|
||||
owner: "ownerId",
|
||||
interactions_url: "interactionsUrl",
|
||||
terms_of_service_url: "termsOfServiceUrl",
|
||||
privacy_policy_url: "privacyPolicyUrl",
|
||||
},
|
||||
functions: {
|
||||
id: (bot) => bot._id,
|
||||
ownerId: (bot) => bot.owner,
|
||||
token: (bot) => bot.token,
|
||||
public: (bot) => bot.public,
|
||||
analytics: (bot) => bot.analytics!,
|
||||
discoverable: (bot) => bot.discoverable!,
|
||||
interactionsUrl: (bot) => bot.interactions_url!,
|
||||
termsOfServiceUrl: (bot) => bot.terms_of_service_url!,
|
||||
privacyPolicyUrl: (bot) => bot.privacy_policy_url!,
|
||||
flags: (bot) => bot.flags!,
|
||||
},
|
||||
initialHydration: () => ({}),
|
||||
};
|
||||
|
||||
/**
|
||||
* Flags attributed to users
|
||||
*/
|
||||
export enum BotFlags {}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { ReactiveSet } from "@solid-primitives/set";
|
||||
import { Channel as ApiChannel, OverrideField } from "revolt-api";
|
||||
|
||||
import { Client, File } from "..";
|
||||
import type { Merge } from "../lib/merge";
|
||||
|
||||
import { Hydrate } from ".";
|
||||
|
||||
export type HydratedChannel = {
|
||||
id: string;
|
||||
channelType: ApiChannel["channel_type"];
|
||||
|
||||
name: string;
|
||||
description?: string;
|
||||
icon?: File;
|
||||
|
||||
active: boolean;
|
||||
typingIds: ReactiveSet<string>;
|
||||
recipientIds: ReactiveSet<string>;
|
||||
|
||||
userId?: string;
|
||||
ownerId?: string;
|
||||
serverId?: string;
|
||||
|
||||
permissions?: number;
|
||||
defaultPermissions?: OverrideField;
|
||||
rolePermissions?: Record<string, OverrideField>;
|
||||
nsfw: boolean;
|
||||
|
||||
lastMessageId?: string;
|
||||
};
|
||||
|
||||
export const channelHydration: Hydrate<Merge<ApiChannel>, HydratedChannel> = {
|
||||
keyMapping: {
|
||||
_id: "id",
|
||||
channel_type: "channelType",
|
||||
recipients: "recipientIds",
|
||||
user: "userId",
|
||||
owner: "ownerId",
|
||||
server: "serverId",
|
||||
default_permissions: "defaultPermissions",
|
||||
role_permissions: "rolePermissions",
|
||||
last_message_id: "lastMessageId",
|
||||
},
|
||||
functions: {
|
||||
id: (channel) => channel._id,
|
||||
channelType: (channel) => channel.channel_type,
|
||||
name: (channel) => channel.name,
|
||||
description: (channel) => channel.description!,
|
||||
icon: (channel, ctx) => new File(ctx as Client, channel.icon!),
|
||||
active: (channel) => channel.active || false,
|
||||
typingIds: () => new ReactiveSet(),
|
||||
recipientIds: (channel) => new ReactiveSet(channel.recipients),
|
||||
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,
|
||||
nsfw: (channel) => channel.nsfw || false,
|
||||
lastMessageId: (channel) => channel.last_message_id!,
|
||||
},
|
||||
initialHydration: () => ({
|
||||
typingIds: new ReactiveSet(),
|
||||
recipientIds: new ReactiveSet(),
|
||||
}),
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import { ReactiveSet } from "@solid-primitives/set";
|
||||
|
||||
import { API } from "..";
|
||||
import type { Merge } from "../lib/merge";
|
||||
|
||||
import { Hydrate } from ".";
|
||||
|
||||
export type HydratedChannelUnread = {
|
||||
id: string;
|
||||
lastMessageId?: string;
|
||||
messageMentionIds: ReactiveSet<string>;
|
||||
};
|
||||
|
||||
export const channelUnreadHydration: Hydrate<
|
||||
Merge<API.ChannelUnread>,
|
||||
HydratedChannelUnread
|
||||
> = {
|
||||
keyMapping: {
|
||||
_id: "id",
|
||||
last_id: "lastMessageId",
|
||||
mentions: "messageMentionIds",
|
||||
},
|
||||
functions: {
|
||||
id: (unread) => unread._id.channel,
|
||||
lastMessageId: (unread) => unread.last_id!,
|
||||
messageMentionIds: (unread) => new ReactiveSet(unread.mentions!),
|
||||
},
|
||||
initialHydration: () => ({
|
||||
messageMentionIds: new ReactiveSet(),
|
||||
}),
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Emoji as ApiEmoji, EmojiParent } from "revolt-api";
|
||||
|
||||
import type { Merge } from "../lib/merge";
|
||||
|
||||
import { Hydrate } from ".";
|
||||
|
||||
export type HydratedEmoji = {
|
||||
id: string;
|
||||
parent: EmojiParent;
|
||||
creatorId: string;
|
||||
name: string;
|
||||
animated: boolean;
|
||||
nsfw: boolean;
|
||||
};
|
||||
|
||||
export const emojiHydration: Hydrate<Merge<ApiEmoji>, HydratedEmoji> = {
|
||||
keyMapping: {
|
||||
_id: "id",
|
||||
creator_id: "creatorId",
|
||||
},
|
||||
functions: {
|
||||
id: (emoji) => emoji._id,
|
||||
parent: (emoji) => emoji.parent,
|
||||
creatorId: (emoji) => emoji.creator_id,
|
||||
name: (emoji) => emoji.name,
|
||||
animated: (emoji) => emoji.animated || false,
|
||||
nsfw: (emoji) => emoji.nsfw || false,
|
||||
},
|
||||
initialHydration: () => ({}),
|
||||
};
|
||||
@@ -0,0 +1,111 @@
|
||||
import { botHydration } from "./bot";
|
||||
import { channelHydration } from "./channel";
|
||||
import { channelUnreadHydration } from "./channelUnread";
|
||||
import { emojiHydration } from "./emoji";
|
||||
import { messageHydration } from "./message";
|
||||
import { serverHydration } from "./server";
|
||||
import { serverMemberHydration } from "./serverMember";
|
||||
import { userHydration } from "./user";
|
||||
|
||||
export { BotFlags } from "./bot";
|
||||
export { ServerFlags } from "./server";
|
||||
export { UserBadges, UserFlags } from "./user";
|
||||
|
||||
export type { HydratedBot } from "./bot";
|
||||
export type { HydratedChannel } from "./channel";
|
||||
export type { HydratedChannelUnread } from "./channelUnread";
|
||||
export type { HydratedEmoji } from "./emoji";
|
||||
export type { HydratedMessage } from "./message";
|
||||
export type { HydratedServer } from "./server";
|
||||
export type { HydratedServerMember } from "./serverMember";
|
||||
export type { HydratedUser } from "./user";
|
||||
|
||||
/**
|
||||
* Functions to map from one object to another
|
||||
*/
|
||||
export type MappingFns<Input, Output, Key extends keyof Output> = Record<
|
||||
Key,
|
||||
(value: Input, context: unknown) => Output[Key]
|
||||
>;
|
||||
|
||||
/**
|
||||
* Key mapping information
|
||||
*/
|
||||
export type KeyMapping<Input, Output> = Record<keyof Input, keyof Output>;
|
||||
|
||||
/**
|
||||
* Hydration information
|
||||
*/
|
||||
export type Hydrate<Input, Output> = {
|
||||
keyMapping: Partial<KeyMapping<Input, Output>>;
|
||||
functions: MappingFns<Input, Output, keyof Output>;
|
||||
initialHydration: () => Partial<Output>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Hydrate some data
|
||||
* @param hydration Hydration data
|
||||
* @param input Input data
|
||||
* @returns Output data
|
||||
*/
|
||||
function hydrateInternal<Input extends object, Output>(
|
||||
hydration: Hydrate<Input, Output>,
|
||||
input: Input,
|
||||
context: unknown
|
||||
): Output {
|
||||
return (Object.keys(input) as (keyof Input)[]).reduce((acc, key) => {
|
||||
let targetKey, value;
|
||||
try {
|
||||
targetKey = hydration.keyMapping[key] ?? key;
|
||||
value = hydration.functions[targetKey as keyof Output](input, context);
|
||||
} catch (err) {
|
||||
if (key === "type") return acc;
|
||||
console.debug(`Skipping key ${String(key)} during hydration!`);
|
||||
return acc;
|
||||
}
|
||||
|
||||
return {
|
||||
...acc,
|
||||
[targetKey]: value,
|
||||
};
|
||||
}, {} as Output);
|
||||
}
|
||||
|
||||
const hydrators = {
|
||||
bot: botHydration,
|
||||
channel: channelHydration,
|
||||
channelUnread: channelUnreadHydration,
|
||||
emoji: emojiHydration,
|
||||
message: messageHydration,
|
||||
server: serverHydration,
|
||||
serverMember: serverMemberHydration,
|
||||
user: userHydration,
|
||||
};
|
||||
|
||||
export type Hydrators = typeof hydrators;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type ExtractInput<T> = T extends Hydrate<infer I, any> ? I : never;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type ExtractOutput<T> = T extends Hydrate<any, infer O> ? O : never;
|
||||
|
||||
/**
|
||||
* Hydrate some input with a given type
|
||||
* @param type Type
|
||||
* @param input Input Object
|
||||
* @param initial Whether this is the initial hydration
|
||||
* @returns Hydrated Object
|
||||
*/
|
||||
export function hydrate<T extends keyof Hydrators>(
|
||||
type: T,
|
||||
input: Partial<ExtractInput<Hydrators[T]>>,
|
||||
context: unknown,
|
||||
initial?: boolean
|
||||
) {
|
||||
return hydrateInternal(
|
||||
hydrators[type] as never,
|
||||
initial ? { ...hydrators[type].initialHydration(), ...input } : input,
|
||||
context
|
||||
) as ExtractOutput<Hydrators[T]>;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { ReactiveMap } from "@solid-primitives/map";
|
||||
import { ReactiveSet } from "@solid-primitives/set";
|
||||
|
||||
import { API, Client, File, MessageEmbed, SystemMessage } from "..";
|
||||
import type { Merge } from "../lib/merge";
|
||||
|
||||
import { Hydrate } from ".";
|
||||
|
||||
export type HydratedMessage = {
|
||||
id: string;
|
||||
nonce?: string;
|
||||
channelId: string;
|
||||
authorId?: string;
|
||||
content?: string;
|
||||
systemMessage?: SystemMessage;
|
||||
attachments?: File[];
|
||||
editedAt?: Date;
|
||||
embeds?: MessageEmbed[];
|
||||
mentionIds?: string[];
|
||||
replyIds?: string[];
|
||||
reactions: ReactiveMap<string, ReactiveSet<string>>;
|
||||
interactions?: API.Interactions;
|
||||
masquerade?: API.Masquerade;
|
||||
};
|
||||
|
||||
export const messageHydration: Hydrate<Merge<API.Message>, HydratedMessage> = {
|
||||
keyMapping: {
|
||||
_id: "id",
|
||||
channel: "channelId",
|
||||
author: "authorId",
|
||||
system: "systemMessage",
|
||||
edited: "editedAt",
|
||||
mentions: "mentionIds",
|
||||
replies: "replyIds",
|
||||
},
|
||||
functions: {
|
||||
id: (message) => message._id,
|
||||
nonce: (message) => message.nonce!,
|
||||
channelId: (message) => message.channel,
|
||||
authorId: (message) => message.author,
|
||||
content: (message) => message.content!,
|
||||
systemMessage: (message, ctx) =>
|
||||
SystemMessage.from(ctx as Client, message.system!),
|
||||
attachments: (message, ctx) =>
|
||||
message.attachments!.map((file) => new File(ctx as Client, file)),
|
||||
editedAt: (message) => new Date(message.edited!),
|
||||
embeds: (message, ctx) =>
|
||||
message.embeds!.map((embed) => MessageEmbed.from(ctx as Client, embed)),
|
||||
mentionIds: (message) => message.mentions!,
|
||||
replyIds: (message) => message.replies!,
|
||||
reactions: (message) => {
|
||||
const map = new ReactiveMap<string, ReactiveSet<string>>();
|
||||
if (message.reactions) {
|
||||
for (const reaction of Object.keys(message.reactions)) {
|
||||
map.set(reaction, new ReactiveSet(message.reactions![reaction]));
|
||||
}
|
||||
}
|
||||
return map;
|
||||
},
|
||||
interactions: (message) => message.interactions,
|
||||
masquerade: (message) => message.masquerade!,
|
||||
},
|
||||
initialHydration: () => ({
|
||||
reactions: new ReactiveMap(),
|
||||
}),
|
||||
};
|
||||
@@ -0,0 +1,77 @@
|
||||
import { ReactiveMap } from "@solid-primitives/map";
|
||||
import { ReactiveSet } from "@solid-primitives/set";
|
||||
import {
|
||||
Server as ApiServer,
|
||||
Category,
|
||||
Role,
|
||||
SystemMessageChannels,
|
||||
} from "revolt-api";
|
||||
|
||||
import { Client, File } from "..";
|
||||
|
||||
import { Hydrate } from ".";
|
||||
|
||||
export type HydratedServer = {
|
||||
id: string;
|
||||
ownerId: string;
|
||||
|
||||
name: string;
|
||||
description?: string;
|
||||
|
||||
icon?: File;
|
||||
banner?: File;
|
||||
|
||||
channelIds: ReactiveSet<string>;
|
||||
categories?: Category[];
|
||||
|
||||
systemMessages?: SystemMessageChannels;
|
||||
roles: ReactiveMap<string, Role>;
|
||||
defaultPermissions: number;
|
||||
|
||||
flags: ServerFlags;
|
||||
analytics: boolean;
|
||||
discoverable: boolean;
|
||||
nsfw: boolean;
|
||||
};
|
||||
|
||||
export const serverHydration: Hydrate<ApiServer, HydratedServer> = {
|
||||
keyMapping: {
|
||||
_id: "id",
|
||||
owner: "ownerId",
|
||||
channels: "channelIds",
|
||||
system_messages: "systemMessages",
|
||||
default_permissions: "defaultPermissions",
|
||||
},
|
||||
functions: {
|
||||
id: (server) => server._id,
|
||||
ownerId: (server) => server.owner,
|
||||
name: (server) => server.name,
|
||||
description: (server) => server.description!,
|
||||
channelIds: (server) => new ReactiveSet(server.channels),
|
||||
categories: (server) => server.categories ?? [],
|
||||
systemMessages: (server) => server.system_messages ?? {},
|
||||
roles: (server) =>
|
||||
new ReactiveMap(
|
||||
Object.keys(server.roles!).map((id) => [id, server.roles![id]])
|
||||
),
|
||||
defaultPermissions: (server) => 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!,
|
||||
analytics: (server) => server.analytics || false,
|
||||
discoverable: (server) => server.discoverable || false,
|
||||
nsfw: (server) => server.nsfw || false,
|
||||
},
|
||||
initialHydration: () => ({
|
||||
channelIds: new ReactiveSet(),
|
||||
roles: new ReactiveMap(),
|
||||
}),
|
||||
};
|
||||
|
||||
/**
|
||||
* Flags attributed to servers
|
||||
*/
|
||||
export enum ServerFlags {
|
||||
Official = 1,
|
||||
Verified = 2,
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Member as ApiMember, MemberCompositeKey } from "revolt-api";
|
||||
|
||||
import { Client, File } from "..";
|
||||
import type { Merge } from "../lib/merge";
|
||||
|
||||
import { Hydrate } from ".";
|
||||
|
||||
export type HydratedServerMember = {
|
||||
id: MemberCompositeKey;
|
||||
joinedAt: Date;
|
||||
nickname?: string;
|
||||
avatar?: File;
|
||||
roles: string[];
|
||||
timeout?: Date;
|
||||
};
|
||||
|
||||
export const serverMemberHydration: Hydrate<
|
||||
Merge<ApiMember>,
|
||||
HydratedServerMember
|
||||
> = {
|
||||
keyMapping: {
|
||||
_id: "id",
|
||||
joined_at: "joinedAt",
|
||||
},
|
||||
functions: {
|
||||
id: (member) => member._id,
|
||||
joinedAt: (member) => new Date(member.joined_at),
|
||||
nickname: (member) => member.nickname!,
|
||||
avatar: (member, ctx) => new File(ctx as Client, member.avatar!),
|
||||
roles: (member) => member.roles,
|
||||
timeout: (member) => new Date(member.timeout!),
|
||||
},
|
||||
initialHydration: () => ({
|
||||
roles: [],
|
||||
}),
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
import {
|
||||
User as ApiUser,
|
||||
BotInformation,
|
||||
RelationshipStatus,
|
||||
UserStatus,
|
||||
} from "revolt-api";
|
||||
|
||||
import { Client, File } from "..";
|
||||
|
||||
import { Hydrate } from ".";
|
||||
|
||||
export type HydratedUser = {
|
||||
id: string;
|
||||
username: string;
|
||||
relationship: RelationshipStatus;
|
||||
|
||||
online: boolean;
|
||||
privileged: boolean;
|
||||
|
||||
badges: UserBadges;
|
||||
flags: UserFlags;
|
||||
|
||||
avatar?: File;
|
||||
status?: UserStatus;
|
||||
bot?: BotInformation;
|
||||
};
|
||||
|
||||
export const userHydration: Hydrate<ApiUser, HydratedUser> = {
|
||||
keyMapping: {
|
||||
_id: "id",
|
||||
},
|
||||
functions: {
|
||||
id: (user) => user._id,
|
||||
username: (user) => user.username,
|
||||
relationship: (user) => user.relationship!,
|
||||
|
||||
online: (user) => user.online!,
|
||||
privileged: (user) => user.privileged,
|
||||
|
||||
badges: (user) => user.badges!,
|
||||
flags: (user) => user.flags!,
|
||||
|
||||
avatar: (user, ctx) => new File(ctx as Client, user.avatar!),
|
||||
status: (user) => user.status!,
|
||||
bot: (user) => user.bot!,
|
||||
},
|
||||
initialHydration: () => ({
|
||||
relationship: "None",
|
||||
}),
|
||||
};
|
||||
|
||||
/**
|
||||
* Badges available to users
|
||||
*/
|
||||
export enum UserBadges {
|
||||
Developer = 1,
|
||||
Translator = 2,
|
||||
Supporter = 4,
|
||||
ResponsibleDisclosure = 8,
|
||||
Founder = 16,
|
||||
PlatformModeration = 32,
|
||||
ActiveSupporter = 64,
|
||||
Paw = 128,
|
||||
EarlyAdopter = 256,
|
||||
ReservedRelevantJokeBadge1 = 512,
|
||||
ReservedRelevantJokeBadge2 = 1024,
|
||||
}
|
||||
|
||||
/**
|
||||
* Flags attributed to users
|
||||
*/
|
||||
export enum UserFlags {
|
||||
Suspended = 1,
|
||||
Deleted = 2,
|
||||
Banned = 4,
|
||||
}
|
||||
+7
-29
@@ -1,30 +1,8 @@
|
||||
export { Channel } from "./maps/Channels";
|
||||
export { Member } from "./maps/Members";
|
||||
export { Message } from "./maps/Messages";
|
||||
export { Server } from "./maps/Servers";
|
||||
export { User } from "./maps/Users";
|
||||
export { Emoji } from "./maps/Emojis";
|
||||
|
||||
export * from "./Client";
|
||||
export * from "./config";
|
||||
|
||||
export {
|
||||
UserPermission,
|
||||
Permission,
|
||||
DEFAULT_PERMISSION,
|
||||
DEFAULT_PERMISSION_DIRECT_MESSAGE,
|
||||
DEFAULT_PERMISSION_SAVED_MESSAGES,
|
||||
DEFAULT_PERMISSION_SERVER,
|
||||
DEFAULT_PERMISSION_VIEW_ONLY,
|
||||
U32_MAX,
|
||||
} from "./permissions/definitions";
|
||||
export { calculatePermission } from "./permissions/calculator";
|
||||
|
||||
export { Nullable, toNullable, toNullableDate } from "./util/null";
|
||||
export {
|
||||
ReadyPacket,
|
||||
ClientboundNotification,
|
||||
ServerboundNotification,
|
||||
} from "./websocket/notifications";
|
||||
|
||||
export * from "./classes";
|
||||
export * from "./lib/regex";
|
||||
export * from "./collections";
|
||||
export * as API from "revolt-api";
|
||||
export type { Session } from "./Client";
|
||||
export { Client, ClientOptions } from "./Client";
|
||||
export { EventClient, ConnectionState } from "./events";
|
||||
export { ServerFlags, UserBadges, UserFlags, BotFlags } from "./hydration";
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
// Merge type provided by https://dev.to/lucianbc/union-type-merging-in-typescript-9al
|
||||
export type Merge<T extends object> = {
|
||||
[k in CommonKeys<T>]: PickTypeOf<T, k>;
|
||||
} & {
|
||||
[k in NonCommonKeys<T>]?: PickTypeOf<T, k>;
|
||||
};
|
||||
|
||||
type PickTypeOf<T, K extends string | number | symbol> = K extends AllKeys<T>
|
||||
? PickType<T, K>
|
||||
: never;
|
||||
|
||||
// eslint-disable-next-line
|
||||
type PickType<T, K extends AllKeys<T>> = T extends { [k in K]?: any }
|
||||
? T[K]
|
||||
: undefined;
|
||||
|
||||
type Subtract<A, C> = A extends C ? never : A;
|
||||
type NonCommonKeys<T extends object> = Subtract<AllKeys<T>, CommonKeys<T>>;
|
||||
|
||||
// eslint-disable-next-line
|
||||
type AllKeys<T> = T extends any ? keyof T : never;
|
||||
type CommonKeys<T extends object> = keyof T;
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Regular expression for mentions.
|
||||
*/
|
||||
export const RE_MENTIONS = /<@([0-9ABCDEFGHJKMNPQRSTVWXYZ]{26})>/g;
|
||||
|
||||
/**
|
||||
* Regular expression for channels.
|
||||
*/
|
||||
export const RE_CHANNELS = /<#([0-9ABCDEFGHJKMNPQRSTVWXYZ]{26})>/g;
|
||||
|
||||
/**
|
||||
* Regular expression for spoilers.
|
||||
*/
|
||||
export const RE_SPOILER = /!!.+!!/g;
|
||||
@@ -1,119 +0,0 @@
|
||||
import { runInAction } from "mobx";
|
||||
|
||||
import { Client } from "../Client";
|
||||
|
||||
import { InviteBotDestination, DataEditBot, DataCreateBot, OwnedBotsResponse } from 'revolt-api';
|
||||
|
||||
export default class Bots {
|
||||
client: Client;
|
||||
|
||||
constructor(client: Client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a bot
|
||||
* @param id Bot ID
|
||||
* @returns Bot and User object
|
||||
*/
|
||||
async fetch(id: string) {
|
||||
const { bot, user } = await this.client.api.get(
|
||||
`/bots/${id as ''}`,
|
||||
);
|
||||
|
||||
return {
|
||||
bot,
|
||||
user: await this.client.users.fetch(user._id, user),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a bot
|
||||
* @param id Bot ID
|
||||
*/
|
||||
async delete(id: string) {
|
||||
await this.client.api.delete(`/bots/${id as ''}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a public bot
|
||||
* @param id Bot ID
|
||||
* @returns Public Bot object
|
||||
*/
|
||||
async fetchPublic(id: string) {
|
||||
return await this.client.api.get(
|
||||
`/bots/${id as ''}/invite`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invite a public bot
|
||||
* @param id Bot ID
|
||||
* @param destination The group or server to add to
|
||||
*/
|
||||
async invite(
|
||||
id: string,
|
||||
destination: InviteBotDestination,
|
||||
) {
|
||||
return await this.client.api.post(
|
||||
`/bots/${id as ''}/invite`,
|
||||
destination,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a bot
|
||||
* @param id Bot ID
|
||||
* @returns Bot and User objects
|
||||
*/
|
||||
async fetchOwned() {
|
||||
const { bots, users: userObjects } = await this.client.api.get(
|
||||
`/bots/@me`,
|
||||
) as OwnedBotsResponse;
|
||||
|
||||
const users = [];
|
||||
for (const obj of userObjects) {
|
||||
users.push(await this.client.users.fetch(obj._id, obj));
|
||||
}
|
||||
|
||||
return { bots, users };
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit a bot
|
||||
* @param id Bot ID
|
||||
* @param data Bot edit data object
|
||||
*/
|
||||
async edit(id: string, data: DataEditBot) {
|
||||
await this.client.api.patch(`/bots/${id as ''}`, data);
|
||||
|
||||
if (data.name) {
|
||||
const user = this.client.users.get(id);
|
||||
if (user) {
|
||||
runInAction(() => {
|
||||
user!.username = data.name!;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a bot
|
||||
* @param data Bot creation data
|
||||
*/
|
||||
async create(data: DataCreateBot) {
|
||||
const bot = await this.client.api.post("/bots/create", data);
|
||||
const user = await this.client.users.fetch(bot._id, {
|
||||
_id: bot._id,
|
||||
username: data.name,
|
||||
bot: {
|
||||
owner: this.client.user!._id,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
bot,
|
||||
user,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,774 +0,0 @@
|
||||
import type {
|
||||
Channel as ChannelI,
|
||||
DataCreateGroup,
|
||||
DataEditChannel,
|
||||
DataMessageSend,
|
||||
FieldsChannel,
|
||||
Message as MessageI,
|
||||
OptionsMessageSearch,
|
||||
} from "revolt-api";
|
||||
import type { File } from "revolt-api";
|
||||
import type { Member } from "revolt-api";
|
||||
import type { User } from "revolt-api";
|
||||
|
||||
import { action, computed, makeAutoObservable, runInAction } from "mobx";
|
||||
import isEqual from "lodash.isequal";
|
||||
import { decodeTime, ulid } from "ulid";
|
||||
|
||||
import { Nullable, toNullable } from "../util/null";
|
||||
import Collection from "./Collection";
|
||||
import { Message } from "./Messages";
|
||||
import { Client, FileArgs } from "..";
|
||||
import { Permission } from "../permissions/definitions";
|
||||
import { INotificationChecker } from "../util/Unreads";
|
||||
import { Override, OverrideField } from "revolt-api";
|
||||
import type { APIRoutes } from "revolt-api/dist/routes";
|
||||
import { bitwiseAndEq, calculatePermission } from "../permissions/calculator";
|
||||
|
||||
export class Channel {
|
||||
client: Client;
|
||||
|
||||
_id: string;
|
||||
channel_type: ChannelI["channel_type"];
|
||||
|
||||
/**
|
||||
* Whether this DM is active.
|
||||
* @requires `DirectMessage`
|
||||
*/
|
||||
active: Nullable<boolean> = null;
|
||||
|
||||
/**
|
||||
* The ID of the group owner.
|
||||
* @requires `Group`
|
||||
*/
|
||||
owner_id: Nullable<string> = null;
|
||||
|
||||
/**
|
||||
* The ID of the server this channel is in.
|
||||
* @requires `TextChannel`, `VoiceChannel`
|
||||
*/
|
||||
server_id: Nullable<string> = null;
|
||||
|
||||
/**
|
||||
* Permissions for group members.
|
||||
* @requires `Group`
|
||||
*/
|
||||
permissions: Nullable<number> = null;
|
||||
|
||||
/**
|
||||
* Default server channel permissions.
|
||||
* @requires `TextChannel`, `VoiceChannel`
|
||||
*/
|
||||
default_permissions: Nullable<OverrideField> = null;
|
||||
|
||||
/**
|
||||
* Channel permissions for each role.
|
||||
* @requires `TextChannel`, `VoiceChannel`
|
||||
*/
|
||||
role_permissions: Nullable<{ [key: string]: OverrideField }> = null;
|
||||
|
||||
/**
|
||||
* Channel name.
|
||||
* @requires `Group`, `TextChannel`, `VoiceChannel`
|
||||
*/
|
||||
name: Nullable<string> = null;
|
||||
|
||||
/**
|
||||
* Channel icon.
|
||||
* @requires `Group`, `TextChannel`, `VoiceChannel`
|
||||
*/
|
||||
icon: Nullable<File> = null;
|
||||
|
||||
/**
|
||||
* Channel description.
|
||||
* @requires `Group`, `TextChannel`, `VoiceChannel`
|
||||
*/
|
||||
description: Nullable<string> = null;
|
||||
|
||||
/**
|
||||
* Group / DM members.
|
||||
* @requires `Group`, `DM`
|
||||
*/
|
||||
recipient_ids: Nullable<string[]> = null;
|
||||
|
||||
/**
|
||||
* Id of last message in channel.
|
||||
* @requires `Group`, `DM`, `TextChannel`, `VoiceChannel`
|
||||
*/
|
||||
last_message_id: Nullable<string> = null;
|
||||
|
||||
/**
|
||||
* Users typing in channel.
|
||||
*/
|
||||
typing_ids: Set<string> = new Set();
|
||||
|
||||
/**
|
||||
* Channel is not safe for work.
|
||||
* @requires `Group`, `TextChannel`, `VoiceChannel`
|
||||
*/
|
||||
nsfw: Nullable<boolean> = null;
|
||||
|
||||
/**
|
||||
* The group owner.
|
||||
* @requires `Group`
|
||||
*/
|
||||
get owner() {
|
||||
if (this.owner_id === null) return;
|
||||
return this.client.users.get(this.owner_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Server this channel belongs to.
|
||||
* @requires `Server`
|
||||
*/
|
||||
get server() {
|
||||
if (this.server_id === null) return;
|
||||
return this.client.servers.get(this.server_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* The DM recipient.
|
||||
* @requires `DM`
|
||||
*/
|
||||
get recipient() {
|
||||
const user_id = this.recipient_ids?.find(
|
||||
(x) => this.client.user!._id !== x,
|
||||
);
|
||||
if (!user_id) return;
|
||||
|
||||
return this.client.users.get(user_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Last message sent in this channel.
|
||||
* @requires `Group`, `DM`, `TextChannel`, `VoiceChannel`
|
||||
*/
|
||||
get last_message() {
|
||||
const id = this.last_message_id;
|
||||
if (!id) return;
|
||||
|
||||
return this.client.messages.get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last message ID if it is present or the origin timestamp.
|
||||
* TODO: deprecate
|
||||
*/
|
||||
get last_message_id_or_past() {
|
||||
return this.last_message_id ?? "0";
|
||||
}
|
||||
|
||||
/**
|
||||
* Group recipients.
|
||||
* @requires `Group`
|
||||
*/
|
||||
get recipients() {
|
||||
return this.recipient_ids?.map((id) => this.client.users.get(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Users typing.
|
||||
*/
|
||||
get typing() {
|
||||
return Array.from(this.typing_ids).map((id) =>
|
||||
this.client.users.get(id),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get timestamp when this channel was created.
|
||||
*/
|
||||
get createdAt() {
|
||||
return decodeTime(this._id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get timestamp when this channel last had a message sent or when it was created
|
||||
*/
|
||||
get updatedAt() {
|
||||
return this.last_message_id
|
||||
? decodeTime(this.last_message_id)
|
||||
: this.createdAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Absolute pathname to this channel in the client.
|
||||
*/
|
||||
get path() {
|
||||
if (this.server_id) {
|
||||
return `/server/${this.server_id}/channel/${this._id}`;
|
||||
} else {
|
||||
return `/channel/${this._id}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get URL to this channel.
|
||||
*/
|
||||
get url() {
|
||||
return this.client.configuration?.app + this.path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the channel is currently unread
|
||||
* @param permit Callback function to determine whether a channel has certain properties
|
||||
* @returns Whether the channel is unread
|
||||
*/
|
||||
@computed isUnread(permit: INotificationChecker) {
|
||||
if (permit.isMuted(this)) return false;
|
||||
return this.unread;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all message IDs of unread messages
|
||||
* @param permit Callback function to determine whether a channel has certain properties
|
||||
* @returns Array of message IDs which are unread
|
||||
*/
|
||||
@computed getMentions(permit: INotificationChecker) {
|
||||
if (permit.isMuted(this)) return [];
|
||||
return this.mentions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get whether this channel is unread.
|
||||
*/
|
||||
get unread() {
|
||||
if (
|
||||
!this.last_message_id ||
|
||||
this.channel_type === "SavedMessages" ||
|
||||
this.channel_type === "VoiceChannel"
|
||||
)
|
||||
return false;
|
||||
|
||||
return (
|
||||
(
|
||||
this.client.unreads?.getUnread(this._id)?.last_id ?? "0"
|
||||
).localeCompare(this.last_message_id) === -1
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get mentions in this channel for user.
|
||||
*/
|
||||
get mentions() {
|
||||
if (
|
||||
this.channel_type === "SavedMessages" ||
|
||||
this.channel_type === "VoiceChannel"
|
||||
)
|
||||
return [];
|
||||
return this.client.unreads?.getUnread(this._id)?.mentions ?? [];
|
||||
}
|
||||
|
||||
constructor(client: Client, data: ChannelI) {
|
||||
this.client = client;
|
||||
|
||||
this._id = data._id;
|
||||
this.channel_type = data.channel_type;
|
||||
|
||||
switch (data.channel_type) {
|
||||
case "DirectMessage": {
|
||||
this.active = toNullable(data.active);
|
||||
this.recipient_ids = toNullable(data.recipients);
|
||||
this.last_message_id = toNullable(data.last_message_id);
|
||||
break;
|
||||
}
|
||||
case "Group": {
|
||||
this.recipient_ids = toNullable(data.recipients);
|
||||
this.name = toNullable(data.name);
|
||||
this.owner_id = toNullable(data.owner);
|
||||
this.description = toNullable(data.description);
|
||||
this.last_message_id = toNullable(data.last_message_id);
|
||||
this.icon = toNullable(data.icon);
|
||||
this.permissions = toNullable(data.permissions);
|
||||
this.nsfw = toNullable(data.nsfw);
|
||||
break;
|
||||
}
|
||||
case "TextChannel":
|
||||
case "VoiceChannel": {
|
||||
this.server_id = toNullable(data.server);
|
||||
this.name = toNullable(data.name);
|
||||
this.description = toNullable(data.description);
|
||||
this.icon = toNullable(data.icon);
|
||||
this.default_permissions = toNullable(data.default_permissions);
|
||||
this.role_permissions = toNullable(data.role_permissions);
|
||||
|
||||
if (data.channel_type === "TextChannel") {
|
||||
this.last_message_id = toNullable(data.last_message_id);
|
||||
this.nsfw = toNullable(data.nsfw);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
makeAutoObservable(this, {
|
||||
_id: false,
|
||||
client: false,
|
||||
});
|
||||
}
|
||||
|
||||
@action update(data: Partial<ChannelI>, clear: FieldsChannel[] = []) {
|
||||
const apply = (key: string, target?: string) => {
|
||||
if (
|
||||
// @ts-expect-error TODO: clean up types here
|
||||
typeof data[key] !== "undefined" &&
|
||||
// @ts-expect-error TODO: clean up types here
|
||||
!isEqual(this[target ?? key], data[key])
|
||||
) {
|
||||
// @ts-expect-error TODO: clean up types here
|
||||
this[target ?? key] = data[key];
|
||||
}
|
||||
};
|
||||
|
||||
for (const entry of clear) {
|
||||
switch (entry) {
|
||||
case "Description":
|
||||
this.description = null;
|
||||
break;
|
||||
case "Icon":
|
||||
this.icon = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
apply("active");
|
||||
apply("owner", "owner_id");
|
||||
apply("permissions");
|
||||
apply("default_permissions");
|
||||
apply("role_permissions");
|
||||
apply("name");
|
||||
apply("icon");
|
||||
apply("description");
|
||||
apply("recipients", "recipient_ids");
|
||||
apply("last_message_id");
|
||||
apply("nsfw");
|
||||
}
|
||||
|
||||
@action updateGroupJoin(user: string) {
|
||||
this.recipient_ids?.push(user);
|
||||
}
|
||||
|
||||
@action updateGroupLeave(user: string) {
|
||||
this.recipient_ids = toNullable(
|
||||
this.recipient_ids?.filter((x) => x !== user),
|
||||
);
|
||||
}
|
||||
|
||||
@action updateStartTyping(id: string) {
|
||||
this.typing_ids.add(id);
|
||||
}
|
||||
|
||||
@action updateStopTyping(id: string) {
|
||||
this.typing_ids.delete(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a channel's members.
|
||||
* @requires `Group`
|
||||
* @returns An array of the channel's members.
|
||||
*/
|
||||
async fetchMembers() {
|
||||
const members = await this.client.api.get(
|
||||
`/channels/${this._id as ""}/members`,
|
||||
);
|
||||
|
||||
return members.map(this.client.users.createObj);
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit a channel
|
||||
* @param data Edit data
|
||||
*/
|
||||
async edit(data: DataEditChannel) {
|
||||
this.update(
|
||||
await this.client.api.patch(`/channels/${this._id as ""}`, data),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a channel
|
||||
* @requires `DM`, `Group`, `TextChannel`, `VoiceChannel`
|
||||
*/
|
||||
async delete(leave_silently?: boolean, avoidReq?: boolean) {
|
||||
if (!avoidReq)
|
||||
await this.client.api.delete(`/channels/${this._id as ""}`, {
|
||||
leave_silently,
|
||||
});
|
||||
|
||||
runInAction(() => {
|
||||
if (this.channel_type === "DirectMessage") {
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
this.channel_type === "TextChannel" ||
|
||||
this.channel_type === "VoiceChannel"
|
||||
) {
|
||||
const server = this.server;
|
||||
if (server) {
|
||||
server.channel_ids = server.channel_ids.filter(
|
||||
(x) => x !== this._id,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.client.channels.delete(this._id);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a user to a group
|
||||
* @param user_id ID of the target user
|
||||
*/
|
||||
async addMember(user_id: string) {
|
||||
return await this.client.api.put(
|
||||
`/channels/${this._id as ""}/recipients/${user_id as ""}`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a user from a group
|
||||
* @param user_id ID of the target user
|
||||
*/
|
||||
async removeMember(user_id: string) {
|
||||
return await this.client.api.delete(
|
||||
`/channels/${this._id as ""}/recipients/${user_id as ""}`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message
|
||||
* @param data Either the message as a string or message sending route data
|
||||
* @returns The message
|
||||
*/
|
||||
async sendMessage(
|
||||
data: string | DataMessageSend,
|
||||
idempotencyKey: string = ulid(),
|
||||
) {
|
||||
const msg: DataMessageSend =
|
||||
typeof data === "string" ? { content: data } : data;
|
||||
|
||||
const message = await this.client.api.post(
|
||||
`/channels/${this._id as ""}/messages`,
|
||||
msg,
|
||||
{
|
||||
headers: {
|
||||
"Idempotency-Key": idempotencyKey,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return this.client.messages.createObj(message, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a message by its ID
|
||||
* @param message_id ID of the target message
|
||||
* @returns The message
|
||||
*/
|
||||
async fetchMessage(message_id: string) {
|
||||
const message = await this.client.api.get(
|
||||
`/channels/${this._id as ""}/messages/${message_id as ""}`,
|
||||
);
|
||||
|
||||
return this.client.messages.createObj(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch multiple messages from a channel
|
||||
* @param params Message fetching route data
|
||||
* @returns The messages
|
||||
*/
|
||||
async fetchMessages(
|
||||
params?: Omit<
|
||||
(APIRoutes & {
|
||||
method: "get";
|
||||
path: "/channels/{target}/messages";
|
||||
})["params"],
|
||||
"include_users"
|
||||
>,
|
||||
) {
|
||||
const messages = (await this.client.api.get(
|
||||
`/channels/${this._id as ""}/messages`,
|
||||
{ ...params },
|
||||
)) as MessageI[];
|
||||
return runInAction(() => messages.map(this.client.messages.createObj));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch multiple messages from a channel including the users that sent them
|
||||
* @param params Message fetching route data
|
||||
* @returns Object including messages and users
|
||||
*/
|
||||
async fetchMessagesWithUsers(
|
||||
params?: Omit<
|
||||
(APIRoutes & {
|
||||
method: "get";
|
||||
path: "/channels/{target}/messages";
|
||||
})["params"],
|
||||
"include_users"
|
||||
>,
|
||||
) {
|
||||
const data = (await this.client.api.get(
|
||||
`/channels/${this._id as ""}/messages`,
|
||||
{ ...params, include_users: true },
|
||||
)) as { messages: MessageI[]; users: User[]; members?: Member[] };
|
||||
return runInAction(() => {
|
||||
return {
|
||||
messages: data.messages.map(this.client.messages.createObj),
|
||||
users: data.users.map(this.client.users.createObj),
|
||||
members: data.members?.map(this.client.members.createObj),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for messages
|
||||
* @param params Message searching route data
|
||||
* @returns The messages
|
||||
*/
|
||||
async search(params: Omit<OptionsMessageSearch, "include_users">) {
|
||||
const messages = (await this.client.api.post(
|
||||
`/channels/${this._id as ""}/search`,
|
||||
params,
|
||||
)) as MessageI[];
|
||||
return runInAction(() => messages.map(this.client.messages.createObj));
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for messages including the users that sent them
|
||||
* @param params Message searching route data
|
||||
* @returns The messages
|
||||
*/
|
||||
async searchWithUsers(params: Omit<OptionsMessageSearch, "include_users">) {
|
||||
const data = (await this.client.api.post(
|
||||
`/channels/${this._id as ""}/search`,
|
||||
{ ...params, include_users: true },
|
||||
)) as { messages: MessageI[]; users: User[]; members?: Member[] };
|
||||
return runInAction(() => {
|
||||
return {
|
||||
messages: data.messages.map(this.client.messages.createObj),
|
||||
users: data.users.map(this.client.users.createObj),
|
||||
members: data.members?.map(this.client.members.createObj),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch stale messages
|
||||
* @param ids IDs of the target messages
|
||||
* @returns The stale messages
|
||||
*/
|
||||
async fetchStale(ids: string[]) {
|
||||
/*const data = await this.client.api.post(
|
||||
`/channels/${this._id as ''}/messages/stale`,
|
||||
{ ids },
|
||||
);
|
||||
|
||||
runInAction(() => {
|
||||
data.deleted.forEach((id) => this.client.messages.delete(id));
|
||||
data.updated.forEach((data) =>
|
||||
this.client.messages.get(data._id)?.update(data),
|
||||
);
|
||||
});
|
||||
|
||||
return data;*/
|
||||
return { deprecated: ids };
|
||||
}
|
||||
|
||||
async deleteMessages(ids: string[]) {
|
||||
await this.client.api.delete(
|
||||
`/channels/${this._id as ""}/messages/bulk`,
|
||||
{ data: { ids } },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an invite to the channel
|
||||
* @returns Newly created invite code
|
||||
*/
|
||||
async createInvite() {
|
||||
return await this.client.api.post(
|
||||
`/channels/${this._id as ""}/invites`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Join a call in a channel
|
||||
* @returns Join call response data
|
||||
*/
|
||||
async joinCall() {
|
||||
return await this.client.api.post(
|
||||
`/channels/${this._id as ""}/join_call`,
|
||||
);
|
||||
}
|
||||
|
||||
private ackTimeout?: number;
|
||||
private ackLimit?: number;
|
||||
|
||||
/**
|
||||
* Mark a channel as read
|
||||
* @param message Last read message or its ID
|
||||
* @param skipRateLimiter Whether to skip the internal rate limiter
|
||||
*/
|
||||
async ack(message?: Message | string, skipRateLimiter?: boolean) {
|
||||
const id =
|
||||
(typeof message === "string" ? message : message?._id) ??
|
||||
this.last_message_id ??
|
||||
ulid();
|
||||
const performAck = () => {
|
||||
delete this.ackLimit;
|
||||
this.client.api.put(`/channels/${this._id}/ack/${id as ""}`);
|
||||
};
|
||||
|
||||
if (!this.client.options.ackRateLimiter || skipRateLimiter)
|
||||
return performAck();
|
||||
|
||||
clearTimeout(this.ackTimeout);
|
||||
if (this.ackLimit && +new Date() > this.ackLimit) {
|
||||
performAck();
|
||||
}
|
||||
|
||||
// We need to use setTimeout here for both Node.js and browser.
|
||||
this.ackTimeout = setTimeout(performAck, 5000) as unknown as number;
|
||||
|
||||
if (!this.ackLimit) {
|
||||
this.ackLimit = +new Date() + 15e3;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set role permissions
|
||||
* @param role_id Role Id, set to 'default' to affect all users
|
||||
* @param permissions Permission value
|
||||
*/
|
||||
async setPermissions(role_id = "default", permissions: Override) {
|
||||
return await this.client.api.put(
|
||||
`/channels/${this._id as ""}/permissions/${role_id as ""}`,
|
||||
{ permissions },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start typing in this channel
|
||||
*/
|
||||
startTyping() {
|
||||
this.client.websocket.send({ type: "BeginTyping", channel: this._id });
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop typing in this channel
|
||||
*/
|
||||
stopTyping() {
|
||||
this.client.websocket.send({ type: "EndTyping", channel: this._id });
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate URL to icon for this channel
|
||||
* @param args File parameters
|
||||
* @returns File URL
|
||||
*/
|
||||
generateIconURL(...args: FileArgs) {
|
||||
if (this.channel_type === "DirectMessage") {
|
||||
return this.client.generateFileURL(
|
||||
this.recipient?.avatar ?? undefined,
|
||||
...args,
|
||||
);
|
||||
}
|
||||
|
||||
return this.client.generateFileURL(this.icon ?? undefined, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Permission the currently authenticated user has against this channel
|
||||
*/
|
||||
@computed get permission() {
|
||||
return calculatePermission(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether we have a given permission in a channel
|
||||
* @param permission Permission Names
|
||||
* @returns Whether we have this permission
|
||||
*/
|
||||
@computed havePermission(...permission: (keyof typeof Permission)[]) {
|
||||
return bitwiseAndEq(
|
||||
this.permission,
|
||||
...permission.map((x) => Permission[x]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default class Channels extends Collection<string, Channel> {
|
||||
constructor(client: Client) {
|
||||
super(client);
|
||||
this.createObj = this.createObj.bind(this);
|
||||
}
|
||||
|
||||
@action $get(id: string, data?: ChannelI) {
|
||||
const channel = this.get(id)!;
|
||||
if (data) channel.update(data);
|
||||
return channel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a channel should currently exist
|
||||
* @param id Channel ID
|
||||
* @returns Whether it should current exist
|
||||
*/
|
||||
exists(id: string) {
|
||||
const channel = this.get(id);
|
||||
if (channel) {
|
||||
switch (channel.channel_type) {
|
||||
case "DirectMessage":
|
||||
return channel.active;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a channel
|
||||
* @param id Channel ID
|
||||
* @returns The channel
|
||||
*/
|
||||
async fetch(id: string, data?: ChannelI) {
|
||||
if (this.has(id)) return this.$get(id);
|
||||
const res =
|
||||
data ?? (await this.client.api.get(`/channels/${id as ""}`));
|
||||
return this.createObj(res);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a channel object.
|
||||
* This is meant for internal use only.
|
||||
* @param data: Channel Data
|
||||
* @param emit Whether to emit creation event
|
||||
* @returns Channel
|
||||
*/
|
||||
createObj(data: ChannelI, emit?: boolean | number) {
|
||||
if (this.has(data._id)) return this.$get(data._id);
|
||||
const channel = new Channel(this.client, data);
|
||||
|
||||
runInAction(() => {
|
||||
this.set(data._id, channel);
|
||||
});
|
||||
|
||||
if (emit === true) this.client.emit("channel/create", channel);
|
||||
return channel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a group
|
||||
* @param data Group create route data
|
||||
* @returns The newly-created group
|
||||
*/
|
||||
async createGroup(data: DataCreateGroup) {
|
||||
const group = await this.client.api.post(`/channels/create`, data);
|
||||
return (await this.fetch(group._id, group))!;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { ObservableMap } from "mobx";
|
||||
|
||||
import { Client } from "../Client";
|
||||
|
||||
export default class Collection<K, V> extends ObservableMap<K, V> {
|
||||
client: Client;
|
||||
|
||||
constructor(client: Client) {
|
||||
super();
|
||||
this.client = client;
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
import type { Emoji as EmojiI, EmojiParent } from "revolt-api";
|
||||
|
||||
import { makeAutoObservable, runInAction } from "mobx";
|
||||
|
||||
import Collection from "./Collection";
|
||||
import { Client } from "..";
|
||||
import { decodeTime } from "ulid";
|
||||
|
||||
export class Emoji {
|
||||
client: Client;
|
||||
|
||||
_id: string;
|
||||
name: string;
|
||||
creator_id: string;
|
||||
parent: EmojiParent;
|
||||
animated: boolean;
|
||||
nsfw: boolean;
|
||||
|
||||
/**
|
||||
* Get timestamp when this message was created.
|
||||
*/
|
||||
get createdAt() {
|
||||
return decodeTime(this._id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get creator of this emoji.
|
||||
*/
|
||||
get creator() {
|
||||
return this.client.users.get(this.creator_id);
|
||||
}
|
||||
|
||||
constructor(client: Client, data: EmojiI) {
|
||||
this.client = client;
|
||||
this._id = data._id;
|
||||
|
||||
this.name = data.name;
|
||||
this.creator_id = data.creator_id;
|
||||
this.parent = data.parent;
|
||||
this.animated = data.animated ?? false;
|
||||
this.nsfw = data.nsfw ?? false;
|
||||
|
||||
makeAutoObservable(this, {
|
||||
_id: false,
|
||||
client: false,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a message
|
||||
*/
|
||||
async delete() {
|
||||
return await this.client.api.delete(`/custom/emoji/${this._id as ""}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate emoji URL
|
||||
*/
|
||||
get imageURL() {
|
||||
return `${this.client.configuration?.features.autumn.url}/emojis/${
|
||||
this._id
|
||||
}${this.animated ? "" : "?max_side=128"}`;
|
||||
}
|
||||
}
|
||||
|
||||
export default class Emojis extends Collection<string, Emoji> {
|
||||
constructor(client: Client) {
|
||||
super(client);
|
||||
this.createObj = this.createObj.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an emoji object.
|
||||
* This is meant for internal use only.
|
||||
* @param data Emoji Data
|
||||
* @param emit Whether to emit creation event
|
||||
* @returns Emoji
|
||||
*/
|
||||
createObj(data: EmojiI, emit?: boolean | number) {
|
||||
if (this.has(data._id)) return this.get(data._id)!;
|
||||
const emoji = new Emoji(this.client, data);
|
||||
|
||||
runInAction(() => {
|
||||
this.set(data._id, emoji);
|
||||
});
|
||||
|
||||
if (emit === true) this.client.emit("emoji/create", emoji);
|
||||
return emoji;
|
||||
}
|
||||
}
|
||||
@@ -1,342 +0,0 @@
|
||||
import type {
|
||||
DataMemberEdit,
|
||||
FieldsMember,
|
||||
Member as MemberI,
|
||||
MemberCompositeKey,
|
||||
Role,
|
||||
} from "revolt-api";
|
||||
import type { File } from "revolt-api";
|
||||
|
||||
import { makeAutoObservable, runInAction, action, computed } from "mobx";
|
||||
import isEqual from "lodash.isequal";
|
||||
|
||||
import { Nullable, toNullable, toNullableDate } from "../util/null";
|
||||
import Collection from "./Collection";
|
||||
import { Channel, Client, FileArgs, Server } from "..";
|
||||
import { bitwiseAndEq, calculatePermission } from "../permissions/calculator";
|
||||
import { Permission } from "../permissions/definitions";
|
||||
|
||||
export class Member {
|
||||
client: Client;
|
||||
|
||||
_id: MemberCompositeKey;
|
||||
joined_at: Date;
|
||||
|
||||
nickname: Nullable<string> = null;
|
||||
avatar: Nullable<File> = null;
|
||||
roles: Nullable<string[]> = null;
|
||||
timeout: Nullable<Date> = null;
|
||||
|
||||
private _timeout: number | undefined;
|
||||
|
||||
/**
|
||||
* Associated user.
|
||||
*/
|
||||
get user() {
|
||||
return this.client.users.get(this._id.user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Associated server.
|
||||
*/
|
||||
get server() {
|
||||
return this.client.servers.get(this._id.server);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the client has a higher rank than this member.
|
||||
*/
|
||||
get inferior() {
|
||||
return (this.server?.member?.ranking ?? Infinity) < this.ranking;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the client can kick this user.
|
||||
*/
|
||||
get kickable() {
|
||||
return this.server?.havePermission("KickMembers") && this.inferior;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the client can ban this user.
|
||||
*/
|
||||
get bannable() {
|
||||
return this.server?.havePermission("BanMembers") && this.inferior;
|
||||
}
|
||||
|
||||
constructor(client: Client, data: MemberI) {
|
||||
this.client = client;
|
||||
this._id = data._id;
|
||||
this.joined_at = new Date(data.joined_at);
|
||||
|
||||
this.nickname = toNullable(data.nickname);
|
||||
this.avatar = toNullable(data.avatar);
|
||||
this.roles = toNullable(data.roles);
|
||||
this.timeout = toNullableDate(data.timeout);
|
||||
|
||||
this.scheduleTimeout();
|
||||
|
||||
makeAutoObservable(this, {
|
||||
_id: false,
|
||||
client: false,
|
||||
});
|
||||
}
|
||||
|
||||
@action update(data: Partial<MemberI>, clear: FieldsMember[] = []) {
|
||||
const apply = (key: string) => {
|
||||
// This code has been tested.
|
||||
if (
|
||||
// @ts-expect-error TODO: clean up types here
|
||||
typeof data[key] !== "undefined" &&
|
||||
// @ts-expect-error TODO: clean up types here
|
||||
!isEqual(this[key], data[key])
|
||||
) {
|
||||
// @ts-expect-error TODO: clean up types here
|
||||
this[key] =
|
||||
// @ts-expect-error TODO: clean up types here
|
||||
key === "timeout" ? toNullableDate(data[key]) : data[key];
|
||||
}
|
||||
};
|
||||
|
||||
for (const field of clear) {
|
||||
switch (field) {
|
||||
case "Nickname":
|
||||
this.nickname = null;
|
||||
break;
|
||||
case "Avatar":
|
||||
this.avatar = null;
|
||||
break;
|
||||
case "Roles":
|
||||
this.roles = [];
|
||||
break;
|
||||
case "Timeout":
|
||||
this.timeout = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
apply("nickname");
|
||||
apply("avatar");
|
||||
apply("roles");
|
||||
apply("timeout");
|
||||
|
||||
this.scheduleTimeout();
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule timeout revocation
|
||||
*/
|
||||
private scheduleTimeout() {
|
||||
delete this._timeout;
|
||||
clearTimeout(this._timeout);
|
||||
|
||||
if (this.timeout) {
|
||||
const offset = +this.timeout - +new Date();
|
||||
if (offset > 0) {
|
||||
this._timeout = setTimeout(() => {
|
||||
runInAction(() => {
|
||||
this.timeout = null;
|
||||
delete this._timeout;
|
||||
});
|
||||
}, offset) as unknown as number;
|
||||
} else {
|
||||
runInAction(() => {
|
||||
this.timeout = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit a server member
|
||||
* @param data Member editing route data
|
||||
* @returns Server member object
|
||||
*/
|
||||
async edit(data: DataMemberEdit) {
|
||||
return await this.client.api.patch(
|
||||
`/servers/${this._id.server as ""}/members/${this._id.user as ""}`,
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Kick server member
|
||||
* @param user_id User ID
|
||||
*/
|
||||
async kick() {
|
||||
return await this.client.api.delete(
|
||||
`/servers/${this._id.server as ""}/members/${this._id.user as ""}`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an ordered list of roles for this member, from lowest to highest priority.
|
||||
*/
|
||||
@computed get orderedRoles() {
|
||||
const member_roles = new Set(this.roles);
|
||||
const server = this.server!;
|
||||
|
||||
return Object.keys(server.roles ?? {})
|
||||
.filter((x) => member_roles.has(x))
|
||||
.map(
|
||||
(role_id) =>
|
||||
[role_id, server.roles![role_id]] as [string, Role],
|
||||
)
|
||||
.sort(([, a], [, b]) => b.rank! - a.rank!);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get this member's currently hoisted role.
|
||||
*/
|
||||
@computed get hoistedRole() {
|
||||
const roles = this.orderedRoles.filter((x) => x[1].hoist);
|
||||
if (roles.length > 0) {
|
||||
const [id, role] = roles[roles.length - 1];
|
||||
return {
|
||||
id,
|
||||
...role
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get this member's current role colour.
|
||||
*/
|
||||
@computed get roleColour() {
|
||||
const roles = this.orderedRoles.filter((x) => x[1].colour);
|
||||
if (roles.length > 0) {
|
||||
return roles[roles.length - 1][1].colour;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get this member's ranking.
|
||||
* Smaller values are ranked as higher priotity.
|
||||
*/
|
||||
@computed get ranking() {
|
||||
if (this._id.user === this.server?.owner) {
|
||||
return -Infinity;
|
||||
}
|
||||
|
||||
const roles = this.orderedRoles;
|
||||
if (roles.length > 0) {
|
||||
return roles[roles.length - 1][1].rank!;
|
||||
} else {
|
||||
return Infinity;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a pre-configured avatar URL of a member
|
||||
*/
|
||||
get avatarURL() {
|
||||
return this.generateAvatarURL({ max_side: 256 }) ?? this.user?.avatarURL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a pre-configured animated avatar URL of a member
|
||||
*/
|
||||
get animatedAvatarURL() {
|
||||
return this.generateAvatarURL({ max_side: 256 }, true) ?? this.user?.animatedAvatarURL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate URL to this member's avatar
|
||||
* @param args File parameters
|
||||
* @returns File URL
|
||||
*/
|
||||
@computed generateAvatarURL(...args: FileArgs) {
|
||||
return this.client.generateFileURL(this.avatar ?? undefined, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the permissions that this member has against a certain object
|
||||
* @param target Target object to check permissions against
|
||||
* @returns Permissions that this member has
|
||||
*/
|
||||
@computed getPermissions(target: Server | Channel) {
|
||||
return calculatePermission(target, { member: this });
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a member has a certain permission against a certain object
|
||||
* @param target Target object to check permissions against
|
||||
* @param permission Permission names to check for
|
||||
* @returns Whether the member has this permission
|
||||
*/
|
||||
@computed hasPermission(
|
||||
target: Server | Channel,
|
||||
...permission: (keyof typeof Permission)[]
|
||||
) {
|
||||
return bitwiseAndEq(
|
||||
this.getPermissions(target),
|
||||
...permission.map((x) => Permission[x]),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the target member has a higher rank than this member.
|
||||
* @param target The member to compare against
|
||||
* @returns Whether this member is inferior to the target
|
||||
*/
|
||||
@computed inferiorTo(target: Member) {
|
||||
return target.ranking < this.ranking;
|
||||
}
|
||||
}
|
||||
|
||||
export default class Members extends Collection<string, Member> {
|
||||
constructor(client: Client) {
|
||||
super(client);
|
||||
this.createObj = this.createObj.bind(this);
|
||||
}
|
||||
|
||||
static toKey(id: MemberCompositeKey) {
|
||||
return JSON.stringify(id, Object.keys(id).sort());
|
||||
}
|
||||
|
||||
hasKey(id: MemberCompositeKey) {
|
||||
return super.has(Members.toKey(id));
|
||||
}
|
||||
|
||||
getKey(id: MemberCompositeKey) {
|
||||
return super.get(Members.toKey(id));
|
||||
}
|
||||
|
||||
setKey(id: MemberCompositeKey, member: Member) {
|
||||
return super.set(Members.toKey(id), member);
|
||||
}
|
||||
|
||||
deleteKey(id: MemberCompositeKey) {
|
||||
return super.delete(Members.toKey(id));
|
||||
}
|
||||
|
||||
@action $get(id: MemberCompositeKey, data?: MemberI) {
|
||||
const member = this.getKey(id)!;
|
||||
if (data) member.update(data);
|
||||
return member;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a member object.
|
||||
* This is meant for internal use only.
|
||||
* @param data: Member Data
|
||||
* @param emit Whether to emit creation event
|
||||
* @returns Member
|
||||
*/
|
||||
createObj(data: MemberI, emit?: boolean | number) {
|
||||
if (this.hasKey(data._id)) return this.$get(data._id, data);
|
||||
|
||||
const member = new Member(this.client, data);
|
||||
|
||||
runInAction(() => {
|
||||
this.setKey(data._id, member);
|
||||
});
|
||||
|
||||
if (emit === true) this.client.emit("member/join", member);
|
||||
return member;
|
||||
}
|
||||
}
|
||||
@@ -1,341 +0,0 @@
|
||||
import type {
|
||||
DataEditMessage,
|
||||
DataMessageSend,
|
||||
Embed,
|
||||
Interactions,
|
||||
Masquerade,
|
||||
Message as MessageI,
|
||||
SystemMessage,
|
||||
} from "revolt-api";
|
||||
import type { File } from "revolt-api";
|
||||
|
||||
import {
|
||||
makeAutoObservable,
|
||||
runInAction,
|
||||
action,
|
||||
computed,
|
||||
ObservableMap,
|
||||
ObservableSet,
|
||||
} from "mobx";
|
||||
import isEqual from "lodash.isequal";
|
||||
|
||||
import { Nullable, toNullable, toNullableDate } from "../util/null";
|
||||
import Collection from "./Collection";
|
||||
import { Client } from "..";
|
||||
import { decodeTime } from "ulid";
|
||||
|
||||
export class Message {
|
||||
client: Client;
|
||||
|
||||
_id: string;
|
||||
nonce?: string;
|
||||
channel_id: string;
|
||||
author_id: string;
|
||||
|
||||
content: Nullable<string>;
|
||||
system: Nullable<SystemMessage>;
|
||||
attachments: Nullable<File[]>;
|
||||
edited: Nullable<Date>;
|
||||
embeds: Nullable<Embed[]>;
|
||||
mention_ids: Nullable<string[]>;
|
||||
reply_ids: Nullable<string[]>;
|
||||
masquerade: Nullable<Masquerade>;
|
||||
reactions: ObservableMap<string, ObservableSet<string>>;
|
||||
interactions: Nullable<Interactions>;
|
||||
|
||||
get channel() {
|
||||
return this.client.channels.get(this.channel_id);
|
||||
}
|
||||
|
||||
get author() {
|
||||
return this.client.users.get(this.author_id);
|
||||
}
|
||||
|
||||
get member() {
|
||||
const channel = this.channel;
|
||||
if (channel?.channel_type === "TextChannel") {
|
||||
return this.client.members.getKey({
|
||||
server: channel.server_id!,
|
||||
user: this.author_id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
get mentions() {
|
||||
return this.mention_ids?.map((id) => this.client.users.get(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get timestamp when this message was created.
|
||||
*/
|
||||
get createdAt() {
|
||||
return decodeTime(this._id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Absolute pathname to this message in the client.
|
||||
*/
|
||||
get path() {
|
||||
return this.channel?.path + "/" + this._id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get URL to this message.
|
||||
*/
|
||||
get url() {
|
||||
return this.client.configuration?.app + this.path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the username for this message.
|
||||
*/
|
||||
get username() {
|
||||
return this.masquerade?.name ?? this.member?.nickname ?? this.author?.username;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the role colour for this message.
|
||||
*/
|
||||
get roleColour() {
|
||||
return this.masquerade?.colour ?? this.member?.roleColour;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the avatar URL for this message.
|
||||
*/
|
||||
get avatarURL() {
|
||||
return this.generateMasqAvatarURL()
|
||||
?? (this.member
|
||||
? this.member?.avatarURL
|
||||
: this.author?.avatarURL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the animated avatar URL for this message.
|
||||
*/
|
||||
get animatedAvatarURL() {
|
||||
return this.generateMasqAvatarURL()
|
||||
?? (this.member
|
||||
? this.member?.animatedAvatarURL
|
||||
: this.author?.animatedAvatarURL);
|
||||
}
|
||||
|
||||
@computed generateMasqAvatarURL() {
|
||||
const avatar = this.masquerade?.avatar;
|
||||
return avatar ? this.client.proxyFile(avatar) : undefined;
|
||||
}
|
||||
|
||||
@computed
|
||||
get asSystemMessage() {
|
||||
const system = this.system;
|
||||
if (!system) return { type: "none" };
|
||||
|
||||
const { type } = system;
|
||||
const get = (id: string) => this.client.users.get(id);
|
||||
switch (system.type) {
|
||||
case "text":
|
||||
return system;
|
||||
case "user_added":
|
||||
return { type, user: get(system.id), by: get(system.by) };
|
||||
case "user_remove":
|
||||
return { type, user: get(system.id), by: get(system.by) };
|
||||
case "user_joined":
|
||||
return { type, user: get(system.id) };
|
||||
case "user_left":
|
||||
return { type, user: get(system.id) };
|
||||
case "user_kicked":
|
||||
return { type, user: get(system.id) };
|
||||
case "user_banned":
|
||||
return { type, user: get(system.id) };
|
||||
case "channel_renamed":
|
||||
return { type, name: system.name, by: get(system.by) };
|
||||
case "channel_description_changed":
|
||||
return { type, by: get(system.by) };
|
||||
case "channel_icon_changed":
|
||||
return { type, by: get(system.by) };
|
||||
case "channel_ownership_changed":
|
||||
return { type, from: get(system.from), to: get(system.to) };
|
||||
}
|
||||
}
|
||||
|
||||
constructor(client: Client, data: MessageI) {
|
||||
this.client = client;
|
||||
this._id = data._id;
|
||||
this.nonce = data.nonce ?? undefined;
|
||||
this.channel_id = data.channel;
|
||||
this.author_id = data.author;
|
||||
|
||||
this.content = toNullable(data.content);
|
||||
this.system = toNullable(data.system);
|
||||
this.attachments = toNullable(data.attachments);
|
||||
this.edited = toNullableDate(data.edited);
|
||||
this.embeds = toNullable(data.embeds);
|
||||
this.mention_ids = toNullable(data.mentions);
|
||||
this.reply_ids = toNullable(data.replies);
|
||||
this.masquerade = toNullable(data.masquerade);
|
||||
this.interactions = toNullable(data.interactions);
|
||||
|
||||
this.reactions = new ObservableMap();
|
||||
for (const reaction of Object.keys(data.reactions ?? {})) {
|
||||
this.reactions.set(
|
||||
reaction,
|
||||
new ObservableSet(data.reactions![reaction]),
|
||||
);
|
||||
}
|
||||
|
||||
makeAutoObservable(this, {
|
||||
_id: false,
|
||||
client: false,
|
||||
});
|
||||
}
|
||||
|
||||
@action update(data: Partial<MessageI>) {
|
||||
const apply = (
|
||||
key: string,
|
||||
target?: string,
|
||||
transform?: (obj: unknown) => unknown,
|
||||
) => {
|
||||
// This code has been tested.
|
||||
if (
|
||||
// @ts-expect-error TODO: clean up types here
|
||||
typeof data[key] !== "undefined" &&
|
||||
// @ts-expect-error TODO: clean up types here
|
||||
!isEqual(this[target ?? key], data[key])
|
||||
) {
|
||||
// @ts-expect-error TODO: clean up types here
|
||||
this[target ?? key] = transform
|
||||
? // @ts-expect-error TODO: clean up types here
|
||||
transform(data[key])
|
||||
: // @ts-expect-error TODO: clean up types here
|
||||
data[key];
|
||||
}
|
||||
};
|
||||
|
||||
apply("content");
|
||||
apply("attachments");
|
||||
apply("edited", undefined, toNullableDate as (obj: unknown) => unknown);
|
||||
apply("embeds");
|
||||
apply("mentions", "mention_ids");
|
||||
apply("masquerade");
|
||||
apply("reactions");
|
||||
apply("interactions");
|
||||
}
|
||||
|
||||
@action append({ embeds }: Pick<Partial<MessageI>, "embeds">) {
|
||||
if (embeds) {
|
||||
this.embeds = [...(this.embeds ?? []), ...embeds];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit a message
|
||||
* @param data Message edit route data
|
||||
*/
|
||||
async edit(data: DataEditMessage) {
|
||||
return await this.client.api.patch(
|
||||
`/channels/${this.channel_id as ""}/messages/${this._id as ""}`,
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a message
|
||||
*/
|
||||
async delete() {
|
||||
return await this.client.api.delete(
|
||||
`/channels/${this.channel_id as ""}/messages/${this._id as ""}`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Acknowledge this message as read
|
||||
*/
|
||||
ack() {
|
||||
this.channel?.ack(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reply to Message
|
||||
*/
|
||||
reply(
|
||||
data:
|
||||
| string
|
||||
| (Omit<DataMessageSend, "nonce"> & {
|
||||
nonce?: string;
|
||||
}),
|
||||
mention = true,
|
||||
) {
|
||||
const obj = typeof data === "string" ? { content: data } : data;
|
||||
return this.channel?.sendMessage({
|
||||
...obj,
|
||||
replies: [{ id: this._id, mention }],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all reactions from this message
|
||||
*/
|
||||
async clearReactions() {
|
||||
return await this.client.api.delete(
|
||||
`/channels/${this.channel_id as ""}/messages/${
|
||||
this._id as ""
|
||||
}/reactions`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* React to a message
|
||||
* @param emoji Unicode or emoji ID
|
||||
*/
|
||||
async react(emoji: string) {
|
||||
return await this.client.api.put(
|
||||
`/channels/${this.channel_id as ""}/messages/${
|
||||
this._id as ""
|
||||
}/reactions/${emoji as ""}`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unreact from a message
|
||||
* @param emoji Unicode or emoji ID
|
||||
*/
|
||||
async unreact(emoji: string) {
|
||||
return await this.client.api.delete(
|
||||
`/channels/${this.channel_id as ""}/messages/${
|
||||
this._id as ""
|
||||
}/reactions/${emoji as ""}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default class Messages extends Collection<string, Message> {
|
||||
constructor(client: Client) {
|
||||
super(client);
|
||||
this.createObj = this.createObj.bind(this);
|
||||
}
|
||||
|
||||
@action $get(id: string, data?: MessageI) {
|
||||
const msg = this.get(id)!;
|
||||
if (data) msg.update(data);
|
||||
return msg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a message object.
|
||||
* This is meant for internal use only.
|
||||
* @param data Message Data
|
||||
* @param emit Whether to emit creation event
|
||||
* @returns Message
|
||||
*/
|
||||
createObj(data: MessageI, emit?: boolean | number) {
|
||||
if (this.has(data._id)) return this.$get(data._id);
|
||||
const message = new Message(this.client, data);
|
||||
|
||||
runInAction(() => {
|
||||
this.set(data._id, message);
|
||||
});
|
||||
|
||||
if (emit === true) this.client.emit("message", message);
|
||||
return message;
|
||||
}
|
||||
}
|
||||
@@ -1,563 +0,0 @@
|
||||
import type {
|
||||
Category,
|
||||
Channel as ChannelI,
|
||||
DataBanCreate,
|
||||
DataCreateChannel,
|
||||
DataCreateServer,
|
||||
DataEditRole,
|
||||
DataEditServer,
|
||||
FieldsServer,
|
||||
Role,
|
||||
Server as ServerI,
|
||||
SystemMessageChannels,
|
||||
} from "revolt-api";
|
||||
import type { File } from "revolt-api";
|
||||
|
||||
import { makeAutoObservable, action, runInAction, computed } from "mobx";
|
||||
import isEqual from "lodash.isequal";
|
||||
|
||||
import { Nullable, toNullable } from "../util/null";
|
||||
import { Permission } from "../permissions/definitions";
|
||||
import Collection from "./Collection";
|
||||
import { User } from "./Users";
|
||||
import { Channel, Client, FileArgs } from "..";
|
||||
import { decodeTime } from "ulid";
|
||||
import { INotificationChecker } from "../util/Unreads";
|
||||
import { Override } from "revolt-api";
|
||||
import { bitwiseAndEq, calculatePermission } from "../permissions/calculator";
|
||||
|
||||
export class Server {
|
||||
client: Client;
|
||||
|
||||
_id: string;
|
||||
owner: string;
|
||||
name: string;
|
||||
description: Nullable<string> = null;
|
||||
|
||||
channel_ids: string[] = [];
|
||||
categories: Nullable<Category[]> = null;
|
||||
system_messages: Nullable<SystemMessageChannels> = null;
|
||||
|
||||
roles: Nullable<{ [key: string]: Role }> = null;
|
||||
default_permissions: number;
|
||||
|
||||
icon: Nullable<File> = null;
|
||||
banner: Nullable<File> = null;
|
||||
|
||||
nsfw: Nullable<boolean> = null;
|
||||
flags: Nullable<number> = null;
|
||||
|
||||
get channels() {
|
||||
return this.channel_ids
|
||||
.map((x) => this.client.channels.get(x))
|
||||
.filter((x) => x);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get timestamp when this server was created.
|
||||
*/
|
||||
get createdAt() {
|
||||
return decodeTime(this._id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Absolute pathname to this server in the client.
|
||||
*/
|
||||
get path() {
|
||||
return `/server/${this._id}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get URL to this server.
|
||||
*/
|
||||
get url() {
|
||||
return this.client.configuration?.app + this.path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array of ordered categories with their respective channels.
|
||||
* Uncategorised channels are returned in `id="default"` category.
|
||||
*/
|
||||
@computed get orderedChannels(): (Omit<Category, "channels"> & {
|
||||
channels: Channel[];
|
||||
})[] {
|
||||
const uncategorised = new Set(
|
||||
this.channel_ids.filter((key) => this.client.channels.has(key)),
|
||||
);
|
||||
|
||||
const elements = [];
|
||||
let defaultCategory;
|
||||
|
||||
if (this.categories) {
|
||||
for (const category of this.categories) {
|
||||
const channels = [];
|
||||
for (const key of category.channels) {
|
||||
if (uncategorised.delete(key)) {
|
||||
channels.push(this.client.channels.get(key)!);
|
||||
}
|
||||
}
|
||||
|
||||
const cat = {
|
||||
...category,
|
||||
channels,
|
||||
};
|
||||
|
||||
if (cat.id === "default") {
|
||||
if (channels.length === 0) continue;
|
||||
|
||||
defaultCategory = cat;
|
||||
}
|
||||
|
||||
elements.push(cat);
|
||||
}
|
||||
}
|
||||
|
||||
if (uncategorised.size > 0) {
|
||||
const channels = [...uncategorised].map(
|
||||
(key) => this.client.channels.get(key)!,
|
||||
);
|
||||
|
||||
if (defaultCategory) {
|
||||
defaultCategory.channels = [
|
||||
...defaultCategory.channels,
|
||||
...channels,
|
||||
];
|
||||
} else {
|
||||
elements.unshift({
|
||||
id: "default",
|
||||
title: "Default",
|
||||
channels,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return elements;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default channel for this server
|
||||
*/
|
||||
@computed get defaultChannel(): Channel | undefined {
|
||||
return this.orderedChannels.find((cat) => cat.channels.length)
|
||||
?.channels[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an ordered array of roles with their IDs attached.
|
||||
* The highest ranking roles will be first followed by lower
|
||||
* ranking roles. This is dictated by the "rank" property
|
||||
* which is smaller for higher priority roles.
|
||||
*/
|
||||
@computed get orderedRoles() {
|
||||
return Object.keys(this.roles ?? {})
|
||||
.map((id) => {
|
||||
return {
|
||||
id,
|
||||
...this.roles![id],
|
||||
};
|
||||
})
|
||||
.sort((a, b) => (a.rank || 0) - (b.rank || 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the server is currently unread
|
||||
* @param permit Callback function to determine whether a server has certain properties
|
||||
* @returns Whether the server is unread
|
||||
*/
|
||||
@computed isUnread(permit?: INotificationChecker) {
|
||||
if (permit?.isMuted(this)) return false;
|
||||
return this.channels.find(
|
||||
(channel) => !permit?.isMuted(channel) && channel?.unread,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all message IDs of unread messages
|
||||
* @param permit Callback function to determine whether a server has certain properties
|
||||
* @returns Array of message IDs which are unread
|
||||
*/
|
||||
@computed getMentions(permit?: INotificationChecker) {
|
||||
if (permit?.isMuted(this)) return [];
|
||||
const arr = this.channels
|
||||
.filter((channel) => !permit?.isMuted(channel))
|
||||
.map((channel) => channel?.mentions) as string[][];
|
||||
|
||||
return ([] as string[]).concat(...arr);
|
||||
}
|
||||
|
||||
constructor(client: Client, data: ServerI) {
|
||||
this.client = client;
|
||||
|
||||
this._id = data._id;
|
||||
this.owner = data.owner;
|
||||
this.name = data.name;
|
||||
this.description = toNullable(data.description);
|
||||
|
||||
this.channel_ids = data.channels;
|
||||
this.categories = toNullable(data.categories);
|
||||
this.system_messages = toNullable(data.system_messages);
|
||||
|
||||
this.roles = toNullable(data.roles);
|
||||
this.default_permissions = data.default_permissions;
|
||||
|
||||
this.icon = toNullable(data.icon);
|
||||
this.banner = toNullable(data.banner);
|
||||
|
||||
this.nsfw = toNullable(data.nsfw);
|
||||
this.flags = toNullable(data.flags);
|
||||
|
||||
makeAutoObservable(this, {
|
||||
_id: false,
|
||||
client: false,
|
||||
});
|
||||
}
|
||||
|
||||
@action update(data: Partial<ServerI>, clear: FieldsServer[] = []) {
|
||||
const apply = (key: string, target?: string) => {
|
||||
// This code has been tested.
|
||||
if (
|
||||
// @ts-expect-error TODO: clean up types here
|
||||
typeof data[key] !== "undefined" &&
|
||||
// @ts-expect-error TODO: clean up types here
|
||||
!isEqual(this[target ?? key], data[key])
|
||||
) {
|
||||
// @ts-expect-error TODO: clean up types here
|
||||
this[target ?? key] = data[key];
|
||||
}
|
||||
};
|
||||
|
||||
for (const entry of clear) {
|
||||
switch (entry) {
|
||||
case "Banner":
|
||||
this.banner = null;
|
||||
break;
|
||||
case "Description":
|
||||
this.description = null;
|
||||
break;
|
||||
case "Icon":
|
||||
this.icon = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
apply("owner");
|
||||
apply("name");
|
||||
apply("description");
|
||||
apply("channels", "channel_ids");
|
||||
apply("categories");
|
||||
apply("system_messages");
|
||||
apply("roles");
|
||||
apply("default_permissions");
|
||||
apply("icon");
|
||||
apply("banner");
|
||||
apply("nsfw");
|
||||
apply("flags");
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a channel
|
||||
* @param data Channel create route data
|
||||
* @returns The newly-created channel
|
||||
*/
|
||||
async createChannel(data: DataCreateChannel) {
|
||||
return await this.client.api.post(
|
||||
`/servers/${this._id as ""}/channels`,
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit a server
|
||||
* @param data Server editing route data
|
||||
*/
|
||||
async edit(data: DataEditServer) {
|
||||
return await this.client.api.patch(`/servers/${this._id as ""}`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a guild
|
||||
*/
|
||||
async delete(leave_silently?: boolean, avoidReq?: boolean) {
|
||||
if (!avoidReq)
|
||||
await this.client.api.delete(`/servers/${this._id as ""}`, {
|
||||
leave_silently,
|
||||
});
|
||||
|
||||
runInAction(() => {
|
||||
this.client.servers.delete(this._id);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a server as read
|
||||
*/
|
||||
async ack() {
|
||||
return await this.client.api.put(`/servers/${this._id}/ack`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ban user
|
||||
* @param user_id User ID
|
||||
*/
|
||||
async banUser(user_id: string, data: DataBanCreate) {
|
||||
return await this.client.api.put(
|
||||
`/servers/${this._id as ""}/bans/${user_id}`,
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unban user
|
||||
* @param user_id User ID
|
||||
*/
|
||||
async unbanUser(user_id: string) {
|
||||
return await this.client.api.delete(
|
||||
`/servers/${this._id as ""}/bans/${user_id}`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a server's invites
|
||||
* @returns An array of the server's invites
|
||||
*/
|
||||
async fetchInvites() {
|
||||
return await this.client.api.get(`/servers/${this._id as ""}/invites`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a server's bans
|
||||
* @returns An array of the server's bans.
|
||||
*/
|
||||
async fetchBans() {
|
||||
return await this.client.api.get(`/servers/${this._id as ""}/bans`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set role permissions
|
||||
* @param role_id Role Id, set to 'default' to affect all users
|
||||
* @param permissions Permission value
|
||||
*/
|
||||
async setPermissions(role_id = "default", permissions: Override | number) {
|
||||
return await this.client.api.put(
|
||||
`/servers/${this._id as ""}/permissions/${role_id as ""}`,
|
||||
{ permissions: permissions as Override },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create role
|
||||
* @param name Role name
|
||||
*/
|
||||
async createRole(name: string) {
|
||||
return await this.client.api.post(`/servers/${this._id as ""}/roles`, {
|
||||
name,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit a role
|
||||
* @param role_id Role ID
|
||||
* @param data Role editing route data
|
||||
*/
|
||||
async editRole(role_id: string, data: DataEditRole) {
|
||||
return await this.client.api.patch(
|
||||
`/servers/${this._id as ""}/roles/${role_id as ""}`,
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete role
|
||||
* @param role_id Role ID
|
||||
*/
|
||||
async deleteRole(role_id: string) {
|
||||
return await this.client.api.delete(
|
||||
`/servers/${this._id as ""}/roles/${role_id as ""}`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a server member
|
||||
* @param user User or User ID
|
||||
* @returns Server member object
|
||||
*/
|
||||
async fetchMember(user: User | string) {
|
||||
const user_id = typeof user === "string" ? user : user._id;
|
||||
const existing = this.client.members.getKey({
|
||||
server: this._id,
|
||||
user: user_id,
|
||||
});
|
||||
if (existing) return existing;
|
||||
|
||||
const member = await this.client.api.get(
|
||||
`/servers/${this._id as ""}/members/${user_id as ""}`,
|
||||
);
|
||||
|
||||
return this.client.members.createObj(member);
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimised member fetch route.
|
||||
* @param exclude_offline
|
||||
*/
|
||||
async syncMembers(exclude_offline?: boolean) {
|
||||
const data = await this.client.api.get(
|
||||
`/servers/${this._id as ""}/members`,
|
||||
{ exclude_offline },
|
||||
);
|
||||
|
||||
runInAction(() => {
|
||||
if (exclude_offline) {
|
||||
for (let i = 0; i < data.users.length; i++) {
|
||||
const user = data.users[i];
|
||||
if (user.online) {
|
||||
this.client.users.createObj(user);
|
||||
this.client.members.createObj(data.members[i]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < data.users.length; i++) {
|
||||
this.client.users.createObj(data.users[i]);
|
||||
this.client.members.createObj(data.members[i]);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a server's members.
|
||||
* @returns An array of the server's members and their user objects.
|
||||
*/
|
||||
async fetchMembers() {
|
||||
const data = await this.client.api.get(
|
||||
`/servers/${this._id as ""}/members`,
|
||||
);
|
||||
|
||||
// Note: this takes 986 ms (Testers server)
|
||||
return runInAction(() => {
|
||||
return {
|
||||
members: data.members.map(this.client.members.createObj),
|
||||
users: data.users.map(this.client.users.createObj),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate URL to icon for this server
|
||||
* @param args File parameters
|
||||
* @returns File URL
|
||||
*/
|
||||
@computed generateIconURL(...args: FileArgs) {
|
||||
return this.client.generateFileURL(this.icon ?? undefined, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate URL to banner for this server
|
||||
* @param args File parameters
|
||||
* @returns File URL
|
||||
*/
|
||||
@computed generateBannerURL(...args: FileArgs) {
|
||||
return this.client.generateFileURL(this.banner ?? undefined, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get our own member object for this server
|
||||
*/
|
||||
@computed get member() {
|
||||
return this.client.members.getKey({
|
||||
server: this._id,
|
||||
user: this.client.user!._id,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Permission the currently authenticated user has against this server
|
||||
*/
|
||||
@computed get permission() {
|
||||
return calculatePermission(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether we have a given permission in a server
|
||||
* @param permission Permission Names
|
||||
* @returns Whether we have this permission
|
||||
*/
|
||||
@computed havePermission(...permission: (keyof typeof Permission)[]) {
|
||||
return bitwiseAndEq(
|
||||
this.permission,
|
||||
...permission.map((x) => Permission[x]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default class Servers extends Collection<string, Server> {
|
||||
constructor(client: Client) {
|
||||
super(client);
|
||||
this.createObj = this.createObj.bind(this);
|
||||
}
|
||||
|
||||
@action $get(id: string, data?: ServerI) {
|
||||
const server = this.get(id)!;
|
||||
if (data) server.update(data);
|
||||
return server;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a server
|
||||
* @param id Server ID
|
||||
* @returns The server
|
||||
*/
|
||||
async fetch(id: string, data?: ServerI, channels?: ChannelI[]) {
|
||||
if (this.has(id)) return this.$get(id, data);
|
||||
const res = data ?? (await this.client.api.get(`/servers/${id as ""}`));
|
||||
|
||||
return runInAction(async () => {
|
||||
if (channels) {
|
||||
for (const channel of channels) {
|
||||
await this.client.channels.fetch(channel._id, channel);
|
||||
}
|
||||
} else {
|
||||
for (const channel of res.channels) {
|
||||
// ! FIXME: add route for fetching all channels
|
||||
// ! FIXME: OR the WHOLE server
|
||||
try {
|
||||
await this.client.channels.fetch(channel);
|
||||
// future proofing for when not
|
||||
} catch (err) {}
|
||||
}
|
||||
}
|
||||
|
||||
return this.createObj(res);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a server object.
|
||||
* This is meant for internal use only.
|
||||
* @param data: Server Data
|
||||
* @returns Server
|
||||
*/
|
||||
createObj(data: ServerI) {
|
||||
if (this.has(data._id)) return this.$get(data._id, data);
|
||||
const server = new Server(this.client, data);
|
||||
|
||||
runInAction(() => {
|
||||
this.set(data._id, server);
|
||||
});
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a server
|
||||
* @param data Server create route data
|
||||
* @returns The newly-created server
|
||||
*/
|
||||
async createServer(data: DataCreateServer) {
|
||||
const { server, channels } = await this.client.api.post(
|
||||
`/servers/create`,
|
||||
data,
|
||||
);
|
||||
return await this.fetch(server._id, server, channels);
|
||||
}
|
||||
}
|
||||
@@ -1,307 +0,0 @@
|
||||
import type {
|
||||
BotInformation,
|
||||
UserStatus,
|
||||
User as UserI,
|
||||
RelationshipStatus,
|
||||
FieldsUser,
|
||||
DataEditUser,
|
||||
} from "revolt-api";
|
||||
import type { File } from "revolt-api";
|
||||
|
||||
import { makeAutoObservable, action, runInAction, computed } from "mobx";
|
||||
import isEqual from "lodash.isequal";
|
||||
|
||||
import { U32_MAX, UserPermission } from "../permissions/definitions";
|
||||
import { toNullable, Nullable } from "../util/null";
|
||||
import Collection from "./Collection";
|
||||
import { Client, FileArgs } from "..";
|
||||
import _ from "lodash";
|
||||
import { decodeTime } from "ulid";
|
||||
|
||||
export class User {
|
||||
client: Client;
|
||||
|
||||
_id: string;
|
||||
username: string;
|
||||
|
||||
avatar: Nullable<File>;
|
||||
badges: Nullable<number>;
|
||||
status: Nullable<UserStatus>;
|
||||
relationship: Nullable<RelationshipStatus>;
|
||||
online: boolean;
|
||||
privileged: boolean;
|
||||
flags: Nullable<number>;
|
||||
bot: Nullable<BotInformation>;
|
||||
|
||||
/**
|
||||
* Get timestamp when this user was created.
|
||||
*/
|
||||
get createdAt() {
|
||||
return decodeTime(this._id);
|
||||
}
|
||||
|
||||
constructor(client: Client, data: UserI) {
|
||||
this.client = client;
|
||||
|
||||
this._id = data._id;
|
||||
this.username = data.username;
|
||||
|
||||
this.avatar = toNullable(data.avatar);
|
||||
this.badges = toNullable(data.badges);
|
||||
this.status = toNullable(data.status);
|
||||
this.relationship = toNullable(data.relationship);
|
||||
this.online = data.online ?? false;
|
||||
this.privileged = data.privileged ?? false;
|
||||
this.flags = toNullable(data.flags);
|
||||
this.bot = toNullable(data.bot);
|
||||
|
||||
makeAutoObservable(this, {
|
||||
_id: false,
|
||||
client: false,
|
||||
});
|
||||
}
|
||||
|
||||
@action update(data: Partial<UserI>, clear: FieldsUser[] = []) {
|
||||
const apply = (key: string) => {
|
||||
// This code has been tested.
|
||||
if (
|
||||
// @ts-expect-error TODO: clean up types here
|
||||
typeof data[key] !== "undefined" &&
|
||||
// @ts-expect-error TODO: clean up types here
|
||||
!isEqual(this[key], data[key])
|
||||
) {
|
||||
// @ts-expect-error TODO: clean up types here
|
||||
this[key] = data[key];
|
||||
|
||||
if (key === "relationship") {
|
||||
this.client.emit("user/relationship", this);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (const entry of clear) {
|
||||
switch (entry) {
|
||||
case "Avatar":
|
||||
this.avatar = null;
|
||||
break;
|
||||
case "StatusText": {
|
||||
if (this.status) {
|
||||
this.status.text = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
apply("username");
|
||||
apply("avatar");
|
||||
apply("badges");
|
||||
apply("status");
|
||||
apply("relationship");
|
||||
apply("online");
|
||||
apply("privileged");
|
||||
apply("flags");
|
||||
apply("bot");
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a DM with a user
|
||||
* @returns DM Channel
|
||||
*/
|
||||
async openDM() {
|
||||
let dm = [...this.client.channels.values()].find(
|
||||
(x) => x.channel_type === "DirectMessage" && x.recipient == this,
|
||||
);
|
||||
|
||||
if (!dm) {
|
||||
const data = await this.client.api.get(
|
||||
`/users/${this._id as ""}/dm`,
|
||||
);
|
||||
dm = await this.client.channels.fetch(data._id, data)!;
|
||||
}
|
||||
|
||||
runInAction(() => {
|
||||
dm!.active = true;
|
||||
});
|
||||
|
||||
return dm;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a friend request to a user
|
||||
*/
|
||||
async addFriend() {
|
||||
return await this.client.api.post(`/users/friend`, {
|
||||
username: this.username,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a user from the friend list
|
||||
*/
|
||||
async removeFriend() {
|
||||
return await this.client.api.delete(`/users/${this._id as ""}/friend`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Block a user
|
||||
*/
|
||||
async blockUser() {
|
||||
return await this.client.api.put(`/users/${this._id as ""}/block`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unblock a user
|
||||
*/
|
||||
async unblockUser() {
|
||||
return await this.client.api.delete(`/users/${this._id as ""}/block`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the profile of a user
|
||||
* @returns The profile of the user
|
||||
*/
|
||||
async fetchProfile() {
|
||||
return await this.client.api.get(`/users/${this._id as ""}/profile`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the mutual connections of the current user and a target user
|
||||
* @returns The mutual connections of the current user and a target user
|
||||
*/
|
||||
async fetchMutual() {
|
||||
return await this.client.api.get(`/users/${this._id as ""}/mutual`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default avatar URL of a user
|
||||
*/
|
||||
get defaultAvatarURL() {
|
||||
return `${this.client.apiURL}/users/${this._id}/default_avatar`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a pre-configured avatar URL of a user
|
||||
*/
|
||||
get avatarURL() {
|
||||
return this.generateAvatarURL({ max_side: 256 });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a pre-configured animated avatar URL of a user
|
||||
*/
|
||||
get animatedAvatarURL() {
|
||||
return this.generateAvatarURL({ max_side: 256 }, true);
|
||||
}
|
||||
|
||||
@computed generateAvatarURL(...args: FileArgs) {
|
||||
return (
|
||||
this.client.generateFileURL(this.avatar ?? undefined, ...args) ??
|
||||
this.defaultAvatarURL
|
||||
);
|
||||
}
|
||||
|
||||
@computed get permission() {
|
||||
let permissions = 0;
|
||||
switch (this.relationship) {
|
||||
case "Friend":
|
||||
case "User":
|
||||
return U32_MAX;
|
||||
case "Blocked":
|
||||
case "BlockedOther":
|
||||
return UserPermission.Access;
|
||||
case "Incoming":
|
||||
case "Outgoing":
|
||||
permissions = UserPermission.Access;
|
||||
}
|
||||
|
||||
if (
|
||||
[...this.client.channels.values()].find(
|
||||
(channel) =>
|
||||
(channel.channel_type === "Group" ||
|
||||
channel.channel_type === "DirectMessage") &&
|
||||
channel.recipient_ids?.includes(this.client.user!._id),
|
||||
) ||
|
||||
[...this.client.members.values()].find(
|
||||
(member) => member._id.user === this.client.user!._id,
|
||||
)
|
||||
) {
|
||||
if (this.client.user?.bot || this.bot) {
|
||||
permissions |= UserPermission.SendMessage;
|
||||
}
|
||||
|
||||
permissions |= UserPermission.Access | UserPermission.ViewProfile;
|
||||
}
|
||||
|
||||
return permissions;
|
||||
}
|
||||
}
|
||||
|
||||
export default class Users extends Collection<string, User> {
|
||||
constructor(client: Client) {
|
||||
super(client);
|
||||
this.createObj = this.createObj.bind(this);
|
||||
this.set(
|
||||
"00000000000000000000000000",
|
||||
new User(client, {
|
||||
_id: "00000000000000000000000000",
|
||||
username: "Revolt",
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@action $get(id: string, data?: UserI) {
|
||||
const user = this.get(id)!;
|
||||
if (data) user.update(data);
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a user
|
||||
* @param id User ID
|
||||
* @returns User
|
||||
*/
|
||||
async fetch(id: string, data?: UserI) {
|
||||
if (this.has(id)) return this.$get(id, data);
|
||||
const res = data ?? (await this.client.api.get(`/users/${id as ""}`));
|
||||
|
||||
return this.createObj(res);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a user object.
|
||||
* This is meant for internal use only.
|
||||
* @param data: User Data
|
||||
* @returns User
|
||||
*/
|
||||
createObj(data: UserI) {
|
||||
if (this.has(data._id)) return this.$get(data._id, data);
|
||||
const user = new User(this.client, data);
|
||||
|
||||
runInAction(() => {
|
||||
this.set(data._id, user);
|
||||
});
|
||||
|
||||
this.client.emit("user/relationship", user);
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit the current user
|
||||
* @param data User edit data object
|
||||
*/
|
||||
async edit(data: DataEditUser) {
|
||||
await this.client.api.patch("/users/@me", data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the username of the current user
|
||||
* @param username New username
|
||||
* @param password Current password
|
||||
*/
|
||||
async changeUsername(username: string, password: string) {
|
||||
return await this.client.api.patch("/users/@me/username", {
|
||||
username,
|
||||
password,
|
||||
});
|
||||
}
|
||||
}
|
||||
+132
-141
@@ -1,11 +1,13 @@
|
||||
import Long from "long";
|
||||
import { Channel, Server, Member } from "..";
|
||||
|
||||
import { Channel, Client, Server, ServerMember } from "..";
|
||||
|
||||
import {
|
||||
ALLOW_IN_TIMEOUT,
|
||||
DEFAULT_PERMISSION_DIRECT_MESSAGE,
|
||||
DEFAULT_PERMISSION_VIEW_ONLY,
|
||||
Permission,
|
||||
UserPermission,
|
||||
ALLOW_IN_TIMEOUT,
|
||||
DEFAULT_PERMISSION_DIRECT_MESSAGE,
|
||||
DEFAULT_PERMISSION_VIEW_ONLY,
|
||||
Permission,
|
||||
UserPermission,
|
||||
} from "./definitions";
|
||||
|
||||
/**
|
||||
@@ -14,8 +16,8 @@ import {
|
||||
* @param b Inputs (OR'd together)
|
||||
*/
|
||||
export function bitwiseAndEq(a: number, ...b: number[]) {
|
||||
const value = b.reduce((prev, cur) => prev.or(cur), Long.fromNumber(0));
|
||||
return value.and(a).eq(value);
|
||||
const value = b.reduce((prev, cur) => prev.or(cur), Long.fromNumber(0));
|
||||
return value.and(a).eq(value);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -24,146 +26,135 @@ export function bitwiseAndEq(a: number, ...b: number[]) {
|
||||
* @param options Additional options to use when calculating
|
||||
*/
|
||||
export function calculatePermission(
|
||||
target: Channel | Server,
|
||||
options?: {
|
||||
/**
|
||||
* Pretend to be another member
|
||||
*/
|
||||
member?: Member;
|
||||
},
|
||||
client: Client,
|
||||
target: Channel | Server,
|
||||
options?: {
|
||||
/**
|
||||
* Pretend to be another ServerMember
|
||||
*/
|
||||
member?: ServerMember;
|
||||
}
|
||||
): number {
|
||||
const user = options?.member ? options?.member.user : target.client.user;
|
||||
if (user?.privileged) {
|
||||
return Permission.GrantAllSafe;
|
||||
}
|
||||
const user = options?.member ? options?.member.user : client.user;
|
||||
if (user?.privileged) {
|
||||
return Permission.GrantAllSafe;
|
||||
}
|
||||
|
||||
if (target instanceof Server) {
|
||||
// 1. Check if owner.
|
||||
if (target.owner === user?._id) {
|
||||
return Permission.GrantAllSafe;
|
||||
} else {
|
||||
// 2. Get member.
|
||||
const member = options?.member ??
|
||||
target.client.members.getKey({
|
||||
user: user!._id,
|
||||
server: target._id,
|
||||
}) ?? { roles: null, timeout: null };
|
||||
|
||||
if (!member) return 0;
|
||||
|
||||
// 3. Apply allows from default_permissions.
|
||||
let perm = Long.fromNumber(target.default_permissions);
|
||||
|
||||
// 4. If user has roles, iterate in order.
|
||||
if (member.roles && target.roles) {
|
||||
// 5. Apply allows and denies from roles.
|
||||
const permissions = member.orderedRoles.map(
|
||||
([, role]) => role.permissions,
|
||||
);
|
||||
|
||||
for (const permission of permissions) {
|
||||
perm = perm
|
||||
.or(permission.a)
|
||||
.and(Long.fromNumber(permission.d).not());
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Revoke permissions if member is timed out.
|
||||
if (member.timeout && member.timeout > new Date()) {
|
||||
perm = perm.and(ALLOW_IN_TIMEOUT);
|
||||
}
|
||||
|
||||
return perm.toNumber();
|
||||
}
|
||||
if (target instanceof Server) {
|
||||
// 1. Check if owner.
|
||||
if (target.ownerId === user?.id) {
|
||||
return Permission.GrantAllSafe;
|
||||
} else {
|
||||
// 1. Check channel type.
|
||||
switch (target.channel_type) {
|
||||
case "SavedMessages":
|
||||
return Permission.GrantAllSafe;
|
||||
case "DirectMessage": {
|
||||
// 2. Determine user permissions.
|
||||
const user_permissions = target.recipient?.permission || 0;
|
||||
// 2. Get ServerMember.
|
||||
const member = options?.member ??
|
||||
client.serverMembers.getByKey({
|
||||
user: user!.id,
|
||||
server: target.id,
|
||||
}) ?? { roles: null, timeout: null };
|
||||
|
||||
// 3. Check if the user can send messages.
|
||||
if (user_permissions & UserPermission.SendMessage) {
|
||||
return DEFAULT_PERMISSION_DIRECT_MESSAGE;
|
||||
} else {
|
||||
return DEFAULT_PERMISSION_VIEW_ONLY;
|
||||
}
|
||||
}
|
||||
case "Group": {
|
||||
// 2. Check if user is owner.
|
||||
if (target.owner_id === user!._id) {
|
||||
return DEFAULT_PERMISSION_DIRECT_MESSAGE;
|
||||
} else {
|
||||
// 3. Pull out group permissions.
|
||||
return (
|
||||
target.permissions ?? DEFAULT_PERMISSION_DIRECT_MESSAGE
|
||||
);
|
||||
}
|
||||
}
|
||||
case "TextChannel":
|
||||
case "VoiceChannel": {
|
||||
// 2. Get server.
|
||||
const server = target.server;
|
||||
if (typeof server === "undefined") return 0;
|
||||
if (!member) return 0;
|
||||
|
||||
// 3. If server owner, just grant all permissions.
|
||||
if (server.owner === user?._id) {
|
||||
return Permission.GrantAllSafe;
|
||||
} else {
|
||||
// 4. Get member.
|
||||
const member = options?.member ??
|
||||
target.client.members.getKey({
|
||||
user: user!._id,
|
||||
server: server._id,
|
||||
}) ?? { roles: null, timeout: null };
|
||||
// 3. Apply allows from default_permissions.
|
||||
let perm = Long.fromNumber(target.defaultPermissions);
|
||||
|
||||
if (!member) return 0;
|
||||
// 4. If user has roles, iterate in order.
|
||||
if (member.roles && target.roles) {
|
||||
// 5. Apply allows and denies from roles.
|
||||
const permissions = member.orderedRoles.map(
|
||||
(role) => role.permissions ?? { a: 0, d: 0 }
|
||||
);
|
||||
|
||||
// 5. Calculate server base permissions.
|
||||
let perm = Long.fromNumber(
|
||||
calculatePermission(server, options),
|
||||
);
|
||||
|
||||
// 6. Apply default allows and denies for channel.
|
||||
if (target.default_permissions) {
|
||||
perm = perm
|
||||
.or(target.default_permissions.a)
|
||||
.and(
|
||||
Long.fromNumber(
|
||||
target.default_permissions.d,
|
||||
).not(),
|
||||
);
|
||||
}
|
||||
|
||||
// 7. If user has roles, iterate in order.
|
||||
if (
|
||||
member.roles &&
|
||||
target.role_permissions &&
|
||||
server.roles
|
||||
) {
|
||||
// 5. Apply allows and denies from roles.
|
||||
const roles = member.orderedRoles.map(([id]) => id);
|
||||
|
||||
for (const id of roles) {
|
||||
const override = target.role_permissions[id];
|
||||
if (override) {
|
||||
perm = perm
|
||||
.or(override.a)
|
||||
.and(Long.fromNumber(override.d).not());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 8. Revoke permissions if member is timed out.
|
||||
if (member.timeout && member.timeout > new Date()) {
|
||||
perm = perm.and(ALLOW_IN_TIMEOUT);
|
||||
}
|
||||
|
||||
return perm.toNumber();
|
||||
}
|
||||
}
|
||||
for (const permission of permissions) {
|
||||
perm = perm.or(permission.a).and(Long.fromNumber(permission.d).not());
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Revoke permissions if ServerMember is timed out.
|
||||
if (member.timeout && member.timeout > new Date()) {
|
||||
perm = perm.and(ALLOW_IN_TIMEOUT);
|
||||
}
|
||||
|
||||
return perm.toNumber();
|
||||
}
|
||||
} else {
|
||||
// 1. Check channel type.
|
||||
switch (target.type) {
|
||||
case "SavedMessages":
|
||||
return Permission.GrantAllSafe;
|
||||
case "DirectMessage": {
|
||||
// 2. Determine user permissions.
|
||||
const user_permissions = target.recipient?.permission || 0;
|
||||
|
||||
// 3. Check if the user can send messages.
|
||||
if (user_permissions & UserPermission.SendMessage) {
|
||||
return DEFAULT_PERMISSION_DIRECT_MESSAGE;
|
||||
} else {
|
||||
return DEFAULT_PERMISSION_VIEW_ONLY;
|
||||
}
|
||||
}
|
||||
case "Group": {
|
||||
// 2. Check if user is owner.
|
||||
if (target.ownerId === user!.id) {
|
||||
return DEFAULT_PERMISSION_DIRECT_MESSAGE;
|
||||
} else {
|
||||
// 3. Pull out group permissions.
|
||||
return target.permissions ?? DEFAULT_PERMISSION_DIRECT_MESSAGE;
|
||||
}
|
||||
}
|
||||
case "TextChannel":
|
||||
case "VoiceChannel": {
|
||||
// 2. Get server.
|
||||
const server = target.server;
|
||||
if (typeof server === "undefined") return 0;
|
||||
|
||||
// 3. If server owner, just grant all permissions.
|
||||
if (server.owner === user?.id) {
|
||||
return Permission.GrantAllSafe;
|
||||
} else {
|
||||
// 4. Get ServerMember.
|
||||
const member = options?.member ??
|
||||
client.serverMembers.getByKey({
|
||||
user: user!.id,
|
||||
server: server.id,
|
||||
}) ?? { roles: null, timeout: null };
|
||||
|
||||
if (!member) return 0;
|
||||
|
||||
// 5. Calculate server base permissions.
|
||||
let perm = Long.fromNumber(
|
||||
calculatePermission(client, server, options)
|
||||
);
|
||||
|
||||
// 6. Apply default allows and denies for channel.
|
||||
if (target.defaultPermissions) {
|
||||
perm = perm
|
||||
.or(target.defaultPermissions.a)
|
||||
.and(Long.fromNumber(target.defaultPermissions.d).not());
|
||||
}
|
||||
|
||||
// 7. If user has roles, iterate in order.
|
||||
if (member.roles && target.rolePermissions && server.roles) {
|
||||
// 5. Apply allows and denies from roles.
|
||||
const roles = member.orderedRoles.map(({ id }) => id);
|
||||
|
||||
for (const id of roles) {
|
||||
const override = target.rolePermissions[id];
|
||||
if (override) {
|
||||
perm = perm
|
||||
.or(override.a)
|
||||
.and(Long.fromNumber(override.d).not());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 8. Revoke permissions if ServerMember is timed out.
|
||||
if (member.timeout && member.timeout > new Date()) {
|
||||
perm = perm.and(ALLOW_IN_TIMEOUT);
|
||||
}
|
||||
|
||||
return perm.toNumber();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,93 +2,93 @@
|
||||
* Permission against User
|
||||
*/
|
||||
export const UserPermission = {
|
||||
Access: 1 << 0,
|
||||
ViewProfile: 1 << 1,
|
||||
SendMessage: 1 << 2,
|
||||
Invite: 1 << 3,
|
||||
Access: 1 << 0,
|
||||
ViewProfile: 1 << 1,
|
||||
SendMessage: 1 << 2,
|
||||
Invite: 1 << 3,
|
||||
};
|
||||
|
||||
/**
|
||||
* Permission against Server / Channel
|
||||
*/
|
||||
export const Permission = {
|
||||
// * Generic permissions
|
||||
/// Manage the channel or channels on the server
|
||||
ManageChannel: 2 ** 0,
|
||||
/// Manage the server
|
||||
ManageServer: 2 ** 1,
|
||||
/// Manage permissions on servers or channels
|
||||
ManagePermissions: 2 ** 2,
|
||||
/// Manage roles on server
|
||||
ManageRole: 2 ** 3,
|
||||
/// Manage server customisation (includes emoji)
|
||||
ManageCustomisation: 2 ** 4,
|
||||
// * Generic permissions
|
||||
/// Manage the channel or channels on the server
|
||||
ManageChannel: 2 ** 0,
|
||||
/// Manage the server
|
||||
ManageServer: 2 ** 1,
|
||||
/// Manage permissions on servers or channels
|
||||
ManagePermissions: 2 ** 2,
|
||||
/// Manage roles on server
|
||||
ManageRole: 2 ** 3,
|
||||
/// Manage server customisation (includes emoji)
|
||||
ManageCustomisation: 2 ** 4,
|
||||
|
||||
// % 1 bits reserved
|
||||
// % 1 bits reserved
|
||||
|
||||
// * Member permissions
|
||||
/// Kick other members below their ranking
|
||||
KickMembers: 2 ** 6,
|
||||
/// Ban other members below their ranking
|
||||
BanMembers: 2 ** 7,
|
||||
/// Timeout other members below their ranking
|
||||
TimeoutMembers: 2 ** 8,
|
||||
/// Assign roles to members below their ranking
|
||||
AssignRoles: 2 ** 9,
|
||||
/// Change own nickname
|
||||
ChangeNickname: 2 ** 10,
|
||||
/// Change or remove other's nicknames below their ranking
|
||||
ManageNicknames: 2 ** 11,
|
||||
/// Change own avatar
|
||||
ChangeAvatar: 2 ** 12,
|
||||
/// Remove other's avatars below their ranking
|
||||
RemoveAvatars: 2 ** 13,
|
||||
// * Member permissions
|
||||
/// Kick other members below their ranking
|
||||
KickMembers: 2 ** 6,
|
||||
/// Ban other members below their ranking
|
||||
BanMembers: 2 ** 7,
|
||||
/// Timeout other members below their ranking
|
||||
TimeoutMembers: 2 ** 8,
|
||||
/// Assign roles to members below their ranking
|
||||
AssignRoles: 2 ** 9,
|
||||
/// Change own nickname
|
||||
ChangeNickname: 2 ** 10,
|
||||
/// Change or remove other's nicknames below their ranking
|
||||
ManageNicknames: 2 ** 11,
|
||||
/// Change own avatar
|
||||
ChangeAvatar: 2 ** 12,
|
||||
/// Remove other's avatars below their ranking
|
||||
RemoveAvatars: 2 ** 13,
|
||||
|
||||
// % 7 bits reserved
|
||||
// % 7 bits reserved
|
||||
|
||||
// * Channel permissions
|
||||
/// View a channel
|
||||
ViewChannel: 2 ** 20,
|
||||
/// Read a channel's past message history
|
||||
ReadMessageHistory: 2 ** 21,
|
||||
/// Send a message in a channel
|
||||
SendMessage: 2 ** 22,
|
||||
/// Delete messages in a channel
|
||||
ManageMessages: 2 ** 23,
|
||||
/// Manage webhook entries on a channel
|
||||
ManageWebhooks: 2 ** 24,
|
||||
/// Create invites to this channel
|
||||
InviteOthers: 2 ** 25,
|
||||
/// Send embedded content in this channel
|
||||
SendEmbeds: 2 ** 26,
|
||||
/// Send attachments and media in this channel
|
||||
UploadFiles: 2 ** 27,
|
||||
/// Masquerade messages using custom nickname and avatar
|
||||
Masquerade: 2 ** 28,
|
||||
/// React to messages with emoji
|
||||
React: 2 ** 29,
|
||||
// * Channel permissions
|
||||
/// View a channel
|
||||
ViewChannel: 2 ** 20,
|
||||
/// Read a channel's past message history
|
||||
ReadMessageHistory: 2 ** 21,
|
||||
/// Send a message in a channel
|
||||
SendMessage: 2 ** 22,
|
||||
/// Delete messages in a channel
|
||||
ManageMessages: 2 ** 23,
|
||||
/// Manage webhook entries on a channel
|
||||
ManageWebhooks: 2 ** 24,
|
||||
/// Create invites to this channel
|
||||
InviteOthers: 2 ** 25,
|
||||
/// Send embedded content in this channel
|
||||
SendEmbeds: 2 ** 26,
|
||||
/// Send attachments and media in this channel
|
||||
UploadFiles: 2 ** 27,
|
||||
/// Masquerade messages using custom nickname and avatar
|
||||
Masquerade: 2 ** 28,
|
||||
/// React to messages with emoji
|
||||
React: 2 ** 29,
|
||||
|
||||
// * Voice permissions
|
||||
/// Connect to a voice channel
|
||||
Connect: 2 ** 30,
|
||||
/// Speak in a voice call
|
||||
Speak: 2 ** 31,
|
||||
/// Share video in a voice call
|
||||
Video: 2 ** 32,
|
||||
/// Mute other members with lower ranking in a voice call
|
||||
MuteMembers: 2 ** 33,
|
||||
/// Deafen other members with lower ranking in a voice call
|
||||
DeafenMembers: 2 ** 34,
|
||||
/// Move members between voice channels
|
||||
MoveMembers: 2 ** 35,
|
||||
// * Voice permissions
|
||||
/// Connect to a voice channel
|
||||
Connect: 2 ** 30,
|
||||
/// Speak in a voice call
|
||||
Speak: 2 ** 31,
|
||||
/// Share video in a voice call
|
||||
Video: 2 ** 32,
|
||||
/// Mute other members with lower ranking in a voice call
|
||||
MuteMembers: 2 ** 33,
|
||||
/// Deafen other members with lower ranking in a voice call
|
||||
DeafenMembers: 2 ** 34,
|
||||
/// Move members between voice channels
|
||||
MoveMembers: 2 ** 35,
|
||||
|
||||
// * Misc. permissions
|
||||
// % Bits 36 to 52: free area
|
||||
// % Bits 53 to 64: do not use
|
||||
// * Misc. permissions
|
||||
// % Bits 36 to 52: free area
|
||||
// % Bits 53 to 64: do not use
|
||||
|
||||
// * Grant all permissions
|
||||
/// Safely grant all permissions
|
||||
GrantAllSafe: 0x000f_ffff_ffff_ffff,
|
||||
// * Grant all permissions
|
||||
/// Safely grant all permissions
|
||||
GrantAllSafe: 0x000f_ffff_ffff_ffff,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -100,25 +100,25 @@ export const U32_MAX = 2 ** 32 - 1; // 4294967295
|
||||
* Permissions allowed for a user while in timeout
|
||||
*/
|
||||
export const ALLOW_IN_TIMEOUT =
|
||||
Permission.ViewChannel + Permission.ReadMessageHistory;
|
||||
Permission.ViewChannel + Permission.ReadMessageHistory;
|
||||
|
||||
/**
|
||||
* Default permissions if we can only view
|
||||
*/
|
||||
export const DEFAULT_PERMISSION_VIEW_ONLY =
|
||||
Permission.ViewChannel + Permission.ReadMessageHistory;
|
||||
Permission.ViewChannel + Permission.ReadMessageHistory;
|
||||
|
||||
/**
|
||||
* Default base permissions for channels
|
||||
*/
|
||||
export const DEFAULT_PERMISSION =
|
||||
DEFAULT_PERMISSION_VIEW_ONLY +
|
||||
Permission.SendMessage +
|
||||
Permission.InviteOthers +
|
||||
Permission.SendEmbeds +
|
||||
Permission.UploadFiles +
|
||||
Permission.Connect +
|
||||
Permission.Speak;
|
||||
DEFAULT_PERMISSION_VIEW_ONLY +
|
||||
Permission.SendMessage +
|
||||
Permission.InviteOthers +
|
||||
Permission.SendEmbeds +
|
||||
Permission.UploadFiles +
|
||||
Permission.Connect +
|
||||
Permission.Speak;
|
||||
|
||||
/**
|
||||
* Permissions in saved messages channel
|
||||
@@ -129,10 +129,13 @@ export const DEFAULT_PERMISSION_SAVED_MESSAGES = Permission.GrantAllSafe;
|
||||
* Permissions in direct message channels / default permissions for group DMs
|
||||
*/
|
||||
export const DEFAULT_PERMISSION_DIRECT_MESSAGE =
|
||||
DEFAULT_PERMISSION + Permission.React + Permission.ManageChannel;
|
||||
DEFAULT_PERMISSION + Permission.React + Permission.ManageChannel;
|
||||
|
||||
/**
|
||||
* Permissions in server text / voice channel
|
||||
*/
|
||||
export const DEFAULT_PERMISSION_SERVER =
|
||||
DEFAULT_PERMISSION + Permission.React + Permission.ChangeNickname + Permission.ChangeAvatar;
|
||||
DEFAULT_PERMISSION +
|
||||
Permission.React +
|
||||
Permission.ChangeNickname +
|
||||
Permission.ChangeAvatar;
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { SetStoreFunction, createStore } from "solid-js/store";
|
||||
|
||||
import { Hydrators, hydrate } from "../hydration";
|
||||
|
||||
/**
|
||||
* Wrapper around Solid.js store
|
||||
*/
|
||||
export class ObjectStorage<T> {
|
||||
private store: Record<string, T>;
|
||||
readonly set: SetStoreFunction<Record<string, T>>;
|
||||
|
||||
/**
|
||||
* Create new object storage
|
||||
*/
|
||||
constructor() {
|
||||
const [store, setStore] = createStore({});
|
||||
this.store = store as never;
|
||||
this.set = setStore;
|
||||
this.get = this.get.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get object by ID
|
||||
* @param id ID
|
||||
* @returns Object
|
||||
*/
|
||||
get(id: string) {
|
||||
return this.store[id];
|
||||
}
|
||||
|
||||
/**
|
||||
* Hydrate some data into storage
|
||||
* @param id ID
|
||||
* @param type Hydration type
|
||||
* @param context Context
|
||||
* @param data Input Data
|
||||
*/
|
||||
hydrate(id: string, type: keyof Hydrators, context: unknown, data?: unknown) {
|
||||
if (data) {
|
||||
this.set(id, hydrate(type, data as never, context, true) as T);
|
||||
}
|
||||
}
|
||||
}
|
||||
-107
@@ -1,107 +0,0 @@
|
||||
import { config } from "dotenv";
|
||||
|
||||
import { Client } from ".";
|
||||
|
||||
config();
|
||||
|
||||
// To run this example, you need to have a local Revolt server running and an existing account.
|
||||
// Copy and paste `.env.example` to `.env` and edit accordingly.
|
||||
|
||||
async function user() {
|
||||
const client = new Client({
|
||||
apiURL: process.env.API_URL,
|
||||
debug: true,
|
||||
});
|
||||
|
||||
client.on("ready", async () => {
|
||||
console.info(`Logged in as ${client.user!.username}!`);
|
||||
|
||||
const group = await client.channels.createGroup({
|
||||
name: 'sussy',
|
||||
users: []
|
||||
});
|
||||
|
||||
const msg = await group.sendMessage({
|
||||
content: "embed test",
|
||||
embeds: [
|
||||
{
|
||||
title: 'We do a little!'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
await msg.edit({
|
||||
embeds: [{ title: 'sus' }]
|
||||
});
|
||||
});
|
||||
|
||||
client.on("message", async (message) => {
|
||||
if (message.content === "sus") {
|
||||
message.channel!.sendMessage("sus!");
|
||||
} else if (message.content === "bot") {
|
||||
const bot = await client.api.post("/bots/create", {
|
||||
name: "basedbot12",
|
||||
});
|
||||
message.channel!.sendMessage(JSON.stringify(bot));
|
||||
} else if (message.content === "my bots") {
|
||||
message.channel!.sendMessage(
|
||||
JSON.stringify(await client.api.get("/bots/@me")),
|
||||
);
|
||||
} else if (message.content === "join bot") {
|
||||
await client.api.post(
|
||||
`/bots/${'01FCV7DCMRD9MT3JBYT5VEKVRD'}/invite`,
|
||||
{ group: message.channel_id },
|
||||
);
|
||||
// { server: '01FATEGMHEE2M1QGPA65NS6V8K' });
|
||||
} else if (message.content === "edit bot name") {
|
||||
await client.api.patch(
|
||||
`/bots/${'01FCV7DCMRD9MT3JBYT5VEKVRD'}`,
|
||||
{ name: "testingbkaka" },
|
||||
);
|
||||
} else if (message.content === "make bot public") {
|
||||
await client.api.patch(
|
||||
`/bots/${'01FCV7DCMRD9MT3JBYT5VEKVRD'}`,
|
||||
{ public: true },
|
||||
);
|
||||
} else if (message.content === "delete bot") {
|
||||
await client.api.delete(`/bots/${'01FCV7DCMRD9MT3JBYT5VEKVRD'}`);
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
await client.register({
|
||||
email: process.env.EMAIL as string,
|
||||
password: process.env.PASSWORD as string,
|
||||
});
|
||||
} catch (err) {}
|
||||
|
||||
const onboarding = await client.login({
|
||||
email: process.env.EMAIL as string,
|
||||
password: process.env.PASSWORD as string,
|
||||
});
|
||||
|
||||
onboarding?.('sus', true);
|
||||
}
|
||||
|
||||
/*function bot() {
|
||||
const client = new Client({
|
||||
apiURL: process.env.API_URL,
|
||||
});
|
||||
|
||||
client.on("ready", async () => {
|
||||
console.info(
|
||||
`Logged in as ${client.user!.username}! [${client.user!._id}]`,
|
||||
);
|
||||
});
|
||||
|
||||
client.on("message", async (message) => {
|
||||
if (message.content === "sus") {
|
||||
message.channel!.sendMessage("sus!");
|
||||
}
|
||||
});
|
||||
|
||||
// client.loginBot(process.env.BOT_TOKEN as string)
|
||||
}*/
|
||||
|
||||
user();
|
||||
// bot();
|
||||
@@ -1,121 +0,0 @@
|
||||
import { Client } from "../Client";
|
||||
import {
|
||||
action,
|
||||
computed,
|
||||
makeAutoObservable,
|
||||
ObservableMap,
|
||||
runInAction,
|
||||
} from "mobx";
|
||||
import type { ChannelUnread } from "revolt-api";
|
||||
import { ulid } from "ulid";
|
||||
import { Channel } from "../maps/Channels";
|
||||
import { Server } from "../maps/Servers";
|
||||
|
||||
export interface INotificationChecker {
|
||||
isMuted(target?: Channel | Server): boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles channel unreads.
|
||||
*/
|
||||
export default class Unreads {
|
||||
private client: Client;
|
||||
private loaded: boolean;
|
||||
private channels: ObservableMap<string, Omit<ChannelUnread, "_id">>;
|
||||
|
||||
/**
|
||||
* Construct new Unreads store.
|
||||
*/
|
||||
constructor(client: Client) {
|
||||
this.channels = new ObservableMap();
|
||||
this.loaded = false;
|
||||
makeAutoObservable(this);
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync unread data from the server.
|
||||
*/
|
||||
async sync() {
|
||||
const unreads = await this.client.syncFetchUnreads();
|
||||
runInAction(() => {
|
||||
this.loaded = true;
|
||||
for (const unread of unreads) {
|
||||
const { _id, ...data } = unread;
|
||||
this.channels.set(_id.channel, data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get channel unread object for a given channel.
|
||||
* @param channel_id Target channel ID
|
||||
* @returns Partial channel unread object
|
||||
*/
|
||||
@computed getUnread(channel_id: string) {
|
||||
if (!this.loaded)
|
||||
return {
|
||||
last_id: "40000000000000000000000000",
|
||||
};
|
||||
|
||||
return this.channels.get(channel_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a channel as unread by setting a custom last_id.
|
||||
* @param channel_id Target channel ID
|
||||
* @param last_id New last ID
|
||||
*/
|
||||
@action markUnread(channel_id: string, last_id: string) {
|
||||
this.channels.set(channel_id, {
|
||||
...this.getUnread(channel_id),
|
||||
last_id,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a mention to a channel unread.
|
||||
* @param channel_id Target channel ID
|
||||
* @param message_id Target message ID
|
||||
*/
|
||||
@action markMention(channel_id: string, message_id: string) {
|
||||
const unread = this.getUnread(channel_id);
|
||||
this.channels.set(channel_id, {
|
||||
last_id: "0",
|
||||
...unread,
|
||||
mentions: [...(unread?.mentions ?? []), message_id],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a channel as read.
|
||||
* @param channel_id Target channel ID
|
||||
* @param message_id Target message ID (last sent in channel)
|
||||
* @param emit Whether to emit to server
|
||||
* @param skipRateLimiter Whether to skip the rate limiter
|
||||
*/
|
||||
@action markRead(
|
||||
channel_id: string,
|
||||
message_id?: string,
|
||||
emit = false,
|
||||
skipRateLimiter = false,
|
||||
) {
|
||||
const last_id = message_id ?? ulid();
|
||||
this.channels.set(channel_id, { last_id });
|
||||
|
||||
if (emit) {
|
||||
this.client.channels.get(channel_id)?.ack(last_id, skipRateLimiter);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark multiple channels as read.
|
||||
* @param channel_ids Target channel IDs
|
||||
*/
|
||||
@action markMultipleRead(channel_ids: string[]) {
|
||||
const last_id = ulid();
|
||||
for (const channel_id of channel_ids) {
|
||||
this.channels.set(channel_id, { last_id });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
export type Nullable<T> = T | null;
|
||||
export function toNullable<T>(data?: T) {
|
||||
return typeof data === "undefined" ? null : data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Backwards compatible convert potential Date value to Nullable<Date>.
|
||||
* @param data ISO8601 Timestamp or BSON DateTime
|
||||
* @returns
|
||||
*/
|
||||
export function toNullableDate(data?: { $date: string } | string | null) {
|
||||
return data ? new Date(typeof data === 'string' ? data : data.$date) : null;
|
||||
}
|
||||
@@ -1,796 +0,0 @@
|
||||
import { backOff } from "@insertish/exponential-backoff";
|
||||
import { ObservableSet, runInAction } from "mobx";
|
||||
import WebSocket from "@insertish/isomorphic-ws";
|
||||
import type { MessageEvent } from "ws";
|
||||
import { Role } from "revolt-api";
|
||||
|
||||
import { Client } from "..";
|
||||
import {
|
||||
ServerboundNotification,
|
||||
ClientboundNotification,
|
||||
} from "./notifications";
|
||||
|
||||
export class WebSocketClient {
|
||||
client: Client;
|
||||
ws?: WebSocket;
|
||||
|
||||
heartbeat?: number;
|
||||
connected: boolean;
|
||||
ready: boolean;
|
||||
|
||||
ping?: number;
|
||||
|
||||
constructor(client: Client) {
|
||||
this.client = client;
|
||||
|
||||
this.connected = false;
|
||||
this.ready = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect the WebSocket and disable heartbeats.
|
||||
*/
|
||||
disconnect() {
|
||||
clearInterval(this.heartbeat);
|
||||
this.connected = false;
|
||||
this.ready = false;
|
||||
|
||||
if (
|
||||
typeof this.ws !== "undefined" &&
|
||||
this.ws.readyState === WebSocket.OPEN
|
||||
) {
|
||||
this.ws.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a notification
|
||||
* @param notification Serverbound notification
|
||||
*/
|
||||
send(notification: ServerboundNotification) {
|
||||
if (
|
||||
typeof this.ws === "undefined" ||
|
||||
this.ws.readyState !== WebSocket.OPEN
|
||||
)
|
||||
return;
|
||||
|
||||
const data = JSON.stringify(notification);
|
||||
if (this.client.debug) console.debug("[<] PACKET", data);
|
||||
this.ws.send(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect the WebSocket
|
||||
* @param disallowReconnect Whether to disallow reconnection
|
||||
*/
|
||||
connect(disallowReconnect?: boolean): Promise<void> {
|
||||
this.client.emit("connecting");
|
||||
|
||||
return new Promise((resolve, $reject) => {
|
||||
let thrown = false;
|
||||
const reject = (err: unknown) => {
|
||||
if (!thrown) {
|
||||
thrown = true;
|
||||
$reject(err);
|
||||
}
|
||||
};
|
||||
|
||||
this.disconnect();
|
||||
|
||||
if (typeof this.client.configuration === "undefined") {
|
||||
throw new Error(
|
||||
"Attempted to open WebSocket without syncing configuration from server.",
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof this.client.session === "undefined") {
|
||||
throw new Error(
|
||||
"Attempted to open WebSocket without valid session.",
|
||||
);
|
||||
}
|
||||
|
||||
const ws = new WebSocket(this.client.configuration.ws);
|
||||
this.ws = ws;
|
||||
|
||||
ws.onopen = () => {
|
||||
if (typeof this.client.session === "string") {
|
||||
this.send({
|
||||
type: "Authenticate",
|
||||
token: this.client.session!,
|
||||
});
|
||||
} else {
|
||||
this.send({
|
||||
type: "Authenticate",
|
||||
...this.client.session!,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const process = async (packet: ClientboundNotification) => {
|
||||
this.client.emit("packet", packet);
|
||||
try {
|
||||
switch (packet.type) {
|
||||
case "Bulk": {
|
||||
for (const entry of packet.v) {
|
||||
await process(entry);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "Error": {
|
||||
reject(packet.error);
|
||||
break;
|
||||
}
|
||||
|
||||
case "Authenticated": {
|
||||
disallowReconnect = false;
|
||||
this.client.emit("connected");
|
||||
this.connected = true;
|
||||
break;
|
||||
}
|
||||
|
||||
case "Ready": {
|
||||
runInAction(() => {
|
||||
if (packet.type !== "Ready") throw 0;
|
||||
|
||||
for (const user of packet.users) {
|
||||
this.client.users.createObj(user);
|
||||
}
|
||||
|
||||
for (const channel of packet.channels) {
|
||||
this.client.channels.createObj(channel);
|
||||
}
|
||||
|
||||
for (const server of packet.servers) {
|
||||
this.client.servers.createObj(server);
|
||||
}
|
||||
|
||||
for (const member of packet.members) {
|
||||
this.client.members.createObj(member);
|
||||
}
|
||||
|
||||
for (const emoji of packet.emojis!) {
|
||||
this.client.emojis.createObj(emoji);
|
||||
}
|
||||
});
|
||||
|
||||
this.client.user = this.client.users.get(
|
||||
packet.users.find(
|
||||
(x) => x.relationship === "User",
|
||||
)!._id,
|
||||
)!;
|
||||
|
||||
this.client.emit("ready");
|
||||
this.ready = true;
|
||||
resolve();
|
||||
|
||||
// Sync unreads.
|
||||
this.client.unreads?.sync();
|
||||
|
||||
// Setup heartbeat.
|
||||
if (this.client.heartbeat > 0) {
|
||||
this.send({ type: "Ping", data: +new Date() });
|
||||
this.heartbeat = setInterval(() => {
|
||||
this.send({
|
||||
type: "Ping",
|
||||
data: +new Date(),
|
||||
});
|
||||
|
||||
if (this.client.options.pongTimeout) {
|
||||
let pongReceived = false;
|
||||
|
||||
this.client.once("packet", (p) => {
|
||||
if (p.type == "Pong")
|
||||
pongReceived = true;
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
if (!pongReceived) {
|
||||
if (
|
||||
this.client.options
|
||||
.onPongTimeout == "EXIT"
|
||||
) {
|
||||
throw "Client did not receive a pong in time";
|
||||
} else {
|
||||
console.warn(
|
||||
"Warning: Client did not receive a pong in time; Reconnecting.",
|
||||
);
|
||||
|
||||
this.disconnect();
|
||||
this.connect(
|
||||
disallowReconnect,
|
||||
);
|
||||
}
|
||||
}
|
||||
}, this.client.options.pongTimeout * 1000);
|
||||
}
|
||||
}, this.client.heartbeat * 1e3) as unknown as number;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "Message": {
|
||||
if (!this.client.messages.has(packet._id)) {
|
||||
if (
|
||||
packet.author ===
|
||||
"00000000000000000000000000"
|
||||
) {
|
||||
if (packet.system) {
|
||||
switch (packet.system.type) {
|
||||
case "user_added":
|
||||
case "user_remove":
|
||||
await this.client.users.fetch(
|
||||
packet.system.by,
|
||||
);
|
||||
break;
|
||||
case "user_joined":
|
||||
await this.client.users.fetch(
|
||||
packet.system.id,
|
||||
);
|
||||
break;
|
||||
case "channel_description_changed":
|
||||
case "channel_icon_changed":
|
||||
case "channel_renamed":
|
||||
await this.client.users.fetch(
|
||||
packet.system.by,
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await this.client.users.fetch(
|
||||
packet.author,
|
||||
);
|
||||
}
|
||||
|
||||
const channel =
|
||||
await this.client.channels.fetch(
|
||||
packet.channel,
|
||||
);
|
||||
|
||||
if (channel.channel_type === "TextChannel") {
|
||||
const server =
|
||||
await this.client.servers.fetch(
|
||||
channel.server_id!,
|
||||
);
|
||||
if (
|
||||
packet.author !==
|
||||
"00000000000000000000000000"
|
||||
)
|
||||
await server.fetchMember(packet.author);
|
||||
}
|
||||
|
||||
const message = this.client.messages.createObj(
|
||||
packet,
|
||||
true,
|
||||
);
|
||||
|
||||
runInAction(() => {
|
||||
if (
|
||||
channel.channel_type === "DirectMessage"
|
||||
) {
|
||||
channel.active = true;
|
||||
}
|
||||
|
||||
channel.last_message_id = message._id;
|
||||
|
||||
if (
|
||||
this.client.unreads &&
|
||||
message.mention_ids?.includes(
|
||||
this.client.user!._id,
|
||||
)
|
||||
) {
|
||||
this.client.unreads.markMention(
|
||||
message.channel_id,
|
||||
message._id,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "MessageUpdate": {
|
||||
const message = this.client.messages.get(packet.id);
|
||||
if (message) {
|
||||
message.update(packet.data);
|
||||
this.client.emit("message/update", message);
|
||||
this.client.emit(
|
||||
"message/updated",
|
||||
message,
|
||||
packet,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "MessageAppend": {
|
||||
const message = this.client.messages.get(packet.id);
|
||||
if (message) {
|
||||
message.append(packet.append);
|
||||
this.client.emit("message/append", message);
|
||||
this.client.emit(
|
||||
"message/updated",
|
||||
message,
|
||||
packet,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "MessageDelete": {
|
||||
const msg = this.client.messages.get(packet.id);
|
||||
this.client.messages.delete(packet.id);
|
||||
this.client.emit("message/delete", packet.id, msg);
|
||||
break;
|
||||
}
|
||||
|
||||
case "MessageReact": {
|
||||
const msg = this.client.messages.get(packet.id);
|
||||
if (msg) {
|
||||
if (msg.reactions.has(packet.emoji_id)) {
|
||||
msg.reactions
|
||||
.get(packet.emoji_id)!
|
||||
.add(packet.user_id);
|
||||
} else {
|
||||
msg.reactions.set(
|
||||
packet.emoji_id,
|
||||
new ObservableSet([packet.user_id]),
|
||||
);
|
||||
}
|
||||
|
||||
this.client.emit(
|
||||
"message/updated",
|
||||
msg,
|
||||
packet,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "MessageUnreact": {
|
||||
const msg = this.client.messages.get(packet.id);
|
||||
if (msg) {
|
||||
const user_ids = msg.reactions.get(
|
||||
packet.emoji_id,
|
||||
);
|
||||
|
||||
if (user_ids) {
|
||||
user_ids.delete(packet.user_id);
|
||||
if (user_ids.size === 0) {
|
||||
msg.reactions.delete(packet.emoji_id);
|
||||
}
|
||||
}
|
||||
|
||||
this.client.emit(
|
||||
"message/updated",
|
||||
msg,
|
||||
packet,
|
||||
);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "MessageRemoveReaction": {
|
||||
const msg = this.client.messages.get(packet.id);
|
||||
|
||||
if (msg) {
|
||||
msg.reactions.delete(packet.emoji_id);
|
||||
|
||||
this.client.emit(
|
||||
"message/updated",
|
||||
msg,
|
||||
packet,
|
||||
);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "BulkMessageDelete": {
|
||||
runInAction(() => {
|
||||
for (const id of packet.ids) {
|
||||
const msg = this.client.messages.get(id);
|
||||
this.client.messages.delete(id);
|
||||
this.client.emit("message/delete", id, msg);
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case "ChannelCreate": {
|
||||
runInAction(async () => {
|
||||
if (packet.type !== "ChannelCreate") throw 0;
|
||||
|
||||
if (
|
||||
packet.channel_type === "TextChannel" ||
|
||||
packet.channel_type === "VoiceChannel"
|
||||
) {
|
||||
const server =
|
||||
await this.client.servers.fetch(
|
||||
packet.server,
|
||||
);
|
||||
server.channel_ids.push(packet._id);
|
||||
}
|
||||
|
||||
this.client.channels.createObj(packet, true);
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case "ChannelUpdate": {
|
||||
const channel = this.client.channels.get(packet.id);
|
||||
if (channel) {
|
||||
channel.update(packet.data, packet.clear);
|
||||
this.client.emit("channel/update", channel);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "ChannelDelete": {
|
||||
const channel = this.client.channels.get(packet.id);
|
||||
channel?.delete(false, true);
|
||||
this.client.emit(
|
||||
"channel/delete",
|
||||
packet.id,
|
||||
channel,
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
case "ChannelGroupJoin": {
|
||||
this.client.channels
|
||||
.get(packet.id)
|
||||
?.updateGroupJoin(packet.user);
|
||||
break;
|
||||
}
|
||||
|
||||
case "ChannelGroupLeave": {
|
||||
const channel = this.client.channels.get(packet.id);
|
||||
|
||||
if (channel) {
|
||||
if (packet.user === this.client.user?._id) {
|
||||
channel.delete(false, true);
|
||||
} else {
|
||||
channel.updateGroupLeave(packet.user);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "ServerCreate": {
|
||||
runInAction(async () => {
|
||||
const channels = [];
|
||||
for (const channel of packet.channels) {
|
||||
channels.push(
|
||||
await this.client.channels.fetch(
|
||||
channel._id,
|
||||
channel,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await this.client.servers.fetch(
|
||||
packet.id,
|
||||
packet.server,
|
||||
);
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "ServerUpdate": {
|
||||
const server = this.client.servers.get(packet.id);
|
||||
if (server) {
|
||||
server.update(packet.data, packet.clear);
|
||||
this.client.emit("server/update", server);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "ServerDelete": {
|
||||
const server = this.client.servers.get(packet.id);
|
||||
server?.delete(false, true);
|
||||
this.client.emit(
|
||||
"server/delete",
|
||||
packet.id,
|
||||
server,
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
case "ServerMemberUpdate": {
|
||||
const member = this.client.members.getKey(
|
||||
packet.id,
|
||||
);
|
||||
if (member) {
|
||||
member.update(packet.data, packet.clear);
|
||||
this.client.emit("member/update", member);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "ServerMemberJoin": {
|
||||
runInAction(async () => {
|
||||
await this.client.servers.fetch(packet.id);
|
||||
await this.client.users.fetch(packet.user);
|
||||
|
||||
this.client.members.createObj(
|
||||
{
|
||||
_id: {
|
||||
server: packet.id,
|
||||
user: packet.user,
|
||||
},
|
||||
joined_at: new Date().toISOString(),
|
||||
},
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "ServerMemberLeave": {
|
||||
if (packet.user === this.client.user!._id) {
|
||||
const server_id = packet.id;
|
||||
runInAction(() => {
|
||||
this.client.servers
|
||||
.get(server_id)
|
||||
?.delete(false, true);
|
||||
[...this.client.members.keys()].forEach(
|
||||
(key) => {
|
||||
if (
|
||||
JSON.parse(key).server ===
|
||||
server_id
|
||||
) {
|
||||
this.client.members.delete(key);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
} else {
|
||||
this.client.members.deleteKey({
|
||||
server: packet.id,
|
||||
user: packet.user,
|
||||
});
|
||||
this.client.emit("member/leave", {
|
||||
server: packet.id,
|
||||
user: packet.user,
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "ServerRoleUpdate": {
|
||||
const server = this.client.servers.get(packet.id);
|
||||
if (server) {
|
||||
const role = {
|
||||
...server.roles?.[packet.role_id],
|
||||
...packet.data,
|
||||
} as Role;
|
||||
server.roles = {
|
||||
...server.roles,
|
||||
[packet.role_id]: role,
|
||||
};
|
||||
this.client.emit(
|
||||
"role/update",
|
||||
packet.role_id,
|
||||
role,
|
||||
packet.id,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "ServerRoleDelete": {
|
||||
const server = this.client.servers.get(packet.id);
|
||||
if (server) {
|
||||
const { [packet.role_id]: _, ...roles } =
|
||||
server.roles ?? {};
|
||||
server.roles = roles;
|
||||
this.client.emit(
|
||||
"role/delete",
|
||||
packet.role_id,
|
||||
packet.id,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "UserPlatformWipe": {
|
||||
runInAction(() => {
|
||||
const user_id = packet.user_id;
|
||||
|
||||
this.client.users.get(user_id)?.update(
|
||||
{
|
||||
username: "Removed User",
|
||||
online: false,
|
||||
relationship: "None",
|
||||
flags: packet.flags,
|
||||
},
|
||||
[
|
||||
"Avatar",
|
||||
"ProfileBackground",
|
||||
"ProfileContent",
|
||||
"StatusPresence",
|
||||
"StatusText",
|
||||
],
|
||||
);
|
||||
|
||||
const dm_channel = [
|
||||
...this.client.channels.values(),
|
||||
].find(
|
||||
(channel) =>
|
||||
channel.channel_type ===
|
||||
"DirectMessage" &&
|
||||
channel.recipient_ids?.includes(
|
||||
user_id,
|
||||
),
|
||||
);
|
||||
|
||||
if (dm_channel) {
|
||||
this.client.channels.delete(dm_channel._id);
|
||||
}
|
||||
|
||||
const member_ids = [
|
||||
...this.client.members.values(),
|
||||
]
|
||||
.filter(
|
||||
(member) => member._id.user === user_id,
|
||||
)
|
||||
.map((member) => member._id);
|
||||
|
||||
for (const member_id of member_ids) {
|
||||
this.client.members.deleteKey(member_id);
|
||||
}
|
||||
|
||||
for (const message of [
|
||||
...this.client.messages.values(),
|
||||
].filter(
|
||||
(message) => message.author_id === user_id,
|
||||
)) {
|
||||
message.content = "(message withheld)";
|
||||
message.attachments = [];
|
||||
message.embeds = [];
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case "UserUpdate": {
|
||||
this.client.users
|
||||
.get(packet.id)
|
||||
?.update(packet.data, packet.clear);
|
||||
break;
|
||||
}
|
||||
|
||||
case "UserRelationship": {
|
||||
const user = this.client.users.get(packet.user._id);
|
||||
if (user) {
|
||||
user.update({
|
||||
...packet.user,
|
||||
relationship: packet.status,
|
||||
});
|
||||
} else {
|
||||
this.client.users.createObj(packet.user);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "ChannelStartTyping": {
|
||||
const channel = this.client.channels.get(packet.id);
|
||||
const user = packet.user;
|
||||
|
||||
if (channel) {
|
||||
channel.updateStartTyping(user);
|
||||
|
||||
clearInterval(timeouts[packet.id + user]);
|
||||
timeouts[packet.id + user] = setTimeout(() => {
|
||||
channel!.updateStopTyping(user);
|
||||
}, 3000) as unknown as number;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "ChannelStopTyping": {
|
||||
this.client.channels
|
||||
.get(packet.id)
|
||||
?.updateStopTyping(packet.user);
|
||||
clearInterval(timeouts[packet.id + packet.user]);
|
||||
break;
|
||||
}
|
||||
|
||||
case "ChannelAck": {
|
||||
this.client.unreads?.markRead(
|
||||
packet.id,
|
||||
packet.message_id,
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
case "EmojiCreate": {
|
||||
this.client.emojis.createObj(packet, true);
|
||||
break;
|
||||
}
|
||||
|
||||
case "EmojiDelete": {
|
||||
const emoji = this.client.emojis.get(packet.id);
|
||||
this.client.emit("emoji/delete", packet.id, emoji);
|
||||
break;
|
||||
}
|
||||
|
||||
case "Pong": {
|
||||
this.ping = +new Date() - packet.data;
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
this.client.debug &&
|
||||
console.warn(
|
||||
`Warning: Unhandled packet! ${packet.type}`,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
const timeouts: Record<string, number> = {};
|
||||
const handle = async (msg: WebSocket.MessageEvent) => {
|
||||
const data = msg.data;
|
||||
if (typeof data !== "string") return;
|
||||
|
||||
if (this.client.debug) console.debug("[>] PACKET", data);
|
||||
const packet = JSON.parse(data) as ClientboundNotification;
|
||||
await process(packet);
|
||||
};
|
||||
|
||||
let processing = false;
|
||||
const queue: WebSocket.MessageEvent[] = [];
|
||||
ws.onmessage = async (data: MessageEvent) => {
|
||||
queue.push(data);
|
||||
|
||||
if (!processing) {
|
||||
processing = true;
|
||||
while (queue.length > 0) {
|
||||
await handle(queue.shift()!);
|
||||
}
|
||||
processing = false;
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = (err: any) => {
|
||||
reject(err);
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
this.client.emit("dropped");
|
||||
this.connected = false;
|
||||
this.ready = false;
|
||||
|
||||
Object.keys(timeouts)
|
||||
.map((k) => timeouts[k])
|
||||
.forEach(clearTimeout);
|
||||
|
||||
runInAction(() => {
|
||||
[...this.client.users.values()].forEach(
|
||||
(user) => (user.online = false),
|
||||
);
|
||||
[...this.client.channels.values()].forEach((channel) =>
|
||||
channel.typing_ids.clear(),
|
||||
);
|
||||
});
|
||||
|
||||
if (!disallowReconnect && this.client.autoReconnect) {
|
||||
backOff(() => this.connect(true)).catch(reject);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
import type {
|
||||
Emoji,
|
||||
FieldsChannel,
|
||||
FieldsMember,
|
||||
FieldsServer,
|
||||
FieldsUser,
|
||||
} from "revolt-api";
|
||||
import type { Channel, Message } from "revolt-api";
|
||||
import type { Member, MemberCompositeKey, Role, Server } from "revolt-api";
|
||||
import type { RelationshipStatus, User } from "revolt-api";
|
||||
import type { Session } from "../Client";
|
||||
|
||||
type WebSocketError = {
|
||||
error:
|
||||
| "InternalError"
|
||||
| "InvalidSession"
|
||||
| "OnboardingNotFinished"
|
||||
| "AlreadyAuthenticated";
|
||||
};
|
||||
|
||||
export type ServerboundNotification =
|
||||
| { type: "Ping"; data: number }
|
||||
| { type: "Pong"; data: number }
|
||||
| ({ type: "Authenticate" } & Session)
|
||||
| { type: "Authenticate"; token: string }
|
||||
| { type: "BeginTyping"; channel: string }
|
||||
| { type: "EndTyping"; channel: string };
|
||||
|
||||
export type ReadyPacket = {
|
||||
type: "Ready";
|
||||
users: User[];
|
||||
servers: Server[];
|
||||
channels: Channel[];
|
||||
members: Member[];
|
||||
emojis?: Emoji[];
|
||||
};
|
||||
|
||||
export type ClientboundNotification =
|
||||
| { type: "Bulk"; v: ClientboundNotification[] }
|
||||
| { type: "Ping"; data: number }
|
||||
| { type: "Pong"; data: number }
|
||||
| ({ type: "Error" } & WebSocketError)
|
||||
| { type: "Authenticated" }
|
||||
| ReadyPacket
|
||||
| ({ type: "Message" } & Message)
|
||||
| {
|
||||
type: "MessageUpdate";
|
||||
id: string;
|
||||
channel: string;
|
||||
data: Partial<Message>;
|
||||
}
|
||||
| {
|
||||
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: "MessageUnreact";
|
||||
id: string;
|
||||
channel_id: string;
|
||||
user_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: "ChannelDelete"; id: string }
|
||||
| { type: "ChannelGroupJoin"; id: string; user: string }
|
||||
| { type: "ChannelGroupLeave"; id: string; user: string }
|
||||
| { type: "ChannelStartTyping"; id: string; user: string }
|
||||
| { type: "ChannelStopTyping"; id: string; user: string }
|
||||
| { type: "ChannelAck"; id: string; user: string; message_id: string }
|
||||
| {
|
||||
type: "ServerCreate";
|
||||
id: string;
|
||||
server: Server;
|
||||
channels: Channel[];
|
||||
}
|
||||
| {
|
||||
type: "ServerUpdate";
|
||||
id: string;
|
||||
data: Partial<Server>;
|
||||
clear?: FieldsServer[];
|
||||
}
|
||||
| { type: "ServerDelete"; id: string }
|
||||
| {
|
||||
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: "ServerRoleDelete"; id: string; role_id: string }
|
||||
| {
|
||||
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: "UserPlatformWipe"; user_id: string; flags: number }
|
||||
| ({ type: "EmojiCreate" } & Emoji)
|
||||
| { type: "EmojiDelete"; id: string }
|
||||
| ({
|
||||
type: "Auth";
|
||||
} & (
|
||||
| {
|
||||
event_type: "DeleteSession";
|
||||
user_id: string;
|
||||
session_id: string;
|
||||
}
|
||||
| {
|
||||
event_type: "DeleteAllSessions";
|
||||
user_id: string;
|
||||
exclude_session_id: string;
|
||||
}
|
||||
));
|
||||
@@ -0,0 +1,12 @@
|
||||
require("dotenv").config();
|
||||
|
||||
const { Client } = require(".");
|
||||
|
||||
const client = new Client({ debug: true });
|
||||
|
||||
client.on("ready", () => console.info(`Logged in as ${client.user.username}!`));
|
||||
client.on("disconnected", () => console.info("Disconnected."));
|
||||
|
||||
client.on("messageCreate", (message) => console.info(message.content));
|
||||
|
||||
client.loginBot(process.env.TOKEN);
|
||||
@@ -1,62 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Basic Options */
|
||||
"target": "es6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */,
|
||||
"module": "ES6" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
|
||||
// "lib": [], /* Specify library files to be included in the compilation. */
|
||||
// "allowJs": true, /* Allow javascript files to be compiled. */
|
||||
// "checkJs": true, /* Report errors in .js files. */
|
||||
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
|
||||
"declaration": true /* Generates corresponding '.d.ts' file. */,
|
||||
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
|
||||
// "sourceMap": true, /* Generates corresponding '.map' file. */
|
||||
// "outFile": "./", /* Concatenate and emit output to single file. */
|
||||
"outDir": "./esm" /* Redirect output structure to the directory. */,
|
||||
"rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */,
|
||||
// "composite": true, /* Enable project compilation */
|
||||
// "removeComments": true, /* Do not emit comments to output. */
|
||||
// "noEmit": true, /* Do not emit outputs. */
|
||||
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
|
||||
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
|
||||
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
|
||||
|
||||
/* Strict Type-Checking Options */
|
||||
"strict": true /* Enable all strict type-checking options. */,
|
||||
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
|
||||
// "strictNullChecks": true, /* Enable strict null checks. */
|
||||
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
|
||||
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
|
||||
"strictPropertyInitialization": false /* Enable strict checking of property initialization in classes. */,
|
||||
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
|
||||
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
|
||||
|
||||
/* Additional Checks */
|
||||
// "noUnusedLocals": true, /* Report errors on unused locals. */
|
||||
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||
|
||||
/* Module Resolution Options */
|
||||
"moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
|
||||
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
|
||||
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
||||
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
||||
// "typeRoots": [], /* List of folders to include type definitions from. */
|
||||
// "types": [], /* Type declaration files to be included in compilation. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
|
||||
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
|
||||
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
||||
|
||||
/* Source Map Options */
|
||||
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
|
||||
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
|
||||
|
||||
/* Experimental Options */
|
||||
"experimentalDecorators": true /* Enables experimental support for ES7 decorators. */,
|
||||
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src/*"]
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"extends": "./tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"module": "CommonJS",
|
||||
"outDir": "dist"
|
||||
}
|
||||
}
|
||||
+12
-1
@@ -1,3 +1,14 @@
|
||||
{
|
||||
"extends": "./tsconfig.base.json"
|
||||
"compilerOptions": {
|
||||
"target": "es2016",
|
||||
"module": "commonjs",
|
||||
"rootDir": "./src",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"outDir": "./lib/cjs",
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user