From 11e5cd7f7746a6cd009d90b2ac140203b078d873 Mon Sep 17 00:00:00 2001 From: Papa Date: Sun, 5 Mar 2023 09:05:35 -0700 Subject: [PATCH] Add heatsync and support hot reloading --- index.js | 48 ++++++++--------------- modules/DiscordClient.js | 83 +++++++--------------------------------- modules/DiscordEvents.js | 5 ++- modules/DiscordUtils.js | 63 ++++++++++++++++++++++++++++++ package.json | 5 ++- passthrough.js | 10 +++++ stdin.js | 47 +++++++++++++++++++++++ 7 files changed, 155 insertions(+), 106 deletions(-) create mode 100644 modules/DiscordUtils.js create mode 100644 passthrough.js create mode 100644 stdin.js diff --git a/index.js b/index.js index 24942826..44bfa002 100644 --- a/index.js +++ b/index.js @@ -1,39 +1,23 @@ -const repl = require("repl") -const util = require("util") +const HeatSync = require("heatsync") + +const config = require("./config") +const passthrough = require("./passthrough") + +const sync = new HeatSync() + +Object.assign(passthrough, { config, sync }) const DiscordClient = require("./modules/DiscordClient") -const config = require("./config") - const discord = new DiscordClient(config.discordToken) +passthrough.discord = discord -discord.cloud.connect().then(() => console.log("Discord gateway started")) +;(async () => { + await discord.cloud.connect() + console.log("Discord gateway started") -/** - * @param {string} input - * @param {import("vm").Context} _context - * @param {string} _filename - * @param {(err: Error | null, result: unknown) => unknown} callback - * @returns - */ -async function customEval(input, _context, _filename, callback) { - let depth = 0 - if (input === "exit\n") return process.exit() - if (input.startsWith(":")) { - const depthOverwrite = input.split(" ")[0] - depth = +depthOverwrite.slice(1) - input = input.slice(depthOverwrite.length + 1) - } - /** @type {unknown} */ - let result - try { - result = await eval(input) - const output = util.inspect(result, false, depth, true) - return callback(null, output) - } catch (e) { - return callback(e, undefined) - } -} + require("./stdin") +})() -const cli = repl.start({ prompt: "", eval: customEval, writer: s => s }) -cli.once("exit", process.exit) +process.on("unhandledRejection", console.error) +process.on("uncaughtException", console.error) diff --git a/modules/DiscordClient.js b/modules/DiscordClient.js index 1be4e09d..714db1e6 100644 --- a/modules/DiscordClient.js +++ b/modules/DiscordClient.js @@ -1,7 +1,11 @@ const { SnowTransfer } = require("snowtransfer") const { Client: CloudStorm } = require("cloudstorm") -let wasReadyBefore = false +const passthrough = require("../passthrough") +const { sync } = passthrough + +/** @type {typeof import("./DiscordUtils")} */ +const dUtils = sync.require("./DiscordUtils") class DiscordClient { /** @@ -24,82 +28,21 @@ class DiscordClient { encoding: "json" } }) - /** @type {import("discord-typings").User} */ + this.ready = false + /** @type {import("discord-api-types/v10").APIUser} */ // @ts-ignore avoid setting as or null because we know we need to wait for ready anyways this.user = null - /** @type {import("discord-typings").Application} */ + /** @type {Pick} */ // @ts-ignore this.application = null - /** @type {Map} */ + /** @type {Map} */ this.channels = new Map() - /** @type {Map} */ + /** @type {Map} */ this.guilds = new Map() - /** - * @type {Map>} - * @private - */ + /** @type {Map>} */ this.guildChannelMap = new Map() - this.cloud.on("event", this.onPacket.bind(this)) - } - - /** - * @param {import("cloudstorm").IGatewayMessage} message - * @private - */ - onPacket(message) { - if (message.t === "READY") { - if (wasReadyBefore) return - wasReadyBefore = true - /** @type {import("discord-typings").ReadyPayload} */ - const typed = message.d - this.user = typed.user - this.application = typed.application - console.log(`Discord logged in as ${this.user.username}#${this.user.discriminator} (${this.user.id})`) - - - } else if (message.t === "GUILD_CREATE") { - /** @type {import("discord-typings").Guild} */ - const typed = message.d - this.guilds.set(typed.id, typed) - const arr = [] - this.guildChannelMap.set(typed.id, arr) - for (const channel of typed.channels || []) { - arr.push(channel.id) - this.channels.set(channel.id, channel) - } - - - } else if (message.t === "GUILD_DELETE") { - /** @type {import("discord-typings").Guild} */ - const typed = message.d - this.guilds.delete(typed.id) - const channels = this.guildChannelMap.get(typed.id) - if (channels) { - for (const id of channels) this.channels.delete(id) - } - this.guildChannelMap.delete(typed.id) - - - } else if (message.t === "CHANNEL_CREATE" || message.t === "CHANNEL_DELETE") { - /** @type {import("discord-typings").Channel} */ - const typed = message.d - if (message.t === "CHANNEL_CREATE") { - this.channels.set(typed.id, typed) - if (typed["guild_id"]) { // obj[prop] notation can be used to access a property without typescript complaining that it doesn't exist on all values something can have - const channels = this.guildChannelMap.get(typed["guild_id"]) - if (channels && !channels.includes(typed.id)) channels.push(typed.id) - } - } else { - this.channels.delete(typed.id) - if (typed["guild_id"]) { - const channels = this.guildChannelMap.get(typed["guild_id"]) - if (channels) { - const previous = channels.indexOf(typed.id) - if (previous !== -1) channels.splice(previous, 1) - } - } - } - } + this.cloud.on("event", message => dUtils.onPacket(this, message)) + this.cloud.on("error", console.error) } } diff --git a/modules/DiscordEvents.js b/modules/DiscordEvents.js index 5927f4e7..a33eb13e 100644 --- a/modules/DiscordEvents.js +++ b/modules/DiscordEvents.js @@ -2,10 +2,11 @@ module.exports = { /** * Process Discord messages and convert to a message Matrix can understand * - * @param {import("discord-typings").Message} message + * @param {import("./DiscordClient")} client + * @param {import("discord-api-types/v10").APIMessage} message * @returns {import("../types").MatrixMessage} */ - onMessageCreate: message => { + onMessageCreate(client, message) { return {} } } diff --git a/modules/DiscordUtils.js b/modules/DiscordUtils.js new file mode 100644 index 00000000..46ee4fdb --- /dev/null +++ b/modules/DiscordUtils.js @@ -0,0 +1,63 @@ +const passthrough = require("../passthrough") +const { sync } = passthrough + +/** @type {typeof import("./DiscordEvents")} */ +const DiscordEvents = sync.require("./DiscordEvents") + +const utils = { + /** + * @param {import("./DiscordClient")} client + * @param {import("cloudstorm").IGatewayMessage} message + */ + onPacket(client, message) { + if (message.t === "READY") { + if (client.ready) return + client.ready = true + client.user = message.d.user + client.application = message.d.application + console.log(`Discord logged in as ${client.user.username}#${client.user.discriminator} (${client.user.id})`) + + + } else if (message.t === "GUILD_CREATE") { + client.guilds.set(message.d.id, message.d) + const arr = [] + client.guildChannelMap.set(message.d.id, arr) + for (const channel of message.d.channels || []) { + arr.push(channel.id) + client.channels.set(channel.id, channel) + } + + + } else if (message.t === "GUILD_DELETE") { + client.guilds.delete(message.d.id) + const channels = client.guildChannelMap.get(message.d.id) + if (channels) { + for (const id of channels) client.channels.delete(id) + } + client.guildChannelMap.delete(message.d.id) + + + } else if (message.t === "CHANNEL_CREATE" || message.t === "CHANNEL_DELETE") { + if (message.t === "CHANNEL_CREATE") { + client.channels.set(message.d.id, message.d) + if (message.d["guild_id"]) { // obj[prop] notation can be used to access a property without typescript complaining that it doesn't exist on all values something can have + const channels = client.guildChannelMap.get(message.d["guild_id"]) + if (channels && !channels.includes(message.d.id)) channels.push(message.d.id) + } + } else { + client.channels.delete(message.d.id) + if (message.d["guild_id"]) { + const channels = client.guildChannelMap.get(message.d["guild_id"]) + if (channels) { + const previous = channels.indexOf(message.d.id) + if (previous !== -1) channels.splice(previous, 1) + } + } + } + + + } else if (message.t === "MESSAGE_CREATE") DiscordEvents.onMessageCreate(client, message.d) + } +} + +module.exports = utils diff --git a/package.json b/package.json index d635e14c..0d6caedb 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,8 @@ "author": "Cadence, PapiOphidian", "license": "MIT", "dependencies": { - "cloudstorm": "^0.6.1", - "snowtransfer": "^0.6.1" + "cloudstorm": "^0.7.0", + "heatsync": "^2.4.0", + "snowtransfer": "^0.7.0" } } diff --git a/passthrough.js b/passthrough.js new file mode 100644 index 00000000..77b9c8fb --- /dev/null +++ b/passthrough.js @@ -0,0 +1,10 @@ +/** + * @typedef {Object} Passthrough + * @property {import("repl").REPLServer} repl + * @property {typeof import("./config")} config + * @property {import("./modules/DiscordClient")} discord + * @property {import("heatsync")} sync + */ +/** @type {Passthrough} */ +const pt = {} +module.exports = pt diff --git a/stdin.js b/stdin.js new file mode 100644 index 00000000..95c7a738 --- /dev/null +++ b/stdin.js @@ -0,0 +1,47 @@ +const repl = require("repl") +const util = require("util") + +const passthrough = require("./passthrough") +const { discord, config, sync } = passthrough + +const extraContext = {} + +setImmediate(() => { // assign after since old extraContext data will get removed + if (!passthrough.repl) { + const cli = repl.start({ prompt: "", eval: customEval, writer: s => s }) + Object.assign(cli.context, extraContext, passthrough) + passthrough.repl = cli + } else Object.assign(passthrough.repl.context, extraContext) + // @ts-expect-error Says exit isn't assignable to a string + sync.addTemporaryListener(passthrough.repl, "exit", () => process.exit()) +}) + +/** + * @param {string} input + * @param {import("vm").Context} _context + * @param {string} _filename + * @param {(err: Error | null, result: unknown) => unknown} callback + */ +async function customEval(input, _context, _filename, callback) { + let depth = 0 + if (input === "exit\n") return process.exit() + if (input.startsWith(":")) { + const depthOverwrite = input.split(" ")[0] + depth = +depthOverwrite.slice(1) + input = input.slice(depthOverwrite.length + 1) + } + let result + try { + result = await eval(input) + const output = util.inspect(result, false, depth, true) + return callback(null, output) + } catch (e) { + return callback(null, util.inspect(e, true, 100, true)) + } +} + +sync.events.once(__filename, () => { + for (const key in extraContext) { + delete passthrough.repl.context[key] + } +})