├── .gitignore ├── LICENSE ├── README.md ├── data ├── config.json └── quiz_questions.json ├── nodemon-config.json ├── package.json ├── src ├── commands │ ├── command.js │ ├── command_handler.js │ ├── default_command_handler.js │ ├── poll_command_handler.js │ ├── ref_command_handler.js │ └── role_command_handler.js ├── deprecated │ ├── game_command_handler.js │ ├── games │ │ └── guessing_game.js │ ├── mine_command_handler.js │ ├── quiz │ │ ├── quiz_command_handler.js │ │ └── quiz_session.js │ ├── quiz_command_handler.js │ └── words_command_handler.js ├── events │ ├── member_join_handler.js │ ├── member_leave_handler.js │ ├── member_update_event.js │ ├── message_mod_handler.js │ └── message_sent_handler.js ├── hopson_bot.js ├── main.js └── util.js └── test ├── commands ├── default_command_handler.test.js ├── poll_command_handler.test.js └── role_command_handler.test.js ├── discord_mocks ├── mock_channel.js ├── mock_guild.js ├── mock_guild_member.js ├── mock_message.js ├── mock_role.js └── mock_user.js ├── events └── message_delete.test.js └── test_start.js /.gitignore: -------------------------------------------------------------------------------- 1 | data/config.js 2 | node_modules/ 3 | package-lock.json 4 | *.swp 5 | *.vs 6 | data/words_db.json 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Hopson Community 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HopsonBot 2 | Discord bot for the Hopson Community Server, written using Discord.js 3 | 4 | API key is hidden for obvious reasons. 5 | 6 | ## Features 7 | 8 | ### Command System 9 | 10 | Hopson Bot has commands split into multiple categories, which can be seen from the `>help` command 11 | 12 | ![Help command](https://i.imgur.com/ut8CrO8.png) 13 | 14 | To access the commands of the catergories, the user must put the category name followed by the command. 15 | 16 | For example, to access the `role` commands, the user must do `>role ` 17 | 18 | "Default" commands do not require this, for example 8ball and source command do not require a category prefix. 19 | 20 | #### Role commands 21 | 22 | Role commands are for changing your roles, as well as seeing how many people have a certain role. 23 | 24 | ![Role help](https://i.imgur.com/zaS44A5.png) 25 | 26 | #### Poll commands 27 | 28 | The poll commands allow the user to create a poll, and then have people vote using reactions. 29 | 30 | ![Poll example](https://i.imgur.com/LQwrzEc.png) 31 | -------------------------------------------------------------------------------- /data/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "quizChannels": [ 3 | "bot-testing", 4 | "trusted-house", 5 | "voice-text-chat", 6 | "members-voice-text-chat", 7 | "use-bots-here" 8 | ], 9 | "modifiableRoles": [ 10 | "C++", 11 | "Wot++", 12 | "OpenGL", 13 | "Linux", 14 | "Windows", 15 | "SFML", 16 | "SDL", 17 | "Java", 18 | "C#", 19 | "C-Language", 20 | "Rust", 21 | "Python", 22 | "ASM", 23 | "WebDev", 24 | "CSGO", 25 | "Fortnite", 26 | "Jackbox", 27 | "PUBG", 28 | "ProofWatcher", 29 | "Terraria", 30 | "Events" 31 | ], 32 | "welcomeChannel": "welcome", 33 | "leaveChannel": "goodbye", 34 | "newMemberRole": "NewMember", 35 | "introRole": "Introduced", 36 | "newMemberChannel": "introductions", 37 | "adminRole": "Admins", 38 | "memberJoinChannel": "new-members", 39 | "logBlacklistedChannels": [ 40 | "293461625426411520" 41 | ], 42 | "maxMentions": 3 43 | } 44 | -------------------------------------------------------------------------------- /data/quiz_questions.json: -------------------------------------------------------------------------------- 1 | { 2 | "categories": [ 3 | "Video-Games", 4 | "Movies", 5 | "Nature", 6 | "Maths", 7 | "Computer-Science", 8 | "Geography", 9 | "Music", 10 | "History", 11 | "Science" 12 | ], 13 | "questions": [ 14 | { 15 | "cat": "Video-Games", 16 | "question": "What was Mario from Super Mario Bros. original name?", 17 | "answer": "Jumpman", 18 | "author": "115025985405059076" 19 | }, 20 | { 21 | "cat": "Video-Games", 22 | "question": "What was the forest area from Super Mario World called?", 23 | "answer": "Forest of Illusion", 24 | "author": "115025985405059076" 25 | }, 26 | { 27 | "cat": "Video-Games", 28 | "question": "What was the second Ratchet and Clank game called in the UK? [Hint: Ratchet and Clank: ]", 29 | "answer": "Locked and Loaded", 30 | "author": "115025985405059076" 31 | }, 32 | { 33 | "cat": "Maths", 34 | "question": "What is 2 + 2?", 35 | "answer": "4", 36 | "author": "115025985405059076" 37 | }, 38 | { 39 | "cat": "Maths", 40 | "question": "What's the derivate of e?", 41 | "answer": "e", 42 | "author": "Forces Unknown" 43 | }, 44 | { 45 | "cat": "Geography", 46 | "question": "What is the capital city of England?", 47 | "answer": "London", 48 | "author": "115025985405059076" 49 | }, 50 | { 51 | "cat": "Geography", 52 | "question": "What USA state will you find the city of Orlando?", 53 | "answer": "Florida", 54 | "author": "115025985405059076" 55 | }, 56 | { 57 | "cat": "Video-Games", 58 | "question": "What type is the pokémon bidoof?", 59 | "answer": "Normal", 60 | "author": "202103129330810880" 61 | }, 62 | { 63 | "cat": "Video-Games", 64 | "question": "What type of Pokemon is Weedle?", 65 | "answer": "bug", 66 | "author": "115025985405059076" 67 | }, 68 | { 69 | "cat": "Geography", 70 | "question": "What was the city of Istanbul called before 1930?", 71 | "answer": "Constantinople", 72 | "author": "115025985405059076" 73 | }, 74 | { 75 | "cat": "Geography", 76 | "question": "What is the capital city of France?", 77 | "answer": "Paris", 78 | "author": "115025985405059076" 79 | }, 80 | { 81 | "cat": "Computer-Science", 82 | "question": "Who was the original creator of C++?", 83 | "answer": "Bjarne Stroustrup", 84 | "author": "115025985405059076" 85 | }, 86 | { 87 | "cat": "Movies", 88 | "question": "Who was the director of the 1997 film 'Titanic'?", 89 | "answer": "James Cameron", 90 | "author": "115025985405059076" 91 | }, 92 | { 93 | "cat": "Video-Games", 94 | "question": "What is the most famous game by Bay12Games?", 95 | "answer": "Dwarf Fortress", 96 | "author": "202103129330810880" 97 | }, 98 | { 99 | "cat": "Nature", 100 | "question": "What is a group of lions called? [1 word]", 101 | "answer": "Pride", 102 | "author": "115025985405059076" 103 | }, 104 | { 105 | "cat": "Video-Games", 106 | "question": "Who was the lead game designer of Skyrim?", 107 | "answer": "Todd Howard", 108 | "author": "115025985405059076" 109 | }, 110 | { 111 | "cat": "Movies", 112 | "question": "What was the first Star Wars movie released? Hint: Star Wars: ", 113 | "answer": "A New Hope", 114 | "author": "115025985405059076" 115 | }, 116 | { 117 | "cat": "Computer-Science", 118 | "question": "What language is C++ a superset of?", 119 | "answer": "C", 120 | "author": "115025985405059076" 121 | }, 122 | { 123 | "cat": "Geography", 124 | "question": "Which country lies on the border between Spain and France?", 125 | "answer": "Andorra", 126 | "author": "115025985405059076" 127 | }, 128 | { 129 | "cat": "Computer-Science", 130 | "question": "What does USB stand for?", 131 | "answer": "Universal Serial Bus", 132 | "author": "115025985405059076" 133 | }, 134 | { 135 | "cat": "Maths", 136 | "question": "What is square root of -1?", 137 | "answer": "i" 138 | }, 139 | { 140 | "cat": "Nature", 141 | "question": "How many legs do all insects have?", 142 | "answer": "6", 143 | "author": "115025985405059076" 144 | }, 145 | { 146 | "cat": "Nature", 147 | "question": "What is the only type of bird which has the ability to fly backwards?", 148 | "answer": "hummingbird", 149 | "author": "115025985405059076" 150 | }, 151 | { 152 | "cat": "Nature", 153 | "question": "What shape is a goats pupil?", 154 | "answer": "rectangle", 155 | "author": "115025985405059076" 156 | }, 157 | { 158 | "cat": "Movies", 159 | "question": "How many stars are on the Paramount studio logo?", 160 | "answer": "22", 161 | "author": "115025985405059076" 162 | }, 163 | { 164 | "cat": "Geography", 165 | "question": "What is the capital city of Japan?", 166 | "answer": "Tokyo", 167 | "author": "115025985405059076" 168 | }, 169 | { 170 | "cat": "Video-Games", 171 | "question": "In Minecraft, what is the surface block of the Mushroom biome?", 172 | "answer": "Mycelium", 173 | "author": "115025985405059076" 174 | }, 175 | { 176 | "cat": "Maths", 177 | "question": "What is the seventh number of the Fibbonacci sequence?", 178 | "answer": "13", 179 | "author": "231104432102834176" 180 | }, 181 | { 182 | "cat": "Video-Games", 183 | "question": "What is the tallest mountain in Skyrim? [4 words]", 184 | "answer": "Throat of the World", 185 | "author": "115025985405059076" 186 | }, 187 | { 188 | "cat": "Computer-Science", 189 | "question": "A NOR gate is considered a universal gates; What is the other universal gate?", 190 | "answer": "NAND", 191 | "author": "115025985405059076" 192 | }, 193 | { 194 | "cat": "Maths", 195 | "question": "What is 9 squared?", 196 | "answer": "81", 197 | "author": "115025985405059076" 198 | }, 199 | { 200 | "cat": "Maths", 201 | "question": "What is the square root of 144?", 202 | "answer": "12", 203 | "author": "115025985405059076" 204 | }, 205 | { 206 | "cat": "Geography", 207 | "question": "In what city will you find the Burj Khalifa?", 208 | "answer": "Dubai", 209 | "author": "115025985405059076" 210 | }, 211 | { 212 | "cat": "Geography", 213 | "question": "What is the westernmost country in Europe?", 214 | "answer": "Iceland", 215 | "author": "231104432102834176" 216 | }, 217 | { 218 | "cat": "Video-Games", 219 | "question": "What is the only game from the Mother/Earthbound series that hasn't yet been brought to the US?", 220 | "answer": "Mother 3", 221 | "author": "343644106569940994" 222 | }, 223 | { 224 | "cat": "Geography", 225 | "question": "What is the most commonly spoken language in the world?", 226 | "answer": "Chinese", 227 | "author": "115025985405059076" 228 | }, 229 | { 230 | "cat": "Geography", 231 | "question": "What is the longest river in the world?", 232 | "answer": "Amazon", 233 | "author": "115025985405059076" 234 | }, 235 | { 236 | "cat": "Nature", 237 | "question": "What trees do acorns grow on?", 238 | "answer": "Oak", 239 | "author": "115025985405059076" 240 | }, 241 | { 242 | "cat": "Geography", 243 | "question": "What is the largest country in the world in terms of surface area?", 244 | "answer": "Russia", 245 | "author": "115025985405059076" 246 | }, 247 | { 248 | "cat": "Geography", 249 | "question": "What shape is at the center of the national flag of Japan?", 250 | "answer": "Circle", 251 | "author": "115025985405059076" 252 | }, 253 | { 254 | "cat": "Computer-Science", 255 | "question": "Who is credited for breaking the Nazi's ciphers via the use of the Enigma Machine?", 256 | "answer": "Alan Turing", 257 | "author": "150049709698973696" 258 | }, 259 | { 260 | "cat": "Geography", 261 | "question": "In which USA state is Mount Rushmore located?", 262 | "answer": "South Dakota", 263 | "author": "115025985405059076" 264 | }, 265 | { 266 | "cat": "Geography", 267 | "question": "Which sea borders the south of France?", 268 | "answer": "Mediterranean", 269 | "author": "115025985405059076" 270 | }, 271 | { 272 | "cat": "Geography", 273 | "question": "In which USA state is Area 51 located?", 274 | "answer": "Nevada", 275 | "author": "115025985405059076" 276 | }, 277 | { 278 | "cat": "Movies", 279 | "question": "Who created the monster in the movie 'Frankenstein'?", 280 | "answer": "Dr Frankenstein", 281 | "author": "115025985405059076" 282 | }, 283 | { 284 | "cat": "Nature", 285 | "question": "What is the temperature to which all particles freeze?", 286 | "answer": "-273.15", 287 | "author": "343644106569940994" 288 | }, 289 | { 290 | "cat": "Movies", 291 | "question": "What was the maiden name of Harry Potter's mother?", 292 | "answer": "Lily", 293 | "author": "115025985405059076" 294 | }, 295 | { 296 | "cat": "Movies", 297 | "question": "What was the name of the main antagonist from the horror film 'Sinister'?", 298 | "answer": "Bughuul", 299 | "author": "343644106569940994" 300 | }, 301 | { 302 | "cat": "Geography", 303 | "question": "What is the largest island in the Caribbean?", 304 | "answer": "Cuba", 305 | "author": "115025985405059076" 306 | }, 307 | { 308 | "cat": "Nature", 309 | "question": "What is the fastest fish in the ocean?", 310 | "answer": "Sailfish", 311 | "author": "115025985405059076" 312 | }, 313 | { 314 | "cat": "Movies", 315 | "question": "In 'Mean Girls', what continent did Cady Heron live in before moving to the USA?", 316 | "answer": "Africa", 317 | "author": "115025985405059076" 318 | }, 319 | { 320 | "cat": "Movies", 321 | "question": "What is Darth Vader's real name?", 322 | "answer": "Anakin Skywalker", 323 | "author": "115025985405059076" 324 | }, 325 | { 326 | "cat": "Video-Games", 327 | "question": "In what region of Tamriel is the game TES: Oblivion set?", 328 | "answer": "cyrodiil", 329 | "author": "115025985405059076" 330 | }, 331 | { 332 | "cat": "Movies", 333 | "question": "Which emoji did Patrick Stewart voice in the emoji movie?", 334 | "answer": "💩", 335 | "author": "145945481825091584" 336 | }, 337 | { 338 | "cat": "Music", 339 | "question": "Who is the drummer from the band TooL?", 340 | "answer": "Danny Carrey", 341 | "author": "343644106569940994" 342 | }, 343 | { 344 | "cat": "Maths", 345 | "question": "What is the square root of - 1", 346 | "answer": "i", 347 | "author": "145945481825091584" 348 | }, 349 | { 350 | "cat": "History", 351 | "question": "What year did World War Two end?", 352 | "answer": "1945", 353 | "author": "115025985405059076" 354 | }, 355 | { 356 | "cat": "Computer-Science", 357 | "question": "Who is most usually credited for coming up with the fast inverse square root algorithm?", 358 | "answer": "John Carmack", 359 | "author": "221609129347645442" 360 | }, 361 | { 362 | "cat": "History", 363 | "question": "What year did World War Two begin?", 364 | "answer": "1939", 365 | "author": "115025985405059076" 366 | }, 367 | { 368 | "cat": "History", 369 | "question": "What year did Harold get an arrow in his eye?", 370 | "answer": "1066", 371 | "author": "145945481825091584" 372 | }, 373 | { 374 | "cat": "Science", 375 | "question": "What is 2nd planet from the sun?", 376 | "answer": "Venus", 377 | "author": "115025985405059076" 378 | }, 379 | { 380 | "cat": "Science", 381 | "question": "What will you find inbetween Mars and Jupiter?", 382 | "answer": "asteroid Belt", 383 | "author": "115025985405059076" 384 | }, 385 | { 386 | "cat": "Science", 387 | "question": "What is the powerhouse of the cell?", 388 | "answer": "mitochondria", 389 | "author": "115025985405059076" 390 | }, 391 | { 392 | "cat": "Music", 393 | "question": "What is the abreviattion to the industrial metal band 'Nine Inch Nails'?", 394 | "answer": "NIN", 395 | "author": "343644106569940994" 396 | }, 397 | { 398 | "cat": "Video-Games", 399 | "question": "What does RYNO stand for in the Ratchet and Clank series?", 400 | "answer": "Rip Ya A New One", 401 | "author": "115025985405059076" 402 | }, 403 | { 404 | "cat": "Maths", 405 | "question": "Who invented Fermat's last theorem", 406 | "answer": "Fermat", 407 | "author": "237264833433567233" 408 | }, 409 | { 410 | "cat": "Maths", 411 | "question": "What is 9 + 10 * 2?", 412 | "answer": "29", 413 | "author": "115025985405059076" 414 | }, 415 | { 416 | "cat": "Maths", 417 | "question": "What is a^2+b^2=c^2 known as?", 418 | "answer": "pythagoras theorem", 419 | "author": "115025985405059076" 420 | }, 421 | { 422 | "cat": "Maths", 423 | "question": "What is 101b2 + 100b2?", 424 | "answer": "1001b2", 425 | "author": "340661564556312591" 426 | }, 427 | { 428 | "cat": "Video-Games", 429 | "question": "Who created Stardew Valley all by himself?", 430 | "answer": "Eric Barone", 431 | "author": "343644106569940994" 432 | }, 433 | { 434 | "cat": "Maths", 435 | "question": "What is 5 + 5?", 436 | "answer": "10", 437 | "author": "115025985405059076" 438 | }, 439 | { 440 | "cat": "History", 441 | "question": "The cold war ended with the death of which infamous leader?", 442 | "answer": "Joseph Stalin", 443 | "author": "115025985405059076" 444 | }, 445 | { 446 | "cat": "Geography", 447 | "question": "Which country in the UK does not appear on the flag of the Union Jack?", 448 | "answer": "Wales", 449 | "author": "115025985405059076" 450 | }, 451 | { 452 | "cat": "Movies", 453 | "question": "Which movie is the famous quote 'Houston, we have a problem' from?", 454 | "answer": "Apollo 13", 455 | "author": "115025985405059076" 456 | }, 457 | { 458 | "cat": "Geography", 459 | "question": "What is the capital city of the USA state of New York?", 460 | "answer": "Albany", 461 | "author": "115025985405059076" 462 | }, 463 | { 464 | "cat": "Maths", 465 | "question": "What is 5 + 10?", 466 | "answer": "15", 467 | "author": "115025985405059076" 468 | }, 469 | { 470 | "cat": "Music", 471 | "question": "Complete the lyric's missing space: 'I, hurt myself today, to see if I still __'", 472 | "answer": "feel", 473 | "author": "343644106569940994" 474 | }, 475 | { 476 | "cat": "Maths", 477 | "question": "How many numbers from 1 to 333 contain a 2?", 478 | "answer": "151", 479 | "author": "237264833433567233" 480 | }, 481 | { 482 | "cat": "Maths", 483 | "question": "How many prime numbers are there between 1 and 100?", 484 | "answer": "25", 485 | "author": "115025985405059076" 486 | }, 487 | { 488 | "cat": "Maths", 489 | "question": "How many digits does the number 2^30 have?", 490 | "answer": "10", 491 | "author": "293835615513083918" 492 | }, 493 | { 494 | "cat": "Maths", 495 | "question": "How many zeros does a Googol have?", 496 | "answer": "100", 497 | "author": "343644106569940994" 498 | }, 499 | { 500 | "cat": "History", 501 | "question": "Which state did the USA purchase from Russia in 1867?", 502 | "answer": "Alaska", 503 | "author": "115025985405059076" 504 | }, 505 | { 506 | "cat": "Science", 507 | "question": "What is the greeting of Vsauce in his videos?", 508 | "answer": "Hey, Vsauce, Michael here", 509 | "author": "293835615513083918" 510 | }, 511 | { 512 | "cat": "Geography", 513 | "question": "In alphabetical order, which 4 USA states border Mexico?(Answer like: State1 State2 State3 State4", 514 | "answer": "Arizona California New Mexico Texas", 515 | "author": "115025985405059076" 516 | }, 517 | { 518 | "cat": "History", 519 | "question": "The period of European history that lasted from the 14th to the the 17th century is known as the...?", 520 | "answer": "Renaissance", 521 | "author": "115025985405059076" 522 | }, 523 | { 524 | "cat": "Music", 525 | "question": "What was the most controversial movie the band Nine Inch Nails created?", 526 | "answer": "The Broken Movie", 527 | "author": "343644106569940994" 528 | }, 529 | { 530 | "cat": "Science", 531 | "question": "Which one mostly transports blood with much oxygen: Arteries or veins?", 532 | "answer": "Arteries", 533 | "author": "293835615513083918" 534 | }, 535 | { 536 | "cat": "Music", 537 | "question": "What artist was known for composing DOOM (2016)'s soundtrack", 538 | "answer": "Mick Gordon", 539 | "author": "343644106569940994" 540 | }, 541 | { 542 | "cat": "Geography", 543 | "question": "What country borders the south of USA?", 544 | "answer": "Mexico", 545 | "author": "115025985405059076" 546 | }, 547 | { 548 | "cat": "Geography", 549 | "question": "In which ocean will you find the mariana trench?", 550 | "answer": "pacific", 551 | "author": "115025985405059076" 552 | }, 553 | { 554 | "cat": "Science", 555 | "question": "How many manned missions landed on the moon?", 556 | "answer": "6", 557 | "author": "231104432102834176" 558 | }, 559 | { 560 | "cat": "Movies", 561 | "question": "What was the subtitle of the first Harry Potter movie in the UK? Harry Poterr and the...", 562 | "answer": "Philosopher's Stone", 563 | "author": "115025985405059076" 564 | }, 565 | { 566 | "cat": "Music", 567 | "question": "How many strings does a bass guitar have?", 568 | "answer": "4", 569 | "author": "293835615513083918" 570 | }, 571 | { 572 | "cat": "Movies", 573 | "question": "In the Harry Potter universe, what name is given to non-magic people?", 574 | "answer": "Muggle", 575 | "author": "115025985405059076" 576 | }, 577 | { 578 | "cat": "Movies", 579 | "question": "What are the different worlds called in Inception?", 580 | "answer": "dream layers", 581 | "author": "293835615513083918" 582 | }, 583 | { 584 | "cat": "Science", 585 | "question": "In what year was *the general theory of relativity* published?", 586 | "answer": "1915", 587 | "author": "231104432102834176" 588 | }, 589 | { 590 | "cat": "History", 591 | "question": "What year was The Great Fire of London?", 592 | "answer": "1666", 593 | "author": "115025985405059076" 594 | }, 595 | { 596 | "cat": "Science", 597 | "question": "How many biceps muscles does a human have?", 598 | "answer": "4", 599 | "author": "293835615513083918" 600 | }, 601 | { 602 | "cat": "Video-Games", 603 | "question": "What is the creator of Minecraft's real name?", 604 | "answer": "Markus Persson", 605 | "author": "115025985405059076" 606 | }, 607 | { 608 | "cat": "Maths", 609 | "question": "What is 30/(1/2) + 10?", 610 | "answer": "70", 611 | "author": "293835615513083918" 612 | }, 613 | { 614 | "cat": "Computer-Science", 615 | "question": "What is a pointer technically?", 616 | "answer": "Integer", 617 | "author": "293835615513083918" 618 | }, 619 | { 620 | "cat": "Nature", 621 | "question": "What tree type would you find apples on?", 622 | "answer": "Apple", 623 | "author": "115025985405059076" 624 | }, 625 | { 626 | "cat": "Video-Games", 627 | "question": "What is Mario, from Super Mario Bro's, real job?", 628 | "answer": "Plumber", 629 | "author": "115025985405059076" 630 | }, 631 | { 632 | "cat": "Nature", 633 | "question": "What kind of verebrate is a dolphin?", 634 | "answer": "Mammal", 635 | "author": "293835615513083918" 636 | } 637 | ] 638 | } -------------------------------------------------------------------------------- /nodemon-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignore": [ 3 | "*.json" 4 | ] 5 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hopsonbot", 3 | "version": "2.0.0", 4 | "description": "Bot for my Discord community server.", 5 | "main": "scripts/bot.js", 6 | "scripts": { 7 | "start": "nodemon -x 'node src/main.js ; touch src/main.js' --config nodemon-config.json", 8 | "test": "cross-env SINGLE=true ./node_modules/.bin/qunit test/test_start.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/HopsonCommunity/HopsonBot.git" 13 | }, 14 | "author": "", 15 | "license": "ISC", 16 | "bugs": { 17 | "url": "https://github.com/HopsonCommunity/HopsonBot/issues" 18 | }, 19 | "homepage": "https://github.com/HopsonCommunity/HopsonBot#readme", 20 | "dependencies": { 21 | "cheerio": "^1.0.0-rc.2", 22 | "dateformat": "^3.0.3", 23 | "discord.js": "^11.2.1", 24 | "jsonfile": "^4.0.0", 25 | "node-fetch": "^2.3.0", 26 | "request": "^2.88.0", 27 | "request-promise": "^4.2.4" 28 | }, 29 | "devDependencies": { 30 | "cross-env": "^5.2.0", 31 | "nodemon": "^1.18.10", 32 | "qunit": "^2.9.2" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/commands/command.js: -------------------------------------------------------------------------------- 1 | /* 2 | Just random command info 3 | */ 4 | module.exports = class Comamnd { 5 | /* 6 | * @param {String} description Description of command 7 | * @param {String} example Example useage of command 8 | * @param {String or function(MessageInfo)} action Command action 9 | */ 10 | constructor (description, example, action) { 11 | this.description = description; 12 | this.example = example; 13 | this.action = action; 14 | } 15 | } -------------------------------------------------------------------------------- /src/commands/command_handler.js: -------------------------------------------------------------------------------- 1 | const Discord = require('discord.js'); 2 | const Command = require('./command'); 3 | 4 | /** 5 | * The base of all command handlers 6 | */ 7 | module.exports = class CommandHandlerBase { 8 | /** 9 | * @param {String} commandCategory String sent by user that will invoke this command handler 10 | */ 11 | constructor(commandCategory) { 12 | this.commandCategory = commandCategory; 13 | this.simpleCommands = new Map(); //String and string 14 | this.commands = new Map(); //String and Command 15 | this.addCommand("help", "", "", this.sendHelpList.bind(this)); 16 | } 17 | 18 | /** 19 | * Returns true if this class is the command handler trying to be accessed 20 | * @param {String} id A string that is the first part of a message 21 | */ 22 | isCommand(id) { 23 | return id === this.commandCategory; 24 | } 25 | 26 | /** 27 | * Handles a command 28 | * @param {Discord Text Message} message The raw message 29 | * @param {[String]} args The "args" of the command 30 | */ 31 | handleCommand(message, args, client) { 32 | const command = args[0]; 33 | 34 | if (this.simpleCommands.has(command)) { 35 | message.channel.send(this.simpleCommands.get(command).action); 36 | } 37 | else if (this.commands.has(command)) { 38 | args.splice(0, 1);//remove command name 39 | const cmd = this.commands.get(command); 40 | cmd.action(message, args, client); 41 | } 42 | } 43 | 44 | /** 45 | * Add a command which has the response of an simple message 46 | * @param {String} commandName The ID/ name of the command that will invoke this 47 | * @param {String} description Description of what the command does 48 | * @param {String} example Example useage of the command 49 | * @param {String} action The string that is sent by the bot in response 50 | */ 51 | addBasicCommand(commandName, description, action) { 52 | const fullExample = `>${this.commandCategory} ${commandName}`; 53 | this.simpleCommands.set(commandName, new Command(description, fullExample, action)); 54 | return this; 55 | } 56 | 57 | /** 58 | * Add a command which has the response of a function call 59 | * @param {String} commandName The ID/ name of the command that will invoke this 60 | * @param {String} description Description of what the command does 61 | * @param {String} example Example useage of the command 62 | * @param {function(messageInfo)} action Function that is called in response the command. This must take in a 3 args, (message, args, client) 63 | */ 64 | addCommand(commandName, description, example, action) { 65 | this.commands.set(commandName, new Command(description, example, action)); 66 | return this; 67 | } 68 | 69 | /** 70 | * [Command] Sends help list 71 | * @param {MessageInfo} msgInfo Information about the message 72 | */ 73 | sendHelpList(msgInfo) { 74 | let output = new Discord.RichEmbed() 75 | .setTitle("Commands for " + this.commandCategory.toUpperCase()) 76 | .setColor("#09f228"); 77 | 78 | function addOutput(m) { 79 | m.forEach(function(command, commandName, _) { 80 | if (commandName === "help") return; 81 | output.addField(`**__${commandName}__**`, 82 | `Description: ${command.description}\nExample: *${command.example}*`); 83 | }); 84 | } 85 | addOutput(this.simpleCommands); 86 | addOutput(this.commands); 87 | msgInfo.channel.send(output); 88 | } 89 | } -------------------------------------------------------------------------------- /src/commands/default_command_handler.js: -------------------------------------------------------------------------------- 1 | const CommandHandler = require('./command_handler'); 2 | const Util = require('../util') 3 | 4 | module.exports = class DefaultCommands extends CommandHandler { 5 | constructor() { 6 | super(''); 7 | super.addBasicCommand( 8 | "source", 9 | "Get the HopsonBot source code (link to GitHub", 10 | "https://github.com/HopsonCommunity/HopsonBot"); 11 | /* 12 | super.addCommand( 13 | "8ball", 14 | "Ask the magic 8ball for some wisdom.", 15 | ">8ball Will Hopson upload tomorrow?", 16 | eightball 17 | )*/ 18 | } 19 | 20 | 21 | getCommands() { 22 | return new Map([...this.simpleCommands, ...this.commands]) 23 | } 24 | } 25 | 26 | //8-ball command 27 | const BALL_RESULTS = ["Yes.", "Reply hazy, try again.", "Without a doubt.", 28 | "My sources say no.", "As I see it, yes.", "You may rely on it.", 29 | "Concentrate and ask again.", "Outlook not so good", 30 | "It is decidedly so.", "Better not tell you now.", 31 | "Very doubtful.", "Yes - definitely.", "It is certain.", 32 | "Cannot predict now.", "Most likely.", "Ask again later.", 33 | "My reply is no.", "Outlook good.", "Don't count on it.", 34 | "Are you kidding?", "Probably.", "Yes, in due time.", "Go away.", 35 | "Hopson is more likely to put out a video than that"]; 36 | 37 | /** 38 | * The 8ball command 39 | * @param {TextMessage} message Raw discord text message 40 | * @param {[String]} args Question to ask the 8ball 41 | */ 42 | function eightball(message, args) 43 | { 44 | // Make sure the field is not empty 45 | if (args[0] === undefined) { 46 | Bot.sendMessage(message.channel, "No question given"); 47 | return; 48 | } 49 | 50 | // Get result 51 | const RESPONSE_INDEX = Util.getRandomInt(0, BALL_RESULTS.length); 52 | message.channel.send(`🎱 The Magic 8-Ball says: "${BALL_RESULTS[RESPONSE_INDEX]}" 🎱`); 53 | } -------------------------------------------------------------------------------- /src/commands/poll_command_handler.js: -------------------------------------------------------------------------------- 1 | // Command category created by bag/ Ruixel @ github 2 | // Allows for polling via the use of reactions 3 | // Types of polls: 4 | // - Yes / No 5 | // - Option-based (1, 2, 3, 4) 6 | const CommandHandler = require('./command_handler'); 7 | 8 | // Number emojis 9 | const NUM_EMOJIS = [ 10 | '1⃣', '2⃣', '3⃣', '4⃣', '5⃣', 11 | '6⃣', '7⃣', '8⃣', '9⃣', '🔟']; 12 | 13 | module.exports = class PollCommandHandler extends CommandHandler { 14 | constructor() { 15 | super('poll'); 16 | super.addCommand( 17 | "yesno", 18 | "Poll yes/no style questions", 19 | ">poll yesno Should I go out tonight?", 20 | pollYesno 21 | ) 22 | .addCommand( 23 | "options", 24 | "Poll questions with options", 25 | '>poll options "Question here" optionA optionB', 26 | pollOptions 27 | ); 28 | } 29 | } 30 | 31 | /** 32 | * Sends a poll message for a yes/no question 33 | * @param {Discord message} message The raw discord message 34 | * @param {[String]} args List of string, the command arguments 35 | */ 36 | function pollYesno(message, args) { 37 | const question = args.join(" "); 38 | if (question == "" || question == " ") { 39 | createHopsonPollingStationEmbed(message.channel, "Please add a question."); 40 | return; 41 | } 42 | 43 | createHopsonPollingStationEmbed(message.channel, question) 44 | .then(message => { 45 | message.react("✅"); 46 | 47 | // Small delay so the cross always comes last 48 | setTimeout(_ => { 49 | message.react("❌") 50 | }, 1000); 51 | }); 52 | } 53 | 54 | /** 55 | * Sends a poll message for a question with multiple options 56 | * @param {Discord message} message The raw discord message 57 | * @param {[String]} args List of string, the command arguments 58 | */ 59 | function pollOptions(message, args) { 60 | // Make sure there's at least two choises 61 | const channel = message.channel; 62 | if (validationDoesNotPass(args.length < 1, 'Not enough known to create a poll, please provide a question with options eg `">poll option "How many stars is my food?" 1 2 3 4 5"`', channel)) { 63 | return; 64 | } 65 | 66 | if (validationDoesNotPass(!args[0].startsWith("\""), 'The question should be wrapped between two " characters.', channel)) { 67 | return; 68 | } 69 | //Extract question 70 | let question = ""; 71 | let full = args.join(" ").slice(1) 72 | let isQuestion = false; 73 | for (const c of full) { 74 | full = full.slice(1); 75 | if (c === "\"") { 76 | isQuestion = true; 77 | break; 78 | } 79 | question += c; 80 | } 81 | 82 | if (validationDoesNotPass(!isQuestion, 'The question should be wrapped between two " characters.', channel)) { 83 | return; 84 | } 85 | 86 | //Extract options 87 | const options = full 88 | .split(/(\s+)/) 89 | .filter(v => v != ' ' && v != ''); 90 | 91 | if (validationDoesNotPass(options.length < 2, 'At least 2 options must be provided.', channel)) { 92 | return; 93 | } 94 | 95 | if (validationDoesNotPass(options.length > 9, 'Maximum of 9 options allowed.', channel)) { 96 | return; 97 | } 98 | 99 | //Add options to the outputted text 100 | let fieldText = question; 101 | for (const option in options) { 102 | fieldText += `\nTo answer with ${options[option]}, react with ${NUM_EMOJIS[option]}` 103 | } 104 | 105 | //Fire the question 106 | createHopsonPollingStationEmbed(message.channel, fieldText) 107 | .then(message => { 108 | for (const option in options) { 109 | delayedReactWithNumber(message, option); 110 | } 111 | }); 112 | } 113 | 114 | /** 115 | * Tests whether some expression passes, and sends an error message in the case it doesn't 116 | * @param {Boolean} test An expression to yield true or false, aka the test to check 117 | * @param {String} errorMessage The message to send in the case of a failed test 118 | * @param {DiscordChannel} channel The text channel to send the error message to in the pass of failure 119 | */ 120 | function validationDoesNotPass(validation, errorMessage, channel) { 121 | if (validation) { 122 | createHopsonPollingStationEmbed( 123 | channel, 124 | `Unable to poll! ${errorMessage}` 125 | ); 126 | return true; 127 | } 128 | return false; 129 | } 130 | 131 | /** 132 | * Sends a reaction to a message using the emojis in the array above 133 | * @param {DiscordMessage} message The message to add reactions to 134 | * @param {Number} n The emoji to send, number from the array above 135 | */ 136 | function delayedReactWithNumber(message, n) { 137 | // 0.5s timeout seems to be the best when theres a large number of options 138 | setTimeout(function() { 139 | message.react(NUM_EMOJIS[n]); 140 | }, 500*n); 141 | } 142 | 143 | /** 144 | * Creates a embed message, using "Hopson Polling Station" as the title 145 | * @param {DiscordTextChannel} channel The text channel to send the message to 146 | * @param {String} value The string to embed in the polling station message 147 | */ 148 | function createHopsonPollingStationEmbed(channel, value) { 149 | return channel.send({embed: { 150 | color: 3447003, 151 | fields: [{ 152 | name: "*Hopson Polling Station*", 153 | value: value 154 | }] 155 | }}); 156 | } 157 | -------------------------------------------------------------------------------- /src/commands/ref_command_handler.js: -------------------------------------------------------------------------------- 1 | const CommandHandler = require('./command_handler'); 2 | 3 | const request = require('request-promise'); 4 | const cheerio = require('cheerio'); 5 | 6 | module.exports = class ReferenceCommandHandler extends CommandHandler { 7 | constructor() { 8 | super('ref'); 9 | super.addCommand( 10 | "cpp", 11 | "Gets a link to C++ reference for a specific header", 12 | ">ref cpp vector", 13 | cppReference 14 | ); 15 | } 16 | } 17 | 18 | function cppReference(message, args) { 19 | const channel = message.channel; 20 | if (args.length < 1) { 21 | return; 22 | } 23 | 24 | request("https://en.cppreference.com/w/cpp/header") 25 | .then((html) => { 26 | const anchors = cheerio('a', html); 27 | const hrefs = []; 28 | for (let i = 0; i < anchors.length; i++) { 29 | const ref = anchors[i].attribs.href; 30 | if (ref) { 31 | if (ref.startsWith("/w/cpp/")) { 32 | hrefs.push(anchors[i].attribs.href); 33 | } 34 | } 35 | } 36 | 37 | const results = []; 38 | for (const ref of hrefs) { 39 | if (ref.search(args[0]) > 0) { 40 | console.log(ref); 41 | results.push("https://en.cppreference.com/" + ref); 42 | } 43 | } 44 | 45 | if (results.length > 0) { 46 | let msg = {embed: { 47 | title: `Results found for ${args[0]}:\n`, 48 | color: 3447003, 49 | fields: [] 50 | }}; 51 | 52 | for (const result of results) { 53 | //We implement the limit INSIDE the loop, and break if we are done, so we don't have to loop 54 | //over protentially hundreds of results if we're already out of the limit 55 | if (msg.embed.fields.length >= 24) { 56 | msg.embed.fields.push({ 57 | name: ":warning: Error", 58 | value: "There are too many results to display them all, please use a more specific query" 59 | }); 60 | msg.embed.color = 15452468; 61 | break; 62 | } 63 | 64 | msg.embed.fields.push({ 65 | name: "#" + (msg.embed.fields.length + 1), 66 | value: result, 67 | inline: true 68 | }); 69 | } 70 | 71 | channel.send(msg); 72 | } 73 | else { 74 | channel.send({embed: { 75 | color: 16525315, 76 | fields: [{ 77 | name: "Error", 78 | value: `I cannot find anything in C++ with ${args[0]}` 79 | }] 80 | }}); 81 | } 82 | }) 83 | .catch((error) => { 84 | console.log(`Error: ${error}`); 85 | }); 86 | } 87 | -------------------------------------------------------------------------------- /src/commands/role_command_handler.js: -------------------------------------------------------------------------------- 1 | const Config = require('../../data/config.json'); 2 | const CommandHandler = require('./command_handler'); 3 | const Discord = require('discord.js'); 4 | 5 | module.exports = class RoleEventHandler extends CommandHandler { 6 | constructor() { 7 | super('role'); 8 | super.addCommand( 9 | "list", 10 | "Gets a list of roles that can be modified by the user", 11 | ">role list", 12 | listRoles 13 | ) 14 | .addCommand( 15 | "count", 16 | "Counts how many people have a certain role", 17 | '>role count Admins', 18 | countRole 19 | ) 20 | .addCommand( 21 | "add", 22 | "Add 1 or more roles to yourself", 23 | ">role add C++ Java Linux", 24 | addRoles 25 | ) 26 | .addCommand( 27 | "remove", 28 | "Remove 1 or more roles from yourself", 29 | ">role remove C++ Java Linux", 30 | removeRoles 31 | ) 32 | } 33 | } 34 | 35 | /** 36 | * Outpus number of members to a single discord role 37 | * @param {TextMessage} message The raw discord message 38 | */ 39 | function listRoles(message) { 40 | const roleArray = Config.modifiableRoles; 41 | let output = new Discord.RichEmbed() 42 | .setTitle("Modifiable Roles From >role add/remove Commands"); 43 | 44 | for (const role in roleArray) { 45 | output.addField( 46 | `Role ${(role)}`, 47 | `${roleArray[role]}\n`, 48 | true); 49 | if (role === 25) { 50 | break; 51 | } 52 | } 53 | message.channel.send(output); 54 | } 55 | 56 | /** 57 | * Outpus number of members to a single discord role 58 | * @param {TextMessage} message The raw discord message 59 | * @param {[String]} args args[0] == role to count 60 | */ 61 | function countRole(message, args) { 62 | if (args.length < 1) { 63 | return; 64 | } 65 | const role = message.guild.roles.find((role) => { 66 | return role.name.toLowerCase() === args[0]; 67 | }); 68 | 69 | if (role === null) { 70 | var output = `Role '${args[0]} does not exist.`; 71 | } 72 | else { 73 | var output = `Number of users with role "**${args[0].toUpperCase()}**": ${role.members.size}`; 74 | } 75 | message.channel.send(output); 76 | } 77 | 78 | function addRoles(message, args, client) { 79 | modifyRoles(message, args, "add"); 80 | } 81 | 82 | function removeRoles(message, args, client) { 83 | modifyRoles(message, args, "remove"); 84 | } 85 | 86 | /** 87 | * Extracts roles from args and then adds/remove valid ones to/from the user 88 | * @param {TextMessage} message The raw discord message 89 | * @param {[String]} args List of string, supposedly roles names 90 | * @param {String} action Add or remove 91 | */ 92 | function modifyRoles(message, args, action) { 93 | const roleLists = extractRoles(message.guild, args); 94 | const member = message.member; 95 | 96 | if (roleLists.invalidRoles.length > 0) { 97 | message.channel.send(`I do not recognise the following roles: \n>${roleLists.invalidRoles.join('\n>')}`); 98 | } 99 | 100 | if (roleLists.validRoles.length > 0) { 101 | //Add/ Remove the roles 102 | if (action === "add") { 103 | for (const role of roleLists.validRoles) { 104 | member.addRole(role) 105 | .then (console.log("Role add successful")); 106 | } 107 | var verb = "added"; 108 | var dir = "to"; 109 | } 110 | else if (action === "remove") { 111 | for (role of roleLists.validRoles) { 112 | member.removeRole(role) 113 | .then (console.log("Role remove successful")); 114 | } 115 | var verb = "removed"; 116 | var dir = "from"; 117 | } 118 | //Send result 119 | const output = createOutput(roleLists.validRoles, message.author.id.toString(), verb, dir); 120 | message.channel.send(output); 121 | } 122 | } 123 | 124 | /** 125 | * Creates the output for roles added to the user 126 | * @param {Role} rolesAdded list of discord roles 127 | * @param {String} userID the user's ID 128 | * @param {String} verb can be "added" or "removed" 129 | * @param {String} dir Direction the roles are going (to or from) 130 | */ 131 | function createOutput(rolesAdded, userID, verb, dir) { 132 | if (rolesAdded.length === 0) { 133 | return; 134 | } 135 | const sp = rolesAdded.length == 1 ? "role" : "roles"; 136 | const roleNames = rolesAdded.map((role) => { 137 | return role.name; 138 | }); 139 | let output = `I have **${verb}** the following ${sp} ${dir} **<@${userID}>**:\n> ${roleNames.join("\n>")}\n\n`; 140 | if (rolesAdded.length == 1) { 141 | output += `Psst... Are you aware you can have multiple roles ${verb} at once? Give it a go!\n`; 142 | output += `Example: \`>role add/remove C++ Java Rust\``; 143 | } 144 | return output; 145 | } 146 | 147 | /** 148 | * Extracts guild roles from a string array of role names 149 | * @param {Discord Guild} guild The server where the command was run from 150 | * @param {[String]} roleList Array of role names to be extracted 151 | */ 152 | function extractRoles(guild, roleList) { 153 | const result = { 154 | validRoles: [], 155 | invalidRoles: [] 156 | } 157 | const modifiableRoles = Config.modifiableRoles.map(val => val.toLowerCase()); 158 | for (const roleName of roleList) { 159 | if (modifiableRoles.indexOf(roleName) > -1) { 160 | const role = guild.roles.find((roleToFind) => { 161 | return roleToFind.name.toLowerCase() === roleName; 162 | }); 163 | if (role !== null) { 164 | result.validRoles.push(role); 165 | } 166 | else { 167 | result.invalidRoles.push(roleName); 168 | } 169 | } 170 | else { 171 | result.invalidRoles.push(roleName); 172 | } 173 | } 174 | return result; 175 | } 176 | -------------------------------------------------------------------------------- /src/deprecated/game_command_handler.js: -------------------------------------------------------------------------------- 1 | const CommandHandler = require ("./command_handler") 2 | const GuessingGame = require ('../games/guessing_game'); 3 | 4 | const GAME_GUESS_NUM = 0; 5 | 6 | module.exports = class GameCommandHandler extends CommandHandler { 7 | constructor(gameSessions) { 8 | super('game'); 9 | this.games = [] 10 | this.initCommands(); 11 | this.gameSessions = gameSessions 12 | } 13 | 14 | initCommands() { 15 | this.games[GAME_GUESS_NUM] = false; 16 | super.addCommand( 17 | "guess-num", 18 | "Begins a very fun session of guessing numbers", 19 | ">game guess-num", 20 | this.guessNumber.bind(this) 21 | ) 22 | } 23 | 24 | guessNumber(message, args) { 25 | if (!this.games[GAME_GUESS_NUM]) { 26 | this.games[GAME_GUESS_NUM] = true; 27 | this.gameSessions.push(new GuessingGame(message.channel, this.gameSessions)); 28 | } 29 | else { 30 | message.channel.send("Sorry but guessing number game is already active."); 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /src/deprecated/games/guessing_game.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | module.exports = class GuessingGame { 4 | constructor(channel, sessions) { 5 | this.channel = channel; 6 | this.sessions = sessions; 7 | this.number = Math.round(Math.random() * 1000); 8 | channel.send("I am thinking of a number between 0 and 1000..."); 9 | console.log("Number: " + this.number); 10 | } 11 | 12 | update(message) { 13 | const channel = message.channel; 14 | if (channel === this.channel) { 15 | if (!isNaN(message.content)) { 16 | const n = Number(message.content); 17 | if (n === this.number) { 18 | channel.send(`${message.author} guessed correct! The number was ${this.number}`); 19 | } 20 | else if (n > this.number) { 21 | channel.send(`${n} guessed, but it is too big!`) 22 | } 23 | else { 24 | channel.send(`${n} guessed, but it is too small!`) 25 | } 26 | } 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/deprecated/mine_command_handler.js: -------------------------------------------------------------------------------- 1 | // Command category created by oboforty 2 | // Bot sends minesweeper field using spoilers 3 | const CommandHandler = require('./command_handler'); 4 | 5 | // Number emojis 6 | const NUM_EMOJIS = [':zero:', ':one:', ':two:', ':three:', ':four:', ':five:', ':six:', ':seven:', ':eight:', ':nine:', ':ten:']; 7 | 8 | module.exports = class MineCommandHandler extends CommandHandler { 9 | constructor() { 10 | super('mine'); 11 | this.initCommands(); 12 | } 13 | 14 | initCommands() { 15 | super.addCommand( 16 | "play", 17 | "Gives you a minesweeper field of NxM with B bombs..", 18 | ">mine play 5 5 10", 19 | mineGen 20 | ); 21 | } 22 | } 23 | 24 | /** 25 | * 26 | * @param {Discord message} message The raw discord message 27 | * @param {[String]} args List of string, the command arguments 28 | * @param {_} client unused 29 | */ 30 | function mineGen(message, args, client) { 31 | const mx = args[0]; 32 | const my = args[1]; 33 | const mines = args[2]; 34 | var str0 = ""; 35 | if (mx > 15 || my > 15 || mines >= mx*my*(2/3)){ 36 | message.channel.send("Invalid parameters"); 37 | } else { 38 | message.channel.send("Sending minefield"); 39 | var grid = generateMap(mx, my, mines); 40 | var str0 = createView(grid); 41 | str0 = message.author.username + " here is your mine field :triangular_flag_on_post: \n" + str0; 42 | message.author.send(str0); 43 | } 44 | } 45 | 46 | function generateMap(mx, my, mines) { 47 | var grid = []; 48 | for (var y=0; y= mx || ny >= my) 87 | continue; 88 | 89 | if (grid[ny][nx] == 'x') { 90 | nmines += 1; 91 | } 92 | } 93 | } 94 | 95 | grid[y][x] = nmines; 96 | } 97 | } 98 | 99 | return grid; 100 | } 101 | 102 | function createView(grid) { 103 | var str0 = ""; 104 | 105 | var mx = grid.length; 106 | var my = grid[0].length; 107 | 108 | var str0 = ""; 109 | 110 | for (var y=0; yquiz help"); 33 | return; 34 | } 35 | let quizChnnels = Config.quizChannels; 36 | if (quizChnnels.indexOf(channel.name) === -1) { 37 | Bot.sendMessage(message.channel, `To avoid spam, quizzes only work in the following channels:\n>${quizChnnels.join("\n>")}`); 38 | return; 39 | } 40 | 41 | let command = args[0].toLowerCase(); 42 | args = args.slice(1); 43 | 44 | super.respondToCommand(message, command, args); 45 | } 46 | 47 | //Attempts to begin a quiz 48 | tryStartQuiz(message, args) 49 | { 50 | if (this.quizActive) { 51 | Bot.sendMessage(message.channel, 52 | `Sorry, a quiz is currently active in ${this.session.channel.name}`); 53 | } 54 | else { 55 | this.quizActive = true; 56 | this.session = new QuizSession(message.channel); 57 | } 58 | } 59 | 60 | endQuiz() 61 | { 62 | this.quizActive = false; 63 | this.session.endQuiz(); 64 | this.session = null; 65 | } 66 | 67 | //Attempts to end a quiz 68 | tryEndQuiz(message, args) 69 | { 70 | let channel = message.channel; 71 | let user = message.member; 72 | 73 | if (!this.quizActive) { 74 | Bot.sendMessage(channel, `Sorry, a quiz is not active.`); 75 | } 76 | else if (!channelHasQuizSession(message.channel)) { //Cannot end a quiz from a different channel 77 | Bot.sendMessage(channel, `Sorry, you can not end a quiz from a different channel from which is currently active, which is **'${this.session.channel.name}'**.`); 78 | } 79 | else { 80 | this.endQuiz(); 81 | Bot.sendMessage(channel, `Quiz has been stopped manually by <@${user.id}>`) 82 | } 83 | } 84 | 85 | submitAnswer(message, answer) 86 | { 87 | if (!this.quizActive) return; 88 | if (!channelHasQuizSession(message.channel)) return; 89 | 90 | this.session.submitAnswer(message.member, answer); 91 | } 92 | 93 | printQuestion(message, args) 94 | { 95 | if(this.quizActive) { 96 | this.session.printQuestion("Question reminder"); 97 | } 98 | } 99 | 100 | trySkip(message, args) 101 | { 102 | if(this.quizActive) { 103 | this.session.addSkip(message.member); 104 | } 105 | } 106 | 107 | channelHasQuizSession(channel) { 108 | return this.session.channel === channel; 109 | } 110 | 111 | initializeCommands() 112 | { 113 | super.addSimpleCommand( 114 | "cats", 115 | `Quiz Categories:\n>${QuizJSON.categories.join("\n>")}`, 116 | "Shows a list of quiz catergories.", 117 | "quiz cats" 118 | ) 119 | 120 | super.addFunctionCommand( 121 | "start", 122 | this.tryStartQuiz.bind(this), 123 | "Given a quiz is not already active, this will start a new quiz.", 124 | "quiz start", 125 | true 126 | ); 127 | 128 | super.addFunctionCommand( 129 | "end", 130 | this.tryEndQuiz.bind(this), 131 | "Ends the quiz session.", 132 | "quiz end" 133 | ) 134 | 135 | super.addFunctionCommand( 136 | "add", 137 | tryAddQuestion, 138 | "Adds a new question to the quiz log, requies a quiz category, question, and answer.", 139 | `quiz add Maths "What is 5 + 5?" "10"`, 140 | true 141 | ) 142 | 143 | super.addFunctionCommand( 144 | "skip", 145 | this.trySkip.bind(this), 146 | "Skips the question; but requirs 1/2 of the quiz participants to do so.", 147 | "quiz skip", 148 | ) 149 | 150 | super.addFunctionCommand( 151 | "remind", 152 | this.printQuestion.bind(this), 153 | "Resends the question to remind you what it was.", 154 | "quiz remind", 155 | ) 156 | 157 | super.addFunctionCommand( 158 | "cat-count", 159 | countQuestions, 160 | "Counts the number of questions in each category, and outputs the results.", 161 | "quiz cat-count", 162 | ) 163 | 164 | super.addFunctionCommand( 165 | "author-count", 166 | countAuthors, 167 | "Says how many questions the authors have submitted", 168 | "quiz author-count" 169 | ) 170 | } 171 | } 172 | 173 | //Adds a question to the JSON file 174 | function addQuestion(category, qu, ans, authorID) 175 | { 176 | //Open the JSON file 177 | fs.readFile(questionsFile, 'utf8', function read(err, data){ 178 | if(err) { 179 | console.log(err) 180 | } 181 | else { 182 | let qFile = JSON.parse(data); //Read file into a json object 183 | qFile.questions.push({ //Add question to the questions array 184 | cat: category, 185 | question: qu, 186 | answer: ans, 187 | author: authorID 188 | }); 189 | let qOut = JSON.stringify(qFile, null, 4); //Rewrite the file 190 | fs.writeFile(questionsFile, qOut, function(err){console.log(err);}); 191 | } 192 | }); 193 | } 194 | //on tin 195 | function tryAddQuestion(message, args) 196 | { 197 | let channel = message.channel; 198 | let user = message.member; 199 | console.log(args); 200 | //Check question length 201 | let inFile = JSONFile.readFileSync(questionsFile); 202 | if (args.length == 0 || args.length < 3) { 203 | Bot.sendMessage(channel, "You have not provided me with enough information to add a question; I must know the category, question, and the answer to the question."); 204 | return; 205 | } 206 | //Check if the category input is valid 207 | let category = args[0]; 208 | if (inFile.categories.indexOf(category) === -1) { 209 | Bot.sendMessage(channel, `Category "${category}" doesn't exist. To see the list, use __*>quiz cats*__, and use correct casing.`) 210 | return; 211 | } 212 | //Outputs a message for when the parsing of the strings fails 213 | function parseFail() { 214 | Bot.sendMessage(channel, `For me to recognise a question/ answer, you must start and end your question with"`); 215 | } 216 | //Try extract the question and answer from 217 | args = args.slice(1); 218 | //Get question 219 | let [res1, question, newArgs] = Util.extractStringFromArray(args); 220 | if (!res1) { 221 | parseFail(); 222 | return; 223 | } 224 | //Get answer 225 | let [res2, answer, f] = Util.extractStringFromArray(newArgs); 226 | if (!res2) { 227 | parseFail(); 228 | return; 229 | } 230 | //Finally if all validations passed, add the question 231 | addQuestion(category, question, answer, user.id); 232 | Bot.sendMessage(channel, new Discord.RichEmbed() 233 | .setTitle("New Question Added to my quiz log!") 234 | .setColor(embedColour) 235 | .addField("**Category**", category) 236 | .addField("**Question**", question) 237 | .addField("**Answer**", answer) 238 | .addField("**Question Author**", `<@${user.id}>`)); 239 | } 240 | 241 | //Outputs the number of questions created by people who have created questions. 242 | function countAuthors(message, args) 243 | { 244 | let inFile = JSONFile.readFileSync(questionsFile); 245 | let questions = inFile.questions; 246 | let result = new Map(); 247 | //Count the authors 248 | for (var question of questions) { 249 | let author = question.author; 250 | if(result.has(author)) { 251 | let currentCount = result.get(author); 252 | result.set(author, currentCount + 1); 253 | } else { 254 | result.set(author, 1); 255 | } 256 | } 257 | let output = new Discord.RichEmbed() 258 | .setTitle("Total Questions for each Author") 259 | .setColor(embedColour); 260 | this.count = 0; 261 | result.forEach(function(val, key, map) { 262 | output.addField(`${val.toString()} questions by:`,`<@${key}>`, true); 263 | this.count++; 264 | if (this.count % 3 == 0) { 265 | count = 0; 266 | output.addBlankField(); 267 | } 268 | }.bind(this)); 269 | Bot.sendMessage(message.channel, output); 270 | } 271 | 272 | //Creates a count of how many questions are in each category 273 | function countQuestions(message, args) 274 | { 275 | let inFile = JSONFile.readFileSync(questionsFile); 276 | let cats = inFile.categories; 277 | let questions = inFile.questions; 278 | let result = new Map(); 279 | let total = 0; 280 | for (var category of cats) { 281 | result.set(category, 0); 282 | } 283 | for (var question of questions) { 284 | var val = result.get(question.cat); 285 | result.set(question.cat, val + 1); 286 | total++; 287 | } 288 | let output = new Discord.RichEmbed() 289 | .setTitle("Total Questions in each Category") 290 | .setColor(embedColour) 291 | .addField("Total", total.toString(), true); 292 | result.forEach(function(val, key, map) { 293 | output.addField(key, val.toString(), true); 294 | }); 295 | Bot.sendMessage(message.channel, output); 296 | } -------------------------------------------------------------------------------- /src/deprecated/quiz/quiz_session.js: -------------------------------------------------------------------------------- 1 | const questionsFile = "data/quiz_questions.json"; 2 | 3 | const Bot = require("../main"); 4 | const Util = require("../util"); 5 | const JSONFile = require('jsonfile'); 6 | const Discord = require('discord.js') 7 | 8 | const embedColour = 0x28abed; 9 | const MAX_USERS = 24; 10 | 11 | //Struct holding data about a question 12 | class Question 13 | { 14 | constructor(jsonField) 15 | { 16 | this.category = jsonField.cat; 17 | this.question = jsonField.question; 18 | this.answer = jsonField.answer; 19 | this.author = jsonField.author; 20 | } 21 | } 22 | 23 | //struct for holding data about a current person participating in the quiz 24 | class QuizUser 25 | { 26 | constructor() 27 | { 28 | this.score = 0; 29 | this.hasSkipped = false; 30 | } 31 | } 32 | 33 | 34 | //Handles a single session of a quiz 35 | module.exports = class QuizSession 36 | { 37 | constructor(channel) 38 | { 39 | this.skipVotes = 0; 40 | this.channel = channel; //The channel the quiz is currently in 41 | this.question = this.getQuestion(); 42 | this.users = new Map(); 43 | this.printQuestion(); 44 | Bot.sendMessage(this.channel, "Quiz has begun!"); 45 | } 46 | 47 | //Sends a message to the channel where the quiz is active 48 | sendMessage(message) 49 | { 50 | message.setColor(embedColour); 51 | Bot.sendMessage(this.channel, message); 52 | } 53 | 54 | //Initiates the next question for the quiz 55 | nextQuestion() 56 | { 57 | this.users.forEach(function(user, key, map) { 58 | user.hasSkipped = false; 59 | }.bind(this)); 60 | this.skipVotes = 0; 61 | this.question = this.getQuestion(); 62 | this.printQuestion("New Question"); 63 | } 64 | 65 | //Adds a member to this quiz session if they have not yet been added 66 | tryAddUser(member) 67 | { 68 | if(!this.users.has(member) && this.users.size < MAX_USERS) { 69 | this.users.set(member, new QuizUser()); 70 | } 71 | } 72 | 73 | //Adds a skip to the vote, if it exceeds a certain number then the question is skipped, and a new question is preseneted 74 | addSkip(member) 75 | { 76 | this.tryAddUser(member); 77 | //A user cannot skip twice 78 | if(this.users.get(member).hasSkipped) { 79 | this.sendMessage(new Discord.RichEmbed() 80 | .setTitle(`You cannot vote to skip twice, ${member.displayName}, sorry.`)) 81 | } 82 | else { 83 | this.users.get(member).hasSkipped = true 84 | this.skipVotes++; 85 | let skipsNeeded = Math.floor(this.users.size / 2); 86 | //Check if the current question is able to be skipped 87 | if (this.skipVotes >= skipsNeeded) { 88 | this.sendMessage(new Discord.RichEmbed() 89 | .setTitle("Question Skipped!") 90 | .addField("Question", this.question.question) 91 | .addField("Answer", this.question.answer)); 92 | this.nextQuestion(); 93 | } 94 | else { 95 | this.sendMessage(new Discord.RichEmbed() 96 | .setTitle("Skip Vote Registered") 97 | .addField("Current Votes", this.skipVotes.toString(), true) 98 | .addField("Votes Needed", skipsNeeded.toString(), true)); 99 | } 100 | } 101 | } 102 | 103 | //Gets a random question from the main JSON file 104 | getQuestion() 105 | { 106 | let inFile = JSONFile.readFileSync(questionsFile); 107 | let qIndex = Util.getRandomInt(0, inFile.questions.length); 108 | let question = new Question(inFile.questions[qIndex]); 109 | return question; 110 | } 111 | 112 | //Displays the question 113 | printQuestion(title) 114 | { 115 | this.sendMessage(new Discord.RichEmbed() 116 | .setTitle(title) 117 | .addField("**Category**", this.question.category) 118 | .addField("**Question**", this.question.question) 119 | .addField("**Question Author**", `<@${this.question.author}>`)); 120 | } 121 | 122 | //Ends the quiz 123 | endQuiz() 124 | { 125 | this.sendMessage(new Discord.RichEmbed() 126 | .setTitle("Quiz has ended!")); 127 | this.outputScores("Final"); 128 | } 129 | 130 | //Adds a point to a user, usually because they have answered a question correctly 131 | addPointTo(member) 132 | { 133 | this.tryAddUser(member); 134 | let quizMember = this.users.get(member); 135 | quizMember.score++; 136 | } 137 | 138 | //Sends a message saying the current scores of everyone 139 | outputScores(title) 140 | { 141 | let output = new Discord.RichEmbed() 142 | .setTitle(title + " Scores"); 143 | 144 | this.users.forEach(function(person, user, map) { 145 | //output.addField(`<@${user.id}>`, score.toString(), true); 146 | output.addField(user.displayName, person.score.toString(), true); 147 | }); 148 | 149 | this.sendMessage(output); 150 | } 151 | 152 | //Submits an answer to current question, and if the question is answered correctly, then it gets the next question 153 | submitAnswer(member, answer) 154 | { 155 | if (answer.toLowerCase() == this.question.answer.toLowerCase()) { 156 | this.sendMessage(new Discord.RichEmbed() 157 | .setTitle("Answered sucessfully!") 158 | .addField("Answered By", `<@${member.id}>`)); 159 | this.addPointTo(member); 160 | this.outputScores("Current"); 161 | this.nextQuestion(); 162 | } 163 | } 164 | } -------------------------------------------------------------------------------- /src/deprecated/quiz_command_handler.js: -------------------------------------------------------------------------------- 1 | const CommandHandler = require('./command_handler'); 2 | 3 | module.exports = class PollCommandHandler extends CommandHandler { 4 | constructor() { 5 | super('quiz'); 6 | this.initCommands(); 7 | } 8 | 9 | initCommands() { 10 | super.addCommand( 11 | "begin", 12 | "Begins a quiz session", 13 | ">quiz begin", 14 | () => {} 15 | ); 16 | 17 | super.addCommand( 18 | "add", 19 | "Adds a new question to the quiz database", 20 | ">quiz add Maths \"What is 5 + 5?\" \"10\"", 21 | 22 | ) 23 | } 24 | } 25 | 26 | function addQuizQuestion(message, args) { 27 | const channel = message.channel; 28 | const author = message.author; 29 | } -------------------------------------------------------------------------------- /src/deprecated/words_command_handler.js: -------------------------------------------------------------------------------- 1 | const CommandHandler = require('./command_handler'); 2 | 3 | const Discord = require('discord.js'); 4 | const fs = require('fs'); 5 | 6 | 7 | module.exports = class WordCommandHandler extends CommandHandler { 8 | constructor() { 9 | super('words'); 10 | this.initCommands(); 11 | } 12 | 13 | initCommands() { 14 | super.addCommand( 15 | "top", 16 | "Gets the top n words, up to top 10", 17 | ">words top 3", 18 | topNWords 19 | ); 20 | 21 | super.addCommand( 22 | "count", 23 | "Counts how many times a word has been said", 24 | ">words count hello", 25 | countWords 26 | ); 27 | } 28 | } 29 | 30 | function doAction(callback) { 31 | fs.readFile("data/words_db.json", 'utf8', (err, data) => { 32 | if (err) { 33 | console.log("Error reading file: " + err); 34 | return; 35 | } 36 | const words = JSON.parse(data); 37 | callback(words); 38 | }); 39 | } 40 | 41 | function topNWords(message, args, client) { 42 | if (args.length < 1) { 43 | return; 44 | } 45 | doAction(words => { 46 | const n = parseInt(args[0]); 47 | if (n != NaN) { 48 | const topWords = words.slice(0, Math.min(Math.min(words.length, n), 10)); 49 | const output = new Discord.RichEmbed() 50 | .setTitle(`Top ${n} Words`); 51 | for (const idx in topWords) { 52 | output.addField( 53 | `Rank ${(parseInt(idx) + 1)}`, 54 | `***${topWords[idx].w}***, said ***${topWords[idx].c}*** times` 55 | ); 56 | } 57 | message.channel.send(output); 58 | } 59 | 60 | }); 61 | } 62 | 63 | function countWords(message, args, client) { 64 | if (args.length < 1) { 65 | return; 66 | } 67 | console.log("Seratrching"); 68 | doAction(words => { 69 | const search = args[0]; 70 | for (const word of words) { 71 | if (word.w == search) { 72 | console.log("Found"); 73 | message.channel.send(`${search} has been said ${word.c} times.`); 74 | return; 75 | } 76 | } 77 | message.channel.send(`${search} has never been said`); 78 | }); 79 | } 80 | -------------------------------------------------------------------------------- /src/events/member_join_handler.js: -------------------------------------------------------------------------------- 1 | const Config = require("../../data/config.json"); 2 | const Discord = require('discord.js') 3 | const dateFormat = require('dateformat'); 4 | 5 | //TODO 6 | module.exports = { 7 | handleJoin : (member, client) => { 8 | checkAccountAge(member, client); 9 | /* 10 | //Give new member role 11 | const newMemberRole = member.guild.roles.find('name', Config.newMemberRole); 12 | member.addRole(newMemberRole); 13 | 14 | const channelName = Config.welcomeChannel; 15 | const channel = client.channels.find("name", channelName); 16 | 17 | //Welcome them 18 | channel.send( 19 | `Welcome to Hopson Community server, <@${member.user.id}>! Take a moment to look at the <#293460068483989504>. 20 | Also, please introduce yourself in <#463866762786635777>, and I will give you access to the rest of the server.\n 21 | Enjoy! :)` 22 | ); 23 | */ 24 | } 25 | } 26 | 27 | function checkAccountAge(member, client) { 28 | const channelName = Config.memberJoinChannel; 29 | const channel = client.channels.find("name", channelName); 30 | 31 | const join = dateFormat(member.joinedAt, "dddd, mmmm dS, yyyy, h:MM:ss TT"); 32 | const creation = dateFormat(member.user.createdAt, "dddd, mmmm dS, yyyy, h:MM:ss TT"); 33 | const diff = getTimeDifference(member.joinedAt, member.user.createdAt); 34 | 35 | const embed = new Discord.RichEmbed() 36 | .setTitle("User Join") 37 | .addField("**Name**", `<@${member.user.id}>`) 38 | .addField("**Account Create Data**", creation) 39 | .addField("**Join Date**", join) 40 | .addField("**Time Between Create and Join (est)**", diff.regularDiff) 41 | .addField("**Milliseconds Difference**", diff.unixTimeDiff); 42 | 43 | if (diff.notify) { 44 | embed.setColor(16711680); 45 | } else { 46 | embed.setColor(65280); 47 | } 48 | 49 | channel.send(embed); 50 | } 51 | 52 | 53 | function getTimeDifference(join, create) { 54 | const diff = join - create; 55 | const diffDate = new Date(diff); 56 | const originDate = new Date(0); 57 | 58 | const yearDiff = diffDate.getFullYear() - originDate.getFullYear(); 59 | const monthDiff = diffDate.getMonth(); 60 | const dayDiff = diffDate.getDate() - 1; // [1, 31] to [0, 30] 61 | const hourDiff = diffDate.getHours(); 62 | const minDiff = diffDate.getMinutes(); 63 | const secDiff = diffDate.getSeconds(); 64 | 65 | return { 66 | regularDiff: `${yearDiff} years, ${monthDiff} months, ${dayDiff} days, ${hourDiff} hours, ${minDiff} minutes, ${secDiff} seconds`, 67 | unixTimeDiff: `${diff}`, 68 | notify: diff <= 172800000 // ms in 2 days 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/events/member_leave_handler.js: -------------------------------------------------------------------------------- 1 | const Config = require("../../data/config.json"); 2 | 3 | /** 4 | * Event handler for leaving and that 5 | */ 6 | module.exports = { 7 | handleLeave(member, client) { 8 | const channelName = Config.leaveChannel; 9 | const channel = client.channels.find("name", channelName); 10 | const user = member.displayName; 11 | const id = member.user.id; 12 | 13 | //cya 14 | channel.send(`"${user}" has left the server. ID: <@${id}> - ${id}`); 15 | } 16 | } -------------------------------------------------------------------------------- /src/events/member_update_event.js: -------------------------------------------------------------------------------- 1 | const Discord = require('discord.js'); 2 | 3 | module.exports = { 4 | handleUserUpdate: function (client, oldUser, newUser) 5 | { 6 | let botlog = client.channels.get("362124431801450526"); 7 | let time = (new Date()).toLocaleString('en-GB'); 8 | 9 | if (oldUser.username != newUser.username){ 10 | let embed = new Discord.RichEmbed() 11 | .setDescription(`Username Change at ${time}`) 12 | .setColor(9699539) 13 | .addField("Old Username", oldUser.username) 14 | .addField("New Username", newUser.username); 15 | 16 | botlog.send(embed); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/events/message_mod_handler.js: -------------------------------------------------------------------------------- 1 | const Config = require('../../data/config.json'); 2 | const Discord = require('discord.js'); 3 | 4 | module.exports = { 5 | handleMessageDelete: (client, message) => { 6 | if (isChannelBlacklisted(message.channel)) { 7 | return; 8 | } 9 | 10 | if (message.channel.name === Config.newMemberChannel) { 11 | let introduceRole = message.member.guild.roles.find('name', Config.introRole); 12 | message.member.removeRole(introduceRole); 13 | } 14 | 15 | const botLog = getBotLogChannel(client); 16 | const time = (new Date()).toLocaleString('en-GB'); 17 | const content = sliceLongMessage(message.content); 18 | if (content.length == 0) { 19 | return; 20 | } 21 | botLog.send(new Discord.RichEmbed() 22 | .setDescription(`${message.author} in ${message.channel} at ${time}`) 23 | .setColor(16711680) 24 | .addField("Deleted Message", content)); 25 | 26 | }, 27 | 28 | handleMessageUpdate: (client, oldMessage, newMessage) => { 29 | if (isChannelBlacklisted(oldMessage.channel)) return; 30 | if(oldMessage.content === newMessage.content) return; 31 | 32 | const botLog = getBotLogChannel(client); 33 | const time = (new Date()).toLocaleString('en-GB'); 34 | const oldContent = sliceLongMessage(oldMessage.content); 35 | const newContent = sliceLongMessage(newMessage.content); 36 | 37 | const embed = new Discord.RichEmbed() 38 | .setDescription(`${oldMessage.author} in ${oldMessage.channel} at ${time}`) 39 | .setColor(16737280) 40 | .addField("Old Message", oldContent) 41 | .addField("New Message", newContent); 42 | 43 | botLog.send(embed); 44 | } 45 | } 46 | 47 | function sliceLongMessage(message) { 48 | if (message.length > 1000) message = message.slice(0,1000) + " ..."; 49 | return message 50 | } 51 | 52 | function isChannelBlacklisted(channel) { 53 | for (var blacklistedChannel of Config.logBlacklistedChannels) { 54 | if (channel.id == blacklistedChannel) { 55 | return true; 56 | } 57 | } 58 | return false; 59 | } 60 | 61 | function getBotLogChannel(client) { 62 | return client.channels.get("362124431801450526"); 63 | } 64 | -------------------------------------------------------------------------------- /src/events/message_sent_handler.js: -------------------------------------------------------------------------------- 1 | const PollCommandHandler = require('../commands/poll_command_handler'); 2 | const RoleCommandHandler = require('../commands/role_command_handler'); 3 | const DefaultCommandHandler = require('../commands/default_command_handler'); 4 | const RefCommandHandler = require('../commands/ref_command_handler'); 5 | const Config = require('../../data/config.json'); 6 | const Discord = require('discord.js') 7 | 8 | /** 9 | * Class to handle messages sent by the user 10 | */ 11 | module.exports = class MessageSentHandler { 12 | /** 13 | * Creates the command handlers and constructs the handler 14 | */ 15 | constructor () { 16 | this.defaultCommandHandler = new DefaultCommandHandler(); 17 | this.commandHandlers = [ 18 | new PollCommandHandler(), 19 | new RoleCommandHandler(), 20 | new RefCommandHandler(), 21 | ] 22 | } 23 | /** 24 | * Entry point for handling messages 25 | * @param {Discord.TextMessage} message The raw message sent by a user 26 | * @param {Discord client} client The Discord client the message was sent 27 | */ 28 | handleMessageSent(message, client) { 29 | logMessageInfo(message); 30 | this.handleMessageSentWithoutLog(message, client); 31 | } 32 | 33 | handleMessageSentWithoutLog(message, client) { 34 | if ((message.channel.type !== "text") || 35 | (message.author.bot)) { 36 | return; 37 | } 38 | 39 | let nMentions = message.mentions.members.size; 40 | nMentions += message.mentions.roles.size; 41 | 42 | if (nMentions > Config.maxMentions) { 43 | message.member.kick("Mention spam").catch(err => console.log(err)); 44 | console.log(`Kicked ${message.member.displayName} for ${nMentions} mentions in a single message`); 45 | return; 46 | } 47 | 48 | if (message.channel.name === Config.newMemberChannel) { 49 | let newMemberRole = message.member.guild.roles.find('name', Config.newMemberRole); 50 | let introduceRole = message.member.guild.roles.find('name', Config.introRole); 51 | message.member.removeRole(newMemberRole); 52 | //message.member.addRole(introduceRole); 53 | return; 54 | } 55 | 56 | if (message.content.startsWith('>')) { 57 | this.handleCommand(message, client); 58 | return; 59 | } 60 | } 61 | 62 | /* 63 | //If a quiz is currently active, then it may be someone trying to answer it 64 | if (this.quiz.quizActive) { 65 | console.log(content); 66 | this.quiz.submitAnswer(message, content.toLowerCase()); 67 | } 68 | */ 69 | 70 | /** 71 | * Handles a command message 72 | * @param {MessageInfo} msgInfo Info about message sent by user 73 | */ 74 | handleCommand(message, client) { 75 | const content = message.content 76 | .slice(1) //Remove the '>' if it is there 77 | .split(' ') 78 | .map((s) => { 79 | return s.toLowerCase() 80 | }); 81 | const commandCategory = content[0]; 82 | let args = content.slice(1); 83 | 84 | for (const handler of this.commandHandlers) { 85 | if (handler.isCommand(commandCategory)) { 86 | handler.handleCommand(message, args, client); 87 | return; 88 | } 89 | } 90 | if(commandCategory === "help") { 91 | this._sendHelpList(message.channel); 92 | } 93 | else { 94 | args.unshift(commandCategory); 95 | this.defaultCommandHandler.handleCommand(message, args, client); 96 | } 97 | } 98 | 99 | _sendHelpList(channel) { 100 | let defaultCommands = this.defaultCommandHandler.getCommands(); 101 | let output = new Discord.RichEmbed() 102 | .setTitle("HopsonBot Command List") 103 | .setColor("#09f228");; 104 | 105 | defaultCommands.forEach((command, commandName, _) => { 106 | if (commandName === "help") return; 107 | output.addField(`**__${commandName}__**`, 108 | `Description: ${command.description}\nExample: *${command.example}*`); 109 | }); 110 | 111 | for (let handler of this.commandHandlers) { 112 | let cat = handler.commandCategory; 113 | output.addField(`__**Command Category**: *${cat}*__`, 114 | `See commands: *>${cat} help*`); 115 | } 116 | channel.send(output); 117 | } 118 | } 119 | 120 | function logMessageInfo(message) { 121 | const ch = message.channel.name; 122 | const user = message.member ? message.member.displayName : "No name"; 123 | const msg = message.content; 124 | 125 | console.log("============") 126 | console.log(`Message Sent\nChannel: ${ch}\nUser: ${user}\nContent: ${msg}\n`); 127 | console.log("============\n") 128 | } 129 | -------------------------------------------------------------------------------- /src/hopson_bot.js: -------------------------------------------------------------------------------- 1 | const MessageSentHandler = require('./events/message_sent_handler'); 2 | const MessageModifyHandler = require('./events/message_mod_handler'); 3 | const MemeberJoinHandler = require('./events/member_join_handler'); 4 | const MemberUpdateHandler = require('./events/member_update_event'); 5 | const MemberLeaveHandler = require('./events/member_leave_handler'); 6 | 7 | module.exports = class HopsonBot { 8 | constructor(client) { 9 | this.client = client; 10 | this.messageSentHandler = new MessageSentHandler(); 11 | } 12 | 13 | runBot() { 14 | //Event for when the bot starts 15 | this.client.on("ready", () => { 16 | console.log("Client has logged in to server"); 17 | this.client.user.setPresence({game: {name : "Type >help"}}) 18 | .then(console.log) 19 | .catch(console.error); 20 | }); 21 | 22 | //Event for when bot is dissconnected 23 | this.client.on("disconnect", event => { 24 | console.log(`Client has closed with status code ${event.code} and reason ${event.reason}`) 25 | }); 26 | 27 | //Event for messages sent to any of the discord channels 28 | this.client.on("message", message => { 29 | this.messageSentHandler.handleMessageSent(message, this.client); 30 | }); 31 | 32 | //Event for a message delete 33 | this.client.on("messageDelete", message => { 34 | console.log("Message deleted"); 35 | MessageModifyHandler.handleMessageDelete( 36 | this.client, 37 | message 38 | ); 39 | }); 40 | 41 | //Event for a message edit 42 | this.client.on("messageUpdate", (oldMessage, newMessage) => { 43 | MessageModifyHandler.handleMessageUpdate( 44 | this.client, 45 | oldMessage, 46 | newMessage 47 | ); 48 | }); 49 | 50 | //Event for people joining the server 51 | this.client.on("guildMemberAdd", member => { 52 | MemeberJoinHandler.handleJoin(member, this.client); 53 | }); 54 | 55 | this.client.on("guildMemberRemove", member => { 56 | MemberLeaveHandler.handleLeave(member, this.client); 57 | }) 58 | 59 | //Event for a user update (eg changing their usernem) 60 | this.client.on("userUpdate", (oldUser, newUser) => { 61 | MemberUpdateHandler.handleUserUpdate(this.client, oldUser, newUser); 62 | }); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | const Discord = require('discord.js'); 2 | const Config = require('../data/config'); 3 | const HopsonBot = require('./hopson_bot') 4 | 5 | const client = new Discord.Client(); 6 | 7 | //Entry point of the bot itself 8 | new HopsonBot(client).runBot(); 9 | 10 | client.login(Config.getToken()); -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | getRandomInt : function(min, max) 4 | { 5 | return Math.floor((Math.random() * (max - min) + min)); 6 | }, 7 | 8 | extractStringFromArray : function(args) 9 | { 10 | if(!args[0].startsWith("\"")) { 11 | return [false, "", null]; 12 | } 13 | let str = args[0].slice(1); //remove the starting " from the string 14 | args = args.slice(1); //remove first word as it has already been added 15 | let endFound = false; 16 | //Iterate through the array passed in, and search for the end of a string 17 | var wordCount = 0; 18 | for (var word of args) { 19 | if (word.endsWith("\"")) { 20 | endFound = true; 21 | word = word.slice(0, -1); 22 | } 23 | str += " " + word; 24 | wordCount++; 25 | if (endFound) break; 26 | 27 | } 28 | if(!endFound && args.length > 0) { 29 | return [false, "", null]; 30 | } 31 | if (args.length == 0) { 32 | str = str.slice(0, -1); //For single word answers, this will remove the trailing " at the end of a word 33 | } 34 | return [true, str, args.slice(wordCount)]; 35 | } 36 | } -------------------------------------------------------------------------------- /test/commands/default_command_handler.test.js: -------------------------------------------------------------------------------- 1 | const MessageHandler = require('../../src/events/message_sent_handler'); 2 | const MockMessage = require('../discord_mocks/mock_message') 3 | const MockChannel = require('../discord_mocks/mock_channel') 4 | 5 | QUnit.test( 6 | "Default command handler tests", 7 | assert => { 8 | const messageHandler = new MessageHandler(); 9 | const channel = new MockChannel(); 10 | 11 | //Testing command source 12 | messageHandler.handleMessageSentWithoutLog(new MockMessage(">source", channel), {}); 13 | 14 | assert.deepEqual( 15 | channel.lastMessage().content, 16 | 'https://github.com/HopsonCommunity/HopsonBot', 17 | "The source command should return the GitHub link of the bot." 18 | ); 19 | } 20 | ) 21 | -------------------------------------------------------------------------------- /test/commands/poll_command_handler.test.js: -------------------------------------------------------------------------------- 1 | const MessageHandler = require('../../src/events/message_sent_handler'); 2 | const MockMessage = require('../discord_mocks/mock_message') 3 | const MockChannel = require('../discord_mocks/mock_channel') 4 | 5 | const POLL_STATION_NAME = "*Hopson Polling Station*"; 6 | 7 | QUnit.test( 8 | "Poll yesno command tests", 9 | assert => { 10 | const messageHandler = new MessageHandler(); 11 | const channel = new MockChannel(); 12 | 13 | //Empty message tests 14 | { 15 | messageHandler.handleMessageSentWithoutLog(new MockMessage(">poll yesno", channel, "Tester"), {}); 16 | 17 | assert.deepEqual( 18 | channel.lastMessage().content.embed.fields[0].value, 19 | "Please add a question.", 20 | "Providing yesno with no question should yield a prompt to add a question" 21 | ); 22 | 23 | assert.deepEqual( 24 | channel.lastMessage().content.embed.fields[0].name, 25 | "*Hopson Polling Station*", 26 | "The embed should be called Hopson Polling Station" 27 | ); 28 | } 29 | 30 | //Non-Empty tests 31 | { 32 | const QUESTION = 'will this test pass?' 33 | messageHandler.handleMessageSentWithoutLog(new MockMessage(`>poll yesno ${QUESTION}`, channel, "Tester"), {}); 34 | 35 | assert.deepEqual( 36 | channel.lastMessage().content.embed.fields[0].value, 37 | `${QUESTION}`, 38 | `Asking ${QUESTION} will prompt that question` 39 | ); 40 | 41 | const done = assert.async(); 42 | setTimeout(_ => { 43 | assert.deepEqual( 44 | channel.lastMessage().reactions, 45 | ['✅', '❌'], 46 | `Asking a quesion will give reactions ✅ and ❌` 47 | ); 48 | done(); 49 | }, 1200); 50 | } 51 | } 52 | ); 53 | 54 | QUnit.test( 55 | "Poll options: Empty args", 56 | assert => { 57 | const messageHandler = new MessageHandler(); 58 | const channel = new MockChannel(); 59 | 60 | messageHandler.handleMessageSentWithoutLog( 61 | new MockMessage( 62 | ">poll options", 63 | channel) 64 | ); 65 | 66 | assert.equal( 67 | channel.lastMessage().content.embed.fields[0].value.startsWith("Unable to poll!"), 68 | true, 69 | "Providing options with no question or options should yield a prompt to add a question and options" 70 | ); 71 | 72 | assert.deepEqual( 73 | channel.lastMessage().content.embed.fields[0].name, 74 | "*Hopson Polling Station*", 75 | "The embed should be called Hopson Polling Station" 76 | ); 77 | } 78 | ); 79 | 80 | QUnit.test( 81 | "Poll options: Incorrect args", 82 | assert => { 83 | const messageHandler = new MessageHandler(); 84 | const channel = new MockChannel(); 85 | 86 | messageHandler.handleMessageSentWithoutLog( 87 | new MockMessage( 88 | ">poll options this is a question", 89 | channel) 90 | ); 91 | assert.equal( 92 | channel.lastMessage().content.embed.fields[0].value.startsWith("Unable to poll!"), 93 | true, 94 | "Providing a question without quotations should error" 95 | ); 96 | 97 | 98 | messageHandler.handleMessageSentWithoutLog( 99 | new MockMessage( 100 | `>poll options "this is a question"`, 101 | channel) 102 | ); 103 | assert.equal( 104 | channel.lastMessage().content.embed.fields[0].value.startsWith("Unable to poll!"), 105 | true, 106 | "Providing a question without any options should error" 107 | ); 108 | 109 | messageHandler.handleMessageSentWithoutLog( 110 | new MockMessage( 111 | `>poll options "this is a question`, 112 | channel) 113 | ); 114 | assert.equal( 115 | channel.lastMessage().content.embed.fields[0].value.startsWith("Unable to poll!"), 116 | true, 117 | "Providing a question with a single quote should error" 118 | ); 119 | 120 | 121 | messageHandler.handleMessageSentWithoutLog( 122 | new MockMessage( 123 | `>poll options "this is a question" a`, 124 | channel) 125 | ); 126 | assert.equal( 127 | channel.lastMessage().content.embed.fields[0].value.startsWith("Unable to poll!"), 128 | true, 129 | "Providing a question with only one option should error" 130 | ); 131 | 132 | 133 | messageHandler.handleMessageSentWithoutLog( 134 | new MockMessage( 135 | `>poll options "this is a question" a b c d e f g h i j k l m`, 136 | channel) 137 | ); 138 | assert.equal( 139 | channel.lastMessage().content.embed.fields[0].value.startsWith("Unable to poll!"), 140 | true, 141 | "Providing a question with more than 9 options should error" 142 | ); 143 | } 144 | ) 145 | 146 | QUnit.test( 147 | "Poll options: Correct args", 148 | assert => { 149 | const messageHandler = new MessageHandler(); 150 | const channel = new MockChannel(); 151 | 152 | 153 | messageHandler.handleMessageSentWithoutLog( 154 | new MockMessage( 155 | '>poll options "question asking" a b c d e', 156 | channel) 157 | ); 158 | 159 | const msg = channel.lastMessage(); 160 | 161 | assert.deepEqual( 162 | msg.content.embed.fields[0].value.startsWith("question asking"), 163 | true, 164 | "The polling station should show the question being asked" 165 | ); 166 | 167 | const done = assert.async(); 168 | setTimeout(_ => { 169 | assert.deepEqual( 170 | msg.reactions, 171 | ['1⃣', '2⃣', '3⃣', '4⃣', '5⃣'], 172 | "Giving 5 options should provide 5 reactions" 173 | ); 174 | done(); 175 | }, 5000); 176 | } 177 | ); -------------------------------------------------------------------------------- /test/commands/role_command_handler.test.js: -------------------------------------------------------------------------------- 1 | const djs = require('discord.js') 2 | const MessageHandler = require('../../src/events/message_sent_handler'); 3 | const MockMessage = require('../discord_mocks/mock_message') 4 | const MockChannel = require('../discord_mocks/mock_channel') 5 | const MockMember = require('../discord_mocks/mock_guild_member') 6 | const MockGuild = require('../discord_mocks/mock_guild') 7 | const MockRoles = require('../discord_mocks/mock_role'); 8 | const MockUser = require('../discord_mocks/mock_user') 9 | 10 | const roles = new djs.Collection(); 11 | roles.set(0, new MockRoles("C++")); 12 | roles.set(0, new MockRoles("Java")); 13 | roles.set(0, new MockRoles("Linux")); 14 | 15 | const guild = new MockGuild(roles); 16 | const user = new MockUser(); 17 | 18 | QUnit.test( 19 | "Role add commands", 20 | assert => { 21 | const messageHandler = new MessageHandler(); 22 | const channel = new MockChannel(); 23 | const member = new MockMember(); 24 | 25 | messageHandler.handleMessageSentWithoutLog( 26 | new MockMessage( 27 | ">role add C++ Java", 28 | channel, 29 | member, 30 | user, 31 | guild)); 32 | 33 | 34 | assert.equal(true, true); 35 | } 36 | ); -------------------------------------------------------------------------------- /test/discord_mocks/mock_channel.js: -------------------------------------------------------------------------------- 1 | const MockMessage = require('./mock_message'); 2 | 3 | module.exports = class { 4 | /** 5 | * Creates a "mock" discord channel 6 | * @param {String} name The name of the channel 7 | */ 8 | constructor(name = "Default") { 9 | this.messages = []; 10 | this.name = name; 11 | 12 | this.type = "text"; 13 | } 14 | 15 | /** 16 | * Sends a "mock message" to this channel 17 | * @param {String} The message to send to the channel 18 | */ 19 | send(message) { 20 | const msg = new MockMessage(message, {}); 21 | this.messages.push(msg); 22 | return { 23 | then: f => f(msg) 24 | } 25 | } 26 | 27 | /** 28 | * Gets the last message sent 29 | */ 30 | lastMessage() { 31 | return this.messages[this.messages.length - 1]; 32 | } 33 | } -------------------------------------------------------------------------------- /test/discord_mocks/mock_guild.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = class { 3 | /** 4 | * 5 | * @param {Collection} roles List of roles the server has 6 | */ 7 | constructor(roles) { 8 | this.roles = roles 9 | } 10 | } -------------------------------------------------------------------------------- /test/discord_mocks/mock_guild_member.js: -------------------------------------------------------------------------------- 1 | module.exports = class { 2 | /** 3 | * Creates a guild member 4 | * @param {MockUser} user The user assosiated with the member 5 | * @param {Date} joinDate The date the member joined 6 | * @param {MockGuild} guild The guild the user is part of 7 | */ 8 | constructor(user, joinDate, guild) { 9 | this.roles = []; 10 | this.joinedAt = joinDate; 11 | this.guild = guild; 12 | } 13 | 14 | /** 15 | * Adds a mock role to the user 16 | * @param {MockRole} role The role to add to the user 17 | */ 18 | addRole(role) { 19 | this.roles.push(role); 20 | } 21 | 22 | /** 23 | * Adds a mock role to the user 24 | * @param {MockRole} role The role to add to the user 25 | */ 26 | removeRole(role) { 27 | const index = this.roles.indexOf(role); 28 | if (index !== -1) this.roles.splice(index, 1); 29 | } 30 | } -------------------------------------------------------------------------------- /test/discord_mocks/mock_message.js: -------------------------------------------------------------------------------- 1 | const MockGuildMember = require('./mock_guild_member') 2 | const MockUser = require('./mock_user') 3 | const MockGuild = require('./mock_guild') 4 | module.exports = class { 5 | /** 6 | * Creates a mock message 7 | * @param {String} content The content of a message 8 | * @param {MockChannel} channel The channel to send the message to 9 | * @param {MockMember} member The member who sent the message 10 | * @param {MockUser} author The user who sent the message 11 | */ 12 | constructor(content, channel, member, author = new MockUser(), guild = new MockGuild()) { 13 | this.content = content; 14 | this.author = author; 15 | this.member = member; 16 | this.guild = guild; 17 | this.channel = channel; 18 | this.reactions = []; 19 | } 20 | 21 | /** 22 | * Reacts to the message with an emoji 23 | * @param {Char} reaction The emoji to react with 24 | */ 25 | react(reaction) { 26 | this.reactions.push(reaction); 27 | } 28 | } -------------------------------------------------------------------------------- /test/discord_mocks/mock_role.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = class { 3 | constructor(name) { 4 | this.name = name; 5 | } 6 | } -------------------------------------------------------------------------------- /test/discord_mocks/mock_user.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = class { 3 | 4 | } -------------------------------------------------------------------------------- /test/events/message_delete.test.js: -------------------------------------------------------------------------------- 1 | const MessageModHandle = require("../../src/events/message_mod_handler"); 2 | 3 | -------------------------------------------------------------------------------- /test/test_start.js: -------------------------------------------------------------------------------- 1 | 2 | //Command handler tests 3 | require("./commands/default_command_handler.test"); 4 | require("./commands/poll_command_handler.test"); 5 | require("./commands/role_command_handler.test") 6 | 7 | //Event handler test 8 | require("./events/message_delete.test"); --------------------------------------------------------------------------------