init
This commit is contained in:
commit
8ca9b01243
30 changed files with 1885 additions and 0 deletions
5
.editorconfig
Normal file
5
.editorconfig
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
4
.gitattributes
vendored
Normal file
4
.gitattributes
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
/.yarn/** linguist-vendored
|
||||||
|
/.yarn/releases/* binary
|
||||||
|
/.yarn/plugins/**/* binary
|
||||||
|
/.pnp.* binary linguist-generated
|
16
.gitignore
vendored
Normal file
16
.gitignore
vendored
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
.yarn/*
|
||||||
|
!.yarn/patches
|
||||||
|
!.yarn/plugins
|
||||||
|
!.yarn/releases
|
||||||
|
!.yarn/sdks
|
||||||
|
!.yarn/versions
|
||||||
|
|
||||||
|
# Swap the comments on the following lines if you wish to use zero-installs
|
||||||
|
# In that case, don't forget to run `yarn config set enableGlobalCache false`!
|
||||||
|
# Documentation here: https://yarnpkg.com/features/caching#zero-installs
|
||||||
|
|
||||||
|
#!.yarn/cache
|
||||||
|
.pnp.*
|
||||||
|
|
||||||
|
build/
|
||||||
|
node_modules
|
5
.prettierrc
Normal file
5
.prettierrc
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"useTabs": true,
|
||||||
|
"singleQuote": false,
|
||||||
|
"trailingComma": "all"
|
||||||
|
}
|
4
.vscode/settings.json
vendored
Normal file
4
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.formatOnSaveMode": "modificationsIfAvailable"
|
||||||
|
}
|
1
.yarnrc.yml
Normal file
1
.yarnrc.yml
Normal file
|
@ -0,0 +1 @@
|
||||||
|
nodeLinker: node-modules
|
1
README.md
Normal file
1
README.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
# foxapp
|
8
package.json
Normal file
8
package.json
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"name": "foxapp",
|
||||||
|
"packageManager": "yarn@4.1.0",
|
||||||
|
"private": true,
|
||||||
|
"workspaces": [
|
||||||
|
"packages/*"
|
||||||
|
]
|
||||||
|
}
|
16
packages/cli/package.json
Normal file
16
packages/cli/package.json
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"name": "cli",
|
||||||
|
"dependencies": {
|
||||||
|
"foxchat.js": "workspace:^"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"tsx": "^3.12.7",
|
||||||
|
"typescript": "^5.1.6"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"dev": "tsx --watch --conditions=development src/index.ts",
|
||||||
|
"start": "node build/index.js"
|
||||||
|
},
|
||||||
|
"type": "module"
|
||||||
|
}
|
11
packages/cli/src/index.ts
Normal file
11
packages/cli/src/index.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import { Client } from "foxchat.js";
|
||||||
|
|
||||||
|
const client = new Client({
|
||||||
|
instance: "id.fox.localhost",
|
||||||
|
token: "NbV7gaEZaTnhJFF19tt5IkOsscAWkKhuLk7WdkIPfmM",
|
||||||
|
debug: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
await client.connect();
|
||||||
|
})();
|
10
packages/cli/tsconfig.json
Normal file
10
packages/cli/tsconfig.json
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"module": "NodeNext",
|
||||||
|
"outDir": "build",
|
||||||
|
"strict": true,
|
||||||
|
"target": "ES2022",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
}
|
||||||
|
}
|
25
packages/foxchat.js/package.json
Normal file
25
packages/foxchat.js/package.json
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"name": "foxchat.js",
|
||||||
|
"description": "Client library for foxchat",
|
||||||
|
"exports": {
|
||||||
|
"types": "./src/index.ts",
|
||||||
|
"development": "./src/index.ts",
|
||||||
|
"default": "./build/index.js"
|
||||||
|
},
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc"
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/ws": "^8.5.10",
|
||||||
|
"prettier": "^3.2.5",
|
||||||
|
"typed-emitter": "^2.1.0",
|
||||||
|
"typescript": "^5.3.3"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"eventemitter3": "^5.0.1",
|
||||||
|
"isomorphic-ws": "^5.0.0",
|
||||||
|
"ws": "^8.16.0"
|
||||||
|
}
|
||||||
|
}
|
80
packages/foxchat.js/src/Client.ts
Normal file
80
packages/foxchat.js/src/Client.ts
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
import { RestClient } from "./rest/index.js";
|
||||||
|
import { WebsocketClient } from "./ws/index.js";
|
||||||
|
import * as events from "./entities/ws/events.js";
|
||||||
|
import EventEmitter from "eventemitter3";
|
||||||
|
import { Dispatch } from "./entities/ws/payload.js";
|
||||||
|
|
||||||
|
export interface ClientOptions {
|
||||||
|
instance: string;
|
||||||
|
token: string;
|
||||||
|
debug?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ChatInstance = {
|
||||||
|
id: string;
|
||||||
|
domain: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ClientEvents = {
|
||||||
|
ready: (ready: events.Ready & { instance: ChatInstance }) => void;
|
||||||
|
messageCreate: (
|
||||||
|
message: events.MessageCreate & { instance: ChatInstance },
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
// TODO: have this emit an actual type
|
||||||
|
error: (message: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class Client extends EventEmitter<ClientEvents> {
|
||||||
|
#options: ClientOptions;
|
||||||
|
|
||||||
|
#restClient: RestClient;
|
||||||
|
#wsClient: WebsocketClient;
|
||||||
|
|
||||||
|
constructor(opts: ClientOptions) {
|
||||||
|
super();
|
||||||
|
this.#options = opts;
|
||||||
|
this.#restClient = new RestClient(
|
||||||
|
this.#options.instance,
|
||||||
|
this.#options.token,
|
||||||
|
);
|
||||||
|
this.#wsClient = new WebsocketClient(
|
||||||
|
this.#options.instance,
|
||||||
|
this.#options.token,
|
||||||
|
{ debug: this.#options.debug },
|
||||||
|
);
|
||||||
|
|
||||||
|
this.#wsClient.on("event", this.#handleEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
get rest() {
|
||||||
|
return this.#restClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
get ws() {
|
||||||
|
return this.#wsClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
async connect() {
|
||||||
|
await this.#wsClient.connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
#handleEvent(instanceId: string, event: Dispatch) {
|
||||||
|
const instance = this.#wsClient.instances.find((i) => i.id === instanceId);
|
||||||
|
if (!instance) {
|
||||||
|
this.emit(
|
||||||
|
"error",
|
||||||
|
`Unknown instance ID for event with type "${event.t}"`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (event.t) {
|
||||||
|
case "MESSAGE_CREATE":
|
||||||
|
this.emit("messageCreate", { ...event.d, instance });
|
||||||
|
break;
|
||||||
|
case "READY":
|
||||||
|
this.emit("ready", { ...event.d, instance });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
3
packages/foxchat.js/src/Constants.ts
Normal file
3
packages/foxchat.js/src/Constants.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export default Object.freeze({
|
||||||
|
serverProxyHeader: "X-Foxchat-Server",
|
||||||
|
});
|
15
packages/foxchat.js/src/entities/rest/chat/guild.ts
Normal file
15
packages/foxchat.js/src/entities/rest/chat/guild.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
export interface Guild {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
owner_ids: string[];
|
||||||
|
channels?: PartialChannel[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateGuildParams {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PartialChannel {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}
|
3
packages/foxchat.js/src/entities/rest/chat/index.ts
Normal file
3
packages/foxchat.js/src/entities/rest/chat/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export { Guild, CreateGuildParams, PartialChannel } from "./guild.js";
|
||||||
|
export { PartialUser } from "./user.js";
|
||||||
|
export { Message, CreateMessageParams } from "./message.js";
|
13
packages/foxchat.js/src/entities/rest/chat/message.ts
Normal file
13
packages/foxchat.js/src/entities/rest/chat/message.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import { PartialUser } from "./user.js";
|
||||||
|
|
||||||
|
export interface Message {
|
||||||
|
id: string;
|
||||||
|
channel_id: string;
|
||||||
|
author: PartialUser;
|
||||||
|
content: string | null;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateMessageParams {
|
||||||
|
content: string;
|
||||||
|
}
|
6
packages/foxchat.js/src/entities/rest/chat/user.ts
Normal file
6
packages/foxchat.js/src/entities/rest/chat/user.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
export interface PartialUser {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
instance: string;
|
||||||
|
avatar_url?: string;
|
||||||
|
}
|
4
packages/foxchat.js/src/entities/rest/ident/index.ts
Normal file
4
packages/foxchat.js/src/entities/rest/ident/index.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
export interface Node {
|
||||||
|
software: string;
|
||||||
|
public_key: string;
|
||||||
|
}
|
6
packages/foxchat.js/src/entities/user.ts
Normal file
6
packages/foxchat.js/src/entities/user.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
export interface User {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
instance: string;
|
||||||
|
avatar_url: string | null;
|
||||||
|
}
|
10
packages/foxchat.js/src/entities/ws/events.ts
Normal file
10
packages/foxchat.js/src/entities/ws/events.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import { Guild } from "../rest/chat/guild.js";
|
||||||
|
import { Message } from "../rest/chat/message.js";
|
||||||
|
import { User } from "../user.js";
|
||||||
|
|
||||||
|
export type Ready = {
|
||||||
|
user: User;
|
||||||
|
guilds: Guild[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MessageCreate = Message & { guild_id: string };
|
24
packages/foxchat.js/src/entities/ws/payload.ts
Normal file
24
packages/foxchat.js/src/entities/ws/payload.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import type * as events from "./events";
|
||||||
|
|
||||||
|
export type Dispatch =
|
||||||
|
| { t: "MESSAGE_CREATE"; d: events.MessageCreate }
|
||||||
|
| { t: "READY"; d: events.Ready };
|
||||||
|
|
||||||
|
export type ServerPayload =
|
||||||
|
| { t: "D"; d: { e: Dispatch; s: string } }
|
||||||
|
| { t: "ERROR"; d: { message: string } }
|
||||||
|
| {
|
||||||
|
t: "HELLO";
|
||||||
|
d: {
|
||||||
|
heartbeat_interval: number;
|
||||||
|
guilds: string[];
|
||||||
|
instances: { id: string; domain: string }[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
| { t: "HEARTBEAT_ACK"; d: { t: number } };
|
||||||
|
|
||||||
|
export type ClientPayload =
|
||||||
|
| { t: "IDENTIFY"; d: { token: string } }
|
||||||
|
| { t: "HEARTBEAT"; d: { t: number } };
|
||||||
|
|
||||||
|
export type Payload = ServerPayload | ClientPayload;
|
3
packages/foxchat.js/src/index.ts
Normal file
3
packages/foxchat.js/src/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export { default as Client } from "./Client.js";
|
||||||
|
export { RestClient, ChatRestClient, RestError } from "./rest/index.js";
|
||||||
|
export { default as Constants } from "./Constants.js";
|
136
packages/foxchat.js/src/rest/RestClient.ts
Normal file
136
packages/foxchat.js/src/rest/RestClient.ts
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
import RestError, { ErrorCode } from "./RestError.js";
|
||||||
|
|
||||||
|
import type * as ident from "../entities/rest/ident/index.js";
|
||||||
|
import type * as chat from "../entities/rest/chat/index.js";
|
||||||
|
import Constants from "../Constants.js";
|
||||||
|
|
||||||
|
export default class RestClient {
|
||||||
|
#instance: string;
|
||||||
|
#token: string;
|
||||||
|
|
||||||
|
constructor(instance: string, token: string) {
|
||||||
|
this.#instance = instance;
|
||||||
|
this.#token = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns a new ChatRestClient for the given instance. */
|
||||||
|
chat(instance: string) {
|
||||||
|
return new ChatRestClient(this, this.#instance, instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes a request with this client's token, and handles error responses.
|
||||||
|
* Clients should usually use `requestIdentity` or `requestChat` instead.
|
||||||
|
* @param method The HTTP method to use.
|
||||||
|
* @param url The URL to request.
|
||||||
|
* @param extraHeaders Extra headers to add to the request, if any.
|
||||||
|
* @param body The request body, if any.
|
||||||
|
* @returns A parsed JSON response.
|
||||||
|
* @throws RestError
|
||||||
|
*/
|
||||||
|
async rawRequest<T>(
|
||||||
|
method: string,
|
||||||
|
url: string,
|
||||||
|
extraHeaders?: Record<string, string>,
|
||||||
|
body?: any,
|
||||||
|
): Promise<T> {
|
||||||
|
const requestBody = body ? JSON.stringify(body) : undefined;
|
||||||
|
const headers = requestBody
|
||||||
|
? {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Content-Length": `${requestBody.length}`,
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const resp = await fetch(url, {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
...(headers || {}),
|
||||||
|
...(extraHeaders || {}),
|
||||||
|
Authorization: this.#token,
|
||||||
|
},
|
||||||
|
body: requestBody,
|
||||||
|
});
|
||||||
|
|
||||||
|
// If we didn't get a JSON response, return
|
||||||
|
if (resp.headers.get("Content-Type") !== "application/json") {
|
||||||
|
throw new RestError(ErrorCode.RESPONSE_NOT_JSON);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await resp.json();
|
||||||
|
if (resp.status < 200 || resp.status >= 400)
|
||||||
|
throw new RestError(data.code, data.message);
|
||||||
|
|
||||||
|
return data as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes a request to an identity instance endpoint. The string `/_fox/ident` is automatically prepended to the path.
|
||||||
|
* For example, when making a request to `/_fox/ident/node`, `path` should be `/node`.
|
||||||
|
* @param method The HTTP method to use.
|
||||||
|
* @param path The path to request.
|
||||||
|
* @param body The request body, if any.
|
||||||
|
* @returns A parsed JSON response.
|
||||||
|
* @throws RestError
|
||||||
|
*/
|
||||||
|
request<T>(method: string, path: string, body?: any): Promise<T> {
|
||||||
|
return this.rawRequest(
|
||||||
|
method,
|
||||||
|
`https://${this.#instance}/_fox/ident${path}`,
|
||||||
|
undefined,
|
||||||
|
body,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get the current instance's node data. */
|
||||||
|
getNode() {
|
||||||
|
return this.request<ident.Node>("GET", "/node");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ChatRestClient {
|
||||||
|
#chatInstance: string;
|
||||||
|
#identityInstance: string;
|
||||||
|
#restClient: RestClient;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
restClient: RestClient,
|
||||||
|
identityInstance: string,
|
||||||
|
chatInstance: string,
|
||||||
|
) {
|
||||||
|
this.#restClient = restClient;
|
||||||
|
this.#identityInstance = identityInstance;
|
||||||
|
this.#chatInstance = chatInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes a request to a proxied chat instance endpoint. The string `/_fox/proxy` is automatically prepended to the path.
|
||||||
|
* For example, when making a request to `/_fox/proxy/channels/01HMKRAG13AGK1WN7XJ0BY2KGB/messages`,
|
||||||
|
* `path` should be `/channels/01HMKRAG13AGK1WN7XJ0BY2KGB/messages`.
|
||||||
|
* @param method The HTTP method to use.
|
||||||
|
* @param path The path to request.
|
||||||
|
* @param body The request body, if any.
|
||||||
|
* @returns A parsed JSON response.
|
||||||
|
* @throws RestError
|
||||||
|
*/
|
||||||
|
request<T>(method: string, path: string, body?: any): Promise<T> {
|
||||||
|
return this.#restClient.rawRequest(
|
||||||
|
method,
|
||||||
|
`https://${this.#identityInstance}/_fox/proxy${path}`,
|
||||||
|
{ [Constants.serverProxyHeader]: this.#chatInstance },
|
||||||
|
body,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
createGuild(name: string) {
|
||||||
|
return this.request<chat.Guild>("POST", "/guilds", { name });
|
||||||
|
}
|
||||||
|
|
||||||
|
createMessage(channelId: string, content: string) {
|
||||||
|
return this.request<chat.Message>(
|
||||||
|
"POST",
|
||||||
|
`/channels/${channelId}/messages`,
|
||||||
|
{ content },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
26
packages/foxchat.js/src/rest/RestError.ts
Normal file
26
packages/foxchat.js/src/rest/RestError.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
export default class RestError {
|
||||||
|
#code: ErrorCode;
|
||||||
|
#message: string | undefined;
|
||||||
|
|
||||||
|
constructor(code: ErrorCode, message: string | undefined = undefined) {
|
||||||
|
this.#code = code;
|
||||||
|
this.#message = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
is(code: ErrorCode) {
|
||||||
|
return this.#code === code;
|
||||||
|
}
|
||||||
|
|
||||||
|
toString() {
|
||||||
|
return this.#message
|
||||||
|
? `RestError(${this.#code}): ${this.#message}`
|
||||||
|
: `RestError(${this.#code})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ErrorCode {
|
||||||
|
INTERNAL_SERVER_ERROR = "INTERNAL_SERVER_ERROR",
|
||||||
|
|
||||||
|
// Client-specific errors
|
||||||
|
RESPONSE_NOT_JSON = "ResponseNotJson",
|
||||||
|
}
|
2
packages/foxchat.js/src/rest/index.ts
Normal file
2
packages/foxchat.js/src/rest/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export { default as RestClient, ChatRestClient } from "./RestClient.js";
|
||||||
|
export { default as RestError, ErrorCode } from "./RestError.js";
|
144
packages/foxchat.js/src/ws/WebsocketClient.ts
Normal file
144
packages/foxchat.js/src/ws/WebsocketClient.ts
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
import WebSocket from "isomorphic-ws";
|
||||||
|
import EventEmitter from "eventemitter3";
|
||||||
|
import {
|
||||||
|
ClientPayload,
|
||||||
|
Dispatch,
|
||||||
|
ServerPayload,
|
||||||
|
} from "../entities/ws/payload.js";
|
||||||
|
|
||||||
|
export type ClientOptions = {
|
||||||
|
debug?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type WebsocketEvents = {
|
||||||
|
event: (instanceId: string, event: Dispatch) => void;
|
||||||
|
error: (message: string) => void;
|
||||||
|
wsError: (error: WebSocket.ErrorEvent) => void;
|
||||||
|
wsClose: (event: WebSocket.CloseEvent) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class Client extends EventEmitter<WebsocketEvents> {
|
||||||
|
#instance: string;
|
||||||
|
#token: string;
|
||||||
|
#ws: WebSocket | undefined;
|
||||||
|
#debug = false;
|
||||||
|
#connected = false;
|
||||||
|
#instances: Array<{ id: string; domain: string }> = [];
|
||||||
|
#guilds: string[] = [];
|
||||||
|
|
||||||
|
#heartbeatTimer: any = undefined;
|
||||||
|
#heartbeatInterval: number | undefined;
|
||||||
|
#lastHeartbeatSend: number | undefined;
|
||||||
|
#lastHeartbeatAck: number | undefined;
|
||||||
|
|
||||||
|
constructor(instance: string, token: string, opts?: ClientOptions) {
|
||||||
|
super();
|
||||||
|
this.#debug = opts?.debug || false;
|
||||||
|
this.#instance = instance;
|
||||||
|
this.#token = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
async connect() {
|
||||||
|
this.#debug &&
|
||||||
|
console.log(`Connecting to "wss://${this.#instance}/_fox/ident/ws"`);
|
||||||
|
this.#ws = new WebSocket(`wss://${this.#instance}/_fox/ident/ws`);
|
||||||
|
|
||||||
|
this.#ws.onerror = (error) => {
|
||||||
|
this.emit("wsError", error);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.#ws.onopen = () => {
|
||||||
|
this.send({ t: "IDENTIFY", d: { token: this.#token } });
|
||||||
|
};
|
||||||
|
|
||||||
|
this.#ws.onmessage = (event) => {
|
||||||
|
if (typeof event.data === "string") {
|
||||||
|
this.handleMessage(JSON.parse(event.data));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.#ws.onclose = (event) => {
|
||||||
|
this.emit("wsClose", event);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
this.#ws?.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
send(payload: ClientPayload) {
|
||||||
|
if (this.#debug)
|
||||||
|
console.log(
|
||||||
|
"[CLIENT] Sending %s (%s)",
|
||||||
|
payload.t,
|
||||||
|
JSON.stringify(payload.d),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!this.#ws) throw "Socket is closed";
|
||||||
|
this.#ws.send(JSON.stringify(payload));
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMessage(payload: ServerPayload) {
|
||||||
|
if (this.#debug)
|
||||||
|
console.log(
|
||||||
|
"[SERVER] Received %s (%s)",
|
||||||
|
payload.t,
|
||||||
|
JSON.stringify(payload.d),
|
||||||
|
);
|
||||||
|
|
||||||
|
switch (payload.t) {
|
||||||
|
case "HELLO":
|
||||||
|
this.#connected = true;
|
||||||
|
this.#guilds = payload.d.guilds;
|
||||||
|
this.#instances = payload.d.instances;
|
||||||
|
// Start heartbeat interval
|
||||||
|
this.#heartbeatInterval = payload.d.heartbeat_interval;
|
||||||
|
if (this.#heartbeatTimer) {
|
||||||
|
clearInterval(this.#heartbeatTimer);
|
||||||
|
}
|
||||||
|
this.#heartbeatTimer = setInterval(() => {
|
||||||
|
this.heartbeat();
|
||||||
|
}, this.#heartbeatInterval);
|
||||||
|
|
||||||
|
break;
|
||||||
|
case "HEARTBEAT_ACK":
|
||||||
|
this.#lastHeartbeatAck = Date.now();
|
||||||
|
break;
|
||||||
|
case "D":
|
||||||
|
this.emit("event", payload.d.s, payload.d.e);
|
||||||
|
break;
|
||||||
|
case "ERROR":
|
||||||
|
this.emit("error", payload.d.message);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
heartbeat() {
|
||||||
|
if (
|
||||||
|
this.#heartbeatInterval &&
|
||||||
|
this.#lastHeartbeatAck &&
|
||||||
|
this.#heartbeatInterval * 3 < Date.now() - this.#lastHeartbeatAck
|
||||||
|
) {
|
||||||
|
this.emit(
|
||||||
|
"error",
|
||||||
|
`Last heartbeat ACK was more than ${this.#heartbeatInterval * 3}ms ago, disconnecting`,
|
||||||
|
);
|
||||||
|
this.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#lastHeartbeatSend = Date.now();
|
||||||
|
this.send({
|
||||||
|
t: "HEARTBEAT",
|
||||||
|
d: { t: Math.round(this.#lastHeartbeatSend / 1000) },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get instances() {
|
||||||
|
return this.#instances;
|
||||||
|
}
|
||||||
|
|
||||||
|
get ping() {
|
||||||
|
if (!this.#lastHeartbeatAck || !this.#lastHeartbeatSend) return 0;
|
||||||
|
this.#lastHeartbeatAck - this.#lastHeartbeatSend;
|
||||||
|
}
|
||||||
|
}
|
1
packages/foxchat.js/src/ws/index.ts
Normal file
1
packages/foxchat.js/src/ws/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export { default as WebsocketClient } from "./WebsocketClient.js";
|
10
packages/foxchat.js/tsconfig.json
Normal file
10
packages/foxchat.js/tsconfig.json
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"module": "ES6",
|
||||||
|
"outDir": "build",
|
||||||
|
"strict": true,
|
||||||
|
"target": "ES2022",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue