241 lines
No EOL
6.5 KiB
JavaScript
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
|
|
})
|
|
}
|
|
}; |