commit eb242e85bba23720f3b5d383789b6205aab379dc Author: ShuriZma Date: Sat Mar 30 15:36:10 2024 +0100 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..34977ee --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules +.idea \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..6a3e09f --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +## How to install & run this bot. +Prerequisites: +* [NodeJS/NPM](https://nodejs.org/en/download) +* Potentially [Git](https://git-scm.com/downloads) + +1. Download the repo files. + 1. `git clone https://git.shuri.gg/ShuriZma/WordCounterBot.git` + 2. or just click the 3 dots at the top right and download the ZIP +2. Open a console inside the downloaded repo folder. +3. Run `npm i` +4. Run `npm run bot` \ No newline at end of file diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..99afc65 --- /dev/null +++ b/index.ts @@ -0,0 +1,4 @@ +import { Bot } from "./src/bot"; + +const bot = Bot.getInstance(); +bot.init(); \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..a9a4351 --- /dev/null +++ b/package.json @@ -0,0 +1,21 @@ +{ + "name": "nickskeeeebot", + "version": "1.0.0", + "description": "", + "main": "index.ts", + "scripts": { + "bot": "ts-node index.ts" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "discord.js": "^14.14.1", + "sqlite3": "^5.1.7", + "ts-node": "^10.9.2" + }, + "devDependencies": { + "@types/node": "^20.11.30", + "typescript": "^5.4.3" + } +} diff --git a/src/bot.ts b/src/bot.ts new file mode 100644 index 0000000..49eaee0 --- /dev/null +++ b/src/bot.ts @@ -0,0 +1,254 @@ +import { + ChatInputCommandInteraction, + Client, + Collection, + EmbedBuilder, + Events, + GatewayIntentBits, + Message, PermissionsBitField, REST, + Routes, + User +} from 'discord.js'; +import * as sqlite3 from 'sqlite3'; +import config from "./config"; +import SetChannel from "./commands/setChannel"; +import Library from "./commands/library"; +import Reset from "./commands/reset"; +import SetModRole from "./commands/setModRole"; +import SetIgnorePrefix from "./commands/setIgnorePrefix"; +import SetAutoEmbedMessages from "./commands/setAutoEmbedMessages"; + +export class Bot { + private db: sqlite3.Database; + private channel: string; + private static instance: Bot; + private counter = 0; + private lastUser: string; + private modRole: string; + private ignorePrefix: string = '+'; + private autoEmbedMessages: number = 10; + + public init() { + const client = new Client({intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent]}); + this.connectDatabase() + + this.registerCommands(); + const commands: Collection = new Collection(); + commands.set(SetChannel.data.name, SetChannel); + commands.set(Library.data.name, Library); + commands.set(Reset.data.name, Reset); + commands.set(SetModRole.data.name, SetModRole); + commands.set(SetIgnorePrefix.data.name, SetIgnorePrefix); + commands.set(SetAutoEmbedMessages.data.name, SetAutoEmbedMessages); + + + client.once(Events.ClientReady, readyClient => { + console.log(`Ready! Logged in as ${readyClient.user.tag}`); + }); + + client.on(Events.Error, (err) => { + console.error(err); + }); + + client.on(Events.InteractionCreate, async interaction => { + if (!interaction.isChatInputCommand()) return; + const command = commands.get(interaction.commandName); + + if (!command) { + console.error(`No command matching ${interaction.commandName} was found.`); + return; + } + + try { + await command.execute(interaction); + } catch (error) { + console.error(error); + if (interaction.replied || interaction.deferred) { + await interaction.followUp({ content: 'There was an error while executing this command!', ephemeral: true }); + } else { + await interaction.reply({ content: 'There was an error while executing this command!', ephemeral: true }); + } + } + }); + + client.on(Events.MessageCreate, async (msg) => { + if ( + this.channel + && msg.channel.id === this.channel + && !msg.content.startsWith(this.ignorePrefix) + && !msg.author.bot + ) { + if ( + this.lastUser === msg.author.id + && this.modRole ? msg.member.roles.highest.comparePositionTo(this.modRole) < 0 : !msg.member.permissions.has(PermissionsBitField.Flags.Administrator) + ) { + await msg.delete(); + return; + } + + this.counter++; + + msg.content = msg.content.charAt(0).toUpperCase() + msg.content.slice(1); + this.db.run( + 'INSERT OR REPLACE INTO library (word, amount) VALUES (?, COALESCE((SELECT amount FROM library WHERE word = ?), 0) + 1)', + [ + msg.content, + msg.content, + ] + ); + } + + if (this.counter === this.autoEmbedMessages) { + this.counter = 0; + + this.sendEmbed(msg); + } + }); + + client.login(config.token); + } + + private registerCommands() { + const commands = []; + commands.push( + SetChannel.data.toJSON(), + Library.data.toJSON(), + Reset.data.toJSON(), + SetModRole.data.toJSON(), + SetIgnorePrefix.data.toJSON(), + SetAutoEmbedMessages.data.toJSON(), + ); + + + const rest = new REST().setToken(config.token); + (async () => { + try { + console.log(`Started refreshing ${commands.length} application (/) commands.`); + + const data = await rest.put( + Routes.applicationGuildCommands(config.clientId, config.guildId), + {body: commands}, + ); + + console.log(`Successfully reloaded ${data['length']} application (/) commands.`); + } catch (error) { + // And of course, make sure you catch and log any errors! + console.error(error); + } + })(); + } + + public sendEmbed(msg: Message|ChatInputCommandInteraction) { + this.db.all( + 'SELECT * FROM library ORDER BY amount DESC LIMIT 9', + async (err, rows) => { + if (rows) { + const icons = [ + ':first_place: • ', + ':second_place: • ', + ':third_place: • ', + ':four: • ', + ':five: • ', + ':six: • ', + ':seven: • ', + ':eight: • ', + ':nine: • ', + ]; + const fields = []; + for (let index in rows) { + fields.push({ + name: icons[index] + rows[index]['word'], + value: rows[index]['amount'] + ' uses', + inline: true, + }); + } + + fields.push({ + name: '⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤', + value: '\u200B', + }); + + const embed = new EmbedBuilder() + .setTitle('Top 10 most used words.') + .setDescription('This is the Library with the most used words and their amount.\u200B⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤⏤') + .setColor(0xffe600) + .setFields(fields); + await msg.reply({embeds: [embed]}); + } + } + ); + } + + private connectDatabase() { + this.db = new sqlite3.Database(config.database) + + this.db.run('CREATE TABLE IF NOT EXISTS config (configKey VARCHAR PRIMARY KEY, value VARCHAR NOT NULL)'); + this.db.run('CREATE TABLE IF NOT EXISTS library (word VARCHAR PRIMARY KEY UNIQUE, amount INT NOT NULL DEFAULT 0)'); + + this.db.get('SELECT value FROM config WHERE configKey = "channelId"', (err, row) => { + if (row) { + this.channel = row['value']; + } + }); + + this.db.get('SELECT value FROM config WHERE configKey = "modRoleId"', (err, row) => { + if (row) { + this.modRole = row['value']; + } + }); + + this.db.get('SELECT value FROM config WHERE configKey = "ignorePrefix"', (err, row) => { + if (row) { + this.ignorePrefix = row['value']; + } + }); + + this.db.get('SELECT value FROM config WHERE configKey = "autoEmbedMessages"', (err, row) => { + if (row) { + this.autoEmbedMessages = row['value']; + } + }); + } + + public getDB() { + return this.db; + } + + public setChannel(channelId: string): Bot { + this.channel = channelId; + return this; + } + + public setCounter(counter: number): Bot { + this.counter = counter; + return this; + } + + public setLastUser(lastUserId: string): Bot { + this.lastUser = lastUserId; + return this; + } + + public setModRole(roleId: string): Bot { + this.modRole = roleId; + return this; + } + + public setIgnorePrefix(prefix: string): Bot { + this.ignorePrefix = prefix; + return this; + } + + public setAutoEmbedMessages(counter: number): Bot { + this.autoEmbedMessages = counter; + return this; + } + + public static getInstance(): Bot { + if (!Bot.instance) { + Bot.instance = new Bot(); + } + + return Bot.instance; + } +} \ No newline at end of file diff --git a/src/commands/library.ts b/src/commands/library.ts new file mode 100644 index 0000000..7d3f55d --- /dev/null +++ b/src/commands/library.ts @@ -0,0 +1,18 @@ +import { + ChatInputCommandInteraction, + EmbedBuilder, + SlashCommandBuilder, +} from 'discord.js'; +import { Bot } from "../bot"; + +export default { + data: new SlashCommandBuilder() + .setName('library') + .setDescription('Get an embed of the most used words!'), + async execute(interaction: ChatInputCommandInteraction) { + const bot = Bot.getInstance(); + bot.setCounter(0); + + return bot.sendEmbed(interaction); + }, +}; \ No newline at end of file diff --git a/src/commands/reset.ts b/src/commands/reset.ts new file mode 100644 index 0000000..96769d9 --- /dev/null +++ b/src/commands/reset.ts @@ -0,0 +1,29 @@ +import { + ChatInputCommandInteraction, + PermissionsBitField, + SlashCommandBuilder, + SlashCommandStringOption, +} from 'discord.js'; +import { Bot } from "../bot"; + +const option = new SlashCommandStringOption() + .setName('word') + .setDescription('Only delete this word.'); + +export default { + data: new SlashCommandBuilder() + .setName('reset') + .setDescription('Reset the entire library or remove a single word.') + .setDefaultMemberPermissions(PermissionsBitField.Flags.Administrator) + .addStringOption(option), + async execute(interaction: ChatInputCommandInteraction) { + const bot = Bot.getInstance(); + if (interaction.options.data.at(0)) { + bot.getDB().run('DELETE FROM library WHERE word = ?', [interaction.options.data.at(0).value]); + } + + bot.getDB().run('DELETE FROM library WHERE TRUE'); + bot.setCounter(0); + return await interaction.reply('Done!'); + }, +}; \ No newline at end of file diff --git a/src/commands/setAutoEmbedMessages.ts b/src/commands/setAutoEmbedMessages.ts new file mode 100644 index 0000000..84042eb --- /dev/null +++ b/src/commands/setAutoEmbedMessages.ts @@ -0,0 +1,33 @@ +import { + ChatInputCommandInteraction, + PermissionsBitField, + SlashCommandBuilder, + SlashCommandIntegerOption, +} from 'discord.js'; +import { Bot } from "../bot"; + +const option = (new SlashCommandIntegerOption()) + .setName('amount') + .setDescription('Amount of messages needed.') + .setRequired(true); + +export default { + data: new SlashCommandBuilder() + .setName('setautoembedmessages') + .setDescription('Set the amount of messages required to automatically send the embed.') + .setDefaultMemberPermissions(PermissionsBitField.Flags.Administrator) + .addIntegerOption(option), + async execute(interaction: ChatInputCommandInteraction) { + const bot = Bot.getInstance(); + bot.getDB().run( + 'INSERT OR REPLACE INTO config (configKey, value) VALUES (?, ?)', + [ + 'autoEmbedMessages', + interaction.options.data.at(0).value, + ] + ); + + bot.setAutoEmbedMessages(parseInt(interaction.options.data.at(0).value.toString())); + return await interaction.reply('Done!'); + }, +}; \ No newline at end of file diff --git a/src/commands/setChannel.ts b/src/commands/setChannel.ts new file mode 100644 index 0000000..c736071 --- /dev/null +++ b/src/commands/setChannel.ts @@ -0,0 +1,35 @@ +import { + ChatInputCommandInteraction, + PermissionsBitField, + SlashCommandBuilder, + SlashCommandChannelOption, +} from 'discord.js'; +import { ChannelType } from 'discord-api-types/v10'; +import { Bot } from "../bot"; + +const channelOption = (new SlashCommandChannelOption()) + .setName('channel') + .setDescription('The channel you want the bot to listen to.') + .setRequired(true) + .addChannelTypes(ChannelType.GuildText); + +export default { + data: new SlashCommandBuilder() + .setName('setchannel') + .setDescription('Set the channel the bot should listen to.') + .setDefaultMemberPermissions(PermissionsBitField.Flags.Administrator) + .addChannelOption(channelOption), + async execute(interaction: ChatInputCommandInteraction) { + const bot = Bot.getInstance(); + bot.getDB().run( + 'INSERT OR REPLACE INTO config (configKey, value) VALUES (?, ?)', + [ + 'channelId', + interaction.options.data.at(0).value, + ] + ); + + bot.setChannel(interaction.options.data.at(0).value.toString()); + return await interaction.reply('Done!'); + }, +}; \ No newline at end of file diff --git a/src/commands/setIgnorePrefix.ts b/src/commands/setIgnorePrefix.ts new file mode 100644 index 0000000..97d9cd6 --- /dev/null +++ b/src/commands/setIgnorePrefix.ts @@ -0,0 +1,33 @@ +import { + ChatInputCommandInteraction, + PermissionsBitField, + SlashCommandBuilder, + SlashCommandStringOption, +} from 'discord.js'; +import { Bot } from "../bot"; + +const option = (new SlashCommandStringOption()) + .setName('prefix') + .setDescription('The prefix.') + .setRequired(true); + +export default { + data: new SlashCommandBuilder() + .setName('setignoreprefix') + .setDescription('Set the prefix that causes the bot to ignore a message.') + .setDefaultMemberPermissions(PermissionsBitField.Flags.Administrator) + .addStringOption(option), + async execute(interaction: ChatInputCommandInteraction) { + const bot = Bot.getInstance(); + bot.getDB().run( + 'INSERT OR REPLACE INTO config (configKey, value) VALUES (?, ?)', + [ + 'ignorePrefix', + interaction.options.data.at(0).value, + ] + ); + + bot.setIgnorePrefix(interaction.options.data.at(0).value.toString()); + return await interaction.reply('Done!'); + }, +}; \ No newline at end of file diff --git a/src/commands/setModRole.ts b/src/commands/setModRole.ts new file mode 100644 index 0000000..7f1a053 --- /dev/null +++ b/src/commands/setModRole.ts @@ -0,0 +1,33 @@ +import { + ChatInputCommandInteraction, + PermissionsBitField, + SlashCommandBuilder, + SlashCommandRoleOption, +} from 'discord.js'; +import { Bot } from "../bot"; + +const option = (new SlashCommandRoleOption()) + .setName('role') + .setDescription('The moderator role.') + .setRequired(true); + +export default { + data: new SlashCommandBuilder() + .setName('setmodrole') + .setDescription('Set the moderator role.') + .setDefaultMemberPermissions(PermissionsBitField.Flags.Administrator) + .addRoleOption(option), + async execute(interaction: ChatInputCommandInteraction) { + const bot = Bot.getInstance(); + bot.getDB().run( + 'INSERT OR REPLACE INTO config (configKey, value) VALUES (?, ?)', + [ + 'modRoleId', + interaction.options.data.at(0).value, + ] + ); + + bot.setModRole(interaction.options.data.at(0).value.toString()); + return await interaction.reply('Done!'); + }, +}; \ No newline at end of file diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..c4df66f --- /dev/null +++ b/src/config.ts @@ -0,0 +1,6 @@ +export default { + database: 'name of the database file', + token: 'bot token', + guildId: 'server id', + clientId: 'client id of your bot instance', +}; \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..e69de29