diff --git a/index.js b/index.js new file mode 100644 index 0000000..25618df --- /dev/null +++ b/index.js @@ -0,0 +1,40 @@ +const discord = require("discord.js"); +const proxyHandlers = require("./proxies/handlers"); +const Module = require("module"); +const StructuresDef = require("./proxies/structures.js"); + +const original = discord; +const Structures = StructuresDef; + +const proxy = new Proxy(discord, { + get(target, property, receiver) { + if(property in proxyHandlers) return proxyHandlers[property]; + if(property === "original") return original; + if(property === "Structures") return Structures; + if(property === "hook") return hook; + return Reflect.get(target, property, receiver); + } +}); + + +/** + * Hooks discord.js-structures into discord.js require, which should fix errors in 3rd party libraries trying to use Structures. + * Overwrites global require function + * @see [StackOverflow source](https://stackoverflow.com/a/24602188/8404532) + */ +function hook() { + var origRequire = Module.prototype.require; + var _require = function(context, path) { + return origRequire.call(context, path); + }; + + Module.prototype.require = function(path) { + if(path === "discord.js") { + return proxy; + } + + return _require(this, path); + }; +} + +module.exports = proxy; \ No newline at end of file diff --git a/index.mjs b/index.mjs deleted file mode 100644 index 65314cf..0000000 --- a/index.mjs +++ /dev/null @@ -1,31 +0,0 @@ -import discord from "discord.js"; -import { defaultProxy } from "./proxies/index.mjs"; -import Module from "module"; - -/** The proxied version, in case you like named params */ -export const proxy = new Proxy(discord, defaultProxy); - -/** The original discord.js, unmodified. Useful when using discord.js-structures hooked */ -export const original = discord; - -/** - * Hooks discord.js-structures into discord.js require, which should fix errors in 3rd party libraries trying to use Structures. - * Overwrites global require function - * @see [StackOverflow source](https://stackoverflow.com/a/24602188/8404532) - */ -export function hook() { - var origRequire = Module.prototype.require; - var _require = function(context, path) { - return origRequire.call(context, path); - }; - - Module.prototype.require = function(path) { - if(path === "discord.js") { - return proxy; - } - - return _require(this, path); - }; -} - -export default proxy; \ No newline at end of file diff --git a/package.json b/package.json index 6398a85..f774067 100644 --- a/package.json +++ b/package.json @@ -4,12 +4,15 @@ "description": "Adds discord.js structures back into discord.js", "main": "index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "test": "node test/index.js" }, "keywords": [], "author": "", "license": "ISC", "peerDependencies": { "discord.js": ">=13.0.0-dev.0" + }, + "devDependencies": { + "discord.js": ">=13.0.0-dev.0" } } diff --git a/proxies/cache.js b/proxies/cache.js new file mode 100644 index 0000000..19bc4bd --- /dev/null +++ b/proxies/cache.js @@ -0,0 +1,2 @@ + +module.exports = new WeakMap(); \ No newline at end of file diff --git a/proxies/client.js b/proxies/client.js new file mode 100644 index 0000000..3a70ca7 --- /dev/null +++ b/proxies/client.js @@ -0,0 +1,9 @@ + +/** + * @type {ProxyHandler} + */ +module.exports = { + get(target, property, receiver) { + return Reflect.get(target, property, receiver); + } +} \ No newline at end of file diff --git a/proxies/eventProxy.js b/proxies/eventProxy.js new file mode 100644 index 0000000..77080a4 --- /dev/null +++ b/proxies/eventProxy.js @@ -0,0 +1,81 @@ + +module.exports = class EventProxy { + proxies = new Map(); + once = new Map(); + on = new Map(); + targetEvents = new WeakMap(); + handlers = new Map(); + + setProxy(event, handler) { + this.proxies.set(event, handler); + } + + handleEvent(event, args) { + if(proxies.has(event)) { + args = proxies.get(event)(...args); + } + for(const handler of (proxy.on.get(event) || [])) { + handler(...args); + } + for(const handler of (proxy.once.get(event) || [])) { + handler(...args); + } + proxy.once.delete(event); + } + + get proxy() { + const proxy = this; + /** @type {ProxyHandler} */ + const handler = { + get(target, property) { + const events = proxy.targetEvents.get(target) || []; + switch(property) { + case "on": + return (event, handler) => { + const handlers = proxy.on.get(event) || []; + handlers.push(handler); + proxy.on.set(event, handlers); + if(!events.includes(event)) { + const handler = (...args) => proxy.handleEvent(event, args); + proxy.handlers.set(event, handler); + target.on(event, handler); + } + } + case "once": + return (event, handler) => { + const handlers = proxy.once.get(event) || []; + handlers.push(handler); + proxy.once.set(event, handlers); + if(!events.includes(event)) { + const handler = (...args) => proxy.handleEvent(event, args); + proxy.handlers.set(event, handler); + target.on(event, handler); + } + } + case "off": + return (event, handler) => { + const handlers = proxy.on.get(event) || []; + if(handlers.includes(handler)) handlers.splice(handlers.indexOf(handler), 1); + if(handlers.length) { + proxy.on.set(event, handlers); + } else { + proxy.on.delete(event); + } + const handlers2 = proxy.once.get(event) || []; + if(handlers2.includes(handler)) handlers.splice(handlers.indexOf(handler), 1); + if(handlers2.length) { + proxy.once.set(event, handlers2); + } else { + proxy.once.delete(event); + } + if(!handlers.length && !handlers2.length) { + target.off(event, proxy.handlers.get(event)); + } + } + } + return Reflect.get(...arguments); + } + } + return handler; + } +} \ No newline at end of file diff --git a/proxies/handlers.js b/proxies/handlers.js new file mode 100644 index 0000000..e8a964f --- /dev/null +++ b/proxies/handlers.js @@ -0,0 +1,13 @@ +const Structures = require("./structures.js"); +const discord = require("discord.js"); +const EventProxy = require("./eventProxy.js"); + +module.exports = { + Client: class Client { + constructor(...params) { + var proxy = new EventProxy(); + return new Proxy(new Proxy(new discord.Client(...params), require("./client.js")), proxy.proxy); + } + }, + Structures: Structures +}; \ No newline at end of file diff --git a/proxies/index.mjs b/proxies/index.mjs deleted file mode 100644 index 76fb3a6..0000000 --- a/proxies/index.mjs +++ /dev/null @@ -1,12 +0,0 @@ - -const proxyHandlers = {}; - -/** - * @type {ProxyHandler} - */ -export const defaultProxy = { - get(target, property, receiver) { - if(property in proxyHandlers) return proxyHandlers[property]; - return Reflect.get(target, property, receiver); - } -} \ No newline at end of file diff --git a/proxies/structures.js b/proxies/structures.js new file mode 100644 index 0000000..4102983 --- /dev/null +++ b/proxies/structures.js @@ -0,0 +1,112 @@ +const discord = require("discord.js"); + +/** + * An extendable structure: + * * **`GuildEmoji`** + * * **`DMChannel`** + * * **`TextChannel`** + * * **`VoiceChannel`** + * * **`CategoryChannel`** + * * **`NewsChannel`** + * * **`StoreChannel`** + * * **`GuildMember`** + * * **`Guild`** + * * **`Message`** + * * **`MessageReaction`** + * * **`Presence`** + * * **`ClientPresence`** + * * **`VoiceState`** + * * **`Role`** + * * **`User`** + * @typedef {string} ExtendableStructure + */ + +/** + * Allows for the extension of built-in Discord.js structures that are instantiated by {@link BaseManager Managers}. + */ +class Structures { + constructor() { + throw new Error(`The ${this.constructor.name} class may not be instantiated.`); + } + + /** + * Retrieves a structure class. + * @param {string} structure Name of the structure to retrieve + * @returns {Function} + */ + static get(structure) { + if (typeof structure === 'string') return structures[structure]; + throw new TypeError(`"structure" argument must be a string (received ${typeof structure})`); + } + + /** + * Extends a structure. + * Make sure to extend all structures before instantiating your client. + * Extending after doing so may not work as expected. + * @param {ExtendableStructure} structure Name of the structure class to extend + * @param {Function} extender Function that takes the base class to extend as its only parameter and returns the + * extended class/prototype + * @returns {Function} Extended class/prototype returned from the extender + * @example + * const { Structures } = require('discord.js'); + * + * Structures.extend('Guild', Guild => { + * class CoolGuild extends Guild { + * constructor(client, data) { + * super(client, data); + * this.cool = true; + * } + * } + * + * return CoolGuild; + * }); + */ + static extend(structure, extender) { + if (!structures[structure]) throw new RangeError(`"${structure}" is not a valid extensible structure.`); + if (typeof extender !== 'function') { + const received = `(received ${typeof extender})`; + throw new TypeError( + `"extender" argument must be a function that returns the extended structure class/prototype ${received}.`, + ); + } + + const extended = extender(structures[structure]); + if (typeof extended !== 'function') { + const received = `(received ${typeof extended})`; + throw new TypeError(`The extender function must return the extended structure class/prototype ${received}.`); + } + + if (!(extended.prototype instanceof structures[structure])) { + const prototype = Object.getPrototypeOf(extended); + const received = `${extended.name || 'unnamed'}${prototype.name ? ` extends ${prototype.name}` : ''}`; + throw new Error( + 'The class/prototype returned from the extender function must extend the existing structure class/prototype' + + ` (received function ${received}; expected extension of ${structures[structure].name}).`, + ); + } + + structures[structure] = extended; + return extended; + } +} + +const structures = { + GuildEmoji: discord.GuildEmoji, + DMChannel: discord.DMChannel, + TextChannel: discord.TextChannel, + VoiceChannel: discord.VoiceChannel, + CategoryChannel: discord.CategoryChannel, + NewsChannel: discord.NewsChannel, + StoreChannel: discord.StoreChannel, + GuildMember: discord.GuildMember, + Guild: discord.Guild, + Message: discord.Message, + MessageReaction: discord.MessageReaction, + Presence: discord.Presence, + ClientPresence: discord.ClientPresence, + VoiceState: discord.VoiceState, + Role: discord.Role, + User: discord.User, +}; + +module.exports = Structures; \ No newline at end of file diff --git a/proxies/structures.mjs b/proxies/structures.mjs deleted file mode 100644 index e69de29..0000000 diff --git a/test/index.js b/test/index.js new file mode 100644 index 0000000..7a7cbf4 --- /dev/null +++ b/test/index.js @@ -0,0 +1,25 @@ +const discord = require("../index"); +const config = require("./config.json"); + +discord.Structures.extend('Message', Message => { + class BetterMessage extends Message { + constructor(client, data) { + super(client, data); + this.cool = true; + } + } + + return BetterMessage; +}); + +const client = new discord.Client({ + intents: ["GUILDS", "GUILD_MESSAGES"] +}); +client.login(config.token); + +client.on("ready", () => { + console.log("Ready as", client.user.tag); +}) +client.on("message", (msg) => { + console.log(msg); +}); \ No newline at end of file diff --git a/test/index.mjs b/test/index.mjs deleted file mode 100644 index 3ba0023..0000000 --- a/test/index.mjs +++ /dev/null @@ -1,20 +0,0 @@ -import discord from "../index.mjs"; -import config from "./config.json"; - -discord.Structures.extend('Message', Message => { - class BetterMessage extends Message { - constructor(client, data) { - super(client, data); - this.cool = true; - } - } - - return BetterMessage; -}); - -const client = new discord.Client(); -client.login(config.token); - -client.on("message", (msg) => { - console.log(msg); -}); \ No newline at end of file