├── .eslintrc ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── app.js ├── bin ├── .gitignore ├── auth.sh ├── camperbot-run.sh ├── credentials-demobot.sh ├── credentials-example.sh ├── debug.sh ├── deploy.sh ├── force-update.sh ├── git-clean.sh ├── mocha-debug.sh ├── pm2-run.sh ├── pm2-update.sh ├── post.sh ├── run.sh ├── step-debug.sh ├── update-run-dev.sh ├── wiki-pull-prod.sh ├── wiki-pull.sh └── wiki-update.sh ├── config └── AppConfig.js ├── data ├── RoomData.js ├── rooms │ └── RoomMessages.js └── seed │ ├── bonfireMDNlinks.js │ └── challenges │ ├── advanced-bonfires.json │ ├── basejumps.json │ ├── basic-bonfires.json │ ├── basic-ziplines.json │ ├── expert-bonfires.json │ └── intermediate-bonfires.json ├── dot-EXAMPLE.env ├── example.config.json ├── gulpfile.js ├── lib ├── app │ ├── Bonfires.js │ └── Rooms.js ├── bot │ ├── BotCommands.js │ ├── GBot.js │ ├── InputWrap.js │ ├── KBase.js │ └── cmds │ │ └── thanks.js ├── gitter │ ├── GitterHelper.js │ ├── restApi.js │ └── streamApi.js └── utils │ ├── HttpWrap.js │ ├── TextLib.js │ └── Utils.js ├── logs └── README.md ├── package-lock.json ├── package.json └── test ├── AppConfig.spec.js ├── Commands.spec.js ├── GBot.spec.js ├── GitterHelper.spec.js ├── HttpWrap.spec.js ├── Parser.spec.js ├── RoomMessages.spec.js ├── Rooms.spec.js ├── TextLib.spec.js ├── Thanks.spec.js ├── Utils.spec.js └── helpers ├── TestHelper.js └── testWikiArticle.md /.eslintrc: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "parserOption": { 4 | "ecmaVersion": 6, 5 | "ecmaFeatures": { 6 | "jsx": true 7 | } 8 | }, 9 | "env": { 10 | "browser": true, 11 | "mocha": true, 12 | "node": true 13 | }, 14 | "parser": "babel-eslint", 15 | "plugins": [ 16 | "react" 17 | ], 18 | "globals": { 19 | "Promise": true, 20 | "window": true, 21 | "$": true, 22 | "ga": true, 23 | "jQuery": true, 24 | "router": true 25 | }, 26 | "rules": { 27 | "comma-dangle": 2, 28 | "no-cond-assign": 2, 29 | "no-console": 0, 30 | "no-constant-condition": 2, 31 | "no-control-regex": 2, 32 | "no-debugger": 2, 33 | "no-dupe-keys": 2, 34 | "no-empty": 2, 35 | "no-empty-character-class": 2, 36 | "no-ex-assign": 2, 37 | "no-extra-boolean-cast": 2, 38 | "no-extra-parens": 0, 39 | "no-extra-semi": 2, 40 | "no-func-assign": 2, 41 | "no-inner-declarations": 2, 42 | "no-invalid-regexp": 2, 43 | "no-irregular-whitespace": 2, 44 | "no-negated-in-lhs": 2, 45 | "no-obj-calls": 2, 46 | "no-regex-spaces": 2, 47 | "no-reserved-keys": 0, 48 | "no-sparse-arrays": 2, 49 | "no-unreachable": 2, 50 | "use-isnan": 2, 51 | "valid-jsdoc": 2, 52 | "valid-typeof": 2, 53 | 54 | "block-scoped-var": 0, 55 | "complexity": 0, 56 | "consistent-return": 2, 57 | "curly": 2, 58 | "default-case": 1, 59 | "dot-notation": 0, 60 | "eqeqeq": 1, 61 | "guard-for-in": 1, 62 | "no-alert": 1, 63 | "no-caller": 2, 64 | "no-div-regex": 2, 65 | "no-else-return": 0, 66 | "no-eq-null": 1, 67 | "no-eval": 2, 68 | "no-extend-native": 2, 69 | "no-extra-bind": 2, 70 | "no-fallthrough": 2, 71 | "no-floating-decimal": 2, 72 | "no-implied-eval": 2, 73 | "no-iterator": 2, 74 | "no-labels": 2, 75 | "no-lone-blocks": 2, 76 | "no-loop-func": 1, 77 | "no-multi-spaces": 1, 78 | "no-multi-str": 2, 79 | "no-native-reassign": 2, 80 | "no-new": 2, 81 | "no-new-func": 2, 82 | "no-new-wrappers": 2, 83 | "no-octal": 2, 84 | "no-octal-escape": 2, 85 | "no-process-env": 0, 86 | "no-proto": 2, 87 | "no-redeclare": 1, 88 | "no-return-assign": 2, 89 | "no-script-url": 2, 90 | "no-self-compare": 2, 91 | "no-sequences": 2, 92 | "no-unused-expressions": 2, 93 | "no-void": 1, 94 | "no-warning-comments": [ 95 | 1, 96 | { 97 | "terms": [ 98 | "fixme" 99 | ], 100 | "location": "start" 101 | } 102 | ], 103 | "no-with": 2, 104 | "radix": 2, 105 | "vars-on-top": 0, 106 | "wrap-iife": [2, "any"], 107 | "yoda": 0, 108 | 109 | "strict": 0, 110 | 111 | "no-catch-shadow": 2, 112 | "no-delete-var": 2, 113 | "no-label-var": 2, 114 | "no-shadow": 0, 115 | "no-shadow-restricted-names": 2, 116 | "no-undef": 2, 117 | "no-undef-init": 2, 118 | "no-undefined": 1, 119 | "no-unused-vars": 2, 120 | "no-use-before-define": 0, 121 | 122 | "handle-callback-err": 2, 123 | "no-mixed-requires": 0, 124 | "no-new-require": 2, 125 | "no-path-concat": 2, 126 | "no-process-exit": 2, 127 | "no-restricted-modules": 0, 128 | "no-sync": 0, 129 | 130 | "brace-style": [ 131 | 2, 132 | "1tbs", 133 | { "allowSingleLine": true } 134 | ], 135 | "camelcase": 1, 136 | "comma-spacing": [ 137 | 2, 138 | { 139 | "before": false, 140 | "after": true 141 | } 142 | ], 143 | "comma-style": [ 144 | 2, "last" 145 | ], 146 | "consistent-this": 0, 147 | "eol-last": 2, 148 | "func-names": 0, 149 | "func-style": 0, 150 | "key-spacing": [ 151 | 2, 152 | { 153 | "beforeColon": false, 154 | "afterColon": true 155 | } 156 | ], 157 | "max-nested-callbacks": 0, 158 | "new-cap": 0, 159 | "new-parens": 2, 160 | "no-array-constructor": 2, 161 | "no-inline-comments": 1, 162 | "no-lonely-if": 1, 163 | "no-mixed-spaces-and-tabs": 2, 164 | "no-multiple-empty-lines": [ 165 | 1, 166 | { "max": 2 } 167 | ], 168 | "no-nested-ternary": 2, 169 | "no-new-object": 2, 170 | "semi-spacing": [2, { "before": false, "after": true }], 171 | "no-spaced-func": 2, 172 | "no-ternary": 0, 173 | "no-trailing-spaces": 1, 174 | "no-underscore-dangle": 0, 175 | "one-var": 0, 176 | "operator-assignment": 0, 177 | "padded-blocks": 0, 178 | "quote-props": [2, "as-needed"], 179 | "quotes": [ 180 | 2, 181 | "single", 182 | "avoid-escape" 183 | ], 184 | "semi": [ 185 | 2, 186 | "always" 187 | ], 188 | "sort-vars": 0, 189 | "keyword-spacing": [ 2 ], 190 | "space-before-function-paren": [ 191 | 2, 192 | "never" 193 | ], 194 | "space-before-blocks": [ 195 | 2, 196 | "always" 197 | ], 198 | "space-in-brackets": 0, 199 | "space-in-parens": 0, 200 | "space-infix-ops": 2, 201 | "space-unary-ops": [ 202 | 1, 203 | { 204 | "words": true, 205 | "nonwords": false 206 | } 207 | ], 208 | "spaced-comment": [ 209 | 2, 210 | "always", 211 | { "exceptions": ["-"] } 212 | ], 213 | "wrap-regex": 1, 214 | 215 | "max-depth": 0, 216 | "max-len": [ 217 | 2, 218 | 80, 219 | 2 220 | ], 221 | "max-params": 0, 222 | "max-statements": 0, 223 | "no-bitwise": 1, 224 | "no-plusplus": 0, 225 | 226 | "react/display-name": 1, 227 | "react/jsx-boolean-value": [1, "always"], 228 | "jsx-quotes": [1, "prefer-single"], 229 | "react/jsx-no-undef": 1, 230 | "react/jsx-sort-props": [1, { "ignoreCase": true }], 231 | "react/jsx-uses-react": 1, 232 | "react/jsx-uses-vars": 1, 233 | "react/no-did-mount-set-state": 2, 234 | "react/no-did-update-set-state": 2, 235 | "react/no-multi-comp": [2, { "ignoreStateless": true } ], 236 | "react/prop-types": 2, 237 | "react/react-in-jsx-scope": 1, 238 | "react/self-closing-comp": 1, 239 | "react/wrap-multilines": 1 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | *.log 4 | *.env 5 | !dot-EXAMPLE.env 6 | node_modules 7 | .DS_Store 8 | Thumbs.db 9 | data/wiki 10 | config.json 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "4" 5 | - "6" 6 | 7 | script: npm test 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2017, freeCodeCamp. 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CamperBot 2 | 3 | [![Join the chat at https://gitter.im/FreeCodeCamp/camperbot](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/FreeCodeCamp/camperbot) [![Stories in Ready](https://badge.waffle.io/FreeCodeCamp/camperbot.png?label=ready&title=Ready)](https://waffle.io/FreeCodeCamp/camperbot) 4 | 5 | This is a full featured bot for 6 | [Gitter.im/FreeCodeCamp](https://gitter.im/orgs/FreeCodeCamp/rooms) chat rooms. 7 | 8 | **Main features:** 9 | 10 | - integration with github FCC wiki 11 | - `find` (alias `explain`) command to show wiki pages 12 | - wrapper for commands 13 | 14 | The CamperBot is integrated into various FreeCodeCamp chat rooms. 15 | 16 | Join us in 17 | [Gitter.im/FreeCodeCamp/camperbot](https://gitter.im/FreeCodeCamp/camperbot) 18 | to discuss about camperbot development! 19 | 20 | Test the CamperBot in the 21 | [Gitter.im/FreeCodeCamp/camperbotPlayground](https://gitter.im/FreeCodeCamp/camperbotPlayground) 22 | room. 23 | 24 | CamperBot was originally created by for [Free Code Camp](https://www.freecodecamp.org) by [@dcsan](https://github.com/dcsan) at [RIKAI Labs](mailto:dc@rikai.co), and is now maintained by our open source community. 25 | 26 | ## Contents 27 | - [Introducing CamperBot!](https://github.com/FreeCodeCamp/camperbot/#introducing-camperbot) 28 | - [Installation instructions](https://github.com/FreeCodeCamp/camperbot/#installation-instructions) 29 | - [Mac / Linux](https://github.com/FreeCodeCamp/camperbot/#mac--linux) 30 | - [Windows](https://github.com/FreeCodeCamp/camperbot/#windows) 31 | - [Make your own bot user](https://github.com/FreeCodeCamp/camperbot/#make-your-own-bot-user) 32 | - [Getting your own appID](https://github.com/FreeCodeCamp/camperbot/#getting-your-own-appid) 33 | - [Configure your bot!](https://github.com/FreeCodeCamp/camperbot/#configure-your-bot) 34 | - [Running tests](https://github.com/FreeCodeCamp/camperbot/#running-tests) 35 | - [Wiki Content](https://github.com/FreeCodeCamp/camperbot/#wiki-content) 36 | - [System Overview](https://github.com/FreeCodeCamp/camperbot/#system-overview) 37 | - [Room Joins](https://github.com/FreeCodeCamp/camperbot/#dataroomdatajs) 38 | - [Bot Commands](https://github.com/FreeCodeCamp/camperbot/#libbotbotcommandsjs) 39 | - [Wiki Data](https://github.com/FreeCodeCamp/camperbot/#kbasejs) 40 | - [Room Messages](https://github.com/FreeCodeCamp/camperbot/#roommessagesjs) 41 | - [Create own bot command](https://github.com/FreeCodeCamp/camperbot/#how-to-add-a-new-bot-command) 42 | - [Bot command details](https://github.com/FreeCodeCamp/camperbot/#more-detail-on-how-commands-are-found-and-called) 43 | - [Environment Notes](https://github.com/FreeCodeCamp/camperbot/#environment-notes) 44 | - [Contributing](https://github.com/FreeCodeCamp/camperbot/#contributing) 45 | - [Chat with us!](https://github.com/FreeCodeCamp/camperbot/#chat-with-us) 46 | 47 | ## Introducing CamperBot! 48 | 49 | CamperBot is a full featured chat bot for [Gitter.im](https://gitter.im) 50 | developed to integrate with the chat rooms for 51 | [FreeCodeCamp — the largest online coding bootcamp in the world](https://www.freecodecamp.org/) 52 | , where it serves more than 60,000 campers. 53 | 54 | ### Github Wiki Search 55 | 56 | You can search for articles in a projects github wiki 57 | ![](https://freecodecamp.github.io/camperbot/images/anims/find.gif) 58 | 59 | ### Share wiki summaries in chat 60 | 61 | Use `explain` to pull a wiki summary right into the chat: 62 | ![](https://freecodecamp.github.io/camperbot/images/anims/explain.gif) 63 | 64 | ### Points system 65 | 66 | Allow your users to send points to each other to say `thanks @username` 67 | ![](https://freecodecamp.github.io/camperbot/images/anims/points.gif) 68 | 69 | ### Fixed messages 70 | 71 | Based on scannable expressions, send messages into the chat. 72 | 73 | ### Extensible 74 | 75 | Custom functions can easily be added. Check the [System Overview](https://github.com/FreeCodeCamp/camperbot#system-overview) 76 | 77 | ## Installation instructions 78 | 79 | To run camperbot, you need [Node.js](https://nodejs.org/) 4.2.0 or greater. 80 | 81 | ### Mac / Linux 82 | 83 | To install Node, [follow the instructions here](http://blog.teamtreehouse.com/install-node-js-npm-mac) 84 | 85 | - To make your local server automatically watch for file changes, 86 | install "nodemon" (you may need `sudo`) 87 | 88 | ```sh 89 | npm install -g nodemon 90 | ``` 91 | 92 | - To download the app, clone the repository the bot is in: 93 | 94 | ```sh 95 | git clone https://github.com/FreeCodeCamp/camperbot.git 96 | ``` 97 | 98 | - Run the following commands to run the app: 99 | 100 | ```sh 101 | cd camperbot 102 | cp dot-EXAMPLE.env .env 103 | cp example.config.json config.json 104 | git submodule update --remote --checkout --init --recursive 105 | npm install 106 | nodemon app.js 107 | ``` 108 | 109 | - That's it! The app should be running at 110 | [http://localhost:7891](http://localhost:7891). 111 | 112 | You can now chat to your bot via [Gitter.im](https://gitter.im) at 113 | [https://gitter.im/demobot/test](https://gitter.im/demobot/test) 114 | 115 | ### Windows 116 | 117 | To install Node.js on Windows, [follow these instructions](http://blog.teamtreehouse.com/install-node-js-npm-windows). 118 | 119 | - To make your local server automatically watch for file changes, 120 | install "nodemon" in an administrator console. 121 | 122 | ```sh 123 | npm install -g nodemon 124 | ``` 125 | 126 | - To download the app, clone the repository the bot is in: 127 | 128 | ```sh 129 | git clone https://github.com/FreeCodeCamp/camperbot.git 130 | ``` 131 | 132 | - Run the following commands to run the app: 133 | 134 | ```sh 135 | cd camperbot 136 | copy dot-EXAMPLE.env .env 137 | copy example.config.json config.json 138 | git submodule update --remote --checkout --init --recursive 139 | npm install 140 | nodemon app.js 141 | ``` 142 | 143 | - That's it! The app should be running at [http://localhost:7891](http://localhost:7891). 144 | 145 | You can now chat to your bot via [Gitter.im](https://gitter.im) at 146 | [https://gitter.im/demobot/test](https://gitter.im/demobot/test) 147 | 148 | ## Make your own bot user 149 | If you've followed the instructions so far your bot instance is the demobot 150 | provided for you. 151 | 152 | The `.env` file you copied above contains login info. 153 | This is using the shared "demobot" account so you may find yourself in a 154 | chatroom with other people using the same ID! 155 | 156 | Here are instructions on getting your own bot user running. 157 | ### Setup GitHub user 158 | The first thing you'll want to do is set up a GitHub account which will be the 159 | username of your bot 160 | 161 | You can either 162 | * make a new account 163 | * use an existing account 164 | 165 | Follow the instructions for signing up on [https://github.com/](GitHub) 166 | 167 | change the `SERVER_ENV=demobot` in your `.env` to `server_ENV=USERNAMEHERE` 168 | where *USERNAMEHERE* is your github user name. 169 | 170 | ### Getting your own appID 171 | 172 | To setup your own gitter login info, you should create your own Gitter API key 173 | on their developer site, and replace the info in that `.env` file. 174 | Get your own API keys for gitter from: 175 | [https://developer.gitter.im/apps](https://developer.gitter.im/apps) 176 | 177 | When you sign in to the developer page select the option to make an app. 178 | Name the app what you want and set the callback url to 179 | `http://localhost:7891/login/callback` 180 | 181 | The next page should show you various API keys/secrets. Use those to replace 182 | the demobot default options in your `.env`. 183 | 184 | ### Configure your bot 185 | Now it is time to set up your bot w/ the app. 186 | Copy `example.config.json` to `config.json` and open `config.json` in your 187 | editor. 188 | Replace all instances of GITHUB_USER_ID with your user name 189 | set up earlier. 190 | 191 | Take note of the the rooms property of config. You can set up additional gitter rooms 192 | to connect your bot to here. The default room is `GITHUB_USERID/test` feel free to change this. 193 | 194 | You may chat with us in the CamperBot Dev chat room if you have problems. [contributors chatroom](https://gitter.im/FreeCodeCamp/Contributors). 195 | 196 | ## Running tests 197 | 198 | Tests are located in the `test/` folder can be run, along with linting, 199 | by running `gulp`. 200 | This is a watch task that will rerun whenever a `.js` file changes. 201 | 202 | ## Wiki Content 203 | 204 | The wiki content is pulled in from FCC's wiki using a git submodule. 205 | But then we just copy it and commit it back to the main app as submodules 206 | are nasty to deal with on production servers. 207 | 208 | ```sh 209 | bin/wiki-update.sh 210 | ``` 211 | 212 | ## System Overview 213 | 214 | ### data/RoomData.js 215 | 216 | The list of rooms your bot is going to join. 217 | 218 | To start with create your own bot, a test room to enter and debug in. 219 | This needs to be changed so you would only join your own rooms, otherwise 220 | developers will get into a situation where everyone is joining the same 221 | rooms and the bots go crazy talking to each other! 222 | 223 | ### lib/bot/BotCommands.js 224 | 225 | This is where you add things that the bot can do. Some commands are broken 226 | into separate files such as `cmds/thanks.js` and `cmds/update.js`. 227 | Each command gets a `input` which is a blob of data including what the user 228 | entered, and a bot instance. 229 | 230 | ### KBase.js 231 | 232 | The Knowledge base. This is an interface to all the data in the wiki. 233 | 234 | ### RoomMessages.js 235 | 236 | This is for static messages that are fired based on regex matches. 237 | If you just want to add some basic responses, this is the place to edit. 238 | 239 | ### How to add a new Bot Command 240 | 241 | Look at `BotCommands`, `echo` function. This is an example of a command being 242 | called. Anytime a user types a line starting with `echo` that will get passed 243 | to this function in input. 244 | 245 | ```js 246 | echo: function(input, bot) { 247 | var username = input.message.model.fromUser.username; 248 | return "@" + username + " said: " + input.message.model.text; 249 | } 250 | ``` 251 | 252 | The input object contains `keyword` and `params` fields. 253 | If you type `echo this` you'll get 254 | 255 | ```js 256 | //input 257 | { 258 | keyword: 'echo', 259 | params: 'this' 260 | } 261 | ``` 262 | 263 | From any command you just return the new string you want to output. 264 | So you can add new commands with this knowledge. 265 | 266 | ### More detail on how commands are found and called 267 | 268 | In `GBot.js` 269 | 270 | ```js 271 | if (input.command) { 272 | // this looks up a command and calls it 273 | output = BotCommands[input.keyword](input, this); 274 | } else { 275 | ``` 276 | 277 | `BotCommands` is a list of functions. E.g. 278 | 279 | ```js 280 | BotCommands.thanks = function() { ... } 281 | ``` 282 | 283 | where `input.keyword` is `thanks` then 284 | 285 | `BotCommands[input.keyword]` is like saying `BotCommands.thanks()` 286 | 287 | so then the params get also added in `(input, this)` so its 288 | 289 | ```js 290 | BotCommands[input.keyword](input, this); 291 | //becomes 292 | BotCommands.thanks(input, bot); 293 | ``` 294 | 295 | All of the bot commands expect these two params. E.g. in `thanks.js` 296 | 297 | ```js 298 | var commands = { 299 | thanks: function (input, bot) { 300 | ``` 301 | 302 | In `RoomMessages.js` we also have a table of regex and matching functions. 303 | 304 | ```js 305 | { 306 | regex: /\bth?a?n?[xk]s?q?\b/gim, 307 | func: BotCommands.thanks 308 | } 309 | ``` 310 | 311 | > We may switch all to just use this method in future. Would you like to help? 312 | 313 | ## Environment Notes 314 | 315 | ### wiki data 316 | 317 | We use git submodules for some wiki data. to get these submodules you would do: 318 | 319 | ```sh 320 | git submodule update --remote --checkout --init --recursive 321 | ``` 322 | 323 | ## Contributing 324 | 325 | Have a look at the 326 | [HelpWanted](https://github.com/FreeCodeCamp/camperbot/issues?q=is%3Aopen+label%3A%22help+wanted%22) 327 | label issues and consider making some first steps! 328 | 329 | The labels, P1 = priority one, and 'S' means a small task, 330 | so good places to start. 331 | 332 | ## Chat with us! 333 | 334 | Chat with us in the 335 | [contributors chatroom](https://gitter.im/FreeCodeCamp/Contributors) if you get stuck. 336 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('dotenv').config({ path: '.env' }); 4 | 5 | console.log('--------------- startup ------------------'); 6 | 7 | if (typeof Map !== 'function') { 8 | throw new Error('ES6 is required; add --harmony'); 9 | } 10 | const GBot = require('./lib/bot/GBot'); 11 | GBot.init(); 12 | console.log('camperbot running locally'); 13 | -------------------------------------------------------------------------------- /bin/.gitignore: -------------------------------------------------------------------------------- 1 | credentials* 2 | !credentials-example.sh 3 | !credentials-demobot.sh 4 | -------------------------------------------------------------------------------- /bin/auth.sh: -------------------------------------------------------------------------------- 1 | source bin/credentials.sh 2 | 3 | # this doesnt seem to work 4 | # https://developer.gitter.im/docs/authentication 5 | 6 | 7 | set -x 8 | echo "client_id=${GITTER_APP_KEY}\n" 9 | 10 | curl -i -X POST https://gitter.im/login/oauth/token \ 11 | -d"client_id=${GITTER_APP_KEY}" \ 12 | -d"client_secret=${GITTER_APP_SECRET}" \ 13 | -d"code=CODE" \ 14 | -d"grant_type=authorization_code" \ 15 | -d"redirect_uri=${GITTER_APP_REDIRECT_URL}" -------------------------------------------------------------------------------- /bin/camperbot-run.sh: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | source bin/credentials-camperbot.sh 5 | 6 | set -x 7 | 8 | SERVER_ENV=prod \ 9 | BOT_APP_HOST=bot.freecodecamp.org \ 10 | GITTER_USER_TOKEN=${GITTER_USER_TOKEN} \ 11 | GITTER_APP_KEY=${GITTER_APP_KEY} \ 12 | GITTER_APP_SECRET=${GITTER_APP_SECRET} \ 13 | LOG_LEVEL=10 \ 14 | PORT=7891 \ 15 | nodemon app.js 16 | -------------------------------------------------------------------------------- /bin/credentials-demobot.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # get these keys from here 3 | # https://developer.gitter.im/apps 4 | 5 | # Personal Access Token 6 | GITTER_USER_TOKEN=1ac342045f5a57d99fe537e58da78f6cba94f7db 7 | 8 | # Your Apps 9 | # OAUTH KEY 10 | GITTER_APP_KEY=63ece8ac0eeed9b17b1cc9867f65d4857ec6e5fc 11 | 12 | # OAUTH SECRET 13 | GITTER_APP_SECRET=9026e3b3a74357035ee15a9591f31b2de5cfd3a6 14 | # REDIRECT URL 15 | GITTER_APP_REDIRECT_URL=http://localhost:7891/login/callback 16 | -------------------------------------------------------------------------------- /bin/credentials-example.sh: -------------------------------------------------------------------------------- 1 | # edit this file and save it as credentials.sh 2 | # 3 | # or, create a new file like credentials-ENV.sh 4 | # and symlink it 5 | # $ ln -s credentials-ENV.sh credentials.sh 6 | # this file is included in other run commands 7 | 8 | GITTER_APP_KEY=XXXX 9 | GITTER_APP_SECRET=XXXX 10 | GITTER_APP_REDIRECT_URL=http://localhost:7000/login/callback 11 | 12 | GITTER_USER_TOKEN=XXXX 13 | -------------------------------------------------------------------------------- /bin/debug.sh: -------------------------------------------------------------------------------- 1 | open "http://localhost:8080/?ws=localhost:8080&port=5858" & 2 | 3 | source bin/credentials-bothelp.sh 4 | 5 | set -x 6 | 7 | SERVER_ENV=${SERVER_ENV} \ 8 | GITTER_USER_TOKEN=${GITTER_USER_TOKEN} \ 9 | GITTER_APP_KEY=${GITTER_APP_KEY} \ 10 | GITTER_APP_SECRET=${GITTER_APP_SECRET} \ 11 | LOG_LEVEL=10 \ 12 | PORT=7891 \ 13 | node-debug app.js 14 | 15 | # node debug app.js 16 | 17 | # node-debug app.js 18 | # nodemon -x node app.js 19 | # nodemon app.js 20 | # node-debug app.js 21 | -------------------------------------------------------------------------------- /bin/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -x 4 | bin/wiki-update.sh 5 | ssh freecodecamp@104.131.2.16 "cd /home/freecodecamp/www/gitterbot/nap && \ 6 | git clean -f && \ 7 | git checkout . && \ 8 | git pull && \ 9 | pm2 restart all" 10 | 11 | 12 | # bin/pm2-update.sh" 13 | -------------------------------------------------------------------------------- /bin/force-update.sh: -------------------------------------------------------------------------------- 1 | git checkout . 2 | git clean -f 3 | git reset --hard HEAD 4 | 5 | git pull 6 | -------------------------------------------------------------------------------- /bin/git-clean.sh: -------------------------------------------------------------------------------- 1 | git reset --hard HEAD 2 | git clean -f 3 | -------------------------------------------------------------------------------- /bin/mocha-debug.sh: -------------------------------------------------------------------------------- 1 | open "http://localhost:8080/?ws=localhost:8080&port=5858" & 2 | 3 | source bin/credentials.sh 4 | 5 | set -x 6 | 7 | GITTER_USER_TOKEN=${GITTER_USER_TOKEN} \ 8 | GITTER_APP_KEY=${GITTER_APP_KEY} \ 9 | GITTER_APP_SECRET=${GITTER_APP_SECRET} \ 10 | mocha --debug-brk --harmony 11 | 12 | -------------------------------------------------------------------------------- /bin/pm2-run.sh: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | source bin/credentials-camperbot.sh 6 | 7 | set -x 8 | 9 | SERVER_ENV=prod \ 10 | GITTER_USER_TOKEN=${GITTER_USER_TOKEN} \ 11 | GITTER_APP_KEY=${GITTER_APP_KEY} \ 12 | GITTER_APP_SECRET=${GITTER_APP_SECRET} \ 13 | LOG_LEVEL=10 \ 14 | PORT=7891 \ 15 | pm2 start --name bot --interpreter node app.js 16 | 17 | # node app.js 18 | # node app.js 19 | # nodemon -x node app.js 20 | 21 | pm2 list 22 | pm2 logs all 23 | -------------------------------------------------------------------------------- /bin/pm2-update.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -x 3 | 4 | approot="/home/freecodecamp/www/gitterbot/nap" 5 | logpath="${approot}/logs/bot.log" 6 | 7 | git pull && pm2 restart all && pm2 logs > $logpath && 8 | tail -f $logpath 9 | -------------------------------------------------------------------------------- /bin/post.sh: -------------------------------------------------------------------------------- 1 | source bin/credentials.sh 2 | 3 | BOTZYROOM="55b1a9030fc9f982beaac901" 4 | 5 | curl -X POST -i -H "Content-Type: application/json" \ 6 | -H "Accept: application/json" \ 7 | -H "Authorization: Bearer ${GITTER_USER_TOKEN}" \ 8 | "https://api.gitter.im/v1/rooms/${BOTZYROOM}/chatMessages" \ 9 | -d '{"text":"curl test"}' 10 | -------------------------------------------------------------------------------- /bin/run.sh: -------------------------------------------------------------------------------- 1 | 2 | 3 | # bin/update-wiki.sh 4 | 5 | #source bin/credentials-bothelp.sh 6 | 7 | #set -x 8 | # 9 | #SERVER_ENV=local \ 10 | #GITTER_USER_TOKEN=${GITTER_USER_TOKEN} \ 11 | #GITTER_APP_KEY=${GITTER_APP_KEY} \ 12 | #GITTER_APP_SECRET=${GITTER_APP_SECRET} \ 13 | #LOG_LEVEL=10 \ 14 | #PORT=7891 \ 15 | 16 | # nodemon -x node app.js 17 | 18 | # nodemon app.js 19 | 20 | nodemon app.js 21 | -------------------------------------------------------------------------------- /bin/step-debug.sh: -------------------------------------------------------------------------------- 1 | # open "http://localhost:8080/?ws=localhost:8080&port=5858" & 2 | 3 | source bin/credentials-bothelp.sh 4 | 5 | set -x 6 | 7 | SERVER_ENV=${SERVER_ENV} \ 8 | GITTER_USER_TOKEN=${GITTER_USER_TOKEN} \ 9 | GITTER_APP_KEY=${GITTER_APP_KEY} \ 10 | GITTER_APP_SECRET=${GITTER_APP_SECRET} \ 11 | LOG_LEVEL=10 \ 12 | PORT=7891 \ 13 | node-debug app.js 14 | # node debug app.js 15 | # nodemon -x node app.js 16 | # nodemon app.js 17 | -------------------------------------------------------------------------------- /bin/update-run-dev.sh: -------------------------------------------------------------------------------- 1 | # 2 | bin/update-wiki.sh 3 | bin/run-dev-bothelp.sh -------------------------------------------------------------------------------- /bin/wiki-pull-prod.sh: -------------------------------------------------------------------------------- 1 | # pull down wiki files 2 | # don't commit them back 3 | # just used on prod server 4 | # 5 | # used on the production server only 6 | 7 | APPDIR=/home/freecodecamp/www/gitterbot 8 | 9 | GITPATH=/usr/bin/git 10 | 11 | 12 | cd $APPDIR/data/fcc.wiki 13 | $GITPATH fetch 14 | $GITPATH checkout master 15 | $GITPATH pull origin master 16 | cd $APPDIR 17 | -------------------------------------------------------------------------------- /bin/wiki-pull.sh: -------------------------------------------------------------------------------- 1 | # pull down wiki files 2 | # don't commit them back 3 | # just used on prod server 4 | 5 | # set -x 6 | 7 | git submodule init 8 | git submodule update --init --checkout --recursive --remote 9 | # cd data/fcc.wiki 10 | # git fetch 11 | # git checkout master 12 | # git pull origin master 13 | -------------------------------------------------------------------------------- /bin/wiki-update.sh: -------------------------------------------------------------------------------- 1 | # pull down wiki files 2 | 3 | cd data/fcc.wiki 4 | git fetch 5 | git checkout master 6 | git pull origin master 7 | -------------------------------------------------------------------------------- /config/AppConfig.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const config = require('../config.json'); 5 | require('dotenv').config({ path: '.env' }); 6 | 7 | const AppConfig = { 8 | clientId: process.env.GITTER_APP_KEY, 9 | token: process.env.GITTER_USER_TOKEN, 10 | apiKey: process.env.FCC_API_KEY, 11 | supportDmRooms: false, 12 | botname: null, 13 | roomId: '55b1a9030fc9f982beaac901', 14 | org: 'bothelp', 15 | testUser: 'bothelp', 16 | // so bot doesnt get in a loop replying itself 17 | botlist: ['bothelp', 'camperbot', 'demobot', config.githubId], 18 | webuser: 'webuser', 19 | wikiHost: 'https://github.com/freecodecamp/freecodecamp/wiki/', 20 | gitterHost: 'https://gitter.im/', 21 | botVersion: '0.0.12', 22 | MAX_WIKI_LINES: 20, 23 | botNoiseLevel: 1, 24 | 25 | init: function() { 26 | const serverEnv = process.env.SERVER_ENV; 27 | AppConfig.serverEnv = serverEnv; 28 | this.warn('AppConfig.init serverEnv:', serverEnv); 29 | 30 | const thisConfig = envConfigs[serverEnv]; 31 | if (!thisConfig) { 32 | const msg = ('FATAL ERROR! cant find serverEnv: ' + serverEnv); 33 | console.error(msg); 34 | throw new Error(msg); 35 | } 36 | _.merge(AppConfig, thisConfig); 37 | }, 38 | 39 | warn: function(msg, obj) { 40 | console.warn('WARN> AppConfig', msg, obj); 41 | }, 42 | 43 | // TODO cleanup 44 | // use as a function so it can be set at startup 45 | // before other code calls it at runtime 46 | getBotName: function() { 47 | if (!AppConfig.botname) { 48 | AppConfig.init(); 49 | this.warn('getBotName()', AppConfig.botname ); 50 | console.log('tried to call botname before it was set'); 51 | } 52 | return AppConfig.botname; 53 | }, 54 | 55 | who: function(req) { 56 | let who; 57 | 58 | if (req.user) { 59 | console.warn('got a user in the request but ignoring'); 60 | } else if (req.who) { 61 | who = req.who; 62 | } else { 63 | who = AppConfig.webuser; 64 | } 65 | return who; 66 | }, 67 | 68 | // TODO read from config file for dev/live modes and running env 69 | getOrg: function() { 70 | return AppConfig.org; 71 | }, 72 | 73 | topicDmUri: function(topic) { 74 | let uri = AppConfig.appHost + '/go?dm=y&room=' + AppConfig.getBotName(); 75 | if (topic) { 76 | uri += '&topic=' + topic; 77 | } 78 | return uri; 79 | }, 80 | 81 | dmLink: function() { 82 | return 'https://gitter.im/' + AppConfig.getBotName(); 83 | } 84 | }; 85 | 86 | const envConfigs = { 87 | 88 | demobot: { 89 | botname: 'demobot', 90 | appHost: 'http://localhost:7000', 91 | apiServer: 'www.freecodecamp.org', 92 | appRedirectUrl: 'http://localhost:7891/login/callback' 93 | }, 94 | 95 | test: { 96 | botname: 'bothelp', 97 | appHost: 'http://localhost:7000', 98 | apiServer: 'www.freecodecamp.org', 99 | appRedirectUrl: 'http://localhost:7891/login/callback' 100 | }, 101 | 102 | local: { 103 | botname: 'bothelp', 104 | appHost: 'http://localhost:7000', 105 | apiServer: 'www.freecodecamp.org', 106 | appRedirectUrl: 'http://localhost:7891/login/callback' 107 | }, 108 | beta: { 109 | botname: 'bothelp', 110 | appHost: 'http://localhost:7000', 111 | apiServer: 'beta.freecodecamp.org', 112 | appRedirectUrl: 'http://localhost:7891/login/callback' 113 | }, 114 | prod: { 115 | botname: 'camperbot', 116 | appHost: 'http://bot.freecodecamp.org', 117 | apiServer: 'www.freecodecamp.org', 118 | appRedirectUrl: 'http://bot.freecodecamp.org/login/callback' 119 | } 120 | }; 121 | 122 | envConfigs[config.githubId] = config.user; 123 | AppConfig.init(); 124 | 125 | module.exports = AppConfig; 126 | -------------------------------------------------------------------------------- /data/RoomData.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This file needs to be edited to comment out 4 | // rooms you want to join 5 | 6 | // TODO - move to lib/ dir? 7 | 8 | const AppConfig = require('../config/AppConfig'); 9 | const config = require('../config.json'); 10 | 11 | // from the webapp 12 | // users enter the rooms with a topic=XXX url 13 | // we find a matching room here with that topic 14 | // and redirect them 15 | 16 | // TODO - read this from the JSON file 17 | const bonfireTopics = [ 18 | 'bonfires', 19 | 'Pair Program on Bonfires', 20 | 'Meet Bonfire', 21 | 'Reverse a String', 22 | 'Factorialize a Number', 23 | 'Check for Palindromes', 24 | 'Find the Longest Word in a String', 25 | 'Title Case a Sentence', 26 | 'Return Largest Numbers in Arrays', 27 | 'Confirm the Ending', 28 | 'Repeat a string repeat a string', 29 | 'Truncate a string', 30 | 'Chunky Monkey', 31 | 'Slasher Flick', 32 | 'Mutations', 33 | 'Falsey Bouncer', 34 | 'Where art thou', 35 | 'Seek and Destroy', 36 | 'Where do I belong', 37 | 'Sum All Numbers in a Range', 38 | 'Diff Two Arrays', 39 | 'Roman Numeral Converter', 40 | 'Search and Replace', 41 | 'Pig Latin', 42 | 'DNA Pairing', 43 | 'Missing letters', 44 | 'Boo who', 45 | 'Sorted Union', 46 | 'Convert HTML Entities', 47 | 'Spinal Tap Case', 48 | 'Sum All Odd Fibonacci Numbers', 49 | 'Sum All Primes', 50 | 'Smallest Common Multiple', 51 | 'Finders Keepers', 52 | 'Drop it', 53 | 'Steamroller', 54 | 'Binary Agents', 55 | 'Everything Be True', 56 | 'Arguments Optional' 57 | ]; 58 | 59 | const bonfireDashedNames = [ 60 | 'bonfire-meet-bonfire', 61 | 'bonfire-reverse-a-string', 62 | 'bonfire-factorialize-a-number', 63 | 'bonfire-check-for-palindromes', 64 | 'bonfire-find-the-longest-word-in-a-string', 65 | 'bonfire-title-case-a-sentence', 66 | 'bonfire-return-largest-numbers-in-arrays', 67 | 'bonfire-confirm-the-ending', 68 | 'bonfire-repeat-a-string-repeat-a-string', 69 | 'bonfire-truncate-a-string', 70 | 'bonfire-chunky-monkey', 71 | 'bonfire-slasher-flick', 72 | 'bonfire-mutations', 73 | 'bonfire-falsey-bouncer', 74 | 'bonfire-where-art-thou', 75 | 'bonfire-seek-and-destroy', 76 | 'bonfire-where-do-i-belong', 77 | 'bonfire-sum-all-numbers-in-a-range', 78 | 'bonfire-diff-two-arrays', 79 | 'bonfire-roman-numeral-converter', 80 | 'bonfire-search-and-replace', 81 | 'bonfire-pig-latin', 82 | 'bonfire-dna-pairing', 83 | 'bonfire-missing-letters', 84 | 'bonfire-boo-who', 85 | 'bonfire-sorted-union', 86 | 'bonfire-convert-html-entities', 87 | 'bonfire-spinal-tap-case', 88 | 'bonfire-sum-all-odd-fibonacci-numbers', 89 | 'bonfire-sum-all-primes', 90 | 'bonfire-smallest-common-multiple', 91 | 'bonfire-finders-keepers', 92 | 'bonfire-drop-it', 93 | 'bonfire-steamroller', 94 | 'bonfire-binary-agents', 95 | 'bonfire-everything-be-true', 96 | 'bonfire-arguments-optional', 97 | 'bonfire-make-a-person', 98 | 'bonfire-map-the-debris', 99 | 'bonfire-pairwise', 100 | 'bonfire-validate-us-telephone-numbers', 101 | 'bonfire-symmetric-difference', 102 | 'bonfire-exact-change', 103 | 'bonfire-inventory-update', 104 | 'bonfire-no-repeats-please', 105 | 'bonfire-friendly-date-ranges' 106 | ]; 107 | 108 | const camperBotChatRooms = [ 109 | 'FreeCodeCamp/admin', 110 | 'FreeCodeCamp/camperbotPlayground', 111 | 'FreeCodeCamp/Casual', 112 | 'FreeCodeCamp/CodeReview', 113 | 'FreeCodeCamp/Contributors', 114 | 'FreeCodeCamp/DataScience', 115 | 'FreeCodeCamp/FreeCodeCamp', 116 | 'FreeCodeCamp/Help', 117 | 'FreeCodeCamp/HelpBackEnd', 118 | 'FreeCodeCamp/HelpDataViz', 119 | 'FreeCodeCamp/HelpFrontEnd', 120 | 'FreeCodeCamp/HelpJavaScript', 121 | 'FreeCodeCamp/linux', 122 | 'FreeCodeCamp/PairProgrammingWomen' 123 | ]; 124 | 125 | // @TODO Refactor into a room generator function 126 | const camperBotRooms = [camperBotChatRooms] 127 | .reduce((rooms, currRooms) => rooms.concat(currRooms)) 128 | .map(room => { return { name: room }; }); 129 | 130 | const BotRoomData = { 131 | // this is the demobot that ships with the app 132 | demobot: [{ 133 | title: 'demobot', 134 | name: 'demobot/test', 135 | icon: 'star', 136 | topics: ['getting started'] 137 | }], 138 | // developer bot 139 | bothelp: [ 140 | { 141 | title: 'bothelp', 142 | name: 'bothelp/testing', 143 | icon: 'question', 144 | topics: ['chitchat', 'bots', 'bot-development', 'camperbot'] 145 | }, 146 | { 147 | title: 'HelpBonfires', 148 | icon: 'fire', 149 | name: 'bothelp/HelpBonfires', 150 | topics: bonfireTopics 151 | }, 152 | { 153 | title: 'camperbot/localdev', 154 | name: 'camperbot/localdev' 155 | }, 156 | { 157 | title: 'bothelpDM', 158 | name: 'bothelp' 159 | }, 160 | { 161 | title: 'GeneralChat', 162 | name: 'bothelp/GeneralChat' 163 | }, 164 | { 165 | title: 'PrivateRoomTest', 166 | name: 'bothelp/PrivateRoomTest', 167 | topics: ['general', 'intros'] 168 | }, 169 | { 170 | title: 'EdaanDemo', 171 | name: 'egetzel/demo', 172 | topics: ['egdemo'] 173 | }, 174 | // Bonfire single rooms 175 | { 176 | name: 'bothelp/bonfire-factorialize-a-number', 177 | topics: ['bonfire factorialize a number'], 178 | isBonfire: true 179 | } 180 | ], 181 | camperbot: camperBotRooms 182 | }; 183 | 184 | BotRoomData[config.githubId] = config.rooms; 185 | 186 | 187 | bonfireDashedNames.map(bfName => { 188 | const room = { 189 | name: 'camperbot/' + bfName, 190 | isBonfire: true 191 | }; 192 | BotRoomData.camperbot.push(room); 193 | }); 194 | 195 | BotRoomData.camperbot.map(room => { 196 | room.title = room.title || room.name.split('/')[1]; 197 | if (room.isBonfire) { 198 | room.entry = 'camperbot/testing'; 199 | room.topic = room.title; 200 | } 201 | }); 202 | 203 | const RoomData = { 204 | rooms: function(botname) { 205 | botname = botname || AppConfig.getBotName(); 206 | return BotRoomData[botname]; 207 | }, 208 | 209 | defaultRoom: function() { 210 | return RoomData.rooms().rooms[0]; 211 | } 212 | }; 213 | 214 | module.exports = RoomData; 215 | -------------------------------------------------------------------------------- /data/rooms/RoomMessages.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // the users input is scanned for these keywords 4 | // and can trigger the messages below 5 | // chance controls the frequency the result will be echoed back by the camper 6 | 7 | // using js rather than json so we can have functions and comments 8 | 9 | const BotCommands = require('../../lib/bot/BotCommands'); 10 | const _ = require('lodash'); 11 | const Utils = require('../../lib/utils/Utils'); 12 | 13 | 14 | // TODO - add these to all of the rooms 15 | // this is easier for people to add content to 16 | // as they don't have to add to two lists 17 | const AllRoomMessages = [ 18 | { 19 | regex: /.*help.*bonfire:?s?/i, 20 | text: ' > type `bonfire name` to get some info on that bonfire. ' + 21 | 'And check [HelpBonfires chatroom]' + 22 | '(https://gitter.im/FreeCodeCamp/HelpJavaScript)', 23 | not: 'freecodecamp/HelpJavaScript' 24 | }, 25 | { 26 | regex: /\btroll\b/i, 27 | text: '> :trollface: troll problems? [notify admins here]' + 28 | '(https://gitter.im/FreeCodeCamp/admin)' 29 | }, 30 | { 31 | regex: /allyourbase/, 32 | text: '![all your base]' + 33 | '(https://files.gitter.im/FreeCodeCamp/CoreTeam/Bw51/imgres.jpg)' 34 | }, 35 | { 36 | regex: /'''/, 37 | text: '> :bulb: to format code use backticks! ``` [more info]' + 38 | '(http://forum.freecodecamp.org/t/markdown-code-formatting/18391)' 39 | }, 40 | { 41 | regex: /[^\@]\bholler/i, 42 | text: '> holler back!', 43 | // only say this 50% of the time 44 | chance: 1 45 | }, 46 | { 47 | // tests: https://regex101.com/r/hH5cN7/42 48 | // eslint-disable-next-line max-len 49 | regex: /[^\@]((?:^|\s)(?:(?:th(?:n[qx]|x)|t[xyq]|tn(?:[x]){0,2})|\w*\s*[\.,]*\s*than[kx](?:[sxz]){0,2}|than[kx](?:[sxz]){0,2}(?:[uq]|y(?:ou)?)?)|grazie|arigato(?:[u]{0,1})|doumo|gracias?|spasibo|dhanyavaad(?:hamulu)?|o?brigad(?:o|a)|dziekuje|(?:re)?merci|multumesc|shukra?an|danke)\b/gi, 50 | func: BotCommands.thanks 51 | }, 52 | { 53 | // tests: https://regex101.com/r/pT0zJ1/3 54 | regex: /(?:^|\s)(?:love|luv)\s?(?:u|you|me)?,?\s?(?:cbot|@?camperbot)\b/i, 55 | func: function(input) { 56 | const fromUser = '@' + input.message.model.fromUser.username; 57 | return fromUser + ', :sparkles: :heart_eyes: :sparkles:'; 58 | } 59 | } 60 | ]; 61 | 62 | const RoomMessages = { 63 | scanInput: function(input, roomName, chance) { 64 | if (Math.random() > chance) { 65 | // dont always reply 66 | return null; 67 | } 68 | const chat = input.message.model.text.toLowerCase(); 69 | chance = chance || 1; 70 | roomName = roomName.toLowerCase(); 71 | 72 | // some messages are only for certain rooms so exclude them here 73 | const thisRoomMessages = AllRoomMessages.filter(msg => { 74 | if (msg.not) { 75 | return (msg.not !== roomName); 76 | } else { 77 | return true; 78 | } 79 | }); 80 | if (!thisRoomMessages) { return false; } 81 | 82 | const msgList = thisRoomMessages.filter(item => { 83 | if (!item) { return null; } 84 | 85 | if (item.regex) { 86 | var flag = item.regex.test(chat); 87 | } 88 | 89 | if (flag) { 90 | Utils.clog(chat, item.word, 'flag:' + flag); 91 | } 92 | return flag; 93 | }); 94 | 95 | // now check if chance is high enough 96 | if (msgList.length > 0) { 97 | // if we have multiple messages, make sure to choose just one 98 | const oneMessage = _.sample(msgList); 99 | // check if the chance is high enough so we can have % of time messages 100 | chance = oneMessage.chance || 1; 101 | if (Math.random() < (chance)) { 102 | // we have a winner! 103 | return oneMessage; 104 | } 105 | } 106 | return null; 107 | } 108 | }; 109 | 110 | module.exports = RoomMessages; 111 | -------------------------------------------------------------------------------- /data/seed/bonfireMDNlinks.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // Copied from 3 | // https://github.com/FreeCodeCamp/freecodecamp/blob/staging/seed/bonfireMDNlinks.js 4 | 5 | // MDN Links 6 | 7 | /* These links are for Bonfires. Each key/value pair is used to render a Bonfire with appropriate links. 8 | 9 | 10 | The text of the key is what the link text will be, e.g. Global Array Object 11 | General convention is to use the page title of the MDN reference page. 12 | */ 13 | var links = { 14 | // ========= NON MDN REFS 15 | "Currying": "https://leanpub.com/javascript-allonge/read#pabc", 16 | "Smallest Common Multiple": "https://www.mathsisfun.com/least-common-multiple.html", 17 | "Permutations": "https://www.mathsisfun.com/combinatorics/combinations-permutations.html", 18 | "HTML Entities": "http://dev.w3.org/html5/html-author/charref", 19 | "Symmetric Difference": "https://www.youtube.com/watch?v=PxffSUQRkG4", 20 | 21 | // ========= GLOBAL OBJECTS 22 | "Global Array Object": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array", 23 | "Global Object": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object", 24 | "Global String Object": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String", 25 | "Boolean Objects": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean", 26 | "RegExp": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp", 27 | "Global Function Object": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function", 28 | "Arguments object": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/arguments", 29 | "Closures": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures", 30 | 31 | // ========= GLOBAL OBJECT METHODS 32 | "parseInt()": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/parseInt", 33 | 34 | // ========= PROPERTIES/MISC 35 | "String.length": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/length", 36 | 37 | // ========== OBJECT METHODS 38 | "Object.getOwnPropertyNames()": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyNames", 39 | "Object.keys()": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys", 40 | "Object.hasOwnProperty()": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/hasOwnProperty", 41 | 42 | // ======== STRING METHODS 43 | "String.charAt()": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/charAt", 44 | "String.charCodeAt()": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/charCodeAt", 45 | "String.concat()": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/concat", 46 | "String.indexOf()": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/indexOf", 47 | "String.fromCharCode()": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/fromCharCode", 48 | "String.lastIndexOf()": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/lastIndexOf", 49 | "String.match()": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/match", 50 | "String.replace()": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace", 51 | "String.slice()": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/slice", 52 | "String.split()": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/split", 53 | "String.substring()": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/substring", 54 | "String.substr()": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/substr", 55 | "String.toLowerCase()": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/toLowerCase", 56 | "String.toString()": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/toString", 57 | "String.toUpperCase()": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/toUpperCase", 58 | 59 | // ======== ARRAY METHODS 60 | "Array.concat()": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/concat", 61 | "Array.every()": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/every", 62 | "Array.filter()": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter", 63 | "Array.forEach()": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach", 64 | "Array.indexOf()": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf", 65 | "Array.isArray()": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray", 66 | "Array.join()": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/join", 67 | "Array.lastIndexOf()": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/lastIndexOf", 68 | "Array.map()": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map", 69 | "Array.pop()": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/pop", 70 | "Array.push()": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/push", 71 | "Array.reduce()": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce", 72 | "Array.reverse()": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reverse", 73 | "Array.shift()": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/shift", 74 | "Array.slice()": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice", 75 | "Array.some()": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/some", 76 | "Array.sort()": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort", 77 | "Array.splice()": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice", 78 | "Array.toString()": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/toString", 79 | 80 | // ======== MATH METHODS 81 | "Math.max()": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/max", 82 | "Math.min()": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/min", 83 | "Math.pow()": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/pow", 84 | "Remainder": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Arithmetic_Operators#Remainder_(.25)", 85 | 86 | // ======== GENERAL JAVASCRIPT REFERENCES 87 | "Arithmetic Operators": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Arithmetic_Operators", 88 | "Comparison Operators": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Comparison_Operators", 89 | "Details of the Object Model": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Details_of_the_Object_Model", 90 | "For Loops": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for" 91 | }; 92 | 93 | module.exports = links; 94 | -------------------------------------------------------------------------------- /data/seed/challenges/advanced-bonfires.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Advanced Algorithm Scripting", 3 | "order": 0.011, 4 | "challenges": [ 5 | { 6 | "id": "a2f1d72d9b908d0bd72bb9f6", 7 | "name": "Bonfire: Make a Person", 8 | "dashedName": "bonfire-make-a-person", 9 | "difficulty": "3.01", 10 | "description": [ 11 | "Fill in the object constructor with the methods specified in the tests.", 12 | "Those methods are getFirstName(), getLastName(), getFullName(), setFirstName(first), setLastName(last), and setFullName(firstAndLast).", 13 | "All functions that take an argument have an arity of 1, and the argument will be a string.", 14 | "These methods must be the only available means for interacting with the object.", 15 | "Remember to use RSAP if you get stuck. Try to pair program. Write your own code." 16 | ], 17 | "challengeSeed": [ 18 | "var Person = function(firstAndLast) {", 19 | " return firstAndLast;", 20 | "};", 21 | "", 22 | "var bob = new Person('Bob Ross');", 23 | "bob.getFullName();" 24 | ], 25 | "tests": [ 26 | "expect(Object.keys(bob).length).to.eql(6);", 27 | "expect(bob instanceof Person).to.be.true;", 28 | "expect(bob.firstName).to.be.undefined();", 29 | "expect(bob.lastName).to.be.undefined();", 30 | "expect(bob.getFirstName()).to.eql('Bob');", 31 | "expect(bob.getLastName()).to.eql('Ross');", 32 | "expect(bob.getFullName()).to.eql('Bob Ross');", 33 | "bob.setFirstName('Happy');", 34 | "expect(bob.getFirstName()).to.eql('Happy');", 35 | "bob.setLastName('Trees');", 36 | "expect(bob.getLastName()).to.eql('Trees');", 37 | "bob.setFullName('George Carlin');", 38 | "expect(bob.getFullName()).to.eql('George Carlin');", 39 | "bob.setFullName('Bob Ross');" 40 | ], 41 | "MDNlinks": [ 42 | "Closures", 43 | "Details of the Object Model" 44 | ], 45 | "challengeType": 5, 46 | "nameCn": "", 47 | "descriptionCn": [], 48 | "nameFr": "", 49 | "descriptionFr": [], 50 | "nameRu": "", 51 | "descriptionRu": [], 52 | "nameEs": "", 53 | "descriptionEs": [], 54 | "namePt": "", 55 | "descriptionPt": [] 56 | }, 57 | { 58 | "id": "af4afb223120f7348cdfc9fd", 59 | "name": "Bonfire: Map the Debris", 60 | "dashedName": "bonfire-map-the-debris", 61 | "difficulty": "3.02", 62 | "description": [ 63 | "Return a new array that transforms the element's average altitude into their orbital periods.", 64 | "The array will contain objects in the format {name: 'name', avgAlt: avgAlt}.", 65 | "You can read about orbital periods on wikipedia.", 66 | "The values should be rounded to the nearest whole number. The body being orbited is Earth.", 67 | "The radius of the earth is 6367.4447 kilometers, and the GM value of earth is 398600.4418", 68 | "Remember to use RSAP if you get stuck. Try to pair program. Write your own code." 69 | ], 70 | "challengeSeed": [ 71 | "function orbitalPeriod(arr) {", 72 | " var GM = 398600.4418;", 73 | " var earthRadius = 6367.4447;", 74 | " return arr;", 75 | "}", 76 | "", 77 | "orbitalPeriod([{name : \"sputnik\", avgAlt : 35873.5553}]);" 78 | ], 79 | "tests": [ 80 | "expect(orbitalPeriod([{name : \"sputnik\", avgAlt : 35873.5553}])).to.eqls([{name: \"sputnik\", orbitalPeriod: 86400}]);", 81 | "expect(orbitalPeriod([{name: \"iss\", avgAlt: 413.6}, {name: \"hubble\", avgAlt: 556.7}, {name: \"moon\", avgAlt: 378632.553}])).to.eqls([{name : \"iss\", orbitalPeriod: 5557}, {name: \"hubble\", orbitalPeriod: 5734}, {name: \"moon\", orbitalPeriod: 2377399}]);" 82 | ], 83 | "MDNlinks": [ 84 | "Math.pow()" 85 | ], 86 | "challengeType": 5, 87 | "nameCn": "", 88 | "descriptionCn": [], 89 | "nameFr": "", 90 | "descriptionFr": [], 91 | "nameRu": "", 92 | "descriptionRu": [], 93 | "nameEs": "", 94 | "descriptionEs": [], 95 | "namePt": "", 96 | "descriptionPt": [] 97 | }, 98 | { 99 | "id": "a3f503de51cfab748ff001aa", 100 | "name": "Bonfire: Pairwise", 101 | "dashedName": "bonfire-pairwise", 102 | "difficulty": "3.03", 103 | "description": [ 104 | "Return the sum of all indices of elements of 'arr' that can be paired with one other element to form a sum that equals the value in the second argument 'arg'. If multiple sums are possible, return the smallest sum. Once an element has been used, it cannot be reused to pair with another.", 105 | "For example, pairwise([1, 4, 2, 3, 0, 5], 7) should return 11 because 4, 2, 3 and 5 can be paired with each other to equal 7.", 106 | "pairwise([1, 3, 2, 4], 4) would only equal 1, because only the first two elements can be paired to equal 4, and the first element has an index of 0!", 107 | "Remember to use RSAP if you get stuck. Try to pair program. Write your own code." 108 | ], 109 | "challengeSeed": [ 110 | "function pairwise(arr, arg) {", 111 | " return arg;", 112 | "}", 113 | "", 114 | "pairwise([1,4,2,3,0,5], 7);" 115 | ], 116 | "tests": [ 117 | "expect(pairwise([1, 4, 2, 3, 0, 5], 7)).to.equal(11);", 118 | "expect(pairwise([1, 3, 2, 4], 4)).to.equal(1);", 119 | "expect(pairwise([1,1,1], 2)).to.equal(1);", 120 | "expect(pairwise([0, 0, 0, 0, 1, 1], 1)).to.equal(10);", 121 | "expect(pairwise([], 100)).to.equal(0);" 122 | ], 123 | "MDNlinks": [ 124 | "Array.reduce()" 125 | ], 126 | "challengeType": 5, 127 | "nameCn": "", 128 | "descriptionCn": [], 129 | "nameFr": "", 130 | "descriptionFr": [], 131 | "nameRu": "", 132 | "descriptionRu": [], 133 | "nameEs": "", 134 | "descriptionEs": [], 135 | "namePt": "", 136 | "descriptionPt": [] 137 | } 138 | ] 139 | } 140 | -------------------------------------------------------------------------------- /data/seed/challenges/basejumps.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Full Stack JavaScript Projects", 3 | "order": 0.019, 4 | "challenges": [ 5 | { 6 | "id": "bd7158d8c443eddfaeb5bcef", 7 | "name": "Waypoint: Get Set for Basejumps", 8 | "dashedName": "waypoint-get-set-for-basejumps", 9 | "difficulty": 2.00, 10 | "challengeSeed": ["128451852"], 11 | "description": [ 12 | "Objective: Get the MEAN stack running on Cloud 9, push your code to GitHub, and deploy it to Heroku.", 13 | "We'll build our Basejumps on Cloud 9, a powerful online code editor with a full Ubuntu Linux workspace, all running in the cloud.", 14 | "If you don't already have Cloud 9 account, create one now at http://c9.io.", 15 | "Now let's get your development environment ready for a new Angular-Fullstack application provided by Yeoman.", 16 | "Open up http://c9.io and sign in to your account.", 17 | "Click on Create New Workspace at the top right of the c9.io page, then click on the \"Create a new workspace\" popup that appears below it the button after you click on it.", 18 | "Give your workspace a name.", 19 | "Choose Node.js in the selection area below the name field.", 20 | "Click the Create button. Then click into your new workspace.", 21 | "In the lower right hand corner you should see a terminal window. In this window use the following commands. You don't need to know what these mean at this point.", 22 | "Never run this command on your local machine. But in your Cloud 9 terminal window, run: rm -rf * && echo \"export NODE_PATH=$NODE_PATH:/home/ubuntu/.nvm/v0.10.35/lib/node_modules\" >> ~/.bashrc && source ~/.bashrc && npm install -g yo grunt grunt-cli generator-angular-fullstack && yo angular-fullstack", 23 | "Yeoman will prompt you to answer some questions. Answer them like this:", 24 | "What would you like to write scripts with? JavaScript", 25 | "What would you like to write markup with? HTML", 26 | "What would you like to write stylesheets with? CSS", 27 | "What Angular router would you like to use? ngRoute", 28 | "Would you like to include Bootstrap? Yes", 29 | "Would you like to include UI Bootstrap? Yes", 30 | "Would you like to use MongoDB with Mongoose for data modeling? Yes", 31 | "Would you scaffold out an authentication boilerplate? Yes", 32 | "Would you like to include additional oAuth strategies? Twitter", 33 | "Would you like to use socket.io? No", 34 | "May bower anonymously report usage statistics to improve the tool over time? (Y/n) Y", 35 | "You may get an error similar to ERR! EEXIST, open ‘/home/ubuntu/.npm. This is caused when Cloud9 runs out of memory and kills an install. If you get this, simply re-run this process with the command yo angular-fullstack. You will then be asked a few questions regarding the re-install. Answer them as follows:", 36 | "Existing .yo-rc configuration found, would you like to use it? (Y/n) Y", 37 | "Overwrite client/favicon.ico? (Ynaxdh) Y", 38 | "To finish the installation run the commands: bower install && npm install", 39 | "To start MongoDB, run the following commands in your terminal: mkdir data && echo 'mongod --bind_ip=$IP --dbpath=data --nojournal --rest \"$@\"' > mongod && chmod a+x mongod && ./mongod", 40 | "You will want to open up a new terminal to work from by clicking on the + icon and select New Terminal", 41 | "Start the application by running the following command in your new terminal window: grunt serve", 42 | "Wait for the following message to appear: xdg-open: no method available for opening 'http://localhost:8080' . Now you can open the internal Cloud9 browser. To launch the browser select Preview in the toolbar then select the dropdown option Preview Running Application.", 43 | "Turn the folder in which your application is running into a Git repository by running the following commands: git init && git add . && git commit -am 'initial commit'.", 44 | "Now we need to add your GitHub SSH key to c9.io. Click the \"Add-on Services\" button in the lower left of your C9 dashboard. Click \"activate\" next to the GitHub icon.", 45 | "A pop up will appear. Allow access to your account.", 46 | "While still on the dashboard, under “Account Settings”, click the link for \"Show your SSH key\". Copy the key to you clipboard.", 47 | "Sign in to http://github.com and navigate to the GitHub SSH settings page. Click the \"Add SSH Key\". Give your key the title \"cloud 9\". Paste your SSH Key into the \"Key\" box, then click \"Add Key\".", 48 | "Create a new GitHub repository by and clicking on the + button next to your username in the upper-right hand side of your screen, then selecting \"New Repository\".", 49 | "Enter a project name, then click the \"Create Repository\" button.", 50 | "Find the \"...or push an existing repository from the command line\" section and click the Copy to Clipboard button beside it.", 51 | "Paste the commands from your clipboard into the Cloud9 terminal prompt. This will push your changes to your repository on Cloud 9 up to GitHub.", 52 | "Check back on your GitHub profile to verify the changes were successfully pushed up to GitHub.", 53 | "Now let's push your code to Heroku. If you don't already have a Heroku account, create one at http://heroku.com. You shouldn't be charged for anything, but you will need to add your credit card information to your Heroku before you will be able to use Heroku's free MongoLab add on.", 54 | "Before you publish to Heroku, you should free up as much memory as possible on Cloud9. In each of the Cloud9 terminal prompt tabs where MongoDB and Grunt are running, press the control + c hotkey to shut down these processes.", 55 | "Run the following command in a Cloud9 terminal prompt tab: npm install grunt-contrib-imagemin --save-dev && npm install --save-dev && heroku login. At this point, the terminal will prompt you to log in to Heroku from the command line.", 56 | "Now run yo angular-fullstack:heroku. You can choose a name for your Heroku project, or Heroku will create a random one for you. You can choose whether you want to deploy to servers the US or the EU.", 57 | "Set the config flag for your Heroku environment and add MongoLab for your MongoDB instance by running the following command: cd ~/workspace/dist && heroku config:set NODE_ENV=production && heroku addons:create mongolab.", 58 | "As you build your app, you should frequently commit changes to your codebase. Make sure you're in the ~/workspace directory by running cd ~/workspace. Then you can this code to stage the changes to your changes and commit them: git commit -am \"your commit message\". Note that you should replace \"your commit message\" with a short summary of the changes you made to your code, such as \"added a records controller and corresponding routes\".", 59 | "You can push these new commits to GitHub by running git push origin master, and to Heroku by running grunt --force && grunt buildcontrol:heroku.", 60 | "If you need further guidance on using Yeoman Angular-Fullstack Generator, check out: https://github.com/clnhll/guidetobasejumps.", 61 | "Now you're ready to move on to your first Basejump. Click the \"I've completed this challenge\" and move on." 62 | ], 63 | "challengeType": 2, 64 | "tests": [] 65 | }, 66 | { 67 | "id": "bd7158d8c443eddfaeb5bdef", 68 | "name": "Basejump: Build a Voting App", 69 | "dashedName": "basejump-build-a-voting-app", 70 | "difficulty": 2.01, 71 | "challengeSeed": ["133315786"], 72 | "description": [ 73 | "Objective: Build a full stack JavaScript app that successfully reverse-engineers this: http://votingapp.herokuapp.com/ and deploy it to Heroku.", 74 | "Note that for each Basejump, you should create a new GitHub repository and a new Heroku project. If you can't remember how to do this, revisit http://freecodecamp.org/challenges/get-set-for-basejumps.", 75 | "As you build your app, you should frequently commit changes to your codebase. You can do this by running git commit -am \"your commit message\". Note that you should replace \"your commit message\" with a brief summary of the changes you made to your code.", 76 | "You can push these new commits to GitHub by running git push origin master, and to Heroku by running grunt --force && grunt buildcontrol:heroku.", 77 | "Here are the specific User Stories you should implement for this Basejump:", 78 | "User Story: As an authenticated user, I can keep my polls and come back later to access them.", 79 | "User Story: As an authenticated user, I can share my polls with my friends.", 80 | "User Story: As an authenticated user, I can see the aggregate results of my polls.", 81 | "User Story: As an authenticated user, I can delete polls that I decide I don't want anymore.", 82 | "User Story: As an authenticated user, I can create a poll with any number of possible items.", 83 | "Bonus User Story: As an unauthenticated user, I can see everyone's polls, but I can't vote on anything.", 84 | "Bonus User Story: As an unauthenticated or authenticated user, I can see the results of polls in chart form. (This could be implemented using Chart.js or Google Charts.)", 85 | "Bonus User Story: As an authenticated user, if I don't like the options on a poll, I can create a new option.", 86 | "If you need further guidance on using Yeoman Angular-Fullstack Generator, check out: https://github.com/clnhll/guidetobasejumps.", 87 | "Once you've finished implementing these user stories, click the \"I've completed this challenge\" button and enter the URLs for both your GitHub repository and your live app running on Heroku. If you pair programmed with a friend, enter his or her Free Code Camp username as well so that you both get credit for completing it.", 88 | "If you'd like immediate feedback on your project, click this button and paste in a link to your Heroku project. Otherwise, we'll review it before you start your nonprofit projects.

Click here then add your link to your tweet's text" 89 | ], 90 | "challengeType": 4, 91 | "tests": [], 92 | "nameCn": "", 93 | "descriptionCn": [], 94 | "nameFr": "", 95 | "descriptionFr": [], 96 | "nameRu": "", 97 | "descriptionRu": [], 98 | "nameEs": "", 99 | "descriptionEs": [], 100 | "namePt": "", 101 | "descriptionPt": [] 102 | }, 103 | { 104 | "id": "bd7158d8c443eddfaeb5bdff", 105 | "name": "Basejump: Build a Nightlife Coordination App", 106 | "dashedName": "basejump-build-a-nightlife-coordination-app", 107 | "difficulty": 2.02, 108 | "challengeSeed": ["133315781"], 109 | "description": [ 110 | "Objective: Build a full stack JavaScript app that successfully reverse-engineers this: http://whatsgoinontonight.herokuapp.com/ and deploy it to Heroku.", 111 | "Note that for each Basejump, you should create a new GitHub repository and a new Heroku project. If you can't remember how to do this, revisit http://freecodecamp.org/challenges/get-set-for-basejumps.", 112 | "As you build your app, you should frequently commit changes to your codebase. You can do this by running git commit -am \"your commit message\". Note that you should replace \"your commit message\" with a brief summary of the changes you made to your code.", 113 | "You can push these new commits to GitHub by running git push origin master, and to Heroku by running grunt --force && grunt buildcontrol:heroku.", 114 | "Here are the specific User Stories you should implement for this Basejump:", 115 | "User Story: As an unauthenticated user, I can view all bars in my area.", 116 | "User Story: As an authenticated user, I can add myself to a bar to indicate I am going there tonight.", 117 | "User Story: As an authenticated user, I can remove myself from a bar if I no longer want to go there.", 118 | "Bonus User Story: As an unauthenticated user, when I login I should not have to search again.", 119 | "Hint: Try using the Yelp API to find venues in the cities your users search for.", 120 | "If you need further guidance on using Yeoman Angular-Fullstack Generator, check out: https://github.com/clnhll/guidetobasejumps.", 121 | "Once you've finished implementing these user stories, click the \"I've completed this challenge\" button and enter the URLs for both your GitHub repository and your live app running on Heroku. If you pair programmed with a friend, enter his or her Free Code Camp username as well so that you both get credit for completing it.", 122 | "If you'd like immediate feedback on your project, click this button and paste in a link to your Heroku project. Otherwise, we'll review it before you start your nonprofit projects.

Click here then add your link to your tweet's text" 123 | ], 124 | "challengeType": 4, 125 | "tests": [], 126 | "nameCn": "", 127 | "descriptionCn": [], 128 | "nameFr": "", 129 | "descriptionFr": [], 130 | "nameRu": "", 131 | "descriptionRu": [], 132 | "nameEs": "", 133 | "descriptionEs": [], 134 | "namePt": "", 135 | "descriptionPt": [] 136 | }, 137 | { 138 | "id": "bd7158d8c443eddfaeb5bd0e", 139 | "name": "Basejump: Chart the Stock Market", 140 | "dashedName": "basejump-chart-the-stock-market", 141 | "difficulty": 2.03, 142 | "challengeSeed": ["133315787"], 143 | "description": [ 144 | "Objective: Build a full stack JavaScript app that successfully reverse-engineers this: http://stockstream.herokuapp.com/ and deploy it to Heroku.", 145 | "Note that for each Basejump, you should create a new GitHub repository and a new Heroku project. If you can't remember how to do this, revisit http://freecodecamp.org/challenges/get-set-for-basejumps.", 146 | "As you build your app, you should frequently commit changes to your codebase. You can do this by running git commit -am \"your commit message\". Note that you should replace \"your commit message\" with a brief summary of the changes you made to your code.", 147 | "You can push these new commits to GitHub by running git push origin master, and to Heroku by running grunt --force && grunt buildcontrol:heroku.", 148 | "Here are the specific User Stories you should implement for this Basejump:", 149 | "User Story: As a user, I can view a graph displaying the recent trend lines for each added stock.", 150 | "User Story: As a user, I can add new stocks by their symbol name.", 151 | "User Story: As a user, I can remove stocks.", 152 | "Bonus User Story: As a user, I can see changes in real-time when any other user adds or removes a stock.", 153 | "If you need further guidance on using Yeoman Angular-Fullstack Generator, check out: https://github.com/clnhll/guidetobasejumps.", 154 | "Once you've finished implementing these user stories, click the \"I've completed this challenge\" button and enter the URLs for both your GitHub repository and your live app running on Heroku. If you pair programmed with a friend, enter his or her Free Code Camp username as well so that you both get credit for completing it.", 155 | "If you'd like immediate feedback on your project, click this button and paste in a link to your Heroku project. Otherwise, we'll review it before you start your nonprofit projects.

Click here then add your link to your tweet's text" 156 | ], 157 | "challengeType": 4, 158 | "tests": [], 159 | "nameCn": "", 160 | "descriptionCn": [], 161 | "nameFr": "", 162 | "descriptionFr": [], 163 | "nameRu": "", 164 | "descriptionRu": [], 165 | "nameEs": "", 166 | "descriptionEs": [], 167 | "namePt": "", 168 | "descriptionPt": [] 169 | }, 170 | { 171 | "id": "bd7158d8c443eddfaeb5bd0f", 172 | "name": "Basejump: Manage a Book Trading Club", 173 | "dashedName": "basejump-manage-a-book-trading-club", 174 | "difficulty": 2.04, 175 | "challengeSeed": ["133316032"], 176 | "description": [ 177 | "Objective: Build a full stack JavaScript app that successfully reverse-engineers this: http://bookjump.herokuapp.com/ and deploy it to Heroku.", 178 | "Note that for each Basejump, you should create a new GitHub repository and a new Heroku project. If you can't remember how to do this, revisit http://freecodecamp.org/challenges/get-set-for-basejumps.", 179 | "As you build your app, you should frequently commit changes to your codebase. You can do this by running git commit -am \"your commit message\". Note that you should replace \"your commit message\" with a brief summary of the changes you made to your code.", 180 | "You can push these new commits to GitHub by running git push origin master, and to Heroku by running grunt --force && grunt buildcontrol:heroku.", 181 | "Here are the specific User Stories you should implement for this Basejump:", 182 | "User Story: As an authenticated user, I can view all books posted by every user.", 183 | "User Story: As an authenticated user, I can add a new book.", 184 | "User Story: As an authenticated user, I can update my settings to store my full name, city, and state.", 185 | "Bonus User Story: As an authenticated user, I can propose a trade and wait for the other user to accept the trade.", 186 | "If you need further guidance on using Yeoman Angular-Fullstack Generator, check out: https://github.com/clnhll/guidetobasejumps.", 187 | "Once you've finished implementing these user stories, click the \"I've completed this challenge\" button and enter the URLs for both your GitHub repository and your live app running on Heroku. If you pair programmed with a friend, enter his or her Free Code Camp username as well so that you both get credit for completing it.", 188 | "If you'd like immediate feedback on your project, click this button and paste in a link to your Heroku project. Otherwise, we'll review it before you start your nonprofit projects.

Click here then add your link to your tweet's text" 189 | ], 190 | "challengeType": 4, 191 | "tests": [], 192 | "nameCn": "", 193 | "descriptionCn": [], 194 | "nameFr": "", 195 | "descriptionFr": [], 196 | "nameRu": "", 197 | "descriptionRu": [], 198 | "nameEs": "", 199 | "descriptionEs": [], 200 | "namePt": "", 201 | "descriptionPt": [] 202 | }, 203 | { 204 | "id": "bd7158d8c443eddfaeb5bdee", 205 | "name": "Basejump: Build a Pinterest Clone", 206 | "dashedName": "basejump-build-a-pinterest-clone", 207 | "difficulty": 2.05, 208 | "challengeSeed": ["133315784"], 209 | "description": [ 210 | "Objective: Build a full stack JavaScript app that successfully reverse-engineers this: http://stark-lowlands-3680.herokuapp.com/ and deploy it to Heroku.", 211 | "Note that for each Basejump, you should create a new GitHub repository and a new Heroku project. If you can't remember how to do this, revisit http://freecodecamp.org/challenges/get-set-for-basejumps.", 212 | "As you build your app, you should frequently commit changes to your codebase. You can do this by running git commit -am \"your commit message\". Note that you should replace \"your commit message\" with a brief summary of the changes you made to your code.", 213 | "You can push these new commits to GitHub by running git push origin master, and to Heroku by running grunt --force && grunt buildcontrol:heroku.", 214 | "Here are the specific User Stories you should implement for this Basejump:", 215 | "User Story: As an unauthenticated user, I can login with Twitter.", 216 | "User Story: As an authenticated user, I can link to images.", 217 | "User Story: As an authenticated user, I can delete images that I've linked to.", 218 | "User Story: As an authenticated user, I can see a Pinterest-style wall of all the images I've linked to.", 219 | "User Story: As an unauthenticated user, I can browse other users' walls of images.", 220 | "Bonus User Story: As an authenticated user, if I upload an image that is broken, it will be replaced by a placeholder image. (can use jQuery broken image detection)", 221 | "Hint: Masonry.js is a library that allows for Pinterest-style image grids.", 222 | "If you need further guidance on using Yeoman Angular-Fullstack Generator, check out: https://github.com/clnhll/guidetobasejumps.", 223 | "Once you've finished implementing these user stories, click the \"I've completed this challenge\" button and enter the URLs for both your GitHub repository and your live app running on Heroku. If you pair programmed with a friend, enter his or her Free Code Camp username as well so that you both get credit for completing it.", 224 | "If you'd like immediate feedback on your project, click this button and paste in a link to your Heroku project. Otherwise, we'll review it before you start your nonprofit projects.

Click here then add your link to your tweet's text" 225 | ], 226 | "challengeType": 4, 227 | "tests": [], 228 | "nameCn": "", 229 | "descriptionCn": [], 230 | "nameFr": "", 231 | "descriptionFr": [], 232 | "nameRu": "", 233 | "descriptionRu": [], 234 | "nameEs": "", 235 | "descriptionEs": [], 236 | "namePt": "", 237 | "descriptionPt": [] 238 | } 239 | ] 240 | } 241 | -------------------------------------------------------------------------------- /data/seed/challenges/basic-ziplines.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Basic Front End Development Projects", 3 | "order": 0.008, 4 | "challenges": [ 5 | { 6 | "id": "bd7158d8c442eddfbeb5bd1f", 7 | "name": "Waypoint: Get Set for Ziplines", 8 | "dashedName": "waypoint-get-set-for-ziplines", 9 | "difficulty": 1.00, 10 | "challengeSeed": ["125658022"], 11 | "description": [ 12 | "Now you're ready to start our Zipline challenges. These front-end development challenges will give you many opportunities to apply the HTML, CSS, jQuery and JavaScript you've learned to build static (database-less) applications.", 13 | "For many of these challenges, you will be using JSON data from external API endpoints, such as Twitch.tv and Twitter. Note that you don't need to have a database to use these data.", 14 | "The easiest way to manipulate these data is with jQuery $.getJSON().", 15 | "Whatever you do, don't get discouraged! Remember to use RSAP if you get stuck.", 16 | "We'll build these challenges using CodePen, a popular tool for creating, sharing, and discovering static web applications.", 17 | "Go to http://codepen.io and create an account.", 18 | "Click your user image in the top right corner, then click the \"New pen\" button that drops down.", 19 | "Drag the windows around and press the buttons in the lower-right hand corner to change the orientation to suit your preference.", 20 | "Click the gear next to CSS. Click the \"Quick-add...\" select box and choose Bootstrap.", 21 | "Verify that bootstrap is active by adding the following code to your HTML: <h1 class='text-primary'>Hello CodePen!</h1>. The text's color should be Bootstrap blue.", 22 | "Click the gear next to JavaScript. Click the \"Quick-add...\" select box and choose jQuery.", 23 | "Now add the following code to your JavaScript: $(document).ready(function() { $('.text-primary').text('Hi CodePen!') });. Click the \"Save\" button at the top. Your \"Hello CodePen!\" should change to \"Hi CodePen!\". This means that jQuery is working.", 24 | "You can use this CodePen that you've just created as a starting point for your Ziplines. Just click the \"fork\" button at the top of your CodePen and it will create a duplicate CodePen.", 25 | "Now you're ready for your first Zipline. Click the \"I've completed this challenge\" button." 26 | ], 27 | "challengeType": 2, 28 | "tests": [], 29 | "nameCn": "", 30 | "descriptionCn": [], 31 | "nameFr": "", 32 | "descriptionFr": [], 33 | "nameRu": "", 34 | "descriptionRu": [], 35 | "nameEs": "", 36 | "descriptionEs": [], 37 | "namePt": "", 38 | "descriptionPt": [] 39 | }, 40 | { 41 | "id": "bd7158d8c242eddfaeb5bd13", 42 | "name": "Zipline: Build a Personal Portfolio Webpage", 43 | "dashedName": "zipline-build-a-personal-portfolio-webpage", 44 | "difficulty": 1.01, 45 | "challengeSeed": ["133315782"], 46 | "description": [ 47 | "Objective: Build a CodePen.io app that successfully reverse-engineers this: http://codepen.io/ThiagoFerreir4/full/eNMxEp.", 48 | "Rule #1: Don't look at the example project's code on CodePen. Figure it out for yourself.", 49 | "Rule #2: You may use whichever libraries or APIs you need.", 50 | "Rule #3: Reverse engineer the example project's functionality, and also feel free to personalize it.", 51 | "Here are the user stories you must enable, and optional bonus user stories:", 52 | "User Story: As a user, I can access all of the portfolio webpage's content just by scrolling.", 53 | "User Story: As a user, I can click different buttons that will take me to the portfolio creator's different social media pages.", 54 | "User Story: As a user, I can see thumbnail images of different projects the portfolio creator has built (if you don't haven't built any websites before, use placeholders.)", 55 | "Bonus User Story: As a user, I navigate to different sections of the webpage by clicking buttons in the navigation.", 56 | "Don't worry if you don't have anything to showcase on your portfolio yet - you will build several several apps on the next few CodePen challenges, and can come back and update your portfolio later.", 57 | "There are many great portfolio templates out there, but for this challenge, you'll need to build a portfolio page yourself. Using Bootstrap will make this much easier for you.", 58 | "Note that CodePen.io overrides the Window.open() function, so if you want to open windows using jquery, you will need to target invisible anchor elements like this one: <a target='_blank'&rt;.", 59 | "Remember to use RSAP if you get stuck.", 60 | "When you are finished, click the \"I've completed this challenge\" button and include a link to your CodePen. If you pair programmed, you should also include the Free Code Camp username of your pair.", 61 | "If you'd like immediate feedback on your project, click this button and paste in a link to your CodePen project. Otherwise, we'll review it before you start your nonprofit projects.

Click here then add your link to your tweet's text" 62 | ], 63 | "challengeType": 3, 64 | "tests": [], 65 | "nameCn": "", 66 | "descriptionCn": [], 67 | "nameFr": "", 68 | "descriptionFr": [], 69 | "nameRu": "", 70 | "descriptionRu": [], 71 | "nameEs": "", 72 | "descriptionEs": [], 73 | "namePt": "", 74 | "descriptionPt": [] 75 | }, 76 | { 77 | "id": "bd7158d8c442eddfaeb5bd13", 78 | "name": "Zipline: Build a Random Quote Machine", 79 | "dashedName": "zipline-build-a-random-quote-machine", 80 | "difficulty": 1.02, 81 | "challengeSeed": ["126415122"], 82 | "description": [ 83 | "Objective: Build a CodePen.io app that successfully reverse-engineers this: http://codepen.io/AdventureBear/full/vEoVMw.", 84 | "Rule #1: Don't look at the example project's code on CodePen. Figure it out for yourself.", 85 | "Rule #2: You may use whichever libraries or APIs you need.", 86 | "Rule #3: Reverse engineer the example project's functionality, and also feel free to personalize it.", 87 | "Here are the user stories you must enable, and optional bonus user stories:", 88 | "User Story: As a user, I can click a button to show me a new random quote.", 89 | "Bonus User Story: As a user, I can press a button to tweet out a quote.", 90 | "Note that you can either put your quotes into an array and show them at random, or use an API to get quotes, such as http://forismatic.com/en/api/.", 91 | "Remember to use RSAP if you get stuck.", 92 | "When you are finished, click the \"I've completed this challenge\" button and include a link to your CodePen. If you pair programmed, you should also include the Free Code Camp username of your pair.", 93 | "If you'd like immediate feedback on your project, click this button and paste in a link to your CodePen project. Otherwise, we'll review it before you start your nonprofit projects.

Click here then add your link to your tweet's text" 94 | ], 95 | "challengeType": 3, 96 | "tests": [], 97 | "nameCn": "", 98 | "descriptionCn": [], 99 | "nameFr": "", 100 | "descriptionFr": [], 101 | "nameRu": "", 102 | "descriptionRu": [], 103 | "nameEs": "", 104 | "descriptionEs": [], 105 | "namePt": "", 106 | "descriptionPt": [] 107 | }, 108 | { 109 | "id": "bd7158d8c442eddfaeb5bd10", 110 | "name": "Zipline: Show the Local Weather", 111 | "dashedName": "zipline-show-the-local-weather", 112 | "difficulty": 1.03, 113 | "challengeSeed": ["126415127"], 114 | "description": [ 115 | "Objective: Build a CodePen.io app that successfully reverse-engineers this: http://codepen.io/AdventureBear/full/yNBJRj.", 116 | "Rule #1: Don't look at the example project's code on CodePen. Figure it out for yourself.", 117 | "Rule #2: You may use whichever libraries or APIs you need.", 118 | "Rule #3: Reverse engineer the example project's functionality, and also feel free to personalize it.", 119 | "Here are the user stories you must enable, and optional bonus user stories:", 120 | "User Story: As a user, I can see the weather in my current location.", 121 | "Bonus User Story: As a user, I can see an icon depending on the temperature..", 122 | "Bonus User Story: As a user, I see a different background image depending on the temperature (e.g. snowy mountain, hot desert).", 123 | "Bonus User Story: As a user, I can push a button to toggle between Fahrenheit and Celsius.", 124 | "Remember to use RSAP if you get stuck.", 125 | "When you are finished, click the \"I've completed this challenge\" button and include a link to your CodePen. If you pair programmed, you should also include the Free Code Camp username of your pair.", 126 | "If you'd like immediate feedback on your project, click this button and paste in a link to your CodePen project. Otherwise, we'll review it before you start your nonprofit projects.

Click here then add your link to your tweet's text" 127 | ], 128 | "challengeType": 3, 129 | "tests": [], 130 | "nameCn": "", 131 | "descriptionCn": [], 132 | "nameFr": "", 133 | "descriptionFr": [], 134 | "nameRu": "", 135 | "descriptionRu": [], 136 | "nameEs": "", 137 | "descriptionEs": [], 138 | "namePt": "", 139 | "descriptionPt": [] 140 | }, 141 | { 142 | "id": "bd7158d8c442eddfaeb5bd0f", 143 | "name": "Zipline: Build a Pomodoro Clock", 144 | "dashedName": "zipline-build-a-pomodoro-clock", 145 | "difficulty": 1.04, 146 | "challengeSeed": ["126411567"], 147 | "description": [ 148 | "Objective: Build a CodePen.io app that successfully reverse-engineers this: http://codepen.io/GeoffStorbeck/full/RPbGxZ/.", 149 | "Rule #1: Don't look at the example project's code on CodePen. Figure it out for yourself.", 150 | "Rule #2: You may use whichever libraries or APIs you need.", 151 | "Rule #3: Reverse engineer the example project's functionality, and also feel free to personalize it.", 152 | "Here are the user stories you must enable, and optional bonus user stories:", 153 | "User Story: As a user, I can start a 25 minute pomodoro, and the timer will go off once 25 minutes has elapsed.", 154 | "Bonus User Story: As a user, I can reset the clock for my next pomodoro.", 155 | "Bonus User Story: As a user, I can customize the length of each pomodoro.", 156 | "Remember to use RSAP if you get stuck.", 157 | "When you are finished, click the \"I've completed this challenge\" button and include a link to your CodePen. If you pair programmed, you should also include the Free Code Camp username of your pair.", 158 | "If you'd like immediate feedback on your project, click this button and paste in a link to your CodePen project. Otherwise, we'll review it before you start your nonprofit projects.

Click here then add your link to your tweet's text" 159 | ], 160 | "challengeType": 3, 161 | "tests": [], 162 | "nameCn": "", 163 | "descriptionCn": [], 164 | "nameFr": "", 165 | "descriptionFr": [], 166 | "nameRu": "", 167 | "descriptionRu": [], 168 | "nameEs": "", 169 | "descriptionEs": [], 170 | "namePt": "", 171 | "descriptionPt": [] 172 | }, 173 | { 174 | "id": "bd7158d8c442eddfaeb5bd1f", 175 | "name": "Zipline: Use the Twitch.tv JSON API", 176 | "dashedName": "zipline-use-the-twitchtv-json-api", 177 | "difficulty": 1.05, 178 | "challengeSeed": ["126411564"], 179 | "description": [ 180 | "Objective: Build a CodePen.io app that successfully reverse-engineers this: http://codepen.io/GeoffStorbeck/full/GJKRxZ.", 181 | "Rule #1: Don't look at the example project's code on CodePen. Figure it out for yourself.", 182 | "Rule #2: You may use whichever libraries or APIs you need.", 183 | "Rule #3: Reverse engineer the example project's functionality, and also feel free to personalize it.", 184 | "Here are the user stories you must enable, and optional bonus user stories:", 185 | "User Story: As a user, I can see whether Free Code Camp is currently streaming on Twitch.tv.", 186 | "User Story: As a user, I can click the status output and be sent directly to the Free Code Camp's Twitch.tv channel.", 187 | "User Story: As a user, if Free Code Camp is streaming, I can see additional details about what they are streaming.", 188 | "Bonus User Story: As a user, I can search through the streams listed.", 189 | "Hint: Here's an example call to Twitch.tv's JSON API: https://api.twitch.tv/kraken/streams/freecodecamp.", 190 | "Hint: The relevant documentation about this API call is here: https://github.com/justintv/Twitch-API/blob/master/v3_resources/streams.md#get-streamschannel.", 191 | "Hint: Here's an array of the Twitch.tv usernames of people who regularly stream coding: [\"freecodecamp\", \"storbeck\", \"terakilobyte\", \"habathcx\",\"RobotCaleb\",\"comster404\",\"brunofin\",\"thomasballinger\",\"noobs2ninjas\",\"beohoff\"]", 192 | "Remember to use RSAP if you get stuck.", 193 | "When you are finished, click the \"I've completed this challenge\" button and include a link to your CodePen. If you pair programmed, you should also include the Free Code Camp username of your pair.", 194 | "If you'd like immediate feedback on your project, click this button and paste in a link to your CodePen project. Otherwise, we'll review it before you start your nonprofit projects.

Click here then add your link to your tweet's text" 195 | ], 196 | "challengeType": 3, 197 | "tests": [], 198 | "nameCn": "", 199 | "descriptionCn": [], 200 | "nameFr": "", 201 | "descriptionFr": [], 202 | "nameRu": "", 203 | "descriptionRu": [], 204 | "nameEs": "", 205 | "descriptionEs": [], 206 | "namePt": "", 207 | "descriptionPt": [] 208 | } 209 | ] 210 | } 211 | -------------------------------------------------------------------------------- /data/seed/challenges/expert-bonfires.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Expert Algorithm Scripting", 3 | "order": 0.013, 4 | "challenges": [ 5 | { 6 | "id": "aff0395860f5d3034dc0bfc9", 7 | "name": "Bonfire: Validate US Telephone Numbers", 8 | "dashedName": "bonfire-validate-us-telephone-numbers", 9 | "difficulty": "4.01", 10 | "description": [ 11 | "Return true if the passed string is a valid US phone number", 12 | "The user may fill out the form field any way they choose as long as it is a valid US number. The following are all valid formats for US numbers:", 13 | "555-555-5555, (555)555-5555, (555) 555-5555, 555 555 5555, 5555555555, 1 555 555 5555", 14 | "For this challenge you will be presented with a string such as \"800-692-7753\" or \"8oo-six427676;laskdjf\". Your job is to validate or reject the US phone number based on any combination of the formats provided above. The area code is required. If the country code is provided, you must confirm that the country code is \"1\". Return true if the string is a valid US phone number; otherwise false.", 15 | "Remember to use RSAP if you get stuck. Try to pair program. Write your own code." 16 | ], 17 | "tests": [ 18 | "expect(telephoneCheck(\"555-555-5555\")).to.be.a(\"boolean\");", 19 | "assert.deepEqual(telephoneCheck(\"1 555-555-5555\"), true);", 20 | "assert.deepEqual(telephoneCheck(\"1 (555) 555-5555\"), true);", 21 | "assert.deepEqual(telephoneCheck(\"5555555555\"), true);", 22 | "assert.deepEqual(telephoneCheck(\"555-555-5555\"), true);", 23 | "assert.deepEqual(telephoneCheck(\"(555)555-5555\"), true);", 24 | "assert.deepEqual(telephoneCheck(\"1(555)555-5555\"), true);", 25 | "assert.deepEqual(telephoneCheck(\"1 555 555 5555\"), true);", 26 | "assert.deepEqual(telephoneCheck(\"555-555-5555\"), true);", 27 | "assert.deepEqual(telephoneCheck(\"1 456 789 4444\"), true);", 28 | "assert.deepEqual(telephoneCheck(\"123**&!!asdf#\"), false);", 29 | "assert.deepEqual(telephoneCheck(\"55555555\"), false);", 30 | "assert.deepEqual(telephoneCheck(\"(6505552368)\"), false);", 31 | "assert.deepEqual(telephoneCheck(\"2 (757) 622-7382\"), false);", 32 | "assert.deepEqual(telephoneCheck(\"0 (757) 622-7382\"), false);", 33 | "assert.deepEqual(telephoneCheck(\"-1 (757) 622-7382\"), false);", 34 | "assert.deepEqual(telephoneCheck(\"2 757 622-7382\"), false);", 35 | "assert.deepEqual(telephoneCheck(\"10 (757) 622-7382\"), false);", 36 | "assert.deepEqual(telephoneCheck(\"27576227382\"), false);", 37 | "assert.deepEqual(telephoneCheck(\"(275)76227382\"), false);", 38 | "assert.deepEqual(telephoneCheck(\"2(757)6227382\"), false);", 39 | "assert.deepEqual(telephoneCheck(\"2(757)622-7382\"), false);" 40 | ], 41 | "challengeSeed": [ 42 | "function telephoneCheck(str) {", 43 | " // Good luck!", 44 | " return true;", 45 | "}", 46 | "", 47 | "", 48 | "", 49 | "telephoneCheck(\"555-555-5555\");" 50 | ], 51 | "MDNlinks": [ 52 | "RegExp" 53 | ], 54 | "challengeType": 5, 55 | "nameCn": "", 56 | "descriptionCn": [], 57 | "nameFr": "", 58 | "descriptionFr": [], 59 | "nameRu": "", 60 | "descriptionRu": [], 61 | "nameEs": "", 62 | "descriptionEs": [], 63 | "namePt": "", 64 | "descriptionPt": [] 65 | }, 66 | { 67 | "id": "a3f503de51cf954ede28891d", 68 | "name": "Bonfire: Symmetric Difference", 69 | "dashedName": "bonfire-symmetric-difference", 70 | "difficulty": "4.02", 71 | "description": [ 72 | "Create a function that takes two or more arrays and returns an array of the symmetric difference of the provided arrays.", 73 | "The mathematical term symmetric difference refers to the elements in two sets that are in either the first or second set, but not in both.", 74 | "Remember to use RSAP if you get stuck. Try to pair program. Write your own code." 75 | ], 76 | "challengeSeed": [ 77 | "function sym(args) {", 78 | " return arguments;", 79 | "}", 80 | "", 81 | "sym([1, 2, 3], [5, 2, 1, 4]);" 82 | ], 83 | "tests": [ 84 | "expect(sym([1, 2, 3], [5, 2, 1, 4])).to.equal([3, 5, 4]);", 85 | "assert.deepEqual(sym([1, 2, 5], [2, 3, 5], [3, 4, 5]), [1, 4, 5], 'should return the symmetric difference of the given arrays');", 86 | "assert.deepEqual(sym([1, 1, 2, 5], [2, 2, 3, 5], [3, 4, 5, 5]), [1, 4, 5], 'should return an array of unique values');", 87 | "assert.deepEqual(sym([1, 1]), [1], 'should return an array of unique values');" 88 | ], 89 | "MDNlinks": [ 90 | "Array.reduce()", 91 | "Symmetric Difference" 92 | ], 93 | "challengeType": 5, 94 | "nameCn": "", 95 | "descriptionCn": [], 96 | "nameFr": "", 97 | "descriptionFr": [], 98 | "nameRu": "", 99 | "descriptionRu": [], 100 | "nameEs": "", 101 | "descriptionEs": [], 102 | "namePt": "", 103 | "descriptionPt": [] 104 | }, 105 | { 106 | "id": "aa2e6f85cab2ab736c9a9b24", 107 | "name": "Bonfire: Exact Change", 108 | "dashedName": "bonfire-exact-change", 109 | "difficulty": "4.03", 110 | "description": [ 111 | "Design a cash register drawer function that accepts purchase price as the first argument, payment as the second argument, and cash-in-drawer (cid) as the third argument.", 112 | "cid is a 2d array listing available currency.", 113 | "Return the string \"Insufficient Funds\" if cash-in-drawer is less than the change due. Return the string \"Closed\" if cash-in-drawer is equal to the change due.", 114 | "Otherwise, return change in coin and bills, sorted in highest to lowest order.", 115 | "Remember to use RSAP if you get stuck. Try to pair program. Write your own code." 116 | ], 117 | "challengeSeed": [ 118 | "function drawer(price, cash, cid) {", 119 | " var change;", 120 | " // Here is your change, ma'am.", 121 | " return change;", 122 | "}", 123 | "", 124 | "// Example cash-in-drawer array:", 125 | "// [['PENNY', 1.01],", 126 | "// ['NICKEL', 2.05],", 127 | "// ['DIME', 3.10],", 128 | "// ['QUARTER', 4.25],", 129 | "// ['ONE', 90.00],", 130 | "// ['FIVE', 55.00],", 131 | "// ['TEN', 20.00],", 132 | "// ['TWENTY', 60.00],", 133 | "// ['ONE HUNDRED', 100.00]]", 134 | "", 135 | "drawer(19.50, 20.00, [['PENNY', 1.01], ['NICKEL', 2.05], ['DIME', 3.10], ['QUARTER', 4.25], ['ONE', 90.00], ['FIVE', 55.00], ['TEN', 20.00], ['TWENTY', 60.00], ['ONE HUNDRED', 100.00]]);" 136 | ], 137 | "tests": [ 138 | "expect(drawer(19.50, 20.00, [['PENNY', 1.01], ['NICKEL', 2.05], ['DIME', 3.10], ['QUARTER', 4.25], ['ONE', 90.00], ['FIVE', 55.00], ['TEN', 20.00], ['TWENTY', 60.00], ['ONE HUNDRED', 100.00]])).to.be.a('array');", 139 | "expect(drawer(19.50, 20.00, [['PENNY', 0.01], ['NICKEL', 0], ['DIME', 0], ['QUARTER', 0], ['ONE', 0], ['FIVE', 0], ['TEN', 0], ['TWENTY', 0], ['ONE HUNDRED', 0]])).to.be.a('string');", 140 | "expect(drawer(19.50, 20.00, [['PENNY', 0.50], ['NICKEL', 0], ['DIME', 0], ['QUARTER', 0], ['ONE', 0], ['FIVE', 0], ['TEN', 0], ['TWENTY', 0], ['ONE HUNDRED', 0]])).to.be.a('string');", 141 | "assert.deepEqual(drawer(19.50, 20.00, [['PENNY', 1.01], ['NICKEL', 2.05], ['DIME', 3.10], ['QUARTER', 4.25], ['ONE', 90.00], ['FIVE', 55.00], ['TEN', 20.00], ['TWENTY', 60.00], ['ONE HUNDRED', 100.00]]), [['QUARTER', 0.50]], 'return correct change');", 142 | "assert.deepEqual(drawer(3.26, 100.00, [['PENNY', 1.01], ['NICKEL', 2.05], ['DIME', 3.10], ['QUARTER', 4.25], ['ONE', 90.00], ['FIVE', 55.00], ['TEN', 20.00], ['TWENTY', 60.00], ['ONE HUNDRED', 100.00]]), [['TWENTY', 60.00], ['TEN', 20.00], ['FIVE', 15], ['ONE', 1], ['QUARTER', 0.50], ['DIME', 0.20], ['PENNY', 0.04] ], 'return correct change with multiple coins and bills');", 143 | "assert.deepEqual(drawer(19.50, 20.00, [['PENNY', 0.01], ['NICKEL', 0], ['DIME', 0], ['QUARTER', 0], ['ONE', 0], ['FIVE', 0], ['TEN', 0], ['TWENTY', 0], ['ONE HUNDRED', 0]]), 'Insufficient Funds', 'insufficient funds');", 144 | "assert.deepEqual(drawer(19.50, 20.00, [['PENNY', 0.50], ['NICKEL', 0], ['DIME', 0], ['QUARTER', 0], ['ONE', 0], ['FIVE', 0], ['TEN', 0], ['TWENTY', 0], ['ONE HUNDRED', 0]]), \"Closed\", 'cash-in-drawer equals change');" 145 | ], 146 | "MDNlinks": [ 147 | "Global Object" 148 | ], 149 | "challengeType": 5, 150 | "nameCn": "", 151 | "descriptionCn": [], 152 | "nameFr": "", 153 | "descriptionFr": [], 154 | "nameRu": "", 155 | "descriptionRu": [], 156 | "nameEs": "", 157 | "descriptionEs": [], 158 | "namePt": "", 159 | "descriptionPt": [] 160 | }, 161 | { 162 | "id": "a56138aff60341a09ed6c480", 163 | "name": "Bonfire: Inventory Update", 164 | "dashedName": "bonfire-inventory-update", 165 | "difficulty": "4.04", 166 | "description": [ 167 | "Compare and update inventory stored in a 2d array against a second 2d array of a fresh delivery. Update current inventory item quantity, and if an item cannot be found, add the new item and quantity into the inventory array in alphabetical order.", 168 | "Remember to use RSAP if you get stuck. Try to pair program. Write your own code." 169 | ], 170 | "challengeSeed": [ 171 | "function inventory(arr1, arr2) {", 172 | " // All inventory must be accounted for or you're fired!", 173 | " return arr1;", 174 | "}", 175 | "", 176 | "// Example inventory lists", 177 | "var curInv = [", 178 | " [21, 'Bowling Ball'],", 179 | " [2, 'Dirty Sock'],", 180 | " [1, 'Hair Pin'],", 181 | " [5, 'Microphone']", 182 | "];", 183 | "", 184 | "var newInv = [", 185 | " [2, 'Hair Pin'],", 186 | " [3, 'Half-Eaten Apple'],", 187 | " [67, 'Bowling Ball'],", 188 | " [7, 'Toothpaste']", 189 | "];", 190 | "", 191 | "inventory(curInv, newInv);" 192 | ], 193 | "tests": [ 194 | "expect(inventory([[21, 'Bowling Ball'], [2, 'Dirty Sock'], [1, 'Hair Pin'], [5, 'Microphone']], [[2, 'Hair Pin'], [3, 'Half-Eaten Apple'], [67, 'Bowling Ball'], [7, 'Toothpaste']])).to.be.a('array');", 195 | "assert.equal(inventory([[21, 'Bowling Ball'], [2, 'Dirty Sock'], [1, 'Hair Pin'], [5, 'Microphone']], [[2, 'Hair Pin'], [3, 'Half-Eaten Apple'], [67, 'Bowling Ball'], [7, 'Toothpaste']]).length, 6);", 196 | "assert.deepEqual(inventory([[21, 'Bowling Ball'], [2, 'Dirty Sock'], [1, 'Hair Pin'], [5, 'Microphone']], [[2, 'Hair Pin'], [3, 'Half-Eaten Apple'], [67, 'Bowling Ball'], [7, 'Toothpaste']]), [[88, 'Bowling Ball'], [2, 'Dirty Sock'], [3, 'Hair Pin'], [3, 'Half-Eaten Apple'], [5, 'Microphone'], [7, 'Toothpaste']]);", 197 | "assert.deepEqual(inventory([[21, 'Bowling Ball'], [2, 'Dirty Sock'], [1, 'Hair Pin'], [5, 'Microphone']], []), [[21, 'Bowling Ball'], [2, 'Dirty Sock'], [1, 'Hair Pin'], [5, 'Microphone']]);", 198 | "assert.deepEqual(inventory([], [[2, 'Hair Pin'], [3, 'Half-Eaten Apple'], [67, 'Bowling Ball'], [7, 'Toothpaste']]), [[67, 'Bowling Ball'], [2, 'Hair Pin'], [3, 'Half-Eaten Apple'], [7, 'Toothpaste']]);", 199 | "assert.deepEqual(inventory([[0, 'Bowling Ball'], [0, 'Dirty Sock'], [0, 'Hair Pin'], [0, 'Microphone']], [[1, 'Hair Pin'], [1, 'Half-Eaten Apple'], [1, 'Bowling Ball'], [1, 'Toothpaste']]), [[1, 'Bowling Ball'], [0, 'Dirty Sock'], [1, 'Hair Pin'], [1, 'Half-Eaten Apple'], [0, 'Microphone'], [1, 'Toothpaste']]);" 200 | ], 201 | "MDNlinks": [ 202 | "Global Array Object" 203 | ], 204 | "challengeType": 5, 205 | "nameCn": "", 206 | "descriptionCn": [], 207 | "nameFr": "", 208 | "descriptionFr": [], 209 | "nameRu": "", 210 | "descriptionRu": [], 211 | "nameEs": "", 212 | "descriptionEs": [], 213 | "namePt": "", 214 | "descriptionPt": [] 215 | }, 216 | { 217 | "id": "a7bf700cd123b9a54eef01d5", 218 | "name": "Bonfire: No repeats please", 219 | "dashedName": "bonfire-no-repeats-please", 220 | "difficulty": "4.05", 221 | "description": [ 222 | "Return the number of total permutations of the provided string that don't have repeated consecutive letters.", 223 | "For example, 'aab' should return 2 because it has 6 total permutations, but only 2 of them don't have the same letter (in this case 'a') repeating.", 224 | "Remember to use RSAP if you get stuck. Try to pair program. Write your own code." 225 | ], 226 | "challengeSeed": [ 227 | "function permAlone(str) {", 228 | " return str;", 229 | "}", 230 | "", 231 | "permAlone('aab');" 232 | ], 233 | "tests": [ 234 | "expect(permAlone('aab')).to.be.a('number');", 235 | "expect(permAlone('aab')).to.equal(2);", 236 | "expect(permAlone('aaa')).to.equal(0);", 237 | "expect(permAlone('aabb')).to.equal(8);", 238 | "expect(permAlone('abcdefa')).to.equal(3600);", 239 | "expect(permAlone('abfdefa')).to.equal(2640);", 240 | "expect(permAlone('zzzzzzzz')).to.equal(0);" 241 | ], 242 | "MDNlinks": [ 243 | "Permutations", 244 | "RegExp" 245 | ], 246 | "challengeType": 5, 247 | "nameCn": "", 248 | "descriptionCn": [], 249 | "nameFr": "", 250 | "descriptionFr": [], 251 | "nameRu": "", 252 | "descriptionRu": [], 253 | "nameEs": "", 254 | "descriptionEs": [], 255 | "namePt": "", 256 | "descriptionPt": [] 257 | }, 258 | { 259 | "id": "a19f0fbe1872186acd434d5a", 260 | "name": "Bonfire: Friendly Date Ranges", 261 | "dashedName": "bonfire-friendly-date-ranges", 262 | "difficulty": "4.06", 263 | "description": [ 264 | "Implement a way of converting two dates into a more friendly date range that could be presented to a user.", 265 | "It must not show any redundant information in the date range.", 266 | "For example, if the year and month are the same then only the day range should be displayed.", 267 | "Secondly, if the starting year is the current year, and the ending year can be inferred by the reader, the year should be omitted.", 268 | "Input date is formatted as YYYY-MM-DD", 269 | "Remember to use RSAP if you get stuck. Try to pair program. Write your own code." 270 | ], 271 | "challengeSeed": [ 272 | "function friendly(str) {", 273 | " return str;", 274 | "}", 275 | "", 276 | "friendly(['2015-07-01', '2015-07-04']);" 277 | ], 278 | "tests": [ 279 | "assert.deepEqual(friendly(['2015-07-01', '2015-07-04']), ['July 1st','4th'], 'ending month should be omitted since it is already mentioned');", 280 | "assert.deepEqual(friendly(['2015-12-01', '2016-02-03']), ['December 1st','February 3rd'], 'two months apart can be inferred if it is the next year');", 281 | "assert.deepEqual(friendly(['2015-12-01', '2017-02-03']), ['December 1st, 2015','February 3rd, 2017']);", 282 | "assert.deepEqual(friendly(['2016-03-01', '2016-05-05']), ['March 1st','May 5th'], 'one month apart can be inferred it is the same year');", 283 | "assert.deepEqual(friendly(['2017-01-01', '2017-01-01']), ['January 1st, 2017'], 'since we do not duplicate only return once');", 284 | "assert.deepEqual(friendly(['2022-09-05', '2023-09-04']), ['September 5th, 2022','September 4th, 2023']);" 285 | ], 286 | "MDNlinks": [ 287 | "String.split()", 288 | "String.substr()", 289 | "parseInt()" 290 | ], 291 | "challengeType": 5, 292 | "nameCn": "", 293 | "descriptionCn": [], 294 | "nameFr": "", 295 | "descriptionFr": [], 296 | "nameRu": "", 297 | "descriptionRu": [], 298 | "nameEs": "", 299 | "descriptionEs": [], 300 | "namePt": "", 301 | "descriptionPt": [] 302 | } 303 | ] 304 | } 305 | -------------------------------------------------------------------------------- /dot-EXAMPLE.env: -------------------------------------------------------------------------------- 1 | # make a copy of this file and rename it to ".env" 2 | SERVER_ENV=demobot 3 | GITTER_USER_TOKEN=1ac342045f5a57d99fe537e58da78f6cba94f7db 4 | FCC_API_KEY=TESTAPIKEY 5 | 6 | LOG_LEVEL=10 7 | -------------------------------------------------------------------------------- /example.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "githubId": "GITHUB_USER_ID", 3 | "user": { 4 | "botname": "GITHUB_USER_ID", 5 | "appHost": "http://localhost:7000", 6 | "apiServer": "www.freecodecamp.org", 7 | "appRedirectUrl": "http://localhost:7891/login/callback" 8 | }, 9 | "rooms": [ 10 | { 11 | "title": "bothelp", 12 | "name": "GITHUB_USER_ID/test", 13 | "icon": "question", 14 | "topics": [ 15 | "chitchat", 16 | "bots", 17 | "bot-development", 18 | "camperbot" 19 | ] 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | const eslint = require('gulp-eslint'); 3 | const tape = require('gulp-tape'); 4 | const faucet = require('faucet'); 5 | const env = require('gulp-env'); 6 | 7 | require('dotenv').config({ path: '.env' }); 8 | 9 | const scripts = ['config/*.js', 'data/rooms/*.js', 'data/*.js', 'lib/**/*.js', 10 | 'test/*.js', 'app.js', 'gulpfile.js']; 11 | 12 | gulp.task('lint', () => { 13 | return gulp.src(scripts) 14 | .pipe(eslint()) 15 | .pipe(eslint.format()); 16 | }); 17 | 18 | gulp.task('test', ['set-env'], () => { 19 | return gulp.src('test/*.spec.js') 20 | .pipe(tape({ 21 | reporter: faucet() 22 | })); 23 | }); 24 | 25 | gulp.task('set-env', () => { 26 | env({ 27 | vars: { 28 | SERVER_ENV: 'test', 29 | LOG_LEVEL: 0 30 | } 31 | }); 32 | }); 33 | 34 | gulp.task('watch', () => { 35 | gulp.watch(scripts, ['lint', 'test']); 36 | }); 37 | 38 | gulp.task('default', ['lint', 'test', 'watch']); 39 | -------------------------------------------------------------------------------- /lib/app/Bonfires.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const Utils = require('../../lib/utils/Utils'); 5 | const InputWrap = require('../../lib/bot/InputWrap'); 6 | const KBase = require('../../lib/bot/KBase'); 7 | const TextLib = require('../../lib/utils/TextLib'); 8 | 9 | const newline = '\n'; 10 | 11 | const Bonfires = { 12 | data: null, 13 | fixed: { 14 | hintWarning: '## :construction: ** After this are possible spoiler hints.' + 15 | '**\nMake sure you\'ve tried to hard to solve it yourself before ' + 16 | 'proceeding. :construction:', 17 | menu: '\n- `bonfire info` for more info ' + 18 | '\n- `bonfire links` ' + 19 | '\n- `bonfire script` for the script', 20 | askName: 'give the name of the bonfire and I\'ll try to look it up!', 21 | setName: 'Set a bonfire to talk about with `bonfire name`', 22 | comingSoon: 'Coming Soon! We\'re working on it!', 23 | nameHint: 'no, type part of the name of the bonfire! eg `bonfire roman`', 24 | alert: '\n - :construction: **spoiler alert** :construction:', 25 | 26 | bfRoomLink: function(name) { 27 | return '[spoiler chatroom](https://gitter.im/camperbot/' + name + ')'; 28 | }, 29 | footer: function() { 30 | return '\n\n> more info:  `bf details` | ' + 31 | '`bf links` | `hint` '; 32 | }, 33 | reminder: function(name) { 34 | return 'we\'re talking about bonfire :fire: ' + name; 35 | }, 36 | cantFind: function(name) { 37 | return '> Sorry, can\'t find a bonfire called ' + name + 38 | '. [ [Check the map?]' + 39 | '(http://www.freecodecamp.org/map#Basic-Algorithm-Scripting) ]'; 40 | }, 41 | roomLink: function(name) { 42 | return ':construction: **spoiler alert** ' + this.bfRoomLink(name) + 43 | ' :arrow_forward:'; 44 | }, 45 | goToBonfireRoom: function(bf) { 46 | const link = Utils.linkify(bf.dashedName, 47 | 'camperbot', 'Bonfire\'s Custom Room'); 48 | return '> :construction: Spoilers are only in the ' + link + 49 | ' :point_right: '; 50 | }, 51 | pleaseContribute: function(bf) { 52 | const link = Utils.linkify(bf.dashedName, 53 | 'wiki', 'Bonfire\'s Wiki Hints Page'); 54 | return 'These hints depend on people like you! ' + 55 | 'Please add to this :point_right: ' + link; 56 | } 57 | }, 58 | 59 | load: function() { 60 | // Get document, or throw exception on error 61 | try { 62 | const bfDataFiles = [ 63 | 'basic-bonfires.json', 64 | 'intermediate-bonfires.json', 65 | 'advanced-bonfires.json', 66 | 'expert-bonfires.json' 67 | ]; 68 | 69 | let allData = { 70 | challenges: [] 71 | }; 72 | 73 | bfDataFiles.map(fname => { 74 | const raw = fs.readFileSync('./data/seed/challenges/' + fname, 'utf8'); 75 | const thisData = JSON.parse(raw); 76 | allData.challenges = allData.challenges.concat(thisData.challenges); 77 | }); 78 | 79 | this.data = allData; 80 | 81 | Bonfires.loadWikiHints(); 82 | 83 | // TODO - convert the embedded HTML to markdown tags 84 | // this.data = Utils.toMarkdown(this.data); 85 | } catch (e) { 86 | Utils.error('can\'t load bonfire data', e); 87 | } 88 | return this; 89 | }, 90 | 91 | loadWikiHints: function() { 92 | this.data.challenges = this.data.challenges.map(bf => { 93 | bf.hints = [Bonfires.fixed.hintWarning]; 94 | const wikiHints = KBase.getWikiHints(bf.dashedName); 95 | if (wikiHints) { 96 | bf.hints = bf.hints.concat(wikiHints); 97 | } else { 98 | Utils.tlog('bf.wikiHints not found', bf.dashedName); 99 | } 100 | return bf; 101 | }); 102 | }, 103 | 104 | findBonfire: function(bfName) { 105 | let flag; 106 | bfName = TextLib.dashedName(bfName); 107 | const bfs = this.data.challenges.filter(item => { 108 | flag = (item.dashedName.indexOf(bfName) >= 0); 109 | return flag; 110 | }); 111 | const bf = bfs[0]; 112 | if (!bf) { 113 | Utils.warn('can\'t find bonfire for ' + bfName); 114 | return null; 115 | } else { 116 | return bf; 117 | } 118 | }, 119 | 120 | 121 | getNextHint: function(bonfire, input) { 122 | let hint; 123 | let hintNum = parseInt(input.params, 10); 124 | 125 | if (isNaN(hintNum)) { 126 | hintNum = bonfire.currentHint || 0; 127 | } 128 | hint = bonfire.hints[hintNum]; 129 | 130 | if (hintNum < bonfire.hints.length) { 131 | const hintCounter = hintNum + 1; 132 | hint = '`hint [' + hintCounter + '/' + 133 | bonfire.hints.length + ']`\n## ' + hint; 134 | bonfire.currentHint = hintNum + 1; 135 | hint += this.wikiLinkFooter(bonfire); 136 | return hint; 137 | } else { 138 | bonfire.currentHint = 0; 139 | return Bonfires.fixed.pleaseContribute(bonfire); 140 | } 141 | }, 142 | 143 | toMarkdown: function() { 144 | this.data.challenges = this.data.challenges.map(item => { 145 | item.description = item.description.map(desc => Utils.toMarkdown(desc)); 146 | }); 147 | }, 148 | 149 | allDashedNames: function() { 150 | return this.fieldList('dashedName'); 151 | }, 152 | 153 | allNames: function() { 154 | return this.fieldList('name'); 155 | }, 156 | 157 | fieldList: function(field) { 158 | return this.data.challenges.map(item => item[field]); 159 | }, 160 | 161 | fromInput: function(input) { 162 | const roomName = InputWrap.roomShortName(input); 163 | const bf = this.findBonfire(roomName); 164 | Utils.checkNotNull(bf, 'cant find bonfire for ' + roomName); 165 | return (bf); 166 | }, 167 | 168 | 169 | wikiLinkFooter: function(bonfire) { 170 | let str = '\n\n> type `hint` for next hint :pencil: '; 171 | const text = '[Contribute at the FCC Wiki]'; 172 | 173 | return str + Utils.linkify(bonfire.dashedName, 'wiki', text); 174 | }, 175 | 176 | getDescription: function(bonfire) { 177 | return bonfire.description.join('\n'); 178 | }, 179 | 180 | getLinks: function(bonfire) { 181 | return 'links: \n' + Utils.makeMdnLinks(bonfire.MDNlinks, 'mdn'); 182 | }, 183 | 184 | getLinksFromInput: function(input) { 185 | const bf = Bonfires.fromInput(input); 186 | 187 | if (!bf || !bf.MDNlinks) { 188 | const msg = ('no links found for: ' + input.params); 189 | Utils.error('Bonfires>', msg, bf); 190 | return msg; 191 | } 192 | return this.getLinks(bf); 193 | }, 194 | 195 | getSeed: function(bonfire) { 196 | const seed = bonfire.challengeSeed.join('\n'); 197 | return '```js ' + newline + seed + '```'; 198 | }, 199 | 200 | getChallengeSeedFromInput: function(input) { 201 | const bf = Bonfires.fromInput(input); 202 | 203 | if (!bf || !bf.challengeSeed) { 204 | const msg = ('no challengeSeed found for: ' + input.params); 205 | Utils.error('Bonfires>', msg, bf); 206 | return msg; 207 | } 208 | 209 | const seed = bf.challengeSeed.join('\n'); 210 | 211 | return '```js ' + newline + seed + '```'; 212 | }, 213 | 214 | // methods that describe a bonfire that accept/expect a bonfire parameter 215 | bonfireInfo: function(bonfire) { 216 | if (!bonfire) { 217 | Utils.error('Bonfires.bonfireInfo', 'no bonfire'); 218 | } 219 | 220 | return this.bonfireHeader(bonfire) + newline + 221 | this.bonfireScript(bonfire) + newline + 222 | this.bonfireDescription(bonfire) + newline + 223 | newline + this.fixed.footer(bonfire.dashedName); 224 | }, 225 | 226 | bonfireStatus: function(bonfire) { 227 | return '\n- hints: ' + bonfire.hints.length; 228 | }, 229 | 230 | bonfireHeader: function(bonfire) { 231 | return '## :fire:' + TextLib.mdLink(bonfire.name, 232 | 'www.freecodecamp.org/challenges/' + bonfire.dashedName) + ' :link:'; 233 | }, 234 | 235 | bonfireDetails: function(bonfire) { 236 | return this.bonfireHeader(bonfire) + newline + 237 | this.bonfireScript(bonfire) + newline + 238 | this.bonfireDescription(bonfire, 50) + newline + 239 | this.bonfireLinks(bonfire); 240 | }, 241 | 242 | bonfireDescription: function(bonfire, lines) { 243 | if (lines) { 244 | const desc = bonfire.description.slice(0, lines); 245 | return desc.join('\n'); 246 | } else { 247 | return bonfire.description[0]; 248 | } 249 | }, 250 | 251 | bonfireLinks: function(bonfire) { 252 | return Bonfires.getLinks(bonfire); 253 | }, 254 | 255 | bonfireScript: function(bonfire) { 256 | return Bonfires.getSeed(bonfire); 257 | }, 258 | 259 | bonfireWiki: function() { 260 | const link = Utils.linkify(this.currentBonfire.name); 261 | return '> :fire: wiki: ' + link; 262 | } 263 | }; 264 | 265 | // ideally KBase should be loaded first, 266 | // though in theory it will load itself before data is needed ...? 267 | 268 | Bonfires.load(); 269 | 270 | module.exports = Bonfires; 271 | -------------------------------------------------------------------------------- /lib/app/Rooms.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const RoomData = require('../../data/RoomData'); 4 | const _ = require('lodash'); 5 | const Utils = require('../utils/Utils'); 6 | 7 | const Rooms = { 8 | 9 | findByTopic: function(topic) { 10 | const rooms = RoomData.rooms().filter(rm => { 11 | const topics = rm.topics; 12 | if (topics && topics.indexOf(topic) !== -1) { 13 | return true; 14 | } 15 | return false; 16 | }); 17 | 18 | return (this.checkRoom(rooms[0], 'findByTopic', topic)); 19 | }, 20 | 21 | findByName: function(name) { 22 | const room = _.findWhere(RoomData.rooms(), { name: name }); 23 | if (room) { 24 | return room; 25 | } 26 | return Utils.error('cant find room name:', name); 27 | }, 28 | 29 | isBonfire: function(name) { 30 | const room = this.findByName(name); 31 | if (room) { 32 | return room.isBonfire; 33 | } 34 | return false; 35 | }, 36 | 37 | names: function() { 38 | this.roomList = RoomData.rooms().map(room => room.name); 39 | return this.roomList; 40 | }, 41 | 42 | checkRoom: function(room, how, tag) { 43 | if (room) { 44 | return room; 45 | } 46 | return Utils.error('Rooms.checkRoom> failed', how, tag); 47 | } 48 | }; 49 | 50 | module.exports = Rooms; 51 | -------------------------------------------------------------------------------- /lib/bot/BotCommands.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const KBase = require('../bot/KBase'); 4 | const Utils = require('../../lib/utils/Utils'); 5 | const AppConfig = require('../../config/AppConfig'); 6 | const InputWrap = require('../bot/InputWrap'); 7 | const _ = require('lodash'); 8 | 9 | const newline = '\n'; 10 | 11 | const BotCommands = { 12 | 13 | /* 14 | //TODO - FIXME - this is not working correctly 15 | announce: function(input) { 16 | const parts = input.params.split(' '); 17 | const roomName = parts[0]; 18 | const text = parts.join(' '); 19 | this.bot.sayToRoom(text, roomName); 20 | }, 21 | */ 22 | 23 | archive: function(input) { 24 | const roomName = input.message.room.name; 25 | const shortName = InputWrap.roomShortName(input); 26 | const roomUri = AppConfig.gitterHost + roomName + '/archives/'; 27 | const timeStamp = Utils.timeStamp('yesterday'); 28 | 29 | return 'Archives for **' + shortName + '**' + newline + 30 | '\n- [All Time](' + roomUri + 'all)' + 31 | '\n- [Yesterday](' + roomUri + timeStamp + ')'; 32 | }, 33 | 34 | botenv: function() { 35 | return 'env: ' + AppConfig.serverEnv; 36 | }, 37 | 38 | botstatus: function() { 39 | return 'All bot systems are go! \n' + this.botversion() + newline + 40 | this.botenv() + newline + 'botname: ' + AppConfig.getBotName() + newline; 41 | }, 42 | 43 | botversion: function() { 44 | return 'botVersion: ' + AppConfig.botVersion; 45 | }, 46 | 47 | cbot: function(input, bot) { 48 | switch (input.params) { 49 | case 'version': 50 | return this.botversion(input, bot); 51 | case 'status': 52 | Utils.log('input', input); 53 | const status = this.botstatus(input, bot); 54 | Utils.clog('status', status); 55 | return status; 56 | default: 57 | return 'you called?'; 58 | } 59 | }, 60 | 61 | commands: function() { 62 | return '## commands:\n- ' + BotCommands.cmdList.join('\n- '); 63 | }, 64 | 65 | eightball: function(input) { 66 | const fromUser = '@' + input.message.model.fromUser.username; 67 | const replies = [ 68 | 'it is certain', 'it is decidedly so', 'without a doubt', 69 | 'yes. Definitely', 'you may rely on it', 'as I see it, yes', 70 | 'most likely', 'outlook good', 'yes', 'signs point to yes', 71 | 'reply hazy try again', 'ask again later', 'better not tell you now', 72 | 'cannot predict now', 'concentrate and ask again', 'don\'t count on it', 73 | 'my reply is no', 'my sources say no', 'outlook not so good', 74 | 'very doubtful' 75 | ]; 76 | 77 | var reply = replies[Math.floor(Math.random() * replies.length)]; 78 | return fromUser + ' :8ball: ' + reply + ' :sparkles:'; 79 | }, 80 | 81 | find: function(input, bot) { 82 | if (input.message.model.text.toLowerCase().includes( 83 | 'the meaning of life')) { 84 | return '42'; 85 | } 86 | 87 | const shortList = KBase.getTopicsAsList(input.params); 88 | 89 | bot.context = { 90 | state: 'finding', 91 | commands: shortList.commands 92 | }; 93 | 94 | const str = 'find **' + input.params + '**\n' + shortList; 95 | bot.makeListOptions(str); 96 | return str; 97 | }, 98 | 99 | init: function(bot) { 100 | // TODO - FIXME this is sketchy storing references like a global 101 | // called from the bot where we don't always have an instance 102 | BotCommands.bot = bot; 103 | }, 104 | 105 | isCommand: function(input) { 106 | let res; 107 | 108 | const cmds = BotCommands.cmdList.filter(c => { 109 | return (c === input.keyword); 110 | }); 111 | 112 | const one = cmds[0]; 113 | if (one) { 114 | res = true; 115 | } else { 116 | res = false; 117 | // Todo : raisedadead : commenting out the below for clean up later 118 | /* 119 | Utils.warn('isCommand', 'not command', input); 120 | Utils.warn('isCommand', 121 | '[ isCommand: ' + input.keyword + ' one: ' + one + ' res: ' + res ); 122 | */ 123 | } 124 | return res; 125 | }, 126 | 127 | music: function() { 128 | return '## Music!\n http://musare.com/'; 129 | }, 130 | 131 | // TODO - FIXME this isn't working it seems 132 | // rejoin: function (input, bot) { 133 | // clog('GBot', GBot); 134 | // BotCommands.bot.scanRooms(); 135 | // return 'rejoined'; 136 | // }, 137 | 138 | rooms: function() { 139 | return '#### freeCodeCamp rooms:' + 140 | '\n:point_right: Here is a [list of our official chat rooms]' + 141 | '(https://forum.freecodecamp.com/t/' + 142 | 'free-code-camp-official-chat-rooms/19390)'; 143 | }, 144 | 145 | wiki: function() { 146 | return '#### freeCodeCamp Wiki:' + 147 | '\n:point_right: The freeCodeCamp wiki can be found on ' + 148 | '[our forum](https://forum.freecodecamp.org). ' + 149 | '\nPlease follow the link and search there.' 150 | ; 151 | } 152 | }; 153 | 154 | 155 | // TODO - iterate and read all files in /cmds 156 | const thanks = require('./cmds/thanks'); 157 | 158 | _.merge(BotCommands, thanks); 159 | 160 | // aliases 161 | BotCommands.explain = BotCommands.wiki; 162 | BotCommands.thank = BotCommands.thanks; 163 | 164 | // TODO - some of these should be filtered/as private 165 | BotCommands.cmdList = Object.keys(BotCommands); 166 | 167 | module.exports = BotCommands; 168 | -------------------------------------------------------------------------------- /lib/bot/GBot.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const AppConfig = require('../../config/AppConfig'); 4 | const RoomData = require('../../data/RoomData'); 5 | const Utils = require('../../lib/utils/Utils'); 6 | const KBase = require('../../lib/bot/KBase'); 7 | const BotCommands = require('../../lib/bot/BotCommands'); 8 | const Bonfires = require('../app/Bonfires'); 9 | const Gitter = require('node-gitter'); 10 | const GitterHelper = require('../../lib/gitter/GitterHelper'); 11 | const RoomMessages = require('../../data/rooms/RoomMessages'); 12 | 13 | function clog(msg, obj) { 14 | Utils.clog('GBot>', msg, obj); 15 | } 16 | 17 | let apiWait = 0; 18 | let apiDelay = 1000; 19 | 20 | const GBot = { 21 | 22 | init: function() { 23 | // TODO refresh and add oneToOne rooms 24 | KBase.initSync(); 25 | this.roomList = []; 26 | this.listReplyOptions = []; 27 | this.gitter = new Gitter(AppConfig.token); 28 | this.joinKnownRooms(); 29 | 30 | // listen to other rooms for 1:1 31 | if (AppConfig.supportDmRooms) { 32 | this.gitter.currentUser().then(user => { 33 | this.scanRooms(user, AppConfig.token); 34 | }, err => { 35 | Utils.error('GBot.currentUser>', 'failed', err); 36 | }); 37 | } 38 | BotCommands.init(this); 39 | }, 40 | 41 | getName: function() { 42 | return AppConfig.botlist[0]; 43 | }, 44 | 45 | // listen to a known room 46 | // does a check to see if not already joined according to internal data 47 | listenToRoom: function(room) { 48 | if (this.addToRoomList(room) === false) { 49 | return; 50 | } 51 | 52 | const chats = room.streaming().chatMessages(); 53 | 54 | // The 'chatMessages' event is emitted on each new message 55 | chats.on('chatMessages', message => { 56 | if (message.operation !== 'create') { 57 | return; 58 | } 59 | if (GBot.isBot(message.model.fromUser.username)) { 60 | return; 61 | } 62 | 63 | message.room = room; 64 | GBot.handleReply(message); 65 | }); 66 | }, 67 | 68 | handleReply: function(message) { 69 | clog(message.room.uri + ' @' + message.model.fromUser.username + ':'); 70 | clog(' in|', message.model.text); 71 | const output = this.findAnyReply(message); 72 | if (output) { 73 | clog('out| ', output); 74 | GBot.say(output, message.room); 75 | } 76 | // for debugging 77 | return output; 78 | }, 79 | 80 | // using a callback to get roomId 81 | sayToRoom: function(text, roomName) { 82 | const sayIt = () => { 83 | console.log('sayIt', text, roomName); 84 | GBot.say(text, roomName); 85 | }; 86 | GitterHelper.findRoomByName(roomName, sayIt); 87 | }, 88 | 89 | say: function(text, room) { 90 | // did we get a room 91 | Utils.hasProperty(room, 'path', 'expected room object'); 92 | if (!text) { 93 | console.warn('tried to say with no text'); 94 | } 95 | try { 96 | GitterHelper.sayToRoomName(text, room.uri); 97 | } catch (err) { 98 | Utils.warn('GBot.say>', 'failed', err); 99 | Utils.warn('GBot.say>', 'room', room); 100 | } 101 | }, 102 | 103 | // search all reply methods 104 | // returns a string to send 105 | // handleReply takes care of sending to chat system 106 | findAnyReply: function(message) { 107 | const input = this.parseInput(message); 108 | const listReplyOptionsAvailable = this.findListOption(input); 109 | let output; 110 | 111 | if (input.command && BotCommands.hasOwnProperty(input.keyword) 112 | && typeof BotCommands[input.keyword] === 'function') { 113 | // this looks up a command and calls it 114 | output = BotCommands[input.keyword](input, this); 115 | } else if (listReplyOptionsAvailable !== false) { 116 | // if a list exists and user chose an option 117 | output = listReplyOptionsAvailable; 118 | } else { 119 | // non-command keywords like 'troll' 120 | const scanCommand = RoomMessages.scanInput(input, input.message.room.name, 121 | AppConfig.botNoiseLevel); 122 | if (scanCommand) { 123 | if (scanCommand.text) { 124 | output = (scanCommand.text); 125 | } 126 | if (scanCommand.func) { 127 | output = scanCommand.func(input, this); 128 | } 129 | } 130 | } 131 | // TODO - check its a string or nothing 132 | return output; 133 | }, 134 | 135 | // save a list of options 136 | // when the bot sends out a list 137 | makeListOptions: function(output) { 138 | let matches = []; 139 | // find what is between [] brackets in the list of links 140 | // example [bonfire arguments optional] 141 | output.replace(/\[([a-zA-Z ]+)\]/g, (g0, g1) => { 142 | matches.push(g1); 143 | }); 144 | // stores 'bonfire arguments optional' and the like in an array 145 | this.listReplyOptions = matches; 146 | return matches; 147 | }, 148 | 149 | // reply option to user 150 | // if they chose an option from the list 151 | findListOption: function(input) { 152 | const parsedInput = parseInt(input.cleanText, 10); 153 | 154 | if (!this.listReplyOptions || this.listReplyOptions.length === 0) { 155 | return false; 156 | } else if (input.cleanText.match(/^[0-9]+$/i) === null) { 157 | // check if input is not a number 158 | return false; 159 | } else if (typeof this.listReplyOptions[parsedInput] === 'undefined') { 160 | return false; 161 | } 162 | 163 | // get chosen wiki or bonfire article to output 164 | input.params = this.listReplyOptions[parsedInput]; 165 | let output; 166 | if (input.params.split(' ')[0] === 'bonfire') { 167 | output = BotCommands['bonfire'](input, this); 168 | } else { 169 | output = BotCommands['wiki'](input, this); 170 | } 171 | 172 | this.listReplyOptions = []; 173 | return output; 174 | }, 175 | 176 | // turns raw text input into a json format 177 | parseInput: function(message) { 178 | Utils.hasProperty(message, 'model'); 179 | 180 | let cleanText = message.model.text; 181 | cleanText = Utils.sanitize(cleanText); 182 | 183 | let input = Utils.splitParams(cleanText); 184 | input = this.cleanInput(input); 185 | input.message = message; 186 | input.cleanText = cleanText; 187 | 188 | if (BotCommands.isCommand(input)) { 189 | input.command = true; 190 | } 191 | return input; 192 | }, 193 | 194 | cleanInput: function(input) { 195 | // 'bot' keyword is an object = bad things happen when called as a command 196 | if (input.keyword === 'bot') { 197 | input.keyword = 'help'; 198 | } 199 | return input; 200 | }, 201 | 202 | announce: function(opts) { 203 | clog('announce', opts); 204 | this.joinRoom(opts, true); 205 | }, 206 | 207 | joinRoom: function(opts) { 208 | const roomUrl = opts.roomObj.name; 209 | 210 | GBot.gitter.rooms.join(roomUrl, (err, room) => { 211 | if (err) { 212 | console.warn('Not possible to join the room: ', err, roomUrl); 213 | } 214 | GBot.roomList.push(room); 215 | // have to stagger this for gitter rate limit 216 | GBot.listenToRoom(room); 217 | const text = GBot.getAnnounceMessage(opts); 218 | GBot.say(text, room); 219 | 220 | return room; 221 | }); 222 | 223 | return false; 224 | }, 225 | 226 | // checks if joined already, otherwise adds 227 | addToRoomList: function(room) { 228 | // check for dupes 229 | this.roomList = this.roomList || []; 230 | if (this.hasAlreadyJoined(room, this.roomList)) { 231 | return false; 232 | } 233 | 234 | this.roomList.push(room); 235 | return true; 236 | }, 237 | 238 | // checks if a room is already in bots internal list of joined rooms 239 | // this is to avoid listening twice 240 | // see https://github.com/gitterHQ/node-gitter/issues/15 241 | // note this is only the bots internal tracking 242 | // it has no concept if the gitter API/state already thinks 243 | // you're joined/listening 244 | hasAlreadyJoined: function(room) { 245 | const checks = this.roomList.filter(rm => { 246 | return (rm.name === room.name); 247 | }); 248 | 249 | const oneRoom = checks[0]; 250 | if (oneRoom) { 251 | Utils.warn('GBot', 'hasAlreadyJoined:', oneRoom.url); 252 | return true; 253 | } 254 | 255 | return false; 256 | }, 257 | 258 | getAnnounceMessage: function() { 259 | return ''; 260 | }, 261 | 262 | // dont reply to bots or you'll get a feedback loop 263 | isBot: function(who) { 264 | // 'of' IS correct even tho ES6Lint doesn't get it 265 | for (let bot of AppConfig.botlist) { 266 | if (who === bot) { 267 | return true; 268 | } 269 | } 270 | return false; 271 | }, 272 | 273 | // this joins rooms contained in the data/RoomData.js file 274 | // ie a set of bot specific discussion rooms 275 | joinKnownRooms: function() { 276 | clog('botname on rooms', AppConfig.getBotName()); 277 | 278 | RoomData.rooms().map(oneRoomData => { 279 | const roomUrl = oneRoomData.name; 280 | this.delayedJoin(roomUrl); 281 | }); 282 | }, 283 | 284 | 285 | delayedJoin: function(roomUrl) { 286 | apiWait += apiDelay; 287 | setTimeout(() => { 288 | this.gitter.rooms.join(roomUrl, (err, room) => { 289 | if (err) { 290 | Utils.warn('Not possible to join the room:', roomUrl, err); 291 | return; 292 | } 293 | clog('joined> ', room.name); 294 | this.listenToRoom(room); 295 | }); 296 | }, apiWait); 297 | }, 298 | 299 | joinBonfireRooms: function() { 300 | Bonfires.allDashedNames().map(name => { 301 | const roomUrl = AppConfig.getBotName() + '/' + name; 302 | this.delayedJoin(roomUrl); 303 | }); 304 | }, 305 | 306 | // uses gitter helper to fetch the list of rooms this user is 'in' 307 | // and then tries to listen to them 308 | // this is mainly to pick up new oneOnOne conversations 309 | // when a user DMs the bot 310 | // as I can't see an event the bot would get to know about that 311 | // so its kind of like 'polling' and currently only called from the webUI 312 | scanRooms: function(user, token) { 313 | clog('user', user); 314 | clog('token', token); 315 | GitterHelper.fetchRooms(user, token, (err, rooms) => { 316 | if (err) { 317 | Utils.warn('GBot', 'fetchRooms', err); 318 | } 319 | if (!rooms) { 320 | Utils.warn('cant scanRooms'); 321 | return; 322 | } 323 | clog('scanRooms.rooms', rooms); 324 | rooms.map(room => { 325 | if (room.oneToOne) { 326 | clog('oneToOne', room.name); 327 | this.gitter.rooms.find(room.id) 328 | .then(roomObj => { 329 | this.listenToRoom(roomObj); 330 | }); 331 | } 332 | }); 333 | }); 334 | }, 335 | 336 | // TODO - FIXME doesnt work for some reason >.< 337 | // needs different type of token? 338 | updateRooms: function() { 339 | GBot.gitter.currentUser() 340 | .then(user => { 341 | const list = user.rooms((err, obj) => { 342 | clog('rooms', err, obj); 343 | }); 344 | clog('user', user); 345 | clog('list', list); 346 | return list; 347 | }); 348 | } 349 | }; 350 | 351 | module.exports = GBot; 352 | -------------------------------------------------------------------------------- /lib/bot/InputWrap.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Utils = require('../utils/Utils'); 4 | 5 | const InputWrap = { 6 | roomShortName: function(input) { 7 | let name = input.message.room.name; 8 | name = name.split('/'); 9 | name = name[1] || name[0]; 10 | return name; 11 | }, 12 | 13 | fromUser: function(input) { 14 | try { 15 | return '@' + input.message.model.fromUser.username; 16 | } catch (e) { 17 | Utils.error('InputWrap', 'no fromUser', input); 18 | return null; 19 | } 20 | }, 21 | 22 | mentioned: function(input) { 23 | const mentions = input.message.model.mentions; 24 | let names; 25 | if (mentions) { 26 | // TODO - build a list 27 | return names; 28 | } 29 | return null; 30 | } 31 | }; 32 | 33 | module.exports = InputWrap; 34 | -------------------------------------------------------------------------------- /lib/bot/KBase.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const TextLib = require('../utils/TextLib'); 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | const Utils = require('../utils/Utils'); 7 | 8 | // topicNameList - list of individual topic keywords eg 'chai-cheat' 9 | // topics - Hash full data of all topics 10 | 11 | // example topic: 12 | // 13 | // 'js-for': 14 | // { path: '/Users/dc/dev/fcc/gitterbot/nap/data/wiki/js-for.md', 15 | // topic: 'js-for', 16 | // fname: 'js-for.md', 17 | // data: 'The javascript `for` command iterates through a list of items.\n\n 18 | // ```js\nfor (var i = 0; i < 9; i++) {\n console.log(i);\n 19 | // // more statements\n}\n```\n\n----', 20 | // shortData: 'The javascript `for` command iterates through a list of items. 21 | // \n\n```js\nfor (var i = 0; i < 9; i++) {\n console.log(i);\n 22 | // // more statements\n}\n```\n\n----' }, 23 | 24 | const KBase = { 25 | files: [], 26 | topics: null, 27 | findMoreResults: [], 28 | 29 | initSync: function() { 30 | // TODO - FIXME works relative? 31 | const wikiDataDir = path.join(__dirname, 32 | '/../../data/'); 33 | 34 | KBase.allData = []; 35 | fs.readdirSync(wikiDataDir).forEach(name => { 36 | if ((/md$/).test(name)) { 37 | const filePath = path.join(wikiDataDir, name); 38 | const arr = filePath.split(path.sep); 39 | const fileName = arr[arr.length - 1].toLowerCase(); 40 | const topicName = fileName.replace('.md', ''); 41 | const data = fs.readFileSync(filePath, 'utf8'); 42 | 43 | const blob = { 44 | path: filePath, 45 | displayName: Utils.namify(topicName), 46 | fileName: fileName, 47 | data: data, 48 | shortData: TextLib.fixRelativeLink(TextLib.trimLines(data), 49 | topicName), 50 | dashedName: TextLib.dashedName(topicName) 51 | }; 52 | 53 | KBase.allData.push(blob); 54 | } 55 | }); 56 | return KBase.allData; 57 | }, 58 | 59 | getWikiHints: function(bfName) { 60 | const topicData = this.getTopicData(bfName); 61 | if (topicData) { 62 | return topicData.data.split('##'); 63 | } else { 64 | return null; 65 | } 66 | }, 67 | 68 | getTopicData: function(params) { 69 | const searchDashName = TextLib.dashedName(params); 70 | 71 | if (!KBase.allData) { 72 | KBase.initSync(); 73 | return null; 74 | } else { 75 | const shortList = KBase.allData.filter(t => { 76 | return (t.dashedName.includes(searchDashName)); 77 | }); 78 | if (shortList) { 79 | return shortList[0]; 80 | } else { 81 | Utils.warn('KBase', 'cant find topicData for', params); 82 | Utils.warn('Kbase', 'allData', KBase.allData); 83 | return null; 84 | } 85 | } 86 | }, 87 | 88 | getTopics: function(keyword) { 89 | // TODO - refac and use function above 90 | const searchDashName = TextLib.dashedName(keyword); 91 | const shortList = this.allData.filter(t => { 92 | return (t.dashedName.includes(searchDashName)); 93 | }); 94 | return shortList; 95 | }, 96 | 97 | // return topics as markdown links 98 | getTopicsAsList: function(keyword) { 99 | const shortList = this.getTopics(keyword); 100 | let findResults; 101 | if (shortList.length === 0) { 102 | return 'nothing found'; 103 | } 104 | if (this.findMoreResults[0] === keyword) { 105 | // continue list of entries after limit 106 | findResults = this.findMoreResults[1]; 107 | this.findMoreResults = []; 108 | return '> more entries: \n ' + findResults; 109 | } 110 | this.findMoreResults = []; 111 | // else 112 | Utils.log('shortList', shortList); 113 | 114 | const emojiList = [':zero:', ':one:', ':two:', ':three:', ':four:', 115 | ':five:', ':six:', ':seven:', ':eight:', ':nine:' 116 | ]; 117 | const listLimit = 20; 118 | 119 | findResults = ''; 120 | for (let i = 0; i < shortList.length; i++) { 121 | let topicData = shortList[i]; 122 | let link = Utils.linkify(topicData.dashedName, 'wiki', 123 | topicData.displayName); 124 | let line; 125 | if (i < 10) { 126 | line = '\n ' + emojiList[i] + ' ' + link; 127 | } else if (i < 100) { 128 | let iSplit = i.toString().split(''); 129 | line = '\n ' + emojiList[iSplit[0]] + 130 | emojiList[iSplit[1]] + ' ' + link; 131 | } 132 | 133 | if (i === listLimit) { 134 | // meets limit 135 | findResults += '\n > limited to first ' + listLimit + ' entries.' + 136 | '\n > type `find ' + keyword + 137 | '` again for more entries.'; 138 | this.findMoreResults[0] = keyword; 139 | this.findMoreResults[1] = '' + line; 140 | } else if (i > listLimit) { 141 | // exceeds limit 142 | this.findMoreResults[1] += line; 143 | } else { 144 | // below limit 145 | findResults += line; 146 | } 147 | } 148 | return findResults; 149 | } 150 | }; 151 | 152 | KBase.initSync(); 153 | 154 | module.exports = KBase; 155 | -------------------------------------------------------------------------------- /lib/bot/cmds/thanks.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Utils = require('../../../lib/utils/Utils'); 4 | const HttpWrap = require('../../../lib/utils/HttpWrap'); 5 | const TextLib = require('../../../lib/utils/TextLib'); 6 | 7 | const thanksCommands = { 8 | 9 | // messages: { 10 | // wikiHint: function(fromUser) { 11 | // const wikiUrl = '(https://github.com/freecodecamp/' + 12 | // 'freecodecamp/wiki/wiki-style-guide)'; 13 | // 14 | // return '\n> hey @' + fromUser + ' if you found this info helpful ' + 15 | // ':point_right: *[consider adding a wiki article!]' + wikiUrl + '*'; 16 | // } 17 | // }, 18 | 19 | thanks: function(input, bot) { 20 | Utils.hasProperty(input, 'message', 'thanks expects an object'); 21 | 22 | const mentions = input.message.model.mentions; 23 | // just 'thanks' in a message 24 | if (mentions && mentions.length === 0) { 25 | Utils.warn('thanks', 'without any mentions', input.message.model); 26 | return null; 27 | } 28 | 29 | const fromUser = input.message.model.fromUser.username.toLowerCase(); 30 | const options = { 31 | method: 'POST', 32 | input: input, 33 | bot: bot 34 | }; 35 | 36 | const namesList = mentions.reduce((userList, mention) => { 37 | const toUser = mention.screenName.toLowerCase(); 38 | if (toUser !== fromUser && userList.indexOf(toUser) === -1) { 39 | const apiPath = '/api/users/give-brownie-points?receiver=' + toUser + 40 | '&giver=' + fromUser; 41 | HttpWrap.callApi(apiPath, options, thanksCommands.showInfoCallback); 42 | userList.push(toUser); 43 | } 44 | return userList; 45 | }, []); 46 | 47 | if (namesList.length > 0) { 48 | const toUserMessage = namesList.join(' and @'); 49 | return '> ' + fromUser + ' sends brownie points to @' + toUserMessage + 50 | ' :sparkles: :thumbsup: :sparkles: '; 51 | } else { 52 | return '> sorry ' + fromUser + ', you can\'t send brownie points to ' + 53 | 'yourself! :sparkles: :sparkles: '; 54 | } 55 | }, 56 | 57 | about: function(input, bot) { 58 | const mentions = input.message.model.mentions, 59 | them = mentions[0]; 60 | 61 | if (!them) { 62 | Utils.warn('about without any mentions', input.message.model); 63 | return null; 64 | } 65 | const name = them.screenName.toLowerCase(); 66 | const options = { 67 | method: 'GET', 68 | input: input, 69 | bot: bot 70 | }; 71 | 72 | const apiPath = '/api/users/about?username=' + name; 73 | HttpWrap.callApi(apiPath, options, thanksCommands.showInfoCallback); 74 | return null; 75 | }, 76 | 77 | // called back from apiCall so can't use Global GBot here 78 | // blob: 79 | // response 80 | // bot 81 | // input 82 | showInfoCallback: function(blob) { 83 | // in case we want to filter the message 84 | const cleanMessage = message => { 85 | if (message.match(/^FCC: no user/)) { 86 | message = 'hmm, can\'t find that user on the beta site. wait til ' + 87 | 'we release new version!'; 88 | } 89 | 90 | const msgPattern = /^could not find receiver for /; 91 | if (message.match(msgPattern)) { 92 | message = message.replace( 93 | msgPattern, 94 | '@') + '\'s account is not linked with freeCodeCamp' + 95 | '. Please visit [the settings]' + 96 | '(https://freecodecamp.org/settings) and link your ' + 97 | 'GitHub account.'; 98 | } 99 | message = '> :warning: ' + message; 100 | return message; 101 | }; 102 | 103 | if (blob.response.error) { 104 | const message = cleanMessage(blob.response.error.message); 105 | 106 | Utils.warn('WARN @thanks>', blob.response.error.message, 107 | blob.response.error); 108 | 109 | // show the error to the user 110 | blob.bot.say(message, blob.input.message.room); 111 | return false; 112 | } 113 | 114 | let str; 115 | try { 116 | const username = blob.response.about.username; 117 | const about = blob.response.about; 118 | const brownieEmoji = about.browniePoints < 1000 ? ':cookie:' : ':star2:'; 119 | const uri = 'http://www.freecodecamp.org/' + username; 120 | str = `> ${brownieEmoji} ${about.browniePoints} | @${username} |`; 121 | str += TextLib.mdLink(uri, uri); 122 | } catch (err) { 123 | Utils.error('can\'t create response from API callback', err); 124 | Utils.warn('thanks>', 'blob>', blob); 125 | str = 'api offline'; 126 | } 127 | return blob.bot.say(str, blob.input.message.room); 128 | } 129 | }; 130 | 131 | module.exports = thanksCommands; 132 | -------------------------------------------------------------------------------- /lib/gitter/GitterHelper.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const gitterHost = process.env.HOST || 'https://gitter.im'; 4 | const AppConfig = require('../../config/AppConfig'); 5 | const Utils = require('../../lib/utils/Utils'); 6 | const request = require('request'); 7 | const _ = require('lodash'); 8 | 9 | // Gitter API client helper 10 | const GitterHelper = { 11 | 12 | roomDataCache: {}, 13 | 14 | fetch: function(path, callback, options) { 15 | options = options || {}; 16 | 17 | const defaultOptions = { 18 | uri: gitterHost + '/api/v1' + path, 19 | headers: { 20 | 'Content-Type': 'application/json', 21 | Accept: 'application/json', 22 | Authorization: 'Bearer ' + AppConfig.token 23 | } 24 | }; 25 | // opts takes priority 26 | _.extend(defaultOptions, options); 27 | 28 | request(defaultOptions, (err, res, body) => { 29 | if (err) { 30 | Utils.error('GitterHelper.fetch', err); 31 | Utils.error('GitterHelper.fetch.options', defaultOptions); 32 | return callback(err); 33 | } 34 | /* eslint-disable consistent-return */ 35 | if (!callback) { return; } 36 | /* eslint-enable consistent-return */ 37 | 38 | if (res.statusCode === 200) { 39 | let data; 40 | // TODO - FIXME sometimes we get JSON back (from POST requests) 41 | // sometimes we just get a string 42 | if (typeof body === 'string') { 43 | data = JSON.parse(body); 44 | } else { 45 | // hope its json! 46 | data = body; 47 | } 48 | return callback(null, data); 49 | } else { 50 | Utils.warn('GitterHelper', 'non 200 response from', defaultOptions); 51 | Utils.warn('GitterHelper', 'body', body); 52 | return callback('err' + res.statusCode); 53 | } 54 | }); 55 | }, 56 | 57 | postMessage: function(text, roomId, callback, opts) { 58 | const data = { text: text }; 59 | opts = { 60 | method: 'POST', 61 | body: data, 62 | json: true 63 | }; 64 | 65 | this.fetch( 66 | '/rooms/' + roomId + '/chatMessages', 67 | callback, 68 | opts 69 | ); 70 | }, 71 | 72 | fetchCurrentUser: function(token, cb) { 73 | this.fetch('/user/', (err, user) => { 74 | cb(err, user[0]); 75 | }); 76 | }, 77 | 78 | // TODO - refactor not to take a token on each req 79 | fetchRooms: function(user, token, cb) { 80 | this.fetch('/user/' + user.id + '/rooms', (err, rooms) => { 81 | cb(err, rooms); 82 | }); 83 | }, 84 | 85 | findRoomByName: function(roomUri, callback, cbParams) { 86 | cbParams = cbParams || {}; 87 | 88 | // avoid doing rest calls if we're posting to a known room 89 | const cached = GitterHelper.roomDataCache[roomUri]; 90 | if (cached) { 91 | cbParams.gitterRoom = cached; 92 | return callback(cbParams); 93 | } else { 94 | return this.fetch('/rooms', (err, rooms) => { 95 | if (err) { 96 | return callback(err); 97 | } 98 | if (!rooms) { 99 | Utils.error('can\'t find rooms with roomUri', roomUri); 100 | return null; 101 | } 102 | const roomList = rooms.filter(rm => { 103 | return rm.uri === roomUri; 104 | }); 105 | if (roomList.length > 0) { 106 | const room = roomList[0]; 107 | GitterHelper.roomDataCache[roomUri] = room; 108 | cbParams.gitterRoom = room; 109 | return callback(cbParams); 110 | } 111 | return null; 112 | }); 113 | } 114 | }, 115 | 116 | responseCallback: function() { 117 | Utils.clog('GitterHelper.response callback'); 118 | }, 119 | 120 | sayToRoomObj: function(text, opts) { 121 | GitterHelper.postMessage(text, opts.id); 122 | }, 123 | 124 | sayToRoomName: function(text, roomUri) { 125 | GitterHelper.findRoomByName(roomUri, opts => { 126 | GitterHelper.sayToRoomObj(text, opts.gitterRoom); 127 | }); 128 | } 129 | }; 130 | 131 | 132 | module.exports = GitterHelper; 133 | -------------------------------------------------------------------------------- /lib/gitter/restApi.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const gitterHost = process.env.HOST || 'https://gitter.im', 4 | _ = require('underscore'), 5 | request = require('request'), 6 | AppConfig = require('../../../config/AppConfig'); 7 | 8 | function handleCallback(err) { 9 | if (err) { 10 | console.error('ERROR \n'); 11 | } 12 | } 13 | 14 | // Gitter API client helper 15 | const gitter = { 16 | stashToken: function(token) { 17 | if (token) { 18 | AppConfig.token = token; 19 | } else { 20 | console.error('tried to stash null token:', token); 21 | } 22 | console.log('stashToken AppConfig:', AppConfig); 23 | token = token || AppConfig.token; 24 | return token; 25 | }, 26 | 27 | checkUser: function(user) { 28 | if (user === '[') { 29 | console.error('WTF user is ['); 30 | user = AppConfig.user; 31 | } 32 | return user; 33 | }, 34 | 35 | fetch: function(path, token, cb, opts) { 36 | token = token || AppConfig.token; 37 | const options = { 38 | url: gitterHost + path, 39 | headers: { 40 | 'Content-Type': 'application/json', 41 | Accept: 'application/json', 42 | Authorization: 'Bearer ' + token 43 | } 44 | }; 45 | 46 | opts = opts || {}; 47 | // opts takes priority 48 | _.extend(options, opts); 49 | 50 | request(options, (err, res, body) => { 51 | if (err) { return cb(err); } 52 | 53 | if (res.statusCode === 200) { 54 | return cb(null, body); 55 | } 56 | return cb('err ' + res.statusCode); 57 | }); 58 | }, 59 | 60 | fetchCurrentUser: function(token, cb) { 61 | this.fetch('/api/v1/user/', token, (err, user) => { 62 | cb(err, user[0]); 63 | }); 64 | }, 65 | 66 | fetchRooms: function(user, token, cb) { 67 | // TODO - FIXME 68 | user = this.checkUser(user); 69 | token = this.stashToken(token); 70 | this.fetch('/api/v1/user/' + user.id + '/rooms', token, (err, rooms) => { 71 | cb(err, rooms); 72 | }); 73 | }, 74 | 75 | postMessage: function(text, roomId) { 76 | const token = this.stashToken(); 77 | roomId = roomId || AppConfig.roomId; 78 | const data = { text: text }; 79 | const opts = { 80 | method: 'POST', 81 | // body: JSON.stringify(data), 82 | body: data, 83 | json: true 84 | }; 85 | 86 | this.fetch( 87 | '/api/v1/rooms/' + roomId + '/chatMessages', 88 | token, 89 | handleCallback, 90 | opts 91 | ); 92 | } 93 | }; 94 | 95 | 96 | gitter.currentUser().then(user => { 97 | console.log('---- You are logged in as:', user.username); 98 | }); 99 | 100 | 101 | module.exports = gitter; 102 | -------------------------------------------------------------------------------- /lib/gitter/streamApi.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const https = require('https'); 4 | 5 | function listenToRoom(roomId, bot) { 6 | const token = process.env.GITTER_USER_TOKEN; 7 | const heartbeat = ' \n'; 8 | 9 | console.log('listenToRoom', roomId); 10 | 11 | const options = { 12 | hostname: 'stream.gitter.im', 13 | port: 443, 14 | path: '/v1/rooms/' + roomId + '/chatMessages', 15 | method: 'GET', 16 | headers: { 17 | Authorization: 'Bearer ' + token 18 | } 19 | }; 20 | 21 | const req = https.request(options, res => { 22 | res.on('data', chunk => { 23 | const msg = chunk.toString(); 24 | if (msg !== heartbeat) { 25 | const blob = JSON.parse(msg); 26 | blob.roomId = roomId; 27 | bot.reply(blob); 28 | } 29 | }); 30 | }); 31 | 32 | req.on('error', e => { 33 | console.log('Something went wrong: ' + e.message); 34 | }); 35 | 36 | req.end(); 37 | } 38 | 39 | module.exports = { 40 | listenToRoom: listenToRoom 41 | }; 42 | -------------------------------------------------------------------------------- /lib/utils/HttpWrap.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const https = require('https'); 4 | const _ = require('lodash'); 5 | const AppConfig = require('../../config/AppConfig'); 6 | const Utils = require('./Utils'); 7 | 8 | const HttpWrap = { 9 | defaultOptions: { 10 | host: AppConfig.apiServer, 11 | port: 443, 12 | protocol: 'https:', 13 | timeout: 5000, 14 | debug: false, 15 | headers: { 16 | Authorization: AppConfig.apiKey 17 | } 18 | }, 19 | 20 | callApi: function(apiPath, options, callback) { 21 | 22 | _.merge(this.defaultOptions, options); 23 | 24 | // TODO add authorisation to header 25 | this.defaultOptions.path = apiPath; 26 | 27 | const handleResponse = response => { 28 | let str = ''; 29 | 30 | // another chunk of data has been received, so append it to `str` 31 | response.on('data', chunk => { 32 | str += chunk; 33 | }); 34 | 35 | // the whole response has been recieved, so we just print it out here 36 | response.on('end', () => { 37 | try { 38 | const blob = JSON.parse(str); 39 | options.response = blob; 40 | } catch (err) { 41 | Utils.error('cant parse API response', str); 42 | Utils.error('error>', err); 43 | options.response = 'api offline'; 44 | } 45 | callback(options); 46 | }); 47 | }; 48 | 49 | const handleTimeout = err => { 50 | Utils.error('HttpWrap', 'timeout', err); 51 | }; 52 | 53 | const request = https.request(this.defaultOptions, handleResponse); 54 | request.setTimeout(3000, handleTimeout); 55 | request.end(); 56 | } 57 | }; 58 | 59 | module.exports = HttpWrap; 60 | -------------------------------------------------------------------------------- /lib/utils/TextLib.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const AppConfig = require('../../config/AppConfig'); 4 | 5 | const TextLib = { 6 | // we only show the first para 7 | // and limit to 20 lines 8 | fixRelativeLink: function fixRelativeLink(wikiContent, topicName, baseUrl) { 9 | if (typeof baseUrl === 'undefined') { 10 | baseUrl = 'http://github.com/FreeCodeCamp/FreeCodeCamp/wiki/'; 11 | } 12 | const linkMatchRegExp = /.+\]\((.+)\)/g; 13 | return wikiContent.split('\n').map((line) => { 14 | if (line.match(linkMatchRegExp) && 15 | line.match(linkMatchRegExp).length > 0) { 16 | line = line.split(' ').map((word) => { 17 | if (word.match(linkMatchRegExp)) { 18 | if (!word.match(/\:\/\//gi)) { 19 | if (word.match(/\(\#(.+)/gi)) { 20 | word = word.replace(/\(\#(.+)/gi, `(${baseUrl}${topicName}#$1`); 21 | } else { 22 | word = word.replace(/\((.+)\)/gi, `(${baseUrl}$1)`); 23 | } 24 | return word; 25 | } 26 | } 27 | return word; 28 | }).join(' '); 29 | } 30 | return (line); 31 | }).join('\n'); 32 | }, 33 | 34 | trimLines: function(data, lines) { 35 | const part = data.split('\n## ')[0]; 36 | lines = lines || AppConfig.MAX_WIKI_LINES; 37 | let subset = part.split('\n'); 38 | subset = subset.slice(0, lines).join('\n'); 39 | return subset; 40 | }, 41 | 42 | mdLink: function(text, uri) { 43 | return '[' + text + '](' + uri + ')'; 44 | }, 45 | 46 | dashedName: function(str) { 47 | if (!str) { 48 | return; 49 | } 50 | str = str.replace(/\s/g, '-'); 51 | str = str.toLowerCase(); 52 | // in case of doubles 53 | str = str.replace('--', '-'); 54 | str = str.replace('.md', ''); 55 | str = str.replace(/([^a-z0-9áéíóúñü_@\-\s]|[\t\n\f\r\v\0])/gim, ''); 56 | /* eslint-disable consistent-return */ 57 | return str; 58 | /* eslint-enable consistent-return */ 59 | } 60 | }; 61 | 62 | module.exports = TextLib; 63 | -------------------------------------------------------------------------------- /lib/utils/Utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('dotenv').config({ path: '.env' }); 4 | 5 | const clc = require('cli-color'); 6 | const _ = require('lodash'); 7 | const AppConfig = require('../../config/AppConfig'); 8 | const MDNlinks = require('../../data/seed/bonfireMDNlinks'); 9 | 10 | const LOG_LEVEL_ERROR = 2; 11 | const LOG_LEVEL_WARN = 3; 12 | const LOG_LEVEL_INFO = 5; 13 | 14 | const Utils = { 15 | 16 | cols: { 17 | error: clc.bgRedBright.white.bold, 18 | warn: clc.black.bgYellow.bold, 19 | info: clc.black.cyanBright, 20 | info2: clc.black.bgCyan, 21 | notice: clc.blue, 22 | bright: clc.xterm(237).bgXterm(195), 23 | dimmed: clc.xterm(232).bgXterm(253), 24 | warning: clc.xterm(232).bgXterm(215), 25 | errorColors: clc.xterm(232).bgRedBright 26 | }, 27 | 28 | // default 29 | logLevel: process.env.LOG_LEVEL || 10, 30 | 31 | log: function(msg, obj) { 32 | let where = ''; 33 | if (this.logLevel > LOG_LEVEL_INFO) { 34 | where = this.stackLines(3, 4); 35 | } 36 | Utils.clog(where, msg, obj); 37 | }, 38 | 39 | clog: function(where, msg, obj) { 40 | if (this.logLevel < LOG_LEVEL_INFO) { 41 | return; 42 | } 43 | obj = obj || ''; 44 | console.log(this.cols.info(where), this.cols.info(msg), obj); 45 | }, 46 | 47 | // log during test 48 | tlog: function() { 49 | if (process.env.SERVER_ENV) { return; } 50 | const args = Array.prototype.slice.call(arguments); 51 | const p1 = args.shift() || '_'; 52 | const p2 = args.shift() || '_'; 53 | const p3 = args.shift() || '_'; 54 | console.log(this.cols.bright(p1), p2, p3); 55 | args.forEach(p => { 56 | if (p) { console.log(p); } 57 | }); 58 | }, 59 | 60 | warn: function(where, msg, obj) { 61 | if (this.logLevel < LOG_LEVEL_WARN) { 62 | return; 63 | } 64 | obj = obj || ''; 65 | console.warn(this.cols.warn(where), this.cols.warn(msg), obj); 66 | }, 67 | 68 | stackTrace: function() { 69 | const err = new Error(); 70 | console.log(err); 71 | return err.stack; 72 | }, 73 | 74 | stackLines: function(from, to) { 75 | const err = new Error(); 76 | const lines = err.stack.split('\n'); 77 | return lines.slice(from, to).join('\n'); 78 | }, 79 | 80 | error: function(where, msg, obj) { 81 | if (this.logLevel < LOG_LEVEL_ERROR) { 82 | return; 83 | } 84 | obj = obj || ''; 85 | console.error(this.cols.error(where), this.cols.error(msg), obj); 86 | 87 | const stackLines = this.stackLines(3, 10); 88 | where = 'ERROR: ' + stackLines + '\n / ' + where; 89 | console.log(stackLines); 90 | }, 91 | 92 | // move to TextLib 93 | // does ~same as dashedName() method so remove this one 94 | sanitize: function(str, opts) { 95 | if (opts && opts.spaces) { 96 | str = str.replace(/\s/g, '-'); 97 | } 98 | str = str.toLowerCase(); 99 | str = str.replace('.md', ''); 100 | str = str.replace(/([^a-z0-9áéíóúñü_@\-\s]|[\t\n\f\r\v\0])/gim, ''); 101 | return str; 102 | }, 103 | 104 | // display filenames replace the - with a space 105 | namify: function(str) { 106 | str = str.replace(/-/g, ' '); 107 | return str; 108 | }, 109 | 110 | asFileName: function(str) { 111 | if (str) { 112 | str = str.replace(/ /g, '-'); 113 | } 114 | str = str.toLowerCase(); 115 | return str; 116 | }, 117 | 118 | // text is optional if we want URL to be different from displayed text 119 | linkify: function(path, where, text) { 120 | let host; 121 | 122 | where = where || 'wiki'; 123 | text = text || path; 124 | if (!path) { 125 | Utils.error('tried to linkify an empty item'); 126 | return '-----'; 127 | } 128 | // not URL encoded 129 | path = path.replace('?', '%3F'); 130 | 131 | switch (where) { 132 | case 'gitter': 133 | case 'camperbot': 134 | host = AppConfig.gitterHost + AppConfig.getBotName() + '/'; 135 | break; 136 | case 'wiki': 137 | host = AppConfig.wikiHost; 138 | break; 139 | default: 140 | break; 141 | } 142 | 143 | const uri = host + path; 144 | const name = Utils.namify(text); 145 | const link = '[' + name + '](' + uri + ')'; 146 | Utils.clog('Utils.linkify>', 'link', link); 147 | return link; 148 | }, 149 | 150 | splitParams: function(text) { 151 | if (typeof text !== 'string') { 152 | this.warn('splitParams>', 'text is not a string'); 153 | return null; 154 | } 155 | 156 | let params; 157 | const parts = text.split(' '); 158 | const keyword = parts.shift(); 159 | 160 | if (parts.length > 0) { 161 | params = parts.join(' '); 162 | } 163 | const res = { 164 | keyword: keyword, 165 | params: params 166 | }; 167 | 168 | return res; 169 | }, 170 | 171 | checkNotNull: function(item, msg) { 172 | if (item) { 173 | // means OK 174 | return true; 175 | } else { 176 | Utils.error(msg); 177 | return false; 178 | } 179 | }, 180 | 181 | isObject: function(obj, errmsg) { 182 | errmsg = errmsg || 'not an object'; 183 | 184 | if (typeof obj === 'object') { 185 | // means OK 186 | return true; 187 | } else { 188 | this.error(errmsg, obj); 189 | return false; 190 | } 191 | }, 192 | 193 | 194 | makeMdnLinks: function(items) { 195 | let out = ''; 196 | if (!items) { 197 | Utils.error('tried to makeMdnLinks for no items'); 198 | return ''; 199 | } 200 | items.forEach(one => { 201 | out += '\n- [' + one + '](' + MDNlinks[one] + ')'; 202 | }); 203 | return out; 204 | }, 205 | 206 | timeStamp: function(when, baseDate) { 207 | let month; 208 | let day; 209 | baseDate = baseDate || new Date(); 210 | const d1 = new Date(); 211 | 212 | switch (when) { 213 | case 'yesterday': 214 | default: 215 | d1.setDate(baseDate.getDate() - 1); 216 | } 217 | 218 | month = d1.getMonth() + 1; 219 | month = _.padLeft(month, 2, '0'); 220 | 221 | day = d1.getDate(); 222 | day = _.padLeft(day, 2, '0'); 223 | 224 | const timestamp = d1.getFullYear() + '/' + month + '/' + day; 225 | return timestamp; 226 | }, 227 | 228 | hasProperty: function(obj, prop, msg) { 229 | if (obj && obj.hasOwnProperty(prop)) { 230 | return true; 231 | } 232 | msg = msg || 'ERROR'; 233 | Utils.error(msg); 234 | Utils.error('missing property', prop, obj); 235 | return false; 236 | }, 237 | 238 | betaFooter: function() { 239 | return '\n\n >this feature is linked to our [beta site](beta.freecodecamp' + 240 | '.com), so it may not have all users til we go live with the new ' + 241 | 'release. Also check that FCC ID matches githubID!'; 242 | } 243 | }; 244 | 245 | Utils.logLevel = parseInt(process.env.LOG_LEVEL || 4, 10); 246 | 247 | module.exports = Utils; 248 | -------------------------------------------------------------------------------- /logs/README.md: -------------------------------------------------------------------------------- 1 | this file keeps the directory even if contents are ignored -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "camperbot", 3 | "version": "0.0.13", 4 | "dependencies": { 5 | "cli-color": "", 6 | "dotenv": "^1.2.0", 7 | "lodash": "^3.10.1", 8 | "node-gitter": "^1.2.8", 9 | "request": "~2.27.0" 10 | }, 11 | "devDependencies": { 12 | "babel-eslint": "^6.0.0", 13 | "eslint": "^2.0.0", 14 | "eslint-plugin-react": "^3.11.3", 15 | "faucet": "0.0.1", 16 | "gulp": "^3.9.0", 17 | "gulp-env": "^0.2.0", 18 | "gulp-eslint": "^2.0.0", 19 | "gulp-tape": "0.0.7", 20 | "nodemon": "~1.0.15", 21 | "tap-spec": "^4.1.1", 22 | "tape": "^4.2.2" 23 | }, 24 | "scripts": { 25 | "lint": "node_modules/eslint/bin/eslint.js 'config/*.js' 'data/rooms/*.js' 'data/*.js' 'lib/**/*.js' 'test/*.js' 'app.js' 'gulpfile.js'", 26 | "start": "node app.js", 27 | "test": "npm run make-env npm && npm run make-config && npm run lint && LOG_LEVEL=0 SERVER_ENV=test node_modules/tape/bin/tape test/*.spec.js | tap-spec", 28 | "make-env": "node -e 'var fs = require(\"fs\"); if(!fs.existsSync(\"./.env\")) { var rs = fs.createReadStream(\"./dot-EXAMPLE.env\"); var ws = fs.createWriteStream(\".env\"); rs.pipe(ws); }'", 29 | "make-config": "node -e 'var fs = require(\"fs\"); if(!fs.existsSync(\"./config.json\")) { var rs = fs.createReadStream(\"./example.config.json\"); var ws = fs.createWriteStream(\"config.json\"); rs.pipe(ws); }'" 30 | }, 31 | "description": "[![Join the chat at https://gitter.im/FreeCodeCamp/camperbot](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/FreeCodeCamp/camperbot) [![Stories in Ready](https://badge.waffle.io/FreeCodeCamp/camperbot.png?label=ready&title=Ready)](https://waffle.io/FreeCodeCamp/camperbot)", 32 | "main": "app.js", 33 | "directories": { 34 | "test": "test" 35 | }, 36 | "repository": { 37 | "type": "git", 38 | "url": "git+https://github.com/freeCodeCamp/camperbot.git" 39 | }, 40 | "keywords": [ 41 | "bot", 42 | "gitter", 43 | "chat" 44 | ], 45 | "author": "freeCodeCamp", 46 | "license": "BSD-3-Clause", 47 | "bugs": { 48 | "url": "https://github.com/freeCodeCamp/camperbot/issues" 49 | }, 50 | "homepage": "https://github.com/freeCodeCamp/camperbot#readme" 51 | } 52 | -------------------------------------------------------------------------------- /test/AppConfig.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('tape'); 4 | const AppConfig = require('../config/AppConfig'); 5 | 6 | test('AppConfig test', t => { 7 | t.plan(3); 8 | 9 | t.equal(AppConfig.testUser, 'bothelp', 'should have default AppConfig'); 10 | 11 | t.test('should make a topicDmUri', (st) => { 12 | const topicDmUri = AppConfig.topicDmUri(); 13 | const expUri = AppConfig.appHost + '/go?dm=y&room=bothelp'; 14 | st.plan(1); 15 | st.equal(topicDmUri, expUri); 16 | st.end(); 17 | }); 18 | 19 | t.equal(AppConfig.getBotName(), 'bothelp', 'should setup the botname'); 20 | 21 | t.end(); 22 | }); 23 | -------------------------------------------------------------------------------- /test/Commands.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('tape'); 4 | const GBot = require('../lib/bot/GBot.js'); 5 | const BotCommands = require('../lib/bot/BotCommands'); 6 | const TestHelper = require('./helpers/TestHelper'); 7 | 8 | test('Command tests', t => { 9 | t.plan(3); 10 | 11 | t.test('isCommand: XXXX false', st => { 12 | st.plan(1); 13 | const input = { keyword: 'XXXX' }; 14 | const res = BotCommands.isCommand(input); 15 | st.notOk(res); 16 | st.end(); 17 | }); 18 | 19 | t.test('should show archives', st => { 20 | st.plan(2); 21 | const archive = BotCommands.archive(TestHelper.stubInput); 22 | st.notEqual(archive, null, 'archive should not be null'); 23 | st.ok(archive.includes('Archives for '), 'should be a valid archive'); 24 | st.end(); 25 | }); 26 | 27 | t.test('should have a find command', st => { 28 | st.plan(3); 29 | const input = TestHelper.makeInputFromString('find js'); 30 | const res = BotCommands.find(input, GBot); 31 | st.equal(input.keyword, 'find', 'keyword should be find'); 32 | st.equal(input.params, 'js', 'param should be js'); 33 | st.ok(res.includes('find **js**'), 'response should be valid'); 34 | st.end(); 35 | }); 36 | 37 | t.end(); 38 | }); 39 | -------------------------------------------------------------------------------- /test/GBot.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('tape'); 4 | const AppConfig = require('../config/AppConfig'); 5 | const GBot = require('../lib/bot/GBot'); 6 | const TestHelper = require('./helpers/TestHelper'); 7 | const KBase = require('../lib/bot/KBase'); 8 | 9 | function testMessage(command) { 10 | const message = TestHelper.makeMessageFromString(command); 11 | return GBot.findAnyReply(message); 12 | } 13 | 14 | test('GBot tests', t => { 15 | t.plan(8); 16 | 17 | t.doesNotThrow(() => { 18 | KBase.initSync(); 19 | }, 'kbase should load'); 20 | 21 | t.equal(GBot.getName(), 'bothelp', 'bot should have a name'); 22 | 23 | t.test('GBot should not reply to itself', st => { 24 | st.plan(1); 25 | const botname = AppConfig.getBotName(); 26 | const flag = GBot.isBot(botname); 27 | st.ok(flag); 28 | st.end(); 29 | }); 30 | 31 | t.test('GBot should format non-help as false command', st => { 32 | st.plan(1); 33 | const input = TestHelper.makeMessageFromString('DONT bootstrap'); 34 | const output = GBot.parseInput(input); 35 | st.notOk(output.command, 'should return false'); 36 | st.end(); 37 | }); 38 | 39 | t.skip('GBot should respond to wiki migration', st => { 40 | st.plan(1); 41 | const res = testMessage('wiki'); 42 | console.log(res); 43 | st.ok(res.includes('forum')); 44 | st.end(); 45 | }); 46 | 47 | t.test('GBot should have a botstatus response', st => { 48 | st.plan(1); 49 | const res = testMessage('botstatus'); 50 | st.ok(res.includes('All bot systems are go!')); 51 | st.end(); 52 | }); 53 | 54 | t.test('GBot should send a thanks karma reply', st => { 55 | st.plan(1); 56 | const res = testMessage('thanks @bob'); 57 | st.ok(res.includes('testuser sends brownie points to')); 58 | st.end(); 59 | }); 60 | 61 | t.test('GBot should have a find command', st => { 62 | st.plan(1); 63 | const res = testMessage('find XXX'); 64 | st.ok(res.includes('find **')); 65 | st.end(); 66 | }); 67 | 68 | t.end(); 69 | }); 70 | -------------------------------------------------------------------------------- /test/GitterHelper.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('tape'); 4 | const GitterHelper = require('../lib/gitter/GitterHelper'); 5 | 6 | const TEST_ROOM_NAME = 'camperbot/localdev'; 7 | 8 | test('GitterHelper tests', t => { 9 | t.plan(3); 10 | 11 | t.test('GitterHelper should find room by name', st => { 12 | st.plan(1); 13 | const foundRoom = blob => { 14 | const foundRoomName = blob.gitterRoom.uri.toLowerCase(); 15 | st.equal(foundRoomName, TEST_ROOM_NAME); 16 | st.end(); 17 | }; 18 | GitterHelper.findRoomByName(TEST_ROOM_NAME, foundRoom); 19 | }); 20 | 21 | t.test('GitterHelper should store room info in cache', st => { 22 | st.plan(1); 23 | const foundRoom2 = () => { 24 | const cachedRoom = GitterHelper.roomDataCache[TEST_ROOM_NAME]; 25 | st.equal(cachedRoom.uri, TEST_ROOM_NAME); 26 | st.end(); 27 | }; 28 | GitterHelper.findRoomByName(TEST_ROOM_NAME, foundRoom2); 29 | }); 30 | 31 | t.doesNotThrow(() => { 32 | GitterHelper.sayToRoomName('autotest', TEST_ROOM_NAME); 33 | }, 'should send a message to a named room'); 34 | 35 | t.end(); 36 | }); 37 | -------------------------------------------------------------------------------- /test/HttpWrap.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('tape'); 4 | const HttpWrap = require('../lib/utils/HttpWrap.js'); 5 | 6 | test('HttpWrap tests', t => { 7 | t.plan(1); 8 | const name = 'berkeleytrue'; 9 | const apiPath = '/api/users/about?username=' + name; 10 | const options = { method: 'GET' }; 11 | 12 | HttpWrap.callApi(apiPath, options, apiResult => { 13 | t.equal(apiResult.response.about.username, 'berkeleytrue', 14 | 'should return correct username'); 15 | t.end(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /test/Parser.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('tape'); 4 | const GBot = require('../lib/bot/GBot.js'); 5 | const TestHelper = require('./helpers/TestHelper'); 6 | 7 | function testParser(command) { 8 | const msg = TestHelper.makeMessageFromString(command); 9 | return GBot.parseInput(msg); 10 | } 11 | 12 | test('Parser tests', t => { 13 | t.plan(2); 14 | 15 | t.test('should find a thanks command', st => { 16 | st.plan(2); 17 | const res = testParser('thanks @bob'); 18 | st.equal(res.keyword, 'thanks', 'keyword should be thanks'); 19 | st.ok(res.command); 20 | st.end(); 21 | }); 22 | 23 | t.test('should parse a thanks command with a hashtag', st => { 24 | st.plan(2); 25 | const res = testParser('thanks @bob #hashtag'); 26 | st.equal(res.keyword, 'thanks'); 27 | st.ok(res.command); 28 | st.end(); 29 | }); 30 | 31 | }); 32 | -------------------------------------------------------------------------------- /test/RoomMessages.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('tape'); 4 | const RoomMessages = require('../data/rooms/RoomMessages'); 5 | const TestHelper = require('./helpers/TestHelper'); 6 | 7 | test('RoomMessages tests', t => { 8 | t.plan(4); 9 | 10 | t.test('should find a message', st => { 11 | st.plan(1); 12 | const msg = 'you gotta holler i say'; 13 | const input = TestHelper.makeInputFromString(msg); 14 | const res = RoomMessages.scanInput(input, 'camperbot/testing', 1); 15 | st.equal(res.text, '> holler back!'); 16 | st.end(); 17 | }); 18 | 19 | t.test('should be silent in 0 chance tooms', st => { 20 | st.plan(1); 21 | const msg = 'you gotta holler i say'; 22 | const input = TestHelper.makeInputFromString(msg); 23 | const res = RoomMessages.scanInput(input, 'camperbot/testing', 0); 24 | st.equal(res, null); 25 | st.end(); 26 | }); 27 | 28 | t.test('should find a message three ticks \'\'\'', st => { 29 | st.plan(1); 30 | const msg = 'mistake \'\'\' text'; 31 | const input = TestHelper.makeInputFromString(msg); 32 | const res = RoomMessages.scanInput(input, 'camperbot/testing', 1); 33 | st.ok(res.text.includes('> :bulb: to format')); 34 | st.end(); 35 | }); 36 | 37 | t.test('should find a message for a bonfire', st => { 38 | st.plan(1); 39 | const msg = 'help for bonfire XXX'; 40 | const input = TestHelper.makeInputFromString(msg); 41 | const res = RoomMessages.scanInput(input, 'camperbot/testing', 1); 42 | st.ok(res.text.includes('> type `bonfire name`')); 43 | st.end(); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /test/Rooms.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('tape'); 4 | const Rooms = require('../lib/app/Rooms.js'); 5 | 6 | test('Rooms tests', t => { 7 | t.plan(3); 8 | 9 | t.equal(Rooms.findByTopic('bonfires').name, 10 | 'bothelp/HelpBonfires', 11 | 'should find a room for topic'); 12 | 13 | t.equal(Rooms.findByName('bothelp/HelpBonfires').name, 14 | 'bothelp/HelpBonfires', 15 | 'should find a room by name'); 16 | 17 | t.test('should find a bonfire room', st => { 18 | st.plan(2); 19 | const room = Rooms.findByName('bothelp/bonfire-factorialize-a-number'); 20 | st.equal(room.name, 'bothelp/bonfire-factorialize-a-number', 21 | 'name is correct'); 22 | st.equal(room.isBonfire, true, 'should be flagged as bonfire'); 23 | st.end(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test/TextLib.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const test = require('tape'); 6 | const TextLib = require('../lib/utils/TextLib'); 7 | 8 | test('TextLib tests', t => { 9 | t.plan(2); 10 | 11 | var longTextBlock = `# Headline 12 | line 1 13 | line 2 14 | line 3 15 | line 4 16 | line 5 17 | line 6 18 | line 7 19 | line 8 20 | line 9 21 | line 10 22 | `; 23 | 24 | t.test('should take the first 5 lines of a chunk', st => { 25 | st.plan(3); 26 | const short = TextLib.trimLines(longTextBlock, 5); 27 | const split = short.split('\n'); 28 | st.equal(split.length, 5, 'should have trimmed correct number of lines'); 29 | st.ok(split[0].includes('# Headline'), 'first line should be correct'); 30 | st.ok(split[split.length - 1].includes('line 4'), 31 | 'last line should be correct'); 32 | st.end(); 33 | }); 34 | 35 | t.test('should trim camperbot entry', st => { 36 | st.plan(3); 37 | let topicData = fs.readFileSync(path.resolve(__dirname, 38 | 'helpers/testWikiArticle.md')).toString(); 39 | let short = TextLib.trimLines(topicData); 40 | let split = short.split('\n'); 41 | st.equal(split.length, 12, 'should have trimmed correct number of lines'); 42 | st.ok(split[0].includes('Hi, I\'m **[CamperBot'), 43 | 'first line should be correct'); 44 | st.equal(split[split.length - 1], '', 45 | 'last line should be correct'); 46 | st.end(); 47 | }); 48 | 49 | t.end(); 50 | }); 51 | -------------------------------------------------------------------------------- /test/Thanks.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('tape'); 4 | const GBot = require('../lib/bot/GBot'); 5 | const TestHelper = require('./helpers/TestHelper'); 6 | 7 | test('Thanks tests', t => { 8 | t.plan(1); 9 | 10 | t.test('should work for two users', st => { 11 | st.plan(1); 12 | const msg = TestHelper.makeInputFromString('thanks @dcsan @bob'); 13 | const res = GBot.findAnyReply(msg.message); 14 | st.ok(res.includes('> testuser sends brownie points to ' + 15 | '@dcsan and @berkeleytrue')); 16 | st.end(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /test/Utils.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('tape'); 4 | const Utils = require('../lib/utils/Utils'); 5 | 6 | test('Utils tests', t => { 7 | t.plan(9); 8 | 9 | t.equal(Utils.linkify('wiki', 'wiki'), 10 | '[wiki](https://github.com/freecodecamp/freecodecamp/wiki/wiki)', 11 | 'should linkify'); 12 | 13 | t.equal(Utils.namify('some-page-here'), 14 | 'some page here', 15 | 'should namify'); 16 | 17 | t.equal(Utils.sanitize('something-special?.md'), 18 | 'something-special', 19 | 'should sanitize file name strings'); 20 | 21 | t.equal(Utils.sanitize('thanks bob', { spaces: false }), 22 | 'thanks bob', 23 | 'sanitize with spaces:false should not remove spaces'); 24 | 25 | t.equal(Utils.sanitize('thanks for that', { spaces: true }), 26 | 'thanks-for-that', 27 | 'sanitize with spaces:true should convert spaces to dashes'); 28 | 29 | t.test('splitParams command only', st => { 30 | st.plan(2); 31 | const res = Utils.splitParams('menu'); 32 | st.equal(res.keyword, 'menu', 'should have menu keyword'); 33 | st.notOk(res.params, 'should have no params'); 34 | st.end(); 35 | }); 36 | 37 | t.test('splitParams command and one param', st => { 38 | st.plan(2); 39 | const res = Utils.splitParams('menu options'); 40 | st.equal(res.keyword, 'menu', 'should have menu keyword'); 41 | st.equal(res.params, 'options', 'should have options param'); 42 | st.end(); 43 | }); 44 | 45 | t.test('splitParams command and multiple params', st => { 46 | st.plan(2); 47 | const res = Utils.splitParams('menu with more stuff'); 48 | st.equal(res.keyword, 'menu', 'should have menu keyword'); 49 | st.equal(res.params, 'with more stuff', 50 | 'should have with more stuff params'); 51 | st.end(); 52 | }); 53 | 54 | t.equal(Utils.linkify('SomePageName'), 55 | '[SomePageName](https://github.com/' + 56 | 'freecodecamp/freecodecamp/wiki/SomePageName)', 57 | 'should make a wiki link'); 58 | }); 59 | -------------------------------------------------------------------------------- /test/helpers/TestHelper.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const GBot = require('../../lib/bot/GBot'); 4 | 5 | const TestHelper = { 6 | aboutInput: { 7 | keyword: 'about', 8 | params: '@bothelp ', 9 | message: { 10 | operation: 'create', 11 | model: { 12 | id: '55b95acb5bc8dc88744243ff', 13 | text: 'about @bothelp', 14 | html: 'about @bothelp ', 16 | sent: '2015-07-29T22:59:23.187Z', 17 | editedAt: null, 18 | fromUser: { 19 | username: 'testuser' 20 | }, 21 | unread: true, 22 | readBy: 0, 23 | urls: [], 24 | mentions: [ 25 | { screenName: 'dcsan' }, 26 | { screenName: 'berkeleytrue' } 27 | ], 28 | issues: [], 29 | meta: {}, 30 | v: 1 31 | }, 32 | room: { 33 | path: '/rooms', 34 | id: '55b3d5780fc9f982beaaf7f4', 35 | name: 'camperbot/localdev', 36 | topic: ' testing', 37 | uri: 'camperbot/localdev', 38 | oneToOne: false, 39 | userCount: 5, 40 | unreadItems: 100, 41 | mentions: 27, 42 | lastAccessTime: '2015-07-29T14:41:04.820Z', 43 | lurk: false, 44 | url: '/camperbot/localdev', 45 | githubType: 'USER_CHANNEL', 46 | security: 'PUBLIC', 47 | noindex: false, 48 | v: 1, 49 | client: [Object], 50 | faye: [Object], 51 | _events: [Object] 52 | } 53 | }, 54 | cleanText: 'about @bothelp ', 55 | command: true 56 | }, 57 | 58 | stubInput: { 59 | keyword: 'hint', 60 | params: null, 61 | message: { 62 | operation: 'create', 63 | model: { 64 | id: '55b91175c35e438c74fc725c', 65 | text: 'hint', 66 | html: 'hint', 67 | sent: '2015-07-29T17:46:29.190Z', 68 | editedAt: null, 69 | fromUser: [Object], 70 | unread: true, 71 | readBy: 0, 72 | urls: [], 73 | mentions: [], 74 | issues: [], 75 | meta: {}, 76 | v: 1 77 | }, 78 | room: { 79 | path: '/rooms', 80 | id: '55b8fc980fc9f982beab6b19', 81 | name: 'bothelp/bonfire-factorialize-a-number', 82 | topic: '', 83 | uri: 'bothelp/bonfire-factorialize-a-number', 84 | oneToOne: false, 85 | userCount: 3, 86 | unreadItems: 9, 87 | mentions: 0, 88 | lastAccessTime: '2015-07-29T16:17:28.850Z', 89 | lurk: false, 90 | url: '/bothelp/bonfire-factorialize-a-number', 91 | githubType: 'USER_CHANNEL', 92 | security: 'PUBLIC', 93 | noindex: false, 94 | client: [Object], 95 | faye: [Object], 96 | _events: [Object] 97 | } 98 | }, 99 | cleanText: 'hint', 100 | command: true 101 | }, 102 | 103 | mockInput: function(roomName) { 104 | const input = { 105 | message: { 106 | room: { 107 | name: roomName 108 | } 109 | } 110 | }; 111 | 112 | return input; 113 | }, 114 | 115 | // used for tests 116 | // and also strings to commands 117 | // https://developer.gitter.im/docs/messages-resource 118 | // makeInputFromString: function (text) { 119 | // var message = {}; 120 | // var model = { 121 | // text: text 122 | // }; 123 | // message.model = model; 124 | // return message; 125 | // }, 126 | 127 | makeInputFromString: function(text) { 128 | let input = TestHelper.aboutInput; 129 | // initialize before parsing 130 | input.message.model.text = text; 131 | // add keywords etc. 132 | input = GBot.parseInput(input.message); 133 | 134 | input.message.model.text = text; 135 | input.message.model.fromUser = { 136 | username: 'testuser' 137 | }; 138 | return input; 139 | }, 140 | 141 | makeMessageFromString: function(text) { 142 | const input = TestHelper.makeInputFromString(text); 143 | return input.message; 144 | } 145 | }; 146 | 147 | module.exports = TestHelper; 148 | -------------------------------------------------------------------------------- /test/helpers/testWikiArticle.md: -------------------------------------------------------------------------------- 1 | Hi, I'm **[CamperBot](https://github.com/FreeCodeCamp/freecodecamp/wiki/camperbot)**! I can help you in this chatroom :smile: 2 | 3 | ### Basic Commands: 4 | - ``find TOPIC`` find all entries about topic. ex: `find js` 5 | - `wiki TOPIC` show contents of topic page 6 | - `thanks @username` send brownie points to another user 7 | - `about @username` shows info on that user 8 | - `Algorithm BONFIRENAME` info on a Algorithm 9 | 10 | :speech_balloon: [meet CamperBot in this room!](https://gitter.im/FreeCodeCamp/camperbotPlayground) 11 | 12 | 13 | ## Example Commands 14 | 15 | ``` 16 | find js # all JS entries 17 | wiki muta # first entry with muta in name 18 | wiki bobbytables # showing images 19 | wiki video # and video 20 | Algorithm roman # any Algorithm with roman in name 21 | ``` 22 | For playing with CamperBot please use the testing channel: 23 | [https://gitter.im/FreeCodeCamp/camperbotPlayground](https://gitter.im/FreeCodeCamp/camperbotPlayground) 24 | 25 | ## Help on Algorithms 26 | Live currently on the HelpBonFires channel on Gitter 27 | [https://gitter.im/FreeCodeCamp/HelpBonfires](https://gitter.im/FreeCodeCamp/HelpBonfires) 28 | 29 | We've added some Algorithm specific commands. If you type `Algorithm $BONFIRENAME` (where $BONFIRENAME is part of a Algorithm name) it will set the chat to be about that Algorithm. Then you can use some other Algorithm specific commands: 30 | 31 | - bf details - more info 32 | - bf spoiler - show some hints 33 | 34 | ## More commands 35 | - `update` pulls new wiki edits asap 36 | - `topics` selected pages from the wiki 37 | - `rooms` all rooms the bot is in 38 | - `archives` show history 39 | - `music` deprecated, plug DJ no longer exists. 40 | - `twitch` show the twitch feed 41 | 42 | ## Content for the wiki 43 | Feel free to make new pages, an example entry is here: 44 | https://github.com/FreeCodeCamp/freecodecamp/wiki/example 45 | 46 | ## Source Repository 47 | ### [https://github.com/FreeCodeCamp/camperbot](https://github.com/FreeCodeCamp/camperbot) 48 | Fork it and have fun! 49 | 50 | ## Roadmap 51 | We're looking for ideas for new features to add, and some people to help working on the bot. 52 | Have a look at Tickets with [help wanted](https://github.com/FreeCodeCamp/camperbot/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22) on the repo and maybe you can contribute? 53 | 54 | In future, we're planning: 55 | - Algorithm step-by-step tutorials will be available via chat and from the CamperBot 56 | - realtime tagging and searching of chats by topic 57 | - a scripting language and natural language processing 58 | Get involved and let us know what you'd like to see next! 59 | 60 | ## Developer Chatroom 61 | - [Join us on our repository chat room](https://gitter.im/FreeCodeCamp/camperbot) to discuss new features. Perhaps we can pair up to work on the bot 2.0! 62 | - [Join this chat room](https://gitter.im/FreeCodeCamp/camperbotPlayground) to mess around with the CamperBot and try out commands, proofread your edits to wiki content etc. 63 | 64 | 65 | ![enjoy](https://avatars1.githubusercontent.com/camperbot?&s=100) Happy Coding! 66 | --------------------------------------------------------------------------------