├── .env.example ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug-issue-template.md │ └── feature-request-issue-template.md └── PULL_REQUEST_TEMPLATE │ ├── command.md │ ├── core.md │ └── event.md ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── commands ├── announce.js ├── define.js ├── help.js ├── poll.js ├── quote.js ├── reload.js ├── resetconfig.js ├── sectional.js ├── setchannel.js ├── setrole.js └── ticket.js ├── data └── commands.json ├── ecosystem.config.js ├── events ├── guildMemberAdd.js ├── message.js └── ready.js ├── index.js ├── jest.config.js ├── package.json ├── server ├── routes.js └── templates │ ├── email │ └── .gitkeep │ └── web │ └── index.html ├── shrinkwrap.yaml ├── tests ├── config.test.js └── quote.test.js └── utils ├── auth.js ├── client.js ├── config.js ├── db.js ├── index.js ├── server.js └── view.js /.env.example: -------------------------------------------------------------------------------- 1 | # Discord 2 | DISCORD_BOT_TOKEN= 3 | 4 | # Miscellaneous Configuration 5 | CMD_PREFIX= 6 | WEB_SERVER_PORT= 7 | API_ROUTE_PREFIX= 8 | 9 | # Firebase Configuration 10 | FIREBASE_API_KEY= 11 | FIREBASE_AUTH_DOMAIN= 12 | FIREBASE_DB_URL= 13 | FIREBASE_PROJECT_ID= 14 | FIREBASE_STORAGE_BUCKET= 15 | FIREBASE_MESSAGING_SENDER_ID= 16 | 17 | # Giphy Configuration 18 | GIPHY_API_KEY= 19 | 20 | # Nasa Configuration 21 | NASA_API_KEY= -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # These owners will be the default owners for everything in 2 | # the repo. 3 | * @crock @benjaminspak 4 | 5 | # In this example, @doctocat owns any files in the build/logs 6 | # directory at the root of the repository and any of its 7 | # subdirectories. 8 | /commands/define.js @ap4gh 9 | /commands/sectional.js @ap4gh 10 | /commands/quote.js @ThomasErhel 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-issue-template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "[BUG] Bug Report" 3 | about: Use this template when you find a bug with the core bot. 4 | labels: bug 5 | 6 | --- 7 | 8 | **Brief description of bug:** 9 | 10 | 11 | **Steps to re-produce:** 12 | - Step 1 13 | - Step 2 14 | - Step 3 -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request-issue-template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "[FEATURE] Feature Request" 3 | about: Use this template when you want to make a feature request. 4 | labels: enhancement 5 | 6 | --- 7 | 8 | **Brief description of feature:** 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/command.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "[COMMAND] New Command" 3 | about: Use this template when you submit a PR with a new bot command 4 | labels: 5 | 6 | --- 7 | 8 | **Command Name:** ping 9 | 10 | **Command Description:** When the user types `!ping`, the bot replies with `Pong!` 11 | 12 | **Permissions Required:** 13 | - Send Messages 14 | 15 | **NPM Dependencies Added:** 16 | - None 17 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/core.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "[CORE] New Core Contribution" 3 | about: Use this template when you submit a PR with a new contribution to core bot functionality. 4 | labels: 5 | 6 | --- 7 | 8 | **Brief description of improvement:** 9 | 10 | **Permissions Required:** 11 | - None 12 | 13 | **NPM Dependencies Added:** 14 | - None 15 | 16 | **NPM Dependencies Removed:** 17 | - None -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/event.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "[EVENT] New Event" 3 | about: Use this template when you submit a PR with a new event for the bot to listen 4 | to. 5 | labels: 6 | 7 | --- 8 | 9 | **Event Name:** guildMemberAdd 10 | 11 | **Event Action Description:** Greets the user that joins the server 12 | 13 | **Permissions Required:** 14 | - Send Messages 15 | 16 | **NPM Dependencies Added:** 17 | - None 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Editor configurations 2 | .idea/ 3 | .vscode/ 4 | 5 | # Node Dependencies 6 | node_modules/ 7 | 8 | # Useless files 9 | .DS_Store 10 | 11 | # Private Configuration files 12 | config.json 13 | .env 14 | 15 | dist/ 16 | 17 | # log files 18 | logs/*.log 19 | 20 | # lock files 21 | package-lock.json 22 | yarn.lock -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Citizen Code of Conduct 2 | 3 | ## 1. Purpose 4 | 5 | A primary goal of Code Career is to be inclusive to the largest number of contributors, with the most varied and diverse backgrounds possible. As such, we are committed to providing a friendly, safe and welcoming environment for all, regardless of gender, sexual orientation, ability, ethnicity, socioeconomic status, and religion (or lack thereof). 6 | 7 | This code of conduct outlines our expectations for all those who participate in our community, as well as the consequences for unacceptable behavior. 8 | 9 | We invite all those who participate in Code Career to help us create safe and positive experiences for everyone. 10 | 11 | ## 2. Open [Source/Culture/Tech] Citizenship 12 | 13 | A supplemental goal of this Code of Conduct is to increase open [source/culture/tech] citizenship by encouraging participants to recognize and strengthen the relationships between our actions and their effects on our community. 14 | 15 | Communities mirror the societies in which they exist and positive action is essential to counteract the many forms of inequality and abuses of power that exist in society. 16 | 17 | If you see someone who is making an extra effort to ensure our community is welcoming, friendly, and encourages all participants to contribute to the fullest extent, we want to know. 18 | 19 | ## 3. Expected Behavior 20 | 21 | The following behaviors are expected and requested of all community members: 22 | 23 | * Participate in an authentic and active way. In doing so, you contribute to the health and longevity of this community. 24 | * Exercise consideration and respect in your speech and actions. 25 | * Attempt collaboration before conflict. 26 | * Refrain from demeaning, discriminatory, or harassing behavior and speech. 27 | * Be mindful of your surroundings and of your fellow participants. Alert community leaders if you notice a dangerous situation, someone in distress, or violations of this Code of Conduct, even if they seem inconsequential. 28 | * Remember that community event venues may be shared with members of the public; please be respectful to all patrons of these locations. 29 | 30 | ## 4. Unacceptable Behavior 31 | 32 | The following behaviors are considered harassment and are unacceptable within our community: 33 | 34 | * Violence, threats of violence or violent language directed against another person. 35 | * Sexist, racist, homophobic, transphobic, ableist or otherwise discriminatory jokes and language. 36 | * Posting or displaying sexually explicit or violent material. 37 | * Posting or threatening to post other people's personally identifying information ("doxing"). 38 | * Personal insults, particularly those related to gender, sexual orientation, race, religion, or disability. 39 | * Inappropriate photography or recording. 40 | * Inappropriate physical contact. You should have someone's consent before touching them. 41 | * Unwelcome sexual attention. This includes, sexualized comments or jokes; inappropriate touching, groping, and unwelcomed sexual advances. 42 | * Deliberate intimidation, stalking or following (online or in person). 43 | * Advocating for, or encouraging, any of the above behavior. 44 | * Sustained disruption of community events, including talks and presentations. 45 | 46 | ## 5. Weapons Policy 47 | 48 | No weapons will be allowed at Code Career events, community spaces, or in other spaces covered by the scope of this Code of Conduct. Weapons include but are not limited to guns, explosives (including fireworks), and large knives such as those used for hunting or display, as well as any other item used for the purpose of causing injury or harm to others. Anyone seen in possession of one of these items will be asked to leave immediately, and will only be allowed to return without the weapon. Community members are further expected to comply with all state and local laws on this matter. 49 | 50 | ## 6. Consequences of Unacceptable Behavior 51 | 52 | Unacceptable behavior from any community member, including sponsors and those with decision-making authority, will not be tolerated. 53 | 54 | Anyone asked to stop unacceptable behavior is expected to comply immediately. 55 | 56 | If a community member engages in unacceptable behavior, the community organizers may take any action they deem appropriate, up to and including a temporary ban or permanent expulsion from the community without warning (and without refund in the case of a paid event). 57 | 58 | ## 7. Reporting Guidelines 59 | 60 | If you are subject to or witness unacceptable behavior, or have any other concerns, please notify a community organizer as soon as possible. croc122@gmail.com. 61 | 62 | [Reporting guidelines](https://github.com/GitCodeCareer/discord-bot/wiki/Abuse) 63 | 64 | Additionally, community organizers are available to help community members engage with local law enforcement or to otherwise help those experiencing unacceptable behavior feel safe. In the context of in-person events, organizers will also provide escorts as desired by the person experiencing distress. 65 | 66 | ## 8. Addressing Grievances 67 | 68 | If you feel you have been falsely or unfairly accused of violating this Code of Conduct, you should notify CodeCareer.org with a concise description of your grievance. Your grievance will be handled in accordance with our existing governing policies. [Policy](https://github.com/GitCodeCareer/discord-bot/wiki/Abuse) 69 | 70 | 71 | 72 | ## 9. Scope 73 | 74 | We expect all community participants (contributors, paid or otherwise; sponsors; and other guests) to abide by this Code of Conduct in all community venues--online and in-person--as well as in all one-on-one communications pertaining to community business. 75 | 76 | This code of conduct and its related procedures also applies to unacceptable behavior occurring outside the scope of community activities when such behavior has the potential to adversely affect the safety and well-being of community members. 77 | 78 | ## 10. Contact info 79 | 80 | croc122@gmail.com 81 | 82 | ## 11. License and attribution 83 | 84 | The Citizen Code of Conduct is distributed by [Stumptown Syndicate](http://stumptownsyndicate.org) under a [Creative Commons Attribution-ShareAlike license](http://creativecommons.org/licenses/by-sa/3.0/). 85 | 86 | Portions of text derived from the [Django Code of Conduct](https://www.djangoproject.com/conduct/) and the [Geek Feminism Anti-Harassment Policy](http://geekfeminism.wikia.com/wiki/Conference_anti-harassment/Policy). 87 | 88 | _Revision 2.3. Posted 6 March 2017._ 89 | 90 | _Revision 2.2. Posted 4 February 2016._ 91 | 92 | _Revision 2.1. Posted 23 June 2014._ 93 | 94 | _Revision 2.0, adopted by the [Stumptown Syndicate](http://stumptownsyndicate.org) board on 10 January 2013. Posted 17 March 2013._ 95 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | Getting started with your first pull request (PR) is easy! Just follow these steps... 3 | 4 | 1. Fork the repository to your own Github account. 5 | 2. Clone the forked repository to a directory on your computer. 6 | 3. Run `npm install` from within the directory. If you prefer [Yarn](https://yarnpkg.com/en/), feel free to use that instead. 7 | 4. Create a new development bot at https://discordapp.com/developers/applications/. Be sure to also create a bot user in order to interact with it. 8 | 5. Create a new project at https://console.firebase.google.com/. Go to **Settings** then **Add an application** and under **Firebase SDK snippet**, choose **CDN**. Copy/Paste correct values in `.env` 9 | 6. Duplicate the `.env.example` file and rename it to `.env` and fill in the appropriate values from the Discord Developer portal and from Firebase Console 10 | 7. Create a new javascript file in the commands folder that is named after the command you want to add. If you want to add a new event instead, make the event in `index.js`. 11 | 8. To test the bot, run `node index.js` 12 | 9. Make sure to debug the new command and test it a lot. If you need help, ask in the CodeCareer Discord anytime! Use the [#open-source-😺](https://discord.gg/nVCtqvQ) channel. When you are ready to submit the PR, head back to this page and click `New pull request`. 13 | 10. Wait patiently for an admin to look it over and if everything checks out, it will be merged. 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-2019 Code Career 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CodeCareer Discord Bot 2 | 3 | ## Our Vision 4 | We created this Discord bot to not only provide some useful functionality for the CodeCareer community Discord server, but to also help new developers on their journey to landing a full-time position in tech by providing the opportunity and guidance a new developer needs to make their first real world contribution to open-source projects such as this one. 5 | 6 | ## Commands 7 | Visit the wiki to see a list of all current commands. 8 | https://github.com/GitCodeCareer/discord-bot/wiki/Commands 9 | 10 | ## Get Started 11 | Getting started with your first pull request (PR) is easy! Just follow these steps... 12 | 13 | 1. Fork the repository to your own Github account. 14 | 2. Clone the forked repository to a directory on your computer. 15 | 3. Run `npm install` from within the directory. If you prefer [Yarn](https://yarnpkg.com/en/), feel free to use that instead. 16 | 4. Create a new development bot at https://discordapp.com/developers/applications/. Be sure to also create a bot user in order to interact with it. 17 | 5. Create a new project at https://console.firebase.google.com/. Go to **Settings** then **Add an application** and under **Firebase SDK snippet**, choose **CDN**. Copy/Paste correct values in `.env` 18 | 6. Duplicate the `.env.example` file and rename it to `.env` and fill in the appropriate values from the Discord Developer portal and from Firebase Console 19 | 7. Create a new javascript file in the commands folder that is named after the command you want to add. If you want to add a new event instead, make the event in `index.js`. 20 | 8. To test the bot, run `node index.js` 21 | 9. Make sure to debug the new command and test it a lot. If you need help, ask in the CodeCareer Discord anytime! Use the [#open-source-😺](https://discord.gg/nVCtqvQ) channel. When you are ready to submit the PR, head back to this page and click `New pull request`. 22 | 10. Wait patiently for an admin to look it over and if everything checks out, it will be merged. 23 | -------------------------------------------------------------------------------- /commands/announce.js: -------------------------------------------------------------------------------- 1 | const Auth = require('../utils/auth') 2 | 3 | exports.run = async (message, args) => { 4 | 5 | if (Auth.isAdmin(message.member)) { 6 | 7 | if(!args || args.length < 1) 8 | return message.reply("Must provide the text for the announcement after the command."); 9 | 10 | const announcement = args.join(' '); 11 | 12 | await message.channel.send(announcement); 13 | await message.delete(); 14 | } else { 15 | message.reply('You must be an admin in order to run this command.'); 16 | } 17 | 18 | }; -------------------------------------------------------------------------------- /commands/define.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * command_name: define 4 | * version: 3.1.0 5 | * description: Provides definition for words from web. 6 | * npm_dependencies: { request, request-promise-native } 7 | * author: ap4gh(Github), debjay(on CodeCareer Discord Server) 8 | * license: MIT https://opensource.org/licenses/MIT 9 | * 10 | * COMMENT GLOSSARY 11 | * ---------------- 12 | * 1. TODO: To be done in future. 13 | * 2. NOTE: Important statement about code. 14 | * 3. PROC: Explains the following procedure. 15 | * 16 | */ 17 | 18 | const request = require('request-promise-native'); 19 | 20 | const maxRelatedTopics = 4; 21 | 22 | // run function for !define command: 23 | exports.run = (message, args) => { 24 | if (args.length === 0) 25 | return sendMessage(message, 'Use `!define --help` to get command guide.'); 26 | runUserCommand(message, args, args[0]); // args[0]: option name 27 | }; 28 | 29 | /** runUserCommand: screen command and execute handler. */ 30 | const runUserCommand = (message, args, option = '') => { 31 | const availableCommands = { 32 | '-h': showHelp, 33 | '--help': showHelp, 34 | wiki: wikipediaOpenSearch, 35 | default: ddgInstantAnswer 36 | }; 37 | // PROC: if option does not match any, return default 38 | return (availableCommands[option] || availableCommands.default)(message, args); 39 | }; 40 | 41 | 42 | /** -------------- 43 | * OPTION HANDLERS 44 | * ---------------*/ 45 | 46 | /** ddgInstantAnswer: get answers from DuckDuckGo */ 47 | async function ddgInstantAnswer (message, args) { 48 | 49 | const searchPhrase = args.slice(0, args.length).join(' '); // search phrase 50 | let data; // to collect fetched data. 51 | 52 | try { 53 | data = await request({ 54 | url: generateQueryURL(searchPhrase), 55 | json: true, 56 | headers: { 'Content-Type': 'application/json' } 57 | }); 58 | } catch (err) { 59 | console.error(err); 60 | return notifyErrors(message, e); 61 | } 62 | 63 | let result = `:mag: \`${searchPhrase}\`\n`; 64 | const relatedTopics = data['RelatedTopics']; 65 | const abstractText = data['AbstractText']; 66 | const abstractURL = data['AbstractURL']; 67 | 68 | // PROC: if nothing was found: 69 | if (relatedTopics.length === 0) 70 | result += `Cannot find information on *${searchPhrase}* :no_good: Read the command guide with \`!define --help\` to get accurate results.`; 71 | else if (!abstractText || !abstractURL) { 72 | result += `*"${searchPhrase}" may refer to following things* :point_down:\n\n`; 73 | for (let topic of relatedTopics) { 74 | /** 75 | * NOTE: keeping maximum of 3 related topics to be displayed. 76 | * maximum related topics can be changed at the top. 77 | * discord do not allow a message length > 2000 chars. 78 | */ 79 | if ( 80 | topic['Text'] === undefined || 81 | topic['FirstURL'] === undefined || 82 | relatedTopics.indexOf(topic) >= maxRelatedTopics 83 | ) break; 84 | 85 | result += `${topic['Text']}\n${topic['FirstURL']}\n\n`; 86 | } 87 | } // if abstract data exist: 88 | else { 89 | result += '```' + abstractText + '```:link: ' + abstractURL; 90 | } 91 | return sendMessage(message, result); 92 | }; 93 | 94 | /** wikipediaOpenSearch: get answers from Wikipedia */ 95 | async function wikipediaOpenSearch (message, args) { 96 | 97 | const searchPhrase = args.slice(1, args.length).join(' '); // search phrase 98 | let data; 99 | 100 | try { 101 | data = await request({ 102 | url: generateQueryURL(searchPhrase, 'wiki'), 103 | json: true, 104 | headers: { 105 | 'Content-Type': 'application/json' 106 | } 107 | }); 108 | } catch (e) { 109 | console.error(e); 110 | return notifyErrors(message, e); 111 | } 112 | 113 | const definitions = data[2]; // all definitions 114 | const links = data[3]; // all wikipedia page links 115 | let wikipediaPageLink = ':link: ' + links[0]; // main definition page link 116 | 117 | let result = definitions[0]; // first definition 118 | if (!result) // PROC: if no definition was found 119 | result = `No information provided for *${searchPhrase}* :no_good: `; 120 | // More answers? collect them. 121 | else if (result.match(/may refer to/g)) { 122 | result = `:mag: **Wikipedia**: \`${searchPhrase}\`\n\n` + 123 | '```\n' + 124 | result + 125 | '\n\n'; 126 | 127 | definitions.shift(); // PROC: remove useless definition at index 0 128 | let nonEmptyDefinitions = []; 129 | 130 | for (let d of definitions) 131 | if (d.length > 0) 132 | nonEmptyDefinitions.push(d); 133 | 134 | for (let i = 0; i < maxRelatedTopics; ++i) { 135 | if (nonEmptyDefinitions[i] == undefined) 136 | break; 137 | result += `${i + 1}. ${nonEmptyDefinitions[i]}\n\n`; // PROC: store all non-empty defn. 138 | } 139 | result += '```'; 140 | } 141 | else { // PROC: if exact meaning was obtained, send it. 142 | result = `:mag: ${searchPhrase}` + '```' + result + '```' + wikipediaPageLink; 143 | } 144 | return sendMessage(message, result); 145 | }; 146 | 147 | /** showHelp: command manual. */ 148 | function showHelp (message, args) { 149 | // TODO: Move into manuals folder 150 | sendMessage( 151 | message, 152 | ` 153 | \`\`\` 154 | NAME 155 | define -- provide definition for words from web. 156 | 157 | DESCRIPTION 158 | !define command gets abstract information on a word from 159 | duckduckgo and wikipedia. 160 | 161 | COMMANDS 162 | 1) !define DuckDuckGo instant answer. 163 | 2) !define wiki Wikipedia definition. 164 | 3) !define -h[--help] Provides help text. 165 | 166 | EXAMPLES 167 | > !define yellow stone 168 | > !define wiki object oriented programming 169 | 170 | GUIDE 171 | !define will only show definition on receiving exact info, 172 | in case a word have more than one meaning, related topics 173 | (not more than three) will be displayed. To get more acc- 174 | urate results pass more keywords in search phrase sepearted 175 | with spaces. Eg: 'react' means many things but if you want 176 | to get definition for 'reactjs' use command like this: 177 | 178 | > !define reactjs 179 | OR 180 | > !define wiki react javascript 181 | 182 | For now, !define only provide information about things, places, 183 | events, news etc. and does not provide meaning of the words from 184 | english dictionary. DDG bang redirects will also not work here. 185 | \`\`\` 186 | ` 187 | ); 188 | }; 189 | 190 | /** --------------- 191 | * HELPER FUNCTIONS 192 | * ----------------*/ 193 | 194 | /** notifyErrors: Notify maintainer and the end-user about the error. */ 195 | async function notifyErrors(message, err = '') { 196 | const maintainerID = '274434863711518722'; 197 | // NOTE: maintainer ID can be changed above 198 | const author = message.guild.member(maintainerID); 199 | author.send(`Message ID: ${message.id}\n` + '```' + err + '```'); 200 | await message.channel.send(`Some internal error occured, maintainer ${author} has been notified.`); 201 | }; 202 | 203 | /** sendMessage: checks for errors then send message to the channel. */ 204 | async function sendMessage(message, messageContent) { 205 | try { 206 | await message.channel.send(messageContent); 207 | } catch (err) { 208 | return notifyErrors(message, err); 209 | } 210 | }; 211 | 212 | /** generateQueryURL: generate api query URL for service. */ 213 | function generateQueryURL(phrase, service = 'ddg') { 214 | const queryURLs = { 215 | wiki: `https://en.wikipedia.org/w/api.php?action=opensearch&list=search&search=${phrase}&format=json&formatversion=2`, 216 | ddg: `https://api.duckduckgo.com/?q=${phrase}&format=json` 217 | }; 218 | return encodeURI(queryURLs[service]); 219 | }; 220 | 221 | -------------------------------------------------------------------------------- /commands/help.js: -------------------------------------------------------------------------------- 1 | const Discord = require("discord.js"); 2 | const { client, config, auth } = require('../utils/') 3 | 4 | exports.run = (message, args) => { 5 | 6 | const bot = client.getClient() 7 | let commandList = require('../data/commands.json') 8 | let member = message.member 9 | let isStaff = auth.isMod(member) || auth.isAdmin(member) || auth.isOwner(member) 10 | let flag = (args && args.length > 0) ? args[0].replace(/-+/, '').slice(0,1) : null 11 | 12 | let shouldDisplayUserCommands = flag === 'u' || !isStaff 13 | let shouldDisplayStaffCommands = isStaff && flag === 's' 14 | let shouldDisplayOwnerCommands = auth.isOwner(member) && flag === 'o' 15 | let shouldDisplayAdminCommands = auth.isAdmin(member) && flag === 'a' 16 | let shouldDisplayModCommands = auth.isMod(member) && flag === 'm' 17 | 18 | const sendNoRevealingSecretsDM = () => { 19 | let dmEmbed = new Discord.RichEmbed() 20 | dmEmbed.addField('Important Notice', 'As a staff member, in order to show the general user commands without revealing the staff commmands, use the `-u` flag.') 21 | message.author.send({ embed: dmEmbed }) 22 | } 23 | 24 | let embedTitle = shouldDisplayStaffCommands && !auth.isOwner(member) ? `${member.highestRole.name} ` : '' 25 | 26 | const embed = new Discord.RichEmbed() 27 | .setTitle(`${embedTitle}Bot Commands`) 28 | .setAuthor(bot.user.username, bot.user.avatarURL) 29 | 30 | commandList.map((cmd) => { 31 | let cmdArgs = cmd.arguments.map(arg => { 32 | return `[${arg}]` 33 | }).join(' ') 34 | let fieldTitle = `${config.getCommandPrefix()}${cmd.command} ${cmdArgs}` 35 | 36 | switch(cmd.permissionLevel) { 37 | case "all": 38 | if (shouldDisplayUserCommands) 39 | embed.addField(fieldTitle, cmd.description) 40 | break 41 | case "mod": 42 | if (shouldDisplayModCommands || shouldDisplayStaffCommands) 43 | embed.addField(fieldTitle, cmd.description) 44 | break 45 | case "admin": 46 | if (shouldDisplayAdminCommands || shouldDisplayStaffCommands) 47 | embed.addField(fieldTitle, cmd.description) 48 | break 49 | case "owner": 50 | if (shouldDisplayOwnerCommands) 51 | embed.addField(fieldTitle, cmd.description) 52 | break 53 | } 54 | }) 55 | 56 | return !flag && isStaff ? sendNoRevealingSecretsDM() : message.channel.send({embed}); 57 | } 58 | -------------------------------------------------------------------------------- /commands/poll.js: -------------------------------------------------------------------------------- 1 | const { auth } = require('../utils'); 2 | 3 | exports.run = async (message, args) => { 4 | 5 | if (auth.isAdmin(message.member)) { 6 | 7 | if(!args || args.length < 1) 8 | return message.reply("Please provide description of poll after command name."); 9 | 10 | const question = args.join(' ') 11 | 12 | const msg = await message.channel.send(`Hey everyone, ${message.author} have started a poll.\n\`\`\`${question}\`\`\``); 13 | await msg.react("✅"); 14 | await msg.react("❌"); 15 | await message.delete(); 16 | 17 | } else { 18 | await message.reply('You must be an admin in order to run this command.'); 19 | } 20 | 21 | }; -------------------------------------------------------------------------------- /commands/quote.js: -------------------------------------------------------------------------------- 1 | const Discord = require('discord.js'); 2 | const axios = require('axios'); 3 | 4 | exports.run = async message => { 5 | try { 6 | axios.all([ 7 | axios.get('http://quotes.stormconsultancy.co.uk/random.json').catch(function (error) { 8 | if (error.response) { 9 | console.log(error.response.data); 10 | console.log(error.response.status); 11 | console.log(error.response.headers); 12 | message.channel.send( 13 | "🤖 The request was made and the server responded with a status code that falls out of the range of 2xx 🐛" 14 | ); 15 | } else if (error.request) { 16 | console.log(error.request); 17 | message.channel.send( 18 | "🤖 The request was made but no response was received `error.request` is an instance of XMLHttpRequest in the browser and an instance of http.ClientRequest in node.js 🐛" 19 | ); 20 | } else { 21 | console.log('Error', error.message); 22 | message.channel.send( 23 | "🤖 Something happened in setting up the request that triggered an Error 🐛" 24 | ); 25 | } 26 | console.log(error.config); 27 | }), 28 | axios.get('https://quotes.rest/qod.json').catch(function (error) { 29 | if (error.response) { 30 | console.log(error.response.data); 31 | console.log(error.response.status); 32 | console.log(error.response.headers); 33 | message.channel.send( 34 | "🤖 The request was made and the server responded with a status code that falls out of the range of 2xx 🐛" 35 | ); 36 | } else if (error.request) { 37 | console.log(error.request); 38 | message.channel.send( 39 | "🤖 The request was made but no response was received `error.request` is an instance of XMLHttpRequest in the browser and an instance of http.ClientRequest in node.js 🐛" 40 | ); 41 | } else { 42 | console.log('Error', error.message); 43 | message.channel.send( 44 | "🤖 Something happened in setting up the request that triggered an Error 🐛" 45 | ); 46 | } 47 | console.log(error.config); 48 | }), 49 | axios.get(`https://api.giphy.com/v1/gifs/random?tag=motivational&rating=g&api_key=${process.env.GIPHY_API_KEY}`).catch(function (error) { 50 | if (error.response) { 51 | console.log(error.response.data); 52 | console.log(error.response.status); 53 | console.log(error.response.headers); 54 | message.channel.send( 55 | "🤖 The request was made and the server responded with a status code that falls out of the range of 2xx 🐛" 56 | ); 57 | } else if (error.request) { 58 | console.log(error.request); 59 | message.channel.send( 60 | "🤖 The request was made but no response was received `error.request` is an instance of XMLHttpRequest in the browser and an instance of http.ClientRequest in node.js 🐛" 61 | ); 62 | } else { 63 | console.log('Error', error.message); 64 | message.channel.send( 65 | "🤖 Something happened in setting up the request that triggered an Error 🐛" 66 | ); 67 | } 68 | console.log(error.config); 69 | }), 70 | axios.get(`https://api.nasa.gov/planetary/apod?api_key=${process.env.NASA_API_KEY}&hd=true`).catch(function (error) { 71 | if (error.response) { 72 | console.log(error.response.data); 73 | console.log(error.response.status); 74 | console.log(error.response.headers); 75 | message.channel.send( 76 | "🤖 The request was made and the server responded with a status code that falls out of the range of 2xx 🐛" 77 | ); 78 | } else if (error.request) { 79 | console.log(error.request); 80 | message.channel.send( 81 | "🤖 The request was made but no response was received `error.request` is an instance of XMLHttpRequest in the browser and an instance of http.ClientRequest in node.js 🐛" 82 | ); 83 | } else { 84 | console.log('Error', error.message); 85 | message.channel.send( 86 | "🤖 Something happened in setting up the request that triggered an Error 🐛" 87 | ); 88 | } 89 | console.log(error.config); 90 | }) 91 | ]) 92 | .then(axios.spread((randomquote, quoteoftheday, giphy, nasa) => { 93 | const quoteEmbed = new Discord.RichEmbed() 94 | .setColor("#5DBCD2") 95 | .setAuthor(randomquote.data.author, "https://robohash.org/CodeCareer.io.png") 96 | .setThumbnail(nasa.data.hdurl) 97 | .setTitle("💡 Motivational Quote 🌈🎯🚀") 98 | .setURL("https://codecareer.io/") 99 | .setDescription(randomquote.data.quote) 100 | .addField("🌠 Quote Of The Day :", quoteoftheday.data.contents.quotes[0].quote, true) 101 | .addField(quoteoftheday.data.contents.quotes[0].author, quoteoftheday.data.contents.quotes[0].tags, true) 102 | .addField("🔮 Random GIF :", giphy.data.data.title, true) 103 | .setImage(giphy.data.data.image_url) 104 | .setFooter( 105 | "CodeCareer is committed to helping new developers make their first PR!", 106 | "https://avatars3.githubusercontent.com/u/42856887?s=200&v=4" 107 | ) 108 | .setTimestamp(); 109 | message.channel.send(quoteEmbed); 110 | })); 111 | } catch (error) { 112 | if (error.response) { 113 | console.log(error.response.data); 114 | console.log(error.response.status); 115 | console.log(error.response.headers); 116 | message.channel.send( 117 | "🤖 The request was made and the server responded with a status code that falls out of the range of 2xx 🐛" 118 | ); 119 | } else if (error.request) { 120 | console.log(error.request); 121 | message.channel.send( 122 | "🤖 The request was made but no response was received `error.request` is an instance of XMLHttpRequest in the browser and an instance of http.ClientRequest in node.js 🐛" 123 | ); 124 | } else { 125 | console.log('Error', error.message); 126 | message.channel.send( 127 | "🤖 Something happened in setting up the request that triggered an Error 🐛" 128 | ); 129 | } 130 | console.log(error.config); 131 | } 132 | }; 133 | -------------------------------------------------------------------------------- /commands/reload.js: -------------------------------------------------------------------------------- 1 | exports.run = (message, args) => { 2 | if(!args || args.size < 1) return message.reply("Must provide a command name to reload."); 3 | // the path is relative to the *current folder*, so just ./filename.js 4 | delete require.cache[require.resolve(`./${args[0]}.js`)]; 5 | message.reply(`The command ${args[0]} has been reloaded`); 6 | }; -------------------------------------------------------------------------------- /commands/resetconfig.js: -------------------------------------------------------------------------------- 1 | const Auth = require('../utils/auth') 2 | const Config = require('../utils/config') 3 | 4 | exports.run = (message, args) => { 5 | 6 | if (Auth.isOwner(message.member)) { 7 | 8 | Config.resetConfig() 9 | message.reply("Config reset") 10 | 11 | } else { 12 | message.reply('You must be the guild owner in order to run this command.'); 13 | } 14 | 15 | }; -------------------------------------------------------------------------------- /commands/sectional.js: -------------------------------------------------------------------------------- 1 | /** 2 | * command_name: sectional 3 | * version: 1.1.0 4 | * description: set/delete/update section content in information channel. 5 | * author: ap4gh(Github), debjay(on CodeCareer Discord Server) 6 | * license: MIT https://opensource.org/licenses/MIT 7 | * 8 | * COMMENT GLOSSARY 9 | * ---------------- 10 | * 1. TODO: To be done in future. 11 | * 2. NOTE: Important statement about code. 12 | * 3. PROC: Explains the following procedure. 13 | */ 14 | 15 | const { db, config, auth } = require("../utils"); 16 | 17 | exports.run = async (message, args) => { 18 | // PROC: check if member is eligible to run this command. 19 | let member = message.guild.members.find(m => m.id === message.author.id); 20 | if (!auth.isAdmin(member)) 21 | return message.channel.send("`[ℹ]: You must be an admin in order to run this command.`"); 22 | 23 | runCommand(args[0], message, args); // args[0]: option name 24 | 25 | } 26 | 27 | /** runCommand: screen option and execute command. */ 28 | function runCommand(option, message, args) { 29 | 30 | const availableOptions = { 31 | '--init': init, 32 | "--list": listSections, 33 | "--update-content": updateContent, 34 | "--update-header": updateHeader, 35 | "--delete": deleteSection, 36 | "--reload": reloadSections, 37 | "-h": showHelp, 38 | "--help": showHelp, 39 | usual: usual 40 | } 41 | return (availableOptions[option] || availableOptions.usual)(message, args); 42 | 43 | } 44 | 45 | /** -------------- 46 | * OPTION HANDLERS 47 | * ---------------*/ 48 | 49 | /** usual: default response */ 50 | function usual(message, args) { 51 | message.channel.send(`\`[❌]: Unknown option "${args[0]}"\`. Try \`!sectional --help\``); 52 | } 53 | 54 | /** init: setup sectionals in database and server channel. */ 55 | async function init(message, args) { 56 | 57 | const sections = [ 58 | ["server-information", "https://i.imgur.com/ULdJ9EH.png"], 59 | ["community-rules", "https://i.imgur.com/VgmjM84.png"], 60 | ["role-hierarchy", "https://i.imgur.com/8xRbLSb.png"], 61 | ["open-source", "https://i.imgur.com/MrkZ96M.png"], 62 | ["donate", "https://i.imgur.com/qyFYSY2.png"], 63 | ["server-invite", "https://i.imgur.com/lUXjLAu.png"] 64 | ]; 65 | 66 | // PROC: check if sectionals already exist, if not add them to database. 67 | if (await collectionExist("config/sectionals")) 68 | return message.channel.send(`\`[ℹ️]: Sectionals were initialized once. If you wish to do so again, first delete existing data from database.\``); 69 | await addSectionFieldsDB(sections); 70 | // PROC: get newly saved data in JSON from database. 71 | const savedDataJSON = (await getAllData()).toJSON(); 72 | // PROC: default channel to send messages. 73 | const infoChannel = message.guild.channels.find(ch => ch.id == config.getChannel("information")) 74 | // PROC: Send messages in Information channel 75 | for (sectionName of Object.keys(savedDataJSON)) { 76 | if (!savedDataJSON[sectionName]["content_message_id"]) { 77 | const headerMsg = await infoChannel.send({ files: [savedDataJSON[sectionName]["img_header_url"]] }); 78 | const contentMsg = await headerMsg.channel.send(`${sectionName} `); 79 | // save sent messages ID for future edits 80 | await db.updateData(`/config/sectionals/${sectionName}`, { 81 | "content_message_id": contentMsg.id, 82 | "header_message_id": headerMsg.id 83 | }); 84 | } 85 | } 86 | 87 | } 88 | 89 | /** listSections: Sends a list of sections in database. */ 90 | async function listSections(message, args) { 91 | const data = (await getAllData()).toJSON(); 92 | return message.channel.send(`Server has following sections: \`${Object.keys(data).join(", ")}\``); 93 | } 94 | 95 | /** updateContent: update message content for a section. */ 96 | async function updateContent(message, args) { 97 | 98 | const newContent = args.slice(2, args.length).join(" "); 99 | const sectionObject = (await getAllData()).toJSON()[args[1]]; // args[1]: name of the section 100 | if (!sectionObject) 101 | return message.channel.send(`\`[❌]: Cannot find section "${args[1]}" in database.\``); 102 | 103 | const info_channel = message.guild.channels.find(ch => ch.id === config.getChannel("information")); 104 | const discord_message = await info_channel.fetchMessage(sectionObject["content_message_id"]); 105 | // PROC: edit message with new content 106 | await discord_message.edit(newContent); 107 | // PROC: update message content in firebase db 108 | await updateSection(args[1], { "message_content": newContent }); 109 | } 110 | 111 | /** updateHeader: set/update header url of a section. */ 112 | async function updateHeader(message, args) { 113 | 114 | const headerURL = args[2]; 115 | // sectionObject: as stored in firebase db 116 | const sectionObject = (await getAllData()).toJSON()[args[1]]; // args[1]: name of the section 117 | if (!sectionObject) 118 | return message.channel.send(`\`[❌]: Cannot understand section name "${args[1]}"\``); 119 | // PROC: get previously sent discord message for edit. 120 | const discord_message = await info_channel.fetchMessage(sectionObject["header_message_id"]); 121 | const info_channel = message.guild.channels.find(ch => ch.id === config.getChannel("information")); 122 | // PROC: edit message with new Header URL 123 | await discord_message.edit({ files: [headerURL] }); 124 | // PROC: update message content in firebase db, args[1]: section name 125 | await updateSection(args[1], { "img_header_url": headerURL }); 126 | } 127 | 128 | /** deleteSection: completely delete section from database and discord. */ 129 | async function deleteSection(message, args) { 130 | 131 | const sectionName = args[1]; 132 | // PROC: check if sectioName not exist 133 | if (!collectionExist(`config/sectionals/${sectionName}`)) 134 | return message.channel.send(`\`[ℹ️]: Cannot delete "${sectionName}" -- It does not exist.\``); 135 | // PROC: retrive existing section data and delete messages from discord channel and database 136 | const sectionData = (await getAllData()).toJSON()[sectionName]; 137 | const info_channel = message.guild.channels.find(ch => ch.id === config.getChannel("information")); 138 | const discord_message_content = await info_channel.fetchMessage(sectionData["content_message_id"]); 139 | const discord_message_header = await info_channel.fetchMessage(sectionData["header_message_id"]); 140 | await discord_message_content.delete(); 141 | await discord_message_header.delete(); 142 | await db.deleteData(`config/sectionals/${sectionName}`); 143 | 144 | } 145 | 146 | /** reloadSections: delete and re-send sectionals on discord. */ 147 | async function reloadSections(message, args) { 148 | /** 149 | * NOTE: In case if the changes does not display 150 | * on discord, user can pass this option 151 | * to re-upload sections with new data from 152 | * database. 153 | */ 154 | const sectionObject = (await getAllData()).toJSON(); 155 | const infoChannel = message.guild.channels.find(ch => ch.id === config.getChannel("information")); 156 | 157 | for (section of Object.keys(sectionObject)) { 158 | // PROC: delete existing messages 159 | let discord_message_content = await infoChannel.fetchMessage(sectionObject[section]["content_message_id"]); 160 | let discord_message_header = await infoChannel.fetchMessage(sectionObject[section]["header_message_id"]); 161 | await discord_message_content.delete(); 162 | await discord_message_header.delete(); 163 | // PROC: send messages with new data 164 | const msg = await infoChannel.send({ files: [sectionObject[section]["img_header_url"]] }) 165 | const msg2 = await msg.channel.send(sectionObject[section]["message_content"] || ""); 166 | // PROC: update database with new message IDs. 167 | await db.updateData(`/config/sectionals/${section}`, { 168 | "content_message_id": msg2.id, 169 | "header_message_id": msg.id 170 | }) 171 | } 172 | } 173 | 174 | /** showHelp: Display manual for this command. */ 175 | function showHelp(message, args) { 176 | /** TODO: move to manuals directory. */ 177 | message.channel.send(` 178 | \`\`\` 179 | NAME 180 | !sectional -- set/delete/update section content in information channel. 181 | 182 | RUN LEVEL 183 | 0: Admin 184 | Only Admins can run this command. 185 | 186 | OPTIONS 187 | --init initialize sections in database and channel 188 | --list list all existing sections 189 | --update-content set section content of a section 190 | --update-header set section header image url 191 | --delete completely remove section 192 | --reload re-upload sections with new data 193 | --help or -h display manual for this command 194 | 195 | EXAMPLES 196 | If you want to update section's text content: 197 | >>!sectional --update-content donate https://codecareer.io/donate 198 | 199 | Use codeblock, bold, italic etc. to beautify content 200 | >>!sectional --update-content community-rules 201 | 1. Rule ... 202 | 2. Rule ... 203 | 204 | 205 | Delete a section 206 | >>!sectional --delete server-info 207 | 208 | NOTE 209 | If you want to update header image, first update image url with: 210 | >>!sectional --update-header server-info http://img.com/myImg 211 | Then reload the sections to see updates 212 | >>!sectional --reload 213 | 214 | \`\`\` 215 | `); 216 | 217 | } 218 | 219 | /** --------------- 220 | * HELPER FUNCTIONS 221 | * ----------------*/ 222 | 223 | 224 | /* addSectionalFields: update db with empty section fields. */ 225 | async function addSectionFieldsDB(sections) { 226 | 227 | const updateObj = {}; 228 | for (section of sections) { 229 | updateObj[section[0]] = { 230 | "img_header_url": section[1], 231 | "header_message_id": "", 232 | "content_message_id": "", 233 | "message_content": "" 234 | } 235 | } 236 | 237 | await db.updateData(`/config/sectionals`, updateObj); 238 | } 239 | 240 | /* getAllData: return sectional collection object. */ 241 | async function getAllData() { 242 | const ref = await db.getDatabase().ref('/config/sectionals'); 243 | const data = await ref.once("value"); 244 | if (data.val() === null) 245 | return null; 246 | return data; 247 | } 248 | 249 | /** updateSection: update a section with new data(obj). */ 250 | async function updateSection(sectionName, obj) { 251 | const ref = await db.getDatabase().ref(`/config/sectionals/${sectionName}`); 252 | const responseData = await ref.once("value"); 253 | if (responseData.val() === null) 254 | return console.log("`ERROR: No such section exists ... `"); 255 | await db.updateData(`/config/sectionals/${sectionName}`, obj); 256 | } 257 | 258 | /** collectionExist: check if requested collection exist in database. */ 259 | async function collectionExist(path) { 260 | const ref = await db.getDatabase().ref(path); 261 | const snapshot = await ref.once("value"); 262 | return snapshot.exists(); 263 | } -------------------------------------------------------------------------------- /commands/setchannel.js: -------------------------------------------------------------------------------- 1 | const Auth = require('../utils/auth') 2 | const Config = require('../utils/config') 3 | 4 | exports.run = (message, args) => { 5 | 6 | if (Auth.isOwner(message.member)) { 7 | if (!args || args.length < 1) return 8 | 9 | let channelType = args[0].toLowerCase().trim() 10 | 11 | message.mentions ? message.mentions.channels.map( mention => { 12 | Config.setChannel(channelType, mention.id) 13 | }) : message.reply("You need to tag a channel") 14 | 15 | message.reply("Config updated") 16 | 17 | } else { 18 | message.reply('You must be the guild owner in order to run this command.'); 19 | } 20 | 21 | }; -------------------------------------------------------------------------------- /commands/setrole.js: -------------------------------------------------------------------------------- 1 | const Auth = require('../utils/auth') 2 | const Config = require('../utils/config') 3 | 4 | exports.run = (message, args) => { 5 | 6 | if (Auth.isOwner(message.member)) { 7 | if(!args || args.length < 1) return 8 | 9 | let roleType = args[0].toLowerCase().trim() 10 | 11 | message.mentions ? message.mentions.roles.map( mention => { 12 | Config.setRole(roleType, mention.id) 13 | }) : message.reply("You need to tag a role") 14 | 15 | message.reply("Config updated") 16 | 17 | } else { 18 | message.reply('You must be the guild owner in order to run this command.'); 19 | } 20 | 21 | }; -------------------------------------------------------------------------------- /commands/ticket.js: -------------------------------------------------------------------------------- 1 | const Discord = require("discord.js"); 2 | const Config = require('../utils/config'); 3 | 4 | exports.run = (message, args) => { 5 | 6 | if(!args || args.length < 1) return message.reply(`, you must type your question after the command. You may also use "!ticket PRIVATE" if you would like your question to remain confidential.`); 7 | 8 | const request = args.join(' '); 9 | 10 | try { 11 | 12 | const member_embed = new Discord.RichEmbed() 13 | .setTitle("CodeCareer Assistance") 14 | .setDescription(`You have requested personalized assistance. A CodeCareer staff member has been assigned to this request and will DM you within 24 hours. Please make sure to have your DM settings open to other server members.`) 15 | .addField("Request", request, false) 16 | .addField("Date of Request", message.createdAt, true); 17 | 18 | message.author.send({embed: member_embed}) 19 | 20 | const assignee = message.guild.roles.get(Config.getRole(role_type="admin")).members.random(1) 21 | 22 | const staff_embed = new Discord.RichEmbed() 23 | .setTitle("CodeCareer Assistance") 24 | .setDescription(`${message.author.toString()} has requested personalized assistance.`) 25 | .addField("Request", request, false) 26 | .addField("Assignee", `${assignee.toString()}`, true) 27 | .addField("Date of Request", message.createdAt, true); 28 | 29 | const channel = message.guild.channels.get(Config.getChannel("tickets")) 30 | channel.send({embed: staff_embed}) 31 | 32 | message.delete() 33 | 34 | } catch(e) { 35 | console.error(e) 36 | } 37 | 38 | }; -------------------------------------------------------------------------------- /data/commands.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "command": "announce", 4 | "description": "For staff members to easily make server-wide announcements as the bot.", 5 | "permissionLevel": "mod", 6 | "arguments": [ 7 | "announcement" 8 | ], 9 | "active": true 10 | }, 11 | { 12 | "command": "help", 13 | "description": "Display a list of commands and other information on how to use the bot.", 14 | "permissionLevel": "all", 15 | "arguments": [ 16 | "--[user|staff|mod|admin|owner]" 17 | ], 18 | "active": true 19 | }, 20 | { 21 | "command": "poll", 22 | "description": "For staff members to easily post a reaction poll targeted at the community.", 23 | "permissionLevel": "mod", 24 | "arguments": [ 25 | "question" 26 | ], 27 | "active": true 28 | }, 29 | { 30 | "command": "reload", 31 | "description": "Reload a command file. Useful for development purposes only.", 32 | "permissionLevel": "owner", 33 | "arguments": [ 34 | "command name" 35 | ], 36 | "active": true 37 | }, 38 | { 39 | "command": "resetconfig", 40 | "description": "Reset the entire configuration in Firebase. Be careful with this!", 41 | "permissionLevel": "owner", 42 | "arguments": [], 43 | "active": true 44 | }, 45 | { 46 | "command": "setchannel", 47 | "description": "Mark a tagged channel as a specific type", 48 | "permissionLevel": "admin", 49 | "arguments": [ 50 | "channel type", 51 | "tagged channel" 52 | ], 53 | "active": true 54 | }, 55 | { 56 | "command": "setrole", 57 | "description": "Mark a tagged role as a specific permission level", 58 | "permissionLevel": "owner", 59 | "arguments": [ 60 | "permission level", 61 | "tagged role" 62 | ], 63 | "active": true 64 | }, 65 | { 66 | "command": "ticket", 67 | "description": "Opens a support ticket with a randomly assigned staff member. You will be messaged by the bot privately.", 68 | "permissionLevel": "all", 69 | "arguments": [ 70 | "enquiry" 71 | ], 72 | "active": true 73 | }, 74 | { 75 | "command": "sectional", 76 | "description": "A complex command to create/read/update/delete sectionals in the information channel.", 77 | "permissionLevel": "mod", 78 | "arguments": [ 79 | "-h" 80 | ], 81 | "active": true 82 | }, 83 | { 84 | "command": "define", 85 | "description": "Provides definition for words from the web.", 86 | "permissionLevel": "all", 87 | "arguments": [ 88 | "term" 89 | ], 90 | "active": true 91 | }, 92 | { 93 | "command": "quote", 94 | "description": "A fun command to display a random moditvational quote and a random gif", 95 | "permissionLevel": "all", 96 | "arguments": [], 97 | "active": true 98 | } 99 | ] -------------------------------------------------------------------------------- /ecosystem.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | /** 3 | * Application configuration section 4 | * http://pm2.keymetrics.io/docs/usage/application-declaration/ 5 | */ 6 | apps : [ 7 | { 8 | name : 'cc-bot', 9 | script : 'index.js', 10 | interpreter: 'node@10.16.0', 11 | env: { 12 | NODE_ENV: "development" 13 | }, 14 | env_production : { 15 | NODE_ENV: 'production' 16 | } 17 | } 18 | ], 19 | }; 20 | -------------------------------------------------------------------------------- /events/guildMemberAdd.js: -------------------------------------------------------------------------------- 1 | const Config = require('../utils/config'); 2 | 3 | exports.run = (member) => { 4 | 5 | // Send the message to a designated channel on a server: 6 | const channel = member.guild.channels.get(Config.getChannel("joins")); 7 | 8 | // Do nothing if the channel wasn't found on this server 9 | if (!channel) return; 10 | 11 | // Send the message, mentioning the member 12 | channel.send(`Welcome to the server, ${member}`); 13 | 14 | }; -------------------------------------------------------------------------------- /events/message.js: -------------------------------------------------------------------------------- 1 | const Config = require('../utils/config'); 2 | const path = require('path') 3 | 4 | exports.run = (msg) => { 5 | 6 | if (msg.author.bot) 7 | return; 8 | 9 | if(msg.content.indexOf(Config.getCommandPrefix()) !== 0) 10 | return; 11 | 12 | const args = msg.content.slice(Config.getCommandPrefix().length).trim().split(/ +/g); 13 | const command = args.shift().toLowerCase().replace('/', ''); 14 | try { 15 | let commandFile = require(path.join(__dirname, '..', 'commands', `${command}.js`)) 16 | commandFile.run(msg, args); 17 | } catch (err) { 18 | console.error([ 19 | "❌ : command '" + command + "' does not exist\n", 20 | `${err}`.red 21 | ].join('')); 22 | } 23 | 24 | }; -------------------------------------------------------------------------------- /events/ready.js: -------------------------------------------------------------------------------- 1 | const bot = require('../utils/client').getClient() 2 | 3 | exports.run = async function () { 4 | console.log(`Logged in as ${bot.user.tag.black.bgYellow}`); 5 | // set activity 6 | const presense = await bot.user.setActivity("codecareer.io"); 7 | console.log(`Activity set to ${presense.game ? presense.game.name.black.bgYellow : "none"}`) 8 | } 9 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // Load environment variables with dotenv 2 | require('dotenv').config(); 3 | 4 | require('colors'); 5 | 6 | const moduleAlias = require('module-alias'); 7 | 8 | moduleAlias.addAliases({ 9 | '@root' : __dirname, 10 | '@utils': __dirname + '/utils', 11 | '@events': __dirname + '/events', 12 | '@commands': __dirname + '/commands' 13 | }); 14 | 15 | // Load config utility 16 | require('./utils/config') 17 | 18 | // Require the bot client utility class and login 19 | require('./utils/client').login() 20 | 21 | // Require the api web server client utility class and start listening 22 | require('./utils/server').startListening() 23 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | module.exports = { 5 | // All imported modules in your tests should be mocked automatically 6 | // automock: false, 7 | 8 | // Stop running tests after `n` failures 9 | // bail: 0, 10 | 11 | // Respect "browser" field in package.json when resolving modules 12 | // browser: false, 13 | 14 | // The directory where Jest should store its cached dependency information 15 | // cacheDirectory: "/private/var/folders/p6/jphk387d57n6x6n2q65b483c0000gp/T/jest_dy", 16 | 17 | // Automatically clear mock calls and instances between every test 18 | clearMocks: true, 19 | 20 | // Indicates whether the coverage information should be collected while executing the test 21 | // collectCoverage: false, 22 | 23 | // An array of glob patterns indicating a set of files for which coverage information should be collected 24 | // collectCoverageFrom: null, 25 | 26 | // The directory where Jest should output its coverage files 27 | // coverageDirectory: null, 28 | 29 | // An array of regexp pattern strings used to skip coverage collection 30 | // coveragePathIgnorePatterns: [ 31 | // "/node_modules/" 32 | // ], 33 | 34 | // A list of reporter names that Jest uses when writing coverage reports 35 | // coverageReporters: [ 36 | // "json", 37 | // "text", 38 | // "lcov", 39 | // "clover" 40 | // ], 41 | 42 | // An object that configures minimum threshold enforcement for coverage results 43 | // coverageThreshold: null, 44 | 45 | // A path to a custom dependency extractor 46 | // dependencyExtractor: null, 47 | 48 | // Make calling deprecated APIs throw helpful error messages 49 | // errorOnDeprecated: false, 50 | 51 | // Force coverage collection from ignored files usin a array of glob patterns 52 | // forceCoverageMatch: [], 53 | 54 | // A path to a module which exports an async function that is triggered once before all test suites 55 | // globalSetup: null, 56 | 57 | // A path to a module which exports an async function that is triggered once after all test suites 58 | // globalTeardown: null, 59 | 60 | // A set of global variables that need to be available in all test environments 61 | // globals: {}, 62 | 63 | // An array of directory names to be searched recursively up from the requiring module's location 64 | // moduleDirectories: [ 65 | // "node_modules" 66 | // ], 67 | 68 | // An array of file extensions your modules use 69 | // moduleFileExtensions: [ 70 | // "js", 71 | // "json", 72 | // "jsx", 73 | // "ts", 74 | // "tsx", 75 | // "node" 76 | // ], 77 | 78 | // A map from regular expressions to module names that allow to stub out resources with a single module 79 | // moduleNameMapper: {}, 80 | 81 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 82 | // modulePathIgnorePatterns: [], 83 | 84 | // Activates notifications for test results 85 | // notify: false, 86 | 87 | // An enum that specifies notification mode. Requires { notify: true } 88 | // notifyMode: "failure-change", 89 | 90 | // A preset that is used as a base for Jest's configuration 91 | // preset: null, 92 | 93 | // Run tests from one or more projects 94 | // projects: null, 95 | 96 | // Use this configuration option to add custom reporters to Jest 97 | // reporters: undefined, 98 | 99 | // Automatically reset mock state between every test 100 | // resetMocks: false, 101 | 102 | // Reset the module registry before running each individual test 103 | // resetModules: false, 104 | 105 | // A path to a custom resolver 106 | // resolver: null, 107 | 108 | // Automatically restore mock state between every test 109 | // restoreMocks: false, 110 | 111 | // The root directory that Jest should scan for tests and modules within 112 | // rootDir: null, 113 | 114 | // A list of paths to directories that Jest should use to search for files in 115 | // roots: [ 116 | // "" 117 | // ], 118 | 119 | // Allows you to use a custom runner instead of Jest's default test runner 120 | // runner: "jest-runner", 121 | 122 | // The paths to modules that run some code to configure or set up the testing environment before each test 123 | // setupFiles: [], 124 | 125 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 126 | // setupFilesAfterEnv: [], 127 | 128 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 129 | // snapshotSerializers: [], 130 | 131 | // The test environment that will be used for testing 132 | testEnvironment: "node", 133 | 134 | // Options that will be passed to the testEnvironment 135 | // testEnvironmentOptions: {}, 136 | 137 | // Adds a location field to test results 138 | // testLocationInResults: false, 139 | 140 | // The glob patterns Jest uses to detect test files 141 | // testMatch: [ 142 | // "**/__tests__/**/*.[jt]s?(x)", 143 | // "**/?(*.)+(spec|test).[tj]s?(x)" 144 | // ], 145 | 146 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 147 | // testPathIgnorePatterns: [ 148 | // "/node_modules/" 149 | // ], 150 | 151 | // The regexp pattern or array of patterns that Jest uses to detect test files 152 | // testRegex: [], 153 | 154 | // This option allows the use of a custom results processor 155 | // testResultsProcessor: null, 156 | 157 | // This option allows use of a custom test runner 158 | // testRunner: "jasmine2", 159 | 160 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 161 | // testURL: "http://localhost", 162 | 163 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 164 | // timers: "real", 165 | 166 | // A map from regular expressions to paths to transformers 167 | // transform: null, 168 | 169 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 170 | // transformIgnorePatterns: [ 171 | // "/node_modules/" 172 | // ], 173 | 174 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 175 | // unmockedModulePathPatterns: undefined, 176 | 177 | // Indicates whether each individual test should be reported during the run 178 | // verbose: null, 179 | 180 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 181 | // watchPathIgnorePatterns: [], 182 | 183 | // Whether to use watchman for file crawling 184 | // watchman: true, 185 | }; 186 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "codecareer-discord-bot", 3 | "version": "1.0.0", 4 | "description": "CodeCareer Discord Bot", 5 | "main": "index.js", 6 | "repository": "https://github.com/GitCodeCareer/discord-bot.git", 7 | "author": "CodeCareer.org", 8 | "license": "MIT", 9 | "private": true, 10 | "scripts": { 11 | "test": "nodemon --exec jest --silent", 12 | "watch": "nodemon index.js", 13 | "start": "node index.js" 14 | }, 15 | "dependencies": { 16 | "axios": "^0.19.2", 17 | "discord.js": "^11.6.3", 18 | "dotenv": "^8.2.0", 19 | "firebase": "^7.14.1", 20 | "handlebars": "^4.7.6", 21 | "module-alias": "^2.2.2", 22 | "node-fetch": "2.6.1", 23 | "request": "^2.88.2", 24 | "request-promise-native": "^1.0.8" 25 | }, 26 | "devDependencies": { 27 | "colors": "^1.4.0", 28 | "jest": "^25.3.0", 29 | "nodemon": "^2.0.3" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /server/routes.js: -------------------------------------------------------------------------------- 1 | const client = require('../utils/client').getClient() 2 | const { view } = require('../utils') 3 | 4 | module.exports = [ 5 | { 6 | path: `/`, 7 | middleware: ['web'], 8 | method: () => { 9 | return view('index', { test: "hello world" }); 10 | } 11 | }, 12 | { 13 | path: `/member-count`, 14 | middleware: ['api'], 15 | method: () => { 16 | return { 17 | memberCount: client.guilds.first().memberCount 18 | } 19 | } 20 | } 21 | ] -------------------------------------------------------------------------------- /server/templates/email/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitCodeCareer/discord-bot/5dd3d07a7d8cddf403d5299dda1fc7947d9a4ebd/server/templates/email/.gitkeep -------------------------------------------------------------------------------- /server/templates/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | CodeCareer Bot API 4 | 5 | 6 |

