├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .husky └── pre-commit ├── .idea ├── .gitignore ├── Cheeka-Development.iml ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── fastRequest │ └── fastRequestCurrentProjectLocalConfig.xml ├── inspectionProfiles │ └── Project_Default.xml ├── jpa-buddy.xml ├── jsLinters │ └── eslint.xml ├── misc.xml ├── modules.xml └── vcs.xml ├── .prettierrc.js ├── .sample.env ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── TODO.md ├── assets └── membercode-bg-small.png ├── environment.d.ts ├── package.json ├── prisma └── schema.prisma ├── src ├── commands │ ├── admin │ │ └── trigger.ts │ ├── general │ │ ├── ping.ts │ │ ├── rep.ts │ │ ├── serverinfo.ts │ │ └── userinfo.ts │ ├── tag │ │ ├── code.ts │ │ ├── info.ts │ │ └── rule.ts │ └── userContextMenus │ │ ├── ban.ts │ │ └── rep.ts ├── config.example.ts ├── data │ ├── constants.ts │ ├── cooldown.ts │ ├── idData.dev.ts │ ├── idData.prod.ts │ ├── index.ts │ ├── messages.ts │ └── sourcebinLanguageData.ts ├── events │ ├── InteractionCreate.ts │ ├── MessageCreate.ts │ └── Ready.ts ├── features │ ├── announcementsReaction.ts │ ├── boosterDM.ts │ ├── index.ts │ ├── promotionTimeout.ts │ ├── reputation.ts │ └── triggerSystem.ts ├── handlers │ ├── handleAutocomplete.ts │ ├── handleButtons.ts │ ├── handleContentMenus.ts │ ├── handleEvents.ts │ ├── handleSlashCommands.ts │ └── index.ts ├── index.ts ├── interactions │ ├── buttons │ │ ├── tagAccept.ts │ │ ├── tagDecline.ts │ │ ├── tagModifyAccept.ts │ │ └── tagModifyDecline.ts │ └── contextMenus │ │ └── user │ │ ├── ban.ts │ │ ├── kick.ts │ │ └── softban.ts ├── lib │ ├── classes │ │ ├── ApplicationCommand.ts │ │ ├── Button.ts │ │ ├── Cheeka.ts │ │ └── Event.ts │ ├── functions │ │ ├── cacheData.ts │ │ ├── createTag.ts │ │ ├── deleteTag.ts │ │ ├── getTagNames.ts │ │ ├── getTopReps.ts │ │ ├── modifyTag.ts │ │ ├── registerApplicatonCommands.ts │ │ ├── registerButtons.ts │ │ ├── tagCreateRequest.ts │ │ ├── tagModifyRequest.ts │ │ └── viewTag.ts │ └── index.ts ├── modules │ ├── addRep.ts │ ├── index.ts │ ├── logRep.ts │ └── trigger │ │ ├── handleTriggerPattern.ts │ │ └── handleTriggerType.ts ├── types │ ├── HandlersTypes.ts │ ├── InteractionTypes.ts │ ├── TagTypes.ts │ ├── configType.ts │ ├── envType.ts │ ├── index.ts │ ├── miscTypes.ts │ └── repTypes.ts └── utils │ ├── Cache.ts │ ├── HumanizeMillisecond.ts │ ├── getFiles.ts │ ├── index.ts │ └── raise.ts ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: './node_modules/gts/', 3 | rules: { 4 | '@typescript-eslint/no-explicit-any': 'off', 5 | '@typescript-eslint/no-unused-vars': 'off', 6 | 'node/no-unpublished-import': 'off', 7 | 'prettier/prettier': ['error', { endOfLine: 'lf' }], 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Unwanted items 2 | dist/ 3 | node_modules/ 4 | 5 | # Secrets 6 | .env 7 | src/config.ts 8 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | yarn prerunjob 5 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /.idea/Cheeka-Development.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 65 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/fastRequest/fastRequestCurrentProjectLocalConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/jpa-buddy.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/jsLinters/eslint.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ...require('gts/.prettierrc.json'), 3 | singleQuote: true, 4 | tabWidth: 4, 5 | bracketSpacing: true, 6 | endOfLine: 'lf', 7 | }; 8 | -------------------------------------------------------------------------------- /.sample.env: -------------------------------------------------------------------------------- 1 | 2 | NODE_ENV='' # 'dev' | 'prod' 3 | 4 | # Token 5 | DEV_BOT_TOKEN='' 6 | PROD_BOT_TOKEN='' 7 | 8 | # Bot IDs 9 | DEV_CLIENT_ID='' 10 | PROD_CLIENT_ID='' 11 | 12 | # Guilds 13 | DEV_GUILD_ID='' 14 | MAIN_GUILD_ID='' 15 | 16 | # Database 17 | DATABASE_URL="mongodb://username:password@localhost:5432/database?schema=public" 18 | 19 | #Reaction stuffs 20 | OPENAI_API_KEY='' 21 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": ["Cheeka"] 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023 IGP Developers Team 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 🐶 Cheeka 3 |

4 | 5 |
6 | Cheeka is a Discord Bot for ImagineGamingPlay's Server. 7 | 8 |
9 | 10 | ### Contents: 11 | 12 | 13 | 14 | - [Contents:](#contents) 15 | - [📂 Setup](#📂-setup) 16 | - [⚙️ Support](#️-support) 17 | - [🙋‍♂️ Contributing](#🙋‍️-contributing) 18 | - [👨‍💻 Authors](#👨‍💻-authors) 19 | - [📄 License](#📄-license) 20 | 21 | 22 | 23 | --- 24 | 25 | ## 📂 Setup 26 | 27 | 1. Clone this repository 28 | 29 | ``` 30 | git clone https://github.com/ImagineGamingPlay/Cheeka-Development 31 | ``` 32 | 33 | 2. Fill-in `.env.sample` file. 34 | 3. Rename `.env.sample` → `.env` 35 | 4. Update `src/config.ts` 36 | 5. Install all the dependencies 37 | 38 | ```python 39 | yarn 40 | ``` 41 | 42 | OR 43 | 44 | ``` 45 | node install 46 | ``` 47 | 48 | 6. Build the project 49 | ``` 50 | yarn build 51 | ``` 52 | OR 53 | ``` 54 | npm run build 55 | ``` 56 | 7. Run your bot 57 | ``` 58 | yarn start 59 | ``` 60 | OR 61 | ``` 62 | npm start 63 | ``` 64 | 65 | ## ⚙️ Support 66 | 67 | Please join our [Discord Server](https://discord.gg/igp-s-coding-villa-697495719816462436) for getting help with the bot. If you are facing any issues, and you're sure it's a problem on our side, please [create an issue](https://github.com/ImagineGamingPlay/Cheeka-Development/issues/new). 68 | 69 | ## 🙋‍♂️ Contributing 70 | 71 | Contributions are always welcomed! Please refer to [CONTRIBUTING.md](https://github.com/ImagineGamingPlay/Cheeka-Development/blob/v2/CONTRIBUTING.md) for more information 72 | 73 | ## 👨‍💻 Authors 74 | 75 | - [@Aljoberg](https://github.com/Aljoberg) 76 | - [@Daysling](https://github.com/NightSling) 77 | - [@GoodBoyNeon](https://github.com/GoodBoyNeon) 78 | 79 | ## 📄 License 80 | 81 | This project uses the [MIT](https://mit-license.org/) License 82 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | - [ ] Warning System 2 | - [x] Promotion Blacklist system 3 | - [x] Tag systems (divided into three different commands - `/rule`, `/info` & `/code`. ) 4 | - [ ] Moderation commands. 5 | - [ ] Better tip system. 6 | -------------------------------------------------------------------------------- /assets/membercode-bg-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImagineGamingPlay/Cheeka-Development/88d92125b53de7c5974ad53b8defbc397b440bfe/assets/membercode-bg-small.png -------------------------------------------------------------------------------- /environment.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * This file declares a global namespace for the 'ProcessEnv' interface 3 | * which consists of all the key value pairs available in .env file 4 | */ 5 | 6 | declare global { 7 | namespace NodeJS { 8 | interface ProcessEnv { 9 | NODE_ENV: 'dev' | 'prod'; 10 | 11 | DEV_BOT_TOKEN: string; 12 | PROD_BOT_TOKEN: string; 13 | 14 | DEV_CLIENT_ID: string; 15 | PROD_CLIENT_ID: string; 16 | 17 | DEV_GUILD_ID: string; 18 | MAIN_GUILD_ID: string; 19 | 20 | DATABASE_URL: string; 21 | 22 | OPENAI_API_KEY: string; 23 | } 24 | } 25 | } 26 | 27 | export {}; 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cheeka", 3 | "version": "1.0.0", 4 | "description": "A powerful Discord Bot for Imagine Gaming Play's Discord Server", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "build": "tsc", 8 | "build:tsup": "tsup src/index.ts --format cjs,esm", 9 | "start": "node .", 10 | "prod": "yarn build && yarn start", 11 | "dev": "nodemon src/index.ts", 12 | "lint": "eslint .", 13 | "lint:fix": "eslint --fix .", 14 | "prettier": "prettier . --check", 15 | "prettier:fix": "prettier . --write", 16 | "prerunjob": "yarn prettier:fix && yarn lint:fix", 17 | "prepare": "husky install", 18 | "dbgen": "prisma format && prisma generate" 19 | }, 20 | "engines": { 21 | "node": "20" 22 | }, 23 | "license": "MIT", 24 | "dependencies": { 25 | "@prisma/client": "^5.2.0", 26 | "axios": "^1.4.0", 27 | "canvas": "^2.11.2", 28 | "chalk": "^4.1.2", 29 | "console-wizard": "^1.3.2", 30 | "discord.js": "^14.11.0", 31 | "dotenv": "^16.0.3", 32 | "form-data": "^4.0.0", 33 | "get-image-colors": "^4.0.1", 34 | "glob": "7.2.0", 35 | "openai": "^3.2.1", 36 | "regex-fun": "^2.0.3", 37 | "save-buffer": "^1.3.1", 38 | "util": "^0.12.5" 39 | }, 40 | "devDependencies": { 41 | "@types/get-image-colors": "^4.0.2", 42 | "@types/glob": "^7.2.0", 43 | "@types/node": "^20.2.5", 44 | "@types/ws": "^8.5.4", 45 | "@typescript-eslint/eslint-plugin": "5.6.0", 46 | "@typescript-eslint/parser": "5.6.0", 47 | "cz-conventional-changelog": "^3.3.0", 48 | "eslint": "7.32.0", 49 | "gts": "^3.1.1", 50 | "husky": "^8.0.0", 51 | "nodemon": "^2.0.20", 52 | "prettier": "^2.8.8", 53 | "prisma": "^5.2.0", 54 | "ts-node": "^10.9.1", 55 | "tsup": "^7.1.0", 56 | "typescript": "^5.0.4" 57 | }, 58 | "config": { 59 | "commitizen": { 60 | "path": "./node_modules/cz-conventional-changelog" 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | datasource db { 5 | provider = "mongodb" 6 | url = env("DATABASE_URL") 7 | } 8 | 9 | generator client { 10 | provider = "prisma-client-js" 11 | } 12 | 13 | model Config { 14 | id String @id @default(auto()) @map("_id") @db.ObjectId 15 | repLeaderboardMsgId String? 16 | } 17 | 18 | model Trigger { 19 | type String @unique 20 | id String @id @default(auto()) @map("_id") @db.ObjectId 21 | stringMatch String[] @default([]) 22 | regexMatch String[] @default([]) 23 | replyMessageContent String 24 | } 25 | 26 | // - user 27 | model User { 28 | id String @id @default(cuid()) @map("_id") 29 | userId String @unique @map("userId") 30 | createdAt DateTime @default(now()) 31 | updatedAt DateTime @updatedAt 32 | punishments Punishment[] 33 | Reputation Reputation? 34 | } 35 | 36 | model Punishment { 37 | id String @id @default(auto()) @map("_id") @db.ObjectId 38 | type PunishmentType 39 | reason String 40 | User User? @relation(fields: [userId], references: [id]) 41 | userId String? 42 | } 43 | 44 | enum PunishmentType { 45 | TIMEOUT 46 | WARN 47 | KICK 48 | SOFTBAN 49 | BAN 50 | } 51 | 52 | // TAG SYSTEM {{ 53 | model Tag { 54 | id String @id @default(cuid()) @map("_id") 55 | name String @unique 56 | type TagType 57 | accepted Boolean @default(false) 58 | content String 59 | newContent String? 60 | createdAt DateTime @default(now()) 61 | updatedAt DateTime @updatedAt 62 | ownerId String 63 | // owner User? @relation(fields: [ownerId], references: [id]) 64 | } 65 | 66 | enum TagType { 67 | RULE 68 | CODE 69 | INFO 70 | } 71 | 72 | // }} 73 | 74 | model PromotionBlacklist { 75 | id String @id @default(auto()) @map("_id") @db.ObjectId 76 | userId String @unique 77 | indexData IndexData[] 78 | } 79 | 80 | model IndexData { 81 | id String @id @default(auto()) @map("_id") @db.ObjectId 82 | channelId String 83 | index Int 84 | PromotionBlacklist PromotionBlacklist @relation(fields: [userId], references: [userId]) 85 | userId String 86 | 87 | @@unique([userId, channelId]) 88 | } 89 | 90 | model Reputation { 91 | id String @id @default(auto()) @map("_id") @db.ObjectId 92 | count Int 93 | user User @relation(fields: [userId], references: [userId]) 94 | userId String @unique 95 | } 96 | -------------------------------------------------------------------------------- /src/commands/admin/trigger.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ApplicationCommandOptionType, 3 | ApplicationCommandType, 4 | } from 'discord.js'; 5 | import { prisma } from '../..'; 6 | import { Command } from '../../lib'; 7 | import { cacheTriggerPatterns } from '../../lib/functions/cacheData'; 8 | import { handleTriggerPattern, handleTriggerType } from '../../modules'; 9 | 10 | export default new Command({ 11 | name: 'trigger', 12 | description: 'Create/Delete Triggers', 13 | type: ApplicationCommandType.ChatInput, 14 | defaultMemberPermissions: ['KickMembers'], 15 | options: [ 16 | { 17 | name: 'type', 18 | description: 'Update trigger types', 19 | type: ApplicationCommandOptionType.SubcommandGroup, 20 | options: [ 21 | { 22 | name: 'add', 23 | description: 'Add a trigger type', 24 | type: ApplicationCommandOptionType.Subcommand, 25 | options: [ 26 | { 27 | name: 'name', 28 | description: 'Name of the trigger type', 29 | type: ApplicationCommandOptionType.String, 30 | required: true, 31 | }, 32 | { 33 | name: 'reply_message', 34 | description: 'The content of the message to reply', 35 | type: ApplicationCommandOptionType.String, 36 | required: true, 37 | }, 38 | ], 39 | }, 40 | { 41 | name: 'delete', 42 | description: 'Delete a trigger type', 43 | type: ApplicationCommandOptionType.Subcommand, 44 | options: [ 45 | { 46 | name: 'name', 47 | description: 'Name of the type to delete', 48 | type: ApplicationCommandOptionType.String, 49 | // choices: triggerTypeChoiceData, 50 | autocomplete: true, 51 | required: true, 52 | }, 53 | ], 54 | }, 55 | { 56 | name: 'modify', 57 | description: 'Delete a trigger', 58 | type: ApplicationCommandOptionType.Subcommand, 59 | options: [ 60 | { 61 | name: 'name', 62 | description: 'Name of the type to modify', 63 | type: ApplicationCommandOptionType.String, 64 | // choices: triggerTypeChoiceData, 65 | autocomplete: true, 66 | required: true, 67 | }, 68 | { 69 | name: 'reply_message', 70 | description: 'The content of the message to reply', 71 | type: ApplicationCommandOptionType.String, 72 | required: true, 73 | }, 74 | ], 75 | }, 76 | ], 77 | }, 78 | { 79 | name: 'pattern', 80 | description: 'Modify trigger patterns', 81 | type: ApplicationCommandOptionType.SubcommandGroup, 82 | options: [ 83 | { 84 | name: 'add', 85 | description: 'Add a trigger pattern', 86 | type: ApplicationCommandOptionType.Subcommand, 87 | options: [ 88 | { 89 | name: 'type', 90 | description: 91 | 'Select the type of trigger you want to add', 92 | type: ApplicationCommandOptionType.String, 93 | required: true, 94 | // choices: triggerTypeChoiceData, 95 | autocomplete: true, 96 | }, 97 | { 98 | name: 'pattern', 99 | description: 100 | 'string or regex (javascript syntax for regex)', 101 | type: ApplicationCommandOptionType.String, 102 | required: true, 103 | }, 104 | ], 105 | }, 106 | { 107 | name: 'delete', 108 | description: 'Delete a trigger pattern', 109 | type: ApplicationCommandOptionType.Subcommand, 110 | options: [ 111 | { 112 | name: 'type', 113 | description: 114 | 'Select the type of trigger you want to delete', 115 | type: ApplicationCommandOptionType.String, 116 | required: true, 117 | // choices: triggerTypeChoiceData, 118 | autocomplete: true, 119 | }, 120 | { 121 | name: 'pattern', 122 | description: 123 | 'string or regex (javascript syntax for regex)', 124 | type: ApplicationCommandOptionType.String, 125 | required: true, 126 | autocomplete: true, 127 | }, 128 | ], 129 | }, 130 | ], 131 | }, 132 | ], 133 | async autocomplete(interaction) { 134 | const { name, value } = interaction.options.getFocused(true); 135 | 136 | if (name === 'pattern') { 137 | const triggers = await prisma.trigger.findMany(); 138 | if (!triggers) return; 139 | 140 | const choices = triggers.reduce((acc, cur) => { 141 | return [...acc, ...cur.regexMatch, ...cur.stringMatch]; 142 | }, [] as string[]); 143 | 144 | const filtered = choices.filter(c => c.includes(value)); 145 | await interaction.respond( 146 | filtered.map(c => ({ name: c, value: c })) 147 | ); 148 | } 149 | if (name === 'name' || name === 'type') { 150 | const triggers = await prisma.trigger.findMany(); 151 | const choices = triggers.map(trigger => ({ 152 | name: trigger.type, 153 | value: trigger.type, 154 | })); 155 | 156 | const filtered = choices.filter(c => c.name.includes(value)); 157 | await interaction.respond(filtered); 158 | } 159 | }, 160 | run: async ({ interaction }) => { 161 | await interaction.deferReply(); 162 | const subcommandGroup = interaction.options.getSubcommandGroup(); 163 | if (!subcommandGroup) return; 164 | 165 | if (subcommandGroup === 'type') await handleTriggerType(interaction); 166 | if (subcommandGroup === 'pattern') { 167 | await handleTriggerPattern(interaction); 168 | } 169 | // Cache the trigger patterns 170 | await cacheTriggerPatterns(); 171 | }, 172 | }); 173 | -------------------------------------------------------------------------------- /src/commands/general/ping.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationCommandType, EmbedBuilder } from 'discord.js'; 2 | import { Command } from '../../lib'; 3 | import { humanizeMillisecond } from '../../utils/HumanizeMillisecond'; 4 | 5 | export default new Command({ 6 | name: 'ping', 7 | description: 'View the connection status', 8 | type: ApplicationCommandType.ChatInput, 9 | 10 | run: async ({ interaction, client }) => { 11 | const uptimeInMilliseconds = client.uptime ?? 0; 12 | const { hours, minutes } = humanizeMillisecond(uptimeInMilliseconds); 13 | 14 | const message = await interaction.deferReply({ fetchReply: true }); 15 | const clientPing = 16 | message.createdTimestamp - interaction.createdTimestamp; 17 | const websocketPing = client.ws.ping; 18 | 19 | const clientPingEmoji = getPingStatusInEmoji(clientPing); 20 | const websocketPingEmoji = getPingStatusInEmoji(websocketPing); 21 | 22 | const embed = new EmbedBuilder({ 23 | title: ':ping_pong: Ping Status', 24 | description: 'Information about the latency and uptime!', 25 | color: client.config.colors.blurple, 26 | fields: [ 27 | { 28 | name: 'Ping', 29 | value: `Client: ${clientPingEmoji} ${clientPing}ms\nWebsocket: ${websocketPingEmoji} ${websocketPing}ms`, 30 | inline: true, 31 | }, 32 | { 33 | name: 'Uptime', 34 | value: `${hours} hours, ${minutes} minutes`, 35 | inline: true, 36 | }, 37 | ], 38 | timestamp: new Date(), 39 | }); 40 | 41 | interaction.editReply({ embeds: [embed] }); 42 | }, 43 | }); 44 | 45 | const getPingStatusInEmoji = (ping: number) => { 46 | if (ping < 150) { 47 | return '🟢'; 48 | } 49 | if (ping < 350) { 50 | return '🟡'; 51 | } 52 | return '🔴'; 53 | }; 54 | -------------------------------------------------------------------------------- /src/commands/general/rep.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ApplicationCommandOptionType, 3 | ApplicationCommandType, 4 | EmbedBuilder, 5 | GuildMember, 6 | } from 'discord.js'; 7 | import { Command } from '../../lib'; 8 | import { addRep } from '../../modules/addRep'; 9 | import { logRep } from '../../modules'; 10 | import { updateRepLeaderboard } from '../../features'; 11 | import { prisma } from '../..'; 12 | 13 | export default new Command({ 14 | name: 'rep', 15 | description: 'Add or view reputations', 16 | type: ApplicationCommandType.ChatInput, 17 | options: [ 18 | { 19 | name: 'add', 20 | type: ApplicationCommandOptionType.Subcommand, 21 | description: 'Add a reputation', 22 | options: [ 23 | { 24 | name: 'user', 25 | type: ApplicationCommandOptionType.User, 26 | description: 'The user to repute', 27 | required: true, 28 | }, 29 | ], 30 | }, 31 | { 32 | name: 'view', 33 | type: ApplicationCommandOptionType.Subcommand, 34 | description: "View someone's reputation", 35 | options: [ 36 | { 37 | name: 'user', 38 | type: ApplicationCommandOptionType.User, 39 | description: 'The target user', 40 | required: false, 41 | }, 42 | ], 43 | }, 44 | { 45 | name: 'remove', 46 | type: ApplicationCommandOptionType.Subcommand, 47 | description: 'remove one reputation from target', 48 | options: [ 49 | { 50 | name: 'user', 51 | type: ApplicationCommandOptionType.User, 52 | description: 'The target user', 53 | required: true, 54 | }, 55 | { 56 | name: 'count', 57 | type: ApplicationCommandOptionType.Number, 58 | description: 'The amount of reps to remove', 59 | required: false, 60 | }, 61 | ], 62 | }, 63 | { 64 | name: 'clear', 65 | type: ApplicationCommandOptionType.Subcommand, 66 | description: 'remove all reputations from target', 67 | options: [ 68 | { 69 | name: 'user', 70 | type: ApplicationCommandOptionType.User, 71 | description: 'The target user', 72 | required: true, 73 | }, 74 | ], 75 | }, 76 | { 77 | name: 'purge', 78 | type: ApplicationCommandOptionType.Subcommand, 79 | description: 'Delete all reps', 80 | }, 81 | ], 82 | run: async ({ client, interaction, options }) => { 83 | const subcommand = options?.getSubcommand(); 84 | 85 | if (subcommand === 'view') { 86 | const user = options?.getUser('user') || interaction.user; 87 | if (user.bot) { 88 | await interaction.reply({ 89 | content: "Welp... bots don't really have reputations..", 90 | ephemeral: true, 91 | }); 92 | } 93 | 94 | const reputation = await prisma.reputation.findUnique({ 95 | where: { 96 | userId: user.id, 97 | }, 98 | }); 99 | 100 | if (!reputation) { 101 | await interaction.reply({ 102 | embeds: [ 103 | new EmbedBuilder({ 104 | description: `***${user} has no reputations***`, 105 | color: client.config.colors.blurple, 106 | }), 107 | ], 108 | }); 109 | return; 110 | } 111 | await interaction.reply({ 112 | embeds: [ 113 | new EmbedBuilder({ 114 | description: `***${user} has ${ 115 | reputation.count 116 | } reputation${reputation.count === 1 ? '' : 's'}***`, 117 | color: client.config.colors.blurple, 118 | }), 119 | ], 120 | }); 121 | } 122 | 123 | if (subcommand === 'add') { 124 | const member = interaction.options.getMember('user') as GuildMember; 125 | if (!member) return; 126 | await addRep(member, interaction); 127 | } 128 | 129 | if ( 130 | subcommand === 'remove' || 131 | (subcommand === 'clear' && 132 | !interaction.member.permissions.has('Administrator')) 133 | ) { 134 | await interaction.reply({ 135 | content: 136 | 'You do NOT have sufficient permissions to perform this action!', 137 | ephemeral: true, 138 | }); 139 | return; 140 | } 141 | if (subcommand === 'remove') { 142 | const member = options?.getMember('user') as GuildMember; 143 | const count = options?.getNumber('count') || 1; 144 | if (!member) return; 145 | 146 | await prisma.reputation.update({ 147 | where: { 148 | userId: member.id, 149 | }, 150 | data: { 151 | count: { 152 | decrement: count, 153 | }, 154 | }, 155 | }); 156 | const reply = await interaction.reply({ 157 | content: `Removed ${count} reputation from ${member}`, 158 | ephemeral: true, 159 | }); 160 | await logRep(member, interaction, reply, 'REMOVE', count); 161 | } 162 | if (subcommand === 'clear') { 163 | const member = options?.getMember('user') as GuildMember; 164 | if (!member) return; 165 | 166 | await prisma.reputation.delete({ 167 | where: { 168 | userId: member.id, 169 | }, 170 | }); 171 | const reply = await interaction.reply({ 172 | content: `Removed all reputations from ${member}`, 173 | ephemeral: true, 174 | }); 175 | await logRep(member, interaction, reply, 'CLEAR'); 176 | } 177 | if (subcommand === 'purge') { 178 | if (interaction.member.id !== client.config.ownerId) { 179 | await interaction.reply({ 180 | content: 181 | 'You do NOT have sufficient permissions to perform this action!', 182 | ephemeral: true, 183 | }); 184 | return; 185 | } 186 | await prisma.reputation.deleteMany(); 187 | } 188 | await updateRepLeaderboard(); 189 | }, 190 | }); 191 | -------------------------------------------------------------------------------- /src/commands/general/serverinfo.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ApplicationCommandType, 3 | ChannelType, 4 | ColorResolvable, 5 | EmbedBuilder, 6 | } from 'discord.js'; 7 | import getColor from 'get-image-colors'; 8 | import { Command } from '../../lib'; 9 | 10 | export default new Command({ 11 | name: 'serverinfo', 12 | description: 'Server Information', 13 | type: ApplicationCommandType.ChatInput, 14 | 15 | run: async ({ interaction }) => { 16 | const { guild } = interaction; 17 | 18 | const totalCategories = 19 | guild?.channels.cache.filter( 20 | channel => channel?.type === ChannelType.GuildCategory 21 | ).size || 0; 22 | const totalThreads = 23 | guild?.channels.cache.filter( 24 | c => 25 | c.type === ChannelType.PublicThread || 26 | c.type === ChannelType.PrivateThread 27 | ).size || 0; 28 | const totalChannels = 29 | (guild?.channels.cache.size || 0) - totalCategories - totalThreads; 30 | const numTextChannel = guild?.channels.cache.filter( 31 | c => c.type === ChannelType.GuildText 32 | ).size; 33 | const numVoiceChannel = guild?.channels.cache.filter( 34 | channel => channel.type === ChannelType.GuildVoice 35 | ).size; 36 | const numForumChannel = guild?.channels.cache.filter( 37 | channel => channel.type === ChannelType.GuildForum 38 | ).size; 39 | 40 | const colors = await getColor( 41 | `${guild?.iconURL({ extension: 'png' })}` 42 | ); 43 | const hexColors = colors.map(color => color.hex()); 44 | const primaryColorHex = hexColors[0] as ColorResolvable; 45 | 46 | if (!guild) return; 47 | 48 | const serverCreatedTimestamp = Math.floor( 49 | guild?.createdTimestamp / 1000 50 | ); 51 | 52 | const embed = new EmbedBuilder() 53 | .setTitle(guild?.name || "IGP's Coding Villa") 54 | .setThumbnail(`${guild?.iconURL()}`) 55 | .setColor(primaryColorHex) 56 | .setFields([ 57 | { 58 | name: 'Server ID', 59 | value: `${guild?.id}`, 60 | inline: true, 61 | }, 62 | { 63 | name: 'Total Members', 64 | value: `${guild?.memberCount}`, 65 | inline: true, 66 | }, 67 | { 68 | name: 'Server Owner', 69 | value: `<@${guild?.ownerId}>`, 70 | inline: true, 71 | }, 72 | { 73 | name: 'Server Created', 74 | value: ``, 75 | inline: true, 76 | }, 77 | { 78 | name: 'Total Boosts', 79 | value: `${guild?.premiumSubscriptionCount}`, 80 | inline: true, 81 | }, 82 | { 83 | name: 'Total Channels', 84 | value: `${totalChannels}`, 85 | inline: true, 86 | }, 87 | { 88 | name: 'Text Channels', 89 | value: `${numTextChannel}`, 90 | inline: true, 91 | }, 92 | { 93 | name: 'Voice Channels', 94 | value: `${numVoiceChannel}`, 95 | inline: true, 96 | }, 97 | { 98 | name: 'Forum Channels', 99 | value: `${numForumChannel}`, 100 | inline: true, 101 | }, 102 | ]); 103 | 104 | interaction.reply({ embeds: [embed] }); 105 | }, 106 | }); 107 | -------------------------------------------------------------------------------- /src/commands/general/userinfo.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ActionRowBuilder, 3 | ApplicationCommandOptionType, 4 | ApplicationCommandType, 5 | ButtonBuilder, 6 | ButtonStyle, 7 | ColorResolvable, 8 | EmbedBuilder, 9 | GuildMember, 10 | } from 'discord.js'; 11 | import getColor from 'get-image-colors'; 12 | import { Command } from '../../lib'; 13 | import { BadgeListType } from '../../types/miscTypes'; 14 | 15 | export default new Command({ 16 | name: 'userinfo', 17 | description: 'Get information about a user or yourself.', 18 | type: ApplicationCommandType.ChatInput, 19 | options: [ 20 | { 21 | name: 'user', 22 | description: 23 | 'The user whose information you want. Leave blank for your information', 24 | required: false, 25 | type: ApplicationCommandOptionType.User, 26 | }, 27 | ], 28 | 29 | run: async ({ interaction, options }) => { 30 | const member = 31 | (options?.getMember('user') as GuildMember) || interaction.member; 32 | 33 | if (!member) { 34 | await interaction.reply({ 35 | content: 'User not found!', 36 | ephemeral: true, 37 | }); 38 | return; 39 | } 40 | 41 | const colors = await getColor( 42 | `${member.displayAvatarURL({ extension: 'png' })}` 43 | ); 44 | const hexColors = colors.map(color => color.hex()); 45 | const primaryColorHex = hexColors[0] as ColorResolvable; 46 | 47 | if (!member || !member.joinedTimestamp) return; 48 | 49 | const userCreatedTimestamp = Math.floor( 50 | member.user.createdTimestamp / 1000 51 | ); 52 | const userJoinedTimestamp = Math.floor(member.joinedTimestamp / 1000); 53 | 54 | let memberPremiumStatus = 'No'; 55 | 56 | if (member.premiumSince) { 57 | memberPremiumStatus = 'Yes'; 58 | } 59 | 60 | const userFlags = member.user.flags?.toArray(); 61 | const badges = getBadges(userFlags as string[]); 62 | 63 | const hoistRole = member.roles.hoist; 64 | const highestRole = member.roles.highest; 65 | let mainRoles; 66 | 67 | mainRoles = 68 | hoistRole === highestRole 69 | ? `${hoistRole}` 70 | : `${hoistRole} ${highestRole}`; 71 | 72 | if (!hoistRole) { 73 | mainRoles = `${highestRole}`; 74 | } 75 | 76 | const embed = new EmbedBuilder() 77 | .setTitle(`${member.user.username}`) 78 | .setColor(primaryColorHex) 79 | .setThumbnail(`${member.displayAvatarURL()}`) 80 | .setDescription('**General Information**') 81 | .setFields([ 82 | { 83 | name: 'User ID', 84 | value: `${member.id}`, 85 | inline: true, 86 | }, 87 | { 88 | name: 'Nickname', 89 | value: member.nickname || 'None', 90 | inline: true, 91 | }, 92 | { 93 | name: 'Created', 94 | value: ``, 95 | inline: true, 96 | }, 97 | { 98 | name: 'Joined', 99 | value: ``, 100 | inline: true, 101 | }, 102 | { 103 | name: 'Booster', 104 | value: `${memberPremiumStatus}`, 105 | inline: true, 106 | }, 107 | { 108 | name: '\n', 109 | value: '\n', 110 | inline: false, 111 | }, 112 | { 113 | name: 'Badges and Roles', 114 | value: '\n', 115 | inline: false, 116 | }, 117 | { 118 | name: 'Badges', 119 | value: `${badges.join(' ')}` || 'None', 120 | inline: false, 121 | }, 122 | { 123 | name: 'Major role(s)', 124 | value: `${mainRoles}` || 'None', 125 | inline: false, 126 | }, 127 | ]); 128 | const avatarButton = new ButtonBuilder() 129 | .setURL(member.displayAvatarURL()) 130 | .setStyle(ButtonStyle.Link) 131 | .setLabel('Avatar URL'); 132 | // .setEmoji(':link:'); 133 | 134 | const buttons = [avatarButton]; 135 | 136 | const bannerUrl = member.user.bannerURL(); 137 | if (bannerUrl) { 138 | const bannerButton = new ButtonBuilder() 139 | 140 | .setURL(bannerUrl) 141 | .setStyle(ButtonStyle.Link) 142 | .setLabel('Banner URL'); 143 | // .setEmoji(':link:'); 144 | 145 | buttons.push(bannerButton); 146 | } 147 | 148 | const row = new ActionRowBuilder({ 149 | components: buttons, 150 | }); 151 | await interaction.reply({ 152 | embeds: [embed], 153 | components: [row], 154 | }); 155 | }, 156 | }); 157 | 158 | const getBadges = (userFlags: string[]) => { 159 | const badgeList: BadgeListType = { 160 | ActiveDeveloper: '<:activedeveloperbadge:1116725876709662862> ', 161 | BugHunterLevel1: '<:discordbughunter:1116725897781841993> ', 162 | BugHunterLevel2: '<:discordgoldbughunter:1116725916056424478> ', 163 | PremiumEarlySupporter: '<:earlysupporter:1116725887732305980> ', 164 | Partner: '<:partneredserverowner:1116725854282731543> ', 165 | Staff: '<:discordstaff:1116725872498593814> ', 166 | HypeSquadOnlineHouse1: '<:hypesquadbravery:1116725880627150881> ', 167 | HypeSquadOnlineHouse2: '<:hypesquadbrilliance:1116725893243609171> ', 168 | HypeSquadOnlineHouse3: '<:hypesquadbalance:1116725905428066444> ', 169 | Hypesquad: '<:hypesquadevents:1116737095420104815>', 170 | CertifiedModerator: '<:certifiedmoderator:1116725864026083398> ', 171 | VerifiedDeveloper: '<:earlyverifiedbotdeveloper:1116725847106261102>', 172 | }; 173 | 174 | return userFlags.map( 175 | (flagName: string) => badgeList[flagName as keyof BadgeListType] 176 | ); 177 | }; 178 | -------------------------------------------------------------------------------- /src/commands/tag/code.ts: -------------------------------------------------------------------------------- 1 | import { TagType } from '@prisma/client'; 2 | import { 3 | ApplicationCommandOptionType, 4 | ApplicationCommandType, 5 | } from 'discord.js'; 6 | import { Command, tagCreateRequest, deleteTag } from '../../lib/'; 7 | import { TagProps } from '../../types'; 8 | import { viewTag } from '../../lib/functions/viewTag'; 9 | import { tagModifyRequest } from '../../lib/functions/tagModifyRequest'; 10 | import { getTagNames } from '../../lib/functions/getTagNames'; 11 | 12 | const TAG_TYPE = TagType.CODE; 13 | 14 | export default new Command({ 15 | name: 'code', 16 | description: 'View and manage the code snippets!', 17 | type: ApplicationCommandType.ChatInput, 18 | options: [ 19 | { 20 | name: 'view', 21 | description: 'view a code snippet!', 22 | type: ApplicationCommandOptionType.Subcommand, 23 | options: [ 24 | { 25 | name: 'name', 26 | description: 'name of the code snippet', 27 | type: ApplicationCommandOptionType.String, 28 | required: true, 29 | autocomplete: true, 30 | }, 31 | ], 32 | }, 33 | { 34 | name: 'add', 35 | description: 'add a code snippet!', 36 | type: ApplicationCommandOptionType.Subcommand, 37 | options: [ 38 | { 39 | name: 'name', 40 | description: 'name of the code snippet', 41 | type: ApplicationCommandOptionType.String, 42 | required: true, 43 | }, 44 | ], 45 | }, 46 | { 47 | name: 'modify', 48 | description: 'modify a code snippet!', 49 | type: ApplicationCommandOptionType.Subcommand, 50 | options: [ 51 | { 52 | name: 'name', 53 | description: 'name of the code snippet', 54 | type: ApplicationCommandOptionType.String, 55 | required: true, 56 | autocomplete: true, 57 | }, 58 | ], 59 | }, 60 | { 61 | name: 'delete', 62 | description: 'delete a code snippet!', 63 | type: ApplicationCommandOptionType.Subcommand, 64 | options: [ 65 | { 66 | name: 'name', 67 | description: 'name of the code snippet', 68 | type: ApplicationCommandOptionType.String, 69 | required: true, 70 | autocomplete: true, 71 | }, 72 | ], 73 | }, 74 | ], 75 | autocomplete: async interaction => { 76 | const focused = interaction.options.getFocused(); 77 | const tagChoices = await getTagNames(TAG_TYPE); 78 | 79 | const filtered = tagChoices.filter(choice => choice.includes(focused)); 80 | await interaction.respond( 81 | filtered.map(choice => ({ name: choice, value: choice })) 82 | ); 83 | }, 84 | run: async ({ options, interaction }) => { 85 | if (!options) return; 86 | const subcommand = options?.getSubcommand(); 87 | 88 | const name = options?.getString('name'); 89 | if (!name) return; 90 | 91 | if (subcommand === 'view') { 92 | viewTag(name, TAG_TYPE, interaction); 93 | } 94 | 95 | if (subcommand === 'add') { 96 | const props: Omit = { 97 | name, 98 | type: TAG_TYPE, 99 | interaction, 100 | }; 101 | await tagCreateRequest(props); 102 | 103 | return; 104 | } 105 | if (subcommand === 'delete') { 106 | await deleteTag(options.getString('name') || '', interaction); 107 | return; 108 | } 109 | if (subcommand === 'modify') { 110 | const props: Omit = { 111 | name, 112 | interaction, 113 | type: TAG_TYPE, 114 | }; 115 | await tagModifyRequest(props); 116 | } 117 | }, 118 | }); 119 | -------------------------------------------------------------------------------- /src/commands/tag/info.ts: -------------------------------------------------------------------------------- 1 | import { TagType } from '@prisma/client'; 2 | import { 3 | ApplicationCommandOptionType, 4 | ApplicationCommandType, 5 | } from 'discord.js'; 6 | import { Command, tagCreateRequest, deleteTag } from '../../lib/'; 7 | import { TagProps } from '../../types'; 8 | import { viewTag } from '../../lib/functions/viewTag'; 9 | import { tagModifyRequest } from '../../lib/functions/tagModifyRequest'; 10 | import { getTagNames } from '../../lib/functions/getTagNames'; 11 | // import { getTagChoices } from '../../lib/functions/getTagChoices'; 12 | 13 | const TAG_TYPE = TagType.INFO; 14 | 15 | // const tagChoices = await getTagChoices(TAG_TYPE); 16 | 17 | export default new Command({ 18 | name: 'info', 19 | description: 'View and manage the info tags!', 20 | type: ApplicationCommandType.ChatInput, 21 | options: [ 22 | { 23 | name: 'view', 24 | description: 'view a info tag!', 25 | type: ApplicationCommandOptionType.Subcommand, 26 | options: [ 27 | { 28 | name: 'name', 29 | description: 'name of the info tag', 30 | type: ApplicationCommandOptionType.String, 31 | required: true, 32 | autocomplete: true, 33 | }, 34 | ], 35 | }, 36 | { 37 | name: 'add', 38 | description: 'add a info tag!', 39 | type: ApplicationCommandOptionType.Subcommand, 40 | options: [ 41 | { 42 | name: 'name', 43 | description: 'name of the info tag', 44 | type: ApplicationCommandOptionType.String, 45 | required: true, 46 | }, 47 | ], 48 | }, 49 | { 50 | name: 'modify', 51 | description: 'modify a info tag!', 52 | type: ApplicationCommandOptionType.Subcommand, 53 | options: [ 54 | { 55 | name: 'name', 56 | description: 'name of the info tag', 57 | type: ApplicationCommandOptionType.String, 58 | required: true, 59 | autocomplete: true, 60 | }, 61 | ], 62 | }, 63 | { 64 | name: 'delete', 65 | description: 'delete a info tag!', 66 | type: ApplicationCommandOptionType.Subcommand, 67 | options: [ 68 | { 69 | name: 'name', 70 | description: 'name of the info tag', 71 | type: ApplicationCommandOptionType.String, 72 | required: true, 73 | autocomplete: true, 74 | }, 75 | ], 76 | }, 77 | ], 78 | autocomplete: async interaction => { 79 | const focused = interaction.options.getFocused(); 80 | const tagChoices = await getTagNames(TAG_TYPE); 81 | 82 | const filtered = tagChoices.filter(choice => choice.includes(focused)); 83 | await interaction.respond( 84 | filtered.map(choice => ({ name: choice, value: choice })) 85 | ); 86 | }, 87 | run: async ({ options, interaction }) => { 88 | if (!options) return; 89 | const subcommand = options?.getSubcommand(); 90 | 91 | const name = options?.getString('name'); 92 | if (!name) return; 93 | 94 | if (subcommand === 'view') { 95 | viewTag(name, TAG_TYPE, interaction); 96 | } 97 | 98 | if (subcommand === 'add') { 99 | const props: Omit = { 100 | name, 101 | type: TAG_TYPE, 102 | interaction, 103 | }; 104 | await tagCreateRequest(props); 105 | 106 | return; 107 | } 108 | if (subcommand === 'delete') { 109 | await deleteTag(options.getString('name') || '', interaction); 110 | return; 111 | } 112 | if (subcommand === 'modify') { 113 | const props: Omit = { 114 | name, 115 | interaction, 116 | type: TAG_TYPE, 117 | }; 118 | await tagModifyRequest(props); 119 | } 120 | }, 121 | }); 122 | -------------------------------------------------------------------------------- /src/commands/tag/rule.ts: -------------------------------------------------------------------------------- 1 | import { TagType } from '@prisma/client'; 2 | import { 3 | ApplicationCommandOptionType, 4 | ApplicationCommandType, 5 | PermissionFlagsBits, 6 | } from 'discord.js'; 7 | import { Command, deleteTag, tagCreateRequest } from '../../lib/'; 8 | import { getTagNames } from '../../lib/functions/getTagNames'; 9 | import { tagModifyRequest } from '../../lib/functions/tagModifyRequest'; 10 | import { viewTag } from '../../lib/functions/viewTag'; 11 | import { TagProps } from '../../types'; 12 | 13 | const TAG_TYPE = TagType.RULE; 14 | 15 | // const tagChoices = getTagChoices(TAG_TYPE); 16 | 17 | export default new Command({ 18 | name: 'rule', 19 | description: 'View and manage the rule tags!', 20 | type: ApplicationCommandType.ChatInput, 21 | options: [ 22 | { 23 | name: 'view', 24 | description: 'view a rule tag!', 25 | type: ApplicationCommandOptionType.Subcommand, 26 | options: [ 27 | { 28 | name: 'name', 29 | description: 'name of the rule tag', 30 | type: ApplicationCommandOptionType.String, 31 | required: true, 32 | autocomplete: true, 33 | }, 34 | ], 35 | }, 36 | { 37 | name: 'add', 38 | description: 'add a rule tag!', 39 | type: ApplicationCommandOptionType.Subcommand, 40 | options: [ 41 | { 42 | name: 'name', 43 | description: 'name of the rule tag', 44 | type: ApplicationCommandOptionType.String, 45 | required: true, 46 | }, 47 | ], 48 | }, 49 | { 50 | name: 'modify', 51 | description: 'modify a rule tag!', 52 | type: ApplicationCommandOptionType.Subcommand, 53 | options: [ 54 | { 55 | name: 'name', 56 | description: 'name of the rule tag', 57 | type: ApplicationCommandOptionType.String, 58 | required: true, 59 | autocomplete: true, 60 | }, 61 | ], 62 | }, 63 | { 64 | name: 'delete', 65 | description: 'delete a rule tag!', 66 | type: ApplicationCommandOptionType.Subcommand, 67 | options: [ 68 | { 69 | name: 'name', 70 | description: 'name of the rule tag', 71 | type: ApplicationCommandOptionType.String, 72 | required: true, 73 | autocomplete: true, 74 | }, 75 | ], 76 | }, 77 | ], 78 | autocomplete: async interaction => { 79 | const focused = interaction.options.getFocused(); 80 | const tagChoices = await getTagNames(TAG_TYPE); 81 | 82 | const filtered = tagChoices.filter(choice => choice.includes(focused)); 83 | await interaction.respond( 84 | filtered.map(choice => ({ name: choice, value: choice })) 85 | ); 86 | }, 87 | run: async ({ options, interaction }) => { 88 | if (!options) return; 89 | const subcommand = options?.getSubcommand(); 90 | 91 | const name = options?.getString('name'); 92 | if (!name) return; 93 | 94 | const isAdmin = interaction.member.permissions.has( 95 | PermissionFlagsBits.Administrator 96 | ); 97 | 98 | if (subcommand !== 'view' && !isAdmin) { 99 | interaction.reply({ 100 | content: 101 | 'You do not have permissions to add, modify or delete rule tags!', 102 | ephemeral: true, 103 | }); 104 | 105 | return; 106 | } 107 | 108 | if (subcommand === 'view') { 109 | viewTag(name, TAG_TYPE, interaction); 110 | } 111 | 112 | if (subcommand === 'add') { 113 | const props: Omit = { 114 | name, 115 | type: TAG_TYPE, 116 | interaction, 117 | }; 118 | await tagCreateRequest(props); 119 | 120 | return; 121 | } 122 | if (subcommand === 'delete') { 123 | await deleteTag(options.getString('name') || '', interaction); 124 | return; 125 | } 126 | if (subcommand === 'modify') { 127 | const props: Omit = { 128 | name, 129 | interaction, 130 | type: TAG_TYPE, 131 | }; 132 | await tagModifyRequest(props); 133 | } 134 | }, 135 | }); 136 | -------------------------------------------------------------------------------- /src/commands/userContextMenus/ban.ts: -------------------------------------------------------------------------------- 1 | import { logger } from 'console-wizard'; 2 | import { 3 | ActionRowBuilder, 4 | ApplicationCommandType, 5 | EmbedBuilder, 6 | ModalActionRowComponentBuilder, 7 | ModalBuilder, 8 | PermissionFlagsBits, 9 | TextInputBuilder, 10 | TextInputStyle, 11 | } from 'discord.js'; 12 | import { Command } from '../../lib'; 13 | 14 | export default new Command({ 15 | name: 'ban', 16 | type: ApplicationCommandType.User, 17 | defaultMemberPermissions: [PermissionFlagsBits.BanMembers], 18 | 19 | run: async ({ client, interaction }) => { 20 | const target = interaction.targetMember; 21 | 22 | const notBannableErrorEmbed = new EmbedBuilder() 23 | .setTitle(`Cannot ban ${target?.user.username}!`) 24 | .setDescription( 25 | `I do not have sufficient permissions to ban <@${target?.id}>!` 26 | ) 27 | .setColor(client.config.colors.red); 28 | 29 | const rolePosErrorEmbed = new EmbedBuilder() 30 | .setTitle(`Cannot ban ${target?.user.username}!`) 31 | .setDescription( 32 | `You cannot ban <@${target?.id}> as they have a higher role that you!` 33 | ) 34 | .setColor(client.config.colors.red); 35 | 36 | const errorEmbed = new EmbedBuilder() 37 | .setTitle(':red_circle: An unexpected error occured!') 38 | .setDescription(`< @${target?.id} wasn't banned due to an error.`) 39 | .setColor(client.config.colors.red); 40 | 41 | if (!target?.bannable) { 42 | await interaction.reply({ embeds: [notBannableErrorEmbed] }); 43 | return; 44 | } 45 | 46 | const targetHighestRolePos = target.roles.highest.position; 47 | const userHighestRolePos = interaction.member?.roles.highest.position; 48 | if (targetHighestRolePos > userHighestRolePos) { 49 | await interaction.reply({ 50 | embeds: [rolePosErrorEmbed], 51 | }); 52 | return; 53 | } 54 | 55 | const modal = new ModalBuilder() 56 | .setCustomId('ban-reason') 57 | .setTitle('Reason'); 58 | 59 | const reasonInput = new TextInputBuilder() 60 | .setCustomId('reasonInput') 61 | .setLabel('Please specifiy the reason for the ban') 62 | .setPlaceholder('No reason provided') 63 | .setStyle(TextInputStyle.Paragraph) 64 | 65 | .setRequired(false); 66 | 67 | const row = new ActionRowBuilder({ 68 | components: [reasonInput], 69 | }); 70 | modal.addComponents(row); 71 | 72 | await interaction.showModal(modal); 73 | 74 | const modalInteraction = await interaction.awaitModalSubmit({ 75 | time: 2 * 60 * 1000, // 2 minutes 76 | }); 77 | 78 | const reason = 79 | modalInteraction.fields.getTextInputValue('reasonInput') ?? 80 | 'No reason provided'; 81 | 82 | const successEmbed = new EmbedBuilder() 83 | .setTitle(`Banned ${target?.user.username}!`) 84 | .setTimestamp() 85 | .setColor(client.config.colors.green); 86 | try { 87 | await target.send({ 88 | embeds: [ 89 | new EmbedBuilder({ 90 | title: 'You have been banned!', 91 | description: `You have been **banned** from **${interaction.guild?.name}**.`, 92 | fields: [ 93 | { 94 | name: 'Reason', 95 | value: reason, 96 | }, 97 | ], 98 | author: { 99 | name: `${interaction.guild?.name}`, 100 | iconURL: `${interaction.guild?.iconURL()}`, 101 | }, 102 | color: client.config.colors.red, 103 | }), 104 | ], 105 | }); 106 | } catch (err) { 107 | return; 108 | } 109 | 110 | await target?.ban({ reason: reason }).catch(async err => { 111 | await interaction.reply({ embeds: [errorEmbed] }); 112 | logger.error(err); 113 | return; 114 | }); 115 | 116 | await modalInteraction.reply({ embeds: [successEmbed] }); 117 | }, 118 | }); 119 | -------------------------------------------------------------------------------- /src/commands/userContextMenus/rep.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationCommandType } from 'discord.js'; 2 | import { Command } from '../../lib'; 3 | import { addRep } from '../../modules'; 4 | 5 | export default new Command({ 6 | name: '+ rep', 7 | type: ApplicationCommandType.User, 8 | run: async ({ interaction }) => { 9 | await addRep(interaction.targetMember, interaction); 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /src/config.example.ts: -------------------------------------------------------------------------------- 1 | /* Imports */ 2 | import { ConfigType } from './types'; 3 | 4 | export const config: ConfigType = { 5 | environment: '', // 'prod' | 'dev' 6 | openaiApiKey: 'openai_api_key', 7 | 8 | aiReactionChannels: ['channelId', 'channelId', '...'], 9 | 10 | token: 'Bot token', 11 | clientId: 'bot client Id (user Id)', 12 | guildId: "bot's guild id", 13 | ownerId: "bot and guiid's owner's id", 14 | staffRoleId: 'Role ID of staff role', 15 | repCooldownMS: 0, // Rep add cooldown in miliseconds 16 | devGuildId: 'development guild Id', 17 | mainGuildId: 'main/production guild Id', 18 | repLeaderboardColors: ['colors', 'for', 'rep', 'leaderboard', '5 of em'], 19 | 20 | developerRoleId: 'ID of the developer role', 21 | 22 | colors: { 23 | blurple: 0, // Blurple color code as int 24 | red: 0, // red color code as int 25 | green: 0, // green color code as int 26 | white: 0, // white color code as int 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /src/data/constants.ts: -------------------------------------------------------------------------------- 1 | export const codeblockRegex = 2 | /.*?[^|]*(?: *?\| *?([^`]+))? ?\n? ?```([\s\S]*?)\n([\s\S]*?)```(?:.*)/; 3 | -------------------------------------------------------------------------------- /src/data/cooldown.ts: -------------------------------------------------------------------------------- 1 | export const cooldown = new Set(); 2 | -------------------------------------------------------------------------------- /src/data/idData.dev.ts: -------------------------------------------------------------------------------- 1 | export const channels = { 2 | tagVerificationChannelId: '1122880481155878962', 3 | aiReactionChannels: ['1110551425555124324', '1126471827628228648'], 4 | repLogChannel: '1132998180242477236', 5 | repLeaderboardChannel: '1135178098732699758', 6 | messageLogChannel: '952514063873761328', 7 | memberCodesChannel: '1141596933157290054', 8 | }; 9 | 10 | export const categories = { 11 | promotionCategoryId: '1096022509406658692', 12 | }; 13 | -------------------------------------------------------------------------------- /src/data/idData.prod.ts: -------------------------------------------------------------------------------- 1 | export const channels = { 2 | tagVerificationChannelId: '972362384872189972', 3 | aiReactionChannels: ['1110551425555124324', '1126471827628228648'], 4 | repLogChannel: '805744599095050260', 5 | repLeaderboardChannel: '791612079705423872', 6 | messageLogChannel: '1028296071492943945', 7 | memberCodesChannel: '1115733989395791952', 8 | }; 9 | 10 | export const categories = { 11 | promotionCategoryId: '936241367875731467', 12 | }; 13 | -------------------------------------------------------------------------------- /src/data/index.ts: -------------------------------------------------------------------------------- 1 | import * as prodIdData from './idData.prod'; 2 | import * as devIdData from './idData.dev'; 3 | 4 | const env = process.env.NODE_ENV; 5 | 6 | export const idData = env === 'dev' ? devIdData : prodIdData; 7 | 8 | export * from './messages'; 9 | export * from './cooldown'; 10 | export * from './sourcebinLanguageData'; 11 | export * from './constants'; 12 | -------------------------------------------------------------------------------- /src/data/messages.ts: -------------------------------------------------------------------------------- 1 | export const messages = { 2 | promoBlacklistFail: { 3 | title: 'You have been blacklisted from sending promotion!', 4 | description: 5 | "Dear user, you have been blacklisted from sending promotion messages in **IGP's Coding Villa**. You will be removed from blacklist after at least 10 people send a promotion message after your message.", 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /src/events/InteractionCreate.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AutocompleteInteraction, 3 | ButtonInteraction, 4 | ContextMenuCommandInteraction, 5 | Interaction, 6 | } from 'discord.js'; 7 | import { 8 | handleAutocomplete, 9 | handleButtons, 10 | handleContextMenus, 11 | handleSlashCommands, 12 | } from '../handlers/'; 13 | import { Event } from '../lib'; 14 | import { ModifiedChatInputCommandInteraction } from '../types'; 15 | 16 | export default new Event( 17 | 'interactionCreate', 18 | async (interaction: Interaction) => { 19 | if (interaction.isChatInputCommand()) { 20 | await handleSlashCommands( 21 | interaction as ModifiedChatInputCommandInteraction 22 | ); 23 | return; 24 | } 25 | 26 | if (interaction.isButton()) { 27 | await handleButtons(interaction as ButtonInteraction); 28 | return; 29 | } 30 | 31 | if (interaction.isAutocomplete()) { 32 | await handleAutocomplete(interaction as AutocompleteInteraction); 33 | return; 34 | } 35 | 36 | if (interaction.isContextMenuCommand()) { 37 | await handleContextMenus( 38 | interaction as ContextMenuCommandInteraction 39 | ); 40 | } 41 | } 42 | ); 43 | -------------------------------------------------------------------------------- /src/events/MessageCreate.ts: -------------------------------------------------------------------------------- 1 | import { announcementsReaction, promotionTimeout } from '../features'; 2 | import { Event } from '../lib'; 3 | import { config } from '../config'; 4 | import { idData } from '../data'; 5 | import { TextChannel } from 'discord.js'; 6 | import { boosterDM } from '../features'; 7 | import { triggerSystem } from '../features/triggerSystem'; 8 | 9 | export default new Event('messageCreate', async message => { 10 | if (message.author.bot) { 11 | return; 12 | } 13 | 14 | await triggerSystem(message); 15 | 16 | const channel = message.channel as TextChannel; 17 | if (channel.parentId === idData.categories.promotionCategoryId) { 18 | await promotionTimeout(message); 19 | } 20 | boosterDM(message); 21 | if ( 22 | config.aiReactionChannels && 23 | config.openaiApiKey && 24 | config.aiReactionChannels.includes(message.channel.id) 25 | ) 26 | announcementsReaction(message); 27 | boosterDM(message); 28 | }); 29 | -------------------------------------------------------------------------------- /src/events/Ready.ts: -------------------------------------------------------------------------------- 1 | import { client } from '..'; 2 | import { updateRepLeaderboard } from '../features'; 3 | import { Event } from '../lib/classes/Event'; 4 | import { logger } from 'console-wizard'; 5 | import { cacheTriggerPatterns } from '../lib/functions/cacheData'; 6 | 7 | export default new Event('ready', async () => { 8 | logger.success(`Logged into Client: ${client.user?.username}`); 9 | await updateRepLeaderboard(); 10 | await cacheTriggerPatterns(); 11 | }); 12 | -------------------------------------------------------------------------------- /src/features/announcementsReaction.ts: -------------------------------------------------------------------------------- 1 | import { EmojiIdentifierResolvable, Message } from 'discord.js'; 2 | // import { config } from '../config'; 3 | import { Configuration, OpenAIApi } from 'openai'; 4 | 5 | const AI_REACTION_TIMES_CALLED = 5; 6 | 7 | export const announcementsReaction = ( 8 | message: Message, 9 | timesCalled?: number 10 | ) => { 11 | if (!timesCalled) timesCalled = 0; 12 | if (timesCalled > AI_REACTION_TIMES_CALLED) return; 13 | timesCalled++; 14 | const openai = new OpenAIApi( 15 | new Configuration({ apiKey: process.env.OPENAI_API_KEY }) 16 | ); 17 | openai 18 | .createCompletion({ 19 | model: 'text-davinci-003', 20 | prompt: ` 21 | React to the messages with emojis only. Use a JSON array of strings to include the emojis. The array can have 3 or 4 emojis. If you can only think of 1 or 2 relevant emojis, that's fine too. Don't add irrelevant emojis just to meet the desired length. However, don't exceed more than 3 or 4 emojis. Keep the most relevant emojis at the beginning of the array. Only provide the array as your response, no other text. 22 | 23 | Text: ${message.content} 24 | 25 | Assistant: 26 | `, 27 | max_tokens: 50, 28 | temperature: 0.9, 29 | }) 30 | .then(async resp => { 31 | let r = resp.data.choices[0].text as string; 32 | r = r.replace("'", '"'); 33 | try { 34 | const regexpMatch = r.match(/\[.*?\]/); 35 | if (!regexpMatch) return; 36 | 37 | JSON.parse(regexpMatch[0]).forEach( 38 | (e: EmojiIdentifierResolvable) => 39 | message 40 | .react(e) 41 | .catch(() => 42 | announcementsReaction(message, timesCalled) 43 | ) 44 | ); 45 | } catch { 46 | announcementsReaction(message, timesCalled); 47 | } 48 | }) 49 | .catch(() => announcementsReaction(message, timesCalled)); 50 | }; 51 | -------------------------------------------------------------------------------- /src/features/boosterDM.ts: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder, Message } from 'discord.js'; 2 | import { cooldown } from '../data'; 3 | 4 | const BOOSTER_DM_COOLDOWN = 60 * 1000; 5 | 6 | const startCooldown = (userId: string) => 7 | setTimeout(() => cooldown.delete(userId), BOOSTER_DM_COOLDOWN || 60000); 8 | 9 | export const boosterDM = async (message: Message) => { 10 | if ([8, 9, 10, 11].includes(message.type)) { 11 | if (cooldown.has(message.author.id)) return; 12 | 13 | const embed = new EmbedBuilder({ 14 | title: 'Thank you for Boosting', 15 | description: 16 | 'We wholeheartedly thank you for your kindness to the IGP community. And as a token of our gratitude, we would like to offer you some perks as well.\n\n**As long as you\'re boosted, you will:**\n <:igpboost:1120711992009818185> **Get access to the following channels:**\n <#816976990233690122>: Chat with the other boosters here.\n <#816709438693048350> & <#875073982569787422>: to enhance your bot development.\n <#875073982569787422>: Receive help faster here.\n <#1015583996958224424>: Hop on the VC which everyone can see, but only boosters can join!\n \n <:igpboost:1120711992009818185> **Be able to show off your new flashy role with a unique badge that makes you stand out from others.**\n\n** <:igpboost:1120711992009818185> Be able to promote more than others!**\nNormally, a member can only post their advertisement once in a promotion channel, and after that, they have to wait until a certain amount of promotion messages are posted (known as "the promotion messages limit").\nHowever, as a booster, the promotion messages limit for you will be lower so you can promote more often!\n[Click here to know more about the promotion messages limit](https://discord.com/channels/697495719816462436/936242386319863880/1015278382042325013)\n\n**New Perks Plan:**\nNothing planned as of now.\n\nYou can suggest new perks in the #boosters-perk-suggestion channel, we welcome new suggestions with our open arms.', 17 | thumbnail: { 18 | url: 'https://media.discordapp.net/attachments/743528061659643975/1120697032261247036/booster.png', 19 | height: 0, 20 | width: 0, 21 | }, 22 | footer: { 23 | text: "This message won't be spammed, boost as many times as you wish.", 24 | }, 25 | color: 10249133, 26 | }); 27 | 28 | try { 29 | await message.author.send({ embeds: [embed] }); 30 | cooldown.add(message.author.id); 31 | startCooldown(message.author.id); 32 | } catch { 33 | return; 34 | } 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /src/features/index.ts: -------------------------------------------------------------------------------- 1 | export * from './announcementsReaction'; 2 | export * from './boosterDM'; 3 | export * from './promotionTimeout'; 4 | export * from './reputation'; 5 | -------------------------------------------------------------------------------- /src/features/promotionTimeout.ts: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder, Message } from 'discord.js'; 2 | import { client } from '..'; 3 | 4 | const { prisma } = client; 5 | 6 | export const promotionTimeout = async (message: Message) => { 7 | let limit = 10; 8 | 9 | if (message.member?.premiumSince) { 10 | limit = 1; 11 | } 12 | 13 | if (!message.member?.id) return; 14 | 15 | const indexData = await prisma.indexData.findUnique({ 16 | where: { 17 | userId_channelId: { 18 | userId: message.member?.id, 19 | channelId: message.channelId, 20 | }, 21 | }, 22 | }); 23 | 24 | if (indexData) { 25 | await message.delete(); 26 | const guild = client.guilds.cache.get(message?.guildId || ''); 27 | 28 | const { index } = indexData; 29 | 30 | const embed = new EmbedBuilder({ 31 | title: 'Hold up!', 32 | description: `You are on limit for promotion in **IGP's Coding Villa**. You can only send another promotion message after **${index}** messages are sent after yours!`, 33 | color: client.config.colors.red, 34 | author: { 35 | name: guild?.name || 'IGP', 36 | icon_url: guild?.iconURL() || '', 37 | }, 38 | }); 39 | try { 40 | await message?.author.send({ embeds: [embed] }); 41 | } catch { 42 | return; 43 | } 44 | 45 | return; 46 | } 47 | 48 | // Decrement the index of every blacklisted user 49 | await prisma.indexData.updateMany({ 50 | data: { 51 | index: { 52 | decrement: 1, 53 | }, 54 | }, 55 | }); 56 | 57 | // blacklist.forEach(obj => { 58 | // obj.index--; 59 | // }); 60 | 61 | // Remove everyone with 0 index from blacklist 62 | await prisma.indexData.deleteMany({ 63 | where: { 64 | index: { 65 | lte: 0, 66 | }, 67 | }, 68 | }); 69 | 70 | // blacklist = blacklist.filter(obj => obj.index !== 0); 71 | 72 | // Add the message author to blacklist 73 | await prisma.promotionBlacklist.create({ 74 | data: { 75 | userId: message.author.id, 76 | indexData: { 77 | connectOrCreate: { 78 | where: { 79 | userId_channelId: { 80 | userId: message.member?.id, 81 | channelId: message.channelId, 82 | }, 83 | }, 84 | create: { 85 | channelId: message.channelId, 86 | index: limit, 87 | }, 88 | }, 89 | }, 90 | }, 91 | }); 92 | 93 | // blacklist.push({ id: message.author.id, index: limit }); 94 | }; 95 | 96 | // export const promotionTimeout = async (message: Message) => { 97 | // let limit = 3; 98 | // 99 | // // if (message.member?.premiumSince) { 100 | // if (message.member?.id === '453457425429692417') { 101 | // limit = 1; 102 | // } 103 | // 104 | // const alreadyBlacklist = await prisma.promotionBlacklist.findFirst({ 105 | // where: { 106 | // userId: message.author.id, 107 | // }, 108 | // }); 109 | // 110 | // // const alreadyBlacklist = blacklist.some( 111 | // // (obj: PromotionBlacklist) => obj.id === message.author.id 112 | // // ); 113 | // 114 | // if (alreadyBlacklist) { 115 | // await message.delete(); 116 | // const guild = client.guilds.cache.get(message?.guildId || ''); 117 | // 118 | // const { index } = alreadyBlacklist; 119 | // 120 | // const embed = new EmbedBuilder({ 121 | // title: 'Hold up!', 122 | // description: `You are on limit for promotion in **IGP's Coding Villa**. You can only send another promotion message after **${index}** messages are sent after yours!`, 123 | // color: client.config.colors.red, 124 | // author: { 125 | // name: guild?.name || 'IGP', 126 | // icon_url: guild?.iconURL() || '', 127 | // }, 128 | // }); 129 | // 130 | // await message?.author.send({ embeds: [embed] }); 131 | // 132 | // return; 133 | // } 134 | // 135 | // // Decrement the index of every blacklisted user 136 | // await prisma.promotionBlacklist.updateMany({ 137 | // data: { 138 | // index: { 139 | // decrement: 1, 140 | // }, 141 | // }, 142 | // }); 143 | // 144 | // // blacklist.forEach(obj => { 145 | // // obj.index--; 146 | // // }); 147 | // 148 | // // Remove everyone with 0 index from blacklist 149 | // await prisma.promotionBlacklist.deleteMany({ 150 | // where: { 151 | // index: { 152 | // lte: 0, 153 | // }, 154 | // }, 155 | // }); 156 | // 157 | // // blacklist = blacklist.filter(obj => obj.index !== 0); 158 | // 159 | // // Add the message author to blacklist 160 | // await prisma.promotionBlacklist.create({ 161 | // data: { 162 | // userId: message.author.id, 163 | // index: limit, 164 | // }, 165 | // }); 166 | // 167 | // // blacklist.push({ id: message.author.id, index: limit }); 168 | // }; 169 | -------------------------------------------------------------------------------- /src/features/reputation.ts: -------------------------------------------------------------------------------- 1 | import { createCanvas } from 'canvas'; 2 | import { 3 | AttachmentBuilder, 4 | CommandInteraction, 5 | EmbedBuilder, 6 | GuildMember, 7 | TextChannel, 8 | } from 'discord.js'; 9 | import { client, prisma } from '..'; 10 | import { idData } from '../data'; 11 | 12 | type TopReps = { 13 | [key: string]: number; 14 | }; 15 | 16 | const generateRepLeaderboardBar = async (topReps: TopReps) => { 17 | const totalSum = Object.values(topReps).reduce( 18 | (sum, value) => sum + value, 19 | 0 20 | ); 21 | 22 | const topRepPercentage: TopReps = {}; 23 | for (const key in topReps) { 24 | const value = topReps[key]; 25 | const percentage = (value / totalSum) * 100; 26 | topRepPercentage[key as keyof TopReps] = Math.round(percentage); 27 | } 28 | 29 | const { repLeaderboardColors } = client.config; 30 | 31 | const canvas = createCanvas(400, 180); 32 | const ctx = canvas.getContext('2d'); 33 | let startX = 0; 34 | 35 | Object.values(topRepPercentage) 36 | .sort((a, b) => b - a) 37 | .forEach((entry, i) => { 38 | const barWidth = (canvas.width * entry) / 100; 39 | 40 | ctx.fillStyle = repLeaderboardColors[i]; 41 | ctx.fillRect(startX, 0, barWidth, 60); 42 | startX += barWidth; 43 | }); 44 | 45 | const x = 0; 46 | let y = 80; 47 | Object.keys(topRepPercentage) 48 | .sort((a, b) => topRepPercentage[b] - topRepPercentage[a]) 49 | .forEach((entry, i) => { 50 | ctx.fillStyle = repLeaderboardColors[i]; 51 | ctx.fillRect(x, y, 10, 10); 52 | ctx.fillStyle = repLeaderboardColors[i]; 53 | ctx.font = '14px Arial'; 54 | ctx.fillText(`${entry} (${topReps[entry]})`, x + 20, y + 10); 55 | y += 20; 56 | }); 57 | return canvas.toBuffer(); 58 | }; 59 | 60 | export const updateRepLeaderboard = async () => { 61 | const reps = await prisma.reputation.findMany(); 62 | const topReps: TopReps = {}; 63 | 64 | const sortedTopReps = reps.sort((a, b) => b.count - a.count).slice(0, 5); 65 | 66 | for (const rep of sortedTopReps) { 67 | const user = await client.users.fetch(rep.userId); 68 | topReps[user.username] = rep.count; 69 | } 70 | 71 | const barBuffer = await generateRepLeaderboardBar(topReps); 72 | 73 | const barAttachment = new AttachmentBuilder(barBuffer, { 74 | name: 'bar.png', 75 | description: 'Leaderboard Bar', 76 | }); 77 | 78 | const embed = new EmbedBuilder({ 79 | title: 'Reputations Leaderboard', 80 | color: client.config.colors.blurple, 81 | image: { 82 | url: 'attachment://bar.png', 83 | }, 84 | }); 85 | 86 | const config = await prisma.config.findFirst(); 87 | 88 | const output = { 89 | embeds: [embed], 90 | files: [barAttachment], 91 | }; 92 | 93 | const channel = client.channels.cache.get( 94 | idData.channels.repLeaderboardChannel 95 | ) as TextChannel | undefined; 96 | 97 | if (!config || !config.repLeaderboardMsgId) { 98 | const msg = await channel?.send(output); 99 | await prisma.config.create({ 100 | data: { 101 | repLeaderboardMsgId: msg?.id, 102 | }, 103 | }); 104 | return; 105 | } 106 | const message = await channel?.messages.fetch(config.repLeaderboardMsgId); 107 | 108 | await message?.edit(output); 109 | }; 110 | 111 | export const manageRepRole = async ( 112 | member: GuildMember, 113 | interation: CommandInteraction 114 | ) => { 115 | const role = await interation.guild?.roles.fetch('1136925691301072937'); 116 | if (!role) return; 117 | 118 | const rep = await prisma.reputation.findUnique({ 119 | where: { 120 | userId: member.id, 121 | }, 122 | }); 123 | if (!rep) return; 124 | 125 | if (rep.count >= 5) { 126 | await member.roles.add('1136925691301072937'); 127 | } 128 | }; 129 | -------------------------------------------------------------------------------- /src/features/triggerSystem.ts: -------------------------------------------------------------------------------- 1 | import { Message } from 'discord.js'; 2 | import { triggerPatternCache } from '../utils/Cache'; 3 | const matchPattern = (content: string) => { 4 | for (const [, entry] of triggerPatternCache) { 5 | for (const str of entry.stringMatch) { 6 | if (content.includes(str)) { 7 | return entry.replyMessageContent; 8 | } 9 | } 10 | 11 | for (const regex of entry.regexMatch) { 12 | if (regex.test(content)) { 13 | return entry.replyMessageContent; 14 | } 15 | } 16 | } 17 | return undefined; 18 | }; 19 | 20 | export const triggerSystem = async (message: Message) => { 21 | const replyMessageContent = matchPattern(message.content); 22 | if (!replyMessageContent) return; 23 | 24 | await message.reply({ 25 | content: replyMessageContent, 26 | }); 27 | }; 28 | -------------------------------------------------------------------------------- /src/handlers/handleAutocomplete.ts: -------------------------------------------------------------------------------- 1 | import { AutocompleteInteraction } from 'discord.js'; 2 | import { client } from '..'; 3 | import { logger } from 'console-wizard'; 4 | 5 | export const handleAutocomplete = async ( 6 | interaction: AutocompleteInteraction 7 | ) => { 8 | const command = client.commands.get(interaction.commandName); 9 | 10 | if (!command) { 11 | logger.error( 12 | `No command matching ${interaction.commandName} was found.` 13 | ); 14 | return; 15 | } 16 | 17 | if (!command.autocomplete) { 18 | logger.error( 19 | `No autocomplete function found for ${interaction.commandName}!` 20 | ); 21 | return; 22 | } 23 | 24 | try { 25 | await command.autocomplete(interaction); 26 | } catch (error) { 27 | console.error(error); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /src/handlers/handleButtons.ts: -------------------------------------------------------------------------------- 1 | import { ButtonInteraction } from 'discord.js'; 2 | import { client } from '..'; 3 | import { ButtonRunParams } from '../types'; 4 | 5 | export const handleButtons = async (interaction: ButtonInteraction) => { 6 | const { customId } = interaction; 7 | const idParts = customId.split('--'); 8 | const scope = idParts[0]; 9 | const id = idParts[1]; 10 | 11 | const button = client.buttons.get(scope); 12 | 13 | const args: ButtonRunParams = { 14 | interaction, 15 | id, 16 | scope, 17 | }; 18 | 19 | await button?.run(args); 20 | }; 21 | -------------------------------------------------------------------------------- /src/handlers/handleContentMenus.ts: -------------------------------------------------------------------------------- 1 | import { ContextMenuCommandInteraction } from 'discord.js'; 2 | import { client } from '..'; 3 | import { 4 | MessageContextMenuRunParams, 5 | UserContextMenuRunParams, 6 | ModifiedUserContextMenuCommandInteraction, 7 | ModifiedMessageContextMenuCommandInteraction, 8 | } from '../types'; 9 | 10 | export const handleContextMenus = async ( 11 | interaction: ContextMenuCommandInteraction 12 | ) => { 13 | if (interaction.isUserContextMenuCommand()) { 14 | const command = client.userContextMenus.get(interaction.commandName); 15 | 16 | if (!command) return; 17 | 18 | const args: UserContextMenuRunParams = { 19 | client, 20 | interaction: 21 | interaction as ModifiedUserContextMenuCommandInteraction, 22 | }; 23 | 24 | await command.run(args); 25 | } 26 | 27 | if (interaction.isMessageContextMenuCommand()) { 28 | const command = client.messageContextMenus.get(interaction.commandName); 29 | 30 | if (!command) return; 31 | 32 | const args: MessageContextMenuRunParams = { 33 | client, 34 | interaction: 35 | interaction as ModifiedMessageContextMenuCommandInteraction, 36 | }; 37 | 38 | await command.run(args); 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /src/handlers/handleEvents.ts: -------------------------------------------------------------------------------- 1 | import { ClientEvents } from 'discord.js'; 2 | import glob from 'glob'; 3 | import { promisify } from 'util'; 4 | import { client } from '..'; 5 | import { Event } from '../lib'; 6 | 7 | const globPromise = promisify(glob); 8 | 9 | export const handleEvents = async () => { 10 | const events = await globPromise(`${__dirname}/../events/*{.ts,.js}`); 11 | events.forEach(async eventFilePath => { 12 | const eventObj: Event = await ( 13 | await import(eventFilePath) 14 | )?.default; 15 | 16 | client.on(eventObj.event, eventObj.run); 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /src/handlers/handleSlashCommands.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CacheType, 3 | CommandInteractionOptionResolver, 4 | EmbedBuilder, 5 | } from 'discord.js'; 6 | import { client } from '..'; 7 | import { 8 | CommandRunParams, 9 | ModifiedChatInputCommandInteraction, 10 | } from '../types'; 11 | 12 | export const handleSlashCommands = async ( 13 | interaction: ModifiedChatInputCommandInteraction 14 | ) => { 15 | const command = client.commands.get(interaction.commandName); 16 | 17 | if ( 18 | command?.devOnly && 19 | !interaction.member.roles.cache.has(client.config.developerRoleId) 20 | ) { 21 | interaction.reply({ 22 | embeds: [ 23 | new EmbedBuilder() 24 | .setTitle('Oops! Something went wrong') 25 | .setColor(client.config.colors.red) 26 | .setDescription( 27 | 'Turns out this is a **developer only command**, and you do not seem to be my developer!' 28 | ), 29 | ], 30 | ephemeral: true, 31 | }); 32 | return; 33 | } 34 | 35 | if ( 36 | command?.ownerOnly && 37 | interaction.guild?.ownerId !== interaction.member.id 38 | ) { 39 | interaction.reply({ 40 | embeds: [ 41 | new EmbedBuilder() 42 | .setTitle('Oops! Something went wrong') 43 | .setColor(client.config.colors.red) 44 | .setDescription( 45 | 'Apparently you need to be the owner of the server to run this command! *very prestigious, I know*' 46 | ), 47 | ], 48 | ephemeral: true, 49 | }); 50 | return; 51 | } 52 | const args: CommandRunParams = { 53 | client, 54 | interaction, 55 | options: 56 | interaction.options as CommandInteractionOptionResolver, 57 | }; 58 | 59 | await command?.run(args); 60 | }; 61 | -------------------------------------------------------------------------------- /src/handlers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './handleAutocomplete'; 2 | export * from './handleButtons'; 3 | export * from './handleContentMenus'; 4 | export * from './handleEvents'; 5 | export * from './handleSlashCommands'; 6 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | import { Cheeka } from './lib'; 3 | import { logger, setWizardConfig } from 'console-wizard'; 4 | import { config } from './config'; 5 | import { PrismaClient } from '@prisma/client'; 6 | 7 | setWizardConfig({ 8 | includeStatus: true, 9 | includeTimestamp: true, 10 | }); 11 | 12 | export const client: Cheeka = new Cheeka(); 13 | 14 | export const prisma = new PrismaClient({ 15 | log: 16 | config.environment === 'dev' 17 | ? ['query', 'info', 'warn', 'error'] 18 | : ['warn', 'error'], 19 | }); 20 | 21 | client.deploy(); 22 | 23 | process.on('uncaughtException', error => { 24 | logger.error(error.name); 25 | console.log( 26 | `${error.message}\nCause: ${error.cause}\nStack: ${error.stack}` 27 | ); 28 | }); 29 | process.on('unhandledRejection', (error, promise) => { 30 | logger.error(`Unhandled Promise: ${promise}`); 31 | console.log(error); 32 | }); 33 | -------------------------------------------------------------------------------- /src/interactions/buttons/tagAccept.ts: -------------------------------------------------------------------------------- 1 | import { AttachmentBuilder, ChannelType, EmbedBuilder } from 'discord.js'; 2 | import { client, prisma } from '../..'; 3 | import { Button } from '../../lib/classes/Button'; 4 | import { codeblockRegex, idData, sourcebinLanguageData } from '../../data'; 5 | import { Canvas, createCanvas, loadImage } from 'canvas'; 6 | 7 | export default new Button({ 8 | scope: 'tagAccept', 9 | 10 | run: async ({ interaction, id }) => { 11 | const tag = await prisma.tag.findUnique({ 12 | where: { 13 | id, 14 | }, 15 | }); 16 | 17 | if (!tag) { 18 | interaction.reply({ 19 | content: "This tag doesn't exist!", 20 | ephemeral: true, 21 | }); 22 | return; 23 | } 24 | 25 | await prisma.tag.update({ 26 | where: { 27 | id, 28 | }, 29 | data: { 30 | accepted: true, 31 | }, 32 | }); 33 | 34 | await interaction.reply({ 35 | embeds: [ 36 | new EmbedBuilder() 37 | .setTitle('Tag accepted!') 38 | .setDescription( 39 | `Tag \`${tag?.name}\` has been accepted by ${interaction.user}` 40 | ) 41 | .setColor(client.config.colors.green), 42 | ], 43 | }); 44 | 45 | const languageMatch = tag.content.match(codeblockRegex); 46 | const languageMatchStr = languageMatch?.at(2); 47 | if (!languageMatchStr) return; 48 | 49 | const content = languageMatch?.at(3); 50 | const languageId = getLanguageId(languageMatchStr) ?? 222; 51 | 52 | const formData = JSON.stringify({ 53 | files: [ 54 | { 55 | name: tag.name, 56 | content, 57 | languageId, 58 | }, 59 | ], 60 | }); 61 | 62 | const headers = { 63 | 'Content-Type': 'application/json', 64 | }; 65 | const res = await fetch('https://sourceb.in/api/bins', { 66 | method: 'POST', 67 | body: formData, 68 | headers, 69 | }); 70 | const tagOwner = interaction.guild?.members.cache.get(tag.ownerId); 71 | const contentUrl = await res.json(); 72 | const image = await generateCodeImage( 73 | tag.name, 74 | tagOwner?.user.username ?? '' 75 | ); 76 | 77 | const attachment = new AttachmentBuilder(image, { 78 | name: 'membercode-banner.png', 79 | }); 80 | 81 | const channel = interaction.guild?.channels.cache.get( 82 | idData.channels.memberCodesChannel 83 | ); 84 | if (channel?.type !== ChannelType.GuildText) return; 85 | console.log('checkiepointy'); 86 | await channel.send({ 87 | content: `## [${tag.name}](https://srcb.in/${contentUrl.key})\nBy ${tagOwner}`, 88 | files: [attachment], 89 | }); 90 | 91 | const guild = client.guilds.cache.get(client.config.guildId); 92 | const owner = guild?.members.cache.get(tag?.ownerId ?? ''); 93 | 94 | if (!owner) { 95 | await interaction.reply( 96 | `WARNING: Tag owner of ${tag?.name} not found!` 97 | ); 98 | return; 99 | } 100 | 101 | try { 102 | await owner.send({ 103 | embeds: [ 104 | new EmbedBuilder() 105 | .setTitle('Your Tag was Accepted!') 106 | .setDescription( 107 | `Your request to create tag \`${tag?.name}\` was accepted! It is now available on the server` 108 | ) 109 | .setAuthor({ 110 | name: `${guild?.name}`, 111 | iconURL: `${guild?.iconURL()}`, 112 | }) 113 | .setColor(client.config.colors.green), 114 | ], 115 | }); 116 | } catch { 117 | return; 118 | } 119 | }, 120 | }); 121 | 122 | const getLanguageId = (matchStr: string) => { 123 | for (const key in sourcebinLanguageData) { 124 | const languageData = 125 | sourcebinLanguageData[key as keyof typeof sourcebinLanguageData]; 126 | const matchStrArray: string[] = [languageData.name]; 127 | if ('extension' in languageData) 128 | matchStrArray.push(languageData.extension); 129 | 130 | if (matchStrArray.includes(matchStr)) { 131 | return key; 132 | } 133 | } 134 | return; 135 | }; 136 | const applyTextTitle = (canvas: Canvas, text: string) => { 137 | const context = canvas.getContext('2d'); 138 | 139 | // Declare a base size of the font 140 | let fontSize = 35; 141 | 142 | do { 143 | // Assign the font to the context and decrement it so it can be measured again 144 | context.font = `bold ${(fontSize -= 2)}px sans-serif`; 145 | // Compare pixel width of the text to the canvas minus the approximate avatar size 146 | } while (context.measureText(text).width > canvas.width - 100); 147 | 148 | // Return the result to use in the actual canvas 149 | return context.font; 150 | }; 151 | const applyTextAuthor = (canvas: Canvas, text: string) => { 152 | const context = canvas.getContext('2d'); 153 | 154 | // Declare a base size of the font 155 | let fontSize = 20; 156 | 157 | do { 158 | // Assign the font to the context and decrement it so it can be measured again 159 | context.font = `${(fontSize -= 2)}px sans-serif`; 160 | // Compare pixel width of the text to the canvas minus the approximate avatar size 161 | } while (context.measureText(text).width > canvas.width - 300); 162 | 163 | // Return the result to use in the actual canvas 164 | return context.font; 165 | }; 166 | // const applyText = ( 167 | // canvas: Canvas, 168 | // text: string, 169 | // fontSize: number, 170 | // styleText: string 171 | // ) => { 172 | // const context = canvas.getContext('2d'); 173 | // 174 | // do { 175 | // context.font = styleText.replace('#', `${fontSize - 1}px`); 176 | // } while (context.measureText(text).width > canvas.width); 177 | // 178 | // return context.font; 179 | // }; 180 | 181 | export const generateCodeImage = async (codeName: string, author: string) => { 182 | const canvas = createCanvas(600, 316); 183 | const ctx = canvas.getContext('2d'); 184 | 185 | const image = await loadImage( 186 | `${__dirname}/../../../assets/membercode-bg-small.png` 187 | ); 188 | 189 | ctx.drawImage(image, 0, 0); 190 | 191 | ctx.font = applyTextTitle(canvas, codeName); 192 | // ctx.font = 'bold 30px Sans serif'; 193 | ctx.fillStyle = '#d75c5c'; 194 | ctx.textAlign = 'center'; 195 | ctx.textBaseline = 'middle'; 196 | 197 | const titleX = canvas.width / 2; 198 | const titleY = canvas.height / 2; 199 | 200 | ctx.fillText(codeName, titleX, titleY); 201 | 202 | // Author 203 | const titleTextWidth = ctx.measureText(codeName).width; 204 | const titleTextHeight = ctx.measureText(codeName).actualBoundingBoxAscent; 205 | 206 | const authorText = `By ${author}`; 207 | ctx.font = applyTextAuthor(canvas, authorText); 208 | // ctx.font = applyText(canvas, authorText, 20, '# Sans serif'); 209 | // ctx.font = '20px Sans serif'; 210 | ctx.fillStyle = '#494949'; 211 | ctx.textAlign = 'right'; 212 | ctx.textBaseline = 'top'; 213 | 214 | const spacing = 15; 215 | 216 | const authorX = titleX + titleTextWidth / 2; 217 | const authorY = titleY + titleTextHeight + spacing; 218 | 219 | ctx.fillText(authorText, authorX, authorY); 220 | 221 | // const authorX = Math.min(titleX + titleTextWidth, canvas.width - spacing); 222 | return canvas.toBuffer(); 223 | }; 224 | -------------------------------------------------------------------------------- /src/interactions/buttons/tagDecline.ts: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder } from 'discord.js'; 2 | import { client, prisma } from '../..'; 3 | import { Button } from '../../lib/classes/Button'; 4 | 5 | export default new Button({ 6 | scope: 'tagDecline', 7 | run: async ({ interaction, id }) => { 8 | const tag = await prisma.tag.findUnique({ 9 | where: { 10 | id: id, 11 | }, 12 | }); 13 | if (!tag) { 14 | interaction.reply({ 15 | content: "This tag doesn't exist!", 16 | ephemeral: true, 17 | }); 18 | return; 19 | } 20 | 21 | await prisma.tag.delete({ 22 | where: { 23 | id: id, 24 | }, 25 | }); 26 | 27 | interaction.reply({ 28 | embeds: [ 29 | new EmbedBuilder() 30 | .setTitle('Tag declined!') 31 | .setDescription( 32 | `Tag \`${tag?.name}\` has been declined by ${interaction.user}` 33 | ) 34 | .setColor(client.config.colors.red), 35 | ], 36 | }); 37 | 38 | const guild = client.guilds.cache.get(client.config.guildId); 39 | const owner = guild?.members.cache.get(tag?.ownerId || ''); 40 | 41 | if (!owner) { 42 | interaction.reply(`WARNING: Tag owner of ${tag?.name} not found!`); 43 | return; 44 | } 45 | 46 | owner.send({ 47 | embeds: [ 48 | new EmbedBuilder() 49 | .setTitle('Your Tag was Declined!') 50 | .setDescription( 51 | `Unfortunately, your request to create tag \`${tag?.name}\` was declined.` 52 | ) 53 | .setAuthor({ 54 | name: `${guild?.name}`, 55 | iconURL: `${guild?.iconURL()}`, 56 | }) 57 | .setColor(client.config.colors.green), 58 | ], 59 | }); 60 | }, 61 | }); 62 | -------------------------------------------------------------------------------- /src/interactions/buttons/tagModifyAccept.ts: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder } from 'discord.js'; 2 | import { client, prisma } from '../..'; 3 | import { Button } from '../../lib/classes/Button'; 4 | import { modifyTag } from '../../lib/functions/modifyTag'; 5 | 6 | export default new Button({ 7 | scope: 'tagModifyAccept', 8 | run: async ({ interaction, id }) => { 9 | const tag = await prisma.tag.findUnique({ 10 | where: { 11 | id, 12 | }, 13 | }); 14 | 15 | if (!tag) { 16 | interaction.reply({ 17 | content: "This tag doesn't exist!", 18 | ephemeral: true, 19 | }); 20 | return; 21 | } 22 | if (!tag.newContent) { 23 | interaction.reply({ 24 | content: "This tag doesn't have a new content!", 25 | }); 26 | return; 27 | } 28 | 29 | await modifyTag(tag?.name, tag?.newContent); 30 | 31 | interaction.reply({ 32 | embeds: [ 33 | new EmbedBuilder() 34 | .setTitle('Tag Update Request Accepted!') 35 | .setDescription( 36 | `Tag \`${tag?.name}\` update request has been accepted by ${interaction.user}` 37 | ) 38 | .setColor(client.config.colors.green), 39 | ], 40 | }); 41 | const guild = client.guilds.cache.get(client.config.guildId); 42 | const owner = guild?.members.cache.get(tag?.ownerId || ''); 43 | 44 | if (!owner) { 45 | interaction.reply(`WARNING: Tag owner of ${tag?.name} not found!`); 46 | return; 47 | } 48 | 49 | owner.send({ 50 | embeds: [ 51 | new EmbedBuilder() 52 | .setTitle('Your Tag Update Request was Accepted!') 53 | .setDescription( 54 | `Your request to update tag \`${tag?.name}\` was accepted!` 55 | ) 56 | .setAuthor({ 57 | name: `${guild?.name}`, 58 | iconURL: `${guild?.iconURL()}`, 59 | }) 60 | .setColor(client.config.colors.green), 61 | ], 62 | }); 63 | }, 64 | }); 65 | -------------------------------------------------------------------------------- /src/interactions/buttons/tagModifyDecline.ts: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder } from 'discord.js'; 2 | import { client, prisma } from '../..'; 3 | import { Button } from '../../lib/classes/Button'; 4 | 5 | export default new Button({ 6 | scope: 'tagModifyDecline', 7 | 8 | run: async ({ interaction, id }) => { 9 | const tag = await prisma.tag.findUnique({ 10 | where: { 11 | id, 12 | }, 13 | }); 14 | 15 | if (!tag) { 16 | interaction.reply({ 17 | content: "This tag doesn't exist!", 18 | ephemeral: true, 19 | }); 20 | return; 21 | } 22 | 23 | await prisma.tag.update({ 24 | where: { 25 | id, 26 | }, 27 | data: { 28 | newContent: null, 29 | }, 30 | }); 31 | 32 | interaction.reply({ 33 | embeds: [ 34 | new EmbedBuilder() 35 | .setTitle('Tag Update Request Declined!') 36 | .setDescription( 37 | `Tag \`${tag?.name}\` has been declined for updating by ${interaction.user}` 38 | ) 39 | .setColor(client.config.colors.red), 40 | ], 41 | }); 42 | const guild = client.guilds.cache.get(client.config.guildId); 43 | const owner = guild?.members.cache.get(tag?.ownerId || ''); 44 | 45 | if (!owner) { 46 | interaction.reply(`WARNING: Tag owner of ${tag?.name} not found!`); 47 | return; 48 | } 49 | 50 | owner.send({ 51 | embeds: [ 52 | new EmbedBuilder() 53 | .setTitle('Your Tag Update Request was Declined!') 54 | .setDescription( 55 | `Unfortunately, your request to update tag \`${tag?.name}\` was declined.` 56 | ) 57 | .setAuthor({ 58 | name: `${guild?.name}`, 59 | iconURL: `${guild?.iconURL()}`, 60 | }) 61 | .setColor(client.config.colors.red), 62 | ], 63 | }); 64 | }, 65 | }); 66 | -------------------------------------------------------------------------------- /src/interactions/contextMenus/user/ban.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImagineGamingPlay/Cheeka-Development/88d92125b53de7c5974ad53b8defbc397b440bfe/src/interactions/contextMenus/user/ban.ts -------------------------------------------------------------------------------- /src/interactions/contextMenus/user/kick.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImagineGamingPlay/Cheeka-Development/88d92125b53de7c5974ad53b8defbc397b440bfe/src/interactions/contextMenus/user/kick.ts -------------------------------------------------------------------------------- /src/interactions/contextMenus/user/softban.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImagineGamingPlay/Cheeka-Development/88d92125b53de7c5974ad53b8defbc397b440bfe/src/interactions/contextMenus/user/softban.ts -------------------------------------------------------------------------------- /src/lib/classes/ApplicationCommand.ts: -------------------------------------------------------------------------------- 1 | import { CommandData } from '../../types'; 2 | 3 | export class Command { 4 | constructor(commandOptions: CommandData) { 5 | Object.assign(this, commandOptions); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/lib/classes/Button.ts: -------------------------------------------------------------------------------- 1 | import { ButtonOptions } from '../../types/InteractionTypes'; 2 | 3 | export class Button { 4 | constructor(buttonOptions: ButtonOptions) { 5 | Object.assign(this, buttonOptions); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/lib/classes/Cheeka.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ActivitiesOptions, 3 | ActivityType, 4 | Client, 5 | ClientOptions, 6 | Collection, 7 | GatewayIntentBits, 8 | PresenceUpdateStatus, 9 | } from 'discord.js'; 10 | import { handleEvents } from '../../handlers'; 11 | import { config } from '../../config'; 12 | import { ChatInputCommandData } from '../../types'; 13 | import { logger } from 'console-wizard'; 14 | import { ConfigType } from './../../types/configType'; 15 | import { PrismaClient } from '@prisma/client'; 16 | import { 17 | ButtonOptions, 18 | MessageContextMenuData, 19 | UserContextMenuData, 20 | } from '../../types/InteractionTypes'; 21 | import { registerApplicationCommands } from '../functions/registerApplicatonCommands'; 22 | import { registerButtons } from '../functions/registerButtons'; 23 | import { prisma } from '../..'; 24 | 25 | const { Guilds, GuildMessages, DirectMessages, GuildMembers, MessageContent } = 26 | GatewayIntentBits; 27 | 28 | const clientOptions: ClientOptions = { 29 | intents: [ 30 | Guilds, 31 | GuildMessages, 32 | DirectMessages, 33 | GuildMembers, 34 | MessageContent, 35 | ], 36 | allowedMentions: { 37 | repliedUser: true, 38 | }, 39 | }; 40 | 41 | const setActivityStatus = (client: Cheeka) => { 42 | const activities: ActivitiesOptions[] = [ 43 | { 44 | name: 'Better than Sans the Skeleton', 45 | type: ActivityType.Playing, 46 | }, 47 | { 48 | name: 'main-chat, the help channel', 49 | type: ActivityType.Listening, 50 | }, 51 | { 52 | name: "keita's media stash", 53 | type: ActivityType.Watching, 54 | }, 55 | ]; 56 | const { floor, random } = Math; 57 | 58 | setInterval(() => { 59 | const randomActivityIndex = floor(random() * activities.length); 60 | 61 | client.user?.setPresence({ 62 | activities: [activities[randomActivityIndex]], 63 | status: PresenceUpdateStatus.Online, 64 | }); 65 | }, 2 * 60 * 60 * 1000 /* 2 hours */); 66 | }; 67 | 68 | export class Cheeka extends Client { 69 | config: ConfigType; 70 | commands: Collection; 71 | buttons: Collection; 72 | userContextMenus: Collection; 73 | messageContextMenus: Collection; 74 | prisma: PrismaClient; 75 | 76 | constructor() { 77 | super(clientOptions); 78 | 79 | this.config = config; 80 | 81 | this.commands = new Collection(); 82 | this.buttons = new Collection(); 83 | this.userContextMenus = new Collection(); 84 | this.messageContextMenus = new Collection(); 85 | 86 | this.prisma = prisma; 87 | } 88 | 89 | async deploy() { 90 | await handleEvents(); 91 | await this.login(config.token); 92 | 93 | await registerApplicationCommands(); 94 | await registerButtons(); 95 | await prisma 96 | .$connect() 97 | .then(() => logger.success('Database Connected!')); 98 | setActivityStatus(this); 99 | logger.success('Client Deployed!'); 100 | logger.info(`Environment: ${this.config.environment}`); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/lib/classes/Event.ts: -------------------------------------------------------------------------------- 1 | import { ClientEvents } from 'discord.js'; 2 | 3 | export class Event { 4 | constructor( 5 | public event: K, 6 | public run: (...args: ClientEvents[K]) => void 7 | ) {} 8 | } 9 | -------------------------------------------------------------------------------- /src/lib/functions/cacheData.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from '../..'; 2 | import { triggerPatternCache } from '../../utils/Cache'; 3 | 4 | export const cacheTriggerPatterns = async () => { 5 | const triggers = await prisma.trigger.findMany(); 6 | triggers.forEach(trigger => { 7 | const regexMatch = trigger.regexMatch.map(regex => { 8 | const pattern = regex.slice(1, regex.lastIndexOf('/')); 9 | const flags = regex.slice(regex.lastIndexOf('/') + 1); 10 | return new RegExp(pattern, flags); 11 | }); 12 | triggerPatternCache.set(trigger.type, { 13 | stringMatch: trigger.stringMatch, 14 | replyMessageContent: trigger.replyMessageContent, 15 | regexMatch, 16 | }); 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /src/lib/functions/createTag.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from '../..'; 2 | import { TagProps } from '../../types'; 3 | 4 | export const createTag = async ({ 5 | name, 6 | content, 7 | type, 8 | ownerId, 9 | }: Omit) => { 10 | await prisma.tag.create({ 11 | data: { 12 | name, 13 | content, 14 | type, 15 | ownerId, 16 | // owner: { 17 | // connectOrCreate: { 18 | // where: { 19 | // userId: ownerId, 20 | // }, 21 | // create: { 22 | // userId: ownerId, 23 | // }, 24 | // }, 25 | // }, 26 | }, 27 | }); 28 | }; 29 | -------------------------------------------------------------------------------- /src/lib/functions/deleteTag.ts: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder } from 'discord.js'; 2 | import { client, prisma } from '../..'; 3 | import { ModifiedChatInputCommandInteraction } from '../../types'; 4 | 5 | export const deleteTag = async ( 6 | name: string, 7 | interaction: ModifiedChatInputCommandInteraction 8 | ) => { 9 | const isTagOwner = await prisma.tag.findFirst({ 10 | where: { 11 | name, 12 | ownerId: interaction.user.id, 13 | }, 14 | }); 15 | 16 | const isStaff = interaction.member.roles.cache.has( 17 | client.config.staffRoleId 18 | ); 19 | if (!isTagOwner && !isStaff) { 20 | interaction.reply({ 21 | embeds: [ 22 | new EmbedBuilder() 23 | .setTitle('Failed to delete tag!') 24 | .setDescription( 25 | 'You are not able to delete this tag as you are neither the owner of the tag nor have sufficient permissions to forcefully delete it!' 26 | ) 27 | .setColor(client.config.colors.red), 28 | ], 29 | }); 30 | return; 31 | } 32 | 33 | const tag = await prisma.tag.findUnique({ 34 | where: { 35 | name, 36 | }, 37 | }); 38 | if (!tag) { 39 | interaction.reply({ 40 | embeds: [ 41 | new EmbedBuilder() 42 | .setTitle('Tag not found!') 43 | .setDescription("The tag wasn't found in the database!") 44 | .setColor(client.config.colors.red), 45 | ], 46 | }); 47 | return; 48 | } 49 | 50 | await prisma.tag.delete({ 51 | where: { 52 | name, 53 | }, 54 | }); 55 | 56 | interaction.reply({ 57 | embeds: [ 58 | new EmbedBuilder() 59 | .setTitle('Tag deleted!') 60 | .setDescription(`Tag ${name} has been deleted!`) 61 | .setColor(client.config.colors.red), 62 | ], 63 | }); 64 | }; 65 | -------------------------------------------------------------------------------- /src/lib/functions/getTagNames.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from '../..'; 2 | import { TagType } from '.prisma/client'; 3 | 4 | export const getTagNames = async (type: TagType) => { 5 | const tags = await prisma.tag.findMany({ 6 | where: { 7 | type, 8 | }, 9 | }); 10 | 11 | return tags.map(tag => tag.name); 12 | }; 13 | -------------------------------------------------------------------------------- /src/lib/functions/getTopReps.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from '../..'; 2 | 3 | export const getTopReps = async (index: number) => { 4 | const reputations = await prisma.reputation.findMany(); 5 | const topReps = reputations.sort((a, b) => b.count - a.count); 6 | return topReps.slice(0, index); 7 | }; 8 | -------------------------------------------------------------------------------- /src/lib/functions/modifyTag.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from '../..'; 2 | 3 | export const modifyTag = async (name: string, newContent: string) => { 4 | await prisma.tag.update({ 5 | where: { 6 | name, 7 | }, 8 | data: { 9 | content: newContent, 10 | newContent: null, 11 | }, 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /src/lib/functions/registerApplicatonCommands.ts: -------------------------------------------------------------------------------- 1 | import { client } from '../..'; 2 | import { CommandData, LoadedApplicationCommands } from '../../types'; 3 | import { logger } from 'console-wizard'; 4 | import { getFiles } from '../../utils'; 5 | import { ApplicationCommandType } from 'discord.js'; 6 | 7 | export const registerApplicationCommands = async () => { 8 | const commands: CommandData[] = []; 9 | const commandFiles = getFiles(`${__dirname}/../../commands`, true); 10 | 11 | const loadedCommandNames: LoadedApplicationCommands[] = []; 12 | 13 | for (const file of commandFiles) { 14 | const commandClass: CommandData = await (await import(file)).default; 15 | const { ...command } = commandClass; 16 | 17 | if (!command.name) { 18 | logger.error('One of the command is lacking name!'); 19 | return; 20 | } 21 | 22 | if (!command.type) return; 23 | 24 | if (command.type === ApplicationCommandType.ChatInput) { 25 | client.commands.set(command.name, command); 26 | } 27 | if (command.type === ApplicationCommandType.User) { 28 | client.userContextMenus.set(command.name, command); 29 | } 30 | if (command.type === ApplicationCommandType.Message) { 31 | client.messageContextMenus.set(command.name, command); 32 | } 33 | 34 | commands.push(command); 35 | loadedCommandNames.push({ LoadedCommands: command.name }); 36 | } 37 | if (client.config.environment === 'dev') { 38 | const devGuild = client.guilds.cache.get(client.config.devGuildId); 39 | // client.application?.commands.set([], devGuild?.id || ''); 40 | await devGuild?.commands.set(commands); 41 | 42 | console.table(loadedCommandNames); 43 | logger.info('Command Type: Guild'); 44 | return; 45 | } 46 | if (client.config.environment === 'prod') { 47 | await client.application?.commands.set(commands); 48 | 49 | console.table(loadedCommandNames); 50 | logger.info( 51 | 'Command Type: Global [Discord takes upto an hour to update the commands]' 52 | ); 53 | return; 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /src/lib/functions/registerButtons.ts: -------------------------------------------------------------------------------- 1 | import { logger } from 'console-wizard'; 2 | import { ButtonOptions } from '../../types/InteractionTypes'; 3 | import { getFiles } from '../../utils'; 4 | import { client } from '../..'; 5 | 6 | export const registerButtons = async () => { 7 | const buttons: ButtonOptions[] = []; 8 | const buttonFiles = getFiles( 9 | `${__dirname}/../../interactions/buttons`, 10 | false 11 | ); 12 | 13 | for (const file of buttonFiles) { 14 | const { ...button }: ButtonOptions = await (await import(file)).default; 15 | 16 | if (!button.scope) { 17 | logger.error('A button is missing scope!'); 18 | return; 19 | } 20 | buttons.push(button); 21 | client.buttons.set(button.scope, button); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /src/lib/functions/tagCreateRequest.ts: -------------------------------------------------------------------------------- 1 | import { logger } from 'console-wizard'; 2 | import { 3 | ActionRowBuilder, 4 | ButtonBuilder, 5 | ButtonStyle, 6 | EmbedBuilder, 7 | } from 'discord.js'; 8 | import { client, prisma } from '../..'; 9 | import { codeblockRegex, idData } from '../../data'; 10 | import { TagProps } from '../../types'; 11 | 12 | const { channels } = idData; 13 | 14 | export const tagCreateRequest = async ({ 15 | name, 16 | type, 17 | interaction, 18 | }: Omit) => { 19 | const { 20 | config: { colors }, 21 | } = client; 22 | 23 | const alreadyExists = await prisma.tag.findUnique({ 24 | where: { 25 | name, 26 | }, 27 | }); 28 | if (alreadyExists) { 29 | interaction.reply({ 30 | content: 'That tag already exists! Please choose a unique name', 31 | ephemeral: true, 32 | }); 33 | return; 34 | } 35 | 36 | const lowerCasedType = type.toLowerCase(); 37 | await interaction.reply({ 38 | embeds: [ 39 | new EmbedBuilder() 40 | .setTitle(`Add a ${lowerCasedType} Tag!`) 41 | .setDescription( 42 | 'Send the tag content as the next message in this channel to set the content of the code snippet!\n\n*Type `cancel` to cancel.*' 43 | ) 44 | .setColor(colors.blurple) 45 | .setFooter({ 46 | text: 'You have 15 seconds to send your message', 47 | }), 48 | ], 49 | }); 50 | let shouldExit = false; 51 | const userMessageCollection = await interaction.channel 52 | ?.awaitMessages({ 53 | max: 1, 54 | time: 15 * 1000, 55 | errors: ['time'], 56 | filter: m => m.author.id === interaction.user.id, 57 | }) 58 | .catch(async () => { 59 | if (shouldExit) return; 60 | await interaction.editReply({ 61 | embeds: [ 62 | new EmbedBuilder() 63 | .setTitle('Oops, You ran out of time!') 64 | .setDescription( 65 | "Your tag creation process has been canceled as you did not provide the tag's content in time. Run the command again to restart the process." 66 | ) 67 | .setColor(colors.red), 68 | ], 69 | }); 70 | shouldExit = true; 71 | return; 72 | }); 73 | if (shouldExit) return; 74 | const userMessage = userMessageCollection?.first(); 75 | 76 | const channel = interaction.channel; 77 | if (!channel?.isTextBased()) return; 78 | 79 | const ownerId = interaction.user.id; 80 | if (!name) { 81 | await interaction.followUp({ 82 | content: 'Please provide a name for the tag!', 83 | ephemeral: true, 84 | }); 85 | return; 86 | } 87 | const content = userMessage?.content; 88 | if (!content) { 89 | await interaction.followUp({ 90 | content: 91 | 'Please send actual message for the tag. Attachments are not supported!', 92 | ephemeral: true, 93 | }); 94 | return; 95 | } 96 | if (content.toLowerCase() === 'cancel') return; 97 | 98 | if (content.length > 2000) { 99 | await interaction.followUp({ 100 | embeds: [ 101 | new EmbedBuilder() 102 | .setTitle('Failed to create tag!') 103 | .setDescription( 104 | 'The content cannot have more than 2000 characters!' 105 | ) 106 | .setColor(client.config.colors.red), 107 | ], 108 | ephemeral: true, 109 | }); 110 | return; 111 | } 112 | if (type === 'CODE' && !content.match(codeblockRegex)) { 113 | await interaction.followUp({ 114 | embeds: [ 115 | new EmbedBuilder() 116 | .setTitle('Failed to create tag!') 117 | .setDescription( 118 | 'Your code tag does not contain a codeblock! Create one by:\n\\`\\`\\`language-name-here\ncode-here\n\\`\\`\\`' 119 | ) 120 | .setColor(client.config.colors.red), 121 | ], 122 | ephemeral: true, 123 | }); 124 | return; 125 | } 126 | const tag = await prisma.tag.create({ 127 | data: { 128 | name, 129 | content, 130 | accepted: false, 131 | type, 132 | ownerId, 133 | }, 134 | }); 135 | const tagVerificationChannel = client.channels.cache.get( 136 | channels.tagVerificationChannelId 137 | ); 138 | if (!tagVerificationChannel?.isTextBased()) { 139 | logger.error("'tagVerificationChannel' is not a text channel"); 140 | return; 141 | } 142 | 143 | const formData = JSON.stringify({ 144 | files: [ 145 | { 146 | content, 147 | name, 148 | languageId: 222, 149 | }, 150 | ], 151 | }); 152 | const headers = { 153 | 'Content-Type': 'application/json', 154 | }; 155 | const res = await fetch('https://sourceb.in/api/bins', { 156 | method: 'POST', 157 | body: formData, 158 | headers, 159 | }); 160 | const contentUrl = await res.json(); 161 | 162 | const embed = new EmbedBuilder().setTitle('New tag request').setFields([ 163 | { 164 | name: 'Name', 165 | value: `${name}`, 166 | inline: true, 167 | }, 168 | { 169 | name: 'Owner', 170 | value: `${client.users.cache.get(ownerId)}`, 171 | inline: true, 172 | }, 173 | { 174 | name: 'Type', 175 | value: `${type.toLowerCase()}`, 176 | inline: true, 177 | }, 178 | { 179 | name: 'Content', 180 | value: `[Click here](https://srcb.in/${contentUrl.key})`, 181 | inline: true, 182 | }, 183 | ]); 184 | 185 | const acceptButton = new ButtonBuilder() 186 | .setLabel('Accept') 187 | .setStyle(ButtonStyle.Success) 188 | .setCustomId(`tagAccept--${tag.id}`); 189 | 190 | const declineButton = new ButtonBuilder() 191 | .setLabel('Decline') 192 | .setStyle(ButtonStyle.Danger) 193 | .setCustomId(`tagDecline--${tag.id}`); 194 | const row = new ActionRowBuilder({ 195 | components: [acceptButton, declineButton], 196 | }); 197 | 198 | tagVerificationChannel.send({ 199 | embeds: [embed], 200 | components: [row], 201 | }); 202 | 203 | userMessage.reply({ 204 | embeds: [ 205 | new EmbedBuilder() 206 | .setTitle('Request Sent!') 207 | .setDescription( 208 | 'A request has has been sent to create your tag! We will notify you when your tag has been accepted or declined.' 209 | ) 210 | .setFooter({ 211 | text: 'Do NOT ping or DM the moderators to get your tag accepted', 212 | }) 213 | .setColor(client.config.colors.green), 214 | ], 215 | }); 216 | }; 217 | -------------------------------------------------------------------------------- /src/lib/functions/tagModifyRequest.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ActionRowBuilder, 3 | ButtonBuilder, 4 | ButtonStyle, 5 | EmbedBuilder, 6 | PermissionFlagsBits, 7 | } from 'discord.js'; 8 | import { client, prisma } from '../..'; 9 | import { TagProps } from '../../types'; 10 | import { idData } from '../../data'; 11 | import { logger } from 'console-wizard'; 12 | import { modifyTag } from './modifyTag'; 13 | 14 | const { channels } = idData; 15 | 16 | export const tagModifyRequest = async ({ 17 | name, 18 | interaction, 19 | type, 20 | }: Omit) => { 21 | const tag = await prisma.tag.findUnique({ 22 | where: { 23 | name, 24 | }, 25 | }); 26 | 27 | if (!tag) { 28 | interaction.reply({ 29 | content: 'Tag not found!', 30 | ephemeral: true, 31 | }); 32 | return; 33 | } 34 | 35 | const isAdmin = interaction.member.permissions.has( 36 | PermissionFlagsBits.Administrator 37 | ); 38 | 39 | const ownerId = tag.ownerId; 40 | const content = tag.content; 41 | 42 | if (!ownerId) { 43 | await interaction.reply({ 44 | content: 'Owner not found!', 45 | ephemeral: true, 46 | }); 47 | return; 48 | } 49 | 50 | const isTagOwner = await prisma.tag.findFirst({ 51 | where: { 52 | name, 53 | ownerId: interaction.user.id, 54 | }, 55 | }); 56 | if (!isTagOwner && !isAdmin) { 57 | interaction.reply({ 58 | embeds: [ 59 | new EmbedBuilder() 60 | .setTitle('Failed to modify tag!') 61 | .setDescription( 62 | 'You are not able to modify this tag as you are neither the owner of the tag nor have sufficient permissions!' 63 | ) 64 | .setColor(client.config.colors.red), 65 | ], 66 | }); 67 | return; 68 | } 69 | 70 | const lowerCasedType = type.toLowerCase(); 71 | await interaction.reply({ 72 | embeds: [ 73 | new EmbedBuilder() 74 | .setTitle(`Add a ${lowerCasedType} Tag!`) 75 | .setDescription( 76 | 'Send the new content as the next message in this channel to set the content of the code snippet!\n\n*Type `cancel` to cancel.*' 77 | ) 78 | .setColor(client.config.colors.blurple) 79 | .setFooter({ 80 | text: 'You have 15 seconds to send your message', 81 | }), 82 | ], 83 | }); 84 | 85 | const userMessage = ( 86 | await interaction.channel 87 | ?.awaitMessages({ 88 | max: 1, 89 | time: 15 * 1000, 90 | errors: ['time'], 91 | filter: m => m.author.id === interaction.user.id, 92 | }) 93 | .catch(() => { 94 | interaction.reply({ 95 | embeds: [ 96 | new EmbedBuilder() 97 | .setTitle('Oops, You ran out of time!') 98 | .setDescription( 99 | "Your tag creation process has been canceled as you did not provide the tag's content in time. Run the command again to restart the process." 100 | ) 101 | .setColor(client.config.colors.red), 102 | ], 103 | }); 104 | return; 105 | }) 106 | )?.first(); 107 | const channel = interaction.channel; 108 | if (!channel?.isTextBased()) return; 109 | 110 | if (!name) { 111 | interaction.reply({ 112 | content: 'Please provide a name for the tag!', 113 | ephemeral: true, 114 | }); 115 | return; 116 | } 117 | const newContent = userMessage?.content; 118 | if (!content) { 119 | interaction.reply({ 120 | content: 121 | 'Please send actual message for the tag. Attachments are not supported!', 122 | ephemeral: true, 123 | }); 124 | return; 125 | } 126 | 127 | if (!newContent) return; 128 | 129 | if (isAdmin) { 130 | modifyTag(name, newContent); 131 | interaction.reply( 132 | "Modified the tag! You're an admin, so you immediately modified it." 133 | ); 134 | return; 135 | } 136 | 137 | await prisma.tag.update({ 138 | where: { 139 | name, 140 | }, 141 | data: { 142 | newContent, 143 | }, 144 | }); 145 | const headers = { 146 | 'Content-Type': 'application/json', 147 | }; 148 | const oldContentRes = await fetch('https://sourceb.in/api/bins', { 149 | method: 'POST', 150 | headers, 151 | body: JSON.stringify({ 152 | files: [ 153 | { 154 | content, 155 | name, 156 | languageId: 222, 157 | }, 158 | ], 159 | }), 160 | }); 161 | 162 | const newContentRes = await fetch('https://sourceb.in/api/bins', { 163 | method: 'POST', 164 | headers, 165 | body: JSON.stringify({ 166 | files: [ 167 | { 168 | content: newContent, 169 | name, 170 | languageId: 222, 171 | }, 172 | ], 173 | }), 174 | }); 175 | const oldContentUrl = await oldContentRes.json(); 176 | const newContentUrl = await newContentRes.json(); 177 | 178 | const embed = new EmbedBuilder() 179 | .setTitle('Tag Update Request') 180 | .setColor(client.config.colors.blurple) 181 | .setFields([ 182 | { 183 | name: 'Name', 184 | value: `${name}`, 185 | inline: true, 186 | }, 187 | { 188 | name: 'Owner', 189 | value: `${client.users.cache.get(ownerId)}`, 190 | inline: true, 191 | }, 192 | { 193 | name: 'Type', 194 | value: `${type.toLowerCase()}`, 195 | inline: true, 196 | }, 197 | { 198 | name: 'Old Content', 199 | value: `[Click here](https://srcb.in/${oldContentUrl.key})`, 200 | inline: false, 201 | }, 202 | { 203 | name: 'New Content', 204 | value: `[Click here](https://srcb.in/${newContentUrl.key})`, 205 | inline: true, 206 | }, 207 | ]); 208 | 209 | const acceptButton = new ButtonBuilder() 210 | .setLabel('Accept') 211 | .setStyle(ButtonStyle.Success) 212 | .setCustomId(`tagModifyAccept--${tag.id}`); 213 | 214 | const declineButton = new ButtonBuilder() 215 | .setLabel('Decline') 216 | .setStyle(ButtonStyle.Danger) 217 | .setCustomId(`tagModifyDecline--${tag.id}`); 218 | const row = new ActionRowBuilder({ 219 | components: [acceptButton, declineButton], 220 | }); 221 | 222 | const tagVerificationChannel = client.channels.cache.get( 223 | channels.tagVerificationChannelId 224 | ); 225 | if (!tagVerificationChannel?.isTextBased()) { 226 | logger.error("'tagVerificationChannel' is not a text channel"); 227 | return; 228 | } 229 | tagVerificationChannel.send({ 230 | embeds: [embed], 231 | components: [row], 232 | }); 233 | 234 | userMessage.reply({ 235 | embeds: [ 236 | new EmbedBuilder() 237 | .setTitle('Request Sent!') 238 | .setDescription( 239 | 'A request has has been sent to modify your tag! We will notify you when your request has been accepted or declined.' 240 | ) 241 | .setFooter({ 242 | text: 'Do NOT ping or DM the moderators to get your tag accepted', 243 | }) 244 | .setColor(client.config.colors.green), 245 | ], 246 | }); 247 | }; 248 | -------------------------------------------------------------------------------- /src/lib/functions/viewTag.ts: -------------------------------------------------------------------------------- 1 | import { TagType } from '@prisma/client'; 2 | import { EmbedBuilder } from 'discord.js'; 3 | import { client, prisma } from '../..'; 4 | import { ModifiedChatInputCommandInteraction } from '../../types'; 5 | 6 | export const viewTag = async ( 7 | name: string, 8 | type: TagType, 9 | interaction: ModifiedChatInputCommandInteraction 10 | ) => { 11 | const tag = await prisma.tag.findFirst({ 12 | where: { 13 | name, 14 | type, 15 | }, 16 | }); 17 | 18 | if (!tag) { 19 | interaction.reply({ 20 | embeds: [ 21 | new EmbedBuilder() 22 | .setTitle(`${name.toLowerCase()} tag not found!`) 23 | .setDescription( 24 | 'The tag you requested was not found! Please verify the tag name.' 25 | ) 26 | .setColor(client.config.colors.red), 27 | ], 28 | ephemeral: true, 29 | }); 30 | return; 31 | } 32 | if (!tag.accepted) { 33 | interaction.reply({ 34 | embeds: [ 35 | new EmbedBuilder() 36 | .setTitle(`${name.toLowerCase()} tag is under review!`) 37 | .setDescription( 38 | `${name.toLowerCase()} is being reviewed by the moderators and cannot be accessed right now!` 39 | ) 40 | .setColor(client.config.colors.red), 41 | ], 42 | }); 43 | return; 44 | } 45 | const { content } = tag; 46 | interaction.reply({ 47 | content, 48 | }); 49 | }; 50 | -------------------------------------------------------------------------------- /src/lib/index.ts: -------------------------------------------------------------------------------- 1 | /* Classes */ 2 | export * from './classes/ApplicationCommand'; 3 | export * from './classes/Cheeka'; 4 | export * from './classes/Event'; 5 | /* Functions */ 6 | export * from './functions/createTag'; 7 | export * from './functions/deleteTag'; 8 | export * from './functions/getTagNames'; 9 | export * from './functions/getTopReps'; 10 | export * from './functions/modifyTag'; 11 | export * from './functions/tagCreateRequest'; 12 | export * from './functions/tagModifyRequest'; 13 | export * from './functions/viewTag'; 14 | export * from './functions/registerApplicatonCommands'; 15 | export * from './functions/registerButtons'; 16 | -------------------------------------------------------------------------------- /src/modules/addRep.ts: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder, GuildMember } from 'discord.js'; 2 | import { client, prisma } from '..'; 3 | import { manageRepRole } from '../features'; 4 | import { logRep } from './logRep'; 5 | import { 6 | ModifiedChatInputCommandInteraction, 7 | ModifiedUserContextMenuCommandInteraction, 8 | } from '../types'; 9 | import { repCooldownCache } from '../utils/Cache'; 10 | 11 | export const addRep = async ( 12 | member: GuildMember, 13 | interaction: 14 | | ModifiedChatInputCommandInteraction 15 | | ModifiedUserContextMenuCommandInteraction 16 | ) => { 17 | if (member.id === interaction.member.id) { 18 | await interaction.reply({ 19 | content: 'You cannot add reputation to yourself!', 20 | ephemeral: true, 21 | }); 22 | return; 23 | } 24 | if (member.user.bot) { 25 | await interaction.reply({ 26 | content: "You may not add reputation to bots.", 27 | ephemeral: true, 28 | }); 29 | return; 30 | }; 31 | const cooldownTimestamp = repCooldownCache.get(interaction.member.id); 32 | 33 | if (cooldownTimestamp) { 34 | await interaction.reply({ 35 | embeds: [ 36 | new EmbedBuilder({ 37 | title: 'Failed to add rep', 38 | description: 'You are on cooldown!', 39 | fields: [ 40 | { 41 | name: 'Expires', 42 | value: ``, 43 | }, 44 | ], 45 | color: client.config.colors.red, 46 | }), 47 | ], 48 | }); 49 | return; 50 | } 51 | 52 | try { 53 | const reputation = await prisma.reputation.findUnique({ 54 | where: { 55 | userId: member.id, 56 | }, 57 | }); 58 | if (!reputation) { 59 | await prisma.reputation.create({ 60 | data: { 61 | count: 0, 62 | userId: member.id, 63 | }, 64 | }); 65 | } 66 | await prisma.reputation.update({ 67 | where: { 68 | userId: member.id, 69 | }, 70 | data: { 71 | count: { 72 | increment: 1, 73 | }, 74 | }, 75 | }); 76 | } catch (err) { 77 | await interaction.reply({ 78 | content: 'An enexpected error occured!', 79 | ephemeral: true, 80 | }); 81 | } 82 | 83 | await manageRepRole(member, interaction); 84 | 85 | const cooldownTime = client.config.repCooldownMS || 3 * 60 * 60 * 1000; 86 | 87 | repCooldownCache.set( 88 | interaction.member.id, 89 | Math.floor((Date.now() + cooldownTime) / 1000) 90 | ); 91 | setTimeout( 92 | () => repCooldownCache.delete(interaction.member.id), 93 | cooldownTime 94 | ); 95 | 96 | const reply = await interaction.reply({ 97 | embeds: [ 98 | new EmbedBuilder({ 99 | title: 'Reputation Added!', 100 | description: `You have added reputation to ${member}!`, 101 | footer: { 102 | text: 'Only add reputation to people who helped you.', 103 | }, 104 | color: client.config.colors.green, 105 | }), 106 | ], 107 | }); 108 | await logRep(member, interaction, reply, 'ADD'); 109 | }; 110 | -------------------------------------------------------------------------------- /src/modules/index.ts: -------------------------------------------------------------------------------- 1 | export * from './addRep'; 2 | export * from './logRep'; 3 | export * from './trigger/handleTriggerPattern'; 4 | export * from './trigger/handleTriggerType'; 5 | -------------------------------------------------------------------------------- /src/modules/logRep.ts: -------------------------------------------------------------------------------- 1 | import { logger } from 'console-wizard'; 2 | import { EmbedBuilder, GuildMember, InteractionResponse } from 'discord.js'; 3 | import { client } from '..'; 4 | import { idData } from '../data'; 5 | import { 6 | ModifiedChatInputCommandInteraction, 7 | ModifiedUserContextMenuCommandInteraction, 8 | } from '../types'; 9 | import { RepActionType } from '../types/'; 10 | 11 | export const logRep = async ( 12 | member: GuildMember, 13 | interaction: 14 | | ModifiedChatInputCommandInteraction 15 | | ModifiedUserContextMenuCommandInteraction, 16 | reply: InteractionResponse, 17 | type: RepActionType, 18 | count?: number 19 | ) => { 20 | const logChannel = await client.channels.fetch( 21 | idData.channels.repLogChannel 22 | ); 23 | if (!logChannel) { 24 | logger.error('RepLogChannel not found!'); 25 | return; 26 | } 27 | const desc = `${ 28 | type === 'ADD' 29 | ? `${interaction.member} has added reputation to ${member}` 30 | : type === 'REMOVE' 31 | ? `${count} Reputation(s) of ${member} has been removed by ${interaction.member}` 32 | : `Reputation of ${member} has been cleared by ${interaction.member}` 33 | }\n[Go to Chat](${(await reply.fetch()).url})`; 34 | 35 | const embed = new EmbedBuilder({ 36 | title: 'Reputation Log', 37 | description: desc, 38 | timestamp: new Date(), 39 | color: client.config.colors.white, 40 | }); 41 | 42 | const channel = client.channels.cache.get(idData.channels.repLogChannel); 43 | 44 | if (!channel?.isTextBased()) return; 45 | channel.send({ 46 | embeds: [embed], 47 | }); 48 | }; 49 | -------------------------------------------------------------------------------- /src/modules/trigger/handleTriggerPattern.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from '../..'; 2 | import { ModifiedChatInputCommandInteraction } from '../../types'; 3 | 4 | export const handleTriggerPattern = async ( 5 | interaction: ModifiedChatInputCommandInteraction 6 | ) => { 7 | let exit = false; 8 | const subcommand = interaction.options.getSubcommand(); 9 | 10 | const type = interaction.options.getString('type'); 11 | const pattern = interaction.options.getString('pattern'); 12 | 13 | if (!type) return; 14 | if (!pattern) return; 15 | 16 | const isRegex = pattern.startsWith('/'); 17 | 18 | if (subcommand === 'add') { 19 | if (isRegex) { 20 | await prisma.trigger 21 | .update({ 22 | where: { 23 | type, 24 | }, 25 | data: { 26 | regexMatch: { 27 | push: pattern, 28 | }, 29 | }, 30 | }) 31 | .catch(async () => { 32 | await interaction.followUp({ 33 | content: 34 | 'Error creating pattern: Make sure there are no types with the same name!', 35 | }); 36 | exit = true; 37 | }); 38 | if (exit) return; 39 | } else { 40 | await prisma.trigger 41 | .update({ 42 | where: { 43 | type, 44 | }, 45 | data: { 46 | stringMatch: { 47 | push: pattern, 48 | }, 49 | }, 50 | }) 51 | .catch(async () => { 52 | await interaction.followUp({ 53 | content: 54 | 'Error creating pattern: Make sure there are no types with the same name!', 55 | }); 56 | exit = true; 57 | }); 58 | } 59 | 60 | if (exit) return; 61 | await interaction.followUp({ 62 | content: `Added new **${type}** pattern: \`${pattern}\``, 63 | }); 64 | } 65 | if (subcommand === 'delete') { 66 | const trigger = await prisma.trigger.findUnique({ where: { type } }); 67 | const array = isRegex ? trigger?.regexMatch : trigger?.stringMatch; 68 | const index = array?.indexOf(pattern); 69 | 70 | if (index === undefined || index === -1) { 71 | await interaction.followUp({ 72 | content: 73 | 'Error deleting pattern: Make sure the pattern for that type exists!', 74 | }); 75 | return; 76 | } 77 | array?.splice(index, 1); 78 | 79 | if (isRegex) { 80 | await prisma.trigger 81 | .update({ 82 | where: { 83 | type, 84 | }, 85 | data: { 86 | regexMatch: { 87 | set: array, 88 | }, 89 | }, 90 | }) 91 | .catch(async () => { 92 | await interaction.followUp({ 93 | content: 94 | 'Error deleting pattern: Make sure the pattern for that type exists!', 95 | }); 96 | exit = true; 97 | }); 98 | if (exit) return; 99 | } else { 100 | await prisma.trigger 101 | .update({ 102 | where: { 103 | type, 104 | }, 105 | data: { 106 | stringMatch: { 107 | set: array, 108 | }, 109 | }, 110 | }) 111 | .catch(async () => { 112 | await interaction.followUp({ 113 | content: 114 | 'Error deleting pattern: Make sure the pattern for that type exists!', 115 | }); 116 | exit = true; 117 | }); 118 | } 119 | if (exit) return; 120 | await interaction.followUp({ 121 | content: `Deleted **${type}** pattern: \`${pattern}\``, 122 | }); 123 | } 124 | }; 125 | -------------------------------------------------------------------------------- /src/modules/trigger/handleTriggerType.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from '../..'; 2 | import { ModifiedChatInputCommandInteraction } from '../../types'; 3 | 4 | export const handleTriggerType = async ( 5 | interaction: ModifiedChatInputCommandInteraction 6 | ) => { 7 | let exit = false; 8 | const subcommand = interaction.options.getSubcommand(); 9 | 10 | const type = interaction.options.getString('name'); 11 | if (!type) return; 12 | 13 | const replyMessageContent = interaction.options.getString('reply_message'); 14 | 15 | if (subcommand === 'add') { 16 | if (!replyMessageContent) return; 17 | 18 | await prisma.trigger 19 | .create({ 20 | data: { 21 | type, 22 | replyMessageContent, 23 | }, 24 | }) 25 | .catch(async () => { 26 | await interaction.followUp({ 27 | content: 28 | 'Error creating type: Make sure there are no types with the same name!', 29 | }); 30 | exit = true; 31 | }); 32 | if (exit) return; 33 | 34 | await interaction.followUp({ 35 | content: `Added ${type} trigger!.\n**Content:**\n${replyMessageContent}`, 36 | }); 37 | return; 38 | } 39 | if (subcommand === 'modify') { 40 | if (!replyMessageContent) return; 41 | 42 | await prisma.trigger 43 | .update({ 44 | where: { type }, 45 | data: { replyMessageContent }, 46 | }) 47 | .catch(async () => { 48 | await interaction.followUp({ 49 | content: 'Error creating type: Make sure the type exists!', 50 | }); 51 | exit = true; 52 | }); 53 | if (exit) return; 54 | await interaction.followUp({ 55 | content: `Updated ${type} trigger's content to:\n${replyMessageContent}`, 56 | }); 57 | return; 58 | } 59 | if (subcommand === 'delete') { 60 | await prisma.trigger 61 | .delete({ 62 | where: { type }, 63 | }) 64 | .catch(async () => { 65 | await interaction.followUp({ 66 | content: 'Error creating type: Make sure the type exists!', 67 | }); 68 | exit = true; 69 | }); 70 | if (exit) return; 71 | await interaction.followUp({ 72 | content: `Deleted ${type} trigger!`, 73 | }); 74 | return; 75 | } 76 | }; 77 | -------------------------------------------------------------------------------- /src/types/HandlersTypes.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImagineGamingPlay/Cheeka-Development/88d92125b53de7c5974ad53b8defbc397b440bfe/src/types/HandlersTypes.ts -------------------------------------------------------------------------------- /src/types/InteractionTypes.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ApplicationCommandType, 3 | AutocompleteInteraction, 4 | ButtonInteraction, 5 | ChatInputApplicationCommandData, 6 | ChatInputCommandInteraction, 7 | CommandInteractionOptionResolver, 8 | GuildMember, 9 | MessageApplicationCommandData, 10 | MessageContextMenuCommandInteraction, 11 | UserApplicationCommandData, 12 | UserContextMenuCommandInteraction, 13 | } from 'discord.js'; 14 | import { Cheeka } from '../lib'; 15 | 16 | /** 17 | * TODO: Remove modified interactions and take a much safer 18 | * approach with checking for cached guilds and using cached 19 | * interactions 20 | */ 21 | 22 | export interface ModifiedChatInputCommandInteraction 23 | extends ChatInputCommandInteraction { 24 | member: GuildMember; 25 | } 26 | 27 | export interface ModifiedUserContextMenuCommandInteraction 28 | extends UserContextMenuCommandInteraction { 29 | targetMember: GuildMember; 30 | member: GuildMember; 31 | } 32 | 33 | export interface ModifiedMessageContextMenuCommandInteraction 34 | extends MessageContextMenuCommandInteraction { 35 | targetMember: GuildMember; 36 | member: GuildMember; 37 | } 38 | 39 | export interface LoadedApplicationCommands { 40 | LoadedCommands: string; 41 | } 42 | 43 | export interface CommandRunParams { 44 | client: Cheeka; 45 | interaction: ModifiedChatInputCommandInteraction; 46 | options?: CommandInteractionOptionResolver; 47 | } 48 | export interface UserContextMenuRunParams { 49 | client: Cheeka; 50 | interaction: ModifiedUserContextMenuCommandInteraction; 51 | } 52 | export interface MessageContextMenuRunParams { 53 | client: Cheeka; 54 | interaction: ModifiedMessageContextMenuCommandInteraction; 55 | } 56 | 57 | type Command = { 58 | type: T; 59 | } & U & 60 | V; 61 | 62 | export type ChatInputCommandData = Command< 63 | ApplicationCommandType.ChatInput, 64 | { 65 | devOnly?: boolean; 66 | ownerOnly?: boolean; 67 | run: (params: CommandRunParams) => Promise; 68 | autocomplete?: (interaction: AutocompleteInteraction) => Promise; 69 | }, 70 | ChatInputApplicationCommandData 71 | >; 72 | 73 | export type UserContextMenuData = Command< 74 | ApplicationCommandType.User, 75 | { 76 | devOnly?: boolean; 77 | ownerOnly?: boolean; 78 | run: (params: UserContextMenuRunParams) => Promise; 79 | }, 80 | UserApplicationCommandData 81 | >; 82 | 83 | export type MessageContextMenuData = Command< 84 | ApplicationCommandType.Message, 85 | { 86 | devOnly?: boolean; 87 | ownerOnly?: boolean; 88 | run: (params: MessageContextMenuRunParams) => Promise; 89 | }, 90 | MessageApplicationCommandData 91 | >; 92 | 93 | // export interface ChatInputCommandData extends ChatInputApplicationCommandData { 94 | // type: ApplicationCommandType.ChatInput; 95 | // devOnly?: boolean; 96 | // ownerOnly?: boolean; 97 | // run: (params: CommandRunParams) => Promise; 98 | // autocomplete?: (interaction: AutocompleteInteraction) => Promise; 99 | // } 100 | // export interface UserContextMenuData extends UserApplicationCommandData { 101 | // type: ApplicationCommandType.User; 102 | // devOnly?: boolean; 103 | // ownerOnly?: boolean; 104 | // run: (params: UserContextMenuRunParams) => Promise; 105 | // } 106 | // 107 | // export interface MessageContextMenuData extends MessageApplicationCommandData { 108 | // type: ApplicationCommandType.Message; 109 | // devOnly?: boolean; 110 | // ownerOnly?: boolean; 111 | // run: (params: MessageContextMenuRunParams) => Promise; 112 | // } 113 | 114 | export type CommandData = 115 | | ChatInputCommandData 116 | | UserContextMenuData 117 | | MessageContextMenuData; 118 | 119 | export interface ButtonRunParams { 120 | interaction: ButtonInteraction; 121 | id: string; 122 | scope: string; 123 | } 124 | 125 | export interface ButtonOptions { 126 | scope: string; 127 | run: (params: ButtonRunParams) => Promise; 128 | } 129 | -------------------------------------------------------------------------------- /src/types/TagTypes.ts: -------------------------------------------------------------------------------- 1 | import { TagType } from '@prisma/client'; 2 | import { ModifiedChatInputCommandInteraction } from './'; 3 | 4 | export interface TagProps { 5 | name: string; 6 | type: TagType; 7 | ownerId: string; 8 | content: string; 9 | interaction: ModifiedChatInputCommandInteraction; 10 | } 11 | -------------------------------------------------------------------------------- /src/types/configType.ts: -------------------------------------------------------------------------------- 1 | type ColorsType = { 2 | blurple: number; 3 | red: number; 4 | green: number; 5 | white: number; 6 | }; 7 | 8 | export interface ConfigType { 9 | boosterDMCooldown?: number; 10 | aiReactionTimesCalled?: number; 11 | aiReactionChannels?: string[]; 12 | openaiApiKey?: string; 13 | guildId: string; 14 | ownerId: string; 15 | devGuildId: string; 16 | staffRoleId: string; 17 | mainGuildId: string; 18 | environment: string; 19 | repCooldownMS?: number; 20 | token: string; 21 | clientId: string; 22 | colors: ColorsType; 23 | developerRoleId: string; 24 | repLeaderboardColors: string[]; 25 | } 26 | -------------------------------------------------------------------------------- /src/types/envType.ts: -------------------------------------------------------------------------------- 1 | export type EnvironmentType = 'dev' | 'prod'; 2 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './InteractionTypes'; 2 | export * from './TagTypes'; 3 | export * from './configType'; 4 | export * from './envType'; 5 | export * from './miscTypes'; 6 | export * from './repTypes'; 7 | -------------------------------------------------------------------------------- /src/types/miscTypes.ts: -------------------------------------------------------------------------------- 1 | export interface BadgeListType { 2 | ActiveDeveloper: string; 3 | BugHunterLevel1: string; 4 | BugHunterLevel2: string; 5 | PremiumEarlySupporter: string; 6 | Partner: string; 7 | Staff: string; 8 | HypeSquadOnlineHouse1: string; 9 | HypeSquadOnlineHouse2: string; 10 | HypeSquadOnlineHouse3: string; 11 | Hypesquad: string; 12 | CertifiedModerator: string; 13 | VerifiedDeveloper: string; 14 | } 15 | -------------------------------------------------------------------------------- /src/types/repTypes.ts: -------------------------------------------------------------------------------- 1 | export type RepActionType = 'ADD' | 'REMOVE' | 'CLEAR'; 2 | -------------------------------------------------------------------------------- /src/utils/Cache.ts: -------------------------------------------------------------------------------- 1 | export class BotCache { 2 | private cache: Map; 3 | 4 | constructor() { 5 | this.cache = new Map(); 6 | } 7 | 8 | /** 9 | * Add a entry to the cache 10 | * 11 | * @param {K} key 12 | * @param {V} value 13 | * 14 | * @returns {Map} 15 | */ 16 | set(key: K, value: V): Map { 17 | return this.cache.set(key, value); 18 | } 19 | 20 | /** 21 | * Get value associated with the given key. 22 | * 23 | * @param {K} key 24 | * @returns {V | undefined} 25 | */ 26 | get(key: K): V | undefined { 27 | return this.cache.get(key); 28 | } 29 | 30 | /** 31 | * Check weather a key exists on the cache 32 | * 33 | * @param {K} key 34 | * @returns {Boolean} 35 | */ 36 | has(key: K): boolean { 37 | return this.cache.has(key); 38 | } 39 | 40 | /** 41 | * Delete an entry from the cache 42 | * 43 | * @param {K} key 44 | */ 45 | delete(key: K) { 46 | this.cache.delete(key); 47 | } 48 | 49 | /** 50 | * Clear everything from the cache 51 | * 52 | */ 53 | clear() { 54 | this.cache.clear(); 55 | } 56 | } 57 | 58 | export const repCooldownCache = new BotCache(); 59 | export const triggerPatternCache = new Map< 60 | string, 61 | { 62 | stringMatch: string[]; 63 | regexMatch: RegExp[]; 64 | replyMessageContent: string; 65 | } 66 | >(); 67 | -------------------------------------------------------------------------------- /src/utils/HumanizeMillisecond.ts: -------------------------------------------------------------------------------- 1 | const { floor } = Math; 2 | 3 | export const humanizeMillisecond = (ms: number) => { 4 | // const milliseconds = ms / 100; 5 | const seconds = (ms / 1000) % 60; 6 | const minutes = (ms / (1000 * 60)) % 60; 7 | const hours = (ms / (1000 * 60 * 60)) % 24; 8 | 9 | const humanizedHours = floor(hours < 10 ? 0 + hours : hours); 10 | const humanizedMinutes = floor(minutes < 10 ? 0 + minutes : minutes); 11 | const humanizedSeconds = floor(seconds < 10 ? 0 + seconds : seconds); 12 | 13 | const res = { 14 | hours: humanizedHours, 15 | minutes: humanizedMinutes, 16 | seconds: humanizedSeconds, 17 | }; 18 | 19 | return res; 20 | }; 21 | -------------------------------------------------------------------------------- /src/utils/getFiles.ts: -------------------------------------------------------------------------------- 1 | import { readdirSync } from 'fs'; 2 | import { join } from 'path'; 3 | 4 | export const getFiles = (path: string, categorized: boolean): string[] => { 5 | const files: string[] = []; 6 | 7 | // FS = FileSystem (FSNode representing both files and folders) 8 | const firstDepthFSNodes = readdirSync(path); 9 | 10 | firstDepthFSNodes.forEach(FSNode => { 11 | if (!categorized) { 12 | files.push(join(path, FSNode)); 13 | return files; 14 | } 15 | 16 | // Basically files but named this way for consistency 17 | const secondDepthFSNodes = readdirSync(`${path}/${FSNode}`); 18 | 19 | secondDepthFSNodes.forEach(fileName => { 20 | files.push(join(path, FSNode, fileName)); 21 | return files; 22 | }); 23 | }); 24 | return files; 25 | }; 26 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './HumanizeMillisecond'; 2 | export * from './getFiles'; 3 | export * from './raise'; 4 | -------------------------------------------------------------------------------- /src/utils/raise.ts: -------------------------------------------------------------------------------- 1 | import { logger } from 'console-wizard'; 2 | 3 | export const raise = (err: string): never => { 4 | throw logger.error(err); 5 | }; 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "CommonJS", 4 | "target": "esnext", 5 | "moduleResolution": "node", 6 | "outDir": "./dist", 7 | "rootDir": "./src", 8 | "allowJs": true, 9 | "removeComments": true, 10 | "esModuleInterop": true, 11 | // Type-checking 12 | "strict": true, 13 | "noImplicitAny": true, 14 | "noImplicitOverride": true, 15 | "noUnusedLocals": true, 16 | "noUnusedParameters": true, 17 | "paths": { 18 | "@assets/": ["assets/*"] 19 | } 20 | }, 21 | "include": ["src/**/*", "environment.d.ts"], 22 | "exclude": ["node_modules/", "dist/"] 23 | } 24 | --------------------------------------------------------------------------------