├── .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 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/fastRequest/fastRequestCurrentProjectLocalConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/jpa-buddy.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/jsLinters/eslint.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
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 |
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 |
--------------------------------------------------------------------------------