├── .github ├── FUNDING.yml ├── CODEOWNERS └── workflows │ ├── node-ci.yml │ └── code-ql.yml ├── .gitignore ├── .prettierrc ├── CODE_OF_CONDUCT.md ├── src ├── interfaces │ ├── messageInt.ts │ ├── BotInt.ts │ ├── CommandInt.ts │ ├── rocketChat.ts │ └── apiInt.ts ├── helpers │ ├── botLogging.ts │ ├── sendToLog.ts │ ├── isModerator.ts │ └── getModerators.ts ├── commands │ ├── ping.ts │ ├── _CommandList.ts │ ├── quote.ts │ ├── algo.ts │ ├── coc.ts │ ├── eightball.ts │ ├── resources.ts │ ├── rescind.ts │ ├── help.ts │ ├── close.ts │ ├── add.ts │ ├── modHelp.ts │ ├── _CommandHandler.ts │ ├── private.ts │ ├── warn.ts │ └── kick.ts ├── index.ts └── assets │ ├── motivational-quotes.ts │ └── algorithm-structure.ts ├── CONTRIBUTING.md ├── renovate.json ├── tsconfig.json ├── README.md ├── sample.env ├── .eslintrc.json ├── package.json ├── LICENSE.md └── docs ├── commands.md └── creating-command.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: freecodecamp 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /prod/ 3 | .env -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @freecodecamp/dev-team 2 | 3 | /package.json 4 | /package-lock.json 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "endOfLine": "lf", 3 | "useTabs": false, 4 | "singleQuote": false 5 | } 6 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | > Our Code of Conduct is available here: 2 | -------------------------------------------------------------------------------- /src/interfaces/messageInt.ts: -------------------------------------------------------------------------------- 1 | import { IMessage } from "@rocket.chat/sdk/dist/config/messageInterfaces"; 2 | 3 | export interface MessageInt extends IMessage { 4 | replies?: string[]; 5 | } 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Our contributing docs are available here: . 2 | 3 | Looking to edit these docs? Read [this document](https://contribute.freecodecamp.org/#/how-to-work-on-the-docs-theme) first. 4 | -------------------------------------------------------------------------------- /src/interfaces/BotInt.ts: -------------------------------------------------------------------------------- 1 | export interface BotInt { 2 | botId: string; 3 | botName: string; 4 | hostPath: string; 5 | modLogChannel: string; 6 | botLogChannel: string; 7 | version: string; 8 | prefix: string; 9 | modRoles: string[]; 10 | botRateLimit: number; 11 | lastCommandCalled: number; 12 | } 13 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base"], 3 | "prCreation": "not-pending", 4 | "labels": ["renovate"], 5 | "packageRules": [ 6 | { 7 | "matchUpdateTypes": ["minor", "patch", "pin", "digest"], 8 | "matchCurrentVersion": "!/^0/", 9 | "automerge": true 10 | } 11 | ], 12 | "rangeStrategy": "pin" 13 | } 14 | -------------------------------------------------------------------------------- /src/helpers/botLogging.ts: -------------------------------------------------------------------------------- 1 | import { driver } from "@rocket.chat/sdk"; 2 | import { BotInt } from "../interfaces/BotInt"; 3 | 4 | export const logBotMessage = async ( 5 | logText: string, 6 | BOT: BotInt 7 | ): Promise => { 8 | if (!BOT.botLogChannel) { 9 | return; 10 | } 11 | 12 | await driver.sendToRoom(logText, BOT.botLogChannel); 13 | }; 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES5", 4 | "module": "CommonJS", 5 | "rootDir": "./", 6 | "outDir": "./prod", 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "skipLibCheck": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "resolveJsonModule": true, 12 | "noImplicitAny": false, 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RocketChat Bot 2 | 3 | This is a test instance for developing a chat bot with the Rocket.Chat SDK. 4 | 5 | ## About 6 | [RocketChat](https://github.com/RocketChat/Rocket.Chat.GitHub.Bot) is a customizable open source communications platform for organizations with high standards of data protection. It enables real-time conversations between colleagues, with other companies or with your customers, across devices on web, desktop or mobile. 7 | 8 | 9 | -------------------------------------------------------------------------------- /sample.env: -------------------------------------------------------------------------------- 1 | ROCKETCHAT_URL="URL to your chat server" 2 | ROCKETCHAT_USER="Username for bot user" 3 | ROCKETCHAT_PASSWORD="Password for bot user" 4 | ROCKETCHAT_USE_SSL="Boolean for should use SSL or not" 5 | BOTNAME="Bot's name" 6 | ROLE_LIST="Comma separated list of moderator roles" 7 | MOD_LOG_CHANNEL="Log channel for moderation activity" 8 | BOT_LOG_CHANNEL="Log channel for bot status/issues" 9 | PREFIX="The string prefix to trigger bot responses" 10 | ROCKETCHAT_ROOM="Comma separated list of rooms to join on load." 11 | BOT_RATE_LIMIT=10 -------------------------------------------------------------------------------- /src/helpers/sendToLog.ts: -------------------------------------------------------------------------------- 1 | import { driver } from "@rocket.chat/sdk"; 2 | import { BotInt } from "../interfaces/BotInt"; 3 | 4 | /** 5 | * Sends the given message to the logChannel set 6 | * in the .env 7 | * @param {string} message Text to send 8 | * @param {BotInt} BOT Bot configuration object 9 | * @returns {Promise} nothing 10 | */ 11 | export const sendToLog = async ( 12 | message: string, 13 | BOT: BotInt 14 | ): Promise => { 15 | const logChannel = BOT.modLogChannel; 16 | 17 | if (!logChannel) { 18 | return; 19 | } 20 | 21 | await driver.sendToRoom(message, logChannel); 22 | }; 23 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es2020": true, 4 | "node": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/recommended", 9 | "plugin:prettier/recommended" 10 | ], 11 | "parser": "@typescript-eslint/parser", 12 | "parserOptions": { 13 | "ecmaVersion": 11, 14 | "sourceType": "module" 15 | }, 16 | "plugins": ["@typescript-eslint"], 17 | "rules": { 18 | "linebreak-style": ["error", "unix"], 19 | "quotes": ["error", "double", { "allowTemplateLiterals": true }], 20 | "semi": ["error", "always"], 21 | "prefer-const": "error", 22 | "eqeqeq": ["error", "always"], 23 | "curly": ["error"] 24 | } 25 | } -------------------------------------------------------------------------------- /src/interfaces/CommandInt.ts: -------------------------------------------------------------------------------- 1 | import { IMessage } from "@rocket.chat/sdk/dist/config/messageInterfaces"; 2 | import { BotInt } from "./BotInt"; 3 | 4 | export interface CommandInt { 5 | name: string; 6 | description: string; 7 | parameters: string[]; 8 | usage: string[]; 9 | modCommand: boolean; 10 | /** 11 | * The function that executes when the command `name` is passed in the message. 12 | * @param {IMessage} message The message object to run the command against. 13 | * @param {string} room The name of the room the message was received in. 14 | * @param {BotInt} BOT The bot configuration object. 15 | * @returns {Promise} a void promise 16 | */ 17 | command: (message: IMessage, room: string, BOT: BotInt) => Promise; 18 | } 19 | -------------------------------------------------------------------------------- /.github/workflows/node-ci.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | 10 | jobs: 11 | ci: 12 | name: Lint and Build 13 | runs-on: ubuntu-latest 14 | 15 | strategy: 16 | matrix: 17 | node-version: [16.x] 18 | 19 | steps: 20 | - name: Checkout Source Files 21 | uses: actions/checkout@v3 22 | 23 | - name: Use Node.js v${{ matrix.node-version }} 24 | uses: actions/setup-node@v3 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | 28 | - name: Install Dependencies 29 | run: npm ci 30 | 31 | - name: Lint Source Files 32 | run: npm run lint 33 | 34 | - name: Verify Build 35 | run: npm run build 36 | 37 | - name: Run Tests 38 | run: npm run test -------------------------------------------------------------------------------- /src/helpers/isModerator.ts: -------------------------------------------------------------------------------- 1 | import { api } from "@rocket.chat/sdk"; 2 | import { UserInfoInt } from "../interfaces/apiInt"; 3 | import { BotInt } from "../interfaces/BotInt"; 4 | 5 | /** 6 | * Queries the Rocket.Chat API to determine if a given user 7 | * has at least one role in the moderator roles list set 8 | * through the .env 9 | * @param {string} userId The user ID to check 10 | * @param {BotInt} BOT Bot configuration object 11 | * @returns {Promise} Boolean value for "does user have moderator role" 12 | */ 13 | export const isModerator = async ( 14 | userId: string, 15 | BOT: BotInt 16 | ): Promise => { 17 | const userInfo: UserInfoInt = await api.get("users.info", { userId }); 18 | 19 | const roles = BOT.modRoles; 20 | 21 | for (const role of roles) { 22 | if (userInfo.user.roles.includes(role)) { 23 | return true; 24 | } 25 | } 26 | return false; 27 | }; 28 | -------------------------------------------------------------------------------- /src/commands/ping.ts: -------------------------------------------------------------------------------- 1 | import { driver } from "@rocket.chat/sdk"; 2 | import { logBotMessage } from "../helpers/botLogging"; 3 | import { CommandInt } from "../interfaces/CommandInt"; 4 | 5 | export const ping: CommandInt = { 6 | name: "ping", 7 | description: "Returns a response from the bot with the ping time.", 8 | parameters: [], 9 | usage: ["`{prefix} ping` - will return the bot's current response time."], 10 | modCommand: false, 11 | command: async (_message, room, BOT) => { 12 | try { 13 | const start = Date.now(); 14 | 15 | await driver.sendToRoom(`Pong!`, room); 16 | 17 | await driver.sendToRoom(`Response Time: ${Date.now() - start}ms`, room); 18 | } catch (err) { 19 | await logBotMessage( 20 | `${room} had an error with the \`ping\` command. Check the logs for more info.`, 21 | BOT 22 | ); 23 | console.error(err); 24 | } 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /src/commands/_CommandList.ts: -------------------------------------------------------------------------------- 1 | import { CommandInt } from "../interfaces/CommandInt"; 2 | import { add } from "./add"; 3 | import { algo } from "./algo"; 4 | import { close } from "./close"; 5 | import { coc } from "./coc"; 6 | import { eightball } from "./eightball"; 7 | import { help } from "./help"; 8 | import { kick } from "./kick"; 9 | import { modHelp } from "./modHelp"; 10 | import { ping } from "./ping"; 11 | import { priv } from "./private"; 12 | import { quote } from "./quote"; 13 | import { rescind } from "./rescind"; 14 | import { resources } from "./resources"; 15 | import { warn } from "./warn"; 16 | 17 | /** 18 | * Construct an array of the commands for the handler 19 | * to iterate through 20 | */ 21 | export const CommandList: CommandInt[] = [ 22 | coc, 23 | help, 24 | resources, 25 | warn, 26 | priv, 27 | close, 28 | quote, 29 | eightball, 30 | algo, 31 | kick, 32 | modHelp, 33 | ping, 34 | add, 35 | rescind, 36 | ]; 37 | -------------------------------------------------------------------------------- /.github/workflows/code-ql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | analyse: 11 | name: Analyse 12 | runs-on: ubuntu-latest 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | language: ['javascript'] 17 | node-version: [16.x] 18 | steps: 19 | - name: Checkout repository 20 | uses: actions/checkout@v3 21 | - name: Use Node.js v${{ matrix.node-version }} 22 | uses: actions/setup-node@v3 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | - name: Install dependencies 26 | run: npm ci 27 | - name: Build files 28 | run: npm run build 29 | - name: Setup CodeQL 30 | uses: github/codeql-action/init@v2 31 | with: 32 | languages: ${{ matrix.language }} 33 | - name: Perform Analysis 34 | uses: github/codeql-action/analyze@v2 -------------------------------------------------------------------------------- /src/helpers/getModerators.ts: -------------------------------------------------------------------------------- 1 | import { api } from "@rocket.chat/sdk"; 2 | import { RoleListInt } from "../interfaces/apiInt"; 3 | import { BotInt } from "../interfaces/BotInt"; 4 | 5 | /** 6 | * Queries the Rocket.Chat API to retrieve a list of all 7 | * users who have at least one of the moderator roles 8 | * set in the .env file. 9 | * @param {BotInt} BOT The bot's configuration object. 10 | * @returns {Promise} An array of usernames as strings. 11 | */ 12 | export const getModerators = async (BOT: BotInt): Promise => { 13 | const modUsers: string[] = []; 14 | 15 | const roleList = BOT.modRoles; 16 | for (const role of roleList) { 17 | if (role === "none") { 18 | continue; 19 | } 20 | 21 | const modList: RoleListInt = await api.get("roles.getUsersInRole", { 22 | role, 23 | }); 24 | 25 | for (const mod of modList.users) { 26 | if (!modUsers.includes(mod.username)) { 27 | modUsers.push(mod.username); 28 | } 29 | } 30 | } 31 | return modUsers; 32 | }; 33 | -------------------------------------------------------------------------------- /src/commands/quote.ts: -------------------------------------------------------------------------------- 1 | import { driver } from "@rocket.chat/sdk"; 2 | import { quotes } from "../assets/motivational-quotes"; 3 | import { logBotMessage } from "../helpers/botLogging"; 4 | import { CommandInt } from "../interfaces/CommandInt"; 5 | 6 | export const quote: CommandInt = { 7 | name: "quote", 8 | description: "Returns a quote from the freeCodeCamp repository.", 9 | parameters: [], 10 | usage: [ 11 | "`{prefix} quote` - will return a random quote from freeCodeCamp's curated list of motivational quotes/phrases.", 12 | ], 13 | modCommand: false, 14 | command: async (_message, room, BOT) => { 15 | try { 16 | const randomIndex = Math.floor( 17 | Math.random() * quotes.motivationalQuotes.length 18 | ); 19 | 20 | const { quote, author } = quotes.motivationalQuotes[randomIndex]; 21 | 22 | const response = `"${quote}"\n_--${author}_`; 23 | 24 | await driver.sendToRoom(response, room); 25 | } catch (err) { 26 | await logBotMessage( 27 | `${room} had an error with the \`quote\` command. Check the logs for more info.`, 28 | BOT 29 | ); 30 | console.error(err); 31 | } 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rocketchat-bot", 3 | "version": "2.0.0", 4 | "description": "A test repository for building a rocket chat bot\"", 5 | "main": "./prod/index.ts", 6 | "scripts": { 7 | "prebuild": "rm -rf ./prod", 8 | "build": "tsc", 9 | "lint": "eslint ./src/", 10 | "start": "node -r dotenv/config ./prod/src/index.js", 11 | "test": "echo 'No tests yet. Would you like to contribute some?'" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/nhcarrigan/rocketchat-bot.git" 16 | }, 17 | "keywords": [ 18 | "rocketchat", 19 | "bot", 20 | "chatbot", 21 | "typescript" 22 | ], 23 | "author": "Nicholas Carrigan", 24 | "license": "AGPL-3.0-or-later", 25 | "bugs": { 26 | "url": "https://github.com/nhcarrigan/rocketchat-bot/issues" 27 | }, 28 | "homepage": "https://github.com/nhcarrigan/rocketchat-bot#readme", 29 | "engines": { 30 | "node": "18.16.1", 31 | "npm": "9.7.2" 32 | }, 33 | "dependencies": { 34 | "@rocket.chat/sdk": "0.2.9-2", 35 | "dotenv": "16.3.1" 36 | }, 37 | "devDependencies": { 38 | "@types/node": "18.16.18", 39 | "@typescript-eslint/eslint-plugin": "5.60.1", 40 | "@typescript-eslint/parser": "5.60.1", 41 | "eslint": "8.43.0", 42 | "eslint-config-prettier": "8.8.0", 43 | "eslint-plugin-prettier": "4.2.1", 44 | "prettier": "2.8.8", 45 | "typescript": "5.1.3" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/commands/algo.ts: -------------------------------------------------------------------------------- 1 | import { driver } from "@rocket.chat/sdk"; 2 | import { algorithmSlugs } from "../assets/algorithm-structure"; 3 | import { logBotMessage } from "../helpers/botLogging"; 4 | import { CommandInt } from "../interfaces/CommandInt"; 5 | 6 | export const algo: CommandInt = { 7 | name: "algo", 8 | description: "Returns a random freeCodeCamp algorithm challenge.", 9 | parameters: [], 10 | usage: [ 11 | "`{prefix} algo` - Will return a random freeCodeCamp algorithm challenge.", 12 | ], 13 | modCommand: false, 14 | command: async (_, room, BOT) => { 15 | try { 16 | const randomSuper = Math.floor(Math.random() * algorithmSlugs.length); 17 | const selectedSuper = algorithmSlugs[randomSuper]; 18 | const randomBlock = Math.floor( 19 | Math.random() * selectedSuper.blocks.length 20 | ); 21 | const selectedBlock = selectedSuper.blocks[randomBlock]; 22 | const randomChallenge = Math.floor( 23 | Math.random() * selectedBlock.challenges.length 24 | ); 25 | const challenge = selectedBlock.challenges[randomChallenge]; 26 | 27 | const response = `Here is an algorithm challenge for you!\nhttps://freecodecamp.org/learn/${selectedSuper.superblock}/${selectedBlock.block}/${challenge}\nHappy Coding!`; 28 | await driver.sendToRoom(response, room); 29 | } catch (err) { 30 | await logBotMessage( 31 | `${room} had an error with the \`algo\` command. Check the logs for more info.`, 32 | BOT 33 | ); 34 | console.error(err); 35 | } 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2021, freeCodeCamp.org 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 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. 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 | 3. 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 | -------------------------------------------------------------------------------- /src/commands/coc.ts: -------------------------------------------------------------------------------- 1 | import { driver } from "@rocket.chat/sdk"; 2 | import { logBotMessage } from "../helpers/botLogging"; 3 | import { CommandInt } from "../interfaces/CommandInt"; 4 | 5 | export const coc: CommandInt = { 6 | name: "coc", 7 | description: "Returns information on the Code of Conduct.", 8 | parameters: [], 9 | usage: [ 10 | "`{prefix} coc` - will return a summarised version of the freeCodeCamp Code of Conduct.", 11 | ], 12 | modCommand: false, 13 | command: async (_message, room, BOT) => { 14 | try { 15 | const codeOfConduct = `*freeCodeCamp Code of Conduct* 16 | These are the basic rules for interacting with the FreeCodeCamp community on any platform, including this chat server. You can read the full document on the [FreeCodeCamp article](https://freecodecamp.org/news/code-of-conduct). 17 | *No Harassment*: Harassment includes sexual language and imagery, deliberate intimidation, stalking, name-calling, unwelcome attention, libel, and any malicious hacking or social engineering. freeCodeCamp should be a harassment-free experience for everyone, regardless of gender, gender identity and expression, age, sexual orientation, disability, physical appearance, body size, race, national origin, or religion (or lack thereof). 18 | *No Trolling*: Trolling includes posting inflammatory comments to provoke an emotional response or disrupt discussions. 19 | *No Spamming*: Spamming includes posting off-topic messages to disrupt discussions, promoting a product, soliciting donations, advertising a job / internship / gig, or flooding discussions with files or text. 20 | Thank you for following freeCodeCamp's Code of Conduct. 21 | `; 22 | await driver.sendToRoom(codeOfConduct, room); 23 | } catch (err) { 24 | await logBotMessage( 25 | `${room} had an error with the \`coc\` command. Check the logs for more info.`, 26 | BOT 27 | ); 28 | console.error(err); 29 | } 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /src/commands/eightball.ts: -------------------------------------------------------------------------------- 1 | import { driver } from "@rocket.chat/sdk"; 2 | import { logBotMessage } from "../helpers/botLogging"; 3 | import { CommandInt } from "../interfaces/CommandInt"; 4 | 5 | export const eightball: CommandInt = { 6 | name: "eightball", 7 | description: "Answers your question with a Magic Eightball phrase.", 8 | parameters: ["...question"], 9 | usage: [ 10 | "`{prefix} eightball question` - will return a magic eightball response for the given `question`.", 11 | ], 12 | modCommand: false, 13 | command: async (message, room, BOT) => { 14 | try { 15 | const options = [ 16 | "As I see it, yes.", 17 | "Ask again later.", 18 | "Better not tell you now.", 19 | "Cannot predict now.", 20 | "Concentrate and ask again.", 21 | "Don't count on it.", 22 | "It is certain.", 23 | "It is decidedly so.", 24 | "Most likely.", 25 | "My reply is no.", 26 | "My sources say no.", 27 | "Outlook not so good.", 28 | "Outlook good.", 29 | "Reply hazy, try again.", 30 | "Signs point to yes.", 31 | "Very doubtful.", 32 | "Without a doubt.", 33 | "Yes.", 34 | "Yes - definitely.", 35 | "You may rely on it.", 36 | ]; 37 | const randomIndex = Math.floor(Math.random() * options.length); 38 | const [...question] = message.msg!.split(" ").slice(2); 39 | 40 | if (!question || !question.length) { 41 | await driver.sendToRoom( 42 | "Sorry, but what question did you want me to answer?", 43 | room 44 | ); 45 | return; 46 | } 47 | 48 | const response = `*Question:* ${question.join(" ")}\n*Response:* ${ 49 | options[randomIndex] 50 | }`; 51 | await driver.sendToRoom(response, room); 52 | } catch (err) { 53 | await logBotMessage( 54 | `${room} had an error with the \`eightball\` command. Check the logs for more info.`, 55 | BOT 56 | ); 57 | console.error(err); 58 | } 59 | }, 60 | }; 61 | -------------------------------------------------------------------------------- /docs/commands.md: -------------------------------------------------------------------------------- 1 | # Commands 2 | 3 | The bot currently has the following commands. Commands must be prefixed with... 4 | 5 | ## Public Commands 6 | 7 | These commands are available to all users. 8 | 9 | - `algo`: Returns a random freeCodeCamp algorithm challenge. 10 | - `coc`: Returns a brief version of the freeCodeCamp Code of Conduct, with a link to the full article. 11 | - `eightball <...question>`: Returns a response from a Magic 8-Ball toy for the given `question`. 12 | - `help `: Returns a list of the bot's available commands, or a detailed description of the specific `command`. 13 | - `ping`: Returns a message with the bot's response time in milliseconds. 14 | - `quote`: Returns a random motivational quote from freeCodeCamp's curated list. 15 | - `resources`: Returns a list of helpful freeCodeCamp resources. 16 | 17 | ## Private Commands 18 | 19 | These commands are locked to moderators. 20 | 21 | - `add `: This command will add the `username` to the room it was called in. The command will log the action to the log channel and send a DM to the user to let them know they are in a new room. 22 | - `close`: This command will close (delete) the channel it is used in, as long as that channel was created with the `private` command (channel name starts with "private-"). 23 | - `kick <...reason>`: This command kicks the `username` from the room it was called in. The command will log the action to the log channel, and send a DM to the user informing them of the `reason` for the kick. 24 | - `modHelp `: This command returns a list of the bot's available moderation commands, or a detailed description of the specific `command`. 25 | - `private `: This command will create a new private room with the `username` user and all members of the moderator team. 26 | - `rescind `: This command sends a notice rescinding (undoing) the warning previously sent to `username` and logs the action in the log channel. 27 | - `warn <...reason>`: This command sends a DM warning the `username` user for the given `reason`. The bot will send a log to the provided log channel. 28 | -------------------------------------------------------------------------------- /src/commands/resources.ts: -------------------------------------------------------------------------------- 1 | import { driver } from "@rocket.chat/sdk"; 2 | import { logBotMessage } from "../helpers/botLogging"; 3 | import { CommandInt } from "../interfaces/CommandInt"; 4 | 5 | export const resources: CommandInt = { 6 | name: "resources", 7 | description: "Returns a list of helpful links", 8 | parameters: [], 9 | usage: [ 10 | "`{prefix} resources` - will return links to the freeCodeCamp Code of Conduct, the Moderator Handbook, the Contributing Guidelines, the News Style Guide, and PRs ready for review.", 11 | ], 12 | modCommand: false, 13 | command: async (_message, room, BOT) => { 14 | try { 15 | const coc = 16 | "[freeCodeCamp Code of Conduct](https://freecodecamp.org/news/code-of-conduct) - The set of rules we apply to all of our community platforms."; 17 | const mod = 18 | "[Moderator Handbook](https://contribute.freecodecamp.org/#/flight-manuals/moderator-handbook) - The guidelines our team follows when moderating the community."; 19 | const contrib = 20 | "[Contributing Guidelines](https://contribute.freecodecamp.org/) - Instructions for contributing to the freeCodeCamp codebase, platforms, and community."; 21 | const news = 22 | "[News Contributing](https://www.freecodecamp.org/news/developer-news-style-guide/) - Instructions specifically for becoming an author on our `/news` platform."; 23 | const prs = 24 | "[Pull Requests](https://github.com/freeCodeCamp/freeCodeCamp/pulls?q=is%3Aopen+is%3Apr+-label%3A%22status%3A+blocked%22+-label%3A%22status%3A+merge+conflict%22+status%3Asuccess+draft%3Afalse) - A list of GitHub pull requests that are ready for review (not in draft, not blocked, passing all CI checks)."; 25 | const header = "Here are some helpful links for you!"; 26 | const response = `${header}\n${coc}\n${mod}\n${contrib}\n${news}\n${prs}`; 27 | await driver.sendToRoom(response, room); 28 | } catch (err) { 29 | await logBotMessage( 30 | `${room} had an error with the \`resources\` command. Check the logs for more info.`, 31 | BOT 32 | ); 33 | console.error(err); 34 | } 35 | }, 36 | }; 37 | -------------------------------------------------------------------------------- /src/interfaces/rocketChat.ts: -------------------------------------------------------------------------------- 1 | export interface IMessage { 2 | rid: string | null; // room ID 3 | _id?: string; // generated by Random.id() 4 | t?: string; // type e.g. rm 5 | msg?: string; // text content 6 | alias?: string; // ?? 7 | emoji?: string; // emoji to use as avatar 8 | avatar?: string; // url 9 | groupable?: boolean; // ? 10 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 11 | bot?: any; // integration details 12 | urls?: string[]; // ? 13 | mentions?: string[]; // ? 14 | attachments?: IMessageAttachment[]; 15 | reactions?: IMessageReaction; 16 | location?: IMessageLocation; 17 | u?: IUser; // User that sent the message 18 | editedBy?: IUser; // User that edited the message 19 | editedAt?: Date; // When the message was edited 20 | } 21 | 22 | export interface IUser { 23 | _id: string; 24 | username: string; 25 | name?: string; 26 | } 27 | 28 | export interface IMessageAttachment { 29 | fields?: IAttachmentField[]; 30 | actions?: IMessageAction[]; 31 | color?: string; 32 | text?: string; 33 | ts?: string; 34 | thumb_url?: string; 35 | message_link?: string; 36 | collapsed?: boolean; 37 | author_name?: string; 38 | author_link?: string; 39 | author_icon?: string; 40 | title?: string; 41 | title_link?: string; 42 | title_link_download?: string; 43 | image_url?: string; 44 | audio_url?: string; 45 | video_url?: string; 46 | } 47 | 48 | export interface IAttachmentField { 49 | short?: boolean; 50 | title?: string; 51 | value?: string; 52 | } 53 | 54 | export interface IMessageAction { 55 | type?: string; 56 | text?: string; 57 | url?: string; 58 | image_url?: string; 59 | is_webview?: boolean; 60 | webview_height_ratio?: "compact" | "tall" | "full"; 61 | msg?: string; 62 | msg_in_chat_window?: boolean; 63 | button_alignment?: "vertical" | "horizontal"; 64 | temporary_buttons?: boolean; 65 | } 66 | 67 | export interface IMessageLocation { 68 | type: string; // e.g. Point 69 | coordinates: string[]; // longitude latitude 70 | } 71 | 72 | export interface IMessageReaction { 73 | [emoji: string]: { usernames: string[] }; // emoji: [usernames that reacted] 74 | } 75 | -------------------------------------------------------------------------------- /src/commands/rescind.ts: -------------------------------------------------------------------------------- 1 | import { driver } from "@rocket.chat/sdk"; 2 | import { logBotMessage } from "../helpers/botLogging"; 3 | import { isModerator } from "../helpers/isModerator"; 4 | import { sendToLog } from "../helpers/sendToLog"; 5 | import { CommandInt } from "../interfaces/CommandInt"; 6 | 7 | export const rescind: CommandInt = { 8 | name: "rescind", 9 | description: 10 | "Notifies a user that the warning they received has been rescinded.", 11 | parameters: ["user"], 12 | usage: [ 13 | "`{prefix} rescind user` - will notify the `user` that the warning they received has been rescinded, and will log that action in the log channel.", 14 | ], 15 | modCommand: true, 16 | command: async (message, room, BOT) => { 17 | try { 18 | if (!message.msg || !message.u) { 19 | return; 20 | } 21 | 22 | const isMod = await isModerator(message.u.username, BOT); 23 | 24 | if (!isMod) { 25 | await driver.sendToRoom( 26 | "Sorry, but this command is restricted to moderators.", 27 | room 28 | ); 29 | return; 30 | } 31 | 32 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 33 | const [username] = message.msg!.split(" ").slice(2); 34 | 35 | const target = username.startsWith("@") 36 | ? username.substring(1) 37 | : username; 38 | 39 | if (!target) { 40 | await driver.sendToRoom( 41 | "Sorry, but who did you want me to rescind a warning for?", 42 | room 43 | ); 44 | return; 45 | } 46 | 47 | await driver.sendDirectToUser( 48 | `${message.u.username} has rescinded your previous warning. Please remember to follow our [Code of Conduct](https://freecodecamp.org/news/code-of-conduct).`, 49 | target 50 | ); 51 | 52 | await sendToLog( 53 | `${message.u.username} has rescinded the warning against ${target}.`, 54 | BOT 55 | ); 56 | } catch (err) { 57 | await logBotMessage( 58 | `${room} had an error with the \`rescind\` command. Check the logs for more info.`, 59 | BOT 60 | ); 61 | console.error(err); 62 | } 63 | }, 64 | }; 65 | -------------------------------------------------------------------------------- /src/commands/help.ts: -------------------------------------------------------------------------------- 1 | import { driver } from "@rocket.chat/sdk"; 2 | import { logBotMessage } from "../helpers/botLogging"; 3 | import { CommandInt } from "../interfaces/CommandInt"; 4 | import { CommandList } from "./_CommandList"; 5 | 6 | export const help: CommandInt = { 7 | name: "help", 8 | description: "Returns information on the bot's list of commands.", 9 | parameters: ["?command"], 10 | usage: [ 11 | "`{prefix} help` - will return a list of public commands", 12 | "`{prefix} help ?command` - will return a detailed description of that command.", 13 | ], 14 | modCommand: false, 15 | command: async (message, room, BOT) => { 16 | try { 17 | const commands = CommandList.filter((command) => !command.modCommand); 18 | 19 | const commandList = commands.map( 20 | (command) => `\`${BOT.prefix} ${command.name}\`: ${command.description}` 21 | ); 22 | 23 | const [query] = message.msg!.split(" ").slice(2); 24 | 25 | if (!query) { 26 | const response = `Hello! I am a chat bot created specifically for this server! I have a few commands available - here is the information on those commands:\n${commandList 27 | .sort() 28 | .join("\n")}`; 29 | await driver.sendToRoom(response, room); 30 | return; 31 | } 32 | 33 | const targetCommand = commands.find((el) => el.name === query); 34 | 35 | if (!targetCommand) { 36 | const response = `I am so sorry, but I do not have a public ${query} command.`; 37 | await driver.sendToRoom(response, room); 38 | return; 39 | } 40 | 41 | const response = `*Information on my \`${query}\` command:* 42 | _Description_ 43 | ${targetCommand.description} 44 | 45 | _Parameters_ 46 | ${targetCommand.parameters.length ? targetCommand.parameters.join(" ") : "none"} 47 | 48 | _Example Uses_ 49 | ${targetCommand.usage 50 | .map((use) => use.replace(/\{prefix\}/g, BOT.prefix)) 51 | .join("\n")}`; 52 | 53 | await driver.sendToRoom(response, room); 54 | } catch (err) { 55 | await logBotMessage( 56 | `${room} had an error with the \`help\` command. Check the logs for more info.`, 57 | BOT 58 | ); 59 | console.error(err); 60 | } 61 | }, 62 | }; 63 | -------------------------------------------------------------------------------- /src/interfaces/apiInt.ts: -------------------------------------------------------------------------------- 1 | export interface UserInfoInt { 2 | user: { 3 | _id: string; 4 | createdAt: string; 5 | type: string; 6 | roles: string[]; 7 | username: string; 8 | }; 9 | } 10 | 11 | export interface RoleListInt { 12 | users: [ 13 | { 14 | _id: string; 15 | username: string; 16 | type: string; 17 | status: string; 18 | active: boolean; 19 | name: string; 20 | } 21 | ]; 22 | success: boolean; 23 | } 24 | 25 | export interface PrivateChannelCreateInt { 26 | group: { 27 | _id: string; 28 | name: string; 29 | t: string; 30 | usernames: [string]; 31 | msgs: number; 32 | u: { 33 | _id: string; 34 | username: string; 35 | }; 36 | ts: string; 37 | ro: boolean; 38 | sysMes: boolean; 39 | _updatedAt: string; 40 | }; 41 | success: boolean; 42 | } 43 | 44 | export interface PrivateChannelDeleteInt { 45 | success: boolean; 46 | } 47 | 48 | export interface RoomInfoInt { 49 | room: { 50 | _id: string; 51 | name: string; 52 | fname: string; 53 | t: string; 54 | msgs: number; 55 | usersCount: number; 56 | u: { 57 | _id: string; 58 | username: string; 59 | }; 60 | customFields: { [key: string]: unknown }; 61 | broadcast: boolean; 62 | encrypted: boolean; 63 | ts: string; 64 | ro: boolean; 65 | default: boolean; 66 | sysMes: boolean; 67 | _updatedAt: string; 68 | }; 69 | success: boolean; 70 | } 71 | 72 | export interface ChannelKickInt { 73 | group: { 74 | _id: string; 75 | name: string; 76 | t: string; 77 | usernames: [string]; 78 | msgs: number; 79 | u: { 80 | _id: string; 81 | username: string; 82 | }; 83 | ts: string; 84 | ro: boolean; 85 | sysMes: boolean; 86 | _updatedAt: string; 87 | }; 88 | success: boolean; 89 | } 90 | 91 | export interface ChannelInviteInt { 92 | channel: { 93 | _id: string; 94 | ts: string; 95 | t: string; 96 | name: string; 97 | usernames: string[]; 98 | msgs: number; 99 | _updatedAt: string; 100 | lm: string; 101 | }; 102 | success: boolean; 103 | } 104 | -------------------------------------------------------------------------------- /docs/creating-command.md: -------------------------------------------------------------------------------- 1 | # Adding a New Command 2 | 3 | Adding a new command requires a few steps. First, create a new file in the `/src/commands` directory called `commandName.ts` (where `commandName` is the name of your command). 4 | 5 | ## Writing the Command 6 | 7 | Commands use the same `CommandInt` interface, which can be found in `src/interfaces/CommandInt.ts`. A command should be exported to be accessed by the handler. 8 | 9 | NOTE: `CommandInt` will need to be imported. 10 | 11 | ```ts 12 | export const commandName: CommandInt = { 13 | name: "The name of your command (used to call the command)", 14 | description: "A brief description of the command, displayed in the help commands.", 15 | parameters: ["required", "?optional", "...multi-word"] /*Array of command parameters*/, 16 | usage: [ 17 | "`{prefix} commandName required` - Does something", 18 | "`{prefix} commandName required ?optional` - Does something extra", 19 | ], /* Example use cases. The {prefix} is replaced with the bot's actual prefix automatically */ 20 | modCommand: false, 21 | command: async (message, room, BOT) => { 22 | /* Command logic will go here */ 23 | } 24 | ``` 25 | 26 | Commands use a global error-handling logic, which relies on a try/catch around the command logic and a helper function for logging errors. The `logBotMessage` (which needs to be imported) will handle sending a notice to the bot's log channel. 27 | 28 | ```ts 29 | command: async (message, room, BOT) => { 30 | try { 31 | /* Command logic will go here */ 32 | } catch (err) { 33 | await logBotMessage( 34 | `${room} had an error with the \`commandName\` command. Check the logs for more info.`, 35 | BOT 36 | ); 37 | console.error(err); 38 | } 39 | }; 40 | ``` 41 | 42 | ## Importing the Command 43 | 44 | Once you have written your command's logic, you will need to import that new command through the `/src/commands/_CommandList.ts` file. To the existing `CommandList` array, add your new `commandName` command (remember that it must be imported if your IDE does not do so automatically). 45 | 46 | ```ts 47 | export const CommandList: CommandInt[] = [ 48 | aCommand, 49 | anotherCommand, 50 | commandName // This is your command! 51 | ]; 52 | ``` 53 | -------------------------------------------------------------------------------- /src/commands/close.ts: -------------------------------------------------------------------------------- 1 | import { api, driver } from "@rocket.chat/sdk"; 2 | import { logBotMessage } from "../helpers/botLogging"; 3 | import { isModerator } from "../helpers/isModerator"; 4 | import { sendToLog } from "../helpers/sendToLog"; 5 | import { PrivateChannelDeleteInt } from "../interfaces/apiInt"; 6 | import { CommandInt } from "../interfaces/CommandInt"; 7 | 8 | export const close: CommandInt = { 9 | name: "close", 10 | description: "Closes a channel created with the private command.", 11 | parameters: [], 12 | usage: [ 13 | "`{prefix} close` - will close the channel this command is called in, as long as the channel name starts with `private-`.", 14 | ], 15 | modCommand: true, 16 | command: async (message, room, BOT): Promise => { 17 | try { 18 | /** 19 | * While this should not be possible (it is confirmed 20 | * in the command handler), return early if the message does 21 | * not have a user author to make TypeScript happy. 22 | */ 23 | if (!message.u) { 24 | await driver.sendToRoom("Oops I broke it.", room); 25 | return; 26 | } 27 | const modCheck = await isModerator(message.u.username, BOT); 28 | 29 | if (!modCheck) { 30 | await driver.sendToRoom( 31 | "Sorry, but this command is locked to moderators.", 32 | room 33 | ); 34 | return; 35 | } 36 | 37 | if (!room.startsWith("private-")) { 38 | await driver.sendToRoom( 39 | "Sorry, but I can only close channels created with my `private` command.", 40 | room 41 | ); 42 | return; 43 | } 44 | 45 | const deleteChannel: PrivateChannelDeleteInt = await api.post( 46 | "groups.delete", 47 | { roomName: room } 48 | ); 49 | 50 | if (!deleteChannel.success) { 51 | await driver.sendToRoom("Sorry, but I cannot do that right now.", room); 52 | return; 53 | } 54 | 55 | await sendToLog( 56 | `${message.u.username} closed and deleted the ${room} channel.`, 57 | BOT 58 | ); 59 | return; 60 | } catch (err) { 61 | await logBotMessage( 62 | `${room} had an error with the \`close\` command. Check the logs for more info.`, 63 | BOT 64 | ); 65 | console.error(err); 66 | } 67 | }, 68 | }; 69 | -------------------------------------------------------------------------------- /src/commands/add.ts: -------------------------------------------------------------------------------- 1 | import { api, driver } from "@rocket.chat/sdk"; 2 | import { logBotMessage } from "../helpers/botLogging"; 3 | import { isModerator } from "../helpers/isModerator"; 4 | import { sendToLog } from "../helpers/sendToLog"; 5 | import { 6 | ChannelInviteInt, 7 | RoomInfoInt, 8 | UserInfoInt, 9 | } from "../interfaces/apiInt"; 10 | import { CommandInt } from "../interfaces/CommandInt"; 11 | 12 | export const add: CommandInt = { 13 | name: "add", 14 | description: "Adds a user to the room the command is called in", 15 | parameters: ["user"], 16 | usage: [ 17 | "`{prefix} add user` - Will add the `user` to the room the command is called in.", 18 | ], 19 | modCommand: true, 20 | command: async (message, room, BOT) => { 21 | try { 22 | if (!message.u || !message.msg) { 23 | return; 24 | } 25 | 26 | const isMod = await isModerator(message.u.username, BOT); 27 | 28 | if (!isMod) { 29 | await driver.sendToRoom( 30 | "Sorry, but this command is restricted to moderators.", 31 | room 32 | ); 33 | return; 34 | } 35 | 36 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 37 | const [username] = message.msg!.split(" ").slice(2); 38 | 39 | const target = username.startsWith("@") 40 | ? username.substring(1) 41 | : username; 42 | 43 | const userInfoRequest: UserInfoInt = await api.get("users.info", { 44 | username: target, 45 | }); 46 | 47 | const roomInfoRequest: RoomInfoInt = await api.get("rooms.info", { 48 | roomName: room, 49 | }); 50 | 51 | const didAdd: ChannelInviteInt = await api.post("channels.invite", { 52 | roomId: roomInfoRequest.room._id, 53 | userId: userInfoRequest.user._id, 54 | }); 55 | 56 | if (!didAdd || !didAdd.success) { 57 | await driver.sendToRoom("Sorry, but I cannot do that right now.", room); 58 | return; 59 | } 60 | const notice = `${message.u.username} has added you to #${room}.`; 61 | 62 | await driver.sendDirectToUser(notice, target); 63 | 64 | await sendToLog( 65 | `${message.u.username} has added ${target} to #${room}`, 66 | BOT 67 | ); 68 | } catch (err) { 69 | await logBotMessage( 70 | `${room} had an error with the \`add\` command. Check the logs for more info.`, 71 | BOT 72 | ); 73 | console.error(err); 74 | } 75 | }, 76 | }; 77 | -------------------------------------------------------------------------------- /src/commands/modHelp.ts: -------------------------------------------------------------------------------- 1 | import { driver } from "@rocket.chat/sdk"; 2 | import { logBotMessage } from "../helpers/botLogging"; 3 | import { isModerator } from "../helpers/isModerator"; 4 | import { CommandInt } from "../interfaces/CommandInt"; 5 | import { CommandList } from "./_CommandList"; 6 | 7 | export const modHelp: CommandInt = { 8 | name: "modHelp", 9 | description: "Returns information on the bot's list of moderation commands.", 10 | parameters: ["?command"], 11 | usage: [ 12 | "`{prefix} modHelp` - will return a list of moderator-only commands", 13 | "`{prefix} modHelp ?command` - will return a detailed description of that command.", 14 | ], 15 | modCommand: true, 16 | command: async (message, room, BOT) => { 17 | try { 18 | const commands = CommandList.filter((command) => command.modCommand); 19 | 20 | const commandList = commands.map( 21 | (command) => `\`${BOT.prefix} ${command.name}\`: ${command.description}` 22 | ); 23 | 24 | const isMod = await isModerator(message.u!.username, BOT); 25 | if (!isMod) { 26 | await driver.sendToRoom( 27 | "Sorry, but this command is restricted to moderators.", 28 | room 29 | ); 30 | return; 31 | } 32 | 33 | const [query] = message.msg!.split(" ").slice(2); 34 | 35 | if (!query) { 36 | const response = `Here are my available moderation commands:\n${commandList 37 | .sort() 38 | .join("\n")}`; 39 | await driver.sendToRoom(response, room); 40 | return; 41 | } 42 | 43 | const targetCommand = commands.find((el) => el.name === query); 44 | 45 | if (!targetCommand) { 46 | const response = `I am so sorry, but I do not have a private ${query} command.`; 47 | await driver.sendToRoom(response, room); 48 | return; 49 | } 50 | 51 | const response = `*Information on my \`${query}\` command:* 52 | _Description_ 53 | ${targetCommand.description} 54 | 55 | _Parameters_ 56 | ${ 57 | targetCommand.parameters.length ? targetCommand.parameters.join(", ") : "none" 58 | } 59 | 60 | _Example Uses_ 61 | ${targetCommand.usage 62 | .map((use) => use.replace(/\{prefix\}/g, BOT.prefix)) 63 | .join("\n")}`; 64 | 65 | await driver.sendToRoom(response, room); 66 | } catch (err) { 67 | await logBotMessage( 68 | `${room} had an error with the \`modHelp\` command. Check the logs for more info.`, 69 | BOT 70 | ); 71 | console.error(err); 72 | } 73 | }, 74 | }; 75 | -------------------------------------------------------------------------------- /src/commands/_CommandHandler.ts: -------------------------------------------------------------------------------- 1 | import { driver } from "@rocket.chat/sdk"; 2 | import { BOT } from ".."; 3 | import { logBotMessage } from "../helpers/botLogging"; 4 | import { MessageInt } from "../interfaces/messageInt"; 5 | import { CommandList } from "./_CommandList"; 6 | 7 | /** 8 | * This is a command handler which runs on each message 9 | * that the bot receives. 10 | * @param {unknown} err RocketChat SDK uses an error first callback. 11 | * @param {string[]} messages This is actually an array of messages, not a single message. 12 | * @param {unknown} _messageOptions Not sure what this is... 13 | * @returns {Promise} nothing 14 | * @todo Need to determine typing for the error and messageOptions? 15 | */ 16 | export const CommandHandler = async ( 17 | err: unknown, 18 | messages: MessageInt[] 19 | ): Promise => { 20 | if (err) { 21 | console.error(err); 22 | return; 23 | } 24 | const message = messages[0]; 25 | 26 | console.log(message); 27 | 28 | if (!message.u || message.u._id === BOT.botId) { 29 | return; 30 | } 31 | 32 | if (message.replies) { 33 | return; 34 | } 35 | 36 | if (message.editedBy || message.editedAt) { 37 | return; 38 | } 39 | 40 | if (!message.msg) { 41 | console.error("Message empty??"); 42 | return; 43 | } 44 | 45 | if (!message.rid) { 46 | console.error("No room id?"); 47 | return; 48 | } 49 | 50 | const roomName = await driver.getRoomName(message.rid); 51 | const [prefix, commandName] = message.msg.split(" "); 52 | if (prefix.toLowerCase() === BOT.prefix) { 53 | const currentTime = Date.now(); 54 | const timeSinceLastCommand = currentTime - BOT.lastCommandCalled; 55 | console.log(timeSinceLastCommand); 56 | if (timeSinceLastCommand < BOT.botRateLimit * 1000) { 57 | await driver.sendToRoom( 58 | `Sorry, @${ 59 | message.u.username 60 | }, but commands are being called too quickly. Please wait ${ 61 | BOT.botRateLimit - timeSinceLastCommand / 1000 62 | } seconds and try again.`, 63 | roomName 64 | ); 65 | return; 66 | } 67 | for (const Command of CommandList) { 68 | if (commandName === Command.name) { 69 | await Command.command(message, roomName, BOT); 70 | await logBotMessage( 71 | `@${message.u.username} called the \`${commandName}\` command in #${roomName}.`, 72 | BOT 73 | ); 74 | BOT.lastCommandCalled = Date.now(); 75 | return; 76 | } 77 | } 78 | await driver.sendToRoom( 79 | `I am sorry, but \`${commandName}\` is not a valid command.`, 80 | roomName 81 | ); 82 | } 83 | }; 84 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import dotenv from "dotenv"; 2 | import { api, driver, settings } from "@rocket.chat/sdk"; 3 | import { CommandHandler } from "./commands/_CommandHandler"; 4 | import { BotInt } from "./interfaces/BotInt"; 5 | import packageData from "../package.json"; 6 | 7 | dotenv.config(); 8 | 9 | const { 10 | ROCKETCHAT_URL, 11 | ROCKETCHAT_USER, 12 | ROCKETCHAT_PASSWORD, 13 | BOTNAME, 14 | MOD_LOG_CHANNEL, 15 | BOT_LOG_CHANNEL, 16 | BOT_RATE_LIMIT, 17 | } = process.env; 18 | const ROOMS = (process.env.ROCKETCHAT_ROOM || "general") 19 | .split(",") 20 | .map((el) => el.trim()); 21 | 22 | if (!ROCKETCHAT_URL || !ROCKETCHAT_USER || !ROCKETCHAT_PASSWORD || !BOTNAME) { 23 | console.error("Missing required environment variables."); 24 | process.exit(1); 25 | } 26 | 27 | if (!MOD_LOG_CHANNEL || !BOT_LOG_CHANNEL) { 28 | console.warn( 29 | "Missing log channel settings. Logging will be disabled for this instance!" 30 | ); 31 | } 32 | 33 | export const BOT: BotInt = { 34 | botId: "", 35 | botName: BOTNAME, 36 | hostPath: ROCKETCHAT_URL, 37 | modLogChannel: MOD_LOG_CHANNEL || "", 38 | botLogChannel: BOT_LOG_CHANNEL || "", 39 | version: packageData.version, 40 | prefix: process.env.PREFIX || "!bot", 41 | modRoles: process.env.ROLE_LIST?.split(",").map((el) => el.trim()) || [ 42 | "none", 43 | ], 44 | botRateLimit: parseInt(BOT_RATE_LIMIT || "10"), 45 | lastCommandCalled: 0, 46 | }; 47 | 48 | /** 49 | * The primary driver to run the bot. 50 | */ 51 | const runBot = async () => { 52 | const ssl = process.env.ROCKETCHAT_USE_SSL ? true : false; 53 | 54 | // force lib to use host? 55 | settings.host = ROCKETCHAT_URL; 56 | 57 | // Connect to server, log in. 58 | await driver.connect({ host: ROCKETCHAT_URL, useSsl: ssl }); 59 | BOT.botId = await driver.login({ 60 | username: ROCKETCHAT_USER, 61 | password: ROCKETCHAT_PASSWORD, 62 | }); 63 | 64 | // Auth to REST API 65 | await api.login({ username: ROCKETCHAT_USER, password: ROCKETCHAT_PASSWORD }); 66 | 67 | // Join configured rooms. 68 | await driver.joinRooms(ROOMS.concat([BOT.modLogChannel, BOT.botLogChannel])); 69 | console.log("joined rooms"); 70 | 71 | // Listen for messages. 72 | const subscribed = await driver.subscribeToMessages(); 73 | console.log(subscribed); 74 | 75 | // Pass received messages to command handler 76 | await driver.reactToMessages(CommandHandler); 77 | console.log("connected and waiting for messages"); 78 | 79 | //greet 80 | await driver.sendToRoom( 81 | `\`${BOTNAME}\` is online and running version: \`${BOT.version}\`!`, 82 | BOT.botLogChannel || ROOMS[0] 83 | ); 84 | console.log("Greeting message sent."); 85 | }; 86 | 87 | runBot(); 88 | -------------------------------------------------------------------------------- /src/commands/private.ts: -------------------------------------------------------------------------------- 1 | import { api, driver } from "@rocket.chat/sdk"; 2 | import { logBotMessage } from "../helpers/botLogging"; 3 | import { getModerators } from "../helpers/getModerators"; 4 | import { isModerator } from "../helpers/isModerator"; 5 | import { sendToLog } from "../helpers/sendToLog"; 6 | import { PrivateChannelCreateInt } from "../interfaces/apiInt"; 7 | import { CommandInt } from "../interfaces/CommandInt"; 8 | 9 | export const priv: CommandInt = { 10 | name: "private", 11 | description: "Creates a private room with a user and the moderator team", 12 | parameters: ["user"], 13 | usage: [ 14 | "`{prefix} private user` - will create a private channel, add the `user` to that channel, and add all moderators to the channel based on the configured moderator roles.", 15 | ], 16 | modCommand: true, 17 | command: async (message, room, BOT) => { 18 | try { 19 | /** 20 | * While this should not be possible (it is confirmed 21 | * in the command handler), return early if the message does 22 | * not have a user author to make TypeScript happy. 23 | */ 24 | if (!message.u) { 25 | await driver.sendToRoom("Oops I broke it.", room); 26 | return; 27 | } 28 | 29 | const modCheck = await isModerator(message.u.username, BOT); 30 | 31 | if (!modCheck) { 32 | await driver.sendToRoom( 33 | "Sorry, but this command is locked to moderators.", 34 | room 35 | ); 36 | return; 37 | } 38 | 39 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 40 | const [username] = message.msg!.split(" ").slice(2); 41 | 42 | const target = username.startsWith("@") 43 | ? username.substring(1) 44 | : username; 45 | 46 | if (!target) { 47 | await driver.sendToRoom( 48 | "Sorry, but who did you want to create a private room for?", 49 | room 50 | ); 51 | return; 52 | } 53 | 54 | const moderatorTeam = await getModerators(BOT); 55 | 56 | const privateChannel: PrivateChannelCreateInt = await api.post( 57 | "groups.create", 58 | { name: `private-${target}`, members: moderatorTeam.concat(target) } 59 | ); 60 | 61 | if (!privateChannel.success) { 62 | await driver.sendToRoom("Sorry, but I could not do that.", room); 63 | return; 64 | } 65 | 66 | await sendToLog( 67 | `${message.u.username} created a private discussion with ${target}.`, 68 | BOT 69 | ); 70 | } catch (err) { 71 | await logBotMessage( 72 | `${room} had an error with the \`private\` command. Check the logs for more info.`, 73 | BOT 74 | ); 75 | console.error(err); 76 | } 77 | }, 78 | }; 79 | -------------------------------------------------------------------------------- /src/commands/warn.ts: -------------------------------------------------------------------------------- 1 | import { driver } from "@rocket.chat/sdk"; 2 | import { logBotMessage } from "../helpers/botLogging"; 3 | import { isModerator } from "../helpers/isModerator"; 4 | import { sendToLog } from "../helpers/sendToLog"; 5 | import { CommandInt } from "../interfaces/CommandInt"; 6 | 7 | export const warn: CommandInt = { 8 | name: "warn", 9 | description: "Issues a warning to a user. Restricted to moderators.", 10 | parameters: ["user", "...reason"], 11 | usage: [ 12 | "`{prefix} warn user reason` - will issue a DM warning to the `user` for the given `reason` and log the action in the log channel.", 13 | ], 14 | modCommand: true, 15 | command: async (message, room, BOT) => { 16 | try { 17 | /** 18 | * While this should not be possible (it is confirmed 19 | * in the command handler), return early if the message does 20 | * not have a user author to make TypeScript happy. 21 | */ 22 | if (!message.u) { 23 | await driver.sendToRoom("Oops I broke it.", room); 24 | return; 25 | } 26 | 27 | const modCheck = await isModerator(message.u.username, BOT); 28 | 29 | if (!modCheck) { 30 | await driver.sendToRoom( 31 | "Sorry, but this command is locked to moderators.", 32 | room 33 | ); 34 | return; 35 | } 36 | 37 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 38 | const [username, ...reasonArgs] = message.msg!.split(" ").slice(2); 39 | 40 | const target = username.startsWith("@") 41 | ? username.substring(1) 42 | : username; 43 | 44 | const isTargetMod = await isModerator(target, BOT); 45 | 46 | if (isTargetMod) { 47 | await driver.sendToRoom( 48 | "Sorry, but you cannot use this command on a fellow moderator.", 49 | room 50 | ); 51 | return; 52 | } 53 | 54 | const reason = reasonArgs.join(" "); 55 | 56 | if (!reason) { 57 | await driver.sendToRoom( 58 | "Sorry, but would you please provide the reason for this action?", 59 | room 60 | ); 61 | return; 62 | } 63 | 64 | const warning = `${message.u.username} has warned you for:\n${reason}.\nPlease remember to follow our [Code of Conduct](https://freecodecamp.org/news/code-of-conduct).`; 65 | 66 | await driver.sendDirectToUser(warning, target); 67 | 68 | await sendToLog( 69 | `${message.u.username} warned ${target} for: ${reason}.`, 70 | BOT 71 | ); 72 | } catch (err) { 73 | await logBotMessage( 74 | `${room} had an error with the \`warn\` command. Check the logs for more info.`, 75 | BOT 76 | ); 77 | console.error(err); 78 | } 79 | }, 80 | }; 81 | -------------------------------------------------------------------------------- /src/commands/kick.ts: -------------------------------------------------------------------------------- 1 | import { api, driver } from "@rocket.chat/sdk"; 2 | import { logBotMessage } from "../helpers/botLogging"; 3 | import { isModerator } from "../helpers/isModerator"; 4 | import { sendToLog } from "../helpers/sendToLog"; 5 | import { RoomInfoInt, UserInfoInt, ChannelKickInt } from "../interfaces/apiInt"; 6 | import { CommandInt } from "../interfaces/CommandInt"; 7 | 8 | export const kick: CommandInt = { 9 | name: "kick", 10 | description: "Kicks a user from the room.", 11 | parameters: ["user", "...reason"], 12 | usage: [ 13 | "`{prefix} kick user reason` - will kick the `user` from the room the command is called in, DM that `user` with the `reason` for the action, and log the action in the log channel.", 14 | ], 15 | modCommand: true, 16 | command: async (message, room, BOT) => { 17 | try { 18 | if (!message.u) { 19 | return; 20 | } 21 | 22 | if (!message.msg) { 23 | return; 24 | } 25 | 26 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 27 | const [username, ...reasonArgs] = message.msg!.split(" ").slice(2); 28 | 29 | const target = username.startsWith("@") 30 | ? username.substring(1) 31 | : username; 32 | 33 | const reason = reasonArgs.join(" "); 34 | 35 | if (!reason) { 36 | await driver.sendToRoom( 37 | "Sorry, but would you please provide the reason for taking this action?", 38 | room 39 | ); 40 | return; 41 | } 42 | 43 | const isMod = await isModerator(message.u.username, BOT); 44 | 45 | if (!isMod) { 46 | driver.sendToRoom( 47 | "Sorry, but this command is restricted to moderators.", 48 | room 49 | ); 50 | return; 51 | } 52 | 53 | const isTargetMod = await isModerator(target, BOT); 54 | 55 | if (isTargetMod) { 56 | await driver.sendToRoom( 57 | "Sorry, but you cannot use this command on a fellow moderator.", 58 | room 59 | ); 60 | return; 61 | } 62 | 63 | const userInfoRequest: UserInfoInt = await api.get("users.info", { 64 | username: target, 65 | }); 66 | 67 | const roomInfoRequest: RoomInfoInt = await api.get("rooms.info", { 68 | roomName: room, 69 | }); 70 | 71 | const didKick: ChannelKickInt = await api.post("channels.kick", { 72 | userId: userInfoRequest.user._id, 73 | roomId: roomInfoRequest.room._id, 74 | }); 75 | 76 | if (!didKick || !didKick.success) { 77 | await driver.sendToRoom("Sorry, but I cannot do that right now.", room); 78 | return; 79 | } 80 | 81 | const notice = `${message.u.username} has kicked you from #${room} for:\n${reason}\nPlease remember to follow our [code of conduct](https://freecodecamp.org/news/code-of-conduct).`; 82 | 83 | await driver.sendDirectToUser(notice, target); 84 | 85 | await sendToLog( 86 | `${message.u.username} has kicked ${target} from #${room} for: ${reason}`, 87 | BOT 88 | ); 89 | } catch (err) { 90 | await logBotMessage( 91 | `${room} had an error with the \`kick\` command. Check the logs for more info.`, 92 | BOT 93 | ); 94 | console.error(err); 95 | } 96 | }, 97 | }; 98 | -------------------------------------------------------------------------------- /src/assets/motivational-quotes.ts: -------------------------------------------------------------------------------- 1 | export const quotes = { 2 | compliments: [ 3 | "Over the top!", 4 | "Down the rabbit hole we go!", 5 | "Bring that rain!", 6 | "Target acquired.", 7 | "Feel that need for speed!", 8 | "You've got guts!", 9 | "We have liftoff!", 10 | "To infinity and beyond!", 11 | "Encore!", 12 | "Onward!", 13 | "Challenge destroyed!", 14 | "It's on like Donkey Kong!", 15 | "Power level? It's over 9000!", 16 | "Coding spree!", 17 | "Code long and prosper.", 18 | "The crowd goes wild!", 19 | "One for the guinness book!", 20 | "Flawless victory!", 21 | "Most efficient!", 22 | "You've got the touch!", 23 | "You're on fire!", 24 | "The town is now red!", 25 | "To the nines!", 26 | "To the Batmobile!", 27 | "Pull out all the stops!", 28 | "You're a wizard, Harry!", 29 | "You're an all star!", 30 | "Way to go!", 31 | "Outta sight!", 32 | "You're crushing it!", 33 | "What sorcery is this?", 34 | "The world rejoices!", 35 | "That's the way it's done!", 36 | "You rock!", 37 | "Woo-hoo!", 38 | "We knew you could do it!", 39 | "Hyper Combo Finish!", 40 | "Nothing but net!", 41 | "Boom-shakalaka!", 42 | "You're a shooting star!", 43 | "You're unstoppable!", 44 | "Way cool!", 45 | "Walk on that sunshine!", 46 | "Keep on trucking!", 47 | "Off the charts!", 48 | "There is no spoon!", 49 | "Cranked it up to 11!", 50 | "Escape velocity reached!", 51 | "You make this look easy!", 52 | "Passed with flying colors!", 53 | "You've got this!", 54 | "Happy, happy, joy, joy!", 55 | "Tomorrow, the world!", 56 | "Your powers combined!", 57 | "It's alive. It's alive!", 58 | "Sonic Boom!", 59 | "Here's looking at you, Code!", 60 | "Ride like the wind!", 61 | "Legen - wait for it - dary!", 62 | "Ludicrous Speed! Go!", 63 | "Most triumphant!", 64 | "One loop to rule them all!", 65 | "By the power of Grayskull!", 66 | "You did it!", 67 | "Storm that castle!", 68 | "Face-melting guitar solo!", 69 | "Checkmate!", 70 | "Bodacious!", 71 | "Tubular!", 72 | "You're outta sight!", 73 | "Keep calm and code on!", 74 | "Even sad panda smiles!", 75 | "Even grumpy cat approves!", 76 | "Kool Aid Man says oh yeah!", 77 | "Bullseye!", 78 | "Far out!", 79 | "You're heating up!", 80 | "Standing ovation!", 81 | "Nice one!", 82 | "All right!", 83 | "Hasta la vista, challenge!", 84 | "Terminated.", 85 | "Off the hook!", 86 | "Thundercats, Hooo!", 87 | "Shiver me timbers!", 88 | "Raise the roof!", 89 | "Bingo!", 90 | "Even honeybadger cares!", 91 | "Helm, Warp Nine. Engage!", 92 | "Gotta code 'em all!", 93 | "Spool up the FTL drive!", 94 | "Cool beans!", 95 | "They're in another castle.", 96 | "Power UP!", 97 | "Pikachu chooses you!", 98 | "I gotta have more cow bell.", 99 | "Gotta go fast!", 100 | "Yipee!", 101 | "Cowabunga!", 102 | "Moon Prism Power!", 103 | "Plus Ultra!", 104 | ], 105 | motivationalQuotes: [ 106 | { 107 | quote: "Whatever you are, be a good one.", 108 | author: "Abraham Lincoln", 109 | }, 110 | { 111 | quote: "A change in perspective is worth 80 IQ points.", 112 | author: "Alan Kay", 113 | }, 114 | { 115 | quote: "The best way to predict the future is to invent it.", 116 | author: "Alan Kay", 117 | }, 118 | { 119 | quote: 120 | "The future is not laid out on a track. It is something that we can decide, and to the extent that we do not violate any known laws of the universe, we can probably make it work the way that we want to.", 121 | author: "Alan Kay", 122 | }, 123 | { 124 | quote: 125 | "We can only see a short distance ahead, but we can see plenty there that needs to be done.", 126 | author: "Alan Turing", 127 | }, 128 | { 129 | quote: 130 | "In the depth of winter, I finally learned that within me there lay an invincible summer.", 131 | author: "Albert Camus", 132 | }, 133 | { 134 | quote: "A person who never made a mistake never tried anything new.", 135 | author: "Albert Einstein", 136 | }, 137 | { 138 | quote: "Creativity is intelligence having fun.", 139 | author: "Albert Einstein", 140 | }, 141 | { 142 | quote: "I have no special talents. I am only passionately curious.", 143 | author: "Albert Einstein", 144 | }, 145 | { 146 | quote: 147 | "Life is like riding a bicycle. To keep your balance, you must keep moving.", 148 | author: "Albert Einstein", 149 | }, 150 | { 151 | quote: "Make everything as simple as possible, but not simpler.", 152 | author: "Albert Einstein", 153 | }, 154 | { 155 | quote: "Never memorize something that you can look up.", 156 | author: "Albert Einstein", 157 | }, 158 | { 159 | quote: "Once we accept our limits, we go beyond them.", 160 | author: "Albert Einstein", 161 | }, 162 | { 163 | quote: "Play is the highest form of research.", 164 | author: "Albert Einstein", 165 | }, 166 | { 167 | quote: 168 | "We cannot solve our problems with the same thinking we used when we created them.", 169 | author: "Albert Einstein", 170 | }, 171 | { 172 | quote: 173 | "Wisdom is not a product of schooling but of the lifelong attempt to acquire it.", 174 | author: "Albert Einstein", 175 | }, 176 | { 177 | quote: "Your imagination is your preview of life's coming attractions.", 178 | author: "Albert Einstein", 179 | }, 180 | { 181 | quote: 182 | "There is only one corner of the universe you can be certain of improving, and that's your own self.", 183 | author: "Aldous Huxley", 184 | }, 185 | { 186 | quote: 187 | "The most common way people give up their power is by thinking they don't have any.", 188 | author: "Alice Walker", 189 | }, 190 | { 191 | quote: "Follow your inner moonlight. Don't hide the madness.", 192 | author: "Allen Ginsberg", 193 | }, 194 | { 195 | quote: 196 | "The most difficult thing is the decision to act. The rest is merely tenacity.", 197 | author: "Amelia Earhart", 198 | }, 199 | { 200 | quote: "Life shrinks or expands in proportion with one's courage.", 201 | author: "Anaïs Nin", 202 | }, 203 | { 204 | quote: "Weeks of programming can save you hours of planning.", 205 | author: "Unknown", 206 | }, 207 | { 208 | quote: "Quality is not an act, it is a habit.", 209 | author: "Aristotle", 210 | }, 211 | { 212 | quote: "Start where you are. Use what you have. Do what you can.", 213 | author: "Arthur Ashe", 214 | }, 215 | { 216 | quote: `Nothing is impossible, the word itself says "I'm possible"!`, 217 | author: "Audrey Hepburn", 218 | }, 219 | { 220 | quote: "Every strike brings me closer to the next home run.", 221 | author: "Babe Ruth", 222 | }, 223 | { 224 | quote: "By failing to prepare, you are preparing to fail.", 225 | author: "Benjamin Franklin", 226 | }, 227 | { 228 | quote: 229 | "Tell me and I forget. Teach me and I remember. Involve me and I learn.", 230 | author: "Benjamin Franklin", 231 | }, 232 | { 233 | quote: "Well done is better than well said.", 234 | author: "Benjamin Franklin", 235 | }, 236 | { 237 | quote: "There are no short cuts to any place worth going.", 238 | author: "Beverly Sills", 239 | }, 240 | { 241 | quote: "Controlling complexity is the essence of computer programming.", 242 | author: "Brian Kernighan", 243 | }, 244 | { 245 | quote: 246 | "I fear not the man who has practiced 10,000 kicks once, but I fear the man who has practiced one kick 10,000 times.", 247 | author: "Bruce Lee", 248 | }, 249 | { 250 | quote: "There are far, far better things ahead than any we leave behind.", 251 | author: "C.S. Lewis", 252 | }, 253 | { 254 | quote: "We are what we believe we are.", 255 | author: "C.S. Lewis", 256 | }, 257 | { 258 | quote: 259 | "With the possible exception of the equator, everything begins somewhere.", 260 | author: "C.S. Lewis", 261 | }, 262 | { 263 | quote: 264 | "You are never too old to set another goal, or to dream a new dream.", 265 | author: "C.S. Lewis", 266 | }, 267 | { 268 | quote: "Somewhere, something incredible is waiting to be known.", 269 | author: "Carl Sagan", 270 | }, 271 | { 272 | quote: "If you're not making mistakes, then you're not making decisions.", 273 | author: "Catherine Cook", 274 | }, 275 | { 276 | quote: "Find what you love and let it kill you.", 277 | author: "Charles Bukowski", 278 | }, 279 | { 280 | quote: "What matters most is how well you walk through the fire.", 281 | author: "Charles Bukowski", 282 | }, 283 | { 284 | quote: 285 | "It is not the strongest of the species that survive, nor the most intelligent, but the one most responsive to change.", 286 | author: "Charles Darwin", 287 | }, 288 | { 289 | quote: "Life is 10% what happens to you and 90% how you react to it.", 290 | author: "Charles R. Swindoll", 291 | }, 292 | { 293 | quote: "You will do foolish things, but do them with enthusiasm.", 294 | author: "Colette", 295 | }, 296 | { 297 | quote: "It does not matter how slowly you go as long as you do not stop.", 298 | author: "Confucius", 299 | }, 300 | { 301 | quote: "Real knowledge is to know the extent of one's ignorance.", 302 | author: "Confucius", 303 | }, 304 | { 305 | quote: "The past cannot be changed. The future is yet in your power.", 306 | author: "Confucius", 307 | }, 308 | { 309 | quote: 310 | "Looking at code you wrote more than two weeks ago is like looking at code you are seeing for the first time.", 311 | author: "Dan Hurvitz", 312 | }, 313 | { 314 | quote: "Someday is not a day of the week.", 315 | author: "Denise Brennan-Nelson", 316 | }, 317 | { 318 | quote: 319 | "UNIX is simple. It just takes a genius to understand its simplicity.", 320 | author: "Dennis Ritchie", 321 | }, 322 | { 323 | quote: 324 | "Computers are good at following instructions, but not at reading your mind.", 325 | author: "Donald Knuth", 326 | }, 327 | { 328 | quote: 329 | "A good programmer is someone who always looks both ways before crossing a one-way street.", 330 | author: "Doug Linder", 331 | }, 332 | { 333 | quote: "Tough times never last, but tough people do.", 334 | author: "Dr. Robert Schuller", 335 | }, 336 | { 337 | quote: 338 | "If things start happening, don't worry, don't stew, just go right along and you'll start happening too.", 339 | author: "Dr. Seuss", 340 | }, 341 | { 342 | quote: 343 | "Do not go gentle into that good night. Rage, rage against the dying of the light.", 344 | author: "Dylan Thomas", 345 | }, 346 | { 347 | quote: 348 | "The question of whether computers can think is like the question of whether submarines can swim.", 349 | author: "E.W. Dijkstra", 350 | }, 351 | { 352 | quote: 353 | "Any code of your own that you haven't looked at for six or more months might as well have been written by someone else.", 354 | author: "Eagleson's Law", 355 | }, 356 | { 357 | quote: "Do one thing every day that scares you.", 358 | author: "Eleanor Roosevelt", 359 | }, 360 | { 361 | quote: "With the new day comes new strength and new thoughts.", 362 | author: "Eleanor Roosevelt", 363 | }, 364 | { 365 | quote: "You must do the things you think you cannot do.", 366 | author: "Eleanor Roosevelt", 367 | }, 368 | { 369 | quote: "Light tomorrow with today.", 370 | author: "Elizabeth Barrett Browning", 371 | }, 372 | { 373 | quote: "Forever is composed of nows.", 374 | author: "Emily Dickinson", 375 | }, 376 | { 377 | quote: 378 | "Computer science education cannot make anybody an expert programmer any more than studying brushes and pigment can make somebody an expert painter.", 379 | author: "Eric Raymond", 380 | }, 381 | { 382 | quote: "If you don't risk anything, you risk even more.", 383 | author: "Erica Jong", 384 | }, 385 | { 386 | quote: 387 | "The world breaks everyone, and afterward, many are strong at the broken places.", 388 | author: "Ernest Hemingway", 389 | }, 390 | { 391 | quote: 392 | "There is nothing noble in being superior to your fellow man; true nobility is being superior to your former self.", 393 | author: "Ernest Hemingway", 394 | }, 395 | { 396 | quote: "Never confuse a single defeat with a final defeat.", 397 | author: "F. Scott Fitzgerald", 398 | }, 399 | { 400 | quote: 401 | "I attribute my success to this - I never gave or took any excuse.", 402 | author: "Florence Nightingale", 403 | }, 404 | { 405 | quote: "The best revenge is massive success.", 406 | author: "Frank Sinatra", 407 | }, 408 | { 409 | quote: 410 | "The only limit to our realization of tomorrow, will be our doubts of today.", 411 | author: "Franklin D. Roosevelt", 412 | }, 413 | { 414 | quote: 415 | "Right or wrong, it's very pleasant to break something from time to time.", 416 | author: "Fyodor Dostoevsky", 417 | }, 418 | { 419 | quote: "The harder I work, the luckier I get.", 420 | author: "Gary Player", 421 | }, 422 | { 423 | quote: "Giving up is the only sure way to fail.", 424 | author: "Gena Showalter", 425 | }, 426 | { 427 | quote: 428 | "The only truly secure system is one that is powered off, cast in a block of concrete and sealed in a lead-lined room with armed guards.", 429 | author: "Gene Spafford", 430 | }, 431 | { 432 | quote: 433 | "A life spent making mistakes is not only more honorable, but more useful than a life spent doing nothing.", 434 | author: "George Bernard Shaw", 435 | }, 436 | { 437 | quote: 438 | "First learn computer science and all the theory. Next develop a programming style. Then forget all that and just hack.", 439 | author: "George Carrette", 440 | }, 441 | { 442 | quote: 443 | "Discovering the unexpected is more important than confirming the known.", 444 | author: "George Box", 445 | }, 446 | { 447 | quote: "We only see what we know.", 448 | author: "Goethe", 449 | }, 450 | { 451 | quote: "Without hard work, nothing grows but weeds.", 452 | author: "Gordon B. Hinckley", 453 | }, 454 | { 455 | quote: 456 | "The function of good software is to make the complex appear to be simple.", 457 | author: "Grady Booch", 458 | }, 459 | { 460 | quote: 461 | "When you know that you're capable of dealing with whatever comes, you have the only security the world has to offer.", 462 | author: "Harry Browne", 463 | }, 464 | { 465 | quote: "Pain is inevitable. Suffering is optional.", 466 | author: "Haruki Murakami", 467 | }, 468 | { 469 | quote: 470 | "Optimism is the faith that leads to achievement. Nothing can be done without hope and confidence.", 471 | author: "Helen Keller", 472 | }, 473 | { 474 | quote: "The price of anything is the amount of life you exchange for it.", 475 | author: "Henry David Thoreau", 476 | }, 477 | { 478 | quote: "Whether you think you can or think you can't, you're right.", 479 | author: "Henry Ford", 480 | }, 481 | { 482 | quote: 483 | "The most exciting phrase to hear in science, the one that heralds discoveries, is not 'Eureka!' but 'Now that's funny…'", 484 | author: "Isaac Asimov", 485 | }, 486 | { 487 | quote: "We are all failures. At least the best of us are.", 488 | author: "J.M. Barrie", 489 | }, 490 | { 491 | quote: 492 | "You can't wait for inspiration. You have to go after it with a club.", 493 | author: "Jack London", 494 | }, 495 | { 496 | quote: "Don't wish it were easier, wish you were better.", 497 | author: "Jim Rohn", 498 | }, 499 | { 500 | quote: "By seeking and blundering we learn.", 501 | author: "Johann Wolfgang von Goethe", 502 | }, 503 | { 504 | quote: 505 | "Knowing is not enough; we must apply. Wishing is not enough; we must do.", 506 | author: "Johann Wolfgang von Goethe", 507 | }, 508 | { 509 | quote: "We first make our habits, then our habits make us.", 510 | author: "John Dryden", 511 | }, 512 | { 513 | quote: "The power of imagination makes us infinite.", 514 | author: "John Muir", 515 | }, 516 | { 517 | quote: "May you live every day of your life.", 518 | author: "Jonathan Swift", 519 | }, 520 | { 521 | quote: "Perseverance is failing 19 times and succeeding the 20th.", 522 | author: "Julie Andrews", 523 | }, 524 | { 525 | quote: 526 | "The work of today is the history of tomorrow, and we are its makers.", 527 | author: "Juliette Gordon Low", 528 | }, 529 | { 530 | quote: 531 | "If you reveal your secrets to the wind, you should not blame the wind for revealing them to the trees.", 532 | author: "Kahlil Gibran", 533 | }, 534 | { 535 | quote: 536 | "Optimism is an occupational hazard of programming; feedback is the treatment.", 537 | author: "Kent Beck", 538 | }, 539 | { 540 | quote: 541 | "Opportunity does not knock, it presents itself when you beat down the door.", 542 | author: "Kyle Chandler", 543 | }, 544 | { 545 | quote: "To iterate is human, to recurse divine.", 546 | author: "Peter Deutsch", 547 | }, 548 | { 549 | quote: 550 | "A good traveler has no fixed plans and is not intent on arriving.", 551 | author: "Lao Tzu", 552 | }, 553 | { 554 | quote: "An ant on the move does more than a dozing ox.", 555 | author: "Lao Tzu", 556 | }, 557 | { 558 | quote: 559 | "Do the difficult things while they are easy and do the great things while they are small. A journey of a thousand miles must begin with a single step.", 560 | author: "Lao Tzu", 561 | }, 562 | { 563 | quote: 564 | "That's the thing about people who think they hate computers. What they really hate is lousy programmers.", 565 | author: "Larry Niven", 566 | }, 567 | { 568 | quote: 569 | "It had long since come to my attention that people of accomplishment rarely sat back and let things happen to them. They went out and happened to things.", 570 | author: "Leonardo da Vinci", 571 | }, 572 | { 573 | quote: "If you're any good at all, you know you can be better.", 574 | author: "Lindsay Buckingham", 575 | }, 576 | { 577 | quote: 578 | "If people never did silly things, nothing intelligent would ever get done.", 579 | author: "Ludwig Wittgenstein", 580 | }, 581 | { 582 | quote: "You only live once, but if you do it right, once is enough.", 583 | author: "Mae West", 584 | }, 585 | { 586 | quote: 587 | "Live as if you were to die tomorrow. Learn as if you were to live forever.", 588 | author: "Mahatma Gandhi", 589 | }, 590 | { 591 | quote: 592 | "Strength does not come from physical capacity. It comes from an indomitable will.", 593 | author: "Mahatma Gandhi", 594 | }, 595 | { 596 | quote: 597 | "One person's 'paranoia' is another person's 'engineering redundancy'.", 598 | author: "Marcus J. Ranum", 599 | }, 600 | { 601 | quote: 602 | "Nothing in life is to be feared, it is only to be understood. Now is the time to understand more, so that we may fear less.", 603 | author: "Marie Curie", 604 | }, 605 | { 606 | quote: 607 | "If you have everything under control, you're not moving fast enough.", 608 | author: "Mario Andretti", 609 | }, 610 | { 611 | quote: 612 | "Education: the path from cocky ignorance to miserable uncertainty.", 613 | author: "Mark Twain", 614 | }, 615 | { 616 | quote: 617 | "It ain't what you don't know that gets you into trouble. It's what you know for sure that just ain't so.", 618 | author: "Mark Twain", 619 | }, 620 | { 621 | quote: "The secret of getting ahead is getting started.", 622 | author: "Mark Twain", 623 | }, 624 | { 625 | quote: 626 | "The two most important days in your life are the day you are born and the day you find out why.", 627 | author: "Mark Twain", 628 | }, 629 | { 630 | quote: 631 | "Twenty years from now you will be more disappointed by the things that you didn't do than by the ones you did do. So throw off the bowlines. Sail away from the safe harbor. Catch the trade winds in your sails.", 632 | author: "Mark Twain", 633 | }, 634 | { 635 | quote: 636 | "Any fool can write code that a computer can understand. Good programmers write code that humans can understand.", 637 | author: "Martin Fowler", 638 | }, 639 | { 640 | quote: 641 | "I know, somehow, that only when it is dark enough can you see the stars.", 642 | author: "Martin Luther King Jr.", 643 | }, 644 | { 645 | quote: "It is never too late to be what you might have been.", 646 | author: "Mary Anne Evans", 647 | }, 648 | { 649 | quote: "Nothing will work unless you do.", 650 | author: "Maya Angelou", 651 | }, 652 | { 653 | quote: 654 | "We delight in the beauty of the butterfly, but rarely admit the changes it has gone through to achieve that beauty.", 655 | author: "Maya Angelou", 656 | }, 657 | { 658 | quote: "We may encounter many defeats, but we must not be defeated.", 659 | author: "Maya Angelou", 660 | }, 661 | { 662 | quote: "Everybody has talent, but ability takes hard work.", 663 | author: "Michael Jordan", 664 | }, 665 | { 666 | quote: 667 | "I've missed more than 9,000 shots during my career. I've lost almost 300 games. 26 times, I've been trusted to take the game winning shot and missed. I've failed over and over and over again in my life. And that is why I succeed.", 668 | author: "Michael Jordan", 669 | }, 670 | { 671 | quote: 672 | "Impossible is just a big word thrown around by small men who find it easier to live in the world they've been given than to explore the power they have to change it. Impossible is not a fact. It's an opinion. Impossible is not a declaration. It's a dare. Impossible is potential. Impossible is temporary. Impossible is nothing.", 673 | author: "Muhammad Ali", 674 | }, 675 | { 676 | quote: "A winner is a dreamer who never gives up.", 677 | author: "Nelson Mandela", 678 | }, 679 | { 680 | quote: "It always seems impossible until it's done.", 681 | author: "Nelson Mandela", 682 | }, 683 | { 684 | quote: 685 | "Failure will never overtake me if my determination to succeed is strong enough.", 686 | author: "Og Mandino", 687 | }, 688 | { 689 | quote: "I am not young enough to know everything.", 690 | author: "Oscar Wilde", 691 | }, 692 | { 693 | quote: 694 | "There is only one thing that makes a dream impossible to achieve: the fear of failure.", 695 | author: "Paulo Coelho", 696 | }, 697 | { 698 | quote: "Never go to bed mad. Stay up and fight.", 699 | author: "Phyllis Diller", 700 | }, 701 | { 702 | quote: 703 | "You can't cross the sea merely by standing and staring at the water.", 704 | author: "Rabindranath Tagore", 705 | }, 706 | { 707 | quote: 708 | "The only person you are destined to become is the person you decide to be.", 709 | author: "Ralph Waldo Emerson", 710 | }, 711 | { 712 | quote: "What you do speaks so loudly that I cannot hear what you say.", 713 | author: "Ralph Waldo Emerson", 714 | }, 715 | { 716 | quote: 717 | "People who are crazy enough to think they can change the world, are the ones who do.", 718 | author: "Rob Siltanen", 719 | }, 720 | { 721 | quote: "The best way out is always through.", 722 | author: "Robert Frost", 723 | }, 724 | { 725 | quote: "Today's accomplishments were yesterday's impossibilities.", 726 | author: "Robert H. Schuller", 727 | }, 728 | { 729 | quote: 730 | "Don't be satisfied with stories, how things have gone with others. Unfold your own myth.", 731 | author: "Rumi", 732 | }, 733 | { 734 | quote: 735 | "Forget safety. Live where you fear to live. Destroy your reputation. Be notorious.", 736 | author: "Rumi", 737 | }, 738 | { 739 | quote: "Sell your cleverness and buy bewilderment.", 740 | author: "Rumi", 741 | }, 742 | { 743 | quote: "The cure for pain is in the pain.", 744 | author: "Rumi", 745 | }, 746 | { 747 | quote: "Have no fear of perfection - you'll never reach it.", 748 | author: "Salvador Dalí", 749 | }, 750 | { 751 | quote: "Don't watch the clock. Do what it does. Keep going.", 752 | author: "Sam Levenson", 753 | }, 754 | { 755 | quote: 756 | "Ever Tried. Ever failed. No matter. Try again. Fail again. Fail better.", 757 | author: "Samuel Beckett", 758 | }, 759 | { 760 | quote: "The more you know, the more you realize you know nothing.", 761 | author: "Socrates", 762 | }, 763 | { 764 | quote: 765 | "The greatest enemy of knowledge is not ignorance, it is the illusion of knowledge.", 766 | author: "Stephen Hawking", 767 | }, 768 | { 769 | quote: "The universe doesn't allow perfection.", 770 | author: "Stephen Hawking", 771 | }, 772 | { 773 | quote: 774 | "Whether you want to uncover the secrets of the universe, or you want to pursue a career in the 21st century, basic computer programming is an essential skill to learn.", 775 | author: "Stephen Hawking", 776 | }, 777 | { 778 | quote: "The scariest moment is always just before you start.", 779 | author: "Stephen King", 780 | }, 781 | { 782 | quote: 783 | "You can, you should, and if you're brave enough to start, you will.", 784 | author: "Stephen King", 785 | }, 786 | { 787 | quote: "Arise, Awake and Stop not until the goal is reached.", 788 | author: "Swami Vivekananda", 789 | }, 790 | { 791 | quote: 792 | "It is said that your life flashes before your eyes just before you die. That is true, it's called Life.", 793 | author: "Terry Pratchett", 794 | }, 795 | { 796 | quote: "Believe you can and you're halfway there.", 797 | author: "Theodore Roosevelt", 798 | }, 799 | { 800 | quote: "I have not failed. I've just found 10,000 ways that won't work.", 801 | author: "Thomas A. Edison", 802 | }, 803 | { 804 | quote: 805 | "Our greatest weakness lies in giving up. The most certain way to succeed is always to try just one more time.", 806 | author: "Thomas A. Edison", 807 | }, 808 | { 809 | quote: "The harder the conflict, the more glorious the triumph.", 810 | author: "Thomas Paine", 811 | }, 812 | { 813 | quote: 814 | "The Web as I envisaged it, we have not seen it yet. The future is still so much bigger than the past.", 815 | author: "Tim Berners-Lee", 816 | }, 817 | { 818 | quote: "Failure is the condiment that gives success its flavor.", 819 | author: "Truman Capote", 820 | }, 821 | { 822 | quote: 823 | "Those who says it cannot be done should not interrupt the person doing it.", 824 | author: "Unknown", 825 | }, 826 | { 827 | quote: "Even if you fall on your face, you're still moving forward.", 828 | author: "Victor Kiam", 829 | }, 830 | { 831 | quote: "It's not whether you get knocked down, it's whether you get up.", 832 | author: "Vince Lombardi", 833 | }, 834 | { 835 | quote: "I dream my painting and I paint my dream.", 836 | author: "Vincent van Gogh", 837 | }, 838 | { 839 | quote: "Let us cultivate our garden.", 840 | author: "Voltaire", 841 | }, 842 | { 843 | quote: "Aim for the moon. If you miss, you may hit a star.", 844 | author: "W. Clement Stone", 845 | }, 846 | { 847 | quote: "The way to get started is to quit talking and begin doing.", 848 | author: "Walt Disney", 849 | }, 850 | { 851 | quote: "You miss 100% of the shots you don't take.", 852 | author: "Wayne Gretzky", 853 | }, 854 | { 855 | quote: "Don't let yesterday take up too much of today.", 856 | author: "Will Rogers", 857 | }, 858 | { 859 | quote: 860 | "Even if you're on the right track, you'll get run over if you just sit there.", 861 | author: "Will Rogers", 862 | }, 863 | { 864 | quote: 865 | "Do not wait to strike till the iron is hot; but make it hot by striking.", 866 | author: "William Butler Yeats", 867 | }, 868 | { 869 | quote: 870 | "You cannot swim for new horizons until you have courage to lose sight of the shore.", 871 | author: "William Faulkner", 872 | }, 873 | { 874 | quote: 875 | "Be not afraid of greatness. Some are born great, some achieve greatness, and others have greatness thrust upon them.", 876 | author: "William Shakespeare", 877 | }, 878 | { 879 | quote: "We know what we are, but not what we may be.", 880 | author: "William Shakespeare", 881 | }, 882 | { 883 | quote: 884 | "In theory there is no difference between theory and practice. In practice there is.", 885 | author: "Yogi Berra", 886 | }, 887 | { 888 | quote: "You can see a lot by just looking.", 889 | author: "Yogi Berra", 890 | }, 891 | { 892 | quote: "There is no elevator to success, you have to take the stairs.", 893 | author: "Zig Ziglar", 894 | }, 895 | { 896 | quote: 897 | "You don't have to be great to start, but you have to start to be great.", 898 | author: "Zig Ziglar", 899 | }, 900 | ], 901 | }; 902 | -------------------------------------------------------------------------------- /src/assets/algorithm-structure.ts: -------------------------------------------------------------------------------- 1 | export const algorithmSlugs = [ 2 | { 3 | superblock: "javascript-algorithms-and-data-structures", 4 | blocks: [ 5 | { 6 | block: "basic-algorithm-scripting", 7 | challenges: [ 8 | "convert-celsius-to-fahrenheit", 9 | "reverse-a-string", 10 | "factorialize-a-number", 11 | "find-the-longest-word-in-a-string", 12 | "return-largest-numbers-in-arrays", 13 | "confirm-the-ending", 14 | "repeat-a-string-repeat-a-string", 15 | "truncate-a-string", 16 | "finders-keepers", 17 | "boo-who", 18 | "title-case-a-sentence", 19 | "slice-and-splice", 20 | "falsy-bouncer", 21 | "where-do-i-belong", 22 | "mutations", 23 | "chunky-monkey", 24 | ], 25 | }, 26 | { 27 | block: "intermediate-algorithm-scripting", 28 | challenges: [ 29 | "sum-all-numbers-in-a-range", 30 | "diff-two-arrays", 31 | "seek-and-destroy", 32 | "wherefore-art-thou", 33 | "spinal-tap-case", 34 | "pig-latin", 35 | "search-and-replace", 36 | "dna-pairing", 37 | "missing-letters", 38 | "sorted-union", 39 | "convert-html-entities", 40 | "sum-all-odd-fibonacci-numbers", 41 | "sum-all-primes", 42 | "smallest-common-multiple", 43 | "drop-it", 44 | "steamroller", 45 | "binary-agents", 46 | "everything-be-true", 47 | "arguments-optional", 48 | "make-a-person", 49 | "map-the-debris", 50 | ], 51 | }, 52 | ], 53 | }, 54 | { 55 | superblock: "coding-interview-prep", 56 | blocks: [ 57 | { 58 | block: "algorithms", 59 | challenges: [ 60 | "find-the-symmetric-difference", 61 | "inventory-update", 62 | "no-repeats-please", 63 | "pairwise", 64 | "implement-bubble-sort", 65 | "implement-selection-sort", 66 | "implement-insertion-sort", 67 | "implement-quick-sort", 68 | "implement-merge-sort", 69 | ], 70 | }, 71 | { 72 | block: "rosetta-code", 73 | challenges: [ 74 | "100-doors", 75 | "24-game", 76 | "9-billion-names-of-god-the-integer", 77 | "abc-problem", 78 | "abundant,-deficient-and-perfect-number-classifications", 79 | "accumulator-factory", 80 | "ackermann-function", 81 | "align-columns", 82 | "amicable-pairs", 83 | "averages/mode", 84 | "averages/pythagorean-means", 85 | "averages/root-mean-square", 86 | "babbage-problem", 87 | "balanced-brackets", 88 | "circles-of-given-radius-through-two-points", 89 | "closest-pair-problem", 90 | "combinations", 91 | "comma-quibbling", 92 | "compare-a-list-of-strings", 93 | "convert-seconds-to-compound-duration", 94 | "count-occurrences-of-a-substring", 95 | "count-the-coins", 96 | "cramer's-rule", 97 | "cumulative-standard-deviation", 98 | "cusip", 99 | "cut-a-rectangle", 100 | "date-format", 101 | "date-manipulation", 102 | "day-of-the-week", 103 | "deal-cards-for-freecell", 104 | "deepcopy", 105 | "define-a-primitive-data-type", 106 | "department-numbers", 107 | "discordian-date", 108 | "dot-product", 109 | "element-wise-operations", 110 | "emirp-primes", 111 | "entropy", 112 | "equilibrium-index", 113 | "ethiopian-multiplication", 114 | "euler-method", 115 | "evaluate-binomial-coefficients", 116 | "execute-a-markov-algorithm", 117 | "execute-brain****", 118 | "extensible-prime-generator", 119 | "factorial", 120 | "factors-of-a-mersenne-number", 121 | "factors-of-an-integer", 122 | "farey-sequence", 123 | "fibonacci-n-step-number-sequences", 124 | "fibonacci-sequence", 125 | "fibonacci-word", 126 | "fizzbuzz", 127 | "fractran", 128 | "gamma-function", 129 | "gaussian-elimination", 130 | "general-fizzbuzz", 131 | "generate-lower-case-ascii-alphabet", 132 | "generator/exponential", 133 | "gray-code", 134 | "greatest-common-divisor", 135 | "greatest-subsequential-sum", 136 | "hailstone-sequence", 137 | "happy-numbers", 138 | "harshad-or-niven-series", 139 | "hash-from-two-arrays", 140 | "hash-join", 141 | "heronian-triangles", 142 | "hofstadter-figure-figure-sequences", 143 | "hofstadter-q-sequence", 144 | "i-before-e-except-after-c", 145 | "iban", 146 | "identity-matrix", 147 | "iterated-digits-squaring", 148 | "jaro-distance", 149 | "jortsort", 150 | "josephus-problem", 151 | "k-d-tree", 152 | "kaprekar-numbers", 153 | "knapsack-problem/0-1", 154 | "knapsack-problem/bounded", 155 | "knapsack-problem/continuous", 156 | "knapsack-problem/unbounded", 157 | "knight's-tour", 158 | "largest-int-from-concatenated-ints", 159 | "last-letter-first-letter", 160 | "last-friday-of-each-month", 161 | "leap-year", 162 | "least-common-multiple", 163 | "left-factorials", 164 | "letter-frequency", 165 | "levenshtein-distance", 166 | "linear-congruential-generator", 167 | "long-multiplication", 168 | "longest-common-subsequence", 169 | "longest-increasing-subsequence", 170 | "longest-string-challenge", 171 | "look-and-say-sequence", 172 | "loop-over-multiple-arrays-simultaneously", 173 | "lu-decomposition", 174 | "lucas-lehmer-test", 175 | "ludic-numbers", 176 | "luhn-test-of-credit-card-numbers", 177 | "lychrel-numbers", 178 | "lzw-compression", 179 | "s-expressions", 180 | "sailors,-coconuts-and-a-monkey-problem", 181 | "search-a-list-of-records", 182 | "sedols", 183 | "self-describing-numbers", 184 | "self-referential-sequence", 185 | "semiprime", 186 | "set-consolidation", 187 | "set-of-real-numbers", 188 | "sha-1", 189 | "sha-256", 190 | "sort-an-array-of-composite-structures", 191 | "sort-disjoint-sublist", 192 | "sort-stability", 193 | "sort-using-a-custom-comparator", 194 | "sorting-algorithms/bead-sort", 195 | "sorting-algorithms/bogosort", 196 | "sorting-algorithms/cocktail-sort", 197 | "sorting-algorithms/comb-sort", 198 | "sorting-algorithms/gnome-sort", 199 | "sorting-algorithms/pancake-sort", 200 | "sorting-algorithms/permutation-sort", 201 | "sorting-algorithms/shell-sort", 202 | "sorting-algorithms/stooge-sort", 203 | "sorting-algorithms/strand-sort", 204 | "soundex", 205 | "spiral-matrix", 206 | "split-a-character-string-based-on-change-of-character", 207 | "state-name-puzzle", 208 | "stern-brocot-sequence", 209 | "straddling-checkerboard", 210 | "stream-merge", 211 | "strip-control-codes-and-extended-characters-from-a-string", 212 | "subleq", 213 | "sudoku", 214 | "sum-digits-of-an-integer", 215 | "sum-multiples-of-3-and-5", 216 | "sum-of-a-series", 217 | "sum-of-squares", 218 | "sum-to-100", 219 | "sutherland-hodgman-polygon-clipping", 220 | "symmetric-difference", 221 | "taxicab-numbers", 222 | "tokenize-a-string-with-escaping", 223 | "topological-sort", 224 | "top-rank-per-group", 225 | "towers-of-hanoi", 226 | "vector-cross-product", 227 | "vector-dot-product", 228 | "word-frequency", 229 | "word-wrap", 230 | "y-combinator", 231 | "zeckendorf-number-representation", 232 | "zhang-suen-thinning-algorithm", 233 | "zig-zag-matrix", 234 | ], 235 | }, 236 | { 237 | block: "project-euler", 238 | challenges: [ 239 | "problem-1-multiples-of-3-and-5", 240 | "problem-2-even-fibonacci-numbers", 241 | "problem-3-largest-prime-factor", 242 | "problem-4-largest-palindrome-product", 243 | "problem-5-smallest-multiple", 244 | "problem-6-sum-square-difference", 245 | "problem-7-10001st-prime", 246 | "problem-8-largest-product-in-a-series", 247 | "problem-9-special-pythagorean-triplet", 248 | "problem-10-summation-of-primes", 249 | "problem-11-largest-product-in-a-grid", 250 | "problem-12-highly-divisible-triangular-number", 251 | "problem-13-large-sum", 252 | "problem-14-longest-collatz-sequence", 253 | "problem-15-lattice-paths", 254 | "problem-16-power-digit-sum", 255 | "problem-17-number-letter-counts", 256 | "problem-18-maximum-path-sum-i", 257 | "problem-19-counting-sundays", 258 | "problem-20-factorial-digit-sum", 259 | "problem-21-amicable-numbers", 260 | "problem-22-names-scores", 261 | "problem-23-non-abundant-sums", 262 | "problem-24-lexicographic-permutations", 263 | "problem-25-1000-digit-fibonacci-number", 264 | "problem-26-reciprocal-cycles", 265 | "problem-27-quadratic-primes", 266 | "problem-28-number-spiral-diagonals", 267 | "problem-29-distinct-powers", 268 | "problem-30-digit-n-powers", 269 | "problem-31-coin-sums", 270 | "problem-32-pandigital-products", 271 | "problem-33-digit-cancelling-fractions", 272 | "problem-34-digit-factorials", 273 | "problem-35-circular-primes", 274 | "problem-36-double-base-palindromes", 275 | "problem-37-truncatable-primes", 276 | "problem-38-pandigital-multiples", 277 | "problem-39-integer-right-triangles", 278 | "problem-40-champernowne's-constant", 279 | "problem-41-pandigital-prime", 280 | "problem-42-coded-triangle-numbers", 281 | "problem-43-sub-string-divisibility", 282 | "problem-44-pentagon-numbers", 283 | "problem-45-triangular,-pentagonal,-and-hexagonal", 284 | "problem-46-goldbach's-other-conjecture", 285 | "problem-47-distinct-primes-factors", 286 | "problem-48-self-powers", 287 | "problem-49-prime-permutations", 288 | "problem-50-consecutive-prime-sum", 289 | "problem-51-prime-digit-replacements", 290 | "problem-52-permuted-multiples", 291 | "problem-53-combinatoric-selections", 292 | "problem-54-poker-hands", 293 | "problem-55-lychrel-numbers", 294 | "problem-56-powerful-digit-sum", 295 | "problem-57-square-root-convergents", 296 | "problem-58-spiral-primes", 297 | "problem-59-xor-decryption", 298 | "problem-60-prime-pair-sets", 299 | "problem-61-cyclical-figurate-numbers", 300 | "problem-62-cubic-permutations", 301 | "problem-63-powerful-digit-counts", 302 | "problem-64-odd-period-square-roots", 303 | "problem-65-convergents-of-e", 304 | "problem-66-diophantine-equation", 305 | "problem-67-maximum-path-sum-ii", 306 | "problem-68-magic-5-gon-ring", 307 | "problem-69-totient-maximum", 308 | "problem-70-totient-permutation", 309 | "problem-71-ordered-fractions", 310 | "problem-72-counting-fractions", 311 | "problem-73-counting-fractions-in-a-range", 312 | "problem-74-digit-factorial-chains", 313 | "problem-75-singular-integer-right-triangles", 314 | "problem-76-counting-summations", 315 | "problem-77-prime-summations", 316 | "problem-78-coin-partitions", 317 | "problem-79-passcode-derivation", 318 | "problem-80-square-root-digital-expansion", 319 | "problem-81-path-sum-two-ways", 320 | "problem-82-path-sum-three-ways", 321 | "problem-83-path-sum-four-ways", 322 | "problem-84-monopoly-odds", 323 | "problem-85-counting-rectangles", 324 | "problem-86-cuboid-route", 325 | "problem-87-prime-power-triples", 326 | "problem-88-product-sum-numbers", 327 | "problem-89-roman-numerals", 328 | "problem-90-cube-digit-pairs", 329 | "problem-91-right-triangles-with-integer-coordinates", 330 | "problem-92-square-digit-chains", 331 | "problem-93-arithmetic-expressions", 332 | "problem-94-almost-equilateral-triangles", 333 | "problem-95-amicable-chains", 334 | "problem-96-su-doku", 335 | "problem-97-large-non-mersenne-prime", 336 | "problem-98-anagramic-squares", 337 | "problem-99-largest-exponential", 338 | "problem-100-arranged-probability", 339 | "problem-101-optimum-polynomial", 340 | "problem-102-triangle-containment", 341 | "problem-103-special-subset-sums-optimum", 342 | "problem-104-pandigital-fibonacci-ends", 343 | "problem-105-special-subset-sums-testing", 344 | "problem-106-special-subset-sums-meta-testing", 345 | "problem-107-minimal-network", 346 | "problem-108-diophantine-reciprocals-i", 347 | "problem-109-darts", 348 | "problem-110-diophantine-reciprocals-ii", 349 | "problem-111-primes-with-runs", 350 | "problem-112-bouncy-numbers", 351 | "problem-113-non-bouncy-numbers", 352 | "problem-114-counting-block-combinations-i", 353 | "problem-115-counting-block-combinations-ii", 354 | "problem-116-red,-green-or-blue-tiles", 355 | "problem-117-red,-green,-and-blue-tiles", 356 | "problem-118-pandigital-prime-sets", 357 | "problem-119-digit-power-sum", 358 | "problem-120-square-remainders", 359 | "problem-121-disc-game-prize-fund", 360 | "problem-122-efficient-exponentiation", 361 | "problem-123-prime-square-remainders", 362 | "problem-124-ordered-radicals", 363 | "problem-125-palindromic-sums", 364 | "problem-126-cuboid-layers", 365 | "problem-127-abc-hits", 366 | "problem-128-hexagonal-tile-differences", 367 | "problem-129-repunit-divisibility", 368 | "problem-130-composites-with-prime-repunit-property", 369 | "problem-131-prime-cube-partnership", 370 | "problem-132-large-repunit-factors", 371 | "problem-133-repunit-nonfactors", 372 | "problem-134-prime-pair-connection", 373 | "problem-135-same-differences", 374 | "problem-136-singleton-difference", 375 | "problem-137-fibonacci-golden-nuggets", 376 | "problem-138-special-isosceles-triangles", 377 | "problem-139-pythagorean-tiles", 378 | "problem-140-modified-fibonacci-golden-nuggets", 379 | "problem-141-investigating-progressive-numbers,-n,-which-are-also-square", 380 | "problem-142-perfect-square-collection", 381 | "problem-143-investigating-the-torricelli-point-of-a-triangle", 382 | "problem-144-investigating-multiple-reflections-of-a-laser-beam", 383 | "problem-145-how-many-reversible-numbers-are-there-below-one-billion?", 384 | "problem-146-investigating-a-prime-pattern", 385 | "problem-147-rectangles-in-cross-hatched-grids", 386 | "problem-148-exploring-pascal's-triangle", 387 | "problem-149-searching-for-a-maximum-sum-subsequence", 388 | "problem-150-searching-a-triangular-array-for-a-sub-triangle-having-minimum-sum", 389 | "problem-151-paper-sheets-of-standard-sizes-an-expected-value-problem", 390 | "problem-152-writing-one-half-as-a-sum-of-inverse-squares", 391 | "problem-153-investigating-gaussian-integers", 392 | "problem-154-exploring-pascal's-pyramid", 393 | "problem-155-counting-capacitor-circuits", 394 | "problem-156-counting-digits", 395 | "problem-157-solving-the-diophantine-equation", 396 | "problem-158-exploring-strings-for-which-only-one-character-comes-lexicographically-after-its-neighbour-to-the-left", 397 | "problem-159-digital-root-sums-of-factorisations", 398 | "problem-160-factorial-trailing-digits", 399 | "problem-161-triominoes", 400 | "problem-162-hexadecimal-numbers", 401 | "problem-163-cross-hatched-triangles", 402 | "problem-164-numbers-for-which-no-three-consecutive-digits-have-a-sum-greater-than-a-given-value", 403 | "problem-165-intersections", 404 | "problem-166-criss-cross", 405 | "problem-167-investigating-ulam-sequences", 406 | "problem-168-number-rotations", 407 | "problem-169-exploring-the-number-of-different-ways-a-number-can-be-expressed-as-a-sum-of-powers-of-2", 408 | "problem-170-find-the-largest-0-to-9-pandigital-that-can-be-formed-by-concatenating-products", 409 | "problem-171-finding-numbers-for-which-the-sum-of-the-squares-of-the-digits-is-a-square", 410 | "problem-172-investigating-numbers-with-few-repeated-digits", 411 | "problem-173-using-up-to-one-million-tiles-how-many-different-hollow-square-laminae-can-be-formed?", 412 | "problem-174-counting-the-number-of-hollow-square-laminae-that-can-form-one,-two,-three,-...-distinct-arrangements", 413 | "problem-175-fractions-involving-the-number-of-different-ways-a-number-can-be-expressed-as-a-sum-of-powers-of-2", 414 | "problem-176-right-angled-triangles-that-share-a-cathetus", 415 | "problem-177-integer-angled-quadrilaterals", 416 | "problem-178-step-numbers", 417 | "problem-179-consecutive-positive-divisors", 418 | "problem-180-rational-zeros-of-a-function-of-three-variables", 419 | "problem-181-investigating-in-how-many-ways-objects-of-two-different-colours-can-be-grouped", 420 | "problem-182-rsa-encryption", 421 | "problem-183-maximum-product-of-parts", 422 | "problem-184-triangles-containing-the-origin", 423 | "problem-185-number-mind", 424 | "problem-186-connectedness-of-a-network", 425 | "problem-187-semiprimes", 426 | "problem-188-the-hyperexponentiation-of-a-number", 427 | "problem-189-tri-colouring-a-triangular-grid", 428 | "problem-190-maximising-a-weighted-product", 429 | "problem-191-prize-strings", 430 | "problem-192-best-approximations", 431 | "problem-193-squarefree-numbers", 432 | "problem-194-coloured-configurations", 433 | "problem-195-inscribed-circles-of-triangles-with-one-angle-of-60-degrees", 434 | "problem-196-prime-triplets", 435 | "problem-197-investigating-the-behaviour-of-a-recursively-defined-sequence", 436 | "problem-198-ambiguous-numbers", 437 | "problem-199-iterative-circle-packing", 438 | "problem-200-find-the-200th-prime-proof-sqube-containing-the-contiguous-sub-string-200", 439 | "problem-201-subsets-with-a-unique-sum", 440 | "problem-202-laserbeam", 441 | "problem-203-squarefree-binomial-coefficients", 442 | "problem-204-generalised-hamming-numbers", 443 | "problem-205-dice-game", 444 | "problem-206-concealed-square", 445 | "problem-207-integer-partition-equations", 446 | "problem-208-robot-walks", 447 | "problem-209-circular-logic", 448 | "problem-210-obtuse-angled-triangles", 449 | "problem-211-divisor-square-sum", 450 | "problem-212-combined-volume-of-cuboids", 451 | "problem-213-flea-circus", 452 | "problem-214-totient-chains", 453 | "problem-215-crack-free-walls", 454 | "problem-216-investigating-the-primality-of-numbers-of-the-form-2n2-1", 455 | "problem-217-balanced-numbers", 456 | "problem-218-perfect-right-angled-triangles", 457 | "problem-219-skew-cost-coding", 458 | "problem-220-heighway-dragon", 459 | "problem-221-alexandrian-integers", 460 | "problem-222-sphere-packing", 461 | "problem-223-almost-right-angled-triangles-i", 462 | "problem-224-almost-right-angled-triangles-ii", 463 | "problem-225-tribonacci-non-divisors", 464 | "problem-226-a-scoop-of-blancmange", 465 | "problem-227-the-chase", 466 | "problem-228-minkowski-sums", 467 | "problem-229-four-representations-using-squares", 468 | "problem-230-fibonacci-words", 469 | "problem-231-the-prime-factorisation-of-binomial-coefficients", 470 | "problem-232-the-race", 471 | "problem-233-lattice-points-on-a-circle", 472 | "problem-234-semidivisible-numbers", 473 | "problem-235-an-arithmetic-geometric-sequence", 474 | "problem-236-luxury-hampers", 475 | "problem-237-tours-on-a-4-x-n-playing-board", 476 | "problem-238-infinite-string-tour", 477 | "problem-239-twenty-two-foolish-primes", 478 | "problem-240-top-dice", 479 | "problem-241-perfection-quotients", 480 | "problem-242-odd-triplets", 481 | "problem-243-resilience", 482 | "problem-244-sliders", 483 | "problem-245-coresilience", 484 | "problem-246-tangents-to-an-ellipse", 485 | "problem-247-squares-under-a-hyperbola", 486 | "problem-248-numbers-for-which-euler’s-totient-function-equals-13!", 487 | "problem-249-prime-subset-sums", 488 | "problem-250-250250", 489 | "problem-251-cardano-triplets", 490 | "problem-252-convex-holes", 491 | "problem-253-tidying-up", 492 | "problem-254-sums-of-digit-factorials", 493 | "problem-255-rounded-square-roots", 494 | "problem-256-tatami-free-rooms", 495 | "problem-257-angular-bisectors", 496 | "problem-258-a-lagged-fibonacci-sequence", 497 | "problem-259-reachable-numbers", 498 | "problem-260-stone-game", 499 | "problem-261-pivotal-square-sums", 500 | "problem-262-mountain-range", 501 | "problem-263-an-engineers'-dream-come-true", 502 | "problem-264-triangle-centres", 503 | "problem-265-binary-circles", 504 | "problem-266-pseudo-square-root", 505 | "problem-267-billionaire", 506 | "problem-268-counting-numbers-with-at-least-four-distinct-prime-factors-less-than-100", 507 | "problem-269-polynomials-with-at-least-one-integer-root", 508 | "problem-270-cutting-squares", 509 | "problem-271-modular-cubes,-part-1", 510 | "problem-272-modular-cubes,-part-2", 511 | "problem-273-sum-of-squares", 512 | "problem-274-divisibility-multipliers", 513 | "problem-275-balanced-sculptures", 514 | "problem-276-primitive-triangles", 515 | "problem-277-a-modified-collatz-sequence", 516 | "problem-278-linear-combinations-of-semiprimes", 517 | "problem-279-triangles-with-integral-sides-and-an-integral-angle", 518 | "problem-280-ant-and-seeds", 519 | "problem-281-pizza-toppings", 520 | "problem-282-the-ackermann-function", 521 | "problem-283-integer-sided-triangles-for-which-the-area-*-perimeter-ratio-is-integral", 522 | "problem-284-steady-squares", 523 | "problem-285-pythagorean-odds", 524 | "problem-286-scoring-probabilities", 525 | "problem-287-quadtree-encoding-(a-simple-compression-algorithm)", 526 | "problem-288-an-enormous-factorial", 527 | "problem-289-eulerian-cycles", 528 | "problem-290-digital-signature", 529 | "problem-291-panaitopol-primes", 530 | "problem-292-pythagorean-polygons", 531 | "problem-293-pseudo-fortunate-numbers", 532 | "problem-294-sum-of-digits---experience-#23", 533 | "problem-295-lenticular-holes", 534 | "problem-296-angular-bisector-and-tangent", 535 | "problem-297-zeckendorf-representation", 536 | "problem-298-selective-amnesia", 537 | "problem-299-three-similar-triangles", 538 | "problem-300-protein-folding", 539 | "problem-301-nim", 540 | "problem-302-strong-achilles-numbers", 541 | "problem-303-multiples-with-small-digits", 542 | "problem-304-primonacci", 543 | "problem-305-reflexive-position", 544 | "problem-306-paper-strip-game", 545 | "problem-307-chip-defects", 546 | "problem-308-an-amazing-prime-generating-automaton", 547 | "problem-309-integer-ladders", 548 | "problem-310-nim-square", 549 | "problem-311-biclinic-integral-quadrilaterals", 550 | "problem-312-cyclic-paths-on-sierpiński-graphs", 551 | "problem-313-sliding-game", 552 | "problem-314-the-mouse-on-the-moon", 553 | "problem-315-digital-root-clocks", 554 | "problem-316-numbers-in-decimal-expansions", 555 | "problem-317-firecracker", 556 | "problem-318-2011-nines", 557 | "problem-319-bounded-sequences", 558 | "problem-320-factorials-divisible-by-a-huge-integer", 559 | "problem-321-swapping-counters", 560 | "problem-322-binomial-coefficients-divisible-by-10", 561 | "problem-323-bitwise-or-operations-on-random-integers", 562 | "problem-324-building-a-tower", 563 | "problem-325-stone-game-ii", 564 | "problem-326-modulo-summations", 565 | "problem-327-rooms-of-doom", 566 | "problem-328-lowest-cost-search", 567 | "problem-329-prime-frog", 568 | "problem-330-euler's-number", 569 | "problem-331-cross-flips", 570 | "problem-332-spherical-triangles", 571 | "problem-333-special-partitions", 572 | "problem-334-spilling-the-beans", 573 | "problem-335-gathering-the-beans", 574 | "problem-336-maximix-arrangements", 575 | "problem-337-totient-stairstep-sequences", 576 | "problem-338-cutting-rectangular-grid-paper", 577 | "problem-339-peredur-fab-efrawg", 578 | "problem-340-crazy-function", 579 | "problem-341-golomb's-self-describing-sequence", 580 | "problem-342-the-totient-of-a-square-is-a-cube", 581 | "problem-343-fractional-sequences", 582 | "problem-344-silver-dollar-game", 583 | "problem-345-matrix-sum", 584 | "problem-346-strong-repunits", 585 | "problem-347-largest-integer-divisible-by-two-primes", 586 | "problem-348-sum-of-a-square-and-a-cube", 587 | "problem-349-langton's-ant", 588 | "problem-350-constraining-the-least-greatest-and-the-greatest-least", 589 | "problem-351-hexagonal-orchards", 590 | "problem-352-blood-tests", 591 | "problem-353-risky-moon", 592 | "problem-354-distances-in-a-bee's-honeycomb", 593 | "problem-355-maximal-coprime-subset", 594 | "problem-356-largest-roots-of-cubic-polynomials", 595 | "problem-357-prime-generating-integers", 596 | "problem-358-cyclic-numbers", 597 | "problem-359-hilbert's-new-hotel", 598 | "problem-360-scary-sphere", 599 | "problem-361-subsequence-of-thue-morse-sequence", 600 | "problem-362-squarefree-factors", 601 | "problem-363-bézier-curves", 602 | "problem-364-comfortable-distance", 603 | "problem-365-a-huge-binomial-coefficient", 604 | "problem-366-stone-game-iii", 605 | "problem-367-bozo-sort", 606 | "problem-368-a-kempner-like-series", 607 | "problem-369-badugi", 608 | "problem-370-geometric-triangles", 609 | "problem-371-licence-plates", 610 | "problem-372-pencils-of-rays", 611 | "problem-373-circumscribed-circles", 612 | "problem-374-maximum-integer-partition-product", 613 | "problem-375-minimum-of-subsequences", 614 | "problem-376-nontransitive-sets-of-dice", 615 | "problem-377-sum-of-digits,-experience-13", 616 | "problem-378-triangle-triples", 617 | "problem-379-least-common-multiple-count", 618 | "problem-380-amazing-mazes!", 619 | "problem-381-(prime-k)-factorial", 620 | "problem-382-generating-polygons", 621 | "problem-383-divisibility-comparison-between-factorials", 622 | "problem-384-rudin-shapiro-sequence", 623 | "problem-385-ellipses-inside-triangles", 624 | "problem-386-maximum-length-of-an-antichain", 625 | "problem-387-harshad-numbers", 626 | "problem-388-distinct-lines", 627 | "problem-389-platonic-dice", 628 | "problem-390-triangles-with-non-rational-sides-and-integral-area", 629 | "problem-391-hopping-game", 630 | "problem-392-enmeshed-unit-circle", 631 | "problem-393-migrating-ants", 632 | "problem-394-eating-pie", 633 | "problem-395-pythagorean-tree", 634 | "problem-396-weak-goodstein-sequence", 635 | "problem-397-triangle-on-parabola", 636 | "problem-398-cutting-rope", 637 | "problem-399-squarefree-fibonacci-numbers", 638 | "problem-400-fibonacci-tree-game", 639 | "problem-401-sum-of-squares-of-divisors", 640 | "problem-402-integer-valued-polynomials", 641 | "problem-403-lattice-points-enclosed-by-parabola-and-line", 642 | "problem-404-crisscross-ellipses", 643 | "problem-405-a-rectangular-tiling", 644 | "problem-406-guessing-game", 645 | "problem-407-idempotents", 646 | "problem-408-admissible-paths-through-a-grid", 647 | "problem-409-nim-extreme", 648 | "problem-410-circle-and-tangent-line", 649 | "problem-411-uphill-paths", 650 | "problem-412-gnomon-numbering", 651 | "problem-413-one-child-numbers", 652 | "problem-414-kaprekar-constant", 653 | "problem-415-titanic-sets", 654 | "problem-416-a-frog's-trip", 655 | "problem-417-reciprocal-cycles-ii", 656 | "problem-418-factorisation-triples", 657 | "problem-419-look-and-say-sequence", 658 | "problem-420-2x2-positive-integer-matrix", 659 | "problem-421-prime-factors-of-n15+1", 660 | "problem-422-sequence-of-points-on-a-hyperbola", 661 | "problem-423-consecutive-die-throws", 662 | "problem-424-kakuro", 663 | "problem-425-prime-connection", 664 | "problem-426-box-ball-system", 665 | "problem-427-n-sequences", 666 | "problem-428-necklace-of-circles", 667 | "problem-429-sum-of-squares-of-unitary-divisors", 668 | "problem-430-range-flips", 669 | "problem-431-square-space-silo", 670 | "problem-432-totient-sum", 671 | "problem-433-steps-in-euclid's-algorithm", 672 | "problem-434-rigid-graphs", 673 | "problem-435-polynomials-of-fibonacci-numbers", 674 | "problem-436-unfair-wager", 675 | "problem-437-fibonacci-primitive-roots", 676 | "problem-438-integer-part-of-polynomial-equation's-solutions", 677 | "problem-439-sum-of-sum-of-divisors", 678 | "problem-440-gcd-and-tiling", 679 | "problem-441-the-inverse-summation-of-coprime-couples", 680 | "problem-442-eleven-free-integers", 681 | "problem-443-gcd-sequence", 682 | "problem-444-the-roundtable-lottery", 683 | "problem-445-retractions-a", 684 | "problem-446-retractions-b", 685 | "problem-447-retractions-c", 686 | "problem-448-average-least-common-multiple", 687 | "problem-449-chocolate-covered-candy", 688 | "problem-450-hypocycloid-and-lattice-points", 689 | "problem-451-modular-inverses", 690 | "problem-452-long-products", 691 | "problem-453-lattice-quadrilaterals", 692 | "problem-454-diophantine-reciprocals-iii", 693 | "problem-455-powers-with-trailing-digits", 694 | "problem-456-triangles-containing-the-origin-ii", 695 | "problem-457-a-polynomial-modulo-the-square-of-a-prime", 696 | "problem-458-permutations-of-project", 697 | "problem-459-flipping-game", 698 | "problem-460-an-ant-on-the-move", 699 | "problem-461-almost-pi", 700 | "problem-462-permutation-of-3-smooth-numbers", 701 | "problem-463-a-weird-recurrence-relation", 702 | "problem-464-möbius-function-and-intervals", 703 | "problem-465-polar-polygons", 704 | "problem-466-distinct-terms-in-a-multiplication-table", 705 | "problem-467-superinteger", 706 | "problem-468-smooth-divisors-of-binomial-coefficients", 707 | "problem-469-empty-chairs", 708 | "problem-470-super-ramvok", 709 | "problem-471-triangle-inscribed-in-ellipse", 710 | "problem-472-comfortable-distance-ii", 711 | "problem-473-phigital-number-base", 712 | "problem-474-last-digits-of-divisors", 713 | "problem-475-music-festival", 714 | "problem-476-circle-packing-ii", 715 | "problem-477-number-sequence-game", 716 | "problem-478-mixtures", 717 | "problem-479-roots-on-the-rise", 718 | "problem-480-the-last-question", 719 | ], 720 | }, 721 | ], 722 | }, 723 | ]; 724 | --------------------------------------------------------------------------------