CodeCareer Bot API

7 |

You have landed on the base route for the CodeCareer.io bot api.

8 | {{test}} 9 | 10 | -------------------------------------------------------------------------------- /tests/config.test.js: -------------------------------------------------------------------------------- 1 | const Config = require('../utils/config'); 2 | 3 | test('bot token returns string value', () => { 4 | expect(Config.getBotToken()).toMatch(/[\w\d.]+/) 5 | }) 6 | 7 | test('command prefix returns string value of length 1', () => { 8 | expect(Config.getCommandPrefix()).toHaveLength(1) 9 | }) -------------------------------------------------------------------------------- /tests/quote.test.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | 3 | jest.mock('axios'); 4 | 5 | async function getFirstQuote() { 6 | const response = await axios.get('http://quotes.stormconsultancy.co.uk/random.json'); 7 | return response.data[0].quote; 8 | } 9 | 10 | const quote = [ 11 | 'CodeCareer', 12 | 'RandomQuote', 13 | 'QuoteOfTheDay', 14 | 'Giphy', 15 | 'Nasa', 16 | ]; 17 | 18 | test('The quote command has CodeCareer on it', () => { 19 | expect(quote).toContain('CodeCareer'); 20 | expect(new Set(quote)).toContain('CodeCareer'); 21 | }); 22 | 23 | test('There is a "https" in CodeCareer', () => { 24 | expect('https://CodeCareer.io').toMatch(/https/); 25 | }); 26 | 27 | test('The Ultimate Question of Life, the Universe and Everything', () => { 28 | const value = 6 + 6 + 6 + 6 + 6 + 6 + 6; 29 | expect(value).toBeGreaterThan(41); 30 | expect(value).toBeLessThan(43); 31 | 32 | // toBe and toEqual are equivalent for numbers 33 | expect(21 + 21).toBe(42); 34 | expect(value).toBe(42); 35 | expect(value).toEqual(42); 36 | }); 37 | 38 | it('returns the quote of the first randomquote', async () => { 39 | axios.get.mockResolvedValue({ 40 | data: [ 41 | { 42 | author: 'C. A. R. Hoare', 43 | id: 1, 44 | quote: 'We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil.' 45 | }, 46 | { 47 | author: 'Edward V Berard', 48 | id: 2, 49 | quote: 'Walking on water and developing software from a specification are easy if both are frozen.' 50 | } 51 | ] 52 | }); 53 | 54 | const quote = await getFirstQuote(); 55 | expect(quote).toEqual('We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil.'); 56 | }); -------------------------------------------------------------------------------- /utils/auth.js: -------------------------------------------------------------------------------- 1 | const Config = require('./config'); 2 | 3 | class Auth { 4 | constructor() {} 5 | 6 | isOwner(member) { 7 | return member.guild.owner.id === member.id 8 | } 9 | 10 | isAdmin(member) { 11 | return member.roles.find((val) => val.id === Config.getRole('admin')); 12 | } 13 | 14 | isMod(member) { 15 | return member.roles.find((val) => val.id === Config.getRole('mod')); 16 | } 17 | 18 | isRole(role_type) { 19 | return member.roles.find((val) => val.id === Config.getRole(role_type)); 20 | } 21 | } 22 | 23 | module.exports = new Auth(); 24 | -------------------------------------------------------------------------------- /utils/client.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const Discord = require('discord.js'); 4 | const Config = require('./config'); 5 | 6 | class BotClient { 7 | constructor() { 8 | this.client = new Discord.Client(); 9 | 10 | fs.readdir(path.join(__dirname, '..', 'events'), (err, files) => { 11 | if (err) throw err; 12 | files.forEach(file => { 13 | let eventName = file.split('.')[0]; 14 | let eventFile = require(`../events/${file}`); 15 | this.client.on(eventName, object => eventFile.run(object)); 16 | }); 17 | }); 18 | } 19 | 20 | getClient() { 21 | return this.client; 22 | } 23 | 24 | login() { 25 | this.client 26 | .login(Config.getBotToken()) 27 | .then(function(token) { 28 | console.log([ 29 | `Login Successful`.green, 30 | `: Bot is now connected.` 31 | ].join('')) 32 | }) 33 | .catch(function(err) { 34 | console.log([ 35 | "Login failed".red, 36 | "\n❌ : There were some problems during logging to discord.\n", 37 | `${err}`.red 38 | ].join('')); 39 | }); 40 | } 41 | } 42 | 43 | module.exports = new BotClient(); 44 | -------------------------------------------------------------------------------- /utils/config.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const DB = require('./db') 3 | 4 | class Config { 5 | 6 | constructor() { 7 | 8 | this.config = {} 9 | this.configRef = null 10 | this.getConfig() 11 | 12 | } 13 | 14 | initConfig() { 15 | this.setCommandPrefix(process.env.CMD_PREFIX) 16 | this.reload() 17 | console.info("Set the other configuration options via bot commands in the Discord server. Bot will not function until they are set.") 18 | } 19 | 20 | async resetConfig() { 21 | await DB.deleteData('config') 22 | this.initConfig() 23 | } 24 | 25 | reload() { 26 | this.getConfig() 27 | } 28 | 29 | async saveConfig() { 30 | await DB.writeData('config', this.config) 31 | } 32 | 33 | getBotToken() { 34 | return process.env.DISCORD_BOT_TOKEN 35 | } 36 | 37 | getServerPort() { 38 | return process.env.WEB_SERVER_PORT || 3000 39 | } 40 | 41 | getApiRoutePrefix() { 42 | let routePrefix = process.env.API_ROUTE_PREFIX.startsWith('/') 43 | ? process.env.API_ROUTE_PREFIX.slice(1) 44 | : process.env.API_ROUTE_PREFIX; 45 | return routePrefix || 'codecareer' 46 | } 47 | 48 | getRef() { 49 | return this.configRef 50 | } 51 | 52 | async getConfig() { 53 | this.configRef = await DB.startListening('config') 54 | let data = await this.configRef.once('value') 55 | 56 | if (data.val() === null) { 57 | console.log("Initial loading of data from Firebase") 58 | this.initConfig() 59 | } else { 60 | this.config = data.val() 61 | this.saveConfig() 62 | } 63 | 64 | } 65 | 66 | getCommandPrefix() { 67 | return this.config.cmdPrefix 68 | // let data = await this.configRef.once('value') 69 | // return data.val().cmdPrefix 70 | } 71 | 72 | getChannel(name) { 73 | return this.config.channels[name]; 74 | // let data = await this.configRef.once('value') 75 | // return data.val().channels[name] 76 | } 77 | 78 | getRole(roleType) { 79 | return this.config.roles[roleType]; 80 | // let data = await this.configRef.once('value') 81 | // return data.val().roles[roleType] 82 | } 83 | 84 | // Setters 85 | 86 | async setCommandPrefix(prefix) { 87 | //this.config.commandPrefix = prefix; 88 | //this.saveConfig(); 89 | await DB.writeData('config/cmdPrefix', prefix) 90 | this.reload() 91 | } 92 | 93 | async setChannel(channel_type, id) { 94 | // this.config.channels[channel_type] = id; 95 | // this.saveConfig(); 96 | await DB.writeData(`config/channels/${channel_type}`, id) 97 | this.reload() 98 | } 99 | 100 | async setRole(role_type, id) { 101 | // this.config.roles[role_type] = id; 102 | // this.saveConfig(); 103 | await DB.writeData(`config/roles/${role_type}`, id) 104 | this.reload() 105 | } 106 | } 107 | 108 | module.exports = new Config(); 109 | -------------------------------------------------------------------------------- /utils/db.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const firebase = require('firebase/app'); 4 | require('firebase/database'); 5 | 6 | /* env variables required here for testing purposes */ 7 | require('dotenv').config(); 8 | 9 | class DB { 10 | constructor() { 11 | let firebaseConfig = { 12 | apiKey: process.env.FIREBASE_API_KEY, 13 | authDomain: process.env.FIREBASE_AUTH_DOMAIN, 14 | databaseURL: process.env.FIREBASE_DB_URL, 15 | storageBucket: process.env.FIREBASE_STORAGE_BUCKET 16 | }; 17 | firebase.initializeApp(firebaseConfig); 18 | this.database = firebase.database(); 19 | } 20 | 21 | getDatabase() { 22 | return this.database; 23 | } 24 | 25 | startListening(path, eventType = 'value') { 26 | let ref = this.database.ref(path); 27 | ref.on(eventType, this.logData, this.errorData); 28 | console.log(`Firebase: Now listening for updates at /${path}`.magenta); 29 | return ref; 30 | } 31 | 32 | stopListening(path = null, eventType = null) { 33 | this.database.ref(path).off(eventType); 34 | console.log(`Firebase: Stopped listening for updates at /${path}`.magenta); 35 | } 36 | 37 | /* firebase database runtime logs are saved in firebase.log file. 38 | Log files can be found at ./logs */ 39 | logData(data) { 40 | fs.appendFileSync( 41 | path.join(__dirname, '../logs/firebase.log'), 42 | JSON.stringify(data.val()) + '\n', 43 | err => { 44 | if (err) throw err; 45 | console.log(`Firebase: logged message successfully.`.green); 46 | } 47 | ); 48 | } 49 | 50 | /* firebase database errors are saved in firebase.error.log file 51 | Log files can be found at ./logs */ 52 | errorData(data) { 53 | fs.appendFileSync( 54 | path.join(__dirname, '../logs/firebase.error.log'), 55 | JSON.stringify(data.val()) + '\n', 56 | err => { 57 | if (err) throw err; 58 | console.log( 59 | `An error occured while listening to database.\nLog can be found at ${path.join( 60 | __dirname, 61 | '../logs/firebase.error.log' 62 | )}`.red 63 | ); 64 | } 65 | ); 66 | } 67 | 68 | async writeData(path, obj) { 69 | await this.getDatabase() 70 | .ref(path) 71 | .set(obj); 72 | } 73 | 74 | async updateData(path, obj) { 75 | await this.getDatabase() 76 | .ref(path) 77 | .update(obj); 78 | } 79 | 80 | async appendData(path, obj) { 81 | await this.getDatabase() 82 | .ref(path) 83 | .push(obj); 84 | } 85 | 86 | async deleteData(path) { 87 | await this.getDatabase() 88 | .ref(path) 89 | .remove(); 90 | } 91 | } 92 | 93 | module.exports = new DB(); 94 | -------------------------------------------------------------------------------- /utils/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | auth: require("./auth"), 3 | client: require("./client"), 4 | config: require("./config"), 5 | db: require("./db"), 6 | server: require("./server"), 7 | view: require("./view") 8 | } -------------------------------------------------------------------------------- /utils/server.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const http = require('http') 4 | const Config = require('./config') 5 | 6 | class ApiServer { 7 | constructor() { 8 | this.server = http.createServer((req, res) => { 9 | 10 | let headers = { 11 | 'Content-Type': 'application/json', 12 | 'Access-Control-Allow-Origin': '*', 13 | 'Access-Control-Allow-Methods': 'GET', 14 | 'Access-Control-Max-Age': 2592000, 15 | } 16 | 17 | res.writeHead(200, headers) 18 | 19 | let routes = this.getRoutes(); 20 | let routePrefix = Config.getApiRoutePrefix() 21 | 22 | routes.map(route => { 23 | let routePath = route.path.startsWith('/') 24 | ? route.path.slice(1) 25 | : route.path; 26 | let fullPath = `/${routePrefix}/${routePath}`; 27 | if (req.url === fullPath) { this.handleResponse(res, route) } 28 | }) 29 | }) 30 | } 31 | 32 | handleResponse(res, route) { 33 | let contentHeader = { 34 | 'Access-Control-Allow-Origin': '*', 35 | 'Access-Control-Allow-Methods': 'GET', 36 | 'Access-Control-Max-Age': 2592000, 37 | } 38 | 39 | route.middleware.includes('web') ? 40 | contentHeader['Content-Type'] = 'text/html' : 41 | contentHeader['Content-Type'] = 'application/json'; 42 | 43 | res.writeHead(200, contentHeader) 44 | let body = route.middleware.includes('web') 45 | ? route.method().toString() 46 | : JSON.stringify(route.method()) 47 | res.write(body) 48 | res.end() 49 | } 50 | 51 | getServer() { 52 | return this.server 53 | } 54 | 55 | getRoutes() { 56 | return require(path.join(__dirname, '..', 'server', 'routes.js')) 57 | } 58 | 59 | startListening() { 60 | this.getServer().listen(Config.getServerPort(), function () { 61 | console.log( 62 | [`API web server started and listening on port `.yellow, `${Config.getServerPort()}`.magenta.bold].join('') 63 | ) 64 | }) 65 | } 66 | } 67 | 68 | module.exports = new ApiServer() -------------------------------------------------------------------------------- /utils/view.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const fs = require('fs') 3 | const handlebars = require('handlebars') 4 | const { Response } = require('http') 5 | 6 | class View { 7 | constructor(name, variables, email) { 8 | this.templateName = name.endsWith('.html') ? name.replace('.html', '') : name; 9 | this.templateDir = path.join(__dirname, '..', 'server', 'templates') 10 | this.templatePath = path.join(this.templateDir, email ? 'email' : 'web', `${this.templateName}.html`) 11 | 12 | this.renderedView = this.renderView(this.templatePath, variables) 13 | } 14 | 15 | renderView(path, variables) { 16 | let html = fs.readFileSync(path) 17 | let template = handlebars.compile(html.toString()) 18 | return template(variables) 19 | } 20 | 21 | returnView() { 22 | return this.renderedView 23 | } 24 | } 25 | 26 | module.exports = (name, variables={}, email=false) => { 27 | const v = new View(name, variables, email) 28 | return v.returnView() 29 | } --------------------------------------------------------------------------------