add discord bridge
This commit is contained in:
parent
3ef8298745
commit
a7bb23060b
8 changed files with 795 additions and 1 deletions
12
discord-waffle-bridge/package.json
Normal file
12
discord-waffle-bridge/package.json
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"name": "discord-waffle-bridge",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "src/index.js",
|
||||||
|
"license": "MIT",
|
||||||
|
"type": "module",
|
||||||
|
"dependencies": {
|
||||||
|
"dotenv": "^16.0.3",
|
||||||
|
"node-fetch": "^3.2.10",
|
||||||
|
"ws": "^8.10.0"
|
||||||
|
}
|
||||||
|
}
|
333
discord-waffle-bridge/src/DiscordClient.js
Normal file
333
discord-waffle-bridge/src/DiscordClient.js
Normal file
|
@ -0,0 +1,333 @@
|
||||||
|
import EventEmitter from "events";
|
||||||
|
import { WebSocket } from "ws";
|
||||||
|
import fetch from "node-fetch";
|
||||||
|
import logger from "./logging.js";
|
||||||
|
|
||||||
|
const log = logger("DiscordClient");
|
||||||
|
|
||||||
|
const opcodes = {
|
||||||
|
EVENT: 0,
|
||||||
|
CLIENT_HEARTBEAT: 1,
|
||||||
|
IDENTIFY: 2,
|
||||||
|
UPDATE_PRESENCE: 3,
|
||||||
|
RECONNECT: 7,
|
||||||
|
INVALID_SESSION: 9,
|
||||||
|
HELLO: 10,
|
||||||
|
HEARTBEAT_ACK: 11,
|
||||||
|
};
|
||||||
|
|
||||||
|
const skipReconnectFor = [
|
||||||
|
4004, 4010, 4011, 4012, 4013, 4014
|
||||||
|
];
|
||||||
|
|
||||||
|
const CLOSE_CONNECTION_ON_NO_ACK = false;
|
||||||
|
|
||||||
|
class DiscordClient extends EventEmitter {
|
||||||
|
constructor(token, { intents, gatewayUrl="wss://gateway.discord.gg/?v=10&encoding=json", apiBase="https://discord.com/api/v10", presence = { activities: [{name: "the voices", type: 2}], status: "online", afk: false } } = {}) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.token = token;
|
||||||
|
this.gatewayUrl = gatewayUrl;
|
||||||
|
this.apiBase = apiBase;
|
||||||
|
this.ws = null;
|
||||||
|
this.intents = intents;
|
||||||
|
|
||||||
|
this.user = null;
|
||||||
|
this.guilds = [];
|
||||||
|
this.sessionId = null;
|
||||||
|
this.seq = null;
|
||||||
|
this.gotServerHeartbeatACK = true;
|
||||||
|
this.defaultPresence = presence;
|
||||||
|
this.connectEnabled = true;
|
||||||
|
|
||||||
|
this.knownWebhooks = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
_setHeartbeat(interval) {
|
||||||
|
this._heartbeatIntervalTime = interval;
|
||||||
|
if (interval < 0 && this._heartbeatInterval) {
|
||||||
|
clearInterval(this._heartbeatInterval);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._heartbeatInterval = setInterval(() => {
|
||||||
|
if (CLOSE_CONNECTION_ON_NO_ACK && !this.gotServerHeartbeatACK) {
|
||||||
|
log("Closing due to no heartbeat ACK...");
|
||||||
|
this.ws.close(1000, "No heartbeat ACK.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.gotServerHeartbeatACK = false;
|
||||||
|
|
||||||
|
this.ws.send(JSON.stringify({
|
||||||
|
op: opcodes.CLIENT_HEARTBEAT,
|
||||||
|
d: this.seq
|
||||||
|
}));
|
||||||
|
}, this._heartbeatIntervalTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
_getIdentifyPayload() {
|
||||||
|
return {
|
||||||
|
token: this.token,
|
||||||
|
intents: this.intents,
|
||||||
|
properties: {
|
||||||
|
"$os": "linux",
|
||||||
|
"$browser": "generic",
|
||||||
|
"$device": "generic"
|
||||||
|
},
|
||||||
|
presence: this.defaultPresence
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleGatewayMessage(ws, message) {
|
||||||
|
try {
|
||||||
|
message = JSON.parse(message);
|
||||||
|
} catch(e) {
|
||||||
|
log("on 'message': failed to parse incoming message as JSON", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.s) {
|
||||||
|
this.seq = message.s;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = message.d;
|
||||||
|
|
||||||
|
switch (message.op) {
|
||||||
|
case opcodes.HELLO: {
|
||||||
|
log(`HELLO; heartbeat_interval=${payload.heartbeat_interval}`);
|
||||||
|
this._setHeartbeat(payload.heartbeat_interval);
|
||||||
|
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
op: opcodes.IDENTIFY,
|
||||||
|
d: this._getIdentifyPayload()
|
||||||
|
}));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case opcodes.EVENT: {
|
||||||
|
switch (message.t) {
|
||||||
|
case "READY": {
|
||||||
|
this.user = payload.user;
|
||||||
|
this.sessionId = payload.session_id;
|
||||||
|
this.guilds = payload.guilds;
|
||||||
|
log(`READY. Connected as '${this.user.username}', got ${this.guilds.length} guilds.`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "GUILD_CREATE": {
|
||||||
|
const targetGuildIndex = this.guilds.findIndex(e => e.id === payload.id);
|
||||||
|
if (targetGuildIndex < 0) {
|
||||||
|
this.guilds.push(payload);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// The guild already exists in our array. This means that
|
||||||
|
// this GUILD_CREATE event is completing our `Unavailable Guild`
|
||||||
|
// objects that we got from the initial READY.
|
||||||
|
this.guilds[targetGuildIndex] = payload;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "GUILD_UPDATE": {
|
||||||
|
const targetGuildIndex = this.guilds.findIndex(e => e.id === payload.id);
|
||||||
|
if (targetGuildIndex < 0) {
|
||||||
|
// tried to update a guild that doesn't exist???
|
||||||
|
this.emit("warn", "got GUILD_UPDATE for a guild that doesn't exist");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this.guilds[targetGuildIndex] = payload;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "GUILD_DELETE": {
|
||||||
|
const targetGuildIndex = this.guilds.findIndex(e => e.id === payload.id);
|
||||||
|
if (targetGuildIndex < 0) {
|
||||||
|
// tried to delete a guild that doesn't exist???
|
||||||
|
this.emit("warn", "got GUILD_DELETE for a guild that doesn't exist");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this.guilds.splice(targetGuildIndex, 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "CHANNEL_CREATE": {
|
||||||
|
const parentGuildIndex = this.guilds.findIndex(e => e.id === payload.guild_id);
|
||||||
|
if (parentGuildIndex < 0) {
|
||||||
|
this.emit("warn", "got CHANNEL_CREATE for a guild that doesn't exist");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this.guilds[parentGuildIndex].channels.push(payload);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "CHANNEL_UPDATE": {
|
||||||
|
const parentGuildIndex = this.guilds.findIndex(e => e.id === payload.guild_id);
|
||||||
|
if (parentGuildIndex < 0) {
|
||||||
|
this.emit("warn", "got CHANNEL_CREATE for a guild that doesn't exist");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const relevantChannelIndex = this.guilds[parentGuildIndex].channels.findIndex(e => e.id === payload.id);
|
||||||
|
this.guilds[parentGuildIndex].channels[relevantChannelIndex] = payload;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "CHANNEL_DELETE": {
|
||||||
|
const parentGuildIndex = this.guilds.findIndex(e => e.id === payload.guild_id);
|
||||||
|
if (parentGuildIndex < 0) {
|
||||||
|
this.emit("warn", "got CHANNEL_CREATE for a guild that doesn't exist");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const relevantChannelIndex = this.guilds[parentGuildIndex].channels.findIndex(e => e.id === payload.id);
|
||||||
|
this.guilds[parentGuildIndex].channels.splice(relevantChannelIndex, 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
this.emit(message.t, payload);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case opcodes.HEARTBEAT_ACK: {
|
||||||
|
this.gotServerHeartbeatACK = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case opcodes.INVALID_SESSION: {
|
||||||
|
log("INVALID_SESSION - please check your authentication token");
|
||||||
|
log("INVALID_SESSION: will not reconnect");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case opcodes.RECONNECT: {
|
||||||
|
log("gateway is requesting reconnect (payload RECONNECT)");
|
||||||
|
this.connect();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
log(`got unhandled opcode "${message.op}"`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
if (!this.connectEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log("connecting...");
|
||||||
|
if (this.ws) {
|
||||||
|
log("a websocket connection already exists, killing...");
|
||||||
|
this.ws.removeAllListeners();
|
||||||
|
this._setHeartbeat(-1);
|
||||||
|
this.ws.close();
|
||||||
|
this.ws = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ws = new WebSocket(this.gatewayUrl);
|
||||||
|
this.ws = ws;
|
||||||
|
|
||||||
|
ws.on("message", (data, isBinary) => {
|
||||||
|
if (!isBinary) {
|
||||||
|
this._handleGatewayMessage(ws, data.toString());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on("open", () => {
|
||||||
|
log("WebSocket 'open'");
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on("close", (code, reason) => {
|
||||||
|
reason = reason.toString();
|
||||||
|
log(`on 'close': disconnected from gateway: code '${code}', reason '${reason}'`);
|
||||||
|
|
||||||
|
this.emit("close", code, reason);
|
||||||
|
this._setHeartbeat(-1);
|
||||||
|
if (skipReconnectFor.includes(code)) {
|
||||||
|
log("on 'close': the exit code above is in skipReconnectFor, and thus the server will not reconnect.");
|
||||||
|
} else {
|
||||||
|
log("on 'close': the client will now attempt to reconnect...");
|
||||||
|
this.connect();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on("error", (e) => {
|
||||||
|
log("on 'error': websocket error:", e);
|
||||||
|
log("on 'error': reconnecting due to previous websocket error...");
|
||||||
|
this._setHeartbeat(-1);
|
||||||
|
this.connect();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.connectEnabled = false;
|
||||||
|
this._setHeartbeat(-1);
|
||||||
|
this.ws.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
async api(method, path, body=undefined, throwOnError=true) {
|
||||||
|
const options = {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
"authorization": `Bot ${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(`${this.apiBase}${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, avatarUrl=null) {
|
||||||
|
if (typeof content !== "string") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
content = content.trim();
|
||||||
|
|
||||||
|
if (content.length < 1 || content.length > 2000) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let webhook = this.knownWebhooks.get(channelId);
|
||||||
|
if (!webhook) {
|
||||||
|
webhook = (await this.api("GET", `/channels/${channelId}/webhooks`)).find(e => e.name === "well_known__bridge");
|
||||||
|
|
||||||
|
if (!webhook) {
|
||||||
|
webhook = await this.api("POST", `/channels/${channelId}/webhooks`, {
|
||||||
|
name: "well_known__bridge"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.knownWebhooks.set(channelId, webhook);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.api("POST", `/webhooks/${webhook.id}/${webhook.token}?wait=false`, {
|
||||||
|
content,
|
||||||
|
username,
|
||||||
|
avatar_url: avatarUrl,
|
||||||
|
tts: false,
|
||||||
|
allowed_mentions: {
|
||||||
|
parse: ["users"]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DiscordClient;
|
241
discord-waffle-bridge/src/WaffleClient.js
Normal file
241
discord-waffle-bridge/src/WaffleClient.js
Normal file
|
@ -0,0 +1,241 @@
|
||||||
|
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 }) => {
|
||||||
|
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.init(token);
|
||||||
|
}, this.reconnectDelay);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dispatch(GatewayEventType.Close, code);
|
||||||
|
|
||||||
|
log("close");
|
||||||
|
};
|
||||||
|
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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
8
discord-waffle-bridge/src/config.js
Normal file
8
discord-waffle-bridge/src/config.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import * as dotenv from "dotenv";
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
export const WAFFLE_API_BASE = process.env.WAFFLE_BASE ?? "http://localhost:3000/api/v1";
|
||||||
|
export const WAFFLE_GATEWAY_BASE = process.env.WAFFLE_GATEWAY_BASE ?? "ws://localhost:3000/gateway";
|
||||||
|
export const WAFFLE_TOKEN = process.env.WAFFLE_TOKEN;
|
||||||
|
export const DISCORD_TOKEN = process.env.DISCORD_TOKEN;
|
||||||
|
export const DISCORD_GUILD = process.env.DISCORD_GUILD;
|
140
discord-waffle-bridge/src/index.js
Normal file
140
discord-waffle-bridge/src/index.js
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
import { GatewayEventType } from "./WaffleClient.js";
|
||||||
|
import { DISCORD_GUILD, DISCORD_TOKEN, WAFFLE_TOKEN } from "./config.js";
|
||||||
|
import DiscordClient from "./DiscordClient.js";
|
||||||
|
import WaffleClient from "./WaffleClient.js";
|
||||||
|
|
||||||
|
class Bridge {
|
||||||
|
constructor() {
|
||||||
|
this.discordClient = new DiscordClient(
|
||||||
|
DISCORD_TOKEN,
|
||||||
|
{
|
||||||
|
intents: 0 | (1 << 0) | (1 << 9) | (1 << 15), // GUILDS | GUILD_MESSAGES | MESSAGE_CONTENT
|
||||||
|
}
|
||||||
|
);
|
||||||
|
this.waffleClient = new WaffleClient(WAFFLE_TOKEN, {
|
||||||
|
bridgesTo: "Discord Inc. (not affiliated)",
|
||||||
|
privacy: "https://discord.com/privacy",
|
||||||
|
terms: "https://discord.com/terms"
|
||||||
|
});
|
||||||
|
this.waffleChannelIdToDiscordChannelIdMap = new Map();
|
||||||
|
this.discordChannelIdToWaffleChannelIdMap = new Map();
|
||||||
|
|
||||||
|
this.waffleClient.subscribe(GatewayEventType.MessageCreate, async (message) => {
|
||||||
|
if (this.waffleClient.user && message.author_id === this.waffleClient.user.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const channel = this.waffleChannelIdToDiscordChannelId(message.channel_id);
|
||||||
|
if (channel === -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.discordClient.sendMessageAs(channel, message.content, message.author_username);
|
||||||
|
} catch (o_0) {
|
||||||
|
console.error("Failed to send message to Discord", o_0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.discordClient.on("MESSAGE_CREATE", async (message) => {
|
||||||
|
if (message.guild_id !== DISCORD_GUILD || message.application_id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const channel = this.discordChannelIdToWaffleChannelId(message.channel_id);
|
||||||
|
if (channel === -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = "";
|
||||||
|
|
||||||
|
message.attachments.forEach(e => {
|
||||||
|
content += `<attachment ${e.filename}: ${e.proxy_url}> `;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (message.referenced_message) {
|
||||||
|
let trimmedContent = message.referenced_message.content.substring(0, 300);
|
||||||
|
if (trimmedContent !== message.referenced_message.content) {
|
||||||
|
trimmedContent += "...";
|
||||||
|
}
|
||||||
|
if (content !== "") {
|
||||||
|
// Add 2 newlines after the attachments area
|
||||||
|
content += "\n\n";
|
||||||
|
}
|
||||||
|
content += `> ${trimmedContent}\n@Discord/${message.referenced_message.author.username}: `;
|
||||||
|
}
|
||||||
|
|
||||||
|
content += message.content;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.waffleClient.sendMessageAs(channel, content, message.author.username);
|
||||||
|
} catch (o_0) {
|
||||||
|
console.error("Failed to send message to Waffle", o_0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
discordChannelIdToWaffleChannelId(discordChannelId) {
|
||||||
|
if (this.discordChannelIdToWaffleChannelIdMap.get(discordChannelId)) {
|
||||||
|
return this.discordChannelIdToWaffleChannelIdMap.get(discordChannelId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const guild = this.discordClient.guilds.find(e => e.id === DISCORD_GUILD);
|
||||||
|
|
||||||
|
if (!this.waffleClient.authenticated || !this.waffleClient.channels || !guild) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const discordChannel = guild.channels.find(e => e.id === discordChannelId);
|
||||||
|
if (!discordChannel) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const waffleChannel = this.waffleClient.channels.find(e => e.name === discordChannel.name);
|
||||||
|
if (!waffleChannel) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.discordChannelIdToWaffleChannelIdMap.set(discordChannelId, waffleChannel.id);
|
||||||
|
|
||||||
|
return waffleChannel.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
waffleChannelIdToDiscordChannelId(waffleChannelId) {
|
||||||
|
if (this.waffleChannelIdToDiscordChannelIdMap.get(waffleChannelId)) {
|
||||||
|
return this.waffleChannelIdToDiscordChannelIdMap.get(waffleChannelId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const guild = this.discordClient.guilds.find(e => e.id === DISCORD_GUILD);
|
||||||
|
|
||||||
|
if (!this.waffleClient.authenticated || !this.waffleClient.channels || !guild) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const waffleChannel = this.waffleClient.channels.find(a => a.id === waffleChannelId);
|
||||||
|
if (!waffleChannel) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const discordChannel = guild.channels.find(e => e.name === waffleChannel.name);
|
||||||
|
if (!discordChannel) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.waffleChannelIdToDiscordChannelIdMap.set(waffleChannelId, discordChannel.id);
|
||||||
|
|
||||||
|
return discordChannel.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
this.discordClient.connect();
|
||||||
|
this.waffleClient.connect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const bridge = new Bridge();
|
||||||
|
bridge.connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
8
discord-waffle-bridge/src/logging.js
Normal file
8
discord-waffle-bridge/src/logging.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
export default function logger(sink) {
|
||||||
|
return (...args) => {
|
||||||
|
console.log(
|
||||||
|
`[${sink}]`,
|
||||||
|
...args
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
52
discord-waffle-bridge/yarn.lock
Normal file
52
discord-waffle-bridge/yarn.lock
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||||
|
# yarn lockfile v1
|
||||||
|
|
||||||
|
|
||||||
|
data-uri-to-buffer@^4.0.0:
|
||||||
|
version "4.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz#b5db46aea50f6176428ac05b73be39a57701a64b"
|
||||||
|
integrity sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA==
|
||||||
|
|
||||||
|
dotenv@^16.0.3:
|
||||||
|
version "16.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.3.tgz#115aec42bac5053db3c456db30cc243a5a836a07"
|
||||||
|
integrity sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==
|
||||||
|
|
||||||
|
fetch-blob@^3.1.2, fetch-blob@^3.1.4:
|
||||||
|
version "3.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-3.2.0.tgz#f09b8d4bbd45adc6f0c20b7e787e793e309dcce9"
|
||||||
|
integrity sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==
|
||||||
|
dependencies:
|
||||||
|
node-domexception "^1.0.0"
|
||||||
|
web-streams-polyfill "^3.0.3"
|
||||||
|
|
||||||
|
formdata-polyfill@^4.0.10:
|
||||||
|
version "4.0.10"
|
||||||
|
resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz#24807c31c9d402e002ab3d8c720144ceb8848423"
|
||||||
|
integrity sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==
|
||||||
|
dependencies:
|
||||||
|
fetch-blob "^3.1.2"
|
||||||
|
|
||||||
|
node-domexception@^1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5"
|
||||||
|
integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==
|
||||||
|
|
||||||
|
node-fetch@^3.2.10:
|
||||||
|
version "3.2.10"
|
||||||
|
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.2.10.tgz#e8347f94b54ae18b57c9c049ef641cef398a85c8"
|
||||||
|
integrity sha512-MhuzNwdURnZ1Cp4XTazr69K0BTizsBroX7Zx3UgDSVcZYKF/6p0CBe4EUb/hLqmzVhl0UpYfgRljQ4yxE+iCxA==
|
||||||
|
dependencies:
|
||||||
|
data-uri-to-buffer "^4.0.0"
|
||||||
|
fetch-blob "^3.1.4"
|
||||||
|
formdata-polyfill "^4.0.10"
|
||||||
|
|
||||||
|
web-streams-polyfill@^3.0.3:
|
||||||
|
version "3.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz#71c2718c52b45fd49dbeee88634b3a60ceab42a6"
|
||||||
|
integrity sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==
|
||||||
|
|
||||||
|
ws@^8.10.0:
|
||||||
|
version "8.10.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/ws/-/ws-8.10.0.tgz#00a28c09dfb76eae4eb45c3b565f771d6951aa51"
|
||||||
|
integrity sha512-+s49uSmZpvtAsd2h37vIPy1RBusaLawVe8of+GyEPsaJTCMpj/2v8NpeK1SHXjBlQ95lQTmQofOJnFiLoaN3yw==
|
|
@ -183,7 +183,7 @@ function sendPayload(ws: WebSocket, payload: GatewayPayload) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPresenceEntryForConnection(ws: WebSocket, status: GatewayPresenceStatus): GatewayPresenceEntry | null {
|
function getPresenceEntryForConnection(ws: WebSocket, status: GatewayPresenceStatus): GatewayPresenceEntry | null {
|
||||||
if (!ws.state || !ws.state.ready || !ws.state.user) {
|
if (!ws.state || !ws.state.user) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue