diff --git a/bot.js b/bot.js index 9460aab..c8c4e8a 100644 --- a/bot.js +++ b/bot.js @@ -74,17 +74,20 @@ const botProcess = () => { Command.init(); + const help = require("./command/help"); + bot.ws.on('INTERACTION_CREATE', help.interactionHandler); + bot.on('ready', () => { - fs.readdirSync('./event/bot/') + fs.readdirSync('./event/') .filter(filename => filename.endsWith('.js')) .map(filename => filename.substr(0, filename.length - 3)) .forEach(filename => { const event = filename.replace(/([_-][a-z])/gu, character => `${character.substr(1).toUpperCase()}`); if (filename !== 'ready') { - bot.on(event, require(`./event/bot/${filename}`)); + bot.on(event, require(`./event/${filename}`)); } else { - require(`./event/bot/${filename}`)(); + require(`./event/${filename}`)(); } }); }); diff --git a/model/command/avatar.js b/command/avatar.js similarity index 83% rename from model/command/avatar.js rename to command/avatar.js index 52fb064..63a8be6 100644 --- a/model/command/avatar.js +++ b/command/avatar.js @@ -1,18 +1,12 @@ const Logger = require('@lilywonhalf/pretty-logger'); const Discord = require('discord.js'); -const CommandCategory = require('../command-category'); -const CommandPermission = require('../command-permission'); -const Guild = require('../guild'); +const CommandCategory = require('../model/command-category'); +const CommandPermission = require('../model/command-permission'); +const Guild = require('../model/guild'); class Avatar { - static instance = null; - constructor() { - if (Avatar.instance !== null) { - return Avatar.instance; - } - this.aliases = ['av']; this.category = CommandCategory.FUN; this.isAllowedForContext = CommandPermission.yes; diff --git a/model/command/changelog.js b/command/changelog.js similarity index 87% rename from model/command/changelog.js rename to command/changelog.js index 825db46..93c61a3 100644 --- a/model/command/changelog.js +++ b/command/changelog.js @@ -1,20 +1,14 @@ const Logger = require('@lilywonhalf/pretty-logger'); -const Config = require('../../config.json'); -const CommandCategory = require('../command-category'); -const CommandPermission = require('../command-permission'); -const { search } = require('../jira'); +const Config = require('../config.json'); +const CommandCategory = require('../model/command-category'); +const CommandPermission = require('../model/command-permission'); +const { search } = require('../model/jira'); const MAX_CHARACTERS = 1950; class Changelog { - static instance = null; - constructor() { - if (Changelog.instance !== null) { - return Changelog.instance; - } - this.aliases = ['change-log', 'cl']; this.category = CommandCategory.MODERATION; this.isAllowedForContext = CommandPermission.isMemberMod; diff --git a/model/command/clean.js b/command/clean.js similarity index 73% rename from model/command/clean.js rename to command/clean.js index 6e17c69..d9bcb70 100644 --- a/model/command/clean.js +++ b/command/clean.js @@ -1,17 +1,11 @@ const Logger = require('@lilywonhalf/pretty-logger'); -const Config = require('../../config.json'); -const CommandCategory = require('../command-category'); -const CommandPermission = require('../command-permission'); +const Config = require('../config.json'); +const CommandCategory = require('../model/command-category'); +const CommandPermission = require('../model/command-permission'); class Clean { - static instance = null; - constructor() { - if (Clean.instance !== null) { - return Clean.instance; - } - this.aliases = ['clear', 'purge']; this.category = CommandCategory.MODERATION; this.isAllowedForContext = CommandPermission.isMemberMod; diff --git a/model/command/embed.js b/command/embed.js similarity index 97% rename from model/command/embed.js rename to command/embed.js index bd463c9..f70b07c 100644 --- a/model/command/embed.js +++ b/command/embed.js @@ -1,6 +1,6 @@ const { MessageEmbed, Message } = require('discord.js'); -const CommandCategory = require('../command-category'); -const CommandPermission = require('../command-permission'); +const CommandCategory = require('../model/command-category'); +const CommandPermission = require('../model/command-permission'); const RGB_REGEX = /(\d{1,3})\D*,\D*(\d{1,3})\D*,\D*(\d{1,3})\D*/u; @@ -140,7 +140,7 @@ class Embed } this.aliases = []; - this.category = CommandCategory.RESOURCE; + this.category = CommandCategory.MODERATION; this.isAllowedForContext = CommandPermission.isMemberModOrHelper; this.description = 'Allows to post an embed'; } diff --git a/model/command/eval.js b/command/eval.js similarity index 80% rename from model/command/eval.js rename to command/eval.js index d99cf72..d6d71b6 100644 --- a/model/command/eval.js +++ b/command/eval.js @@ -1,23 +1,17 @@ const Discord = require('discord.js'); const Logger = require('@lilywonhalf/pretty-logger'); -const Config = require('../../config.json'); -const CommandCategory = require('../command-category'); -const CommandPermission = require('../command-permission'); +const Config = require('../config.json'); +const CommandCategory = require('../model/command-category'); +const CommandPermission = require('../model/command-permission'); // Including every model here so that it's ready to be used by the command -const Guild = require('../guild'); +const Guild = require('../model/guild'); const JAVASCRIPT_LOGO_URL = 'https://i.discord.fr/IEV8.png'; class Eval { - static instance = null; - constructor() { - if (Eval.instance !== null) { - return Eval.instance; - } - this.aliases = []; this.category = CommandCategory.BOT_MANAGEMENT; this.isAllowedForContext = CommandPermission.isMommy; diff --git a/command/help.js b/command/help.js new file mode 100644 index 0000000..7e266fe --- /dev/null +++ b/command/help.js @@ -0,0 +1,139 @@ +const CommandPermission = require('../model/command-permission'); +const CommandCategory = require('../model/command-category'); + +const commands = require('../model/command').commandList; +const messages = require('../model/messages'); + +const cachelessRequire = (path) => { + if (!typeof path === 'string') { + delete require.cache[require.resolve(path)]; + } + + return typeof path === 'string' ? require(path) : null; +}; + +const cleanString = (str) => str === null || str === undefined ? null : str + .replace("_", " ") + .split(" ") + .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) + .join(" "); + +const mainPage = (id) => { + const buttons = Object.keys(CommandCategory).map(c => ({ + type: 2, + style: 1, + label: cleanString(c), + custom_id: `help-${id}-${c}`, + })) + + return { + embeds: [{ + title: "Help command", + color: 0xA95B44, + description: + 'Spot stands for **S**imply **P**lural b**ot**. This bot is mainly used to generate changelogs and help ' + + 'with the definition of features, so recurring questions can be answered more quickly.\n' + + '\n' + + 'Commands are classified by categories because displaying every command in this small box would be ' + + 'confusing and would break Discord\'s character limit. Click on a buttom below to show the commands ' + + 'available in the corresponding category, or type `.help `.\n' + }], + components: [{ + "type": 1, + "components": buttons, + }] + } + +} + +const categoryPage = (cat, id, direct = false) => { + const c = Object.values(CommandCategory).map(x => cleanString(x).toLowerCase()); + if (!c.includes(cat.toLowerCase())) + return; + + let data = { + embeds: [{ + title: `Help command | ${cleanString(cat)}`, + color: 0xA95B44, + }], + components: [{ + "type": 1, + "components": [{ + type: 2, + style: 2, + label: "Back", + custom_id: `help-${id}-home`, + }], + }] + }; + + if (direct) delete data.components; + + if (cat.toLowerCase() == cleanString(CommandCategory.RESOURCE).toLowerCase()) + data.embeds[0].fields = messages.getList(); + else + data.embeds[0].fields = Array.from(commands.keys()).map(x => { + const cmd = cachelessRequire(commands.get(x)); + if (cleanString(cmd.category)?.toLowerCase() != cat.toLowerCase()) return; + return { name: x, value: cmd.description ?? "No description."}; + }).filter(x => x); + + return data; +} + +class Help +{ + constructor() { + this.aliases = ['commands']; + this.category = CommandCategory.INFO; + this.isAllowedForContext = CommandPermission.yes; + this.description = 'Provides the list of commands'; + } + + /** + * @param {Message} message + * @param {Array} args + * @param {Command} Command + */ + async process(message, args, command) { + if (!args[0]) + await bot.api.channels(message.channel.id).messages.post({ data: mainPage(message.author.id) }); + else + await bot.api.channels(message.channel.id).messages.post({ + data: + categoryPage(args.join(" "), message.author.id, true) + ?? { + content: "Category not found.", + message_reference: { message_id: message.id, guild_id: message.guild?.id }, + allowed_mentions: { parse: [] }, + } + }) + } +} + +module.exports = new Help(); + +module.exports.interactionHandler = async (event) => { + const user = event.member.user.id; + const custom_id = event.data.custom_id; + + let ret; + + if (!custom_id.startsWith(`help-${user}`)) + ret = { + type: 4, + data: { + flags: 64, + content: "This help command was sent by someone else. Please run `.help` again.", + }, + } + else if (custom_id.endsWith('home')) + ret = { type: 7, data: mainPage(user) }; + else { + let page = categoryPage(cleanString(custom_id.split(`help-${user}-`).join("")), user); + if (page) ret = { type: 7, data: page }; + else ret = { type: 4, data: { flags: 64, content: "Category not found." }} + } + + return await bot.api.interactions(event.id, event.token).callback.post({ data: ret }) +} diff --git a/model/command/kill.js b/command/kill.js similarity index 63% rename from model/command/kill.js rename to command/kill.js index 45d9662..e3f23b4 100644 --- a/model/command/kill.js +++ b/command/kill.js @@ -1,17 +1,11 @@ const Logger = require('@lilywonhalf/pretty-logger'); -const Config = require('../../config.json'); -const CommandCategory = require('../command-category'); -const CommandPermission = require('../command-permission'); +const Config = require('../config.json'); +const CommandCategory = require('../model/command-category'); +const CommandPermission = require('../model/command-permission'); class Kill { - static instance = null; - constructor() { - if (Kill.instance !== null) { - return Kill.instance; - } - this.aliases = []; this.category = CommandCategory.BOT_MANAGEMENT; this.isAllowedForContext = CommandPermission.isMemberMod; diff --git a/model/command/reload.js b/command/reload.js similarity index 68% rename from model/command/reload.js rename to command/reload.js index 93bc5d1..897f1fe 100644 --- a/model/command/reload.js +++ b/command/reload.js @@ -1,16 +1,10 @@ const Logger = require('@lilywonhalf/pretty-logger'); -const CommandCategory = require('../command-category'); -const CommandPermission = require('../command-permission'); +const CommandCategory = require('../model/command-category'); +const CommandPermission = require('../model/command-permission'); class Reload { - static instance = null; - constructor() { - if (Reload.instance !== null) { - return Reload.instance; - } - this.aliases = ['reboot']; this.category = CommandCategory.BOT_MANAGEMENT; this.isAllowedForContext = CommandPermission.isMemberMod; diff --git a/model/command/set-avatar.js b/command/set-avatar.js similarity index 76% rename from model/command/set-avatar.js rename to command/set-avatar.js index 3d4959b..bd91a8e 100644 --- a/model/command/set-avatar.js +++ b/command/set-avatar.js @@ -1,16 +1,10 @@ const Logger = require('@lilywonhalf/pretty-logger'); -const CommandCategory = require('../command-category'); -const CommandPermission = require('../command-permission'); +const CommandCategory = require('../model/command-category'); +const CommandPermission = require('../model/command-permission'); class SetAvatar { - static instance = null; - constructor() { - if (SetAvatar.instance !== null) { - return SetAvatar.instance; - } - this.aliases = ['setavatar']; this.category = CommandCategory.BOT_MANAGEMENT; this.isAllowedForContext = CommandPermission.isMemberMod; diff --git a/event/bot/guild-member-add.js b/event/guild-member-add.js similarity index 80% rename from event/bot/guild-member-add.js rename to event/guild-member-add.js index 4401200..0b7011c 100644 --- a/event/bot/guild-member-add.js +++ b/event/guild-member-add.js @@ -1,4 +1,4 @@ -const Guild = require('../../model/guild'); +const Guild = require('../model/guild'); module.exports = async (member) => { if ((member.guild === null || isRightGuild(member.guild.id)) && !member.user.bot) { diff --git a/event/bot/message.js b/event/message.js similarity index 83% rename from event/bot/message.js rename to event/message.js index 698f67d..3fa5deb 100644 --- a/event/bot/message.js +++ b/event/message.js @@ -1,4 +1,4 @@ -const Command = require('../../model/command'); +const Command = require('../model/command'); /** * @param {Message} message diff --git a/event/bot/ready.js b/event/ready.js similarity index 84% rename from event/bot/ready.js rename to event/ready.js index 9982b60..d955764 100644 --- a/event/bot/ready.js +++ b/event/ready.js @@ -1,6 +1,6 @@ const Logger = require('@lilywonhalf/pretty-logger'); -const Config = require('../../config.json'); -const Guild = require('../../model/guild'); +const Config = require('../config.json'); +const Guild = require('../model/guild'); module.exports = async () => { Logger.info('Logged in as ' + bot.user.username + '#' + bot.user.discriminator); diff --git a/messages.js b/messages.js new file mode 100644 index 0000000..9e002d4 --- /dev/null +++ b/messages.js @@ -0,0 +1,216 @@ +// interface Message { +// name string[]; +// description string; +// title string; +// text string; +// } + +module.exports = [ + { + names: [ + "add-friend", + "add-friends", + "addfriend", + "af", + ], + description: 'Explains how to troubleshoot not being able to add a friend.', + title: "Add friend", + "text": + 'If you and your friends are unable to add each other as friends, or can\'t find each other as friends, ' + + 'make sure that you are using a case sensitive username when adding each other. Additionally, it is ' + + 'currently not possible to add people by their user ID, so if you are using a user id to add them as ' + + 'friend this currently does not work but will in the future.' + }, + { + names: [ + "add-set-front", + 'addsetfront', + 'addset-front', + 'add-setfront', + 'add-set', + 'addset', + 'add-front', + 'addfront', + 'set-front', + 'setfront', + 'asf', + ], + description: 'Talks about how to set who is fronting in the application.', + title: "Add / set front", + text: + 'There are two ways in-app to show a member as fronting. Set as front, and add to front.\n' + + '\n' + + 'Using set as front will clear all members from your front list, and then add that member. Add to front ' + + 'won\'t remove anyone from the list, but it will add the member whose add icon you tapped. You can ' + + 'individually remove members by tapping the downward facing arrow next to their name.' + }, + { + names: [ + "custom-fields", + "customfields", + "customfield", + "cfi", + ], + description: 'Talks about the "custom fields" feature.', + title: "Custom fields", + text: + 'The ability to change the fields shown on your member lists is planned. This feature will also bring ' + + 'the ability to remove and add fields, as well as make fields private, public, or show them to only ' + + 'trusted friends.' + }, + { + names: [ + "custom-front", + "custom-fronts", + "customfront", + "customfronts", + "cf", + ], + description: 'Explains what "custom front" is.', + title: "Custom fronts", + text: + 'Custom fronts is a kind of status for fronts, like "blurred", "unknown member", "dissociated", etc... \n' + + '\n' + + 'You don\'t want those to show up as real members in your system list but you still want to be able to ' + + 'set front as one of them — that\'s where custom fronts kick in.\n' + + '\n' + + 'They\'re highly customizable (as per popular request) so you can name them anything you want.' + }, + { + names: [ + "front-history-importing", + 'fronthistoryimporting', + 'fronthistory-importing', + 'front-historyimporting', + 'fronthistoryimport', + 'fronthistory-import', + 'front-historyimport', + 'front-history', + 'fronthistory', + 'front-importing', + 'frontimporting', + 'front-import', + 'frontimport', + 'fhi', + ], + description: 'Talks about importing the front history from PluralKit.', + title: "Front history importing", + text: + 'Importing your front history to and from PluralKit is planned in the future, but is currently ' + + 'impossible to give any front entries to or take entries from PluralKit until APIv2 for PluralKit is ' + + 'finished. This feature also takes a lower priority than features unique to Simply Plural.' + }, + { + names: [ + "messaging", + ], + description: 'Talks about the possibility of having a messaging feature.', + title: "Messaging", + text: + 'The feature of messaging other systems within the app is out of scope, the app is not meant to be a ' + + 'social community app but a tool for you and your friends. Adding messages between systems would need ' + + 'us to implement moderation tools, moderation team and the actual feature, which is not the direction ' + + 'we are taking the app in right now.\n' + + '\n' + + 'Messaging within the system, between headmates, is planned for the future so you can communicate more ' + + 'easily within the system.' + }, + { + names: [ + "notifications", + "notification", + ], + description: 'Explains how your friends can get notifications from your system.', + title: "Notifications", + text: + 'If your friends are not getting notifications, make sure that you go into the settings of the friend ' + + 'by going on their profile and clicking the cog wheel on the right top. Press "They can get ' + + 'notifications". As a second step your friend(s) have to opt-in to get notifications from you, they ' + + 'have to go to your profile in their friends and click "Get notifications if they change front".' + }, + { + names: [ + "see-members", + "seemembers", + "seembmer", + "sm", + ], + description: 'Explains how to allow your friends to see your members.', + title: "See members", + text: + 'If your friends cannot see your members, you have to go into the friend their profile, click on the ' + + 'cogwheel on the right top and press "They can see your shared members", this will allow them to see ' + + 'your public members' + }, + { + names: [ + 'sync-members-pluralkit', + 'sync-member-pluralkit', + 'syncmemberpluralkit', + 'syncmember-pluralkit', + 'sync-memberpluralkit', + 'syncmemberspluralkit', + 'syncmembers-pluralkit', + 'sync-memberspluralkit', + 'sync-member-plural-kit', + 'syncmemberplural-kit', + 'syncmember-plural-kit', + 'sync-memberplural-kit', + 'syncmembersplural-kit', + 'syncmembers-plural-kit', + 'sync-membersplural-kit', + 'sync-member', + 'syncmember', + 'sync-members', + 'syncmembers', + 'sync-pluralkit', + 'syncpluralkit', + 'sync-plural-kit', + 'syncplural-kit', + 'smp', + 'smpk', + ], + description: 'Talks about syncing your members to PluralKit.', + title: "Sync members to PluralKit", + text: + 'If you wish to sync your members to PluralKit, go into the settings page -> Integrations -> PluralKit ' + + 'and fill in your PluralKit token, you can get this token by typing pk;token anywhere and PluralKit ' + + 'will message you the token in a DM. \n' + + '\n' + + 'Once filled out, you can go to actions in the members page and press Sync (rebooting app may be ' + + 'required to see this option after adding the token). You will be prompted with the option to sync to ' + + 'and from pk. \n' + + '\n' + + 'Pay attention that they are linked by the plural kit id found in the individual member settings in ' + + 'Simply Plural. If you make a member on Simply Plural and you make the same member on PluralKit you ' + + 'will have to go into the individual member settings of Simply Plural and fill in the PluralKit user id ' + + 'in the settings. If you don\'t do this you will end up with duplicate members on Plural Kit.' + }, + { + names: [ + "system-relationsips", + "system-relationship", + "systemrelationships", + "sr", + ], + description: 'Explains what "system relationships" are.', + title: "System relationships", + text: + 'This is an open-ended field meant to describe what relationships a headmate has. It can be used to ' + + 'describe the member\'s relationship to the system as a whole, their inner system relationships such as ' + + 'being family, friends, or romantic partners, or it can describe their outer-system relationships with ' + + 'people in the outer world, such as family, friends, and romantic partners.' + }, + { + names: [ + "website", + ], + title: "Website", + description: 'Talks about the possibility of having a web portal for Simply Plural.', + text: + 'A web portal for Simply Plural is unlikely to be made at the moment. The framework that Simply Plural ' + + 'has been created with has the capability of web development, but it is currently experimental, which ' + + 'would make it buggy and unstable to use. This decision may change later in the year when the framework ' + + 'becomes more stable and once the app is more complete.' + } +] \ No newline at end of file diff --git a/model/command-category.js b/model/command-category.js index f7091f5..6e2f204 100644 --- a/model/command-category.js +++ b/model/command-category.js @@ -1,10 +1,7 @@ const CommandCategory = { MODERATION: 'moderation', - ADMINISTRATION: 'administration', BOT_MANAGEMENT: 'bot_management', FUN: 'fun', - INFO: 'info', - ROLE: 'role', RESOURCE: 'resource' }; diff --git a/model/command.js b/model/command.js index c61c311..f303dff 100644 --- a/model/command.js +++ b/model/command.js @@ -3,6 +3,8 @@ const Discord = require('discord.js'); const Config = require('../config.json'); const Guild = require('./guild'); +const customMessages = require("./messages"); + const cachelessRequire = (path) => { if (typeof path === 'string') { delete require.cache[require.resolve(path)]; @@ -19,9 +21,9 @@ const Command = { Command.commandList = new Discord.Collection(); Command.commandAliases = {}; - fs.readdirSync('model/command/').forEach(file => { + fs.readdirSync('command/').forEach(file => { if (file.substr(file.lastIndexOf('.')).toLowerCase() === '.js') { - const commandPath = `./command/${file}`; + const commandPath = `../command/${file}`; const commandInstance = cachelessRequire(commandPath); if (commandInstance !== null) { @@ -44,37 +46,32 @@ const Command = { * @returns {boolean} */ parseMessage: async (message) => { - let isCommand = false; + if (!message.content.toLowerCase().startsWith(Config.prefix)) + return false; - if (message.content.toLowerCase().substr(0, Config.prefix.length) === Config.prefix) { - let content = message.content.substr(Config.prefix.length).trim().split(' '); - const calledCommand = content.shift().toLowerCase(); + let content = message.content.substr(Config.prefix.length).trim().split(' '); + const calledCommand = content.shift().toLowerCase(); - if (await Command.isValid(calledCommand, message)) { - const member = await Guild.getMemberFromMessage(message); + if (embed = customMessages.getEmbed(calledCommand)) + return await message.channel.send({embed}); - if (member === null) { - message.reply('sorry, you do not seem to be on the server.'); - } else { - let commandName = calledCommand; - isCommand = true; + if (!await Command.isValid(calledCommand, message)) return; + + const member = await Guild.getMemberFromMessage(message); + if (member === null) + return message.reply('sorry, you do not seem to be on the server.'); - if (Command.commandAliases.hasOwnProperty(calledCommand)) { - commandName = Command.commandAliases[calledCommand]; - } + let commandName = calledCommand; - const commandInstance = cachelessRequire(Command.commandList.get(commandName)); + if (Command.commandAliases.hasOwnProperty(calledCommand)) + commandName = Command.commandAliases[calledCommand]; - if (commandInstance !== null) { - commandInstance.process(message, content, Command); - } else { - Command.commandList.delete(commandName); - } - } - } - } + const commandInstance = cachelessRequire(Command.commandList.get(commandName)); - return isCommand; + if (commandInstance !== null) + commandInstance.process(message, content, Command); + else + Command.commandList.delete(commandName); }, /** diff --git a/model/command/add-friend.js b/model/command/add-friend.js deleted file mode 100644 index e59f190..0000000 --- a/model/command/add-friend.js +++ /dev/null @@ -1,40 +0,0 @@ -const { MessageEmbed } = require('discord.js'); -const CommandCategory = require('../command-category'); -const CommandPermission = require('../command-permission'); - -class AddFriend -{ - static instance = null; - - constructor() { - if (AddFriend.instance !== null) { - return AddFriend.instance; - } - - this.aliases = ['add-friends', 'addfriends', 'addfriend', 'af']; - this.category = CommandCategory.RESOURCE; - this.isAllowedForContext = CommandPermission.yes; - this.description = 'Explains how to troubleshoot not being able to add a friend.'; - } - - /** - * @param {Message} message - * @param {Array} args - */ - async process(message, args) { - const embed = new MessageEmbed(); - - embed.setColor(APP_MAIN_COLOUR); - embed.setAuthor('Add friend', bot.user.displayAvatarURL({ dynamic: true })); - embed.setDescription( - 'If you and your friends are unable to add each other as friends, or can\'t find each other as friends, ' + - 'make sure that you are using a case sensitive username when adding each other. Additionally, it is ' + - 'currently not possible to add people by their user ID, so if you are using a user id to add them as ' + - 'friend this currently does not work but will in the future.' - ); - - return message.channel.send(embed); - } -} - -module.exports = new AddFriend(); diff --git a/model/command/add-set-front.js b/model/command/add-set-front.js deleted file mode 100644 index 02a95d1..0000000 --- a/model/command/add-set-front.js +++ /dev/null @@ -1,52 +0,0 @@ -const { MessageEmbed } = require('discord.js'); -const CommandCategory = require('../command-category'); -const CommandPermission = require('../command-permission'); - -class AddSetFront -{ - static instance = null; - - constructor() { - if (AddSetFront.instance !== null) { - return AddSetFront.instance; - } - - this.aliases = [ - 'addsetfront', - 'addset-front', - 'add-setfront', - 'add-set', - 'addset', - 'add-front', - 'addfront', - 'set-front', - 'setfront', - 'asf', - ]; - this.category = CommandCategory.RESOURCE; - this.isAllowedForContext = CommandPermission.yes; - this.description = 'Talks about how to set who is fronting in the application.'; - } - - /** - * @param {Message} message - * @param {Array} args - */ - async process(message, args) { - const embed = new MessageEmbed(); - - embed.setColor(APP_MAIN_COLOUR); - embed.setAuthor('Add / set front', bot.user.displayAvatarURL({ dynamic: true })); - embed.setDescription( - 'There are two ways in-app to show a member as fronting. Set as front, and add to front.\n' + - '\n' + - 'Using set as front will clear all members from your front list, and then add that member. Add to front ' + - 'won\'t remove anyone from the list, but it will add the member whose add icon you tapped. You can ' + - 'individually remove members by tapping the downward facing arrow next to their name.' - ); - - return message.channel.send(embed); - } -} - -module.exports = new AddSetFront(); diff --git a/model/command/custom-fields.js b/model/command/custom-fields.js deleted file mode 100644 index 1f88830..0000000 --- a/model/command/custom-fields.js +++ /dev/null @@ -1,39 +0,0 @@ -const { MessageEmbed } = require('discord.js'); -const CommandCategory = require('../command-category'); -const CommandPermission = require('../command-permission'); - -class CustomFields -{ - static instance = null; - - constructor() { - if (CustomFields.instance !== null) { - return CustomFields.instance; - } - - this.aliases = ['custom-fields', 'customfields', 'customfield', 'cfi']; - this.category = CommandCategory.RESOURCE; - this.isAllowedForContext = CommandPermission.yes; - this.description = 'Talks about the "custom fields" feature.'; - } - - /** - * @param {Message} message - * @param {Array} args - */ - async process(message, args) { - const embed = new MessageEmbed(); - - embed.setColor(APP_MAIN_COLOUR); - embed.setAuthor('Custom fields', bot.user.displayAvatarURL({ dynamic: true })); - embed.setDescription( - 'The ability to change the fields shown on your member lists is planned. This feature will also bring ' + - 'the ability to remove and add fields, as well as make fields private, public, or show them to only ' + - 'trusted friends.' - ); - - return message.channel.send(embed); - } -} - -module.exports = new CustomFields(); diff --git a/model/command/custom-front.js b/model/command/custom-front.js deleted file mode 100644 index 6ccb019..0000000 --- a/model/command/custom-front.js +++ /dev/null @@ -1,42 +0,0 @@ -const { MessageEmbed } = require('discord.js'); -const CommandCategory = require('../command-category'); -const CommandPermission = require('../command-permission'); - -class CustomFront -{ - static instance = null; - - constructor() { - if (CustomFront.instance !== null) { - return CustomFront.instance; - } - - this.aliases = ['custom-fronts', 'customfronts', 'customfront', 'cf']; - this.category = CommandCategory.RESOURCE; - this.isAllowedForContext = CommandPermission.yes; - this.description = 'Explains what "custom front" is.'; - } - - /** - * @param {Message} message - * @param {Array} args - */ - async process(message, args) { - const embed = new MessageEmbed(); - - embed.setColor(APP_MAIN_COLOUR); - embed.setAuthor('Custom fronts', bot.user.displayAvatarURL({ dynamic: true })); - embed.setDescription( - 'Custom fronts is a kind of status for fronts, like "blurred", "unknown member", "dissociated", etc... \n' + - '\n' + - 'You don\'t want those to show up as real members in your system list but you still want to be able to ' + - 'set front as one of them — that\'s where custom fronts kick in.\n' + - '\n' + - 'They\'re highly customizable (as per popular request) so you can name them anything you want.' - ); - - return message.channel.send(embed); - } -} - -module.exports = new CustomFront(); diff --git a/model/command/front-history-importing.js b/model/command/front-history-importing.js deleted file mode 100644 index f7212c3..0000000 --- a/model/command/front-history-importing.js +++ /dev/null @@ -1,53 +0,0 @@ -const { MessageEmbed } = require('discord.js'); -const CommandCategory = require('../command-category'); -const CommandPermission = require('../command-permission'); - -class FrontHistoryImporting -{ - static instance = null; - - constructor() { - if (FrontHistoryImporting.instance !== null) { - return FrontHistoryImporting.instance; - } - - this.aliases = [ - 'fronthistoryimporting', - 'fronthistory-importing', - 'front-historyimporting', - 'fronthistoryimport', - 'fronthistory-import', - 'front-historyimport', - 'front-history', - 'fronthistory', - 'front-importing', - 'frontimporting', - 'front-import', - 'frontimport', - 'fhi', - ]; - this.category = CommandCategory.RESOURCE; - this.isAllowedForContext = CommandPermission.yes; - this.description = 'Talks about importing the front history from PluralKit.'; - } - - /** - * @param {Message} message - * @param {Array} args - */ - async process(message, args) { - const embed = new MessageEmbed(); - - embed.setColor(APP_MAIN_COLOUR); - embed.setAuthor('Front history importing', bot.user.displayAvatarURL({ dynamic: true })); - embed.setDescription( - 'Importing your front history to and from PluralKit is planned in the future, but is currently ' + - 'impossible to give any front entries to or take entries from PluralKit until APIv2 for PluralKit is ' + - 'finished. This feature also takes a lower priority than features unique to Simply Plural.' - ); - - return message.channel.send(embed); - } -} - -module.exports = new FrontHistoryImporting(); diff --git a/model/command/help.js b/model/command/help.js deleted file mode 100644 index d5e9722..0000000 --- a/model/command/help.js +++ /dev/null @@ -1,200 +0,0 @@ -const Discord = require('discord.js'); -const Logger = require('@lilywonhalf/pretty-logger'); -const Config = require('../../config.json'); -const EmojiCharacters = require('../../emoji-characters.json'); -const Guild = require('../guild'); -const CommandCategory = require('../command-category'); -const CommandPermission = require('../command-permission'); - -const cachelessRequire = (path) => { - if (typeof path === 'string') { - delete require.cache[require.resolve(path)]; - } - - return typeof path === 'string' ? require(path) : null; -}; - -class HelpDialog -{ - /** - * @param {Message} message - */ - constructor(message) { - this.categoriesMapping = new Discord.Collection(); - this.categoriesEmbed = new Discord.MessageEmbed(); - this.originalMessage = message; - this.categoryCommandMapping = new Discord.Collection(); - this.categoryColourMapping = new Discord.Collection(); - this.postedMessage = null; - this.usedEmojis = []; - this.stopAddingReactions = false; - - this.categoriesEmbed.setColor(APP_MAIN_COLOUR); - this.categoriesEmbed.setFooter('Click on one of the reactions below'); - - for (let category of Object.values(CommandCategory)) { - this.categoryColourMapping.set(category, APP_MAIN_COLOUR); - } - - this.categoryColourMapping.set(CommandCategory.RESOURCE, 'fdfd96'); - } - - /** - * @param {Command} Command - */ - async init(Command) { - const callableCommands = new Discord.Collection(); - const commandList = Command.commandList.keyArray(); - - for (let i = 0; i < commandList.length; i++) { - const commandName = commandList[i]; - const command = cachelessRequire(`../${Command.commandList.get(commandName)}`); - const isAllowed = await command.isAllowedForContext(this.originalMessage); - - if (isAllowed) { - callableCommands.set(commandName, command); - } - } - - callableCommands.forEach((command, commandName) => { - if (!this.categoryCommandMapping.has(command.category)) { - this.categoryCommandMapping.set(command.category, new Discord.Collection()); - } - - this.categoryCommandMapping.get(command.category).set(commandName, command); - }); - - let i = 1; - - const categories = callableCommands.reduce((accumulator, command) => { - if (!this.categoriesMapping.array().includes(command.category)) { - let commandCategory = command.category.replace('_', ' '); - commandCategory = `${commandCategory.slice(0, 1).toUpperCase()}${commandCategory.slice(1)}`; - - this.categoriesMapping.set(EmojiCharacters[i], command.category); - this.usedEmojis.push(EmojiCharacters[i]); - accumulator += `${EmojiCharacters[i]} ${commandCategory}\n`; - i++; - } - - return accumulator; - }, ''); - - this.categoriesEmbed.setTitle('Help command'); - this.categoriesEmbed.setDescription( - 'Spot stands for **S**imply **P**lural b**ot**. This bot is mainly used to generate changelogs and help ' + - 'with the definition of features, so recurring questions can be answered more quickly.\n' + - '\n' + - 'Commands are classified by categories because displaying every command in this small box would be ' + - 'confusing and would break Discord\'s character limit. Click on a reaction to show the commands ' + - 'available in the corresponding category.\n' + - '\n' + - categories - ); - - return this.listCategories(); - } - - /** - * @param {Collection} [collection] - */ - async listCategories(collection) { - this.stopAddingReactions = true; - - if (collection !== undefined && collection.size < 1) { - await this.postedMessage.reactions.removeAll().catch(error => Logger.warning(error.message)); - return; - } - - const member = await Guild.getMemberFromMessage(this.originalMessage); - const filter = (reaction, user) => { - const emoji = reaction.emoji.name; - - return this.usedEmojis.includes(emoji) && user.id === member.user.id; - }; - - if (this.postedMessage === null) { - this.postedMessage = await this.originalMessage.channel.send(this.categoriesEmbed).catch(error => Logger.warning(error.toString())); - } else { - await this.postedMessage.reactions.removeAll().catch(error => Logger.warning(error.message)); - await this.postedMessage.edit('', this.categoriesEmbed); - } - - // 5 minutes - this.postedMessage.awaitReactions(filter, { time: 300000, max: 1 }).then(this.listCommands.bind(this)).catch(Logger.exception); - this.stopAddingReactions = false; - - for (let i = 0; i < this.usedEmojis.length && !this.stopAddingReactions; i++) { - await this.postedMessage.react(this.usedEmojis[i]).catch(error => Logger.warning(error.message)); - } - } - - /** - * @param {Collection} collection - */ - async listCommands(collection) { - this.stopAddingReactions = true; - await this.postedMessage.reactions.removeAll().catch(error => Logger.warning(error.message)); - - if (collection.size < 1) { - this.postedMessage.edit('Timed out! You may send the command again.'); - return; - } - - const member = await Guild.getMemberFromMessage(this.originalMessage); - const filter = (reaction, user) => { - const emoji = reaction.emoji.name; - - return emoji === '↩️' && user.id === member.user.id; - }; - - const category = this.categoriesMapping.get(collection.first().emoji.name); - const commandsEmbed = new Discord.MessageEmbed(); - const commands = this.categoryCommandMapping.get(category).reduce((accumulator, command, commandName) => { - accumulator += `**${Config.prefix}${commandName}** ${command.description}\n\n`; - - return accumulator; - }, ''); - - commandsEmbed.setTitle('Help command'); - commandsEmbed.setDescription(commands); - commandsEmbed.setColor(this.categoryColourMapping.get(category)); - commandsEmbed.setFooter('Click on one of the reactions below'); - - this.postedMessage.edit('', commandsEmbed); - - // 5 minutes - this.postedMessage.awaitReactions(filter, { time: 300000, max: 1 }).then(this.listCategories.bind(this)).catch(Logger.exception); - - await this.postedMessage.react('↩️').catch(error => Logger.warning(error.message)); - } -} - -class Help -{ - static instance = null; - - constructor() { - if (Help.instance !== null) { - return Help.instance; - } - - this.aliases = ['commands']; - this.category = CommandCategory.INFO; - this.isAllowedForContext = CommandPermission.yes; - this.description = 'Provides the list of commands'; - } - - /** - * @param {Message} message - * @param {Array} args - * @param {Command} Command - */ - async process(message, args, Command) { - const dialog = new HelpDialog(message); - - return dialog.init(Command); - } -} - -module.exports = new Help(); diff --git a/model/command/messaging.js b/model/command/messaging.js deleted file mode 100644 index f665b5a..0000000 --- a/model/command/messaging.js +++ /dev/null @@ -1,43 +0,0 @@ -const { MessageEmbed } = require('discord.js'); -const CommandCategory = require('../command-category'); -const CommandPermission = require('../command-permission'); - -class Messaging -{ - static instance = null; - - constructor() { - if (Messaging.instance !== null) { - return Messaging.instance; - } - - this.aliases = []; - this.category = CommandCategory.RESOURCE; - this.isAllowedForContext = CommandPermission.yes; - this.description = 'Talks about the possibility of having a messaging feature.'; - } - - /** - * @param {Message} message - * @param {Array} args - */ - async process(message, args) { - const embed = new MessageEmbed(); - - embed.setColor(APP_MAIN_COLOUR); - embed.setAuthor('Messaging', bot.user.displayAvatarURL({ dynamic: true })); - embed.setDescription( - 'The feature of messaging other systems within the app is out of scope, the app is not meant to be a ' + - 'social community app but a tool for you and your friends. Adding messages between systems would need ' + - 'us to implement moderation tools, moderation team and the actual feature, which is not the direction ' + - 'we are taking the app in right now.\n' + - '\n' + - 'Messaging within the system, between headmates, is planned for the future so you can communicate more ' + - 'easily within the system.' - ); - - return message.channel.send(embed); - } -} - -module.exports = new Messaging(); diff --git a/model/command/notifications.js b/model/command/notifications.js deleted file mode 100644 index d5e91e3..0000000 --- a/model/command/notifications.js +++ /dev/null @@ -1,40 +0,0 @@ -const { MessageEmbed } = require('discord.js'); -const CommandCategory = require('../command-category'); -const CommandPermission = require('../command-permission'); - -class Notifications -{ - static instance = null; - - constructor() { - if (Notifications.instance !== null) { - return Notifications.instance; - } - - this.aliases = ['notification']; - this.category = CommandCategory.RESOURCE; - this.isAllowedForContext = CommandPermission.yes; - this.description = 'Explains how your friends can get notifications from your system.'; - } - - /** - * @param {Message} message - * @param {Array} args - */ - async process(message, args) { - const embed = new MessageEmbed(); - - embed.setColor(APP_MAIN_COLOUR); - embed.setAuthor('Notifications', bot.user.displayAvatarURL({ dynamic: true })); - embed.setDescription( - 'If your friends are not getting notifications, make sure that you go into the settings of the friend ' + - 'by going on their profile and clicking the cog wheel on the right top. Press "They can get ' + - 'notifications". As a second step your friend(s) have to opt-in to get notifications from you, they ' + - 'have to go to your profile in their friends and click "Get notifications if they change front".' - ); - - return message.channel.send(embed); - } -} - -module.exports = new Notifications(); diff --git a/model/command/see-members.js b/model/command/see-members.js deleted file mode 100644 index 8a9d5af..0000000 --- a/model/command/see-members.js +++ /dev/null @@ -1,39 +0,0 @@ -const { MessageEmbed } = require('discord.js'); -const CommandCategory = require('../command-category'); -const CommandPermission = require('../command-permission'); - -class SeeMembers -{ - static instance = null; - - constructor() { - if (SeeMembers.instance !== null) { - return SeeMembers.instance; - } - - this.aliases = ['see-members', 'seemembers', 'seemember', 'sm']; - this.category = CommandCategory.RESOURCE; - this.isAllowedForContext = CommandPermission.yes; - this.description = 'Explains how to allow your friends to see your members.'; - } - - /** - * @param {Message} message - * @param {Array} args - */ - async process(message, args) { - const embed = new MessageEmbed(); - - embed.setColor(APP_MAIN_COLOUR); - embed.setAuthor('See members', bot.user.displayAvatarURL({ dynamic: true })); - embed.setDescription( - 'If your friends cannot see your members, you have to go into the friend their profile, click on the ' + - 'cogwheel on the right top and press "They can see your shared members", this will allow them to see ' + - 'your public members' - ); - - return message.channel.send(embed); - } -} - -module.exports = new SeeMembers(); diff --git a/model/command/sync-members-pluralkit.js b/model/command/sync-members-pluralkit.js deleted file mode 100644 index fbc63cc..0000000 --- a/model/command/sync-members-pluralkit.js +++ /dev/null @@ -1,73 +0,0 @@ -const { MessageEmbed } = require('discord.js'); -const CommandCategory = require('../command-category'); -const CommandPermission = require('../command-permission'); - -class SyncMembersPluralkit -{ - static instance = null; - - constructor() { - if (SyncMembersPluralkit.instance !== null) { - return SyncMembersPluralkit.instance; - } - - this.aliases = [ - 'sync-member-pluralkit', - 'syncmemberpluralkit', - 'syncmember-pluralkit', - 'sync-memberpluralkit', - 'syncmemberspluralkit', - 'syncmembers-pluralkit', - 'sync-memberspluralkit', - 'sync-member-plural-kit', - 'syncmemberplural-kit', - 'syncmember-plural-kit', - 'sync-memberplural-kit', - 'syncmembersplural-kit', - 'syncmembers-plural-kit', - 'sync-membersplural-kit', - 'sync-member', - 'syncmember', - 'sync-members', - 'syncmembers', - 'sync-pluralkit', - 'syncpluralkit', - 'sync-plural-kit', - 'syncplural-kit', - 'smp', - 'smpk', - ]; - this.category = CommandCategory.RESOURCE; - this.isAllowedForContext = CommandPermission.yes; - this.description = 'Talks about syncing your members to PluralKit.'; - } - - /** - * @param {Message} message - * @param {Array} args - */ - async process(message, args) { - const embed = new MessageEmbed(); - - embed.setColor(APP_MAIN_COLOUR); - embed.setAuthor('Sync members to PluralKit', bot.user.displayAvatarURL({ dynamic: true })); - embed.setDescription( - 'If you wish to sync your members to PluralKit, go into the settings page -> Integrations -> PluralKit ' + - 'and fill in your PluralKit token, you can get this token by typing pk;token anywhere and PluralKit ' + - 'will message you the token in a DM. \n' + - '\n' + - 'Once filled out, you can go to actions in the members page and press Sync (rebooting app may be ' + - 'required to see this option after adding the token). You will be prompted with the option to sync to ' + - 'and from pk. \n' + - '\n' + - 'Pay attention that they are linked by the plural kit id found in the individual member settings in ' + - 'Simply Plural. If you make a member on Simply Plural and you make the same member on PluralKit you ' + - 'will have to go into the individual member settings of Simply Plural and fill in the PluralKit user id ' + - 'in the settings. If you don\'t do this you will end up with duplicate members on Plural Kit.' - ); - - return message.channel.send(embed); - } -} - -module.exports = new SyncMembersPluralkit(); diff --git a/model/command/system-relationships.js b/model/command/system-relationships.js deleted file mode 100644 index 62da561..0000000 --- a/model/command/system-relationships.js +++ /dev/null @@ -1,40 +0,0 @@ -const { MessageEmbed } = require('discord.js'); -const CommandCategory = require('../command-category'); -const CommandPermission = require('../command-permission'); - -class SystemRelationships -{ - static instance = null; - - constructor() { - if (SystemRelationships.instance !== null) { - return SystemRelationships.instance; - } - - this.aliases = ['system-relationship', 'systemrelationships', 'systemrelationships', 'sr']; - this.category = CommandCategory.RESOURCE; - this.isAllowedForContext = CommandPermission.yes; - this.description = 'Explains what "system relationships" are.'; - } - - /** - * @param {Message} message - * @param {Array} args - */ - async process(message, args) { - const embed = new MessageEmbed(); - - embed.setColor(APP_MAIN_COLOUR); - embed.setAuthor('System relationships', bot.user.displayAvatarURL({ dynamic: true })); - embed.setDescription( - 'This is an open-ended field meant to describe what relationships a headmate has. It can be used to ' + - 'describe the member\'s relationship to the system as a whole, their inner system relationships such as ' + - 'being family, friends, or romantic partners, or it can describe their outer-system relationships with ' + - 'people in the outer world, such as family, friends, and romantic partners.' - ); - - return message.channel.send(embed); - } -} - -module.exports = new SystemRelationships(); diff --git a/model/command/website.js b/model/command/website.js deleted file mode 100644 index 2d109bd..0000000 --- a/model/command/website.js +++ /dev/null @@ -1,40 +0,0 @@ -const { MessageEmbed } = require('discord.js'); -const CommandCategory = require('../command-category'); -const CommandPermission = require('../command-permission'); - -class Website -{ - static instance = null; - - constructor() { - if (Website.instance !== null) { - return Website.instance; - } - - this.aliases = []; - this.category = CommandCategory.RESOURCE; - this.isAllowedForContext = CommandPermission.yes; - this.description = 'Talks about the possibility of having a web portal for Simply Plural.'; - } - - /** - * @param {Message} message - * @param {Array} args - */ - async process(message, args) { - const embed = new MessageEmbed(); - - embed.setColor(APP_MAIN_COLOUR); - embed.setAuthor('Website', bot.user.displayAvatarURL({ dynamic: true })); - embed.setDescription( - 'A web portal for Simply Plural is unlikely to be made at the moment. The framework that Simply Plural ' + - 'has been created with has the capability of web development, but it is currently experimental, which ' + - 'would make it buggy and unstable to use. This decision may change later in the year when the framework ' + - 'becomes more stable and once the app is more complete.' - ); - - return message.channel.send(embed); - } -} - -module.exports = new Website(); diff --git a/model/messages.js b/model/messages.js new file mode 100644 index 0000000..7173da9 --- /dev/null +++ b/model/messages.js @@ -0,0 +1,54 @@ +let names = {}; +let messages = {}; + +const genRandomId = () => Math.ceil(Math.random() * 1000); + +const load = () => require("../messages").forEach(msg => { + let id = genRandomId(); + while (messages[id]) + id = genRandomId(); + + msg.names.forEach(name => names[name] = id); + messages[id] = msg; +}); + +load(); + +// interface Message { +// names string[]; +// description string; +// title string; +// text string; +// } + +const { MessageEmbed } = require("discord.js"); + +module.exports = { + get names() { + return names; + }, + get messages() { + return messages; + }, + get: (name) => messages[names[name]], + getList: () => Object.keys(messages).map(msg => ({ name: messages[msg].names[0], value: messages[msg].description })), + getEmbed: (name) => { + let msg = messages[names[name]]; + if (!msg) return; + + return { + color: APP_MAIN_COLOUR, + author: { + name: msg.title, + iconURL: bot.user.displayAvatarURL({ dynamic: true }), + }, + description: msg.text, + } + }, + reload: () => { + names = {}; + messages = {}; + delete require.cache[require.resolve("../messages")]; + load(); + } +}