waffle/discord-waffle-bridge/src/WaffleClient.js

241 lines
No EOL
6.5 KiB
JavaScript

import fetch from "node-fetch";
import { WebSocket } from "ws";
import { WAFFLE_API_BASE, WAFFLE_GATEWAY_BASE } from "./config.js";
import logger from "./logging.js";
export const GatewayErrors = {
BAD_PAYLOAD: 4001,
BAD_AUTH: 4002,
AUTHENTICATION_TIMEOUT: 4003,
NO_PING: 4004,
FLOODING: 4005,
ALREADY_AUTHENTICATED: 4006,
PAYLOAD_TOO_LARGE: 4007,
TOO_MANY_SESSIONS: 4008,
};
export const GatewayPayloadType = {
Hello: 0,
Authenticate: 1,
Ready: 2,
Ping: 3,
ChannelCreate: 110,
ChannelUpdate: 111,
ChannelDelete: 112,
MessageCreate: 120,
MessageUpdate: 121,
MessageDelete: 122,
TypingStart: 130,
PresenceUpdate: 140
}
export const GatewayEventType = {
...GatewayPayloadType,
Open: -5,
Close: -4,
BadAuth: -3,
}
export const GatewayPresenceStatus = {
Offline: 0,
Online: 1
}
const log = logger("WaffleClient");
export default class {
constructor(token=null, extraAuthParams={}) {
this.ws = null;
this.authenticated = false;
this.open = false;
this.heartbeatInterval = null;
this.user = null;
this.channels = null;
this.reconnectDelay = 400;
this.reconnectTimeout = null;
this.handlers = new Map();
this.disableReconnect = false;
this.token = token;
this.extraAuthParams = extraAuthParams;
}
connect() {
if (!this.token) {
log("no auth token, skipping connection");
this.dispatch(GatewayEventType.Close, GatewayErrors.BAD_AUTH);
this.dispatch(GatewayEventType.BadAuth, 0);
return false;
}
log(`connecting to gateway - gatewayBase: ${WAFFLE_GATEWAY_BASE}`);
this.ws = new WebSocket(WAFFLE_GATEWAY_BASE);
this.ws.onopen = () => {
if (this.reconnectTimeout) {
clearTimeout(this.reconnectTimeout);
}
this.disableReconnect = false;
this.open = true;
this.dispatch(GatewayEventType.Open, null);
log("open");
};
this.ws.onmessage = (event) => {
const payload = JSON.parse(event.data);
switch (payload.t) {
case GatewayPayloadType.Hello: {
this.send({
t: GatewayPayloadType.Authenticate,
d: {
token: this.token,
...this.extraAuthParams
}
});
this.heartbeatInterval = setInterval(() => {
this.send({
t: GatewayPayloadType.Ping,
d: 0
});
}, payload.d.pingInterval);
log("hello");
break;
}
case GatewayPayloadType.Ready: {
this.user = payload.d.user;
this.channels = payload.d.channels;
this.authenticated = true;
this.reconnectDelay = 400;
log("ready");
break;
}
}
this.dispatch(payload.t, payload.d);
};
this.ws.onclose = ({ code, reason }) => {
this.authenticated = false;
this.user = null;
this.channels = null;
this.open = false;
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
}
if (code === GatewayErrors.BAD_AUTH) {
this.dispatch(GatewayEventType.BadAuth, 1);
if (this.reconnectTimeout)
clearTimeout(this.reconnectTimeout);
} else if (this.disableReconnect) {
if (this.reconnectTimeout)
clearTimeout(this.reconnectTimeout);
} else {
if (this.reconnectDelay < 60000) {
this.reconnectDelay *= 2;
}
this.reconnectTimeout = setTimeout(() => {
this.connect();
}, this.reconnectDelay);
}
this.dispatch(GatewayEventType.Close, code);
log("close", code, reason);
};
this.ws.onerror = (e) => {
log("websocket: onerror", e);
};
return true;
}
send(data) {
return this.ws.send(JSON.stringify(data));
}
dispatch(event, payload) {
const eventHandlers = this.handlers.get(event);
if (!eventHandlers)
return;
eventHandlers.forEach((e) => {
e(payload);
});
}
subscribe(event, handler) {
if (!this.handlers.get(event)) {
this.handlers.set(event, new Set());
}
this.handlers.get(event).add(handler);
return handler; // can later be used for unsubscribe()
}
unsubscribe(event, handler) {
const eventHandlers = this.handlers.get(event);
if (!eventHandlers)
return;
eventHandlers.delete(handler);
if (eventHandlers.size < 1) {
this.handlers.delete(event);
}
}
close() {
this.disableReconnect = true;
if (this.ws)
this.ws.close();
}
async api(method, path, body=undefined, throwOnError=true) {
const options = {
method,
headers: {
"authorization": `Bearer ${this.token}`
}
};
if (method !== "GET" && method !== "HEAD" && typeof body === "object") {
options.headers["content-type"] = "application/json";
options.body = JSON.stringify(body);
}
const response = await fetch(`${WAFFLE_API_BASE}${path}`, options);
let json = {};
try {
json = await response.json();
} catch(o_O) {}
if (!response.ok && throwOnError) {
throw new Error(`API request returned non-success status ${response.status}, with JSON body ${JSON.stringify(json)}`);
}
return json;
}
async sendMessageAs(channelId, content, username) {
if (typeof content !== "string") {
return;
}
content = content.trim();
if (content.length < 1 || content.length > 2000) {
return;
}
await this.api("POST", `/channels/${channelId}/messages`, {
content,
nick_username: username
})
}
};