Skip to content

Commit

Permalink
basic commands
Browse files Browse the repository at this point in the history
  • Loading branch information
Robin-Sch committed Apr 4, 2024
1 parent a4a6f0e commit abea965
Show file tree
Hide file tree
Showing 28 changed files with 1,293 additions and 60 deletions.
1 change: 0 additions & 1 deletion .env.example

This file was deleted.

4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# {name}
# Discord Template

{name} is a [Discord](https://discord.com/) bot, made with [discord.js](https://discord.js.org/) and written in typescript.
Discord Template is a [Discord](https://discord.com/) bot with same "default" features, made with [discord.js](https://discord.js.org/) and written in typescript.

## Installation

Expand Down
3 changes: 2 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ services:
image: ghcr.io/RobinSch/discord-template:latest
restart: always
environment:
- BOT_TOKEN=
- BOT_TOKEN=${BOT_TOKEN}
- POSTGRES_URL=${POSTGRES_URL}
12 changes: 7 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "discord-template",
"version": "1.0.0",
"description": "A Discord.js Bot template",
"description": "A template for your Discord bot ",
"main": "src/index.ts",
"scripts": {
"start": "npm run build && node dist/index.js",
Expand All @@ -14,14 +14,16 @@
"dependencies": {
"@types/node": "^20.11.20",
"discord.js": "^14.14.1",
"typescript": "^5.3.3"
"pg": "^8.11.3",
"typescript": "^5.3.3",
"uuid": "^9.0.1"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^7.0.2",
"@typescript-eslint/parser": "^7.0.2",
"eslint": "^8.56.0",
"eslint-config-prettier": "^8.10.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint": "^8.56.0",
"eslint-config-prettier": "^8.10.0",
"eslint-plugin-prettier": "^5.1.3",
"prettier": "^3.2.5"
}
}
32 changes: 32 additions & 0 deletions src/commands/add.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { ChannelType, ChatInputCommandInteraction, SlashCommandBuilder } from 'discord.js';

import { query } from '../utils/postgres';

export default {
data: new SlashCommandBuilder()
.setName('add')
.setDescription('Add a member to a support ticket')
.addUserOption((option) => option.setName('member').setDescription('The member to give access to this support ticket').setRequired(true)),

category: 'Tickets',
guildOnly: true,

async execute(interaction: ChatInputCommandInteraction) {
if (!interaction.inCachedGuild() || interaction.channel?.type !== ChannelType.GuildText) return interaction.client.error.ONLY_IN_GUILD(interaction);

const { rows: currentSupportRows } = await query('SELECT FROM supports WHERE channel = $1 AND guild = $2;', [interaction.channelId, interaction.guildId]);
if (currentSupportRows.length === 0) return interaction.client.error.ONLY_IN_TICKET(interaction, false);

const member = interaction.options.getMember('member');
if (!member) return interaction.client.error.INVALID_INPUT(interaction, 'member', 'should be nonzero');
const memberID = member.id;

await interaction.channel.permissionOverwrites.create(memberID, {
ViewChannel: true,
ReadMessageHistory: true,
SendMessages: true,
});

return interaction.editReply(`You added ${member.displayName} to this support ticket!`);
},
};
61 changes: 61 additions & 0 deletions src/commands/ban.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { ChatInputCommandInteraction, PermissionFlagsBits, SlashCommandBuilder } from 'discord.js';
import { v4 as uuidv4 } from 'uuid';

import { query } from '../utils/postgres';

export default {
data: new SlashCommandBuilder()
.setName('ban')
.setDescription('Ban a member')
.addUserOption((option) => option.setName('member').setDescription('The member to ban').setRequired(true))
.addStringOption((option) => option.setName('reason').setDescription('The reason to ban').setRequired(true).setMaxLength(512).setMinLength(2))
.setDefaultMemberPermissions(PermissionFlagsBits.BanMembers),

category: 'Moderation',
guildOnly: true,

async execute(interaction: ChatInputCommandInteraction) {
if (!interaction.inCachedGuild()) return interaction.client.error.ONLY_IN_GUILD(interaction);
if (!interaction.member.permissions.has(PermissionFlagsBits.BanMembers)) return interaction.client.error.NO_PERMISSION(interaction);

const moderator = interaction.member;
const moderatorID = moderator.id;

const member = interaction.options.getMember('member');
if (!member) return interaction.client.error.INVALID_INPUT(interaction, 'member', 'should be nonzero');
const memberID = member.id;
if (memberID === moderatorID) return interaction.client.error.INVALID_INPUT(interaction, 'member', 'should not be you');

const reason = interaction.options.getString('reason', true);
if (reason.length < 2 || reason.length > 512) return interaction.client.error.INVALID_INPUT(interaction, 'reason', 'should be 512 characters or less');

const positionDifference = moderator.roles.highest.comparePositionTo(member.roles.highest);
if (positionDifference <= 0) return interaction.client.error.NO_PERMISSION(interaction);

if (!member.bannable) return interaction.editReply('I have no permissions to ban this member!');

member.send(`You are banned for ${reason} in ${interaction.guild.name}, goodbye!`).catch();

try {
member.ban({ reason });
} catch (error) {
return interaction.client.error.UNKNOWN_ERROR(interaction, error);
}

await query('INSERT INTO guilds (id) VALUES ($1) ON CONFLICT DO NOTHING;', [interaction.guildId]);
await query('INSERT INTO users (id) VALUES ($1) ON CONFLICT DO NOTHING;', [memberID]);
await query('INSERT INTO users (id) VALUES ($1) ON CONFLICT DO NOTHING;', [moderatorID]);

const id = uuidv4();

await query('INSERT INTO bans (id, guild, member, reason, moderator, time) VALUES ($1,$2,$3,$4,$5,$6);', [
id,
interaction.guildId,
memberID,
reason,
moderatorID,
interaction.createdAt,
]);
return interaction.editReply(`Banned ${member.displayName} for ${reason}!`);
},
};
28 changes: 28 additions & 0 deletions src/commands/close.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { ChannelType, ChatInputCommandInteraction, SlashCommandBuilder } from 'discord.js';

import { closeTicket } from '../utils/tickets';

export default {
data: new SlashCommandBuilder().setName('close').setDescription('Close a support or mail ticket'),

category: 'Tickets',
guildOnly: true,

async execute(interaction: ChatInputCommandInteraction) {
if (!interaction.inCachedGuild() || interaction.channel?.type !== ChannelType.GuildText) return interaction.client.error.ONLY_IN_GUILD(interaction);

await interaction.editReply({ content: 'This support or mail ticket will be closed!' });

const result = await closeTicket(interaction.channelId, interaction.guildId, interaction.channel);
if (!result.success) return interaction.client.error.ONLY_IN_TICKET(interaction, true);
else if (result.embed && result.memberID) {
// Ticket was a mail ticket
try {
const member = await interaction.client.users.fetch(result.memberID);
await member.send({ embeds: [result.embed] });
} catch (e) {
//
}
}
},
};
36 changes: 36 additions & 0 deletions src/commands/delwarn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { ChatInputCommandInteraction, PermissionFlagsBits, SlashCommandBuilder } from 'discord.js';

import { query } from '../utils/postgres';

export default {
data: new SlashCommandBuilder()
.setName('delwarn')
.setDescription('Delete a warning')
.addStringOption((option) => option.setName('warning').setDescription('The ID of the warning').setRequired(true))
.addStringOption((option) => option.setName('reason').setDescription('The reason to delete a warning').setRequired(true).setMaxLength(512).setMinLength(2))
.setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages),

category: 'Moderation',
guildOnly: true,

async execute(interaction: ChatInputCommandInteraction) {
if (!interaction.inCachedGuild()) return interaction.client.error.ONLY_IN_GUILD(interaction);
if (!interaction.member.permissions.has(PermissionFlagsBits.ManageMessages)) return interaction.client.error.NO_PERMISSION(interaction);

const moderator = interaction.member;
const moderatorID = moderator.id;

const warningID = interaction.options.getString('warning', true);
if (!warningID) return interaction.client.error.INVALID_INPUT(interaction, 'warning', 'should be nonzero');

const reason = interaction.options.getString('reason', true);
if (reason.length < 2 || reason.length > 512) return interaction.client.error.INVALID_INPUT(interaction, 'reason', 'should be 512 characters or less');

const { rows: warningRows } = await query('SELECT guild, member, reason FROM warnings WHERE id = $1;', [warningID]);
if (warningRows[0].length === 0 || warningRows[0].guild !== interaction.guildId) return interaction.editReply('This warning does not exists!');
if (warningRows[0].member === moderatorID) return interaction.client.error.INVALID_INPUT(interaction, 'warning', 'should not be a warning of yours');

await query('DELETE FROM warnings WHERE id = $1 AND guild = $2;', [warningID, interaction.guildId]);
return interaction.editReply(`Deleted warning "${warningRows[0].reason}" for ${reason}!`);
},
};
62 changes: 62 additions & 0 deletions src/commands/giveaway.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { ChatInputCommandInteraction, PermissionFlagsBits, SlashCommandBuilder } from 'discord.js';

import { createEmbed } from '../utils/embed';
import { query } from '../utils/postgres';

export default {
data: new SlashCommandBuilder()
.setName('giveaway')
.setDescription('Create a giveaway')
.addStringOption((option) => option.setName('prize').setDescription('The prize of the giveaway').setRequired(true).setMinLength(2).setMaxLength(512))
.addIntegerOption((option) =>
option.setName('minutes').setDescription('The amount of minutes you want to mute this user for').setMinValue(1).setMaxValue(59)
)
.addIntegerOption((option) => option.setName('hours').setDescription('The amount of hours you want to mute this user for').setMinValue(1).setMaxValue(168))
.addIntegerOption((option) => option.setName('winners').setDescription('The amount of winners').setMinValue(1).setMaxValue(25))
.addRoleOption((option) => option.setName('role').setDescription('Required role to enter'))
.setDefaultMemberPermissions(PermissionFlagsBits.ManageEvents),

category: 'General',

async execute(interaction: ChatInputCommandInteraction) {
if (!interaction.inCachedGuild()) return interaction.client.error.ONLY_IN_GUILD(interaction);
if (!interaction.member.permissions.has(PermissionFlagsBits.ManageGuild)) return interaction.client.error.NO_PERMISSION(interaction);

const prize = interaction.options.getString('prize', true);
const winnersAmount = interaction.options.getInteger('winners') || 1;
const roleRequirement = interaction.options.getRole('role');

const hours = interaction.options.getInteger('hours');
if (hours && (hours < 1 || hours > 168)) return interaction.client.error.INVALID_INPUT(interaction, 'hours', 'should be between 1 and 168');
const minutes = interaction.options.getInteger('minutes');
if (minutes && (minutes < 1 || minutes > 59)) return interaction.client.error.INVALID_INPUT(interaction, 'minutes', 'should be between 1 and 59');
const minutesToAdd = (minutes || 0) + (hours || 0) * 60;
if (minutesToAdd < 1 || minutesToAdd > Number.MAX_SAFE_INTEGER)
return interaction.client.error.INVALID_INPUT(interaction, 'hours/minutes', 'should be nonzero');

if (prize.length < 2 || prize.length > 512)
return interaction.client.error.INVALID_INPUT(interaction, 'prize', 'should have a length of max 512 characters');
if (winnersAmount < 1 || winnersAmount > 25) return interaction.client.error.INVALID_INPUT(interaction, 'winners', 'should be between 1 and 25');

const endTime = interaction.createdTimestamp + minutesToAdd * 60 * 1000;

const embed = await createEmbed();
embed.setTitle(`🎉 ${winnersAmount} Winner${winnersAmount > 1 ? 's' : ''} 🎉`);
embed.setDescription(`
Prize: **${prize}**\n\nEnds in <t:${Math.round(endTime / 1000)}:R> (<t:${Math.round(endTime / 1000)}:f>)!
${roleRequirement ? `\nMust have: ${roleRequirement}` : ''}
`);

const sentResponse = await interaction.editReply({ embeds: [embed] });
await sentResponse.react('🎉');

return query('INSERT INTO giveaways (message, channel, endtime, prize, winners, role) VALUES ($1,$2,$3,$4,$5,$6);', [
sentResponse.id,
sentResponse.channelId,
new Date(endTime),
prize,
winnersAmount,
roleRequirement?.id || null,
]);
},
};
61 changes: 61 additions & 0 deletions src/commands/kick.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { ChatInputCommandInteraction, PermissionFlagsBits, SlashCommandBuilder } from 'discord.js';
import { v4 as uuidv4 } from 'uuid';

import { query } from '../utils/postgres';

export default {
data: new SlashCommandBuilder()
.setName('kick')
.setDescription('Kick a member')
.addUserOption((option) => option.setName('member').setDescription('The member to kick').setRequired(true))
.addStringOption((option) => option.setName('reason').setDescription('The reason to kick').setRequired(true).setMaxLength(512).setMinLength(2))
.setDefaultMemberPermissions(PermissionFlagsBits.KickMembers),

category: 'Moderation',
guildOnly: true,

async execute(interaction: ChatInputCommandInteraction) {
if (!interaction.inCachedGuild()) return interaction.client.error.ONLY_IN_GUILD(interaction);
if (!interaction.member.permissions.has(PermissionFlagsBits.KickMembers)) return interaction.client.error.NO_PERMISSION(interaction);

const moderator = interaction.member;
const moderatorID = moderator.id;

const member = interaction.options.getMember('member');
if (!member) return interaction.client.error.INVALID_INPUT(interaction, 'member', 'should be nonzero');
const memberID = member.id;
if (memberID === moderatorID) return interaction.client.error.INVALID_INPUT(interaction, 'member', 'should not be you');

const reason = interaction.options.getString('reason', true);
if (reason.length < 2 || reason.length > 512) return interaction.client.error.INVALID_INPUT(interaction, 'reason', 'should be 512 characters or less');

const positionDifference = moderator.roles.highest.comparePositionTo(member.roles.highest);
if (positionDifference <= 0) return interaction.client.error.NO_PERMISSION(interaction);

if (!member.kickable) return interaction.editReply('I have no permissions to kick this member!');

member.send(`You are kicked for ${reason} in ${interaction.guild.name}, goodbye!`).catch();

try {
await member.kick(reason);
} catch (error) {
return interaction.client.error.UNKNOWN_ERROR(interaction, error);
}

await query('INSERT INTO guilds (id) VALUES ($1) ON CONFLICT DO NOTHING;', [interaction.guildId]);
await query('INSERT INTO users (id) VALUES ($1) ON CONFLICT DO NOTHING;', [memberID]);
await query('INSERT INTO users (id) VALUES ($1) ON CONFLICT DO NOTHING;', [moderatorID]);

const id = uuidv4();

await query('INSERT INTO kicks (id, guild, member, reason, moderator, time) VALUES ($1,$2,$3,$4,$5,$6);', [
id,
interaction.guildId,
memberID,
reason,
moderatorID,
interaction.createdAt,
]);
return interaction.editReply(`Kicked ${member.displayName} for ${reason}!`);
},
};
Loading

0 comments on commit abea965

Please sign in to comment.