├── .dockerignore ├── .github └── workflows │ └── docker.yml ├── .gitignore ├── .prettierrc ├── CODEOWNERS ├── Dockerfile ├── README.md ├── commands ├── 24game.js ├── 24parser.js ├── 24plus.js ├── admin-standup.js ├── admin.js ├── anonymouspost.js ├── coinFlip.js ├── connect4.js ├── course.js ├── courseRating.js ├── createvc.js ├── csesocLinks.js ├── faq.js ├── faqadmin.js ├── handbook.js ├── help.js ├── joke.js ├── logreport.js ├── meetingtools.js ├── ping.js ├── project-descriptions.js ├── reactforrole.js ├── remind.js ├── rolesPermOverride.js ├── schedulepost.js ├── tictactoe.js ├── travelguide.js ├── vote.js ├── whatweekisit.js ├── wordle.js └── xkcd.js ├── config ├── anon_channel.json ├── carrotboard.yaml ├── cointoss_images │ ├── heads.png │ └── tails.png ├── database.yml ├── handbook.json ├── help.json ├── lunch_buddy.json ├── lunch_buddy_locations.json ├── votes.json ├── wordle.json ├── wordle_images │ ├── blank_box.png │ ├── clear_box.png │ ├── empty_box.png │ ├── green_box.png │ ├── grey_box.png │ └── yellow_box.png ├── words.json └── words_clean.txt ├── data ├── createvc.json └── log_report.csv ├── deploy-commands.js ├── entrypoint.sh ├── eslint.config.mjs ├── events ├── cb_messageDelete.js ├── cb_reactionAdd.js ├── cb_reactionRemove.js ├── cb_reactionRemoveAll.js ├── cb_ready.js ├── channelCreate.js ├── channelDelete.js ├── channelUpdate.js ├── coderunner.js ├── course_removeWrongCommand.js ├── createvc.js ├── db_ready.js ├── faq_ready.js ├── givereactrole.js ├── guildMemberAdd.js ├── guildMemberRemove.js ├── log_ready.js ├── lunch_buddy.js ├── messageCreate.js ├── messageDelete.js ├── messageUpdate.js ├── reactrole_read.js ├── ready.js ├── removereactrole.js ├── role_removeWrongCommand.js ├── schedulepost_ready.js ├── sendscheduled.js ├── sendscheduled_reminders.js ├── standupReset.js ├── standup_ready.js ├── tictactoeButton.js └── travelguide_ready.js ├── index.js ├── lib ├── carrotboard │ └── index.js ├── connect4 │ ├── connect4Game.js │ └── connect4Runner.js ├── database │ ├── database.js │ ├── dbcarrotboard.js │ ├── dblog.js │ ├── dbreactrole.js │ ├── dbschedulepost.js │ ├── dbstandup.js │ ├── dbtravelguide.js │ └── faq.js ├── discordscroll │ └── scroller.js └── tictactoe │ ├── tttGame.js │ └── tttHelper.js ├── package-lock.json ├── package.json └── renovate.json /.dockerignore: -------------------------------------------------------------------------------- 1 | ## Visual Studio Code files: 2 | .vscode/* 3 | # !.vscode/settings.json 4 | # !.vscode/tasks.json 5 | # !.vscode/launch.json 6 | # !.vscode/extensions.json 7 | *.code-workspace 8 | 9 | # Local History for Visual Studio Code 10 | .history/ 11 | 12 | ## Node.js files: 13 | # Logs 14 | logs 15 | *.log 16 | npm-debug.log* 17 | yarn-debug.log* 18 | yarn-error.log* 19 | lerna-debug.log* 20 | .pnpm-debug.log* 21 | 22 | # Diagnostic reports (https://nodejs.org/api/report.html) 23 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 24 | 25 | # Runtime data 26 | pids 27 | *.pid 28 | *.seed 29 | *.pid.lock 30 | 31 | # Directory for instrumented libs generated by jscoverage/JSCover 32 | lib-cov 33 | 34 | # Coverage directory used by tools like istanbul 35 | coverage 36 | *.lcov 37 | 38 | # nyc test coverage 39 | .nyc_output 40 | 41 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 42 | .grunt 43 | 44 | # Bower dependency directory (https://bower.io/) 45 | bower_components 46 | 47 | # node-waf configuration 48 | .lock-wscript 49 | 50 | # Compiled binary addons (https://nodejs.org/api/addons.html) 51 | build/Release 52 | 53 | # Dependency directories 54 | node_modules/ 55 | jspm_packages/ 56 | 57 | # Snowpack dependency directory (https://snowpack.dev/) 58 | web_modules/ 59 | 60 | # TypeScript cache 61 | *.tsbuildinfo 62 | 63 | # Optional npm cache directory 64 | .npm 65 | 66 | # Optional eslint cache 67 | .eslintcache 68 | 69 | # Microbundle cache 70 | .rpt2_cache/ 71 | .rts2_cache_cjs/ 72 | .rts2_cache_es/ 73 | .rts2_cache_umd/ 74 | 75 | # Optional REPL history 76 | .node_repl_history 77 | 78 | # Output of 'npm pack' 79 | *.tgz 80 | 81 | # Yarn Integrity file 82 | .yarn-integrity 83 | 84 | # dotenv environment variables file 85 | .env 86 | .env.test 87 | .env.production 88 | 89 | # parcel-bundler cache (https://parceljs.org/) 90 | .cache 91 | .parcel-cache 92 | 93 | # Next.js build output 94 | .next 95 | out 96 | 97 | # Nuxt.js build / generate output 98 | .nuxt 99 | dist 100 | 101 | # Gatsby files 102 | .cache/ 103 | # Comment in the public line in if your project uses Gatsby and not Next.js 104 | # https://nextjs.org/blog/next-9-1#public-directory-support 105 | # public 106 | 107 | # vuepress build output 108 | .vuepress/dist 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Build projects-bot 2 | 3 | on: 4 | push: 5 | 6 | jobs: 7 | format-lint-check: 8 | name: "Format & lint check" 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout repository 12 | uses: actions/checkout@v4 13 | - uses: actions/setup-node@v4 14 | with: 15 | node-version: 20 16 | - run: npm ci 17 | - run: npm run format:check 18 | - run: npm run lint 19 | build: 20 | name: "Build" 21 | runs-on: ubuntu-latest 22 | needs: ["format-lint-check"] 23 | permissions: 24 | contents: read 25 | packages: write 26 | steps: 27 | - name: Checkout repository 28 | uses: actions/checkout@v4 29 | - name: Set up QEMU 30 | uses: docker/setup-qemu-action@v2 31 | with: 32 | platforms: arm64 33 | - name: Set up Docker Buildx 34 | uses: docker/setup-buildx-action@v2 35 | - name: Log into registry ${{ env.REGISTRY }} 36 | uses: docker/login-action@v2 37 | with: 38 | registry: ghcr.io 39 | username: ${{ github.actor }} 40 | password: ${{ secrets.GH_TOKEN }} 41 | - name: Build and push Docker image 42 | uses: docker/build-push-action@v4 43 | with: 44 | context: . 45 | push: ${{ github.event_name != 'pull_request' && ( github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/projects-bot' ) }} 46 | platforms: linux/amd64 47 | file: Dockerfile 48 | tags: | 49 | ghcr.io/csesoc/projects-discord-bot:${{ github.sha }} 50 | ghcr.io/csesoc/projects-discord-bot:latest 51 | labels: ${{ steps.meta.outputs.labels }} 52 | deploy: 53 | name: Deploy (CD) 54 | runs-on: ubuntu-latest 55 | needs: [build] 56 | if: ${{ github.event_name != 'pull_request' && ( github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/projects-bot' ) }} 57 | concurrency: production 58 | environment: 59 | name: production (projects-bot) 60 | steps: 61 | - name: Checkout repository 62 | uses: actions/checkout@v4 63 | with: 64 | repository: csesoc/deployment 65 | token: ${{ secrets.GH_TOKEN }} 66 | - name: Install yq - portable yaml processor 67 | uses: mikefarah/yq@v4.44.2 68 | - name: Determine file to update 69 | id: get_manifest 70 | env: 71 | BRANCH_NAME: ${{ github.ref }} 72 | run: | 73 | if [ "$BRANCH_NAME" = "refs/heads/projects-bot" ]; then 74 | echo "MANIFEST=projects/bot/ptb/deploy.yml" >> $GITHUB_OUTPUT 75 | echo "DEPLOYMENT=ptb" >> $GITHUB_OUTPUT 76 | elif [ "$BRANCH_NAME" = "refs/heads/develop" ]; then 77 | echo "MANIFEST=projects/bot/qa/deploy.yml" >> $GITHUB_OUTPUT 78 | echo "DEPLOYMENT=qa" >> $GITHUB_OUTPUT 79 | else 80 | exit 1 81 | fi 82 | - name: Update deployment 83 | env: 84 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} 85 | run: | 86 | git config user.name "CSESoc CD" 87 | git config user.email "technical@csesoc.org.au" 88 | git checkout -b update/projects-bot/${{ github.sha }} 89 | yq -i '.spec.template.spec.containers[0].image = "ghcr.io/csesoc/projects-discord-bot:${{ github.sha }}"' ${{ steps.get_manifest.outputs.MANIFEST }} 90 | git add . 91 | git commit -m "feat(projects-bot/${{ steps.get_manifest.outputs.DEPLOYMENT }}): update images" 92 | git push -u origin update/projects-bot/${{ github.sha }} 93 | gh pr create --title "feat(projects-bot/${{ steps.get_manifest.outputs.DEPLOYMENT }}): update images" --body "Updates the images for the projects-bot deployment to commit csesoc/discord-bot@${{ github.sha }}." > URL 94 | gh pr merge $(cat URL) --squash -d 95 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Visual Studio Code files: 2 | .vscode/* 3 | # !.vscode/settings.json 4 | # !.vscode/tasks.json 5 | # !.vscode/launch.json 6 | # !.vscode/extensions.json 7 | *.code-workspace 8 | 9 | # Local History for Visual Studio Code 10 | .history/ 11 | 12 | ## Node.js files: 13 | # Logs 14 | logs 15 | *.log 16 | npm-debug.log* 17 | yarn-debug.log* 18 | yarn-error.log* 19 | lerna-debug.log* 20 | .pnpm-debug.log* 21 | 22 | # Diagnostic reports (https://nodejs.org/api/report.html) 23 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 24 | 25 | # Runtime data 26 | pids 27 | *.pid 28 | *.seed 29 | *.pid.lock 30 | 31 | # Directory for instrumented libs generated by jscoverage/JSCover 32 | lib-cov 33 | 34 | # Coverage directory used by tools like istanbul 35 | coverage 36 | *.lcov 37 | 38 | # nyc test coverage 39 | .nyc_output 40 | 41 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 42 | .grunt 43 | 44 | # Bower dependency directory (https://bower.io/) 45 | bower_components 46 | 47 | # node-waf configuration 48 | .lock-wscript 49 | 50 | # Compiled binary addons (https://nodejs.org/api/addons.html) 51 | build/Release 52 | 53 | # Dependency directories 54 | node_modules/ 55 | jspm_packages/ 56 | 57 | # Snowpack dependency directory (https://snowpack.dev/) 58 | web_modules/ 59 | 60 | # TypeScript cache 61 | *.tsbuildinfo 62 | 63 | # Optional npm cache directory 64 | .npm 65 | 66 | # Optional eslint cache 67 | .eslintcache 68 | 69 | # Microbundle cache 70 | .rpt2_cache/ 71 | .rts2_cache_cjs/ 72 | .rts2_cache_es/ 73 | .rts2_cache_umd/ 74 | 75 | # Optional REPL history 76 | .node_repl_history 77 | 78 | # Output of 'npm pack' 79 | *.tgz 80 | 81 | # Yarn Integrity file 82 | .yarn-integrity 83 | 84 | # dotenv environment variables file 85 | .env 86 | .env.test 87 | .env.production 88 | 89 | # parcel-bundler cache (https://parceljs.org/) 90 | .cache 91 | .parcel-cache 92 | 93 | # Next.js build output 94 | .next 95 | out 96 | 97 | # Nuxt.js build / generate output 98 | .nuxt 99 | dist 100 | 101 | # Gatsby files 102 | .cache/ 103 | # Comment in the public line in if your project uses Gatsby and not Next.js 104 | # https://nextjs.org/blog/next-9-1#public-directory-support 105 | # public 106 | 107 | # vuepress build output 108 | .vuepress/dist 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | #database config file 133 | config/database.yml -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "printWidth": 100, 4 | "semi": true, 5 | "trailingComma": "all", 6 | "singleQuote": false 7 | } 8 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | commands/ @csesoc/discord-bot-leads 2 | config/ @csesoc/discord-bot-leads 3 | data/ @csesoc/discord-bot-leads 4 | events/ @csesoc/discord-bot-leads 5 | lib/ @csesoc/discord-bot-leads 6 | 7 | .github/ @csesoc/technical 8 | renovate.json @csesoc/technical 9 | Dockerfile @csesoc/technical 10 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build layer template for an eventual TS migration 2 | FROM node:20.15.0-slim AS builder 3 | ENV NODE_ENV=production 4 | 5 | # Set working directory 6 | WORKDIR /app 7 | 8 | # Install dependencies 9 | COPY package.json package-lock.json ./ 10 | RUN npm ci --omit=dev 11 | 12 | FROM ghcr.io/puppeteer/puppeteer:22.12.1 13 | ENV NODE_ENV=production 14 | 15 | USER root 16 | 17 | # Set working directory 18 | WORKDIR /app 19 | 20 | # Copy dependencies 21 | COPY --from=builder /app/node_modules ./node_modules 22 | 23 | # Copy bot files 24 | COPY . . 25 | 26 | RUN chmod +x entrypoint.sh 27 | 28 | USER pptruser 29 | 30 | # Run bot 31 | ENTRYPOINT [ "./entrypoint.sh" ] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CSESoc Discord Bot 2 | 3 | ## Installation 4 | 5 | - Install Node.js and npm from https://nodejs.org/en/download/ 6 | - Clone the repository with `git clone https://github.com/csesoc/discord-bot` 7 | - Go to `.env` and fill in 8 | - `DISCORD_TOKEN` with the token of the bot 9 | - `APP_ID` with the ID of the bot application 10 | - Install dependencies with `npm install` 11 | - Register slash commands with `npm run deploy` or `node deploy-commands.js` 12 | - Ensure a PostgreSQL database is setup according to "config/database.yml" 13 | - Start the bot with `node index.js` 14 | 15 | ## Running the bot with Nodemon 16 | 17 | - Nodemon has been installed, this addition allows for continuous integration with and hot reloads the bot upon saving. 18 | - Run the bot with Nodemon using `npm run server` -------------------------------------------------------------------------------- /commands/24game.js: -------------------------------------------------------------------------------- 1 | const { SlashCommandBuilder } = require("@discordjs/builders"); 2 | 3 | const MAX = 9; 4 | 5 | module.exports = { 6 | data: new SlashCommandBuilder() 7 | .setName("24") 8 | .setDescription("Generates 4 random numbers from 0 to 9!"), 9 | async execute(interaction) { 10 | const resultNums = []; 11 | 12 | for (let i = 0; i < 4; i++) { 13 | const random = Math.round(Math.random() * MAX); 14 | resultNums.push(random); 15 | } 16 | 17 | const output = `Your numbers are: ${resultNums.join(" ")}`; 18 | 19 | await interaction.reply(output); 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /commands/24parser.js: -------------------------------------------------------------------------------- 1 | const { SlashCommandBuilder } = require("@discordjs/builders"); 2 | const math = require("mathjs"); 3 | const { Util } = require("discord.js"); 4 | 5 | const illegalPhraseRegexes = [/`/g, /@/g]; 6 | 7 | const isIllegalCharactersPresent = (expression) => { 8 | return illegalPhraseRegexes.some((regex) => regex.test(expression)); 9 | }; 10 | 11 | const tryCompileAndEvaluate = (eqnString) => { 12 | try { 13 | const equationObj = math.compile(eqnString); 14 | if (!equationObj) { 15 | throw Error; 16 | } 17 | 18 | const equationOutcome = equationObj.evaluate(); 19 | 20 | return { 21 | success: true, 22 | equationOutcome, 23 | }; 24 | } catch (e) { 25 | return { 26 | success: false, 27 | message: "Could not compile. The equation is invalid.", 28 | ephemeral: true, 29 | }; 30 | } 31 | }; 32 | 33 | const evaluate = (equationString, target) => { 34 | if (isIllegalCharactersPresent(equationString)) { 35 | return { 36 | success: false, 37 | message: "Could not compile. Illegal input detected.", 38 | ephemeral: true, 39 | }; 40 | } 41 | 42 | const evaluationOutcome = tryCompileAndEvaluate(equationString); 43 | if (!evaluationOutcome.success) { 44 | return { 45 | success: false, 46 | message: evaluationOutcome.message, 47 | ephemeral: true, 48 | }; 49 | } 50 | const { equationOutcome } = evaluationOutcome; 51 | 52 | const outcomeAsNumber = Number(equationOutcome); 53 | if (math.isNaN(outcomeAsNumber)) { 54 | return { 55 | success: false, 56 | message: "Could not compile. The equation does not evaluate to a number.", 57 | ephemeral: true, 58 | }; 59 | } 60 | 61 | return outcomeAsNumber == target 62 | ? { 63 | success: true, 64 | message: `Correct! \`${equationString}\` = ${target}, which is equal to the target.`, 65 | ephemeral: false, 66 | } 67 | : { 68 | success: false, 69 | message: `Incorrect. \`${equationString}\` = ${outcomeAsNumber}, which is not equal to the target of ${target}.`, 70 | ephemeral: false, 71 | }; 72 | }; 73 | 74 | module.exports = { 75 | data: new SlashCommandBuilder() 76 | .setName("24parse") 77 | .setDescription("Checks whether an equation evaluates to 24 (or a number input)!") 78 | .addStringOption((option) => 79 | option.setName("equation").setDescription("Equation for the 24 game").setRequired(true), 80 | ) 81 | .addNumberOption((option) => 82 | option.setName("target").setDescription("Target for your equation").setRequired(false), 83 | ), 84 | async execute(interaction) { 85 | const equationStr = interaction.options.getString("equation"); 86 | const target = interaction.options.getNumber("target") || 24; 87 | 88 | const { success, message, ephemeral } = evaluate(equationStr, target); 89 | 90 | const emoji = success ? "✅" : "❌"; 91 | const output = `${emoji} ${message}`; 92 | 93 | await interaction.reply({ 94 | content: Util.removeMentions(output), 95 | ephemeral, 96 | }); 97 | }, 98 | }; 99 | -------------------------------------------------------------------------------- /commands/24plus.js: -------------------------------------------------------------------------------- 1 | const { SlashCommandBuilder } = require("@discordjs/builders"); 2 | 3 | const MAX = 11; 4 | const MAX_TARGET = 99; 5 | 6 | module.exports = { 7 | data: new SlashCommandBuilder() 8 | .setName("24plus") 9 | .setDescription( 10 | "Generates 4 random numbers from 1 to 12 and a random target from 1 to 100.", 11 | ), 12 | async execute(interaction) { 13 | const resultNums = []; 14 | 15 | for (let i = 0; i < 4; i++) { 16 | const random = Math.round(Math.random() * MAX) + 1; 17 | resultNums.push(random); 18 | } 19 | 20 | const target = Math.round(Math.random() * MAX_TARGET) + 1; 21 | 22 | const output = `Your numbers are: ${resultNums.join(" ")}, with a target of ${target}`; 23 | 24 | await interaction.reply(output); 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /commands/admin-standup.js: -------------------------------------------------------------------------------- 1 | const { SlashCommandBuilder } = require("@discordjs/builders"); 2 | const { EmbedBuilder, ButtonBuilder, PermissionsBitField } = require("discord.js"); 3 | const paginationEmbed = require("discordjs-button-pagination"); 4 | 5 | module.exports = { 6 | data: new SlashCommandBuilder() 7 | .setName("standupstatus") 8 | .setDescription("Get standups [ADMIN]") 9 | .addSubcommand((subcommand) => 10 | subcommand 11 | .setName("getfullstandups") 12 | .setDescription("Returns all standups") 13 | .addMentionableOption((option) => 14 | option 15 | .setName("teamrole") 16 | .setDescription("Mention the team role (@team-role)") 17 | .setRequired(true), 18 | ) 19 | .addIntegerOption((option) => 20 | option 21 | .setName("days") 22 | .setDescription("Number of days in past to retrieve standups from") 23 | .setRequired(false), 24 | ), 25 | ), 26 | 27 | async execute(interaction) { 28 | const standupDB = global.standupDBGlobal; 29 | const TEAM_DIRECTOR_ROLE_ID = "921348676692107274"; 30 | if ( 31 | !interaction.member.permissions.has(PermissionsBitField.Flags.Administrator) && 32 | !interaction.member._roles.includes(TEAM_DIRECTOR_ROLE_ID) 33 | ) { 34 | return await interaction.reply({ 35 | content: "You do not have permission to execute this command.", 36 | ephemeral: true, 37 | }); 38 | } 39 | if (interaction.options.getSubcommand() === "getfullstandups") { 40 | // var teamName = await interaction.options.getString('team'); 41 | let sendmsg = ""; 42 | 43 | try { 44 | const team = await interaction.options.getMentionable("teamrole"); 45 | const numDaysToRetrieve = (await interaction.options.getInteger("days")) ?? 7; 46 | const teamRoleID = team.id; 47 | const role = await interaction.guild.roles.fetch(teamRoleID); 48 | /*eslint-disable */ 49 | var roleMembers = [...role.members?.values()]; 50 | /* eslint-enable */ 51 | const ON_BREAK_ID = "1036905668352942090"; 52 | roleMembers = roleMembers.filter((rm) => !rm._roles.includes(ON_BREAK_ID)); 53 | const thisTeamId = interaction.channel.parentId; 54 | let thisTeamStandups = await standupDB.getStandups(thisTeamId, numDaysToRetrieve); 55 | 56 | const roleNames = {}; 57 | roleMembers.forEach((el) => { 58 | const author = el.user.username; 59 | /* let author = el.nickname; 60 | if (author == undefined) { 61 | author = el.user.username; 62 | }*/ 63 | roleNames[el.user.id] = author; 64 | }); 65 | 66 | thisTeamStandups = thisTeamStandups.filter((st) => 67 | Object.keys(roleNames).includes(st.user_id), 68 | ); 69 | 70 | const standupDone = []; 71 | const standupEmbeded = []; 72 | // add all standups 73 | thisTeamStandups.forEach((standUp) => { 74 | standupDone.push(standUp.user_id); 75 | standupEmbeded.push( 76 | "**" + 77 | `${roleNames[standUp.user_id]}` + 78 | "**" + 79 | "\n" + 80 | standUp.standup_content, 81 | ); 82 | sendmsg += 83 | "**" + 84 | `${roleNames[standUp.user_id]}` + 85 | "**" + 86 | "\n" + 87 | standUp.standup_content; 88 | sendmsg += "\n"; 89 | }); 90 | 91 | const notDone = []; 92 | 93 | roleMembers.forEach((el) => { 94 | const id = el.user.id; 95 | if (!standupDone.includes(id)) { 96 | notDone.push(id); 97 | } 98 | }); 99 | 100 | let notDoneUsersString = ""; 101 | notDoneUsersString = notDone.map((el) => `<@${el}>`).join(", "); 102 | 103 | const embedList = []; 104 | if (notDone.length == 0) { 105 | standupEmbeded.forEach((el) => { 106 | embedList.push( 107 | new EmbedBuilder() 108 | .setTitle("Standups (" + role.name + ")") 109 | .setDescription( 110 | el + "\n\n" + "_Everyone has done their standup_\n", 111 | ), 112 | ); 113 | }); 114 | } else { 115 | standupEmbeded.forEach((el) => { 116 | embedList.push( 117 | new EmbedBuilder() 118 | .setTitle("Standups (" + role.name + ")") 119 | .setDescription( 120 | el + 121 | "\n\n" + 122 | "_These users have not done their standup:_\n" + 123 | notDoneUsersString, 124 | ), 125 | ); 126 | }); 127 | } 128 | 129 | if (thisTeamStandups.length == 0) { 130 | const embed = new EmbedBuilder() 131 | .setTitle("Standups (" + role.name + ")") 132 | .setDescription( 133 | "No standups recorded\n\n" + 134 | "_These users have not done their standup:_\n" + 135 | notDoneUsersString, 136 | ); 137 | return await interaction.reply({ embeds: [embed] }); 138 | } 139 | 140 | const buttonList = [ 141 | new ButtonBuilder() 142 | .setCustomId("previousbtn") 143 | .setLabel("Previous") 144 | .setStyle("DANGER"), 145 | new ButtonBuilder().setCustomId("nextbtn").setLabel("Next").setStyle("SUCCESS"), 146 | ]; 147 | 148 | paginationEmbed(interaction, embedList, buttonList); 149 | 150 | // sendmsg += "\n" + "These users have not done their standup:\n" + notDoneUsersString; 151 | // await interaction.reply(sendmsg); 152 | } catch (error) { 153 | sendmsg = "An error - " + error; 154 | await interaction.reply({ content: sendmsg, ephemeral: true }); 155 | } 156 | } 157 | /* else if (interaction.options.getSubcommand() === "resetstandups") { 158 | try { 159 | await standupDB.deleteAllStandups(); 160 | await interaction.reply({ 161 | content: "Standups reset", 162 | ephemeral: true, 163 | }); 164 | } catch (e) { 165 | await interaction.reply({ 166 | content: `Error when resetting standups:${e}`, 167 | ephemeral: true, 168 | }); 169 | } 170 | }*/ 171 | }, 172 | }; 173 | -------------------------------------------------------------------------------- /commands/admin.js: -------------------------------------------------------------------------------- 1 | const { SlashCommandBuilder } = require("@discordjs/builders"); 2 | const { PermissionsBitField } = require("discord.js"); 3 | 4 | const COMMAND_KICKUNVERIFIED = "kickunverified"; 5 | const COMMAND_DROPUSERTABLE = "dropusertable"; 6 | 7 | module.exports = { 8 | data: new SlashCommandBuilder() 9 | .setName("admin") 10 | .setDescription("Admin-only commands.") 11 | .addSubcommand((subcommand) => 12 | subcommand 13 | .setName(COMMAND_KICKUNVERIFIED) 14 | .setDescription("Kicks all unverified users from the server."), 15 | ) 16 | .addSubcommand((subcommand) => 17 | subcommand 18 | .setName(COMMAND_DROPUSERTABLE) 19 | .setDescription("Deletes the user table and reliant tables."), 20 | ), 21 | async execute(interaction) { 22 | try { 23 | if (!interaction.member.permissions.has(PermissionsBitField.Flags.Administrator)) { 24 | return await interaction.reply({ 25 | content: "You do not have permission to execute this command.", 26 | ephemeral: true, 27 | }); 28 | } 29 | 30 | if (interaction.options.getSubcommand() === COMMAND_KICKUNVERIFIED) { 31 | const role = await interaction.guild.roles.cache.find( 32 | (r) => r.name.toLowerCase() === "unverified", 33 | ); 34 | 35 | // Make sure that the "unverified" role exists 36 | if (role === undefined) { 37 | return await interaction.reply('Error: no "unverified" role exists.'); 38 | } 39 | 40 | const kickMessage = 41 | "You have been removed from the CSESoc Server as you have not verified via the instructions in #welcome.\ 42 | If you wish to rejoin, visit https://cseso.cc/discord"; 43 | 44 | // Member list in the role is cached 45 | let numRemoved = 0; 46 | await role.members.each((member) => { 47 | member.createDM().then((DMChannel) => { 48 | // Send direct message to user being kicked 49 | DMChannel.send(kickMessage).then(() => { 50 | // Message sent, time to kick. 51 | member 52 | .kick(kickMessage) 53 | .then(() => { 54 | ++numRemoved; 55 | console.log(numRemoved + " people removed."); 56 | }) 57 | .catch((e) => { 58 | console.log(e); 59 | }); 60 | }); 61 | }); 62 | }); 63 | return await interaction.reply("Removed unverified members."); 64 | } else if (interaction.options.getSubcommand() === COMMAND_DROPUSERTABLE) { 65 | const userDB = global.userDB; 66 | 67 | await userDB.deleteUsers(); 68 | await userDB.create_table_users(); 69 | 70 | return await interaction.reply("Deleted user table."); 71 | } 72 | 73 | return await interaction.reply("Error: unknown subcommand."); 74 | } catch (error) { 75 | await interaction.reply("Error: " + error); 76 | } 77 | }, 78 | }; 79 | -------------------------------------------------------------------------------- /commands/coinFlip.js: -------------------------------------------------------------------------------- 1 | const { SlashCommandBuilder } = require("@discordjs/builders"); 2 | const { EmbedBuilder } = require("discord.js"); 3 | 4 | module.exports = { 5 | data: new SlashCommandBuilder().setName("coinflip").setDescription("Tosses a coin 💰"), 6 | async execute(interaction) { 7 | const coinNum = await Math.floor(Math.random() * 2); 8 | const coin = coinNum === 0 ? "heads" : "tails"; 9 | /* 10 | let img = 11 | coinNum === 0 12 | ? 'https://assets.gadgets360cdn.com/img/crypto/dogecoin-og-logo.png' 13 | : 'http://assets.stickpng.com/thumbs/5a521f522f93c7a8d5137fc7.png'; 14 | */ 15 | const img = coinNum === 0 ? "attachment://heads.png" : "attachment://tails.png"; 16 | const embed = new EmbedBuilder().setTitle(`it's ${coin}!`).setImage(img); 17 | if (coinNum == 0) { 18 | return await interaction.reply({ 19 | embeds: [embed], 20 | files: ["./config/cointoss_images/heads.png"], 21 | }); 22 | } else { 23 | return await interaction.reply({ 24 | embeds: [embed], 25 | files: ["./config/cointoss_images/tails.png"], 26 | }); 27 | } 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /commands/connect4.js: -------------------------------------------------------------------------------- 1 | const { SlashCommandBuilder } = require("@discordjs/builders"); 2 | const { createConnect4 } = require("../lib/connect4/connect4Runner"); 3 | 4 | const baseCommand = new SlashCommandBuilder() 5 | .setName("connect4") 6 | .setDescription("Start a game of connect 4"); 7 | 8 | module.exports = { 9 | data: baseCommand, 10 | execute: createConnect4, 11 | }; 12 | -------------------------------------------------------------------------------- /commands/courseRating.js: -------------------------------------------------------------------------------- 1 | const { SlashCommandBuilder } = require("@discordjs/builders"); 2 | const { EmbedBuilder, AttachmentBuilder } = require("discord.js"); 3 | const { ChartJSNodeCanvas } = require("chartjs-node-canvas"); 4 | const puppeteer = require("puppeteer"); 5 | 6 | /** 7 | * Extracts the relevant information from the course page 8 | */ 9 | async function extractRating(url) { 10 | const browser = await puppeteer.launch({ 11 | headless: true, 12 | }); 13 | 14 | const page = await browser.newPage(); 15 | await page.goto(url, { waitUntil: "networkidle2" }); 16 | 17 | const courseTitle = await page.$eval( 18 | "h2.text-3xl.font-bold.break-words", 19 | (el) => el.textContent, 20 | ); 21 | const numReviews = await page.$eval(".space-x-2 > span", (el) => el.textContent); 22 | const ratings = await page.$$eval(".flex.flex-wrap.justify-around > div", (items) => { 23 | const result = []; 24 | items.slice(0, 3).forEach((el) => { 25 | const rating = el.querySelector(".text-2xl.font-bold").textContent; 26 | const category = el.querySelector(".text-center.font-bold").textContent; 27 | result.push({ 28 | name: category, 29 | value: `${rating} out of 5`, 30 | inline: true, 31 | }); 32 | }); 33 | return result; 34 | }); 35 | 36 | const fullDescription = await page.$eval(".whitespace-pre-line", (el) => el.textContent); 37 | const description = fullDescription.split(/(?<=[.!?])\s/)[0].trim(); 38 | 39 | await browser.close(); 40 | return { courseTitle, numReviews, description, ratings }; 41 | } 42 | 43 | /** 44 | * Determines the color code based on the given rating. 45 | * 46 | * @param {number} rating - The rating value to evaluate. 47 | * @returns {string} - The corresponding color code in hexadecimal format. 48 | * 49 | */ 50 | function ratingColour(rating) { 51 | if (rating >= 3.5) { 52 | return "#39e75f"; 53 | } else if (rating > 2.5) { 54 | return "#FFA500"; 55 | } 56 | return "#FF0000"; 57 | } 58 | 59 | /** 60 | * Builds a doughnut chart representing the average rating from a list of ratings. 61 | * 62 | * @param {Array} ratings - An array of rating objects 63 | * @returns {Promise} - An image buffer of doughnut chart 64 | */ 65 | async function buildChart(ratings) { 66 | const width = 800; 67 | const height = 300; 68 | const averageRating = 69 | ratings.reduce((sum, rating) => { 70 | return sum + parseFloat(rating.value.split(" ")[0]); 71 | }, 0) / ratings.length; 72 | 73 | const canvas = new ChartJSNodeCanvas({ width, height }); 74 | 75 | const config = { 76 | type: "doughnut", 77 | data: { 78 | datasets: [ 79 | { 80 | data: [averageRating, 5 - averageRating], 81 | backgroundColor: [ratingColour(averageRating), "#e0e0e0"], 82 | borderJoinStyle: "round", 83 | borderRadius: [ 84 | { 85 | outerStart: 20, 86 | innerStart: 20, 87 | }, 88 | { 89 | outerEnd: 20, 90 | innerEnd: 20, 91 | }, 92 | ], 93 | borderWidth: 0, 94 | }, 95 | ], 96 | }, 97 | options: { 98 | rotation: 290, 99 | circumference: 140, 100 | cutout: "88%", 101 | plugins: { 102 | legend: { 103 | display: false, 104 | }, 105 | }, 106 | }, 107 | }; 108 | 109 | const image = await canvas.renderToBuffer(config); 110 | return image; 111 | } 112 | 113 | module.exports = { 114 | data: new SlashCommandBuilder() 115 | .setName("courserating") 116 | .setDescription("Tells you the current rating of a specific course!") 117 | .addStringOption((option) => 118 | option.setName("course").setDescription("Enter the course code").setRequired(true), 119 | ), 120 | async execute(interaction) { 121 | const course = interaction.options.getString("course"); 122 | 123 | const url = `https://unilectives.devsoc.app/course/${course}`; 124 | 125 | const year = new Date().getFullYear(); 126 | const handbookUrl = `https://www.handbook.unsw.edu.au/undergraduate/courses/${year}/${course}`; 127 | 128 | try { 129 | await interaction.deferReply({ ephemeral: true }); 130 | 131 | const { courseTitle, numReviews, description, ratings } = await extractRating(url); 132 | 133 | if (numReviews == "0 reviews") { 134 | await interaction.editReply({ 135 | content: "Sorry there are no reviews for this course yet 😔", 136 | }); 137 | return; 138 | } 139 | 140 | const image = await buildChart(ratings); 141 | const attachment = new AttachmentBuilder(image, { name: "rating.png" }); 142 | ratings.unshift({ 143 | name: "\u200B", 144 | value: `[${course} Handbook](${handbookUrl})`, 145 | }); 146 | const replyEmbed = new EmbedBuilder() 147 | .setColor(0x0099ff) 148 | .setTitle(course + " " + courseTitle) 149 | .setURL(url) 150 | .setDescription(description) 151 | .setImage("attachment://rating.png") 152 | .addFields(ratings) 153 | .setFooter({ text: numReviews }); 154 | 155 | await interaction.editReply({ embeds: [replyEmbed], files: [attachment] }); 156 | } catch (err) { 157 | console.log(err); 158 | await interaction.editReply({ 159 | content: `Sorry the course could not be found! 😔`, 160 | }); 161 | } 162 | }, 163 | }; 164 | -------------------------------------------------------------------------------- /commands/createvc.js: -------------------------------------------------------------------------------- 1 | const { SlashCommandBuilder } = require("@discordjs/builders"); 2 | const fs = require("fs"); 3 | 4 | module.exports = { 5 | data: new SlashCommandBuilder() 6 | .setName("createvc") 7 | .setDescription("Create a temporary voice channel"), 8 | async execute(interaction) { 9 | try { 10 | // Limit on concurrent temporary channels 11 | const CHANNEL_LIMIT = 10; 12 | // Name of the category under which the temporary channels are 13 | const CATEGORY_NAME = "TEMPORARY VCS"; 14 | 15 | const data = JSON.parse(fs.readFileSync("./data/createvc.json", "utf8")); 16 | // console.log(data); 17 | // const authorid = interaction.user.id; 18 | 19 | const size = data.channels.length; 20 | // console.log(size); 21 | if (size < CHANNEL_LIMIT) { 22 | // let temp = {"authorid":authorid,"count":1}; 23 | // data.users.unshift(temp); 24 | 25 | const channelmanager = interaction.guild.channels; 26 | let parentChannel = null; 27 | const allchannels = await channelmanager.fetch(); 28 | 29 | // See if there is a category channel with name - TEMPORARY VCs 30 | // If not, it creates a new category with name CATEGORY_NAME 31 | try { 32 | allchannels.forEach((item) => { 33 | if ( 34 | item != null && 35 | item.type == "GUILD_CATEGORY" && 36 | item.name == CATEGORY_NAME 37 | ) { 38 | parentChannel = item; 39 | } 40 | }); 41 | } catch (error) { 42 | await interaction.reply("Something is wrong!"); 43 | } 44 | 45 | if (parentChannel == null) { 46 | parentChannel = await channelmanager.create(CATEGORY_NAME, { 47 | type: 4, 48 | }); 49 | } 50 | 51 | // Create a new channel and then add it to the limit 52 | 53 | const tempchannel = await channelmanager.create("Temp VC", { 54 | type: 2, 55 | parent: parentChannel, 56 | }); 57 | const data_add = { channel_id: tempchannel.id, delete: false }; 58 | data.channels.unshift(data_add); 59 | 60 | fs.writeFileSync( 61 | "./data/createvc.json", 62 | JSON.stringify({ users: data.users, channels: data.channels }, null, 4), 63 | ); 64 | await interaction.reply("New temporary vc has been created"); 65 | } else { 66 | await interaction.reply("Sorry, daily voice channel limit reached!"); 67 | } 68 | } catch (error) { 69 | await interaction.reply("Error: " + error); 70 | } 71 | }, 72 | }; 73 | -------------------------------------------------------------------------------- /commands/csesocLinks.js: -------------------------------------------------------------------------------- 1 | const { SlashCommandBuilder } = require("@discordjs/builders"); 2 | const cheerio = require("cheerio"); 3 | module.exports = { 4 | data: new SlashCommandBuilder() 5 | .setName("csesoclinks") 6 | .setDescription("Provides CSESoc Linktree links."), 7 | async execute(interaction) { 8 | fetch("https://linktr.ee/csesoc") 9 | .then((res) => { 10 | return res.text(); 11 | }) 12 | .then((html) => { 13 | const $ = cheerio.load(html); 14 | const links = $("a"); 15 | let output = ""; 16 | links.each((index, value) => { 17 | const title = $(value).text().trim(); 18 | const href = $(value).attr("href"); 19 | if (href && href !== "#" && !title.includes("Linktree")) { 20 | output += `${title}: ${href}\n`; 21 | } 22 | }); 23 | interaction.reply({ 24 | content: output, 25 | }); 26 | }) 27 | .catch((err) => { 28 | console.log("Failed to fetch page: ", err); 29 | }); 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /commands/faq.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const { SlashCommandBuilder, SlashCommandSubcommandBuilder } = require("@discordjs/builders"); 3 | const { EmbedBuilder } = require("discord.js"); 4 | const { DiscordScroll } = require("../lib/discordscroll/scroller"); 5 | 6 | // //////////////////////////////////////////// 7 | // //////// SETTING UP THE COMMANDS /////////// 8 | // //////////////////////////////////////////// 9 | 10 | const commandFAQHelp = new SlashCommandSubcommandBuilder() 11 | .setName("help") 12 | .setDescription("Get some information about the help command"); 13 | 14 | const commandFAQGet = new SlashCommandSubcommandBuilder() 15 | .setName("get") 16 | .setDescription("Get the information related to a particular keyword") 17 | .addStringOption((option) => 18 | option.setName("keyword").setDescription("Keyword for the question.").setRequired(true), 19 | ); 20 | 21 | const commandFAQGetAll = new SlashCommandSubcommandBuilder() 22 | .setName("getall") 23 | .setDescription("Get *all* information related to a particular keyword") 24 | .addStringOption((option) => 25 | option.setName("tag").setDescription("Tag to be searched for.").setRequired(true), 26 | ); 27 | 28 | const commandFAQGetKeywords = new SlashCommandSubcommandBuilder() 29 | .setName("keywords") 30 | .setDescription("Get all keywords that exist for current FAQs"); 31 | 32 | const commandFAQGetTags = new SlashCommandSubcommandBuilder() 33 | .setName("tags") 34 | .setDescription("Get all tags that exist for current FAQs"); 35 | 36 | // the base command 37 | const baseCommand = new SlashCommandBuilder() 38 | .setName("faq") 39 | .setDescription("Master FAQ command") 40 | .addSubcommand(commandFAQHelp) 41 | .addSubcommand(commandFAQGet) 42 | .addSubcommand(commandFAQGetAll) 43 | .addSubcommand(commandFAQGetKeywords) 44 | .addSubcommand(commandFAQGetTags); 45 | 46 | // //////////////////////////////////////////// 47 | // ///////// HANDLING THE COMMANDS //////////// 48 | // //////////////////////////////////////////// 49 | 50 | // handle the command 51 | /** @param {CommandInteraction} interaction */ 52 | async function handleInteraction(interaction) { 53 | /** @type {DBFaq} */ 54 | const faqStorage = global.faqStorage; 55 | 56 | // figure out which command was called 57 | const subcommand = interaction.options.getSubcommand(false); 58 | switch (subcommand) { 59 | case "get": 60 | await handleFAQGet(interaction, faqStorage); 61 | break; 62 | case "getall": 63 | await handleFAQGetAll(interaction, faqStorage); 64 | break; 65 | case "help": 66 | await handleFAQHelp(interaction); 67 | break; 68 | case "keywords": 69 | await handleFAQKeywords(interaction, faqStorage); 70 | break; 71 | case "tags": 72 | await handleFAQTags(interaction, faqStorage); 73 | break; 74 | default: 75 | await interaction.reply("Internal Error AHHHHHHH! CONTACT ME PLEASE!"); 76 | } 77 | } 78 | 79 | // //////////////////////////////////////////// 80 | // ///////// HANDLING THE COMMANDS //////////// 81 | // //////////////////////////////////////////// 82 | 83 | /** 84 | * @param {CommandInteraction} interaction 85 | * @param {DBFaq} faqStorage 86 | */ 87 | async function handleFAQGet(interaction, faqStorage) { 88 | // get the keyword 89 | const keyword = String(interaction.options.get("keyword").value).toLowerCase(); 90 | 91 | // get db entry 92 | const rows = await faqStorage.get_faq(keyword); 93 | if (rows.length > 0) { 94 | const answer = rows[0]["answer"]; 95 | await interaction.reply(`FAQ: ${keyword}\n${answer}`); 96 | } else { 97 | await interaction.reply({ 98 | content: "A FAQ for this keyword does not exist!", 99 | ephemeral: true, 100 | }); 101 | } 102 | } 103 | 104 | /** 105 | * @param {CommandInteraction} interaction 106 | * @param {DBFaq} faqStorage 107 | */ 108 | async function handleFAQGetAll(interaction, faqStorage) { 109 | // @TODO: create "tags" system to support fectching multiple FAQs 110 | // get the keyword 111 | const tag = String(interaction.options.get("tag").value).toLowerCase(); 112 | 113 | // get db entry 114 | const rows = await faqStorage.get_tagged_faqs(tag); 115 | if (rows.length > 0) { 116 | const answers = []; 117 | let currentPage = 0; 118 | for (const row of rows) { 119 | const newPage = new EmbedBuilder({ 120 | title: `FAQS for the tag: ${tag}`, 121 | color: 0xf1c40f, 122 | timestamp: new Date().getTime(), 123 | }); 124 | answers.push(newPage); 125 | 126 | answers[currentPage].addFields([ 127 | { 128 | name: row.keyword, 129 | value: row.answer, 130 | inline: true, 131 | }, 132 | ]); 133 | 134 | currentPage++; 135 | } 136 | const scroller = new DiscordScroll(answers); 137 | await scroller.send(interaction); 138 | } else { 139 | await interaction.reply({ 140 | content: "A FAQ for this keyword does not exist!", 141 | ephemeral: true, 142 | }); 143 | } 144 | } 145 | 146 | /** 147 | * @param {CommandInteraction} interaction 148 | * @param {DBFaq} faqStorage 149 | */ 150 | async function handleFAQHelp(interaction) { 151 | // @TODO: expand this function 152 | let description = "Welcome to the help command! You can search for a specific faq"; 153 | description += " by keyword using 'faq get [keyword]', or for everything on a given "; 154 | description += "topic by using 'faq getall [tag]'. "; 155 | description += "Use 'faq keywords' to get a list of all keywords, or "; 156 | description += "use 'faq tags' to get a list of all tags."; 157 | 158 | await interaction.reply(description); 159 | } 160 | 161 | /** 162 | * @param {CommandInteraction} interaction 163 | * @param {DBFaq} faqStorage 164 | */ 165 | async function handleFAQKeywords(interaction, faqStorage) { 166 | // get db entry 167 | const keywords = await faqStorage.get_keywords(); 168 | if (keywords) { 169 | await interaction.reply(`Current list of keyword is:\n${keywords}`); 170 | } else { 171 | await interaction.reply({ 172 | content: "No keywords currently in database!", 173 | ephemeral: true, 174 | }); 175 | } 176 | } 177 | 178 | /** 179 | * @param {CommandInteraction} interaction 180 | * @param {DBFaq} faqStorage 181 | */ 182 | async function handleFAQTags(interaction, faqStorage) { 183 | // get db entry 184 | const tags = await faqStorage.get_tags(); 185 | if (tags) { 186 | await interaction.reply(`Current list of tags is:\n${tags}`); 187 | } else { 188 | await interaction.reply({ 189 | content: "No tags currently in database!", 190 | ephemeral: true, 191 | }); 192 | } 193 | } 194 | 195 | module.exports = { 196 | data: baseCommand, 197 | execute: handleInteraction, 198 | }; 199 | -------------------------------------------------------------------------------- /commands/faqadmin.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const { SlashCommandBuilder, SlashCommandSubcommandBuilder } = require("@discordjs/builders"); 3 | const { PermissionsBitField } = require("discord.js"); 4 | 5 | // //////////////////////////////////////////// 6 | // //////// SETTING UP THE COMMANDS /////////// 7 | // //////////////////////////////////////////// 8 | 9 | // faq admin delete 10 | const commandFAQADelete = new SlashCommandSubcommandBuilder() 11 | .setName("delete") 12 | .setDescription("[ADMIN] Delete a FAQ entry.") 13 | .addStringOption((option) => 14 | option.setName("keyword").setDescription("The identifying word.").setRequired(true), 15 | ); 16 | 17 | // faq admin create 18 | const commandFAQACreate = new SlashCommandSubcommandBuilder() 19 | .setName("create") 20 | .setDescription("[ADMIN] Create a FAQ entry.") 21 | .addStringOption((option) => 22 | option.setName("keyword").setDescription("The identifying word.").setRequired(true), 23 | ) 24 | .addStringOption((option) => 25 | option.setName("answer").setDescription("The answer to the question.").setRequired(true), 26 | ) 27 | .addStringOption((option) => 28 | option.setName("tags").setDescription("The answer to the question.").setRequired(false), 29 | ); 30 | 31 | // the base command 32 | const baseCommand = new SlashCommandBuilder() 33 | .setName("faqadmin") 34 | .setDescription("[ADMIN] Master FAQ admin command") 35 | .addSubcommand(commandFAQACreate) 36 | .addSubcommand(commandFAQADelete); 37 | 38 | // //////////////////////////////////////////// 39 | // ///////// HANDLING THE COMMANDS //////////// 40 | // //////////////////////////////////////////// 41 | 42 | // handle the command 43 | /** @param {CommandInteraction} interaction */ 44 | async function handleInteraction(interaction) { 45 | /** @type {DBFaq} */ 46 | const faqStorage = global.faqStorage; 47 | 48 | // Admin permission check (this may not work uhm) 49 | if (!interaction.member.permissions.has(PermissionsBitField.Flags.Administrator)) { 50 | await interaction.reply({ 51 | content: "You do not have permission to execute this command.", 52 | ephemeral: true, 53 | }); 54 | return; 55 | } 56 | 57 | // figure out which command was called 58 | const subcommand = interaction.options.getSubcommand(false); 59 | let keyword = null; 60 | let answer = null; 61 | let tags = null; 62 | let success = false; 63 | switch (subcommand) { 64 | case "create": 65 | keyword = String(interaction.options.get("keyword").value).toLowerCase(); 66 | answer = String(interaction.options.get("answer").value); 67 | if (answer.length >= 1024) { 68 | await interaction.reply({ 69 | content: "The answer must be < 1024 characters...", 70 | ephemeral: true, 71 | }); 72 | } 73 | 74 | console.log("gets here"); 75 | if (interaction.options.get("tags") != null) { 76 | tags = String(interaction.options.get("tags").value); 77 | // validate "tags" string 78 | if (tags) { 79 | tags = tags.trim(); 80 | const tagRegex = /^([a-zA-Z]+,)*[a-zA-Z]+$/; 81 | if (!tagRegex.test(tags)) { 82 | await interaction.reply({ 83 | content: "ERROR: tags must be comma-separated alphabetic strings", 84 | ephemeral: true, 85 | }); 86 | break; 87 | } 88 | } 89 | } 90 | 91 | success = await faqStorage.new_faq(keyword, answer, tags); 92 | if (success) { 93 | await interaction.reply({ 94 | content: `Successfully created FAQ entry for '${keyword}': ${answer}`, 95 | ephemeral: true, 96 | }); 97 | } else { 98 | await interaction.reply({ 99 | content: "Something went wrong, make sure you are using a unique keyword!", 100 | ephemeral: true, 101 | }); 102 | } 103 | break; 104 | case "delete": 105 | keyword = String(interaction.options.get("keyword").value).toLowerCase(); 106 | success = await faqStorage.del_faq(keyword); 107 | if (success) { 108 | await interaction.reply({ 109 | content: `Successfully Deleted FAQ entry for '${keyword}'.`, 110 | ephemeral: true, 111 | }); 112 | } else { 113 | await interaction.reply({ 114 | content: "Something went wrong, make sure you are giving a unique keyword!", 115 | ephemeral: true, 116 | }); 117 | } 118 | break; 119 | default: 120 | await interaction.reply("Internal Error OH NO! CONTACT ME PLEASE!"); 121 | } 122 | } 123 | 124 | module.exports = { 125 | data: baseCommand, 126 | execute: handleInteraction, 127 | }; 128 | -------------------------------------------------------------------------------- /commands/handbook.js: -------------------------------------------------------------------------------- 1 | const axios = require("axios"); 2 | const textVersion = require("textversionjs"); 3 | const { SlashCommandBuilder } = require("@discordjs/builders"); 4 | const { EmbedBuilder } = require("discord.js"); 5 | const { apiURL, handbookURL } = require("../config/handbook.json"); 6 | 7 | module.exports = { 8 | data: new SlashCommandBuilder() 9 | .setName("handbook") 10 | .setDescription("Displays information from the UNSW Handbook.") 11 | .addSubcommand((subcommand) => 12 | subcommand 13 | .setName("courseinfo") 14 | .setDescription("Displays information about a course.") 15 | .addStringOption((option) => 16 | option 17 | .setName("coursecode") 18 | .setDescription( 19 | "Code of course to display information about (e.g. COMP1511)", 20 | ) 21 | .setRequired(true), 22 | ), 23 | ), 24 | async execute(interaction) { 25 | if (interaction.options.getSubcommand() === "courseinfo") { 26 | const courseCode = await interaction.options.getString("coursecode").toUpperCase(); 27 | 28 | let data; 29 | try { 30 | // Documented at: 31 | // https://circlesapi.csesoc.app/docs#/courses/get_course_courses_getCourse__courseCode__get 32 | const response = await axios.get(`${apiURL}/courses/getCourse/${courseCode}`); 33 | data = response.data; 34 | // console.log(data); 35 | } catch (e) { 36 | return await interaction.reply({ 37 | content: "Invalid course code.", 38 | ephemeral: true, 39 | }); 40 | } 41 | 42 | const { 43 | title, 44 | code, 45 | UOC, 46 | // level, 47 | description, 48 | // study_level, 49 | // school, 50 | // campus, 51 | equivalents, 52 | raw_requirements, 53 | exclusions, 54 | // handbook_note, 55 | terms, 56 | } = data; 57 | 58 | const courseInfo = new EmbedBuilder() 59 | .setTitle(title) 60 | .setURL(`${handbookURL}/${code}`) 61 | .setColor(0x3a76f8) 62 | .setAuthor({ 63 | name: `Course Info: ${code} (${UOC} UOC)`, 64 | iconURL: "https://i.imgur.com/EE3Q40V.png", 65 | }) 66 | .addFields( 67 | { 68 | name: "Overview", 69 | value: textVersion(description).substring( 70 | 0, 71 | Math.min(textVersion(description).indexOf("\n"), 1024), 72 | ), 73 | inline: false, 74 | }, 75 | { 76 | name: "Enrolment Requirements", 77 | value: 78 | raw_requirements.replace( 79 | /[A-Z]{4}[0-9]{4}/g, 80 | `[$&](${handbookURL}/$&)`, 81 | ) || "None", 82 | inline: true, 83 | }, 84 | { 85 | name: "Offering Terms", 86 | value: terms.join(", ") || "None", 87 | inline: true, 88 | }, 89 | { 90 | name: "Equivalent Courses", 91 | value: 92 | Object.keys(equivalents) 93 | .map((course) => `[${course}](${handbookURL}/${course})`) 94 | .join(", ") || "None", 95 | inline: true, 96 | }, 97 | { 98 | name: "Exclusion Courses", 99 | value: 100 | Object.keys(exclusions) 101 | .map((course) => `[${course}](${handbookURL}/${course})`) 102 | .join(", ") || "None", 103 | inline: true, 104 | }, 105 | /* { */ 106 | /* name: "Course Outline", */ 107 | /* value: `[${courseCode} Course Outline](${data["course_outline_url"]})`, */ 108 | /* inline: true, */ 109 | /* }, */ 110 | ) 111 | .setTimestamp() 112 | .setFooter({ 113 | text: "Data fetched from Circles' Api", 114 | }); 115 | await interaction.reply({ embeds: [courseInfo] }); 116 | } 117 | }, 118 | }; 119 | -------------------------------------------------------------------------------- /commands/help.js: -------------------------------------------------------------------------------- 1 | const help = require("../config/help.json"); 2 | const { SlashCommandBuilder } = require("@discordjs/builders"); 3 | const { EmbedBuilder, ActionRowBuilder, ButtonBuilder } = require("discord.js"); 4 | 5 | // Fetches commands from the help data 6 | const commands = help.commands; 7 | 8 | // Creates general object and id constants for function use 9 | const prevId = "helpPrevButtonId"; 10 | const nextId = "helpNextButtonId"; 11 | 12 | const prevButton = new ButtonBuilder({ 13 | style: "SECONDARY", 14 | label: "Previous", 15 | emoji: "⬅️", 16 | customId: prevId, 17 | }); 18 | const nextButton = new ButtonBuilder({ 19 | style: "SECONDARY", 20 | label: "Next", 21 | emoji: "➡️", 22 | customId: nextId, 23 | }); 24 | 25 | const PAGE_SIZE = 10; 26 | 27 | /** 28 | * Creates an embed with commands starting from an index. 29 | * @param {number} start The index to start from. 30 | * @returns {EmbedBuilder} 31 | */ 32 | const generateEmbed = (start) => { 33 | const current = commands.slice(start, start + PAGE_SIZE); 34 | const pageNum = Math.floor(start / PAGE_SIZE) + 1; 35 | 36 | return new EmbedBuilder({ 37 | title: `Help Command - Page ${pageNum}`, 38 | color: 0x3a76f8, 39 | author: { 40 | name: "CSESoc Bot", 41 | icon_url: "https://i.imgur.com/EE3Q40V.png", 42 | }, 43 | fields: current.map((command, index) => ({ 44 | name: `${start + index + 1}. ${command.name}`, 45 | value: `${command.description}\nUsage: ${command.usage}`, 46 | })), 47 | }); 48 | }; 49 | 50 | module.exports = { 51 | // Add new /help command 52 | data: new SlashCommandBuilder() 53 | .setName("help") 54 | .setDescription( 55 | "Displays info for all commands. Also type / in the chat to check out other commands.", 56 | ) 57 | .addNumberOption((option) => 58 | option.setName("page").setDescription("Requested Help Page").setRequired(false), 59 | ), 60 | async execute(interaction) { 61 | // Calculates required command page index if inputted 62 | const page = interaction.options.getNumber("page"); 63 | let currentIndex = 0; 64 | 65 | if (page) { 66 | if (page < 1 || page > Math.ceil(commands.length / PAGE_SIZE)) { 67 | const ephemeralError = { 68 | content: "Your requested page does not exist, please try again.", 69 | ephemeral: true, 70 | }; 71 | 72 | await interaction.reply(ephemeralError); 73 | return; 74 | } else { 75 | const adjustedIndex = (page - 1) * PAGE_SIZE; 76 | if (adjustedIndex < commands.length) { 77 | currentIndex = adjustedIndex; 78 | } 79 | } 80 | } 81 | 82 | // Generates help menu with given or default index and posts embed 83 | const helpEmbed = generateEmbed(currentIndex); 84 | const authorId = interaction.user.id; 85 | 86 | await interaction.reply({ 87 | embeds: [helpEmbed], 88 | components: [ 89 | new ActionRowBuilder({ 90 | components: [ 91 | // previous button if it isn't the start 92 | ...(currentIndex ? [prevButton] : []), 93 | // next button if it isn't the end 94 | ...(currentIndex + PAGE_SIZE < commands.length ? [nextButton] : []), 95 | ], 96 | }), 97 | ], 98 | }); 99 | 100 | // Creates a collector for button interaction events, setting a 120s maximum 101 | // timeout and a 30s inactivity timeout 102 | const filter = (resInteraction) => { 103 | return ( 104 | (resInteraction.customId === prevId || resInteraction.customId === nextId) && 105 | resInteraction.user.id === authorId 106 | ); 107 | }; 108 | const collector = interaction.channel.createMessageComponentCollector({ 109 | filter, 110 | time: 120000, 111 | idle: 30000, 112 | }); 113 | 114 | collector.on("collect", async (i) => { 115 | // Adjusts the currentIndex based on the id of the button pressed 116 | i.customId === prevId ? (currentIndex -= PAGE_SIZE) : (currentIndex += PAGE_SIZE); 117 | 118 | await i.update({ 119 | embeds: [generateEmbed(currentIndex)], 120 | components: [ 121 | new ActionRowBuilder({ 122 | components: [ 123 | // previous button if it isn't the start 124 | ...(currentIndex ? [prevButton] : []), 125 | // next button if it isn't the end 126 | ...(currentIndex + PAGE_SIZE < commands.length ? [nextButton] : []), 127 | ], 128 | }), 129 | ], 130 | }); 131 | }); 132 | 133 | // Clears buttons from embed page after timeout on collector 134 | /*eslint-disable */ 135 | collector.on("end", (collection) => { 136 | interaction.editReply({ components: [] }); 137 | }); 138 | }, 139 | }; 140 | -------------------------------------------------------------------------------- /commands/joke.js: -------------------------------------------------------------------------------- 1 | const { SlashCommandBuilder } = require("@discordjs/builders"); 2 | const { EmbedBuilder } = require("discord.js"); 3 | const axios = require("axios").default; 4 | 5 | module.exports = { 6 | data: new SlashCommandBuilder().setName("joke").setDescription("Replies with a new joke!"), 7 | async execute(interaction) { 8 | axios 9 | .get("https://official-joke-api.appspot.com/random_joke") 10 | .then((res) => { 11 | // console.log(res.data); 12 | const embed = new EmbedBuilder() 13 | .setTitle(res.data.setup) 14 | .setDescription(res.data.punchline); 15 | 16 | interaction.reply({ embeds: [embed], ephemeral: true }); 17 | }) 18 | .catch((err) => { 19 | console.log(err); 20 | interaction.reply({ 21 | content: `sorry something went wrong!😔`, 22 | ephemeral: true, 23 | }); 24 | }); 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /commands/logreport.js: -------------------------------------------------------------------------------- 1 | const { SlashCommandBuilder } = require("@discordjs/builders"); 2 | const { PermissionsBitField } = require("discord.js"); 3 | const path = require("path"); 4 | const nodemailer = require("nodemailer"); 5 | 6 | module.exports = { 7 | data: new SlashCommandBuilder() 8 | .setName("logreport") 9 | .setDescription("[ADMIN] collect message logs") 10 | .addSubcommand((subcommand) => 11 | subcommand 12 | .setName("today") 13 | .setDescription("[ADMIN] get all message logs from today") 14 | .addStringOption((option) => 15 | option 16 | .setName("email") 17 | .setDescription("Email you want log report to be sent to") 18 | .setRequired(true), 19 | ), 20 | ) 21 | .addSubcommand((subcommand) => 22 | subcommand 23 | .setName("timeperiod") 24 | .setDescription("[ADMIN] get all message logs in the set of days specified") 25 | .addStringOption((option) => 26 | option 27 | .setName("email") 28 | .setDescription("Email you want log report to be sent to") 29 | .setRequired(true), 30 | ) 31 | .addStringOption((option) => 32 | option 33 | .setName("start-datetime") 34 | .setDescription("Enter the time as YYYY-MM-DD HH:MM") 35 | .setRequired(true), 36 | ) 37 | .addStringOption((option) => 38 | option 39 | .setName("end-datetime") 40 | .setDescription("Enter the time as YYYY-MM-DD HH:MM") 41 | .setRequired(true), 42 | ), 43 | ), 44 | 45 | async execute(interaction) { 46 | try { 47 | if (!interaction.member.permissions.has(PermissionsBitField.Flags.Administrator)) { 48 | await interaction.reply({ 49 | content: "You do not have permission to execute this command.", 50 | ephemeral: true, 51 | }); 52 | return; 53 | } 54 | 55 | const email_reg = 56 | /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; /* eslint-disable-line */ 57 | const email = interaction.options.getString("email"); 58 | 59 | if (!email_reg.test(email)) { 60 | await interaction.reply({ 61 | content: "Please enter a valid email.", 62 | ephemeral: true, 63 | }); 64 | return; 65 | } 66 | 67 | let start = null; 68 | let end = null; 69 | 70 | if (interaction.options.getSubcommand() === "today") { 71 | const today = new Date(); 72 | 73 | const t_year = today.getFullYear().toString(); 74 | const mon = parseInt(today.getMonth()) + 1; 75 | const t_month = mon.toLocaleString("en-US", { 76 | minimumIntegerDigits: 2, 77 | useGrouping: false, 78 | }); 79 | const t_date = today.getDate().toLocaleString("en-US", { 80 | minimumIntegerDigits: 2, 81 | useGrouping: false, 82 | }); 83 | 84 | const tomorrow_date = (parseInt(t_date) + 1).toLocaleString("en-US", { 85 | minimumIntegerDigits: 2, 86 | useGrouping: false, 87 | }); 88 | 89 | start = t_year + "-" + t_month + "-" + t_date + " 00:01"; 90 | end = t_year + "-" + t_month + "-" + tomorrow_date + " 00:01"; 91 | } else if (interaction.options.getSubcommand() === "timeperiod") { 92 | const re = 93 | /^\d{4}-(0?[1-9]|1[012])-(0?[1-9]|[12][0-9]|3[01]) ([01]\d|2[0-3]):([0-5]\d)$/; 94 | start = interaction.options.getString("start-datetime"); 95 | end = interaction.options.getString("end-datetime"); 96 | 97 | if (!re.test(start)) { 98 | await interaction.reply({ 99 | content: "Please enter the start-datetime as YYYY-MM-DD HH:MM exactly", 100 | ephemeral: true, 101 | }); 102 | return; 103 | } 104 | 105 | if (!re.test(end)) { 106 | await interaction.reply({ 107 | content: "Please enter the end-datetime as YYYY-MM-DD HH:MM exactly", 108 | ephemeral: true, 109 | }); 110 | return; 111 | } 112 | } 113 | 114 | const logDB = global.logDB; 115 | const logs = await logDB.collect_messages(start, end); 116 | 117 | for (let i = 0; i < logs.length; i++) { 118 | logs[i]["username"] = logs[i]["username"].trim(); 119 | logs[i]["message"] = logs[i]["message"].trim(); 120 | logs[i]["original_message"] = logs[i]["original_message"].trim(); 121 | } 122 | 123 | const createCsvWriter = require("csv-writer").createObjectCsvWriter; 124 | const csvWriter = createCsvWriter({ 125 | path: "./data/log_report.csv", 126 | header: [ 127 | { id: "message_id", title: "Message_ID" }, 128 | { id: "user_id", title: "User_ID" }, 129 | { id: "username", title: "Username" }, 130 | { id: "message", title: "Message" }, 131 | { id: "original_message", title: "Original_Message" }, 132 | { id: "deleted", title: "Deleted" }, 133 | { id: "message_datetime", title: "Message_Sent" }, 134 | { id: "channel_id", title: "Channel_ID" }, 135 | { id: "channel_name", title: "Channel_Name" }, 136 | ], 137 | }); 138 | 139 | csvWriter 140 | .writeRecords(logs) 141 | .then(() => console.log("The Log CSV file was written successfully")); 142 | 143 | const logP = path.join(__dirname, "../data/log_report.csv"); 144 | 145 | const transport = nodemailer.createTransport({ 146 | host: "smtp.zoho.com.au", 147 | secure: true, 148 | port: 465, 149 | auth: { 150 | user: process.env.ZOHO_EMAIL, 151 | pass: process.env.ZOHO_PASS, 152 | }, 153 | }); 154 | 155 | // change mail for csesoc specific 156 | const mailOptions = { 157 | from: "csesocbot@gmail.com", 158 | to: email, 159 | subject: "messages logs", 160 | text: "This is the requested report log for " + start + " to " + end, 161 | attachments: [ 162 | { 163 | filename: "log.csv", 164 | path: logP, 165 | }, 166 | ], 167 | }; 168 | 169 | try { 170 | await transport.sendMail(mailOptions); 171 | interaction.reply({ 172 | content: `Sent log report to ${email}`, 173 | ephemeral: true, 174 | }); 175 | } catch (e) { 176 | interaction.reply({ 177 | content: "Error sending email " + e, 178 | ephemeral: true, 179 | }); 180 | } 181 | } catch (error) { 182 | interaction.reply({ content: "Error: " + error, ephemeral: true }); 183 | } 184 | }, 185 | }; 186 | -------------------------------------------------------------------------------- /commands/meetingtools.js: -------------------------------------------------------------------------------- 1 | const { SlashCommandBuilder } = require("@discordjs/builders"); 2 | const { EmbedBuilder } = require("discord.js"); 3 | 4 | // Tools to help manage meetings 5 | 6 | module.exports = { 7 | data: new SlashCommandBuilder() 8 | .setName("meeting") 9 | .setDescription("Tools to help manage meetings in voice channels") 10 | .addSubcommand((subcommand) => 11 | subcommand 12 | .setName("queue") 13 | .setDescription("Generates a queue of vc participants") 14 | .addStringOption((option) => 15 | option 16 | .setName("exclude") 17 | .setDescription("Exclude certain vc participants, enter as @user1 @user2"), 18 | ) 19 | .addStringOption((option) => 20 | option 21 | .setName("include") 22 | .setDescription("Include users that are not in vc, enter as @user1 @user2"), 23 | ), 24 | ) 25 | .addSubcommand((subcommand) => 26 | subcommand 27 | .setName("random") 28 | .setDescription("Picks a random vc participants") 29 | .addStringOption((option) => 30 | option 31 | .setName("exclude") 32 | .setDescription("Exclude certain vc participants, enter as @user1 @user2"), 33 | ) 34 | .addStringOption((option) => 35 | option 36 | .setName("include") 37 | .setDescription("Include users that are not in vc, enter as @user1 @user2"), 38 | ), 39 | ) 40 | .addSubcommand((subcommand) => 41 | subcommand 42 | .setName("groups") 43 | .setDescription("Puts vc participants into groups") 44 | .addIntegerOption((option) => 45 | option 46 | .setName("num_groups") 47 | .setDescription("Number of groups") 48 | .setRequired(true), 49 | ) 50 | .addStringOption((option) => 51 | option 52 | .setName("exclude") 53 | .setDescription("Exclude certain vc participants, enter as @user1 @user2"), 54 | ) 55 | .addStringOption((option) => 56 | option 57 | .setName("include") 58 | .setDescription("Include users that are not in vc, enter as @user1 @user2"), 59 | ), 60 | ), 61 | 62 | async execute(interaction) { 63 | const voice_channel = interaction.member.voice.channel; 64 | 65 | // Check if connected to voice channel 66 | if (!voice_channel) { 67 | return await interaction.reply({ 68 | content: "You are currently not connected to a voice channel", 69 | ephemeral: true, 70 | }); 71 | } 72 | 73 | const participants = []; 74 | 75 | // Gets all participants of the voice channel 76 | voice_channel.members.each((member) => { 77 | participants.push(member.user.tag); 78 | }); 79 | 80 | const include = interaction.options.getString("include"); 81 | const exclude = interaction.options.getString("exclude"); 82 | 83 | // Include extra users from command input 84 | if (include) { 85 | include 86 | .trim() 87 | .split(/\s+/) 88 | .forEach((user) => { 89 | const user_id = user.substr(3, 18); 90 | const member = interaction.member.guild.members.cache.get(user_id); 91 | if (member) { 92 | participants.push(member.user.tag); 93 | } 94 | }); 95 | } 96 | 97 | // Exclude particular users from command input 98 | if (exclude) { 99 | exclude 100 | .trim() 101 | .split(/\s+/) 102 | .forEach((user) => { 103 | const user_id = user.substr(3, 18); 104 | const member = interaction.member.guild.members.cache.get(user_id); 105 | if (member) { 106 | const index = participants.indexOf(member.user.tag); 107 | if (index !== -1) { 108 | participants.splice(index, 1); 109 | } 110 | } 111 | }); 112 | } 113 | 114 | const command = interaction.options.getSubcommand(); 115 | let ret_val = ""; 116 | 117 | if (command === "queue") { 118 | // Create a random queue of users 119 | shuffleArray(participants); 120 | 121 | let counter = 1; 122 | 123 | participants.forEach((participant) => { 124 | ret_val += `${counter}. ${participant}\n`; 125 | counter++; 126 | }); 127 | } else if (command === "random") { 128 | // Selects user at random 129 | ret_val = participants[Math.floor(Math.random() * participants.length)]; 130 | } else if (command === "groups") { 131 | // Groups users into a given number of groups 132 | shuffleArray(participants); 133 | 134 | const num_groups = interaction.options.getInteger("num_groups"); 135 | const members_per_group = Math.round(participants.length / num_groups); 136 | 137 | let group_num = 1; 138 | let member_num = 0; 139 | 140 | participants.forEach((participant) => { 141 | if (member_num == 0) { 142 | ret_val += `Group ${group_num}\n`; 143 | } 144 | 145 | ret_val += participant + "\n"; 146 | member_num++; 147 | 148 | if (group_num < num_groups && member_num == members_per_group) { 149 | member_num = 0; 150 | group_num++; 151 | ret_val += "\n"; 152 | } 153 | }); 154 | } 155 | 156 | const embed = new EmbedBuilder() 157 | .setTitle(command) 158 | .setColor("#0099ff") 159 | .setDescription(ret_val); 160 | 161 | return await interaction.reply({ 162 | embeds: [embed], 163 | }); 164 | }, 165 | }; 166 | 167 | // shuffleArray function from 168 | // https://www.geeksforgeeks.org/how-to-shuffle-an-array-using-javascript/ 169 | 170 | function shuffleArray(array) { 171 | for (let i = array.length - 1; i > 0; i--) { 172 | // Generate random number 173 | const j = Math.floor(Math.random() * (i + 1)); 174 | 175 | const temp = array[i]; 176 | array[i] = array[j]; 177 | array[j] = temp; 178 | } 179 | 180 | return array; 181 | } 182 | -------------------------------------------------------------------------------- /commands/ping.js: -------------------------------------------------------------------------------- 1 | const { SlashCommandBuilder } = require("@discordjs/builders"); 2 | 3 | module.exports = { 4 | data: new SlashCommandBuilder().setName("ping").setDescription("Replies with Pong!"), 5 | async execute(interaction) { 6 | await interaction.reply("🏓 Pong!"); 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /commands/reactforrole.js: -------------------------------------------------------------------------------- 1 | const { SlashCommandBuilder } = require("@discordjs/builders"); 2 | const { PermissionsBitField, EmbedBuilder } = require("discord.js"); 3 | 4 | module.exports = { 5 | data: new SlashCommandBuilder() 6 | .setName("reactforrole") 7 | .setDescription("Creates a new role and assigns role to anyone who reacts with given emoji") 8 | .addStringOption((option) => 9 | option 10 | .setName("emojis") 11 | .setDescription( 12 | "Enter one or more emojis users will use to gain the new role separated by commas (e.g. emoji,emoji)", 13 | ) 14 | .setRequired(true), 15 | ) 16 | .addStringOption((option) => 17 | option 18 | .setName("rolenames") 19 | .setDescription( 20 | "Enter the names of the roles separated by commas (e.g. rolename,rolename)", 21 | ) 22 | .setRequired(true), 23 | ) 24 | .addStringOption((option) => 25 | option.setName("message").setDescription("Enter your message"), 26 | ), 27 | 28 | async execute(interaction) { 29 | // Only admin users should be able to execute this command 30 | if (!interaction.member.permissions.has(PermissionsBitField.Flags.Administrator)) { 31 | return await interaction.reply({ 32 | content: "You do not have permission to execute this command.", 33 | ephemeral: true, 34 | }); 35 | } 36 | 37 | const emojis = interaction.options.getString("emojis"); 38 | const roleNames = interaction.options.getString("rolenames"); 39 | 40 | let message = interaction.options.getString("message"); 41 | 42 | const emojiList = emojis.split(",").map((item) => item.trim()); 43 | const roleList = roleNames.split(",").map((item) => item.trim()); 44 | 45 | // Check emojis are unique 46 | if (emojiList.length !== new Set(emojiList).size) { 47 | return await interaction.reply({ 48 | content: "Please enter unique emojis", 49 | ephemeral: true, 50 | }); 51 | } 52 | 53 | // Check all emojis are valid 54 | const unicode_emoji_regex = 55 | /(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])/; 56 | const custom_emoji_regex = /^<:.*:\d{18}>$/; 57 | for (const element of emojiList) { 58 | if (!unicode_emoji_regex.test(element) && !custom_emoji_regex.test(element)) { 59 | return await interaction.reply({ 60 | content: "Please enter emojis only separated by commas e.g. emoji,emoji", 61 | ephemeral: true, 62 | }); 63 | } 64 | } 65 | 66 | // Check if rolenames and emojis correspond 67 | if (emojiList.length != roleList.length) { 68 | return await interaction.reply({ 69 | content: 70 | "Please have a rolename correspond to each emoji, ensure emojis and role names are separated by commas", 71 | ephemeral: true, 72 | }); 73 | } 74 | 75 | const reactRoles = global.reactRoles; 76 | 77 | const roles = {}; 78 | 79 | let notificationContent = "This command: \n"; 80 | 81 | for (let i = 0; i < roleList.length; i++) { 82 | const roleName = roleList[i]; 83 | let emoji = emojiList[i]; 84 | 85 | if (custom_emoji_regex.test(emoji)) { 86 | emoji = emoji.split(":")[1]; 87 | } 88 | 89 | // Check if role exist 90 | const role = interaction.member.guild.roles.cache.find((r) => r.name === roleName); 91 | let roleID = 0; 92 | 93 | if (role) { 94 | const roleIsAdmin = role.permissions.has("ADMINISTRATOR"); 95 | if (roleIsAdmin) { 96 | return await interaction.reply({ 97 | content: `The existing role '${roleName}' has admin permissions so this command cannot be used to give users this role`, 98 | ephemeral: true, 99 | }); 100 | } 101 | roleID = role.id; 102 | notificationContent += `\t'${emoji}' Used the existing role '${roleName}'\n`; 103 | } else { 104 | // Role does not exist so create one 105 | try { 106 | const newRole = await interaction.member.guild.roles.create({ 107 | name: roleName, 108 | reason: `new role required for react role feature "${roleName}"`, 109 | }); 110 | roleID = newRole.id; 111 | notificationContent += `\t'${emoji}' Created the new role '${roleName}'\n`; 112 | } catch (err) { 113 | console.log(err); 114 | return await interaction.reply({ 115 | content: `An error occured with creating '${roleName} ${err}'`, 116 | ephemeral: true, 117 | }); 118 | } 119 | } 120 | 121 | roles[emoji] = roleID; 122 | } 123 | 124 | if (!message) { 125 | message = ""; 126 | } else { 127 | message += "\n\n"; 128 | } 129 | 130 | message += "React to give yourself a role"; 131 | 132 | for (let j = 0; j < emojiList.length; j++) { 133 | message += `\n${emojiList[j]}: ${roleList[j]}`; 134 | } 135 | 136 | try { 137 | // Send message 138 | const sentMessage = await interaction.guild.channels.cache 139 | .get(interaction.channelId) 140 | .send({ 141 | content: message, 142 | fetchReply: true, 143 | }); 144 | 145 | // Notify user that they used the command 146 | const botName = sentMessage.author.username; 147 | const notification = new EmbedBuilder() 148 | .setColor("#7cd699") 149 | .setTitle("React For Role Command Used!") 150 | .setAuthor(botName, "https://avatars.githubusercontent.com/u/164179?s=200&v=4") 151 | .setDescription( 152 | `You used the 'reactforrole' command in "${interaction.member.guild.name} \n\n` + 153 | notificationContent + 154 | "\nReact ⛔ on the reaction message to stop users from getting the roles", 155 | ); 156 | interaction.reply({ 157 | embeds: [notification], 158 | ephemeral: true, 159 | }); 160 | 161 | // Add react 162 | emojiList.forEach((e) => { 163 | sentMessage.react(e); 164 | }); 165 | 166 | // Add to database 167 | await reactRoles.add_react_role_msg(sentMessage.id, interaction.user.id); 168 | for (const e in roles) { 169 | await reactRoles.add_react_role_role(roles[e], e, sentMessage.id); 170 | } 171 | } catch (err) { 172 | console.log(err); 173 | return await interaction.reply({ 174 | content: `An error occured with creating the role reaction messsage or writing to the database`, 175 | ephemeral: true, 176 | }); 177 | } 178 | }, 179 | }; 180 | -------------------------------------------------------------------------------- /commands/remind.js: -------------------------------------------------------------------------------- 1 | const { SlashCommandBuilder } = require("@discordjs/builders"); 2 | const { Embed } = require("@discordjs/builders"); 3 | 4 | const baseCommand = new SlashCommandBuilder() 5 | .setName("remind") 6 | .setDescription("Be reminded at a certain time by the bot") 7 | .addStringOption((option) => 8 | option 9 | .setName("datetime") 10 | .setDescription("Enter the time as YYYY-MM-DD HH:MM") 11 | .setRequired(true), 12 | ) 13 | .addStringOption((option) => 14 | option 15 | .setName("content") 16 | .setDescription("Enter what the reminder is for") 17 | .setRequired(true), 18 | ); 19 | 20 | module.exports = { 21 | data: baseCommand, 22 | 23 | async execute(interaction) { 24 | const datetime = interaction.options.getString("datetime"); 25 | 26 | const re = /^\d{4}-(0?[1-9]|1[012])-(0?[1-9]|[12][0-9]|3[01]) ([01]\d|2[0-3]):([0-5]\d)$/; 27 | 28 | // Check if datetime is valid 29 | if (!re.test(datetime)) { 30 | return await interaction.reply({ 31 | content: "Please enter the datetime as YYYY-MM-DD HH:MM exactly", 32 | ephemeral: true, 33 | }); 34 | } 35 | 36 | const send_time = new Date(datetime); 37 | const today = new Date(); 38 | const now_time = new Date( 39 | today.getFullYear(), 40 | today.getMonth(), 41 | today.getDate(), 42 | today.getHours(), 43 | today.getMinutes(), 44 | 0, 45 | ); 46 | 47 | const time_send_in = send_time - now_time; 48 | 49 | if (time_send_in <= 0) { 50 | return await interaction.reply({ 51 | content: "Please enter a datetime in the future for 'datetime'", 52 | ephemeral: true, 53 | }); 54 | } 55 | 56 | const iconUrl = "https://avatars.githubusercontent.com/u/164179?s=200&v=4"; 57 | const botName = "CSESOCBOT"; 58 | 59 | const reminderRequestEmbed = new Embed() 60 | .setColor(0xffe5b4) 61 | .setTitle("Reminder has been queued!") 62 | .setAuthor({ name: botName, iconURL: iconUrl }) 63 | .addFields( 64 | { name: "Requested by", value: interaction.user.toString(), inline: true }, 65 | { 66 | name: "For the date", 67 | value: "", 68 | inline: true, 69 | }, 70 | ) 71 | .setTimestamp(); 72 | 73 | const reminderEmbed = new Embed() 74 | .setColor(0xffe5b4) 75 | .setTitle("⌛ Reminder ⌛") 76 | .setAuthor({ name: botName, iconURL: iconUrl }) 77 | .addFields({ 78 | name: "❗ Reminding you to ❗", 79 | value: interaction.options.getString("content"), 80 | }) 81 | .setTimestamp(); 82 | 83 | await interaction.reply({ embeds: [reminderRequestEmbed], ephemeral: true }); 84 | 85 | const message = interaction.user; 86 | 87 | const sleep = async (ms) => await new Promise((r) => setTimeout(r, ms)); 88 | await sleep(time_send_in); 89 | 90 | console.log("Finished sleeping after " + time_send_in / 1000 + " seconds"); 91 | message.send({ embeds: [reminderEmbed] }); 92 | }, 93 | }; 94 | -------------------------------------------------------------------------------- /commands/rolesPermOverride.js: -------------------------------------------------------------------------------- 1 | const { SlashCommandBuilder } = require("@discordjs/builders"); 2 | const { PermissionsBitField } = require("discord.js"); 3 | 4 | const is_valid_course_name = (course) => { 5 | const reg_comp_course = /^comp\d{4}$/; 6 | const reg_math_course = /^math\d{4}$/; 7 | const reg_binf_course = /^binf\d{4}$/; 8 | const reg_engg_course = /^engg\d{4}$/; 9 | const reg_seng_course = /^seng\d{4}$/; 10 | const reg_desn_course = /^desn\d{4}$/; 11 | return ( 12 | reg_comp_course.test(course.toLowerCase()) || 13 | reg_math_course.test(course.toLowerCase()) || 14 | reg_binf_course.test(course.toLowerCase()) || 15 | reg_engg_course.test(course.toLowerCase()) || 16 | reg_seng_course.test(course.toLowerCase()) || 17 | reg_desn_course.test(course.toLowerCase()) 18 | ); 19 | }; 20 | 21 | const in_overwrites = (overwrites, id) => 22 | [1024n, 3072n].includes(overwrites.find((v, k) => k === id)?.allow?.bitfield); 23 | 24 | async function editChannels(interaction, channels) { 25 | for (const data of channels) { 26 | const channel = data[1]; 27 | 28 | if (!channel) continue; 29 | 30 | const is_valid = is_valid_course_name(channel.name); 31 | 32 | if (!is_valid || channel.type !== "GUILD_TEXT") continue; 33 | 34 | let role = interaction.guild.roles.cache.find( 35 | (r) => r.name.toLowerCase() === channel.name.toLowerCase(), 36 | ); 37 | 38 | if (!role) { 39 | role = await interaction.guild.roles.create({ 40 | name: channel.name.toLowerCase(), 41 | color: "BLUE", 42 | }); 43 | } 44 | 45 | const permissions = channel.permissionOverwrites.cache; 46 | 47 | // clear every individual user permission overwrite for the channel 48 | for (const user of channel.members) { 49 | const userId = user[0]; 50 | const userObj = user[1]; 51 | 52 | if (userObj.user.bot) continue; 53 | 54 | // Check if the member has access via individual perms 55 | if (in_overwrites(permissions, userId)) { 56 | // Remove the member from the channel's permission overwrites 57 | await channel.permissionOverwrites.delete(userObj); 58 | await userObj.roles.add(role); 59 | } 60 | } 61 | 62 | // set the permissions for the new role on a channel 63 | // const channel = interaction.guild.channels.cache.get('CHANNEL_ID'); 64 | await channel.permissionOverwrites.create(role, { 65 | VIEW_CHANNEL: true, 66 | SEND_MESSAGES: true, 67 | }); 68 | } 69 | } 70 | 71 | async function isFixed(interaction, channel) { 72 | const is_valid = is_valid_course_name(channel.name); 73 | 74 | if (!is_valid || channel.type !== "GUILD_TEXT") return true; 75 | 76 | const role = interaction.guild.roles.cache.find( 77 | (r) => r.name.toLowerCase() === channel.name.toLowerCase(), 78 | ); 79 | 80 | if (!role) return false; 81 | 82 | const permissions = channel.permissionOverwrites.cache; 83 | 84 | // clear every individual user permission overwrite for the channel 85 | for (const user of channel.members) { 86 | const userId = user[0]; 87 | const userObj = user[1]; 88 | 89 | if (userObj.user.bot) continue; 90 | 91 | // Check if the member has access via individual perms 92 | if (in_overwrites(permissions, userId)) return false; 93 | } 94 | 95 | if (!in_overwrites(permissions, role.id)) return false; 96 | 97 | return true; 98 | } 99 | 100 | async function allFixed(interaction, channels) { 101 | const unfixed = []; 102 | for (const data of channels) { 103 | const channel = data[1]; 104 | 105 | if (!channel) continue; 106 | 107 | const fixed = await isFixed(interaction, channel); 108 | 109 | if (!fixed) unfixed.push(channel.name); 110 | } 111 | 112 | return unfixed; 113 | } 114 | 115 | module.exports = { 116 | data: new SlashCommandBuilder() 117 | .setName("rolespermoverride") 118 | .setDescription( 119 | "Looks for matches between roles and course chats and attaches permissions.", 120 | ) 121 | .addBooleanOption((option) => 122 | option 123 | .setName("singlechannel") 124 | .setDescription( 125 | "Should this command only be run on the current channel? (Default: False)", 126 | ) 127 | .setRequired(false), 128 | ) 129 | .addBooleanOption((option) => 130 | option 131 | .setName("check") 132 | .setDescription( 133 | "Should a check be run on if the channel is fixed? (Default: False)", 134 | ) 135 | .setRequired(false), 136 | ), 137 | async execute(interaction) { 138 | try { 139 | if (!interaction.member.permissions.has(PermissionsBitField.Flags.Administrator)) { 140 | return await interaction.reply({ 141 | content: "You do not have permission to execute this command.", 142 | ephemeral: true, 143 | }); 144 | } 145 | 146 | await interaction.deferReply(); 147 | 148 | // for all roles with name == chat name involving 4 letter prefix comp, seng, engg or binf, 149 | 150 | if (!interaction.options.getBoolean("singlechannel")) { 151 | // Get all channels and run specified function 152 | const channels = interaction.guild.channels.cache; 153 | 154 | if (!interaction.options.getBoolean("check")) { 155 | await editChannels(interaction, channels); 156 | await interaction.editReply( 157 | "Successfully ported all user permissions to roles.", 158 | ); 159 | } else { 160 | const unfixed = await allFixed(interaction, channels); 161 | 162 | if (unfixed.length === 0) { 163 | await interaction.editReply("All channels in this server appear fixed."); 164 | } else { 165 | await interaction.editReply( 166 | `The following channels appear unfixed: ${unfixed.join(", ")}`, 167 | ); 168 | } 169 | } 170 | } else { 171 | const channel = interaction.channel; 172 | 173 | if (!interaction.options.getBoolean("check")) { 174 | await editChannels(interaction, [[undefined, channel]]); 175 | await interaction.editReply( 176 | "Successfully ported user permissions to roles in this channel", 177 | ); 178 | } else { 179 | const fixed = await isFixed(interaction, channel); 180 | 181 | if (fixed) { 182 | await interaction.editReply("This channel appears fixed."); 183 | } else { 184 | await interaction.editReply("This channel appears unfixed."); 185 | } 186 | } 187 | } 188 | } catch (error) { 189 | await interaction.editReply("Error: " + error); 190 | } 191 | }, 192 | }; 193 | -------------------------------------------------------------------------------- /commands/tictactoe.js: -------------------------------------------------------------------------------- 1 | const { SlashCommandBuilder } = require("@discordjs/builders"); 2 | const { createGame } = require("../lib/tictactoe/tttHelper"); 3 | 4 | const baseCommand = new SlashCommandBuilder() 5 | .setName("tictactoe") 6 | .setDescription("Start a game of tictactoe"); 7 | 8 | module.exports = { 9 | data: baseCommand, 10 | execute: createGame, 11 | }; 12 | -------------------------------------------------------------------------------- /commands/vote.js: -------------------------------------------------------------------------------- 1 | const { SlashCommandBuilder } = require("@discordjs/builders"); 2 | const { EmbedBuilder } = require("discord.js"); 3 | let { data } = require("../config/votes.json"); 4 | const fs = require("fs"); 5 | 6 | module.exports = { 7 | data: new SlashCommandBuilder() 8 | .setName("voting") 9 | .setDescription("Manage votes") 10 | .addSubcommand((subcommand) => 11 | subcommand 12 | .setName("vote") 13 | .setDescription("Starts a vote") 14 | .addStringOption((option) => 15 | option 16 | .setName("votestring") 17 | .setDescription("Message you want to vote") 18 | .setRequired(true), 19 | ), 20 | ) 21 | .addSubcommand((subcommand) => 22 | subcommand 23 | .setName("voteresult") 24 | .setDescription("Result of the last vote done on the channel"), 25 | ) 26 | .addSubcommand((subcommand) => 27 | subcommand 28 | .setName("voteresultfull") 29 | .setDescription( 30 | "Full result of the last vote done on the channel (includes the discord names)", 31 | ), 32 | ), 33 | 34 | async execute(interaction) { 35 | // Starting a vote 36 | if (interaction.options.getSubcommand() === "vote") { 37 | // Getting the required string and data from the input 38 | let votestring = await interaction.options.getString("votestring"); 39 | const voteauthorid = interaction.user.id; 40 | const voteauthorname = interaction.user.username; 41 | const channelid = interaction.channelId; 42 | 43 | // Generating the vote string 44 | votestring = votestring + ", vote by " + voteauthorname; 45 | 46 | // Generating the embed 47 | const embed = new EmbedBuilder().setTitle(votestring); 48 | const message = await interaction.reply({ 49 | embeds: [embed], 50 | fetchReply: true, 51 | }); 52 | // Adding the default reacts 53 | message.react("👍"); 54 | message.react("👎"); 55 | 56 | const messageid = message.id; 57 | 58 | // Writing to the data file 59 | data.unshift({ 60 | string: votestring, 61 | authorid: voteauthorid, 62 | channelid: channelid, 63 | messageid: messageid, 64 | }); 65 | fs.writeFileSync("./config/votes.json", JSON.stringify({ data: data }, null, 4)); 66 | } else if (interaction.options.getSubcommand() === "voteresult") { 67 | // Get the last messageid of the vote done on this channel 68 | const channelid = interaction.channelId; 69 | 70 | // Finding the required vote 71 | const found = data.find((element) => element.channelid == channelid); 72 | 73 | if (found == undefined) { 74 | const embed = new EmbedBuilder().setTitle("0 votes found on this channel"); 75 | await interaction.reply({ embeds: [embed], fetchReply: true }); 76 | return; 77 | } else { 78 | const channelID = found.channelid; 79 | const messageID = found.messageid; 80 | 81 | const msghandler = interaction.channel.messages; 82 | const msg = await msghandler.fetch(found.messageid); 83 | 84 | const cacheChannel = msg.guild.channels.cache.get(channelID); 85 | 86 | if (cacheChannel) { 87 | cacheChannel.messages.fetch(messageID).then((reactionMessage) => { 88 | const responses = []; 89 | reactionMessage.reactions.cache.forEach(function (value, key) { 90 | if (key == "👍" || key == "👎") { 91 | responses.push({ 92 | name: String(key), 93 | value: String(value.count - 1), 94 | }); 95 | } else { 96 | responses.push({ 97 | name: String(key), 98 | value: String(value.count), 99 | }); 100 | } 101 | }); 102 | const embed = new EmbedBuilder() 103 | .setTitle(found.string) 104 | .addFields(responses); 105 | 106 | (async () => { 107 | await interaction.reply({ embeds: [embed] }); 108 | })(); 109 | }); 110 | } else { 111 | // If an error occurs, Delete everything on the file 112 | await interaction.reply("An error occurred"); 113 | data = []; 114 | fs.writeFileSync( 115 | "./config/votes.json", 116 | JSON.stringify({ data: data }, null, 4), 117 | ); 118 | } 119 | } 120 | 121 | // await interaction.reply("Done"); 122 | } else if (interaction.options.getSubcommand() === "voteresultfull") { 123 | // Returns the list of all users who voted 124 | // Get the last messageid of the vote done on this channel 125 | const channelid = interaction.channelId; 126 | 127 | const found = data.find((element) => element.channelid == channelid); 128 | 129 | if (found == undefined) { 130 | const embed = new EmbedBuilder().setTitle("0 votes found on this channel"); 131 | await interaction.reply({ embeds: [embed], fetchReply: true }); 132 | return; 133 | } else { 134 | const channelID = found.channelid; 135 | const messageID = found.messageid; 136 | 137 | const msghandler = interaction.channel.messages; 138 | const msg = await msghandler.fetch(found.messageid); 139 | 140 | const cacheChannel = msg.guild.channels.cache.get(channelID); 141 | 142 | if (cacheChannel) { 143 | cacheChannel.messages.fetch(messageID).then((reactionMessage) => { 144 | const responses = []; 145 | reactionMessage.reactions.cache.forEach(function (value, key) { 146 | const temp = {}; 147 | temp["name"] = String(key); 148 | temp["value"] = ""; 149 | value.users.cache.forEach((value_) => { 150 | if (value_.bot == false) { 151 | temp["value"] = temp["value"] + "\n" + String(value_.username); 152 | } 153 | }); 154 | if (temp["value"] == "") { 155 | temp["value"] = "None"; 156 | } 157 | responses.push(temp); 158 | }); 159 | const embed = new EmbedBuilder() 160 | .setTitle(found.string) 161 | .addFields(responses); 162 | 163 | (async () => { 164 | await interaction.reply({ embeds: [embed] }); 165 | })(); 166 | }); 167 | } else { 168 | await interaction.reply("An error occurred"); 169 | data = []; 170 | fs.writeFileSync( 171 | "./config/votes.json", 172 | JSON.stringify({ data: data }, null, 4), 173 | ); 174 | } 175 | } 176 | 177 | // await interaction.reply("Done"); 178 | } 179 | }, 180 | }; 181 | -------------------------------------------------------------------------------- /commands/xkcd.js: -------------------------------------------------------------------------------- 1 | const { SlashCommandBuilder } = require("@discordjs/builders"); 2 | const { EmbedBuilder } = require("discord.js"); 3 | 4 | module.exports = { 5 | data: new SlashCommandBuilder() 6 | .setName("xkcd") 7 | .setDescription("Replies with a new xkcd joke!") 8 | .addSubcommand((subcommand) => 9 | subcommand.setName("latest").setDescription("Get the latest xkcd comic."), 10 | ) 11 | .addSubcommand((subcommand) => 12 | subcommand 13 | .setName("get") 14 | .setDescription("Get xkcd comic by its id.") 15 | .addIntegerOption((option) => 16 | option 17 | .setName("comic-id") 18 | .setRequired(true) 19 | .setDescription("The number id of the xkcd comic you want to get"), 20 | ), 21 | ) 22 | .addSubcommand((subcommand) => 23 | subcommand.setName("random").setDescription("Get a random xkcd comic."), 24 | ), 25 | async execute(interaction) { 26 | const xkcd = require("xkcd-api"); 27 | 28 | if (interaction.options.getSubcommand() === "latest") { 29 | xkcd.latest(async function (error, response) { 30 | if (error) { 31 | console.log(error); 32 | interaction.reply({ 33 | content: `sorry something went wrong!😔`, 34 | ephemeral: true, 35 | }); 36 | } else { 37 | const embed = new EmbedBuilder() 38 | .setTitle(response.safe_title) 39 | .setImage(response.img); 40 | return await interaction.reply({ embeds: [embed] }); 41 | } 42 | }); 43 | } else if (interaction.options.getSubcommand() === "get") { 44 | const comic_id = interaction.options.getInteger("comic-id"); 45 | 46 | xkcd.get(comic_id, async function (error, response) { 47 | if (error) { 48 | console.log(error); 49 | interaction.reply({ 50 | content: error, 51 | ephemeral: true, 52 | }); 53 | } else { 54 | const embed = new EmbedBuilder() 55 | .setTitle(response.safe_title) 56 | .setImage(response.img); 57 | return await interaction.reply({ embeds: [embed] }); 58 | } 59 | }); 60 | } else if (interaction.options.getSubcommand() === "random") { 61 | xkcd.random(async function (error, response) { 62 | if (error) { 63 | console.log(error); 64 | interaction.reply({ 65 | content: `sorry something went wrong!😔`, 66 | ephemeral: true, 67 | }); 68 | } else { 69 | const embed = new EmbedBuilder() 70 | .setTitle(response.safe_title) 71 | .setImage(response.img); 72 | return await interaction.reply({ embeds: [embed] }); 73 | } 74 | }); 75 | } 76 | }, 77 | }; 78 | -------------------------------------------------------------------------------- /config/anon_channel.json: -------------------------------------------------------------------------------- 1 | { 2 | "allowedChannels": [] 3 | } 4 | -------------------------------------------------------------------------------- /config/carrotboard.yaml: -------------------------------------------------------------------------------- 1 | leaderboard_message_id: 994388141614047363 2 | leaderboard_channel_id: 979516523335016509 3 | carrotboard_alert_channel_id: 894267846106963998 4 | guild_id: 884747109935497236 5 | carrot_emoji: 🥳 6 | minimum_carrot_count: 999999 7 | minimum_pin_count: 999999999999 8 | -------------------------------------------------------------------------------- /config/cointoss_images/heads.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csesoc/discord-bot/08f9bd745351ea201d5ad0a416f851be8c50d6db/config/cointoss_images/heads.png -------------------------------------------------------------------------------- /config/cointoss_images/tails.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csesoc/discord-bot/08f9bd745351ea201d5ad0a416f851be8c50d6db/config/cointoss_images/tails.png -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | #################################################################### 2 | # Database Settings # 3 | #################################################################### 4 | 5 | user: user 6 | dbname: bot 7 | password: pass 8 | host: 0.0.0.0 9 | port: 40041 10 | -------------------------------------------------------------------------------- /config/handbook.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiURL": "https://circlesapi.csesoc.app", 3 | "handbookURL": "https://www.handbook.unsw.edu.au/undergraduate/courses/2024" 4 | } 5 | -------------------------------------------------------------------------------- /config/lunch_buddy.json: -------------------------------------------------------------------------------- 1 | { 2 | "voteOriginId": "959995388289495050", 3 | "threadDestinationId": "959995388289495050", 4 | "interactionTimeout": 360000, 5 | "cronString": "" 6 | } -------------------------------------------------------------------------------- /config/lunch_buddy_locations.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "value": "Upper Campus Food Court", 4 | "sub": [ 5 | { 6 | "name": "Tropical Green Pho" 7 | }, 8 | { 9 | "name": "Pho House" 10 | }, 11 | { 12 | "name": "Classic Kebab" 13 | }, 14 | { 15 | "name": "Chinese Takeaway" 16 | }, 17 | { 18 | "name": "Tori Sushi" 19 | }, 20 | { 21 | "name": "Gradu-eat" 22 | }, 23 | { 24 | "name": "The Little Marionette Cafe" 25 | }, 26 | { 27 | "name": "Lhaksa Delight" 28 | }, 29 | { 30 | "name": "Bioscience building Cafe (XS Espresso)" 31 | } 32 | ] 33 | }, 34 | { 35 | "value": "Subway Zone", 36 | "sub": [ 37 | { 38 | "name": "Subway" 39 | }, 40 | { 41 | "name": "Boost" 42 | }, 43 | { 44 | "name": "Southern Wok" 45 | }, 46 | { 47 | "name": "Cafe Brioso" 48 | }, 49 | { 50 | "name": "Penny Lane" 51 | } 52 | ] 53 | }, 54 | { 55 | "value": "Quadrangle Food Court", 56 | "sub": [ 57 | { 58 | "name": "Soul Origin" 59 | }, 60 | { 61 | "name": "PappaRich" 62 | }, 63 | { 64 | "name": "Nene Chicken" 65 | }, 66 | { 67 | "name": "Plume Cafe" 68 | } 69 | ] 70 | }, 71 | { 72 | "value": "Lower Campus", 73 | "sub": [ 74 | { 75 | "name": "Stellinis Pasta Bar" 76 | }, 77 | { 78 | "name": "Guzman Y Gomez" 79 | }, 80 | { 81 | "name": "Mamak Village" 82 | }, 83 | { 84 | "name": "Yallah Eats Kebab and Shawarma" 85 | }, 86 | { 87 | "name": "Sharetea" 88 | }, 89 | { 90 | "name": "Maze Coffee & Food" 91 | }, 92 | { 93 | "name": "Campus Village Cafe" 94 | }, 95 | { 96 | "name": "Home Ground Kiosk" 97 | } 98 | ] 99 | }, 100 | { 101 | "value": "J17 Ainsworth", 102 | "sub": [ 103 | { 104 | "name": "Coffee on Campus Cafe" 105 | } 106 | ] 107 | }, 108 | { 109 | "value": "Other Options", 110 | "sub": [ 111 | { 112 | "name": "Sport" 113 | }, 114 | { 115 | "name": "On Campus Study" 116 | } 117 | ] 118 | } 119 | ] 120 | -------------------------------------------------------------------------------- /config/votes.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [] 3 | } 4 | -------------------------------------------------------------------------------- /config/wordle.json: -------------------------------------------------------------------------------- 1 | { 2 | "players": [] 3 | } -------------------------------------------------------------------------------- /config/wordle_images/blank_box.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csesoc/discord-bot/08f9bd745351ea201d5ad0a416f851be8c50d6db/config/wordle_images/blank_box.png -------------------------------------------------------------------------------- /config/wordle_images/clear_box.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csesoc/discord-bot/08f9bd745351ea201d5ad0a416f851be8c50d6db/config/wordle_images/clear_box.png -------------------------------------------------------------------------------- /config/wordle_images/empty_box.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csesoc/discord-bot/08f9bd745351ea201d5ad0a416f851be8c50d6db/config/wordle_images/empty_box.png -------------------------------------------------------------------------------- /config/wordle_images/green_box.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csesoc/discord-bot/08f9bd745351ea201d5ad0a416f851be8c50d6db/config/wordle_images/green_box.png -------------------------------------------------------------------------------- /config/wordle_images/grey_box.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csesoc/discord-bot/08f9bd745351ea201d5ad0a416f851be8c50d6db/config/wordle_images/grey_box.png -------------------------------------------------------------------------------- /config/wordle_images/yellow_box.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csesoc/discord-bot/08f9bd745351ea201d5ad0a416f851be8c50d6db/config/wordle_images/yellow_box.png -------------------------------------------------------------------------------- /data/createvc.json: -------------------------------------------------------------------------------- 1 | { 2 | "users": [], 3 | "channels": [] 4 | } -------------------------------------------------------------------------------- /data/log_report.csv: -------------------------------------------------------------------------------- 1 | Message_ID,User_ID,Username,Message,Original_Message,Deleted,Message_Sent,Channel_ID 2 | 3 | -------------------------------------------------------------------------------- /deploy-commands.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const { REST } = require("@discordjs/rest"); 3 | const { Routes } = require("discord-api-types/v9"); 4 | require("dotenv").config(); 5 | 6 | const commands = []; 7 | const commandFiles = fs.readdirSync("./commands").filter((file) => file.endsWith(".js")); 8 | 9 | for (const file of commandFiles) { 10 | const command = require(`./commands/${file}`); 11 | commands.push(command.data.toJSON()); 12 | } 13 | 14 | const rest = new REST({ version: "9" }).setToken(process.env.DISCORD_TOKEN); 15 | 16 | (async () => { 17 | try { 18 | console.log("Attempting to register application commands."); 19 | 20 | await rest.put(Routes.applicationCommands(process.env.APP_ID), { 21 | body: commands, 22 | }); 23 | 24 | console.log("Successfully registered application commands."); 25 | } catch (error) { 26 | console.error(error); 27 | } 28 | })(); 29 | -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | node deploy-commands.js 3 | node index.js -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import prettier from "eslint-plugin-prettier"; 2 | import globals from "globals"; 3 | import babelParser from "@babel/eslint-parser"; 4 | import path from "node:path"; 5 | import { fileURLToPath } from "node:url"; 6 | import js from "@eslint/js"; 7 | import { FlatCompat } from "@eslint/eslintrc"; 8 | 9 | const __filename = fileURLToPath(import.meta.url); 10 | const __dirname = path.dirname(__filename); 11 | const compat = new FlatCompat({ 12 | baseDirectory: __dirname, 13 | recommendedConfig: js.configs.recommended, 14 | allConfig: js.configs.all 15 | }); 16 | 17 | export default [...compat.extends("eslint:recommended", "prettier"), { 18 | plugins: { 19 | prettier, 20 | }, 21 | 22 | files: ["**/*.js"], 23 | 24 | ignores: [ 25 | "node_modules/**", 26 | ".puppeteerrc.cjs", 27 | ], 28 | 29 | languageOptions: { 30 | globals: { 31 | ...globals.node, 32 | }, 33 | 34 | parser: babelParser, 35 | ecmaVersion: 2021, 36 | sourceType: "script", 37 | 38 | parserOptions: { 39 | requireConfigFile: false, 40 | }, 41 | }, 42 | 43 | rules: { 44 | "arrow-spacing": ["warn", { 45 | before: true, 46 | after: true, 47 | }], 48 | 49 | "brace-style": ["error", "1tbs", { 50 | allowSingleLine: true, 51 | }], 52 | 53 | "comma-dangle": ["error", "always-multiline"], 54 | "comma-spacing": "error", 55 | "comma-style": "error", 56 | curly: ["error", "multi-line", "consistent"], 57 | "dot-location": ["error", "property"], 58 | "handle-callback-err": "off", 59 | indent: "off", 60 | "keyword-spacing": "error", 61 | 62 | "max-nested-callbacks": ["error", { 63 | max: 4, 64 | }], 65 | 66 | "max-statements-per-line": ["error", { 67 | max: 2, 68 | }], 69 | 70 | "no-console": "off", 71 | "no-empty-function": "error", 72 | "no-floating-decimal": "error", 73 | "no-inline-comments": "error", 74 | "no-lonely-if": "error", 75 | "no-multi-spaces": "error", 76 | 77 | "no-multiple-empty-lines": ["error", { 78 | max: 2, 79 | maxEOF: 1, 80 | maxBOF: 0, 81 | }], 82 | 83 | "no-shadow": ["error", { 84 | allow: ["err", "resolve", "reject"], 85 | }], 86 | 87 | "no-trailing-spaces": ["error"], 88 | "no-var": "error", 89 | "object-curly-spacing": ["error", "always"], 90 | "prefer-const": "error", 91 | "prettier/prettier": [ 92 | "error", 93 | { 94 | "endOfLine": "auto", 95 | } 96 | ], 97 | semi: ["error", "always"], 98 | "space-before-blocks": "error", 99 | "space-in-parens": "error", 100 | "space-infix-ops": "error", 101 | "space-unary-ops": "error", 102 | "spaced-comment": "error", 103 | "no-unused-vars": "warn", 104 | yoda: "error", 105 | }, 106 | }]; -------------------------------------------------------------------------------- /events/cb_messageDelete.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | module.exports = { 4 | name: "messageDelete", 5 | once: false, 6 | /** 7 | * @param {Message} message 8 | */ 9 | async execute(message) { 10 | // check if partial 11 | if (message.partial) { 12 | message = await message.fetch(); 13 | } 14 | 15 | /** @type {CarrotboardStorage} */ 16 | const cbStorage = global.cbStorage; 17 | 18 | // remove it from storage, and update leaderboard 19 | await cbStorage.db.del_entry(message.id, message.channelId); 20 | await cbStorage.updateLeaderboard(); 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /events/cb_reactionAdd.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | module.exports = { 4 | name: "messageReactionAdd", 5 | once: false, 6 | /** 7 | * @param {MessageReaction} reaction 8 | */ 9 | async execute(reaction) { 10 | // check if partial 11 | if (reaction.partial) { 12 | reaction = await reaction.fetch(); 13 | } 14 | 15 | /** @type {CarrotboardStorage} */ 16 | const cbStorage = global.cbStorage; 17 | const message = reaction.message; 18 | 19 | // make sure not a bot and not this client 20 | if (!message.author.bot && !reaction.me) { 21 | const emoji = reaction.emoji.toString(); 22 | const messageID = message.id; 23 | const channelID = message.channelId; 24 | const authorID = message.author.id; 25 | const messageContent = message.cleanContent.slice(0, cbStorage.maxMsgLen); 26 | 27 | // add to storage 28 | await cbStorage.db.add_value(emoji, messageID, authorID, channelID, messageContent); 29 | 30 | // get it from storage 31 | const entry = await cbStorage.db.get_by_msg_emoji(messageID, emoji); 32 | if (entry == null) { 33 | return; 34 | } 35 | 36 | // check whether its a pin 37 | if (emoji == cbStorage.pin) { 38 | if (Number(entry["count"]) == Number(cbStorage.config.pinMinimum)) { 39 | await message.pin(); 40 | // send pin alert 41 | await cbStorage.sendCBAlert(reaction, entry["carrot_id"], emoji); 42 | } 43 | } else if (Number(entry["count"]) == Number(cbStorage.config.minimum)) { 44 | // send normal alert 45 | await cbStorage.sendCBAlert(reaction, entry["carrot_id"], emoji); 46 | } 47 | 48 | await cbStorage.updateLeaderboard(); 49 | } 50 | }, 51 | }; 52 | -------------------------------------------------------------------------------- /events/cb_reactionRemove.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | module.exports = { 4 | name: "messageReactionRemove", 5 | once: false, 6 | /** 7 | * @param {MessageReaction} reaction 8 | */ 9 | async execute(reaction) { 10 | // check if partial 11 | if (reaction.partial) { 12 | reaction = await reaction.fetch(); 13 | } 14 | 15 | /** @type {CarrotboardStorage} */ 16 | const cbStorage = global.cbStorage; 17 | const message = reaction.message; 18 | 19 | // make sure not bot and not the current client 20 | if (!message.author.bot && !reaction.me) { 21 | // get the details 22 | const emoji = reaction.emoji.toString(); 23 | const messageID = message.id; 24 | const channelID = message.channelId; 25 | const authorID = message.author.id; 26 | 27 | // subtract from storage 28 | await cbStorage.db.sub_value(emoji, messageID, authorID, channelID); 29 | 30 | await cbStorage.updateLeaderboard(); 31 | } 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /events/cb_reactionRemoveAll.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | module.exports = { 4 | name: "messageReactionRemoveAll", 5 | once: false, 6 | /** 7 | * @param {Message} message 8 | */ 9 | async execute(message) { 10 | // check if partial 11 | if (message.partial) { 12 | message = await message.fetch(); 13 | } 14 | 15 | /** @type {CarrotboardStorage} */ 16 | const cbStorage = global.cbStorage; 17 | 18 | // remove it from storage, and update leaderboard 19 | await cbStorage.db.del_entry(message.id, message.channelId); 20 | await cbStorage.updateLeaderboard(); 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /events/cb_ready.js: -------------------------------------------------------------------------------- 1 | const { CarrotboardStorage } = require("../lib/carrotboard"); 2 | 3 | module.exports = { 4 | name: "ready", 5 | once: true, 6 | execute(client) { 7 | const cbStorage = new CarrotboardStorage(client); 8 | global.cbStorage = cbStorage; 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /events/channelCreate.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: "channelCreate", 3 | once: false, 4 | async execute(channel) { 5 | const logDB = global.logDB; 6 | logDB.channel_add(channel.id, channel.name); 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /events/channelDelete.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: "channelDelete", 3 | once: false, 4 | async execute(channel) { 5 | const logDB = global.logDB; 6 | logDB.channel_delete(channel.id); 7 | console.log("deleted channel" + channel.id); 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /events/channelUpdate.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: "channelUpdate", 3 | once: false, 4 | async execute(channel) { 5 | const logDB = global.logDB; 6 | const old_name = await logDB.channelname_get(channel.id); 7 | 8 | if (old_name != channel.name) { 9 | await logDB.channelname_update(channel.name, channel.id); 10 | } 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /events/coderunner.js: -------------------------------------------------------------------------------- 1 | const axios = require("axios"); 2 | const { Util } = require("discord.js"); 3 | 4 | module.exports = { 5 | name: "messageCreate", 6 | async execute(message) { 7 | if (message.content.startsWith("/run")) { 8 | const newlineIndex = message.content.indexOf("\n"); 9 | 10 | const language = message.content.substring(5, newlineIndex).toLowerCase(); 11 | 12 | // Message without the "/run language" part 13 | const rawContent = message.content.substring(newlineIndex + 1); 14 | 15 | const firstLine = rawContent.split("\n")[0]; 16 | const args = firstLine.startsWith("args") ? firstLine.substring(5).split(" ") : []; 17 | 18 | const lastLine = rawContent.split("\n").slice(-1)[0]; 19 | const stdin = lastLine.startsWith("stdin") ? lastLine.substring(6) : ""; 20 | 21 | // Remove the first and last line from rawContent 22 | // Remove extra lines for args and stdin if needed 23 | const code = rawContent 24 | .split("\n") 25 | .slice(args.length === 0 ? 1 : 2, stdin === "" ? -1 : -2) 26 | .join("\n"); 27 | 28 | let data = {}; 29 | try { 30 | const response = await axios.get("https://emkc.org/api/v2/piston/runtimes"); 31 | data = response.data; 32 | } catch (e) { 33 | return message.reply("Could not retrieve runtimes."); 34 | } 35 | 36 | const runtime = data.find((r) => r.language === language); 37 | 38 | if (!runtime) { 39 | return message.reply("Language not found."); 40 | } 41 | 42 | const version = runtime.version; 43 | 44 | try { 45 | const response = await axios.post("https://emkc.org/api/v2/piston/execute", { 46 | language: language, 47 | version: version, 48 | files: [{ content: code }], 49 | args: args, 50 | stdin: stdin, 51 | }); 52 | data = response.data; 53 | } catch (e) { 54 | return message.reply("Could not execute code."); 55 | } 56 | 57 | // Trim the output if it is too long 58 | const output = 59 | data.run.output.length > 1000 60 | ? data.run.output.substring(0, 1000) + 61 | `\n...${data.run.output.length - 1000} more characters` 62 | : data.run.output; 63 | 64 | if (!output) { 65 | return message.reply("No output."); 66 | } 67 | const code_output = Util.removeMentions(output); 68 | message.reply("Output:\n" + "```\n" + `${code_output}` + "```\n"); 69 | } 70 | }, 71 | }; 72 | -------------------------------------------------------------------------------- /events/course_removeWrongCommand.js: -------------------------------------------------------------------------------- 1 | const COURSE_CHATS_CHANNEL_ID = "860388285511630868"; 2 | 3 | module.exports = { 4 | name: "messageCreate", 5 | async execute(message) { 6 | try { 7 | /*eslint-disable */ 8 | if ( 9 | message.content.includes("/course") && 10 | message.channelId == COURSE_CHATS_CHANNEL_ID 11 | ) { 12 | const msg = 13 | "❌ Course command entered incorrectly. Please see the above messages on how to correctly give or remove a role."; 14 | 15 | // Send error and then delete it shortly afterwards 16 | // Can't send ephemeral messages though... 17 | await message 18 | .reply({ content: msg, ephemeral: true }) 19 | .then((msg) => { 20 | setTimeout(() => msg.delete(), 5000); 21 | }) 22 | .catch((e) => console.log("error: " + e)); 23 | 24 | return message.delete(); 25 | } 26 | } catch (e) { 27 | await message.reply("An error occurred: " + e); 28 | } 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /events/createvc.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | 3 | // A timer to delete the channels every 60mins 4 | // The function has 2 parts to it. The first part is that which deletes all the channels from the saved file that are deleted in the channel. 5 | // In the second part, we delete all the temporary channels that have no user in it. 6 | 7 | module.exports = { 8 | name: "ready", 9 | once: true, 10 | execute(client) { 11 | const timer = setInterval( 12 | function () { 13 | // Reading data from the file 14 | fs.readFile("./data/createvc.json", "utf-8", (err, jsonString) => { 15 | if (err) { 16 | console.log("Error reading file from disk:", err); 17 | return; 18 | } else { 19 | deleteExistentChannels(client, jsonString); 20 | } 21 | }); 22 | // Write back to the file 23 | }, 24 | 60 * 60 * 1000, 25 | ); 26 | }, 27 | }; 28 | 29 | function deleteExistentChannels(client, jsonString) { 30 | // Converting the data to a dictionary 31 | const data = JSON.parse(jsonString); 32 | 33 | // Deleting all the channels, that should have been deleted 34 | const b = data.channels.filter((e) => e.delete == true); 35 | b.forEach((f) => 36 | data.channels.splice( 37 | data.channels.findIndex((e) => e.delete === f.delete), 38 | 1, 39 | ), 40 | ); 41 | 42 | fs.writeFileSync( 43 | "./data/createvc.json", 44 | JSON.stringify({ users: data.users, channels: data.channels }, null, 4), 45 | ); 46 | 47 | data.channels.forEach((item) => { 48 | // item here is the channel id 49 | if (item.delete == false) { 50 | client.channels 51 | .fetch(item.channel_id) 52 | .then((channel) => { 53 | channel 54 | .fetch() 55 | .then((vcChannel) => { 56 | if (vcChannel.members.size == 0) { 57 | item.delete = true; 58 | fs.writeFileSync( 59 | "./data/createvc.json", 60 | JSON.stringify( 61 | { 62 | users: data.users, 63 | channels: data.channels, 64 | }, 65 | null, 66 | 4, 67 | ), 68 | ); 69 | vcChannel.delete().then(console.log).catch(console.error); 70 | } 71 | }) 72 | .catch(console.error); 73 | }) 74 | .catch(console.error); 75 | } 76 | }); 77 | } 78 | -------------------------------------------------------------------------------- /events/db_ready.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const { DBuser } = require("../lib/database/database"); 3 | const { CronJob } = require("cron"); 4 | 5 | const CSESOC_SERVER_ID = "693779865916276746"; 6 | // const TEST_SERVER_ID = "1220297696829509713"; 7 | 8 | module.exports = { 9 | name: "ready", 10 | once: true, 11 | async execute(client) { 12 | /** @type {DBuser} */ 13 | const userDB = new DBuser(); 14 | global.userDB = userDB; 15 | 16 | // Set up an automatic database check to see if there is any out of date roles. 17 | const role_job = new CronJob("0 0 12 * * *", async function () { 18 | console.log("Performing daily check of old roles at 12:00pm"); 19 | 20 | const old_roles = await userDB.checkTimeAssigned(); 21 | const guild = await client.guilds.fetch(CSESOC_SERVER_ID); 22 | const roles = await guild.roles.fetch(); 23 | 24 | for (const removed_role of old_roles) { 25 | try { 26 | const member = await guild.members.fetch(removed_role.userid); 27 | const role = roles.find((r) => r.name === removed_role.role_name); 28 | 29 | if (member && role) { 30 | await member.roles.remove(role); 31 | await userDB.remove_user_role(removed_role.userid, removed_role.role_name); 32 | // console.log(`Removed role ${removed_role.role_name} from user ${removed_role.userid}`); 33 | } else { 34 | console.log( 35 | `Could not find role ${removed_role.role_name} or user ${removed_role.userid}`, 36 | ); 37 | } 38 | } catch (error) { 39 | console.log(error); 40 | } 41 | } 42 | }); 43 | 44 | role_job.start(); 45 | }, 46 | }; 47 | -------------------------------------------------------------------------------- /events/faq_ready.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const { DBFaq } = require("../lib/database/faq"); 3 | 4 | module.exports = { 5 | name: "ready", 6 | once: true, 7 | async execute() { 8 | const faqStorage = new DBFaq(); 9 | global.faqStorage = faqStorage; 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /events/givereactrole.js: -------------------------------------------------------------------------------- 1 | const { EmbedBuilder } = require("discord.js"); 2 | 3 | module.exports = { 4 | name: "messageReactionAdd", 5 | once: false, 6 | async execute(reaction, user) { 7 | if (user.bot) return; 8 | 9 | if (reaction.partial) { 10 | try { 11 | await reaction.fetch(); 12 | } catch (error) { 13 | console.error("Something went wrong when fetching the message:", error); 14 | return; 15 | } 16 | } 17 | 18 | const messageId = reaction.message.id; 19 | 20 | const reactRoles = global.reactRoles; 21 | 22 | const data = await reactRoles.get_roles(messageId, reaction.emoji.name); 23 | 24 | // Return if message id and emoji doesn't match anything in the database 25 | if (data.length == 0) return; 26 | 27 | const roleId = data[0].role_id; 28 | 29 | const senderId = await reactRoles.get_sender(messageId); 30 | 31 | // Check if emoji is ⛔ and if the user is the sender 32 | if (reaction.emoji.name === "⛔" && user.id === senderId) return; 33 | 34 | // Check if message has ⛔ reacted by the sender 35 | // If not assign the role to the user 36 | const reactions = reaction.message.reactions; 37 | const noEntryReact = reactions.resolve("⛔"); 38 | if (noEntryReact) { 39 | noEntryReact.users.fetch().then(async (userList) => { 40 | if (userList.has(senderId)) { 41 | reactions.resolve(reaction).users.remove(user); 42 | 43 | const botName = await reaction.message.author.username; 44 | 45 | // Notify user that role was not assigned 46 | const notification = new EmbedBuilder() 47 | .setColor("#7cd699") 48 | .setTitle("Role could not be assigned") 49 | .setAuthor( 50 | botName, 51 | "https://avatars.githubusercontent.com/u/164179?s=200&v=4", 52 | ) 53 | .setDescription( 54 | `You can no longer react to the message in "${reaction.message.guild.name}" to get a role`, 55 | ); 56 | user.send({ 57 | embeds: [notification], 58 | }); 59 | } else { 60 | giveRole(reaction, user, roleId); 61 | } 62 | }); 63 | } else { 64 | giveRole(reaction, user, roleId); 65 | } 66 | }, 67 | }; 68 | 69 | async function giveRole(reaction, user, roleId) { 70 | try { 71 | reaction.message.guild.members.cache.get(user.id).roles.add(roleId); 72 | const roleName = await reaction.message.guild.roles.cache.find((r) => r.id === roleId).name; 73 | const botName = await reaction.message.author.username; 74 | 75 | // Notify user role was successfully added 76 | const notification = new EmbedBuilder() 77 | .setColor("#7cd699") 78 | .setTitle("Roles updated!") 79 | .setAuthor(botName, "https://avatars.githubusercontent.com/u/164179?s=200&v=4") 80 | .setDescription( 81 | `You reacted to a message in "${reaction.message.guild.name}" and were assigned the "${roleName}" role`, 82 | ); 83 | user.send({ 84 | embeds: [notification], 85 | }); 86 | } catch (err) { 87 | console.log(err); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /events/guildMemberAdd.js: -------------------------------------------------------------------------------- 1 | const { Events } = require("discord.js"); 2 | 3 | const CSESOC_SERVER_ID = "693779865916276746"; 4 | const REPORT_CHANNEL_ID = "1270283342176059443"; 5 | 6 | module.exports = { 7 | name: Events.GuildMemberAdd, 8 | once: false, 9 | execute(member) { 10 | /** @type {DBuser} */ 11 | const userDB = global.userDB; 12 | 13 | // Get report channel 14 | if (member.user.bot || member.user.system || member.guild.id !== CSESOC_SERVER_ID) return; 15 | 16 | // Get old user info before joining 17 | userDB.get_user_info(member.id).then((user_data) => { 18 | userDB.user_join(member.id).then((joinType) => { 19 | if (joinType === "rejoin") { 20 | // Fetch the channel to output details 21 | const reportChannel = member.guild.channels.cache.get(REPORT_CHANNEL_ID); 22 | 23 | // Fetch formatted date values from joining and leaving events 24 | const joinDate = user_data.joinDate.toLocaleDateString("en-AU"); 25 | const leaveDate = user_data.leaveDate.toLocaleDateString("en-AU"); 26 | 27 | reportChannel.send( 28 | `${member.user} (${member.user.tag}) has rejoined the server. [Last in server: ${leaveDate}, Last joined: ${joinDate}]`, 29 | ); 30 | } 31 | }); 32 | }); 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /events/guildMemberRemove.js: -------------------------------------------------------------------------------- 1 | const { Events } = require("discord.js"); 2 | 3 | module.exports = { 4 | name: Events.GuildMemberRemove, 5 | once: false, 6 | execute(member) { 7 | /** @type {DBuser} */ 8 | const userDB = global.userDB; 9 | 10 | // Get report channel 11 | if (member.user.bot || member.user.system) return; 12 | 13 | userDB.user_leave(member.id); 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /events/log_ready.js: -------------------------------------------------------------------------------- 1 | const { DBlog } = require("../lib/database/dblog"); 2 | 3 | let currentStatusIndex = 0; 4 | const CSESOC_SERVER_ID = "693779865916276746"; 5 | const statusSeconds = 30; 6 | 7 | // In case of events working 8 | // let currentEventIndex = 0; 9 | // const events = ["EVENT"]; 10 | 11 | module.exports = { 12 | name: "ready", 13 | once: true, 14 | execute(client) { 15 | const guilds = client.guilds.cache.map((guild) => guild.id); 16 | const logDB = new DBlog(); 17 | global.logDB = logDB; 18 | 19 | (async () => { 20 | await logDB.create_tables(); 21 | for (let i = 0; i < guilds.length; i++) { 22 | const g = client.guilds.cache.get(guilds[i]); 23 | const channels = g.channels.cache; 24 | 25 | const channels_arr = [...channels.values()]; 26 | const channels_filtered = channels_arr.filter((c) => c.type === "GUILD_TEXT"); 27 | 28 | for (const m in channels_filtered) { 29 | // console.log(channels_filtered[m].id, channels_filtered[m].name); 30 | logDB.channel_add(channels_filtered[m].id, channels_filtered[m].name, g.id); 31 | } 32 | } 33 | })(); 34 | 35 | // Status change functions 36 | const statusFunctions = [ 37 | () => memberCountStatus(client), 38 | // () => specialEventStatus(client, events[currentEventIndex]), 39 | ]; 40 | 41 | setInterval(() => { 42 | statusFunctions[currentStatusIndex](); 43 | currentStatusIndex = (currentStatusIndex + 1) % statusFunctions.length; 44 | }, 1000 * statusSeconds); 45 | }, 46 | }; 47 | 48 | function memberCountStatus(client) { 49 | const server = client.guilds.cache.get(CSESOC_SERVER_ID); 50 | if (!server) { 51 | return; 52 | } 53 | 54 | client.user.setActivity(`${server.memberCount} members!`, { type: "LISTENING" }); 55 | } 56 | 57 | // function specialEventStatus(client, event) { 58 | // client.user.setActivity(event, { type: "COMPETING" }); 59 | // currentEventIndex = (currentEventIndex + 1) % events.length; 60 | // } 61 | -------------------------------------------------------------------------------- /events/messageCreate.js: -------------------------------------------------------------------------------- 1 | function messagelog(message) { 2 | // ignore messages sent from bot 3 | if (message.author.bot) { 4 | return; 5 | } 6 | 7 | const logDB = global.logDB; 8 | logDB.message_create( 9 | message.id, 10 | message.author.id, 11 | message.author.username, 12 | message.content, 13 | message.channelId, 14 | ); 15 | } 16 | 17 | module.exports = { 18 | name: "messageCreate", 19 | async execute(message) { 20 | const standupDB = global.standupDBGlobal; 21 | 22 | messagelog(message); 23 | 24 | if (message.content.startsWith("$standup")) { 25 | // Get standup content 26 | const messages = String(message.content); 27 | const messageContent = messages.slice(8).trim(); 28 | // console.log(message.channel.parent.name) 29 | 30 | const teamId = message.channel.parentId; 31 | 32 | const standupAuthorId = message.author.id; 33 | 34 | await standupDB.addStandup(teamId, standupAuthorId, message.id, messageContent); 35 | } 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /events/messageDelete.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: "messageDelete", 3 | once: false, 4 | async execute(message) { 5 | // ignore messages sent from bot 6 | if (message.author.bot) { 7 | return; 8 | } 9 | 10 | const logDB = global.logDB; 11 | logDB.message_delete(message.id); 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /events/messageUpdate.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: "messageUpdate", 3 | async execute(_oldMessage, message) { 4 | // console.log(message); 5 | if (message.author.bot == true) { 6 | return; 7 | } 8 | const standupDB = global.standupDBGlobal; 9 | 10 | const logDB = global.logDB; 11 | logDB.message_update(_oldMessage.id, message.id, message.content); 12 | 13 | if (message.content.startsWith("$standup")) { 14 | const messages = String(message.content); 15 | const messageContent = messages.slice(8).trim(); 16 | 17 | const teamId = message.channel.parentId; 18 | 19 | const standupAuthorId = message.author.id; 20 | 21 | const standupExists = await standupDB.thisStandupExists(message.id); 22 | const numDaysToRetrieve = 7; 23 | const alreadyStandup = await standupDB.getStandups(teamId, numDaysToRetrieve); 24 | 25 | alreadyStandup.filter((st) => st.user_id == message.author.id); 26 | const latestStandup = new Date(_oldMessage.createdTimestamp); 27 | 28 | // if this standup exists, update the row else insert new row 29 | if (standupExists) { 30 | await standupDB.updateStandup(message.id, messageContent); 31 | } else if (latestStandup > alreadyStandup[0].time_stamp) { 32 | await standupDB.addStandup(teamId, standupAuthorId, message.id, messageContent); 33 | } 34 | 35 | // const mentions = message.mentions.users; 36 | // const mentionsArr = [...mentions.values()]; 37 | 38 | // Contains the list of all users mentioned in the message 39 | // const result = mentionsArr.map((a) => a.id); 40 | } 41 | }, 42 | }; 43 | -------------------------------------------------------------------------------- /events/reactrole_read.js: -------------------------------------------------------------------------------- 1 | const { DBReactRole } = require("../lib/database/dbreactrole"); 2 | 3 | module.exports = { 4 | name: "ready", 5 | once: true, 6 | async execute() { 7 | const reactRoles = new DBReactRole(); 8 | global.reactRoles = reactRoles; 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /events/ready.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: "ready", 3 | once: true, 4 | execute(client) { 5 | console.log("------------------------------------------------------------"); 6 | console.log(`Logged in as ${client.user.tag} (ID: ${client.user.id}).`); 7 | console.log(`Connected to ${client.guilds.cache.size} guilds:`); 8 | for (const guild of client.guilds.cache.values()) { 9 | console.log(`- ${guild.name}`); 10 | } 11 | console.log(`Loaded ${client.commands.size} commands:`); 12 | for (const command of client.commands.values()) { 13 | console.log(`- ${command.data.name}`); 14 | } 15 | console.log("------------------------------------------------------------"); 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /events/removereactrole.js: -------------------------------------------------------------------------------- 1 | const { EmbedBuilder } = require("discord.js"); 2 | 3 | module.exports = { 4 | name: "messageReactionRemove", 5 | once: false, 6 | async execute(reaction, user) { 7 | if (user.bot) return; 8 | 9 | if (reaction.partial) { 10 | try { 11 | await reaction.fetch(); 12 | } catch (error) { 13 | console.error("Something went wrong when fetching the message:", error); 14 | return; 15 | } 16 | } 17 | 18 | const messageId = reaction.message.id; 19 | 20 | const reactRoles = global.reactRoles; 21 | 22 | const data = await reactRoles.get_roles(messageId, reaction.emoji.name); 23 | 24 | // Return if message id and emoji doesn't match anything in the database 25 | if (data.length == 0) return; 26 | 27 | const roleId = data[0].role_id; 28 | 29 | const senderId = await reactRoles.get_sender(messageId); 30 | 31 | // Check if message has ⛔ reacted by the sender and if the user already has the role 32 | // If so remove the role from the user 33 | const noEntryReact = reaction.message.reactions.resolve("⛔"); 34 | if (noEntryReact) { 35 | try { 36 | noEntryReact.users.fetch().then(async (userList) => { 37 | const hasRole = await reaction.message.guild.members.cache 38 | .get(user.id) 39 | ._roles.includes(roleId); 40 | if (!userList.has(senderId) || hasRole) { 41 | removeRole(reaction, user, roleId); 42 | } 43 | }); 44 | } catch (err) { 45 | console.log(err); 46 | } 47 | } else { 48 | removeRole(reaction, user, roleId); 49 | } 50 | }, 51 | }; 52 | 53 | async function removeRole(reaction, user, roleId) { 54 | try { 55 | reaction.message.guild.members.cache.get(user.id).roles.remove(roleId); 56 | const roleName = await reaction.message.guild.roles.cache.find((r) => r.id === roleId).name; 57 | const botName = await reaction.message.author.username; 58 | 59 | // Notify user role was successfully removed 60 | const notification = new EmbedBuilder() 61 | .setColor("#7cd699") 62 | .setTitle("Roles updated!") 63 | .setAuthor(botName, "https://avatars.githubusercontent.com/u/164179?s=200&v=4") 64 | .setDescription( 65 | `You unreacted to a message in "${reaction.message.guild.name}" and were unassigned the "${roleName}" role`, 66 | ); 67 | user.send({ 68 | embeds: [notification], 69 | }); 70 | } catch (err) { 71 | console.log(err); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /events/role_removeWrongCommand.js: -------------------------------------------------------------------------------- 1 | const COURSE_CHATS_CHANNEL_ID = "860388285511630868"; 2 | 3 | module.exports = { 4 | name: "messageCreate", 5 | async execute(message) { 6 | try { 7 | /*eslint-disable */ 8 | if (message.content.includes("/role") && message.channelId == COURSE_CHATS_CHANNEL_ID) { 9 | const msg = 10 | "❌ Role command entered incorrectly. Please see the above messages on how to correctly give or remove a role."; 11 | 12 | // Send error and then delete it shortly afterwards 13 | // Can't send ephemeral messages though... 14 | await message 15 | .reply({ content: msg, ephemeral: true }) 16 | .then((msg) => { 17 | setTimeout(() => msg.delete(), 5000); 18 | }) 19 | .catch((e) => console.log("error: " + e)); 20 | 21 | return message.delete(); 22 | } 23 | } catch (e) { 24 | await message.reply("An error occurred: " + e); 25 | } 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /events/schedulepost_ready.js: -------------------------------------------------------------------------------- 1 | const { DBSchedulePost } = require("../lib/database/dbschedulepost"); 2 | 3 | module.exports = { 4 | name: "ready", 5 | once: true, 6 | async execute() { 7 | const schedulePost = new DBSchedulePost(); 8 | global.schedulePost = schedulePost; 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /events/sendscheduled.js: -------------------------------------------------------------------------------- 1 | const { AttachmentBuilder } = require("discord.js"); 2 | 3 | // Checks database every minute to see if there is a message to be sent 4 | 5 | module.exports = { 6 | name: "ready", 7 | once: true, 8 | execute(client) { 9 | setInterval(async () => { 10 | const today = new Date(); 11 | 12 | const year = String(today.getFullYear()).padStart(4, "0"); 13 | const month = String(today.getMonth() + 1).padStart(2, "0"); 14 | const day = String(today.getDate()).padStart(2, "0"); 15 | const hour = String(today.getHours()).padStart(2, "0"); 16 | const minute = String(today.getMinutes()).padStart(2, "0"); 17 | const now_time = `${year}-${month}-${day} ${hour}:${minute}`; 18 | 19 | const schedulePost = global.schedulePost; 20 | const scheduled = await schedulePost.get_scheduled(now_time); 21 | 22 | for (const post of scheduled) { 23 | try { 24 | const reminder = post.reminder; 25 | const send_channel = client.channels.cache.get(post.send_channel_id); 26 | const init_channel = client.channels.cache.get(post.init_channel_id); 27 | const send_msg = await init_channel.messages.fetch(post.msg_id); 28 | 29 | // Retrieve attachments if applicable 30 | const attachment_list = []; 31 | send_msg.attachments.forEach((attachment) => { 32 | attachment_list.push(new AttachmentBuilder(attachment.proxyURL)); 33 | }); 34 | 35 | // Retrieve message content 36 | let message_content = send_msg.content ? send_msg.content : " "; 37 | 38 | if (reminder) { 39 | message_content = 40 | send_msg.content + "\n \n react ⏰ to be notified about this event!"; 41 | } 42 | 43 | // Send the scheduled message 44 | send_channel 45 | .send({ 46 | content: message_content, 47 | files: attachment_list, 48 | }) 49 | .then(async (sent_message) => { 50 | if (reminder) { 51 | sent_message.react("⏰"); 52 | await schedulePost.add_reminder( 53 | sent_message.id, 54 | post.scheduled_post_id, 55 | ); 56 | } else { 57 | await schedulePost.remove_scheduled(post.scheduled_post_id); 58 | } 59 | }); 60 | } catch (err) { 61 | console.log("An error occured in sendscheduled.js " + err); 62 | } 63 | } 64 | }, 1000 * 60); 65 | }, 66 | }; 67 | -------------------------------------------------------------------------------- /events/sendscheduled_reminders.js: -------------------------------------------------------------------------------- 1 | const { EmbedBuilder } = require("discord.js"); 2 | 3 | // Checks database every minute to check if a reminder needs 4 | // to be sent to the users who reacted with an alarm clock emoji in the original 5 | // scheduled post. Reminders are direct messages to the user contaning the content 6 | // of the original message. 7 | 8 | module.exports = { 9 | name: "ready", 10 | once: true, 11 | execute(client) { 12 | setInterval(async () => { 13 | const today = new Date(); 14 | 15 | const year = String(today.getFullYear()).padStart(4, "0"); 16 | const month = String(today.getMonth() + 1).padStart(2, "0"); 17 | const day = String(today.getDate()).padStart(2, "0"); 18 | const hour = String(today.getHours()).padStart(2, "0"); 19 | const minute = String(today.getMinutes()).padStart(2, "0"); 20 | const now_time = `${year}-${month}-${day} ${hour}:${minute}`; 21 | 22 | const schedulePost = global.schedulePost; 23 | const reminders = await schedulePost.get_reminders(now_time); 24 | 25 | for (const reminder of reminders) { 26 | try { 27 | const sent_channel = await client.channels.fetch(reminder.send_channel_id); 28 | const sent_msg = await sent_channel.messages.fetch(reminder.sent_msg_id); 29 | 30 | const reaction = sent_msg.reactions.cache.get("⏰"); 31 | const users_reacted = await reaction.users.fetch(); 32 | 33 | users_reacted.forEach((user) => { 34 | if (!user.bot) { 35 | const reminder_msg = new EmbedBuilder() 36 | .setColor("#C492B1") 37 | .setTitle("Reminder") 38 | .setDescription( 39 | sent_msg.content.length === 0 ? " " : sent_msg.content, 40 | ); 41 | 42 | client.users.cache.get(user.id).send({ 43 | embeds: [reminder_msg], 44 | }); 45 | } 46 | }); 47 | await schedulePost.remove_scheduled(reminder.scheduled_post_id); 48 | } catch (err) { 49 | console.log("An error occured in sendscheduled_reminders.js " + err); 50 | } 51 | } 52 | }, 1000 * 60); 53 | }, 54 | }; 55 | -------------------------------------------------------------------------------- /events/standupReset.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: "ready", 3 | once: true, 4 | execute() { 5 | require("events").EventEmitter.prototype._maxListeners = 0; 6 | setInterval(async () => { 7 | try { 8 | const today = new Date(); 9 | 10 | const hour = String(today.getHours()).padStart(2, "0"); 11 | const minute = String(today.getMinutes()).padStart(2, "0"); 12 | const day = String(today.getDay()); 13 | 14 | const standupDB = global.standupDBGlobal; 15 | 16 | if (day == 4 && hour == 23 && minute == 55) { 17 | await standupDB.deleteAllStandups(); 18 | console.log("Standups reset @ Thurs 11:55pm"); 19 | } 20 | } catch (err) { 21 | console.log("An error occured in standupReset.js " + err); 22 | } 23 | }, 1000 * 60); 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /events/standup_ready.js: -------------------------------------------------------------------------------- 1 | const { DBstandup } = require("../lib/database/dbstandup"); 2 | 3 | module.exports = { 4 | name: "ready", 5 | once: true, 6 | execute() { 7 | const standupDBGlobal = new DBstandup(); 8 | global.standupDBGlobal = standupDBGlobal; 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /events/tictactoeButton.js: -------------------------------------------------------------------------------- 1 | // const { handleGameButton } = require("../lib/tictactoe/tttHelper"); 2 | 3 | // module.exports = { 4 | // once: false, 5 | // name: "interactionCreate", 6 | // execute(interaction) { 7 | // if (interaction.isButton() && interaction.message.interaction.commandName == "tictactoe") { 8 | // handleGameButton(interaction); 9 | // } else { 10 | // return; 11 | // } 12 | // }, 13 | // }; 14 | -------------------------------------------------------------------------------- /events/travelguide_ready.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const { DBTravelguide } = require("../lib/database/dbtravelguide"); 3 | 4 | module.exports = { 5 | name: "ready", 6 | once: true, 7 | async execute() { 8 | const travelguideStorage = new DBTravelguide(); 9 | global.travelguideStorage = travelguideStorage; 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const { Client, Collection, GatewayIntentBits } = require("discord.js"); 3 | require("dotenv").config(); 4 | 5 | // Create a new client instance 6 | const client = new Client({ 7 | intents: [ 8 | GatewayIntentBits.Guilds, 9 | GatewayIntentBits.GuildMembers, 10 | GatewayIntentBits.GuildMessages, 11 | GatewayIntentBits.GuildMessageReactions, 12 | GatewayIntentBits.GuildVoiceStates, 13 | GatewayIntentBits.GuildPresences, 14 | GatewayIntentBits.MessageContent, 15 | ], 16 | partials: ["MESSAGE", "CHANNEL", "REACTION", "GUILD_MEMBER", "USER"], 17 | }); 18 | // Add commands to the client 19 | client.commands = new Collection(); 20 | const commandFiles = fs.readdirSync("./commands").filter((file) => file.endsWith(".js")); 21 | 22 | for (const file of commandFiles) { 23 | const command = require(`./commands/${file}`); 24 | client.commands.set(command.data.name, command); 25 | } 26 | 27 | require("events").EventEmitter.defaultMaxListeners = 0; 28 | 29 | // Add events to the client 30 | const eventFiles = fs.readdirSync("./events").filter((file) => file.endsWith(".js")); 31 | 32 | for (const file of eventFiles) { 33 | const event = require(`./events/${file}`); 34 | if (event.once) { 35 | client.once(event.name, (...args) => event.execute(...args)); 36 | } else { 37 | client.on(event.name, (...args) => event.execute(...args)); 38 | } 39 | } 40 | 41 | // Handle commands 42 | client.on("interactionCreate", async (interaction) => { 43 | if (!interaction.isCommand()) return; 44 | 45 | const command = client.commands.get(interaction.commandName); 46 | 47 | if (!command) return; 48 | 49 | try { 50 | await command.execute(interaction); 51 | } catch (error) { 52 | console.error(error); 53 | await interaction.reply({ 54 | content: "There was an error while executing this command!", 55 | ephemeral: true, 56 | }); 57 | } 58 | }); 59 | 60 | client.on("shardError", (error) => { 61 | console.error("A websocket connection encountered an error:", error); 62 | }); 63 | 64 | client.login(process.env.DISCORD_TOKEN); 65 | -------------------------------------------------------------------------------- /lib/connect4/connect4Game.js: -------------------------------------------------------------------------------- 1 | class connect4GameObj { 2 | constructor(width = 7, height = 6) { 3 | this.height = height; 4 | this.width = width; 5 | this.board = []; 6 | this.players = [0, 0]; 7 | this.turnOf = 0; 8 | this.movesLeft = width * height; 9 | this.gameWon = false; 10 | this.winner = ""; 11 | this.surrendered = false; 12 | 13 | for (let i = 0; i < height; i++) { 14 | this.board[i] = []; 15 | for (let j = 0; j < width; j++) { 16 | this.board[i][j] = -1; 17 | } 18 | } 19 | } 20 | colIsFull(col) { 21 | if (col < 0 || col >= this.width) return true; 22 | return this.board[0][col] != -1; 23 | } 24 | insertInCol(col) { 25 | if (this.colIsFull(col)) return -1; 26 | let row = 0; 27 | while (row < this.height && this.board[row][col] == -1) { 28 | row++; 29 | } 30 | this.board[row - 1][col] = this.turnOf; 31 | this.turnOf = 1 - this.turnOf; 32 | this.movesLeft--; 33 | this.gameWon = this.checkWin(row - 1, col); 34 | return 0; 35 | } 36 | 37 | getGameWon() { 38 | return this.gameWon; 39 | } 40 | 41 | getMovesLeft() { 42 | return this.movesLeft; 43 | } 44 | 45 | setMovesLeftZero() { 46 | this.movesLeft = 0; 47 | } 48 | 49 | setWinner(winner) { 50 | this.winner = winner; 51 | } 52 | 53 | getWinner() { 54 | return this.winner; 55 | } 56 | 57 | checkWin(row, col) { 58 | if (row < 0 || row >= this.height || col < 0 || col >= this.width) { 59 | return false; 60 | } 61 | 62 | const target = this.board[row][col]; 63 | let count = 0; 64 | 65 | // check horizontal 66 | let x = col; 67 | let y = row; 68 | // go left 69 | while (x >= 0 && this.board[y][x] == target) { 70 | x--; 71 | count++; 72 | } 73 | x = col + 1; 74 | // go right 75 | while (x < this.width && this.board[y][x] == target) { 76 | x++; 77 | count++; 78 | } 79 | 80 | if (count >= 4) return true; 81 | 82 | count = 0; 83 | // check vertical 84 | x = col; 85 | // go up 86 | while (y >= 0 && this.board[y][x] == target) { 87 | y--; 88 | count++; 89 | } 90 | y = row + 1; 91 | // go down 92 | while (y < this.height && this.board[y][x] == target) { 93 | y++; 94 | count++; 95 | } 96 | 97 | if (count >= 4) return true; 98 | 99 | count = 0; 100 | // check diag top left, bottom right 101 | x = col; 102 | y = row; 103 | 104 | // go north-west 105 | while (y >= 0 && x >= 0 && this.board[y][x] == target) { 106 | x--; 107 | y--; 108 | count++; 109 | } 110 | // go south-east 111 | x = col + 1; 112 | y = row + 1; 113 | while (y < this.height && x < this.width && this.board[y][x] == target) { 114 | x++; 115 | y++; 116 | count++; 117 | } 118 | 119 | if (count >= 4) return true; 120 | count = 0; 121 | 122 | // check diag top right, bottom left 123 | x = col; 124 | y = row; 125 | 126 | // go north-east 127 | while (y >= 0 && x < this.width && this.board[y][x] == target) { 128 | x++; 129 | y--; 130 | count++; 131 | } 132 | // go south-west 133 | x = col - 1; 134 | y = row + 1; 135 | while (y < this.height && x >= 0 && this.board[y][x] == target) { 136 | x--; 137 | y++; 138 | count++; 139 | } 140 | 141 | if (count >= 4) return true; 142 | return false; 143 | } 144 | } 145 | 146 | module.exports = { connect4GameObj }; 147 | -------------------------------------------------------------------------------- /lib/connect4/connect4Runner.js: -------------------------------------------------------------------------------- 1 | const { connect4GameObj } = require("./connect4Game"); 2 | 3 | const maxNumGames = 1000; 4 | const symbols = ["⬛", "🔴", "🟡"]; 5 | const keycapEmojis = { 6 | "1️⃣": 0, 7 | "2️⃣": 1, 8 | "3️⃣": 2, 9 | "4️⃣": 3, 10 | "5️⃣": 4, 11 | "6️⃣": 5, 12 | "7️⃣": 6, 13 | "8️⃣": 7, 14 | "9️⃣": 8, 15 | "🔟": 9, 16 | }; 17 | const surrenderEmoji = "🏳️"; 18 | 19 | const selfId = process.env.APP_ID; 20 | const playTimeMinutes = 120; 21 | 22 | const connect4GamesAlive = new Object(); 23 | 24 | // function createGameId() { 25 | // const dateObj = new Date(); 26 | // return dateObj.getTime().toString(); 27 | // } 28 | 29 | function c4BoardToString(C4Game) { 30 | let boardRepr = ""; 31 | for (let i = 0; i < C4Game.width; i++) { 32 | boardRepr += Object.keys(keycapEmojis)[i] + " "; 33 | } 34 | boardRepr += "\n"; 35 | C4Game.board.forEach((boardRow) => { 36 | boardRow.forEach((el) => { 37 | // boardRepr += el + ' '; // for plaintext 38 | boardRepr += symbols[el + 1] + " "; 39 | }); 40 | boardRepr += "\n"; 41 | }); 42 | return boardRepr; 43 | } 44 | 45 | async function createConnect4(interaction) { 46 | if (!interaction.isCommand()) return; 47 | 48 | const newC4 = new connect4GameObj(); 49 | 50 | // limit on number of games; delete oldest game 51 | if (Object.keys(connect4GamesAlive).length >= maxNumGames) { 52 | delete connect4GamesAlive[Object.keys(connect4GamesAlive)[0]]; 53 | } 54 | 55 | const gameMsg = await interaction.reply({ 56 | content: c4BoardToString(newC4) + "\nWaiting for player 1.", 57 | fetchReply: true, 58 | }); 59 | 60 | connect4GamesAlive[gameMsg.id] = newC4; 61 | 62 | // create a collector on reacts 63 | const filter = () => { 64 | return true; 65 | }; 66 | const collector = gameMsg.createReactionCollector({ 67 | filter, 68 | time: playTimeMinutes * 60000, 69 | }); 70 | 71 | collector.on("collect", (reaction, user) => { 72 | handleConnect4React(reaction, user); 73 | }); 74 | 75 | // create initial reacts for game controls 76 | for (let i = 0; i < newC4.width; i++) { 77 | await gameMsg.react(Object.keys(keycapEmojis)[i]); 78 | } 79 | // create surrender react 80 | await gameMsg.react(surrenderEmoji); 81 | } 82 | 83 | async function handleConnect4React(reaction, user) { 84 | // ignore bot's initial reacts 85 | if (user.id == selfId) return; 86 | 87 | // When a reaction is received, check if the structure is partial 88 | if (reaction.partial) { 89 | // If the message this reaction belongs to was removed, the fetching might result in an API error which should be handled 90 | try { 91 | await reaction.fetch(); 92 | } catch (error) { 93 | console.error("Something went wrong when fetching the message:", error); 94 | // Return as `reaction.message.author` may be undefined/null 95 | return; 96 | } 97 | } 98 | 99 | const gameObj = connect4GamesAlive[reaction.message.id]; 100 | 101 | let gameMessage = ""; 102 | 103 | // game was over from before 104 | if (gameObj.getGameWon() && gameObj.getMovesLeft() == 0) { 105 | const winner = gameObj.getWinner(); 106 | if (gameObj.surrendered == true && gameObj.winner == "") { 107 | gameMessage = "Game ended"; 108 | } else if (gameObj.surrendered == true) { 109 | gameMessage = `<@${winner}> won by forfeit.`; 110 | } else { 111 | gameMessage = `Game already finished. <@${winner}> wins.`; 112 | } 113 | 114 | reaction.message.edit({ 115 | content: c4BoardToString(gameObj) + "\n" + gameMessage, 116 | }); 117 | 118 | reaction.users.remove(user.id); 119 | 120 | return; 121 | } 122 | 123 | // separate control flow for surrender move 124 | if (reaction.emoji.name == surrenderEmoji) { 125 | if (gameObj.players[0] == 0) { 126 | gameMessage = "No active players, cannot surrender."; 127 | } else if (user.id == gameObj.players[0]) { 128 | gameMessage = 129 | gameObj.players[1] == 0 130 | ? "Game ended." 131 | : `<@${gameObj.players[1]}> wins by forfeit.`; 132 | gameObj.gameWon = true; 133 | gameObj.setMovesLeftZero(); 134 | gameObj.surrendered = true; 135 | 136 | if (gameObj.players[1] != 0) { 137 | gameObj.setWinner(gameObj.players[1]); 138 | } 139 | } else if (user.id == gameObj.players[1]) { 140 | gameMessage = `<@${gameObj.players[0]}> wins by forfeit.`; 141 | gameObj.gameWon = true; 142 | gameObj.setWinner(gameObj.players[0]); 143 | gameObj.setMovesLeftZero(); 144 | gameObj.surrendered = true; 145 | } else { 146 | reaction.users.remove(user.id); 147 | return; 148 | } 149 | reaction.message.edit({ 150 | content: c4BoardToString(gameObj) + "\n" + gameMessage, 151 | }); 152 | reaction.users.remove(user.id); 153 | return; 154 | } 155 | 156 | if (gameObj.players[0] == 0) { 157 | gameObj.players[0] = user.id; 158 | } else if (gameObj.players[1] == 0 && gameObj.players[0] != user.id) { 159 | // if (gameObj.players[0] == user.id) { 160 | // reaction.message.edit({ 161 | // content: c4BoardToString(gameObj) + "\n" + "You cannot be both players." 162 | // }); 163 | // reaction.users.remove(user.id); 164 | // return; 165 | // } 166 | gameObj.players[1] = user.id; 167 | } 168 | 169 | gameMessage = ""; 170 | 171 | if (gameObj.players[gameObj.turnOf] == 0) { 172 | gameMessage = "Waiting for player 2."; 173 | } else if (gameObj.players[gameObj.turnOf] != user.id) { 174 | gameMessage = `<@${gameObj.players[gameObj.turnOf]}>'s turn.`; 175 | } else if (gameObj.insertInCol(keycapEmojis[reaction.emoji.name])) { 176 | // column is full 177 | gameMessage = "invalid move!"; 178 | } else if (gameObj.getGameWon()) { 179 | // move wins the game 180 | gameObj.setWinner(user.id); 181 | gameObj.setMovesLeftZero(); 182 | gameMessage = `Game over! <@${user.id}> wins.`; 183 | } else if (gameObj.getMovesLeft() == 0) { 184 | gameMessage = "Draw!"; 185 | } else { 186 | gameMessage = 187 | gameObj.players[1] == 0 188 | ? "Waiting for player 2." 189 | : `<@${gameObj.players[gameObj.turnOf]}>'s turn.`; 190 | } 191 | 192 | reaction.message.edit({ 193 | content: c4BoardToString(gameObj) + "\n" + gameMessage, 194 | }); 195 | 196 | reaction.users.remove(user.id); 197 | } 198 | 199 | module.exports = { 200 | createConnect4, 201 | handleConnect4React, 202 | }; 203 | -------------------------------------------------------------------------------- /lib/database/dbreactrole.js: -------------------------------------------------------------------------------- 1 | const { Pool } = require("pg"); 2 | const yaml = require("js-yaml"); 3 | const fs = require("fs"); 4 | 5 | class DBReactRole { 6 | constructor() { 7 | // Loads the db configuration 8 | const details = this.load_db_login(); 9 | 10 | this.pool = new Pool({ 11 | user: details["user"], 12 | password: details["password"], 13 | host: details["host"], 14 | port: details["port"], 15 | database: details["dbname"], 16 | }); 17 | 18 | // Creates the table if it doesn't exists 19 | (async () => { 20 | const has_msgs_table = await this.check_table("react_role_msgs"); 21 | if (has_msgs_table == false) { 22 | await this.create_react_role_messages_table(); 23 | } 24 | 25 | const has_roles_table = await this.check_table("react_role_roles"); 26 | if (has_roles_table == false) { 27 | await this.create_react_role_roles_table(); 28 | } 29 | })(); 30 | } 31 | 32 | // Get document, or throw exception on error 33 | load_db_login() { 34 | try { 35 | const doc = yaml.load(fs.readFileSync("./config/database.yml")); 36 | return doc; 37 | } catch (e) { 38 | console.log(e); 39 | } 40 | } 41 | 42 | // Checks if the table exists in the db 43 | async check_table(table_name) { 44 | const client = await this.pool.connect(); 45 | try { 46 | // console.log("Running check_table command") 47 | await client.query("BEGIN"); 48 | const values = [table_name]; 49 | const result = await client.query( 50 | "select * from information_schema.tables where table_name=$1", 51 | values, 52 | ); 53 | await client.query("COMMIT"); 54 | 55 | if (result.rowCount == 0) { 56 | return false; 57 | } else { 58 | return true; 59 | } 60 | } catch (ex) { 61 | console.log(`Something wrong happend in react role ${ex}`); 62 | } finally { 63 | await client.query("ROLLBACK"); 64 | client.release(); 65 | // console.log("Client released successfully.") 66 | } 67 | } 68 | 69 | // Creates a new table for react role messages 70 | async create_react_role_messages_table() { 71 | const client = await this.pool.connect(); 72 | try { 73 | console.log("Running create_react_role_msgs_table"); 74 | await client.query("BEGIN"); 75 | const query = `CREATE TABLE REACT_ROLE_MSGS ( 76 | MSG_ID BIGINT PRIMARY KEY, 77 | SENDER_ID BIGINT NOT NULL 78 | )`; 79 | await client.query(query); 80 | await client.query("COMMIT"); 81 | } catch (ex) { 82 | console.log(`Something wrong happend in react role ${ex}`); 83 | } finally { 84 | await client.query("ROLLBACK"); 85 | client.release(); 86 | // console.log("Client released successfully.") 87 | } 88 | } 89 | 90 | // Creates new table for react roles 91 | async create_react_role_roles_table() { 92 | const client = await this.pool.connect(); 93 | try { 94 | console.log("Running create_react_role_roles_table"); 95 | await client.query("BEGIN"); 96 | const query = `CREATE TABLE REACT_ROLE_ROLES ( 97 | REACT_ROLE_ID SERIAL PRIMARY KEY, 98 | ROLE_ID BIGINT, 99 | EMOJI VARCHAR NOT NULL, 100 | MSG_ID BIGINT NOT NULL, 101 | FOREIGN KEY (MSG_ID) 102 | REFERENCES REACT_ROLE_MSGS (MSG_ID) 103 | )`; 104 | await client.query(query); 105 | await client.query("COMMIT"); 106 | } catch (ex) { 107 | console.log(`Something wrong happend in react role ${ex}`); 108 | } finally { 109 | await client.query("ROLLBACK"); 110 | client.release(); 111 | // console.log("Client released successfully.") 112 | } 113 | } 114 | 115 | // Add new react role message 116 | async add_react_role_msg(msg_id, sender_id) { 117 | const client = await this.pool.connect(); 118 | try { 119 | await client.query("BEGIN"); 120 | 121 | const query = `INSERT INTO react_role_msgs VALUES ($1,$2)`; 122 | const values = [msg_id, sender_id]; 123 | await client.query(query, values); 124 | await client.query("COMMIT"); 125 | } catch (ex) { 126 | console.log(`Something wrong happend in react role ${ex}`); 127 | } finally { 128 | await client.query("ROLLBACK"); 129 | client.release(); 130 | // console.log("Client released successfully.") 131 | } 132 | } 133 | 134 | // Add new react role role 135 | async add_react_role_role(role_id, emoji, msg_id) { 136 | const client = await this.pool.connect(); 137 | try { 138 | await client.query("BEGIN"); 139 | 140 | const query = `INSERT INTO react_role_roles VALUES (DEFAULT,$1,$2,$3)`; 141 | const values = [role_id, emoji, msg_id]; 142 | await client.query(query, values); 143 | await client.query("COMMIT"); 144 | } catch (ex) { 145 | console.log(`Something wrong happend in react role ${ex}`); 146 | } finally { 147 | await client.query("ROLLBACK"); 148 | client.release(); 149 | // console.log("Client released successfully.") 150 | } 151 | } 152 | 153 | // Get role 154 | async get_roles(msg_id, emoji) { 155 | const client = await this.pool.connect(); 156 | try { 157 | await client.query("BEGIN"); 158 | const values = [msg_id, emoji]; 159 | const result = await client.query( 160 | "select * from react_role_roles where msg_id=$1 and emoji=$2", 161 | values, 162 | ); 163 | await client.query("COMMIT"); 164 | 165 | return result.rows; 166 | } catch (ex) { 167 | console.log(`Something wrong happend in react role ${ex}`); 168 | } finally { 169 | await client.query("ROLLBACK"); 170 | client.release(); 171 | // console.log("Client released successfully.") 172 | } 173 | } 174 | 175 | // Get sender 176 | async get_sender(msg_id) { 177 | const client = await this.pool.connect(); 178 | try { 179 | await client.query("BEGIN"); 180 | const values = [msg_id]; 181 | const result = await client.query( 182 | "select * from react_role_msgs where msg_id=$1", 183 | values, 184 | ); 185 | await client.query("COMMIT"); 186 | 187 | return result.rows[0].sender_id; 188 | } catch (ex) { 189 | console.log(`Something wrong happend in react role ${ex}`); 190 | } finally { 191 | await client.query("ROLLBACK"); 192 | client.release(); 193 | // console.log("Client released successfully.") 194 | } 195 | } 196 | } 197 | 198 | module.exports = { 199 | DBReactRole, 200 | }; 201 | -------------------------------------------------------------------------------- /lib/database/dbschedulepost.js: -------------------------------------------------------------------------------- 1 | const { Pool } = require("pg"); 2 | const yaml = require("js-yaml"); 3 | const fs = require("fs"); 4 | 5 | class DBSchedulePost { 6 | constructor() { 7 | // Loads the db configuration 8 | const details = this.load_db_login(); 9 | 10 | this.pool = new Pool({ 11 | user: details["user"], 12 | password: details["password"], 13 | host: details["host"], 14 | port: details["port"], 15 | database: details["dbname"], 16 | }); 17 | 18 | // Creates the table if it doesn't exists 19 | (async () => { 20 | const has_table = await this.check_table("schedule_post"); 21 | if (has_table == false) { 22 | await this.create_schedule_post_table(); 23 | } 24 | })(); 25 | } 26 | 27 | // Get document, or throw exception on error 28 | load_db_login() { 29 | try { 30 | const doc = yaml.load(fs.readFileSync("./config/database.yml")); 31 | return doc; 32 | } catch (e) { 33 | console.log(e); 34 | } 35 | } 36 | 37 | // Checks if the table exists in the db 38 | async check_table(table_name) { 39 | const client = await this.pool.connect(); 40 | try { 41 | // console.log("Running check_table command") 42 | await client.query("BEGIN"); 43 | const values = [table_name]; 44 | const result = await client.query( 45 | "select * from information_schema.tables where table_name=$1", 46 | values, 47 | ); 48 | await client.query("COMMIT"); 49 | 50 | if (result.rowCount == 0) { 51 | return false; 52 | } else { 53 | return true; 54 | } 55 | } catch (ex) { 56 | console.log(`Something wrong happend in schedule post ${ex}`); 57 | } finally { 58 | await client.query("ROLLBACK"); 59 | client.release(); 60 | // console.log("Client released successfully.") 61 | } 62 | } 63 | 64 | // Creates a new table for scheduled posts 65 | async create_schedule_post_table() { 66 | const client = await this.pool.connect(); 67 | try { 68 | console.log("Running create_schedule_post_table"); 69 | await client.query("BEGIN"); 70 | const query = `CREATE TABLE SCHEDULE_POST ( 71 | SCHEDULED_POST_ID SERIAL PRIMARY KEY, 72 | GUILD_ID BIGINT NOT NULL, 73 | MSG_ID BIGINT NOT NULL, 74 | INIT_CHANNEL_ID BIGINT NOT NULL, 75 | SEND_CHANNEL_ID BIGINT NOT NULL, 76 | DATETIME CHAR(16) NOT NULL, 77 | REMINDER CHAR(16), 78 | SENT_MSG_ID BIGINT 79 | )`; 80 | await client.query(query); 81 | await client.query("COMMIT"); 82 | } catch (ex) { 83 | console.log(`Something wrong happend in schedule post ${ex}`); 84 | } finally { 85 | await client.query("ROLLBACK"); 86 | client.release(); 87 | // console.log("Client released successfully.") 88 | } 89 | } 90 | 91 | // Add new scheduled post 92 | async add_react_role_msg( 93 | guild_id, 94 | msg_id, 95 | init_channel_id, 96 | send_channel_id, 97 | datetime, 98 | reminder, 99 | ) { 100 | const client = await this.pool.connect(); 101 | try { 102 | await client.query("BEGIN"); 103 | 104 | const query = `INSERT INTO schedule_post VALUES (DEFAULT,$1,$2,$3,$4,$5,$6)`; 105 | const values = [guild_id, msg_id, init_channel_id, send_channel_id, datetime, reminder]; 106 | await client.query(query, values); 107 | await client.query("COMMIT"); 108 | } catch (ex) { 109 | console.log(`Something wrong happend in schedule post ${ex}`); 110 | } finally { 111 | await client.query("ROLLBACK"); 112 | client.release(); 113 | // console.log("Client released successfully.") 114 | } 115 | } 116 | 117 | // Add reminder 118 | async add_reminder(sent_msg_id, scheduled_post_id) { 119 | const client = await this.pool.connect(); 120 | try { 121 | await client.query("BEGIN"); 122 | const query = `UPDATE schedule_post SET sent_msg_id=$1 WHERE scheduled_post_id=$2;`; 123 | const values = [sent_msg_id, scheduled_post_id]; 124 | await client.query(query, values); 125 | await client.query("COMMIT"); 126 | } catch (ex) { 127 | console.log(`Something wrong happend in schedule post ${ex}`); 128 | } finally { 129 | await client.query("ROLLBACK"); 130 | client.release(); 131 | // console.log("Client released successfully.") 132 | } 133 | } 134 | 135 | // Get all posts scheduled at given time 136 | async get_scheduled(datetime) { 137 | const client = await this.pool.connect(); 138 | try { 139 | await client.query("BEGIN"); 140 | const values = [datetime]; 141 | const result = await client.query( 142 | "select * from schedule_post where datetime=$1", 143 | values, 144 | ); 145 | await client.query("COMMIT"); 146 | 147 | return result.rows; 148 | } catch (ex) { 149 | console.log(`Something wrong happend in schedule post ${ex}`); 150 | } finally { 151 | await client.query("ROLLBACK"); 152 | client.release(); 153 | // console.log("Client released successfully.") 154 | } 155 | } 156 | 157 | // Get all reminders at given time 158 | async get_reminders(reminder) { 159 | const client = await this.pool.connect(); 160 | try { 161 | await client.query("BEGIN"); 162 | const values = [reminder]; 163 | const result = await client.query( 164 | "select * from schedule_post where reminder=$1", 165 | values, 166 | ); 167 | await client.query("COMMIT"); 168 | 169 | return result.rows; 170 | } catch (ex) { 171 | console.log(`Something wrong happend in schedule post ${ex}`); 172 | } finally { 173 | await client.query("ROLLBACK"); 174 | client.release(); 175 | // console.log("Client released successfully.") 176 | } 177 | } 178 | 179 | async remove_scheduled(scheduled_post_id) { 180 | const client = await this.pool.connect(); 181 | try { 182 | await client.query("BEGIN"); 183 | const values = [scheduled_post_id]; 184 | await client.query("delete from schedule_post where scheduled_post_id=$1", values); 185 | await client.query("COMMIT"); 186 | } catch (ex) { 187 | console.log(`Something wrong happend in schedule post ${ex}`); 188 | } finally { 189 | await client.query("ROLLBACK"); 190 | client.release(); 191 | // console.log("Client released successfully.") 192 | } 193 | } 194 | 195 | async get_scheduled_post_id(msg_id, send_channel_id, datetime) { 196 | const client = await this.pool.connect(); 197 | try { 198 | await client.query("BEGIN"); 199 | const values = [msg_id, send_channel_id, datetime]; 200 | const result = await client.query( 201 | "select * from schedule_post where msg_id=$1 and send_channel_id=$2 and datetime=$3", 202 | values, 203 | ); 204 | await client.query("COMMIT"); 205 | 206 | if (result.rows.length === 0) { 207 | return null; 208 | } else { 209 | return result.rows[0].scheduled_post_id; 210 | } 211 | } catch (ex) { 212 | console.log(`Something wrong happend in schedule post ${ex}`); 213 | } finally { 214 | await client.query("ROLLBACK"); 215 | client.release(); 216 | // console.log("Client released successfully.") 217 | } 218 | } 219 | } 220 | 221 | module.exports = { 222 | DBSchedulePost, 223 | }; 224 | -------------------------------------------------------------------------------- /lib/database/dbstandup.js: -------------------------------------------------------------------------------- 1 | const { Pool } = require("pg"); 2 | const yaml = require("js-yaml"); 3 | const fs = require("fs"); 4 | 5 | // Class for standup db 6 | class DBstandup { 7 | constructor() { 8 | // Loads the db configuration 9 | const details = this.load_db_login(); 10 | 11 | this.pool = new Pool({ 12 | user: details["user"], 13 | password: details["password"], 14 | host: details["host"], 15 | port: details["port"], 16 | database: details["dbname"], 17 | }); 18 | // The name of the table 19 | const table_name = "standups"; 20 | 21 | // Creates the table if it doesn't exists` 22 | (async () => { 23 | const is_check = await this.check_table(table_name); 24 | // console.log(is_check); 25 | if (is_check == false) { 26 | await this.create_table(); 27 | } 28 | })(); 29 | } 30 | 31 | load_db_login() { 32 | // Get document, or throw exception on error 33 | try { 34 | const doc = yaml.load(fs.readFileSync("./config/database.yml")); 35 | return doc; 36 | } catch (e) { 37 | console.log(e); 38 | } 39 | } 40 | 41 | // Checks if the table exists in the db 42 | async check_table(table_name) { 43 | const client = await this.pool.connect(); 44 | try { 45 | // console.log("Running check_table command") 46 | await client.query("BEGIN"); 47 | const values = [table_name]; 48 | const result = await client.query( 49 | "select * from information_schema.tables where table_name=$1", 50 | values, 51 | ); 52 | await client.query("COMMIT"); 53 | 54 | if (result.rowCount == 0) { 55 | return false; 56 | } else { 57 | return true; 58 | } 59 | } catch (ex) { 60 | console.log(`dbstandup:load_db_login:${ex}`); 61 | } finally { 62 | await client.query("ROLLBACK"); 63 | client.release(); 64 | } 65 | } 66 | 67 | async create_table() { 68 | const client = await this.pool.connect(); 69 | try { 70 | console.log("Creating tables for feature standups"); 71 | await client.query("BEGIN"); 72 | const query = ` 73 | CREATE TABLE standup_teams ( 74 | id NUMERIC PRIMARY KEY 75 | ); 76 | 77 | CREATE TABLE standups ( 78 | id SERIAL PRIMARY KEY, 79 | team_id NUMERIC NOT NULL, 80 | user_id NUMERIC NOT NULL, 81 | message_id NUMERIC NOT NULL, 82 | standup_content TEXT, 83 | time_stamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 84 | FOREIGN KEY (team_id) REFERENCES standup_teams (id) 85 | ); 86 | `; 87 | 88 | await client.query(query); 89 | await client.query("COMMIT"); 90 | } catch (ex) { 91 | console.log(`dbstandup:create_table:${ex}`); 92 | } finally { 93 | await client.query("ROLLBACK"); 94 | client.release(); 95 | // console.log("Client released successfully.") 96 | } 97 | } 98 | 99 | async getStandups(channelParentId, numDays) { 100 | const timeInterval = `${numDays} DAYS`; 101 | 102 | const client = await this.pool.connect(); 103 | try { 104 | await client.query("BEGIN"); 105 | 106 | /*eslint-disable */ 107 | const query = ` 108 | SELECT * FROM standups AS s 109 | JOIN standup_teams AS t ON t.id = s.team_id 110 | INNER JOIN ( 111 | SELECT s1.user_id, MAX(s1.time_stamp) AS date 112 | FROM standups AS s1 113 | GROUP BY s1.user_id 114 | ) AS s3 ON s3.user_id = s.user_id AND s.time_stamp = s3.date 115 | WHERE t.id = $1 AND s.time_stamp >= CURRENT_TIMESTAMP - INTERVAL \'${timeInterval}\'; 116 | `; 117 | /* eslint-enable */ 118 | 119 | const values = [channelParentId]; 120 | const result = await client.query(query, values); 121 | await client.query("COMMIT"); 122 | 123 | return result.rows; 124 | } catch (e) { 125 | console.log(`dbstandup:getStandups:${e}`); 126 | } finally { 127 | await client.query("ROLLBACK"); 128 | client.release(); 129 | } 130 | } 131 | 132 | async addStandup(channelParentId, userId, messageId, messageContent) { 133 | const client = await this.pool.connect(); 134 | try { 135 | await client.query("BEGIN"); 136 | 137 | const queryMakeTeamExist = ` 138 | INSERT INTO standup_teams (id) VALUES ($1) ON CONFLICT (id) DO NOTHING; 139 | `; 140 | await client.query(queryMakeTeamExist, [channelParentId]); 141 | 142 | const queryInsertStandup = ` 143 | INSERT INTO standups (team_id, user_id, message_id, standup_content) 144 | VALUES ($1, $2, $3, $4); 145 | `; 146 | const values_params = [channelParentId, userId, messageId, messageContent]; 147 | const result = await client.query(queryInsertStandup, values_params); 148 | await client.query("COMMIT"); 149 | 150 | return result.rows; 151 | } catch (e) { 152 | console.log(`dbstandup:addStandup:${e}`); 153 | } finally { 154 | await client.query("ROLLBACK"); 155 | client.release(); 156 | } 157 | } 158 | 159 | async thisStandupExists(messageId) { 160 | const client = await this.pool.connect(); 161 | try { 162 | await client.query("BEGIN"); 163 | 164 | const query = ` 165 | SELECT * FROM standups AS s 166 | WHERE s.message_id = $1; 167 | `; 168 | const result = await client.query(query, [messageId]); 169 | 170 | await client.query("COMMIT"); 171 | 172 | return result.rows.length != 0; 173 | } catch (e) { 174 | console.log(`dbstandup:thisStandupExists:${e}`); 175 | } finally { 176 | await client.query("ROLLBACK"); 177 | client.release(); 178 | } 179 | } 180 | 181 | async updateStandup(messageId, messageContent) { 182 | const client = await this.pool.connect(); 183 | try { 184 | await client.query("BEGIN"); 185 | 186 | const query = ` 187 | UPDATE standups 188 | SET standup_content = $2 189 | WHERE message_id = $1 190 | AND standup_content IS DISTINCT FROM $2; 191 | `; 192 | 193 | const values_params = [messageId, messageContent]; 194 | await client.query(query, values_params); 195 | await client.query("COMMIT"); 196 | } catch (e) { 197 | console.log(`dbstandup:updateStandup:${e}`); 198 | } finally { 199 | await client.query("ROLLBACK"); 200 | client.release(); 201 | } 202 | } 203 | 204 | async deleteAllStandups() { 205 | const client = await this.pool.connect(); 206 | try { 207 | await client.query("BEGIN"); 208 | 209 | const query = ` 210 | DELETE FROM standups; 211 | `; 212 | 213 | const result = await client.query(query); 214 | await client.query("COMMIT"); 215 | 216 | return result.rows; 217 | } catch (e) { 218 | console.log(`dbstandup:deleteAllStandups:${e}`); 219 | } finally { 220 | await client.query("ROLLBACK"); 221 | client.release(); 222 | } 223 | } 224 | } 225 | 226 | module.exports = { 227 | DBstandup, 228 | }; 229 | -------------------------------------------------------------------------------- /lib/database/faq.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const { Pool } = require("pg"); 3 | const yaml = require("js-yaml"); 4 | const fs = require("fs"); 5 | 6 | // Class for the carrotboard db 7 | class DBFaq { 8 | constructor() { 9 | // Loads the db configuration 10 | const details = this.load_db_login(); 11 | 12 | this.pool = new Pool({ 13 | user: details["user"], 14 | password: details["password"], 15 | host: details["host"], 16 | port: details["port"], 17 | database: details["dbname"], 18 | }); 19 | // The name of the table 20 | const table_name = "faq"; 21 | 22 | // Creates the table if it doesn't exists 23 | (async () => { 24 | const table_exists = await this.check_table(table_name); 25 | if (!table_exists) { 26 | await this.create_table(); 27 | } 28 | })(); 29 | } 30 | 31 | // Get document, or throw exception on error 32 | load_db_login() { 33 | try { 34 | const doc = yaml.load(fs.readFileSync("./config/database.yml")); 35 | return doc; 36 | } catch (e) { 37 | console.log(e); 38 | } 39 | } 40 | 41 | // Checks if the table exists in the db 42 | async check_table(table_name) { 43 | const client = await this.pool.connect(); 44 | try { 45 | await client.query("BEGIN"); 46 | const values = [table_name]; 47 | const result = await client.query( 48 | "select * from information_schema.tables where table_name=$1", 49 | values, 50 | ); 51 | await client.query("COMMIT"); 52 | 53 | // return whether there was a table or not 54 | return result.rowCount > 0; 55 | } catch (err) { 56 | console.log(`Something went wrong ${err}`); 57 | } finally { 58 | await client.query("ROLLBACK"); 59 | client.release(); 60 | } 61 | } 62 | 63 | // Creates a new table 64 | async create_table() { 65 | const client = await this.pool.connect(); 66 | try { 67 | console.log("Running create_table"); 68 | await client.query("BEGIN"); 69 | const query = `CREATE TABLE IF NOT EXISTS FAQ ( 70 | KEYWORD TEXT PRIMARY KEY, 71 | ANSWER TEXT, 72 | TAGS TEXT 73 | )`; 74 | 75 | await client.query(query); 76 | await client.query("COMMIT"); 77 | } catch (err) { 78 | console.log(`Something went wrong ${err}`); 79 | } finally { 80 | await client.query("ROLLBACK"); 81 | client.release(); 82 | } 83 | } 84 | 85 | // get a faq 86 | async get_faq(keyword) { 87 | const client = await this.pool.connect(); 88 | try { 89 | await client.query("BEGIN"); 90 | 91 | const query = "SELECT * from faq where keyword = $1"; 92 | const res = await client.query(query, [keyword]); 93 | await client.query("COMMIT"); 94 | 95 | return res.rows; 96 | } catch (err) { 97 | console.log(`FAQ DB ERR: ${err}`); 98 | } finally { 99 | await client.query("ROLLBACK"); 100 | client.release(); 101 | } 102 | } 103 | 104 | // get all faqs that have a certain tag 105 | async get_tagged_faqs(tag) { 106 | const client = await this.pool.connect(); 107 | try { 108 | await client.query("BEGIN"); 109 | 110 | const query = "SELECT * from faq where tags ~* $1"; 111 | const tag_str = `(${tag}$)|(${tag},)`; 112 | // note: not using tag_str for testing purposes... 113 | const res = await client.query(query, [tag_str]); 114 | await client.query("COMMIT"); 115 | 116 | return res.rows; 117 | } catch (err) { 118 | console.log(`FAQ DB ERR: ${err}`); 119 | } finally { 120 | await client.query("ROLLBACK"); 121 | client.release(); 122 | } 123 | } 124 | 125 | // get keywords 126 | async get_keywords() { 127 | const client = await this.pool.connect(); 128 | try { 129 | await client.query("BEGIN"); 130 | 131 | const query = "SELECT keyword from faq"; 132 | const res = await client.query(query); 133 | await client.query("COMMIT"); 134 | 135 | let keyword_list = ""; 136 | for (const row of res.rows) { 137 | keyword_list += `${row.keyword}\n`; 138 | } 139 | 140 | return keyword_list; 141 | } catch (err) { 142 | console.log(`FAQ DB ERR: ${err}`); 143 | } finally { 144 | await client.query("ROLLBACK"); 145 | client.release(); 146 | } 147 | } 148 | 149 | // get keywords 150 | async get_tags() { 151 | const client = await this.pool.connect(); 152 | try { 153 | await client.query("BEGIN"); 154 | 155 | const query = "SELECT tags from faq"; 156 | const res = await client.query(query); 157 | await client.query("COMMIT"); 158 | 159 | const tag_list = []; 160 | for (const row of res.rows) { 161 | const tags = row.tags.split(","); 162 | tag_list.push(...tags); 163 | } 164 | 165 | const no_dups_tag_list = [...new Set(tag_list)]; 166 | 167 | let tag_list_str = ""; 168 | for (const tag of no_dups_tag_list) { 169 | tag_list_str += `${tag}\n`; 170 | } 171 | 172 | return tag_list_str; 173 | } catch (err) { 174 | console.log(`FAQ DB ERR: ${err}`); 175 | } finally { 176 | await client.query("ROLLBACK"); 177 | client.release(); 178 | } 179 | } 180 | 181 | // Insert a new faq 182 | async new_faq(keyword, answer, tags) { 183 | const client = await this.pool.connect(); 184 | try { 185 | // check if its not already in 186 | const rows = await this.get_faq(keyword); 187 | if (rows.length != 0) { 188 | return false; 189 | } 190 | 191 | await client.query("BEGIN"); 192 | 193 | const query = "INSERT INTO faq(keyword, answer, tags) VALUES ($1, $2, $3)"; 194 | await client.query(query, [keyword, answer, tags]); 195 | await client.query("COMMIT"); 196 | 197 | return true; 198 | } catch (err) { 199 | console.log(`FAQ DB ERR: ${err}`); 200 | } finally { 201 | await client.query("ROLLBACK"); 202 | client.release(); 203 | } 204 | } 205 | 206 | // delete a faq 207 | async del_faq(keyword) { 208 | const client = await this.pool.connect(); 209 | try { 210 | // check if it exists first 211 | const rows = await this.get_faq(keyword); 212 | if (rows.length == 0) { 213 | return false; 214 | } 215 | 216 | await client.query("BEGIN"); 217 | 218 | const query = "DELETE FROM faq WHERE keyword = $1"; 219 | await client.query(query, [keyword]); 220 | await client.query("COMMIT"); 221 | 222 | return true; 223 | } catch (err) { 224 | console.log(`FAQ DB ERR: ${err}`); 225 | } finally { 226 | await client.query("ROLLBACK"); 227 | client.release(); 228 | } 229 | } 230 | } 231 | 232 | module.exports = { 233 | DBFaq, 234 | }; 235 | -------------------------------------------------------------------------------- /lib/discordscroll/scroller.js: -------------------------------------------------------------------------------- 1 | const { EmbedBuilder, CommandInteraction, ButtonBuilder, ActionRowBuilder } = require("discord.js"); 2 | 3 | class DiscordScroll { 4 | /** @protected @type {Boolean} */ 5 | _active = false; 6 | /** @protected @type {Boolean} */ 7 | _used = false; 8 | 9 | /** @protected @type {?Message} */ 10 | _message = null; 11 | /** @protected @type {?InteractionCollector} */ 12 | _collector = null; 13 | /** @protected */ 14 | _buttons = { 15 | left: new ButtonBuilder().setCustomId("scrollLeft").setEmoji("⬅️").setStyle("SECONDARY"), 16 | right: new ButtonBuilder().setCustomId("scrollRight").setEmoji("➡️").setStyle("SECONDARY"), 17 | delete: new ButtonBuilder().setCustomId("scrollDelete").setEmoji("🚮").setStyle("DANGER"), 18 | }; 19 | 20 | /** @protected @type {?EmbedBuilder} */ 21 | _embed = null; 22 | /** @protected @type {Number} */ 23 | _pagenum = 0; 24 | /** @protected @type {EmbedBuilder[]} */ 25 | _pages = []; 26 | 27 | /** 28 | * Constructor for the Scroller 29 | * @param {EmbedBuilder[]} pages 30 | */ 31 | constructor(pages) { 32 | this.pages = pages; 33 | this._embed = this.currentPage; 34 | } 35 | 36 | /** 37 | * The pages of the Scroller. 38 | * @type {EmbedBuilder[]} 39 | */ 40 | get pages() { 41 | return this._pages; 42 | } 43 | 44 | /** 45 | * @param {EmbedBuilder[]} value The array of Embeds 46 | */ 47 | set pages(value) { 48 | // type check the array 49 | if (!(value instanceof Array)) { 50 | throw new TypeError("DiscordScroll.pages expected an array."); 51 | } else if (value.length == 0) { 52 | throw new TypeError("DiscordScroll.pages expected at least one element in the array."); 53 | } else if (!value.every((e) => e instanceof EmbedBuilder)) { 54 | throw new TypeError("DiscordScroll.pages expected an array of EmbedBuilders."); 55 | } 56 | 57 | this._pages = value; 58 | } 59 | 60 | /** 61 | * The current shown Embed. 62 | * @type {EmbedBuilder} 63 | * @readonly 64 | */ 65 | get embed() { 66 | return this._embed; 67 | } 68 | 69 | /** 70 | * The current page. 71 | * @type {EmbedBuilder} 72 | * @readonly 73 | */ 74 | get currentPage() { 75 | return this.pages[this._pagenum]; 76 | } 77 | 78 | /** 79 | * The message id of the scroller 80 | * @type {String} 81 | * @readonly 82 | */ 83 | get messageID() { 84 | return this._message.id; 85 | } 86 | 87 | /** 88 | * Sends the Scroller 89 | * @param {CommandInteraction} interaction The CommandInteraction 90 | * @returns {Promise} 91 | */ 92 | async send(interaction) { 93 | // error checking 94 | if (this._used) { 95 | throw new Error("This Scroller has already been sent."); 96 | } else if (!(interaction instanceof CommandInteraction)) { 97 | throw new TypeError( 98 | "DiscordScroll.send expected a CommandInteraction for first parameter.", 99 | ); 100 | } 101 | 102 | // send the reply 103 | /** @type {InteractionReplyOptions} */ 104 | const replyOptions = { 105 | embeds: [this.embed], 106 | components: [this._getButtonRow], 107 | fetchReply: true, 108 | }; 109 | /** @type {Message} */ 110 | const replyMessage = await interaction.reply(replyOptions); 111 | this._active = true; 112 | this._used = true; 113 | this._message = replyMessage; 114 | 115 | this._setupCollector(replyMessage); 116 | return replyMessage; 117 | } 118 | 119 | /** 120 | * Disables the scroller, as if it has ended. 121 | */ 122 | disable() { 123 | this._collector.stop(); 124 | } 125 | 126 | /** 127 | * Gets the Button Row, updating the button status if needed. 128 | * @protected 129 | * @readonly 130 | * @returns {ActionRowBuilder} 131 | */ 132 | get _getButtonRow() { 133 | if (this._pagenum === 0) { 134 | this._buttons.left.setDisabled(true); 135 | } else { 136 | this._buttons.left.setDisabled(false); 137 | } 138 | 139 | if (this._pagenum === this._pages.length - 1) { 140 | this._buttons.right.setDisabled(true); 141 | } else { 142 | this._buttons.right.setDisabled(false); 143 | } 144 | 145 | return new ActionRowBuilder().addComponents( 146 | this._buttons.left, 147 | this._buttons.right, 148 | this._buttons.delete, 149 | ); 150 | } 151 | 152 | /** 153 | * Sets up the Button Collector 154 | * @param {Message} message The message to attach it to. 155 | */ 156 | async _setupCollector(message) { 157 | const buttonCollector = message.createMessageComponentCollector({ 158 | componentType: "BUTTON", 159 | time: 120000, 160 | idle: 30000, 161 | max: 1000, 162 | }); 163 | 164 | this._collector = buttonCollector; 165 | 166 | buttonCollector.on("collect", (buttonInt) => { 167 | this._scroll(buttonInt); 168 | }); 169 | 170 | buttonCollector.on("end", () => { 171 | this._deactivate(); 172 | }); 173 | } 174 | 175 | /** 176 | * Scrolls the scroller. 177 | * @param {MessageComponentInteraction} buttonInt The Button Interaction. 178 | * @protected 179 | */ 180 | async _scroll(buttonInt) { 181 | if (buttonInt.customId === "scrollLeft" && this._pagenum > 0) { 182 | this._pagenum -= 1; 183 | await this._update(buttonInt); 184 | } else if (buttonInt.customId === "scrollRight" && this._pagenum < this.pages.length - 1) { 185 | this._pagenum += 1; 186 | await this._update(buttonInt); 187 | } else if (buttonInt.customId === "scrollDelete") { 188 | this._active = false; 189 | await this._message.delete(); 190 | } 191 | } 192 | 193 | /** 194 | * Updates the scroller. 195 | * @param {MessageComponentInteraction} buttonInt The Button Interaction. 196 | * @protected 197 | */ 198 | async _update(buttonInt) { 199 | this._embed = this.currentPage; 200 | await buttonInt.update({ 201 | embeds: [this.embed], 202 | components: [this._getButtonRow], 203 | }); 204 | } 205 | 206 | /** 207 | * Deactivates the scroller. 208 | * @protected 209 | */ 210 | async _deactivate() { 211 | if (this._message != null && this._active) { 212 | this._active = false; 213 | await this._message.edit({ components: [] }); 214 | } 215 | } 216 | } 217 | 218 | module.exports = { 219 | DiscordScroll, 220 | }; 221 | -------------------------------------------------------------------------------- /lib/tictactoe/tttGame.js: -------------------------------------------------------------------------------- 1 | class game { 2 | constructor(boardSize = 3) { 3 | this.dim = boardSize; 4 | this.board = []; 5 | this.players = []; 6 | this.turnOf = 0; 7 | this.movesLeft = boardSize ** 2; 8 | 9 | for (let i = 0; i < boardSize; i++) { 10 | this.board[i] = []; 11 | for (let j = 0; j < boardSize; j++) { 12 | this.board[i][j] = -1; 13 | } 14 | } 15 | } 16 | get getGameOver() { 17 | return this.checkGameOver(); 18 | } 19 | checkGameOver() { 20 | // check rows 21 | let validTemp = true; 22 | for (let i = 0; i < this.dim; i++) { 23 | validTemp = true; 24 | for (let j = 1; j < this.dim; j++) { 25 | if (this.board[i][j] == -1 || this.board[i][j] != this.board[i][0]) { 26 | validTemp = false; 27 | break; 28 | } 29 | } 30 | if (validTemp) { 31 | return true; 32 | } 33 | } 34 | // check cols 35 | for (let i = 0; i < this.dim; i++) { 36 | validTemp = true; 37 | for (let j = 1; j < this.dim; j++) { 38 | if (this.board[0][i] == -1 || this.board[j][i] != this.board[0][i]) { 39 | validTemp = false; 40 | break; 41 | } 42 | } 43 | if (validTemp) { 44 | return true; 45 | } 46 | } 47 | 48 | // check top left to bottom right diag 49 | validTemp = true; 50 | for (let i = 1; i < this.dim; i++) { 51 | if (this.board[i][i] == -1 || this.board[i][i] != this.board[0][0]) { 52 | validTemp = false; 53 | break; 54 | } 55 | } 56 | if (validTemp) { 57 | return true; 58 | } 59 | // check bottom left to top right diag 60 | validTemp = true; 61 | for (let i = 0; i < this.dim - 1; i++) { 62 | if ( 63 | this.board[i][i] == -1 || 64 | this.board[i][this.dim - 1 - i] != this.board[this.dim - 1][0] 65 | ) { 66 | validTemp = false; 67 | break; 68 | } 69 | } 70 | if (validTemp) { 71 | return true; 72 | } 73 | return false; 74 | } 75 | } 76 | 77 | module.exports = { game }; 78 | -------------------------------------------------------------------------------- /lib/tictactoe/tttHelper.js: -------------------------------------------------------------------------------- 1 | const { ActionRowBuilder, ButtonBuilder } = require("discord.js"); 2 | const { game } = require("./tttGame"); 3 | 4 | const maxNumGames = 1000; 5 | 6 | const gamesAlive = new Object(); 7 | 8 | function createGameId() { 9 | const dateObj = new Date(); 10 | return dateObj.getTime().toString(); 11 | } 12 | 13 | function gameToButtons(gameObj, gameId) { 14 | const messageRows = []; 15 | const dim = Math.min(gameObj.dim, 5); 16 | for (let i = 0; i < dim; i++) { 17 | const rowButtons = []; 18 | for (let j = 0; j < dim; j++) { 19 | let style = "SECONDARY"; 20 | let label = " "; 21 | const disableButton = gameObj.board[i][j] != -1; 22 | if (disableButton) { 23 | label = gameObj.board[i][j] ? "O" : "X"; 24 | style = gameObj.board[i][j] ? "PRIMARY" : "DANGER"; 25 | } 26 | rowButtons[j] = new ButtonBuilder() 27 | .setCustomId(`${gameId}:${i}:${j}`) 28 | .setLabel(label) 29 | .setStyle(style) 30 | .setDisabled(disableButton); 31 | } 32 | messageRows[i] = new ActionRowBuilder().addComponents(rowButtons); 33 | } 34 | return messageRows; 35 | } 36 | 37 | async function createGame(interaction) { 38 | if (!interaction.isCommand()) return; 39 | const newGame = new game(); 40 | const newGameId = createGameId(); 41 | 42 | // limit on number of games 43 | if (Object.keys(gamesAlive).length >= maxNumGames) { 44 | delete gamesAlive[Object.keys(gamesAlive)[0]]; 45 | } 46 | 47 | gamesAlive[newGameId] = newGame; 48 | 49 | interaction.reply({ 50 | content: "Waiting for player 1", 51 | components: gameToButtons(newGame, newGameId), 52 | }); 53 | setTimeout(() => { 54 | delete gamesAlive[newGameId]; 55 | }, 3600000); 56 | } 57 | 58 | async function handleGameButton(interaction) { 59 | if (!interaction.isButton()) return; 60 | const { member } = interaction; 61 | const gameIdxy = interaction.customId.split(":"); 62 | const gameObj = gamesAlive[gameIdxy[0]]; 63 | 64 | if (gameObj == undefined) { 65 | interaction.reply({ 66 | content: "This game is no longer running.", 67 | ephemeral: true, 68 | }); 69 | return; 70 | } 71 | if (gameObj.players.length == 0) { 72 | gameObj.players[0] = member.id; 73 | } else if (gameObj.players.length == 1) { 74 | // 75 | if (member.id == gameObj.players[0]) { 76 | interaction.reply({ 77 | content: "You cannot be both players", 78 | ephemeral: true, 79 | }); 80 | return; 81 | } else { 82 | gameObj.players[1] = member.id; 83 | } 84 | // 85 | // gameObj.players[1] = member.id; // <- to test on single acct 86 | // uncomment this line, comment out above block between //'s 87 | } else if (gameObj.players[gameObj.turnOf] != member.id) { 88 | interaction.reply({ 89 | content: "It is not your turn.", 90 | ephemeral: true, 91 | }); 92 | return; 93 | } 94 | 95 | gameObj.board[gameIdxy[1]][gameIdxy[2]] = gameObj.turnOf; 96 | if (gameObj.getGameOver) { 97 | interaction.update({ 98 | content: `<@${gameObj.players[gameObj.turnOf]}> wins`, 99 | components: gameToButtons(gameObj, gameIdxy[0]), 100 | }); 101 | delete gamesAlive[gameIdxy[0]]; 102 | return; 103 | } else if (--gameObj.movesLeft == 0) { 104 | interaction.update({ 105 | content: "Draw!", 106 | components: gameToButtons(gameObj, gameIdxy[0]), 107 | }); 108 | delete gamesAlive[gameIdxy[0]]; 109 | return; 110 | } 111 | 112 | gameObj.turnOf = gameObj.turnOf ? 0 : 1; 113 | const nextPlayerId = gameObj.players[gameObj.turnOf]; 114 | const messageContent = 115 | nextPlayerId == undefined ? "Waiting for player 2" : `<@${nextPlayerId}>'s turn`; 116 | 117 | interaction.update({ 118 | content: messageContent, 119 | components: gameToButtons(gameObj, gameIdxy[0]), 120 | }); 121 | } 122 | 123 | module.exports = { 124 | createGameId, 125 | gameToButtons, 126 | createGame, 127 | handleGameButton, 128 | }; 129 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "discord-bot", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "format": "prettier -w ./**/*.js", 8 | "format:check": "prettier --check ./**/*.js", 9 | "lint": "eslint", 10 | "lint:fix": "eslint --fix", 11 | "test": "echo \"Error: no test specified\" && exit 1", 12 | "deploy": "node deploy-commands.js", 13 | "start": "node index.js", 14 | "server": "nodemon index.js" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/csesoc/discord-bot.git" 19 | }, 20 | "author": "", 21 | "license": "ISC", 22 | "bugs": { 23 | "url": "https://github.com/csesoc/discord-bot/issues" 24 | }, 25 | "homepage": "https://github.com/csesoc/discord-bot#readme", 26 | "dependencies": { 27 | "@discordjs/builders": "1.8.1", 28 | "@discordjs/rest": "2.3.0", 29 | "auro-ms-conversion": "1.3.0", 30 | "axios": "^1.6.8", 31 | "canvas": "^2.11.2", 32 | "chart.js": "^3.9.1", 33 | "chartjs-node-canvas": "^4.1.6", 34 | "cheerio": "^1.0.0-rc.12", 35 | "closest-match": "1.3.3", 36 | "cron": "^3.1.7", 37 | "csv-parser": "3.0.0", 38 | "csv-writer": "1.6.0", 39 | "discord-api-types": "0.37.90", 40 | "discord.js": "14.15.2", 41 | "discordjs-button-pagination": "3.0.1", 42 | "dotenv": "16.4.5", 43 | "js-yaml": "4.1.0", 44 | "mathjs": "^13.0.0", 45 | "node-cron": "^3.0.3", 46 | "nodemailer": "6.9.13", 47 | "nodemon": "^3.0.0", 48 | "pg": "8.12.0", 49 | "puppeteer": "^22.12.1", 50 | "textversionjs": "1.1.3", 51 | "voucher-code-generator": "1.3.0", 52 | "xkcd-api": "^1.0.1", 53 | "yaml": "2.4.2" 54 | }, 55 | "devDependencies": { 56 | "@babel/core": "7.24.7", 57 | "@babel/eslint-parser": "^7.24.7", 58 | "@eslint/eslintrc": "^3.1.0", 59 | "@eslint/js": "^9.5.0", 60 | "eslint": "^9.5.0", 61 | "eslint-config-prettier": "9.1.0", 62 | "eslint-plugin-prettier": "^5.0.0", 63 | "globals": "^15.5.0", 64 | "node-pre-gyp": "0.17.0", 65 | "prettier": "3.3.2" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base"], 3 | "lockFileMaintenance": { "enabled": true, "automerge": true }, 4 | "prHourlyLimit": 2, 5 | "labels": ["dependencies"], 6 | "packageRules": [ 7 | { 8 | "matchUpdateTypes": ["minor", "patch", "pin", "digest"], 9 | "automerge": false, 10 | "automergeType": "branch" 11 | }, 12 | { 13 | "matchUpdateTypes": ["patch"], 14 | "groupName": "weekly patch updates", 15 | "schedule": ["before 5am every monday"], 16 | "addLabels": ["deps: patches"] 17 | }, 18 | { 19 | "matchUpdateTypes": ["minor"], 20 | "groupName": "weekly minor updates", 21 | "schedule": ["before 5am every monday"], 22 | "addLabels": ["deps: minor"] 23 | }, 24 | { 25 | "groupName": "docker-github-actions", 26 | "matchPackagePatterns": ["docker/*"], 27 | "automerge": true, 28 | "automergeType": "branch" 29 | } 30 | ] 31 | } 32 | --------------------------------------------------------------------------------