├── .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 | [](https://gitter.im/FreeCodeCamp/camperbot) [](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 | 
58 |
59 | ### Share wiki summaries in chat
60 |
61 | Use `explain` to pull a wiki summary right into the chat:
62 | 
63 |
64 | ### Points system
65 |
66 | Allow your users to send points to each other to say `thanks @username`
67 | 
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": "[](https://gitter.im/FreeCodeCamp/camperbot) [](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 |  Happy Coding!
66 |
--------------------------------------------------------------------------------