From 26a34d3d1c985a6d101f1644d2db76bd0567ec79 Mon Sep 17 00:00:00 2001 From: Serge Date: Thu, 11 Jan 2024 09:32:43 +0600 Subject: [PATCH 001/127] unfinished 0.2 --- .gitignore | 2 +- CONTRIBUTING.md | 2 +- bun.lockb | Bin 60594 -> 60561 bytes example.env | 6 - package.json | 2 +- src/bot.ts | 1 - src/commands/about.ts | 53 ++++++ src/commands/moderation/ban.ts | 69 +++++--- src/commands/moderation/delwarn.ts | 108 ++++++++++++ src/commands/moderation/kick.ts | 72 ++++---- src/commands/moderation/mute.ts | 79 +++++---- src/commands/moderation/purge.ts | 87 +++++----- src/commands/moderation/unban.ts | 63 +++---- src/commands/moderation/unmute.ts | 52 +++--- src/commands/moderation/warn.ts | 73 ++++---- src/commands/moderation/warns.ts | 56 +++--- src/commands/news.ts | 77 +++++++++ src/commands/server/info.ts | 16 ++ src/commands/server/leaderboard.ts | 53 ++++++ src/commands/server/news/add.ts | 102 +++++++++++ src/commands/server/news/edit.ts | 161 ++++++++++++++++++ src/commands/server/news/remove.ts | 61 +++++++ src/commands/server/news/view.ts | 107 ++++++++++++ src/commands/server/subscribe.ts | 55 ++++++ src/commands/serverboard.ts | 97 +++++++++++ src/commands/settings/command/list.ts | 18 +- src/commands/settings/command/toggle.ts | 22 +-- .../settings/leveling/block-channels.ts | 2 +- src/commands/settings/leveling/channel.ts | 2 +- src/commands/settings/leveling/rewards.ts | 119 +++++-------- src/commands/settings/leveling/set.ts | 2 +- src/commands/settings/leveling/toggle.ts | 18 +- src/commands/settings/moderation/logs.ts | 32 ++-- src/commands/settings/news/channel.ts | 43 ++--- src/commands/settings/serverboard/invite.ts | 36 ++-- src/commands/settings/serverboard/reveal.ts | 4 +- src/commands/user/info.ts | 74 ++++++++ src/commands/user/level.ts | 109 ++++++++++++ src/events/easterEggs/Bread.ts | 6 +- src/events/easterEggs/Fan.ts | 2 +- src/events/easterEggs/Fireship.ts | 7 +- src/events/easterEggs/WhoPinged.ts | 2 +- src/events/guildCreate.ts | 18 +- src/events/guildMemberAdd.ts | 44 +++++ src/events/guildMemberRemove.ts | 44 +++++ src/events/interactionCreate.ts | 54 ++++++ src/events/messageCreate.ts | 127 ++++++++++++++ src/handlers/commands.ts | 53 +++--- src/handlers/events.ts | 2 +- src/index.ts | 1 - src/utils/colorGen.ts | 6 +- src/utils/database.ts | 6 +- src/utils/embeds/errorEmbed.ts | 2 +- src/utils/embeds/serverEmbed.ts | 87 ++++------ src/utils/multiReact.ts | 2 +- src/utils/quickSort.ts | 54 ++---- src/utils/randomise.ts | 2 +- src/utils/sendChannelNews.ts | 8 +- src/utils/sendSubscribedNews.ts | 6 +- tsconfig.json | 2 +- 60 files changed, 1866 insertions(+), 604 deletions(-) create mode 100644 src/commands/about.ts create mode 100644 src/commands/moderation/delwarn.ts create mode 100644 src/commands/news.ts create mode 100644 src/commands/server/info.ts create mode 100644 src/commands/server/leaderboard.ts create mode 100644 src/commands/server/news/add.ts create mode 100644 src/commands/server/news/edit.ts create mode 100644 src/commands/server/news/remove.ts create mode 100644 src/commands/server/news/view.ts create mode 100644 src/commands/server/subscribe.ts create mode 100644 src/commands/serverboard.ts create mode 100644 src/commands/user/info.ts create mode 100644 src/commands/user/level.ts create mode 100644 src/events/guildMemberAdd.ts create mode 100644 src/events/guildMemberRemove.ts create mode 100644 src/events/interactionCreate.ts create mode 100644 src/events/messageCreate.ts diff --git a/.gitignore b/.gitignore index 3ec544c..713d500 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ node_modules/ -.env \ No newline at end of file +.env diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4d62514..52b1d59 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,7 +18,7 @@ We use MySQL for the database. You need to set one up to be able to run the bot. - Create a database file called `json.db` -- It is recommended to create a specific user for Nebula only. See [the docs](https://dev.mysql.com/doc/refman/8.0/en/creating-accounts.html) for more information +- It is recommended to create a specific user for Nebula only. See [the docs](https://dev.mysql.com/doc/refman/8.0/en/creating-accounts.html) for more information. ### Setting up .env - Copy the `example.env` file and replace the content with your own credentials inside a file called `.env`. diff --git a/bun.lockb b/bun.lockb index 8c93d62d751d63ca7ae982fa37ef34a0222a3eab..a6d4250375c527ed9001036d147e1e700250a064 100644 GIT binary patch delta 4605 zcmeI0dsJ1$9>@0p2RJ+uA(SI3@8HNo4-XFtcn)SssN|CnDxW+KiVBFLX<$l{1&Xu0 zLRTJ&c|}CT9pZyh%hZIfWulOgZwxhyQqykY{r>jZxVUR|@49!b`)9NKobUX8GxM97 zJ$slv?rSqGYBSvs;MP)=@?@@ArQrN_tlGp7(OtO3+B1)T^V9e#(?5Q7w|gwBd%4YY z#6%B4uor~f09!%8zudd9`#?QlSVPm6Wv0&$6od{>K~TbOm$iw{GILQKgJlJ8#dCsS z1KkU|FLXFBF>}c|?qiI(14=RLWgP@{gq^iuu3?Tr5W!FGWL z!ghdK!lrghGX*KV0KXD;&sdA(xNjQkyXP*>^iMNnCuYomg9Z@lF9^><7r?fI4icv; zT%O^yrzGc;v4ewH2W}S#nqlVMVPA~43|pMk(27MD&C2bOFbpbksG{p zESgt&>6E!{sE@d2Jl#&K+z%@hmK{&;t5w{kGK!`2bQgqR_yvqgtB8XYE%mVtxhdS= zAxv=}xdbUU8;d$l%GDqjDdip`_X6eEXm0S)DOX_WD4r=#AEISH@=o~FUV<>v*v9}Z zo5Kxi9joP4YMt_yTw=-V256O@SblmE3a;s^WmCCfh)%g1K0SOUyvte3?(k0dBE1EH zo+jg6m{|@i>d}N|pcG;GNmjaA%P#RwU!C$RtdsU>{ctRQ*i&n%Y#Cc0g*DdL+D^+X zxY}Q*9EaB`Lh{iQ%7-fo00O+_Y>zuL{tyZr+LC z=ec^Q&UOigMyr6gN3i4?<9ndo126R|@Gc*%;y$eBd5wRVay;HMs%_5GZM4cYuwIm6 zV`j?JPc4l~tF#Lcgea6@tsS*2j;l2~Wi@=^@F{p5p5{%lO2b(W`V~gg4-%;fz<{Mh zY6>tENuwn%UI-}F8!y|WW?&+)1aW}M@fc{Y3yFXdDFaDlAVGO$kSd4TER;M(rC9P- z{1>H|S#mv6inTZBh#Ku59;K>ab8QUoV%F(i7yeZkGL|3a~}CNofFO zG&Aa;98e;q0F`7Qky1xH0M+{&poUelRzuA}9iZ}kfD$P!?IALddMnjCEO{}|10goN zF~o^q53!S;@<(z3DgJH?O+X3G0ZOD~x08WHN&&wk1L=QJn#g59?XO7t$(WHGSCO{> zoq$qrrG~d9?;ohxrkV5S-oloW*7MK3_2=IDKkluAVJmF(w+6=eCATq8tH-A*HKFEPos zzM1hLwPW}7d!OVdy0{lsJZZ5mE)d(JQ>^r7&+IgrAJKd;>y=mR?>SG`?$a+F+waB? zb}o4h{!zy&Kc+UU+uVuO&qX%2viz=1sHIuko+F zRj!I!fBws?ptV^mtcyo!&DJ$n{@`s^6%+1tw&0^Xa~?n1_+|9ig)XB;T7J<~J*5AM z+Jnw*J|*kRqPG{G6+e4zr-i<1ZO%E@y9twvlZHKP2pt}F!oEG{T>O=_UEJc-$%@?L z7ly7VTzqQxLi1%&Z5GX%4-zwq{j6Jl{@T|rr(k09uQ_5(qF$lzV6pvPe5G|m-b(}T z{G5>9Rlno&xDSv1;(2RB{JpLDk*;R~Q_uW%tvb2&@xVJnOQya3#?_AG4Jk*WnoBK~ zcZW<`F8a@NW%{Ef{Xf1vX@1L=%r}2%D8BFFvF`kGzkQo$wYUD}|Duyg*K)7bsbcA= zmCmOhtWsxMt~QqOV9h|+xUoStH#vb8|hax`@uD#tE%|S+jj`Zi2(<>n|*PHhA0Ym+W(NpQd^jZ>p{zKKsb^J?`veO6)%@A0%%+ zXrI+|-pOfO%7A_+qdSj?RbtJep{DpoH#5&nOk0weA%2%xW9l>;b?*=ts52yuu0e#g?05KpGi~vJ`FBlBsz;r+(nFJ<-DPSs?24cZT zobKM`wu*Jgzsf_(Jsh_pN3(e!Yyw5#J)U0fkV79xYXMFT;SI0?B!d)S0I6UeNCOK% z5}?@^g3&++^dJ(f0(A7`0{Svs3g`$!24sU}bTi6CW-*{IH~PH13@(DN zz#-rR-T`G`W(9tbY{U^2K}=kSNel9=3mibI;Xw_1_T9Y1mPI3+&_Ue@vi+U*3PXP zRIG!0H3Wt9T#6sry4O4ED}FLz8=f=XJBNE4FV}tvyBxZ0Viky`2^bcDYxW>s(4cbe zx$vHf@3D$_%b(3{s}h-b32SBO4=me&etK28&4Ji0BOZMc3>xe8b#XixX#OB_$guJIb^-CWeIyz@1@`NF}?+A+)e`pQ?>M2`Q)`|#(vFD6~`^lqGa z**apD562DTxFU_5hndqk|o~V_` z!Khu4dDL{>sw_?{F=*#eTl!im#Vs_q_b$rLQm3WlEzMkv2I@eBn&XBer=xa8_7vh} z%As&r=?6C)c;SHSHU@c)S|QnO%MfM-{Z9oz)kt1YJ$*_hH(>iAn05rh9!&P*plU1+ zDjx%%g?-ebmqDYKURI4N9O_0!%DTu7X~QC!1E|IrWD`bnT$t#|#G2TK)j}_NZdg2Q z1f=*HSS%dyHSlY_usz1LfedFo--;?6l`~|x=wx#acbqo)v2YSersM76mDO2KlSFgcQ#j$7u_VYMcUlJN5v2 z{S3UnFULJ&&gHIWiIAc)um(7wGVu2#j~z6+>v>rfd{ z;q-dwSs(QJ8~DlNIBpW!81&;vi%?MytY}31VN~Nqm7&tJ8_*kI;5%{s^#5u<5pRn~ zi<0L_$_o2$h3h2tpKn9!RC5$(J+lLq+Q2`Lx7|e1Ml-hwRW$SkMzZtJi(W+l$IUl; zaSU0gqET5xKdx~rs%cQCj%2?=ug1WlK^16_uflH93gI2b$)GI6!&3MPU%$GX1=Z1`}c%ycOK^EFM%k1Aho@G_5jd zbkVcB!T}vH6T{1*5LAe17N(HELQJcQfkci)rb0~Z;z(d2wn5B7*dgK(f=U&F0_;UlA*Mt%2x_+fvgR^<%ClhlV}V;|*L+;`UPdG?6^k*$jq4|lCPboK*V=aSh%Q1a5g z3#W2i8aM1aIJ-dT+*dqfTflp3rvEbXwde5f z?!0hmibr{|!=7DlhmJ8VsNOKg&e*k}uwZhDt~C6;>c=PJ4;f`OrnH$(-c6rQ$vffq z!H^Z1xk>X)LGKv2>d!`XzL>k{)|ui>C7srJ-AQ>*H?$?Hh0_c74>9fwJiCA6qrc4Q zX<5GhXk^>Mh?3nyV|O_I6q^Fq>XsU1#`664+YElSEwbn~|F~@3qfpnp*m*Amk1d~EykX~)NRy|0+Z?{z zrz_{=r26Zn)lRC8)o*>K%zv@zjLX8bo%xRh^~r0cBhSO^atqX$;Ce_pod*6NkJ#~mo)Pqy7%ENo51 zCCVPS-rXAS6QHPg`9in#bEc4n*S>YoEK>Ek*KCTP^POS;Lf?;8TsL*R#74Z7*`vC& zY{M^Y@Jjxg0@JEjW1jA?5q?;a$BbOk)YaeR6eXOxn(ih0xpa6#az=C%pYuXiNZIo_ zr(&~H!h#pq$4)!*_`P=~WqzM=^WBe&vKJICYLoT9S-GWLT_)7647BFOmlM8en;pE zIN6@ff?3T;;(!!A`=amN<`dW6X!)FZt^4T>ddfX2lY88l5H> z_bSkjt?qzBYw%3VVfQDwce9(Z>VXw|W32f+sUK%PG>>0U>pI{#*L=NJ6NHO{18~E6 zX&x4`we9xDBdnNC6H0-(VA$HK@UR?ZtJ`6^44OO)7GttOa4*1#J_6s!dfVk^rLytVQ6+#25o2XRDVk9fF_ z{{PIIASL#MUydr6m>pt{hA>YkJSIhD<_zWtmc#ic^LxIY|JQ^8%n*!S*WsA|9f1j; zI~FHhCq@dj9Qu!4U0&dMIO8q6E=Z%*Xz{c+-u$ORa#sDXPn-^>3}*=;=)s6#aQ>L0 z^tijZTOO9<{4M#^8U0hY3=>mF&=goE?cQ+(tUDg^UngrZ+&dm@Urdi4mQPMVL)mbc zaAG{|hOt2M39WmT#{3`0`tZ { new Events(client); - console.log("Starting all commands."); await new Commands(client).registerCommands(); console.log("ちーっす!"); }); diff --git a/src/commands/about.ts b/src/commands/about.ts new file mode 100644 index 0000000..e2c28dc --- /dev/null +++ b/src/commands/about.ts @@ -0,0 +1,53 @@ +import { SlashCommandSubcommandBuilder, EmbedBuilder, type ChatInputCommandInteraction } from "discord.js"; +import { genColor } from "../utils/colorGen.js"; +import { randomise } from "../utils/randomise.js"; + +export default class About { + data: SlashCommandSubcommandBuilder; + deferred: boolean = false; + + constructor() { + this.data = new SlashCommandSubcommandBuilder() + .setName("about") + .setDescription("Shows information about the bot."); + } + + async run(interaction: ChatInputCommandInteraction) { + const client = interaction.client; + const guilds = client.guilds.cache; + const shards = client.shard.count; + const hearts = ["💖", "💝", "💓", "💗", "💘", "💟", "💕", "💞"]; + + const embed = new EmbedBuilder() + .setAuthor({ name: "• About", iconURL: client.user.displayAvatarURL() }) + .setDescription("Nebula is a multiplatform, multipurpose bot with the ability to add extensions to have additional features.") + .setFields( + { + name: "📃 • General", + value: [ + "**Version** 0.2, *Dasshubodo update*", + `**${guilds.size}** guild${guilds.size === 1 ? "" : "s"} • **${shards}** shard${shards === 1 ? "" : "s"}` + ].join("\n") + }, + { + name: "🌌 • Entities involved", + value: [ + "**Head developer**: Goos", + "**Developers**: Golem64, Pigpot, ThatBOI", + "**Designers**: ArtyH, Optix, proJM, Slider_on_the_black", + "**Translators**: Dimkauzh, Golem64, Optix, Sungi, SaFire, ThatBOI", + "And **YOU**, for using Nebula." + ].join("\n") + }, + { + name: "🔗 • Links", + value: "[GitHub](https://www.github.com/NebulaTheBot)・[YouTube](https://www.youtube.com/@NebulaTheBot)・[Instagram](https://instagram.com/NebulaTheBot)・[Mastodon](https://mastodon.online/@NebulaTheBot@mastodon.social)・[Guilded](https://guilded.gg/Nebula)・[Revolt](https://rvlt.gg/28TS9aXy)" + } + ) + .setFooter({ text: `Made by the Nebula team with ${randomise(hearts)}` }) + .setThumbnail(client.user.displayAvatarURL()) + .setColor(genColor(270)); + + await interaction.reply({ embeds: [embed] }); + } +} diff --git a/src/commands/moderation/ban.ts b/src/commands/moderation/ban.ts index 6f6115b..8985521 100644 --- a/src/commands/moderation/ban.ts +++ b/src/commands/moderation/ban.ts @@ -1,14 +1,16 @@ import { SlashCommandSubcommandBuilder, EmbedBuilder, PermissionsBitField, - type ChatInputCommandInteraction, TextChannel, DMChannel + TextChannel, DMChannel, ChannelType, + type Channel, type ChatInputCommandInteraction } from "discord.js"; import { genColor } from "../../utils/colorGen.js"; -import errorEmbed from "../../utils/embeds/errorEmbed.js"; +import { errorEmbed } from "../../utils/embeds/errorEmbed.js"; import { QuickDB } from "quick.db"; import { getSettingsTable } from "../../utils/database.js"; export default class Ban { data: SlashCommandSubcommandBuilder; + deferred: boolean = false; db: QuickDB; constructor(db: QuickDB) { @@ -29,58 +31,75 @@ export default class Ban { async run(interaction: ChatInputCommandInteraction) { const user = interaction.options.getUser("user"); - const reason = interaction.options.getString("reason"); const members = interaction.guild.members.cache; const member = members.get(interaction.member.user.id); const selectedMember = members.get(user.id); const name = selectedMember.nickname ?? user.username; - const dmChannel = (await user.createDM().catch(() => null)) as DMChannel | null;; - const banEmbed = new EmbedBuilder() + if (!member.permissions.has(PermissionsBitField.Flags.BanMembers)) return await interaction.reply({ + embeds: [errorEmbed("You need the **Ban Members** permission to execute this command.")] + }); + + if (selectedMember === member) return await interaction.reply({ + embeds: [errorEmbed("You can't ban yourself.")] + }); + + if (selectedMember.user.id === interaction.client.user.id) return await interaction.reply({ + embeds: [errorEmbed("You can't ban Nebula.")] + }); + + if (!selectedMember.manageable) return await interaction.reply({ + embeds: [errorEmbed(`You can't ban ${name}, because they have a higher role position than Nebula.`)] + }); + + if (member.roles.highest.position < selectedMember.roles.highest.position) return await interaction.reply({ + embeds: [errorEmbed(`You can't ban ${name}, because they have a higher role position than you.`)] + }); + + const reason = interaction.options.getString("reason"); + const embed = new EmbedBuilder() + .setAuthor({ name: `• ${user.username}`, iconURL: user.displayAvatarURL() }) .setTitle(`✅ • Banned ${user.username}`) .setDescription([ `**Moderator**: <@${interaction.user.id}>`, `**Reason**: ${reason ?? "No reason provided"}` ].join("\n")) .setThumbnail(user.displayAvatarURL()) - .setAuthor({ name: `• ${user.username}`, iconURL: user.displayAvatarURL() }) .setFooter({ text: `User ID: ${user.id}` }) .setColor(genColor(100)); const embedDM = new EmbedBuilder() + .setAuthor({ name: `• ${user.username}`, iconURL: user.displayAvatarURL() }) .setTitle(`🔨 • You were banned`) .setDescription([ `**Moderator**: ${interaction.user.username}`, `**Reason**: ${reason ?? "No reason provided"}` ].join("\n")) .setThumbnail(user.displayAvatarURL()) - .setAuthor({ name: `• ${user.username}`, iconURL: user.displayAvatarURL() }) .setFooter({ text: `User ID: ${user.id}` }) .setColor(genColor(0)); - if (!member.permissions.has(PermissionsBitField.Flags.BanMembers)) - return await interaction.followUp({ embeds: [errorEmbed("You need the **Ban Members** permission to execute this command.")] }); - if (selectedMember === member) - return await interaction.followUp({ embeds: [errorEmbed("You can't ban yourself")] }); - if (selectedMember.user.id === interaction.client.user.id) - return await interaction.followUp({ embeds: [errorEmbed("You can't ban Nebula.")] }); - if (!selectedMember.manageable) - return await interaction.followUp({ embeds: [errorEmbed(`You can't ban ${name}, because they have a higher role position than Nebula.`)] }); - if (member.roles.highest.position < selectedMember.roles.highest.position) - return await interaction.followUp({ embeds: [errorEmbed(`You can't ban ${name}, because they have a higher role position than you.`)] }); + const logChannel = await (await getSettingsTable(this.db)) + ?.get(`${interaction.guild.id}.logChannel`) + .then((channel: string | null) => channel ?? null) + .catch(() => null); - const db = this.db; - const settingsTable = await getSettingsTable(db); - const logChannel = await settingsTable?.get(`${interaction.guild.id}.logChannel`).then( - (channel: string | null) => channel ?? null - ).catch(() => null); if (logChannel) { - const channel = (await interaction.guild.channels.cache.get(logChannel).fetch()) as TextChannel; - await channel.send({ embeds: [banEmbed] }); + const channel = await interaction.guild.channels.cache + .get(logChannel) + .fetch() + .then((channel: Channel) => { + if (channel.type != ChannelType.GuildText) return null; + return channel as TextChannel; + }) + .catch(() => null); + + if (channel) await channel.send({ embeds: [embed] }); } + const dmChannel = (await user.createDM().catch(() => null)) as DMChannel | null; if (dmChannel) await dmChannel.send({ embeds: [embedDM] }); await selectedMember.ban({ reason: reason ?? undefined }); - await interaction.followUp({ embeds: [banEmbed] }); + await interaction.reply({ embeds: [embed] }); } } diff --git a/src/commands/moderation/delwarn.ts b/src/commands/moderation/delwarn.ts new file mode 100644 index 0000000..e8d8ea7 --- /dev/null +++ b/src/commands/moderation/delwarn.ts @@ -0,0 +1,108 @@ +import { + SlashCommandSubcommandBuilder, EmbedBuilder, PermissionsBitField, + TextChannel, DMChannel, ChannelType, + type Channel, type ChatInputCommandInteraction +} from "discord.js"; +import { genColor } from "../../utils/colorGen.js"; +import { errorEmbed } from "../../utils/embeds/errorEmbed.js"; +import { QuickDB } from "quick.db"; +import { getModerationTable, getSettingsTable } from "../../utils/database.js"; + +export default class Delwarn { + data: SlashCommandSubcommandBuilder; + db: QuickDB; + + constructor(db: QuickDB) { + this.db = db; + this.data = new SlashCommandSubcommandBuilder() + .setName("delwarn") + .setDescription("Removes a warning from a user.") + .addUserOption(user => user + .setName("user") + .setDescription("The user that you want to free from the warning.") + .setRequired(true) + ) + .addNumberOption(string => string + .setName("id") + .setDescription("The id of the warn.") + .setRequired(true) + ); + } + + async run(interaction: ChatInputCommandInteraction) { + const user = interaction.options.getUser("user"); + const members = interaction.guild.members.cache; + const member = members.get(interaction.member.user.id); + const selectedMember = members.get(user.id); + const name = selectedMember.nickname ?? user.username; + const id = interaction.options.getNumber("id", true); + const warns = await (await getModerationTable(this.db)) + ?.get(`${interaction.guild.id}.${user.id}.warns`) + .then(warns => warns as any[] ?? []) + .catch(() => []); + const newWarns = warns.filter(warn => warn.id !== id); + + if (!member.permissions.has(PermissionsBitField.Flags.ModerateMembers)) return await interaction.followUp({ + embeds: [errorEmbed("You need the **Moderate Members** permission to execute this command.")] + }); + + if (selectedMember === member) return await interaction.followUp({ embeds: [errorEmbed("You can't remove a warn from yourself.")] }); + + if (newWarns.length === warns.length) return await interaction.followUp({ + embeds: [errorEmbed(`There is no warn with the id of ${id}.`)] + }); + + if (!selectedMember.manageable) return await interaction.followUp({ + embeds: [errorEmbed(`You can't unwarn ${name}, because they have a higher role position than Nebula.`)] + }); + + if (member.roles.highest.position < selectedMember.roles.highest.position) return await interaction.followUp({ + embeds: [errorEmbed(`You can't unwarn ${name}, because they have a higher role position than you.`)] + }); + + const embed = new EmbedBuilder() + .setAuthor({ name: `• ${user.username}`, iconURL: user.displayAvatarURL() }) + .setTitle(`✅ • Removed warning`) + .setDescription([ + `**Moderator**: <@${member.id}>`, + `**Original reason**: ${newWarns.find(warn => warn.id === id)?.reason ?? "No reason provided"}` + ].join("\n")) + .setThumbnail(user.displayAvatarURL()) + .setFooter({ text: `User ID: ${user.id}` }) + .setColor(genColor(100)); + + const embedDM = new EmbedBuilder() + .setAuthor({ name: `• ${user.username}`, iconURL: user.displayAvatarURL() }) + .setTitle(`🤝 • Your warning was removed`) + .setDescription([ + `**Moderator**: ${member.user.username}`, + `**Original reason**: ${newWarns.find(warn => warn.id === id)?.reason ?? "No reason provided"}` + ].join("\n")) + .setThumbnail(user.displayAvatarURL()) + .setFooter({ text: `User ID: ${user.id}` }) + .setColor(genColor(100)); + + const logChannel = await (await getSettingsTable(this.db)) + ?.get(`${interaction.guild.id}.logChannel`) + .then((channel: string | null) => channel) + .catch(() => null); + + if (logChannel) { + const channel = await interaction.guild.channels.cache + .get(logChannel) + .fetch() + .then((channel: Channel) => { + if (channel.type != ChannelType.GuildText) return null; + return channel as TextChannel; + }) + .catch(() => null); + + if (channel) await channel.send({ embeds: [embed] }); + } + + const dmChannel = (await user.createDM().catch(() => null)) as DMChannel | null; + if (dmChannel) await dmChannel.send({ embeds: [embedDM] }); + await (await getModerationTable(this.db))?.set(`${interaction.guild.id}.${user.id}.warns`, newWarns); + await interaction.followUp({ embeds: [embed] }); + } +} diff --git a/src/commands/moderation/kick.ts b/src/commands/moderation/kick.ts index 2a433cd..fdbfdef 100644 --- a/src/commands/moderation/kick.ts +++ b/src/commands/moderation/kick.ts @@ -1,10 +1,10 @@ import { SlashCommandSubcommandBuilder, EmbedBuilder, PermissionsBitField, - type ChatInputCommandInteraction, TextChannel, DMChannel, - Channel, ChannelType + TextChannel, DMChannel, ChannelType, + type Channel, type ChatInputCommandInteraction } from "discord.js"; import { genColor } from "../../utils/colorGen.js"; -import errorEmbed from "../../utils/embeds/errorEmbed.js"; +import { errorEmbed } from "../../utils/embeds/errorEmbed.js"; import { QuickDB } from "quick.db"; import { getSettingsTable } from "../../utils/database.js"; @@ -30,63 +30,75 @@ export default class Kick { async run(interaction: ChatInputCommandInteraction) { const user = interaction.options.getUser("user"); - const reason = interaction.options.getString("reason"); const members = interaction.guild.members.cache; const member = members.get(interaction.member.user.id); const selectedMember = members.get(user.id); const name = selectedMember.nickname ?? user.username; - const dmChannel = (await user.createDM().catch(() => null)) as DMChannel | null;; - const kickEmbed = new EmbedBuilder() + if (!member.permissions.has(PermissionsBitField.Flags.KickMembers)) return await interaction.followUp({ + embeds: [errorEmbed("You need the **Kick Members** permission to execute this command.")] + }); + + if (selectedMember === member) return await interaction.followUp({ + embeds: [errorEmbed("You can't kick yourself.")] + }); + + if (selectedMember.user.id === interaction.client.user.id) return await interaction.followUp({ + embeds: [errorEmbed("You can't kick Nebula.")] + }); + + if (!selectedMember.manageable) return await interaction.followUp({ + embeds: [errorEmbed(`You can't kick ${name}, because they have a higher role position than Nebula.`)] + }); + + if (member.roles.highest.position < selectedMember.roles.highest.position) return await interaction.followUp({ + embeds: [errorEmbed(`You can't kick ${name}, because they have a higher role position than you.`)] + }); + + const reason = interaction.options.getString("reason"); + const embed = new EmbedBuilder() + .setAuthor({ name: `• ${user.username}`, iconURL: user.displayAvatarURL() }) .setTitle(`✅ • Kicked <@${user.id}>`) .setDescription([ `**Moderator**: <@${interaction.user.id}>`, `**Reason**: ${reason ?? "No reason provided"}` ].join("\n")) .setThumbnail(user.displayAvatarURL()) - .setAuthor({ name: `• ${user.username}`, iconURL: user.displayAvatarURL() }) .setFooter({ text: `User ID: ${user.id}` }) .setColor(genColor(100)); const embedDM = new EmbedBuilder() - .setTitle(`👢 • You were kicked`) + .setAuthor({ name: `• ${user.username}`, iconURL: user.displayAvatarURL() }) + .setTitle(`🥾 • You were kicked`) .setDescription([ `**Moderator**: ${interaction.user.username}`, `**Reason**: ${reason ?? "No reason provided"}` ].join("\n")) .setThumbnail(user.displayAvatarURL()) - .setAuthor({ name: `• ${user.username}`, iconURL: user.displayAvatarURL() }) .setFooter({ text: `User ID: ${user.id}` }) .setColor(genColor(0)); - if (!member.permissions.has(PermissionsBitField.Flags.KickMembers)) - return await interaction.followUp({ embeds: [errorEmbed("You need the **Kick Members** permission to execute this command.")] }); - if (selectedMember === member) - return await interaction.followUp({ embeds: [errorEmbed("You can't kick yourself.")] }); - if (selectedMember.user.id === interaction.client.user.id) - return await interaction.followUp({ embeds: [errorEmbed("You can't kick Nebula.")] }); - if (!selectedMember.manageable) - return await interaction.followUp({ embeds: [errorEmbed(`You can't kick ${name}, because they have a higher role position than Nebula.`)] }); - if (member.roles.highest.position < selectedMember.roles.highest.position) - return await interaction.followUp({ embeds: [errorEmbed(`You can't kick ${name}, because they have a higher role position than you.`)] }); + const logChannel = await (await getSettingsTable(this.db)) + ?.get(`${interaction.guild.id}.logChannel`) + .then((channel: string | null) => channel ?? null) + .catch(() => null); - const db = this.db; - const settingsTable = await getSettingsTable(db); - const logChannel = await settingsTable?.get(`${interaction.guild.id}.logChannel`).then( - (channel: string | null) => channel ?? null - ).catch(() => null); if (logChannel) { - const channel = await interaction.guild.channels.cache.get(logChannel).fetch().then( - (channel: Channel | null) => { + const channel = await interaction.guild.channels.cache + .get(logChannel) + .fetch() + .then((channel: Channel | null) => { if (channel.type != ChannelType.GuildText) return null; return channel as TextChannel; - } - ).catch(() => null); - if (channel) await channel.send({ embeds: [kickEmbed] }); + }) + .catch(() => null); + + if (channel) await channel.send({ embeds: [embed] }); } + const dmChannel = (await user.createDM().catch(() => null)) as DMChannel | null; if (dmChannel) await dmChannel.send({ embeds: [embedDM] }); await selectedMember.kick(reason ?? undefined); - await interaction.followUp({ embeds: [kickEmbed] }); + await interaction.followUp({ embeds: [embed] }); } } diff --git a/src/commands/moderation/mute.ts b/src/commands/moderation/mute.ts index 9ac2d47..9ac51da 100644 --- a/src/commands/moderation/mute.ts +++ b/src/commands/moderation/mute.ts @@ -1,13 +1,13 @@ import { SlashCommandSubcommandBuilder, EmbedBuilder, PermissionsBitField, - type ChatInputCommandInteraction, TextChannel, DMChannel, - Channel, ChannelType + TextChannel, DMChannel, ChannelType, + type Channel, type ChatInputCommandInteraction } from "discord.js"; -import ms from "ms"; import { genColor } from "../../utils/colorGen.js"; -import errorEmbed from "../../utils/embeds/errorEmbed.js"; +import { errorEmbed } from "../../utils/embeds/errorEmbed.js"; import { getSettingsTable } from "../../utils/database.js"; import { QuickDB } from "quick.db"; +import ms from "ms"; export default class Mute { data: SlashCommandSubcommandBuilder; @@ -42,11 +42,33 @@ export default class Mute { const selectedMember = members.get(user.id); const name = selectedMember.nickname ?? user.username; - const ISOduration = new Date(ms(duration)).toISOString(); - const time = new Date(Date.parse(new Date().toISOString()) + Date.parse(ISOduration)).toISOString(); + if (!member.permissions.has(PermissionsBitField.Flags.MuteMembers)) return await interaction.followUp({ + embeds: [errorEmbed("You need the **Mute Members** permission to execute this command.")] + }); - const dmChannel = (await user.createDM().catch(() => null)) as DMChannel | null; - const muteEmbed = new EmbedBuilder() + if (selectedMember === member) return await interaction.followUp({ + embeds: [errorEmbed("You can't mute yourself.")] + }); + + if (selectedMember.user.bot) return await interaction.followUp({ + embeds: [errorEmbed(`You can't mute ${name}, because it's a bot.`)] + }); + + if (!selectedMember.manageable) return await interaction.followUp({ + embeds: [errorEmbed(`You can't mute ${name}, because they have a higher role position than Nebula.`)] + }); + + if (member.roles.highest.position < selectedMember.roles.highest.position) return await interaction.followUp({ + embeds: [errorEmbed(`You can't mute ${name}, because they have a higher role position than you.`)] + }); + + if (!ms(duration) || ms(duration) > ms("28d")) return await interaction.followUp({ + embeds: [errorEmbed("The duration is invalid or is above the 28 day limit.")] + }); + + const time = new Date(Date.parse(new Date().toISOString()) + Date.parse(new Date(ms(duration)).toISOString())).toISOString(); + const embed = new EmbedBuilder() + .setAuthor({ name: `• ${user.username}`, iconURL: user.displayAvatarURL() }) .setTitle(`✅ • Muted ${user.username}`) .setDescription([ `**Moderator**: <@${member.id}>`, @@ -55,9 +77,10 @@ export default class Mute { ].join("\n")) .setFooter({ text: `User ID: ${user.id}` }) .setThumbnail(user.displayAvatarURL()) - .setAuthor({ name: `• ${user.username}`, iconURL: user.displayAvatarURL() }) .setColor(genColor(100)); + const embedDM = new EmbedBuilder() + .setAuthor({ name: `• ${user.username}`, iconURL: user.displayAvatarURL() }) .setTitle(`🤐 • You were muted`) .setDescription([ `**Moderator**: ${member.user.username}`, @@ -65,40 +88,30 @@ export default class Mute { `**Reason**: ${interaction.options.getString("reason") ?? "No reason provided"}` ].join("\n")) .setThumbnail(user.displayAvatarURL()) - .setAuthor({ name: `• ${user.username}`, iconURL: user.displayAvatarURL() }) .setFooter({ text: `User ID: ${user.id}` }) .setColor(genColor(0)); - if (!member.permissions.has(PermissionsBitField.Flags.MuteMembers)) - return await interaction.followUp({ embeds: [errorEmbed("You need the **Mute Members** permission to execute this command.")] }); - if (selectedMember === member) - return await interaction.followUp({ embeds: [errorEmbed("You can't mute yourself.")] }); - if (selectedMember.user.bot) - return await interaction.followUp({ embeds: [errorEmbed(`You can't mute ${name}, because it's a bot.`)] }); - if (!selectedMember.manageable) - return await interaction.followUp({ embeds: [errorEmbed(`You can't mute ${name}, because they have a higher role position than Nebula.`)] }); - if (member.roles.highest.position < selectedMember.roles.highest.position) - return await interaction.followUp({ embeds: [errorEmbed(`You can't mute ${name}, because they have a higher role position than you.`)] }); - if (!ms(duration) || ms(duration) > ms("28d")) - return await interaction.followUp({ embeds: [errorEmbed("The duration is invalid or is above the 28 day limit.")] }); + const logChannel = await (await getSettingsTable(this.db)) + ?.get(`${interaction.guild.id}.logChannel`) + .then((channel: string | null) => channel) + .catch(() => null); - const db = this.db; - const settingsTable = await getSettingsTable(db); - const logChannel = await settingsTable?.get(`${interaction.guild.id}.logChannel`).then( - (channel: string | null) => channel - ).catch(() => null); if (logChannel) { - const channel = await interaction.guild.channels.cache.get(logChannel).fetch().then( - (channel: Channel) => { + const channel = await interaction.guild.channels.cache + .get(logChannel) + .fetch() + .then((channel: Channel) => { if (channel.type != ChannelType.GuildText) return null; return channel as TextChannel; - } - ).catch(() => null); - if (channel) await channel.send({ embeds: [muteEmbed] }); + }) + .catch(() => null); + + if (channel) await channel.send({ embeds: [embed] }); } + const dmChannel = (await user.createDM().catch(() => null)) as DMChannel | null; if (dmChannel) await dmChannel.send({ embeds: [embedDM] }); await selectedMember.edit({ communicationDisabledUntil: time }); - await interaction.followUp({ embeds: [muteEmbed] }); + await interaction.followUp({ embeds: [embed] }); } } diff --git a/src/commands/moderation/purge.ts b/src/commands/moderation/purge.ts index 94f2e49..a97275c 100644 --- a/src/commands/moderation/purge.ts +++ b/src/commands/moderation/purge.ts @@ -1,10 +1,10 @@ import { SlashCommandSubcommandBuilder, EmbedBuilder, PermissionsBitField, - ChannelType, type ChatInputCommandInteraction, TextChannel, - Channel + ChannelType, TextChannel, type Channel, + type ChatInputCommandInteraction } from "discord.js"; import { genColor } from "../../utils/colorGen.js"; -import errorEmbed from "../../utils/embeds/errorEmbed.js"; +import { errorEmbed } from "../../utils/embeds/errorEmbed.js"; import { getSettingsTable } from "../../utils/database.js"; import { QuickDB } from "quick.db"; @@ -17,34 +17,37 @@ export default class Purge { this.data = new SlashCommandSubcommandBuilder() .setName("purge") .setDescription("Purges messages.") - .addNumberOption((number) => - number.setName("amount").setDescription("The amount of messages that you want to purge.").setRequired(true) + .addNumberOption(number => number + .setName("amount") + .setDescription("The amount of messages that you want to purge.") + .setRequired(true) ) - .addChannelOption((channel) => - channel - .setName("channel") - .setDescription("The channel that you want to purge (if not the current channel)") - .addChannelTypes(ChannelType.GuildText, ChannelType.PublicThread, ChannelType.PrivateThread) + .addChannelOption(channel => channel + .setName("channel") + .setDescription("The channel that you want to purge. Leave empty if you want to purge the current channel.") + .addChannelTypes(ChannelType.GuildText, ChannelType.PublicThread, ChannelType.PrivateThread) ); } async run(interaction: ChatInputCommandInteraction) { const amount = interaction.options.getNumber("amount"); - if (amount > 100) { - return await interaction.followUp({ - embeds: [errorEmbed("You can only purge up to 100 messages at a time.")], - }); - } - if (amount < 1) { - return await interaction.followUp({ - embeds: [errorEmbed("You must purge at least 1 message.")], - }); - } - const channelOption = interaction.options.getChannel("channel"); const member = interaction.guild.members.cache.get(interaction.member.user.id); - const channel = interaction.guild.channels.cache.get(interaction.channel.id ?? channelOption.id); - const purgeEmbed = new EmbedBuilder() + if (amount > 100) return await interaction.followUp({ + embeds: [errorEmbed("You can only purge up to 100 messages at a time.")], + }); + + if (amount < 1) return await interaction.followUp({ + embeds: [errorEmbed("You must purge at least 1 message.")], + }); + + if (!member.permissions.has(PermissionsBitField.Flags.ManageMessages)) return await interaction.followUp({ + embeds: [errorEmbed("You need the **Manage Messages** permission to execute this command.")], + }); + + const channelOption = interaction.options.getChannel("channel"); + const channel = interaction.guild.channels.cache.get(interaction.channel.id ?? channelOption.id); + const embed = new EmbedBuilder() .setTitle(`✅ • Purged ${amount} messages.`) .setDescription([ `**Moderator**: <@${member.id}>`, @@ -52,32 +55,28 @@ export default class Purge { ].join("\n")) .setColor(genColor(100)); - if (!member.permissions.has(PermissionsBitField.Flags.ManageMessages)) { - return await interaction.followUp({ - embeds: [errorEmbed("You need the **Manage Messages** permission to execute this command.")], - }); - } - if (channel.type === ChannelType.GuildText && ChannelType.PublicThread && ChannelType.PrivateThread) { - channel == interaction.channel - ? await channel.bulkDelete(amount + 1, true) - : await channel.bulkDelete(amount, true); - } + if (channel.type === ChannelType.GuildText && ChannelType.PublicThread && ChannelType.PrivateThread) channel == interaction.channel + ? await channel.bulkDelete(amount + 1, true) + : await channel.bulkDelete(amount, true); + + const logChannel = await (await getSettingsTable(this.db)) + ?.get(`${interaction.guild.id}.logChannel`) + .then((channel: string | null) => channel) + .catch(() => null); - const db = this.db; - const settingsTable = await getSettingsTable(db); - const logChannel = await settingsTable?.get(`${interaction.guild.id}.logChannel`).then( - (channel: string | null) => channel - ).catch(() => null); if (logChannel) { - const channel = await interaction.guild.channels.cache.get(logChannel).fetch().then( - (channel: Channel) => { + const channel = await interaction.guild.channels.cache + .get(logChannel) + .fetch() + .then((channel: Channel) => { if (channel.type != ChannelType.GuildText) return null; return channel as TextChannel; - } - ).catch(() => null); - if (channel) await channel.send({ embeds: [purgeEmbed] }); + }) + .catch(() => null); + + if (channel) await channel.send({ embeds: [embed] }); } - await interaction.followUp({ embeds: [purgeEmbed] }); + await interaction.followUp({ embeds: [embed] }); } } diff --git a/src/commands/moderation/unban.ts b/src/commands/moderation/unban.ts index 4633dd3..bbcafe3 100644 --- a/src/commands/moderation/unban.ts +++ b/src/commands/moderation/unban.ts @@ -1,10 +1,10 @@ import { PermissionsBitField, EmbedBuilder, SlashCommandSubcommandBuilder, - type ChatInputCommandInteraction, TextChannel, DMChannel, - Channel, ChannelType + TextChannel, DMChannel, ChannelType, + type Channel, type ChatInputCommandInteraction } from "discord.js"; import { genColor } from "../../utils/colorGen.js"; -import errorEmbed from "../../utils/embeds/errorEmbed.js"; +import { errorEmbed } from "../../utils/embeds/errorEmbed.js"; import { getSettingsTable } from "../../utils/database.js"; import { QuickDB } from "quick.db"; @@ -27,52 +27,53 @@ export default class Unban { async run(interaction: ChatInputCommandInteraction) { const userID = interaction.options.getString("user"); const member = interaction.guild.members.cache.get(interaction.member.user.id); - const bannedMembers = interaction.guild.bans.cache; - const bannedMemberArray = bannedMembers.map(user => user.user); - const selectedBannedMember = bannedMemberArray.filter(user => user.id === userID)[0]; + const selectedBannedMember = (interaction.guild.bans.cache.map(user => user.user)).filter(user => user.id === userID)[0]; - const dmChannel = (await selectedBannedMember.createDM().catch(() => null)) as DMChannel | null;; - const unbanEmbed = new EmbedBuilder() - .setTitle(`✅ • Unbanned ${member.user.username}`) - .setDescription([ - `**Moderator**: <@${member.user.id}>` - ].join("\n")) + if (!member.permissions.has(PermissionsBitField.Flags.BanMembers)) return await interaction.followUp({ + embeds: [errorEmbed("You need the **Ban Members** permission to execute this command.")] + }); + + if (selectedBannedMember == undefined) return await interaction.followUp({ + embeds: [errorEmbed("You can't unban this user because they were never banned.")] + }); + + const embed = new EmbedBuilder() .setAuthor({ name: member.user.username, iconURL: member.user.displayAvatarURL() }) + .setTitle(`✅ • Unbanned ${member.user.username}`) + .setDescription(`**Moderator**: <@${member.user.id}>`) .setThumbnail(selectedBannedMember.displayAvatarURL()) .setFooter({ text: `User ID: ${userID}` }) .setColor(genColor(100)); + const embedDM = new EmbedBuilder() - .setTitle(`🤝 • You were unbanned`) - .setDescription([ - `**Moderator**: ${member.user.username}` - ].join("\n")) .setAuthor({ name: member.user.username, iconURL: member.user.displayAvatarURL() }) + .setTitle(`🤝 • You were unbanned`) + .setDescription(`**Moderator**: ${member.user.username}`) .setThumbnail(selectedBannedMember.displayAvatarURL()) .setFooter({ text: `User ID: ${userID}` }) .setColor(genColor(100)); - if (!member.permissions.has(PermissionsBitField.Flags.BanMembers)) - return await interaction.followUp({ embeds: [errorEmbed("You need the **Ban Members** permission to execute this command.")] }); - if (selectedBannedMember == undefined) - return await interaction.followUp({ embeds: [errorEmbed("You can't unban this user because they were never banned.")] }); + const logChannel = await (await getSettingsTable(this.db)) + ?.get(`${interaction.guild.id}.logChannel`) + .then((channel: string | null) => channel) + .catch(() => null); - const db = this.db; - const settingsTable = await getSettingsTable(db); - const logChannel = await settingsTable?.get(`${interaction.guild.id}.logChannel`).then( - (channel: string | null) => channel - ).catch(() => null); if (logChannel) { - const channel = await interaction.guild.channels.cache.get(logChannel).fetch().then( - (channel: Channel) => { + const channel = await interaction.guild.channels.cache + .get(logChannel) + .fetch() + .then((channel: Channel) => { if (channel.type != ChannelType.GuildText) return null; return channel as TextChannel; - } - ).catch(() => null); - if (channel) await channel.send({ embeds: [unbanEmbed] }); + }) + .catch(() => null); + + if (channel) await channel.send({ embeds: [embed] }); } + const dmChannel = (await selectedBannedMember.createDM().catch(() => null)) as DMChannel | null; if (dmChannel) await dmChannel.send({ embeds: [embedDM] }); await interaction.guild.members.unban(userID); - await interaction.followUp({ embeds: [unbanEmbed] }); + await interaction.followUp({ embeds: [embed] }); } } diff --git a/src/commands/moderation/unmute.ts b/src/commands/moderation/unmute.ts index 72d16ab..9215c9b 100644 --- a/src/commands/moderation/unmute.ts +++ b/src/commands/moderation/unmute.ts @@ -1,10 +1,10 @@ import { SlashCommandSubcommandBuilder, EmbedBuilder, PermissionsBitField, - type ChatInputCommandInteraction, TextChannel, DMChannel, - Channel, ChannelType + TextChannel, DMChannel, ChannelType, + type Channel, type ChatInputCommandInteraction } from "discord.js"; import { genColor } from "../../utils/colorGen.js"; -import errorEmbed from "../../utils/embeds/errorEmbed.js"; +import { errorEmbed } from "../../utils/embeds/errorEmbed.js"; import { getSettingsTable } from "../../utils/database.js"; import { QuickDB } from "quick.db"; @@ -25,54 +25,56 @@ export default class Unmute { } async run(interaction: ChatInputCommandInteraction) { - const user = interaction.options.getUser("user"); const members = interaction.guild.members.cache; - const member = members.get(interaction.member.user.id); - const selectedMember = members.get(user.id); - const name = selectedMember.nickname ?? user.username; + if (!members.get(interaction.member.user.id).permissions.has(PermissionsBitField.Flags.MuteMembers)) + return await interaction.followUp({ + embeds: [errorEmbed("You need the **Mute Members** permission to execute this command.")] + }); - const dmChannel = (await user.createDM().catch(() => null)) as DMChannel | null;; - let unmuteEmbed = new EmbedBuilder() + const user = interaction.options.getUser("user"); + const embed = new EmbedBuilder() + .setAuthor({ name: `• ${members.get(user.id).nickname ?? user.username}`, iconURL: user.displayAvatarURL() }) .setTitle(`✅ • Unmuted ${user.username}`) .setDescription([ `**Moderator**: <@${interaction.user.id}>`, `**Date**: ` ].join("\n")) - .setAuthor({ name: `• ${name}`, iconURL: user.displayAvatarURL() }) .setThumbnail(user.displayAvatarURL()) .setFooter({ text: `User ID: ${user.id}` }) .setColor(genColor(100)); + const embedDM = new EmbedBuilder() + .setAuthor({ name: `• ${user.username}`, iconURL: user.displayAvatarURL() }) .setTitle(`🤝 • You were unmuted`) .setDescription([ `**Moderator**: ${interaction.user.username}`, `**Date**: ` ].join("\n")) .setThumbnail(user.displayAvatarURL()) - .setAuthor({ name: `• ${user.username}`, iconURL: user.displayAvatarURL() }) .setFooter({ text: `User ID: ${user.id}` }) .setColor(genColor(100)); - if (!member.permissions.has(PermissionsBitField.Flags.MuteMembers)) - return await interaction.followUp({ embeds: [errorEmbed("You need the **Mute Members** permission to execute this command.")] }); + const logChannel = await (await getSettingsTable(this.db)) + ?.get(`${interaction.guild.id}.logChannel`) + .then((channel: string | null) => channel) + .catch(() => null); - const db = this.db; - const settingsTable = await getSettingsTable(db); - const logChannel = await settingsTable?.get(`${interaction.guild.id}.logChannel`).then( - (channel: string | null) => channel - ).catch(() => null); if (logChannel) { - const channel = await interaction.guild.channels.cache.get(logChannel).fetch().then( - (channel: Channel) => { + const channel = await interaction.guild.channels.cache + .get(logChannel) + .fetch() + .then((channel: Channel) => { if (channel.type != ChannelType.GuildText) return null; return channel as TextChannel; - } - ).catch(() => null); - if (channel) await channel.send({ embeds: [unmuteEmbed] }); + }) + .catch(() => null); + + if (channel) await channel.send({ embeds: [embed] }); } + const dmChannel = (await user.createDM().catch(() => null)) as DMChannel | null; if (dmChannel) await dmChannel.send({ embeds: [embedDM] }); - await selectedMember.edit({ communicationDisabledUntil: null }); - await interaction.followUp({ embeds: [unmuteEmbed] }); + await members.get(user.id).edit({ communicationDisabledUntil: null }); + await interaction.followUp({ embeds: [embed] }); } } diff --git a/src/commands/moderation/warn.ts b/src/commands/moderation/warn.ts index fa3d9cd..56abc22 100644 --- a/src/commands/moderation/warn.ts +++ b/src/commands/moderation/warn.ts @@ -1,10 +1,10 @@ import { SlashCommandSubcommandBuilder, EmbedBuilder, PermissionsBitField, - type ChatInputCommandInteraction, TextChannel, DMChannel, - Channel, ChannelType + TextChannel, DMChannel, ChannelType, + type Channel, type ChatInputCommandInteraction } from "discord.js"; import { genColor } from "../../utils/colorGen.js"; -import errorEmbed from "../../utils/embeds/errorEmbed.js"; +import { errorEmbed } from "../../utils/embeds/errorEmbed.js"; import { QuickDB } from "quick.db"; import { getModerationTable, getSettingsTable } from "../../utils/database.js"; @@ -29,15 +29,25 @@ export default class Warn { } async run(interaction: ChatInputCommandInteraction) { - const db = this.db; - const modTable = await getModerationTable(db); - const user = interaction.options.getUser("user"); const members = interaction.guild.members.cache; const member = members.get(interaction.member.user.id); const selectedMember = members.get(user.id); const name = selectedMember.nickname ?? user.username; - const dmChannel = (await user.createDM().catch(() => null)) as DMChannel | null;; + + if (!member.permissions.has(PermissionsBitField.Flags.ModerateMembers)) return await interaction.followUp({ + embeds: [errorEmbed("You need the **Moderate Members** permission to execute this command.")] + }); + + if (selectedMember === member) return await interaction.followUp({ embeds: [errorEmbed("You can't warn yourself.")] }); + + if (!selectedMember.manageable) return await interaction.followUp({ + embeds: [errorEmbed(`You can't warn ${name}, because they have a higher role position than Nebula.`)] + }); + + if (member.roles.highest.position < selectedMember.roles.highest.position) return await interaction.followUp({ + embeds: [errorEmbed(`You can't warn ${name}, because they have a higher role position than you.`)] + }); const newWarn = { id: Date.now(), @@ -46,55 +56,48 @@ export default class Warn { reason: interaction.options.getString("reason") ?? "No reason provided" }; - const warnEmbed = new EmbedBuilder() + const embed = new EmbedBuilder() + .setAuthor({ name: `• ${user.username}`, iconURL: user.displayAvatarURL() }) .setTitle(`✅ • Warned ${user.username}`) .setDescription([ `**Moderator**: <@${member.id}>`, `**Reason**: ${interaction.options.getString("reason") ?? "No reason provided"}` ].join("\n")) .setThumbnail(user.displayAvatarURL()) - .setAuthor({ name: `• ${user.username}`, iconURL: user.displayAvatarURL() }) .setFooter({ text: `User ID: ${user.id}` }) .setColor(genColor(100)); + const embedDM = new EmbedBuilder() - .setTitle(`😡 • You were warned`) + .setAuthor({ name: `• ${user.username}`, iconURL: user.displayAvatarURL() }) + .setTitle("⚠️ • You were warned") .setDescription([ - `**Moderator**: ${member.user.username}`, + `**Moderator**: <@${member.id}>`, `**Reason**: ${interaction.options.getString("reason") ?? "No reason provided"}` ].join("\n")) .setThumbnail(user.displayAvatarURL()) - .setAuthor({ name: `• ${user.username}`, iconURL: user.displayAvatarURL() }) .setFooter({ text: `User ID: ${user.id}` }) - .setColor(genColor(0)); - + .setColor(genColor(100)); - if (!member.permissions.has(PermissionsBitField.Flags.ModerateMembers)) - return await interaction.followUp({ embeds: [errorEmbed("You need the **Moderate Members** permission to execute this command.")] }); - if (selectedMember === member) - return await interaction.followUp({ embeds: [errorEmbed("You can't warn yourself.")] }); - if (selectedMember.user.bot) - return await interaction.followUp({ embeds: [errorEmbed(`You can't warn ${name}, because it's a bot.`)] }); - if (!selectedMember.manageable) - return await interaction.followUp({ embeds: [errorEmbed(`You can't warn ${name}, because they have a higher role position than Nebula.`)] }); - if (member.roles.highest.position < selectedMember.roles.highest.position) - return await interaction.followUp({ embeds: [errorEmbed(`You can't warn ${name}, because they have a higher role position than you.`)] }); + const logChannel = await (await getSettingsTable(this.db)) + ?.get(`${interaction.guild.id}.logChannel`) + .then((channel: string | null) => channel) + .catch(() => null); - const settingsTable = await getSettingsTable(db); - const logChannel = await settingsTable?.get(`${interaction.guild.id}.logChannel`).then( - (channel: string | null) => channel - ).catch(() => null); if (logChannel) { - const channel = await interaction.guild.channels.cache.get(logChannel).fetch().then( - (channel: Channel) => { + const channel = await interaction.guild.channels.cache + .get(logChannel) + .fetch() + .then((channel: Channel) => { if (channel.type != ChannelType.GuildText) return null; return channel as TextChannel; - } - ).catch(() => null); - if (channel) await channel.send({ embeds: [warnEmbed] }); + }).catch(() => null); + + if (channel) await channel.send({ embeds: [embed] }); } + const dmChannel = (await user.createDM().catch(() => null)) as DMChannel | null; if (dmChannel) await dmChannel.send({ embeds: [embedDM] }); - await modTable?.push(`${interaction.guild.id}.${user.id}.warns`, newWarn); - await interaction.followUp({ embeds: [warnEmbed] }); + await (await getModerationTable(this.db))?.push(`${interaction.guild.id}.${user.id}.warns`, newWarn); + await interaction.followUp({ embeds: [embed] }); } } diff --git a/src/commands/moderation/warns.ts b/src/commands/moderation/warns.ts index 2a42fc5..5813c09 100644 --- a/src/commands/moderation/warns.ts +++ b/src/commands/moderation/warns.ts @@ -3,9 +3,9 @@ import { type ChatInputCommandInteraction } from "discord.js"; import { genColor } from "../../utils/colorGen.js"; -import errorEmbed from "../../utils/embeds/errorEmbed.js"; import { QuickDB } from "quick.db"; import { getModerationTable } from "../../utils/database.js"; +import { errorEmbed } from "../../utils/embeds/errorEmbed.js"; type Warn = { id: number; @@ -30,46 +30,38 @@ export default class Warns { } async run(interaction: ChatInputCommandInteraction) { - const db = this.db; - const modTable = await getModerationTable(db); + const member = interaction.guild.members.cache.get(interaction.member.user.id); - const user = interaction.options.getUser("user"); - const members = interaction.guild.members.cache; - const member = members.get(interaction.member.user.id); + if (!member.permissions.has(PermissionsBitField.Flags.ModerateMembers)) return await interaction.followUp({ + embeds: [errorEmbed("You need the **Moderate Members** permission to execute this command.")] + }); - const warns = await modTable.get(`${interaction.guild.id}.${user.id}.warns`).then( - warns => { + const user = interaction.options.getUser("user"); + const warns = await (await getModerationTable(this.db)) + .get(`${interaction.guild.id}.${user.id}.warns`) + .then(warns => { if (!warns) return [] as Warn[]; return warns as Warn[] ?? [] as Warn[]; - } - ).catch(() => [] as Warn[]); + }) + .catch(() => [] as Warn[]); - const warnsEmbed = new EmbedBuilder() + const embed = new EmbedBuilder() + .setAuthor({ name: `• ${user.username}`, iconURL: user.displayAvatarURL() }) .setTitle(`✅ • Warns of ${user.username}`) - .setFields( - warns.length > 0 ? warns.map(warn => { - return { - name: `#${warn.id}`, - value: [ - `**Moderator**: <@${warn.moderator}>`, - `**Reason**: ${warn.reason}`, - `**Date**: `, - ].join("\n"), - inline: true, - }; - }) : [{ - name: "No warns", - value: "This user has no warns." - }] - ) + .setFields(warns.length > 0 ? warns.map(warn => { + return { + name: `#${warn.id}`, + value: [ + `**Moderator**: <@${warn.moderator}>`, + `**Reason**: ${warn.reason}`, + `**Date**: `, + ].join("\n") + }; + }) : [{ name: "No warns", value: "This user has no warns." }]) .setThumbnail(user.displayAvatarURL()) - .setAuthor({ name: `• ${user.username}`, iconURL: user.displayAvatarURL() }) .setFooter({ text: `User ID: ${user.id}` }) .setColor(genColor(100)); - if (!member.permissions.has(PermissionsBitField.Flags.ModerateMembers)) - return await interaction.followUp({ embeds: [errorEmbed("You need the **Moderate Members** permission to execute this command.")] }); - - await interaction.followUp({ embeds: [warnsEmbed] }); + await interaction.followUp({ embeds: [embed] }); } } diff --git a/src/commands/news.ts b/src/commands/news.ts new file mode 100644 index 0000000..a87bbce --- /dev/null +++ b/src/commands/news.ts @@ -0,0 +1,77 @@ +import { + SlashCommandSubcommandBuilder, EmbedBuilder, ActionRowBuilder, + ButtonBuilder, ButtonStyle, type ChatInputCommandInteraction +} from "discord.js"; +import { genColor } from "../utils/colorGen.js"; +import { getNewsTable } from "../utils/database.js"; +import { QuickDB } from "quick.db"; + +export default class News { + data: SlashCommandSubcommandBuilder; + db: QuickDB; + + constructor(db?: QuickDB) { + this.db = db; + this.data = new SlashCommandSubcommandBuilder() + .setName("news") + .setDescription("The news of Nebula.") + .addNumberOption(option => option + .setName("page") + .setDescription("The page of the news you want to see") + ); + } + + async run(interaction: ChatInputCommandInteraction) { + let page = interaction.options.getNumber("page") ?? 1; + const news = await (await getNewsTable(this.db)) + .get(`903852579837059113.news`) // News of the Nebula server + .then(news => news as any[] ?? []) + .catch(() => []); + + const newsSorted = (Object.values(news) as any[])?.sort((a, b) => b.createdAt - a.createdAt); + if (page > newsSorted.length) page = newsSorted.length; + if (page < 1) page = 1; + + let currentNews = newsSorted[page - 1]; + let newsEmbed = new EmbedBuilder() + .setAuthor({ name: currentNews.author, iconURL: currentNews.authorPfp ?? null }) + .setTitle(currentNews.title) + .setDescription(currentNews.body) + .setImage(currentNews.imageURL || null) + .setTimestamp(parseInt(currentNews.updatedAt)) + .setFooter({ text: `Page ${page} of ${newsSorted.length} • ID: ${currentNews.id}` }) + .setColor(genColor(270)); + + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId("left") + .setEmoji("1137330341472915526") + .setStyle(ButtonStyle.Primary), + new ButtonBuilder() + .setCustomId("right") + .setEmoji("1137330125004869702") + .setStyle(ButtonStyle.Primary) + ); + + await interaction.followUp({ embeds: [newsEmbed], components: [row] }); + interaction.channel + .createMessageComponentCollector({ filter: i => i.user.id === interaction.user.id, time: 60000 }) + .on("collect", async i => { + if (!i.isButton()) return; + + if (i.customId === "left") { + page--; + if (page < 1) page = newsSorted.length; + } else if (i.customId === "right") { + page++; + if (page > newsSorted.length) page = 1; + } + + currentNews = currentNews; + newsEmbed = newsEmbed; + + await interaction.editReply({ embeds: [newsEmbed], components: [row] }); + await i.deferUpdate(); + }); + } +} diff --git a/src/commands/server/info.ts b/src/commands/server/info.ts new file mode 100644 index 0000000..26de1ad --- /dev/null +++ b/src/commands/server/info.ts @@ -0,0 +1,16 @@ +import { SlashCommandSubcommandBuilder, type ChatInputCommandInteraction } from "discord.js"; +import { serverEmbed } from "../../utils/embeds/serverEmbed.js"; + +export default class ServerInfo { + data: SlashCommandSubcommandBuilder; + constructor() { + this.data = new SlashCommandSubcommandBuilder() + .setName("info") + .setDescription("Shows this server's info."); + } + + async run(interaction: ChatInputCommandInteraction) { + const embed = await serverEmbed({ guild: interaction.guild, roles: true }); + await interaction.followUp({ embeds: [embed] }); + } +} diff --git a/src/commands/server/leaderboard.ts b/src/commands/server/leaderboard.ts new file mode 100644 index 0000000..f1eb3ec --- /dev/null +++ b/src/commands/server/leaderboard.ts @@ -0,0 +1,53 @@ +import { SlashCommandSubcommandBuilder, EmbedBuilder, type ChatInputCommandInteraction } from "discord.js"; +import { genColor } from "../../utils/colorGen.js"; +import { getLevelingTable, getSettingsTable } from "../../utils/database.js"; +import { errorEmbed } from "../../utils/embeds/errorEmbed.js"; +import { QuickDB } from "quick.db"; + +export default class Leaderboard { + data: SlashCommandSubcommandBuilder; + db: QuickDB; + + constructor(db?: QuickDB) { + this.db = db; + this.data = new SlashCommandSubcommandBuilder() + .setName("leaderboard") + .setDescription("Shows the server's leaderboard in levels."); + } + + async run(interaction: ChatInputCommandInteraction) { + const settingsTable = await getSettingsTable(this.db); + const levelingTable = await getLevelingTable(this.db); + + // return await interaction.followUp({ + // embeds: [errorEmbed("This command is under maintenance.")] + // }); + + const levelEnabled = await settingsTable?.get(`${interaction.guild.id}.leveling.enabled`).catch(() => { }); + const levels = await levelingTable?.get(`${interaction.guild.id}`).catch(() => { }); + const levelKeys = Object.keys(levels) + const convertLevelsAndExpToExp = (levels: any) => { + const exp = Object.keys(levels).map(level => levels[level].exp); + return exp.reduce((a, b) => a + b); + }; + + if (!levelEnabled) return await interaction.followUp({ + embeds: [errorEmbed("Leveling is disabled for this server.")] + }); + + const embed = new EmbedBuilder() + .setTitle("⚡ • Top 10 active members") + .setDescription(levelKeys + .slice(0, 10) + .map(level => [ + `#${Object.keys(levels).indexOf(level) + 1} • <@${level}>`, + `**Level ${levels[level].levels}** - Next Level: ${levels[level].levels + 1}`, + `**Exp**: ${levels[level].exp}/${Math.floor((2 * 50) * 1.25 * (levels[level].levels + 1))} until level up` + ]) + .join("\n\n")) + .setColor(genColor(200)) + .setTimestamp(); + + await interaction.followUp({ embeds: [embed] }); + } +} diff --git a/src/commands/server/news/add.ts b/src/commands/server/news/add.ts new file mode 100644 index 0000000..dbd8b26 --- /dev/null +++ b/src/commands/server/news/add.ts @@ -0,0 +1,102 @@ +import { + SlashCommandSubcommandBuilder, EmbedBuilder, PermissionsBitField, + ModalBuilder, TextInputBuilder, ActionRowBuilder, + TextInputStyle, type ChatInputCommandInteraction +} from "discord.js"; +import { getNewsTable } from "../../../utils/database.js"; +import { genColor } from "../../../utils/colorGen.js"; +import { errorEmbed } from "../../../utils/embeds/errorEmbed.js"; +import { sendSubscribedNews, News } from "../../../utils/sendSubscribedNews.js"; +import { sendChannelNews } from "../../../utils/sendChannelNews.js"; +import { QuickDB } from "quick.db"; + +export default class Add { + data: SlashCommandSubcommandBuilder; + deferred: boolean = false; + db: QuickDB; + + constructor(db?: QuickDB) { + this.db = db; + this.data = new SlashCommandSubcommandBuilder() + .setName("add") + .setDescription("Adds news to your guild."); + } + + async run(interaction: ChatInputCommandInteraction) { + const member = interaction.guild.members.cache.get(interaction.user.id); + if (!member.permissions.has(PermissionsBitField.Flags.ManageGuild)) return await interaction.reply({ + embeds: [errorEmbed("You need **Manage Server** permissions to add news.")], + }); + + const newsModal = new ModalBuilder() + .setCustomId("addnews") + .setTitle("Create new News for your server/project"); + + const titleInput = new TextInputBuilder() + .setCustomId("title") + .setPlaceholder("Write a title") + .setStyle(TextInputStyle.Short) + .setMaxLength(100) + .setLabel("Title") + .setRequired(true); + + const bodyInput = new TextInputBuilder() + .setCustomId("body") + .setPlaceholder("Insert your content here") + .setMaxLength(4000) + .setStyle(TextInputStyle.Paragraph) + .setLabel("Content (supports Markdown)") + .setRequired(true); + + const imageURLInput = new TextInputBuilder() + .setCustomId("imageurl") + .setPlaceholder("Place a link to your image") + .setStyle(TextInputStyle.Short) + .setMaxLength(1000) + .setLabel("Image URL (placed at the bottom)") + .setRequired(false); + + const firstActionRow = new ActionRowBuilder().addComponents(titleInput) as ActionRowBuilder; + const secondActionRow = new ActionRowBuilder().addComponents(bodyInput) as ActionRowBuilder; + const thirdActionRow = new ActionRowBuilder().addComponents(imageURLInput) as ActionRowBuilder; + + newsModal.addComponents(firstActionRow, secondActionRow, thirdActionRow); + await interaction.showModal(newsModal).catch(err => console.error(err)); + + interaction.client.once("interactionCreate", async interaction => { + if (!interaction.isModalSubmit()) return; + if (interaction.customId !== "addnews") return; + + const imageURL = interaction.fields.getTextInputValue("imageurl") as string | undefined; + if (imageURL) { + await interaction.reply({ + embeds: [errorEmbed("The image URL you provided is invalid.")], + }); + return; + } + + const id = crypto.randomUUID(); + const news = { + id, + title: interaction.fields.getTextInputValue("title") as string, + body: interaction.fields.getTextInputValue("body") as string, + imageURL, + author: interaction.user.displayName ?? interaction.user.username, + authorPfp: interaction.user.avatarURL(), + createdAt: Date.now().toString(), + updatedAt: Date.now().toString(), + messageId: null + }; + + sendSubscribedNews(interaction.guild, news as News).catch(err => console.error(err)); + sendChannelNews(interaction.guild, news as News, id).catch(err => console.error(err)); + + const embed = new EmbedBuilder() + .setTitle("✅ • News sent!") + .setColor(genColor(100)); + + await (await getNewsTable(this.db)).set(`${interaction.guild.id}.news.${id}`, news); + await interaction.reply({ embeds: [embed] }); + }); + } +} diff --git a/src/commands/server/news/edit.ts b/src/commands/server/news/edit.ts new file mode 100644 index 0000000..3373fe6 --- /dev/null +++ b/src/commands/server/news/edit.ts @@ -0,0 +1,161 @@ +import { + SlashCommandSubcommandBuilder, EmbedBuilder, PermissionsBitField, + ModalBuilder, TextInputBuilder, ActionRowBuilder, + TextInputStyle, TextChannel, Message, + type ChatInputCommandInteraction +} from "discord.js"; +import { getNewsTable } from "../../../utils/database.js"; +import { genColor } from "../../../utils/colorGen.js"; +import { errorEmbed } from "../../../utils/embeds/errorEmbed.js"; +import { sendSubscribedNews, News } from "../../../utils/sendSubscribedNews.js"; +import { QuickDB } from "quick.db"; + +export default class Edit { + data: SlashCommandSubcommandBuilder; + deferred: boolean = false; + db: QuickDB; + + constructor(db?: QuickDB) { + this.db = db; + this.data = new SlashCommandSubcommandBuilder() + .setName("edit") + .setDescription("Edits new of your guild.") + .addStringOption(option => option + .setName("id") + .setDescription("The ID of the news you want to edit.") + .setRequired(true) + ); + } + + async run(interaction: ChatInputCommandInteraction) { + const db = this.db; + const newsTable = await getNewsTable(db); + + const user = interaction.user; + const guild = interaction.guild; + const id = interaction.options.getString("id", true).trim(); + const news = await newsTable + ?.get(`${guild.id}.news.${id}`) + .then(news => news as News) + .catch(() => null as News | null); + + if (!news) return await interaction.followUp({ + embeds: [errorEmbed("The specified news doesn't exist.")] + }); + + const author = user.displayName ?? user.username; + const timestamp = Date.now().toString(); + const member = guild.members.cache.get(user.id); + + if (!member.permissions.has(PermissionsBitField.Flags.ManageGuild)) return await interaction.followUp({ + embeds: [errorEmbed("You need **Manage Server** permissions to add news.")], + }); + + const editModal = new ModalBuilder() + .setCustomId("editnews") + .setTitle("Edit News: " + news.title); + + const titleInput = new TextInputBuilder() + .setCustomId("title") + .setPlaceholder("Title") + .setStyle(TextInputStyle.Short) + .setMaxLength(100) + .setLabel("Title") + .setValue(news.title) + .setRequired(true); + + const bodyInput = new TextInputBuilder() + .setCustomId("body") + .setPlaceholder("Content (markdown)") + .setMaxLength(4000) + .setStyle(TextInputStyle.Paragraph) + .setLabel("Content (markdown)") + .setValue(news.body) + .setRequired(true); + + const imageURLInput = new TextInputBuilder() + .setCustomId("imageurl") + .setPlaceholder("Big image URL (bottom)") + .setStyle(TextInputStyle.Short) + .setMaxLength(1000) + .setLabel("Big image URL (bottom)") + .setValue(news.imageURL) + .setRequired(false); + + const firstActionRow = new ActionRowBuilder().addComponents(titleInput) as ActionRowBuilder; + const secondActionRow = new ActionRowBuilder().addComponents(bodyInput) as ActionRowBuilder; + const thirdActionRow = new ActionRowBuilder().addComponents(imageURLInput) as ActionRowBuilder; + + editModal.addComponents(firstActionRow, secondActionRow, thirdActionRow); + await interaction.showModal(editModal).catch(err => console.error(err)); + + interaction.client.once("interactionCreate", async (interaction) => { + if (!interaction.isModalSubmit()) return; + if (interaction.customId !== "editnews") return; + + const title = interaction.fields.getTextInputValue("title") as string; + const body = interaction.fields.getTextInputValue("body") as string; + const imageURL = interaction.fields.getTextInputValue("imageurl") as string | undefined; + + if (imageURL) await interaction.reply({ + embeds: [errorEmbed("The image URL you provided is invalid.")], + }); + + const newNews = { + ...news, + title, + body, + imageURL, + author, + authorPfp: user.avatarURL(), + updatedAt: timestamp + }; + + sendSubscribedNews(guild, {...newNews, title: `Updated: ${newNews.title}`} as News) + .catch(err => console.error(err)); + + const newsEmbed = new EmbedBuilder() + .setAuthor({ name: newNews.author, iconURL: newNews.authorPfp ?? null }) + .setTitle(newNews.title) + .setDescription(newNews.body) + .setImage(newNews.imageURL || null) + .setTimestamp(parseInt(newNews.updatedAt)) + .setFooter({ text: `Updated news from ${guild.name}` }) + .setColor(genColor(200)); + + const subscribedNewsChannel = await newsTable + ?.get(`${guild.id}.channel`) + .then(channel => channel as { channelId: string; roleId: string } | null) + .catch(() => { + return { channelId: null as string | null, roleId: null as string | null }; + }); + + if (subscribedNewsChannel.channelId) { + const messageId = newNews?.messageId; + const newsChannel = (await guild.channels.fetch(subscribedNewsChannel?.channelId ?? "").catch(() => { })) as TextChannel | null; + + if (!messageId && newsChannel.id) newNews.messageId = ((await newsChannel + ?.send({ + embeds: [newsEmbed], + content: subscribedNewsChannel.roleId ? `<@&${subscribedNewsChannel.roleId}>` : null + }) + .catch(() => { })) as Message | null + )?.id; + + else if (newsChannel.id) await newsChannel?.messages + .edit(messageId, { + embeds: [newsEmbed], + content: subscribedNewsChannel.roleId ? `<@&${subscribedNewsChannel.roleId}>` : null + }) + .catch(() => { }); + } + + const embed = new EmbedBuilder() + .setTitle("✅ • News edited!") + .setColor(genColor(100)); + + await newsTable.set(`${guild.id}.news.${id}`, newNews); + await interaction.reply({ embeds: [embed] }); + }); + } +} diff --git a/src/commands/server/news/remove.ts b/src/commands/server/news/remove.ts new file mode 100644 index 0000000..c196d29 --- /dev/null +++ b/src/commands/server/news/remove.ts @@ -0,0 +1,61 @@ +import { + SlashCommandSubcommandBuilder, EmbedBuilder, PermissionsBitField, + TextChannel, type ChatInputCommandInteraction +} from "discord.js"; +import { getNewsTable } from "../../../utils/database.js"; +import { genColor } from "../../../utils/colorGen.js"; +import { errorEmbed } from "../../../utils/embeds/errorEmbed.js"; +import { QuickDB } from "quick.db"; + +export default class Remove { + data: SlashCommandSubcommandBuilder; + db: QuickDB; + + constructor(db?: QuickDB) { + this.db = db; + this.data = new SlashCommandSubcommandBuilder() + .setName("remove") + .setDescription("Removes news from your guild.") + .addStringOption(option => option + .setName("id") + .setDescription("The ID of the news. Found in the footer of the news.") + .setRequired(true) + ); + } + + async run(interaction: ChatInputCommandInteraction) { + const db = this.db; + const newsTable = await getNewsTable(db); + const user = interaction.user; + const guild = interaction.guild; + const providedId = interaction.options.getString("id"); + const member = guild.members.cache.get(user.id); + + if (!member.permissions.has(PermissionsBitField.Flags.ManageGuild)) return await interaction.followUp({ + embeds: [errorEmbed("You need **Manage Server** permissions to delete news.")], + }); + + const embed = new EmbedBuilder() + .setTitle("✅ • News deleted!") + .setColor(genColor(100)); + + const subscribedChannel = await newsTable + ?.get(`${guild.id}.channel`) + .then(channel => channel as { channelId: string, roleId: string }) + .catch(() => { + return { channelId: null, roleId: null }; + }); + + const news = await newsTable?.get(providedId).catch(() => null); + if (!news) return await interaction.followUp({ + embeds: [errorEmbed("The specified news doesn't exist.")] + }); + + const messageId = news?.messageId; + const newsChannel = (await interaction.guild.channels.fetch(subscribedChannel?.channelId ?? "").catch(() => null)) as TextChannel | null; + if (newsChannel) await newsChannel?.messages.delete(messageId).catch(() => null); + + await newsTable?.delete(`${guild.id}.news.${providedId}`).catch(() => null); + await interaction.followUp({ embeds: [embed] }); + } +} diff --git a/src/commands/server/news/view.ts b/src/commands/server/news/view.ts new file mode 100644 index 0000000..8904206 --- /dev/null +++ b/src/commands/server/news/view.ts @@ -0,0 +1,107 @@ +import { + SlashCommandSubcommandBuilder, EmbedBuilder, ActionRowBuilder, + ButtonBuilder, ButtonStyle, type ChatInputCommandInteraction +} from "discord.js"; +import { genColor } from "../../../utils/colorGen.js"; +import { getNewsTable } from "../../../utils/database.js"; +import { QuickDB } from "quick.db"; +import { errorEmbed } from "../../../utils/embeds/errorEmbed.js"; + +export default class View { + data: SlashCommandSubcommandBuilder; + db: QuickDB; + + constructor(db?: QuickDB) { + this.db = db; + this.data = new SlashCommandSubcommandBuilder() + .setName("view") + .setDescription("View the news of this server.") + .addNumberOption(option => option + .setName("page") + .setDescription("The page of the news you want to see.") + ); + } + + async run(interaction: ChatInputCommandInteraction) { + const db = this.db; + const newsletterTable = await getNewsTable(db); + const guild = interaction.guild; + let page = interaction.options.getNumber("page") ?? 1; + + const news = await newsletterTable + ?.get(`${guild.id}.news`) + .then(news => news as any[] ?? []) + .catch(() => []); + + const newsSorted = ( + Object.values(news).map((newsItem, i) => { + return { + id: Object.keys(news)[i], + ...newsItem + } + }) as any[] + )?.sort((a, b) => b.createdAt - a.createdAt); + + if (!news) return await interaction.followUp({ + embeds: [errorEmbed("No news found.\nAdmins can add news with the **/settings news add** command.")] + }); + + if (!newsSorted) return await interaction.followUp({ + embeds: [errorEmbed("No news found.\nAdmins can add news with the **/settings news add** command.")] + }); + + if (newsSorted.length == 0) return await interaction.followUp({ + embeds: [errorEmbed("No news found.\nAdmins can add news with the **/settings news add** command.")] + }); + + if (page > newsSorted.length) page = newsSorted.length; + if (page < 1) page = 1; + + let currentNews = newsSorted[page - 1]; + let newsEmbed = new EmbedBuilder() + .setAuthor({ name: currentNews.author, iconURL: currentNews.authorPfp ?? null }) + .setTitle(currentNews.title) + .setDescription(currentNews.body) + .setImage(currentNews.imageURL || null) + .setTimestamp(parseInt(currentNews.updatedAt)) + .setFooter({ text: `Page ${page} of ${newsSorted.length} • ID: ${currentNews.id}` }) + .setColor(genColor(200)); + + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId("left") + .setEmoji("1137330341472915526") + .setStyle(ButtonStyle.Primary), + new ButtonBuilder() + .setCustomId("right") + .setEmoji("1137330125004869702") + .setStyle(ButtonStyle.Primary) + ); + + await interaction.followUp({ embeds: [newsEmbed], components: [row] }); + + const buttonCollector = interaction.channel.createMessageComponentCollector({ + filter: i => i.user.id === interaction.user.id, + time: 60000 + }); + + buttonCollector.on("collect", async i => { + if (!i.isButton()) return; + const id = i.customId; + + if (id == "left") { + page--; + if (page < 1) page = newsSorted.length; + } else if (id == "right") { + page++; + if (page > newsSorted.length) page = 1; + } + + currentNews = currentNews; + newsEmbed = newsEmbed; + + await interaction.editReply({ embeds: [newsEmbed], components: [row] }); + await i.deferUpdate(); + }); + } +} diff --git a/src/commands/server/subscribe.ts b/src/commands/server/subscribe.ts new file mode 100644 index 0000000..230ee60 --- /dev/null +++ b/src/commands/server/subscribe.ts @@ -0,0 +1,55 @@ +import { + SlashCommandSubcommandBuilder, EmbedBuilder, DMChannel, + type ChatInputCommandInteraction +} from "discord.js"; +import { genColor } from "../../utils/colorGen.js"; +import { getNewsTable } from "../../utils/database.js"; +import { QuickDB } from "quick.db"; +import { errorEmbed } from "../../utils/embeds/errorEmbed.js"; + +export default class Subscribe { + data: SlashCommandSubcommandBuilder; + db: QuickDB; + + constructor(db?: QuickDB) { + this.db = db; + this.data = new SlashCommandSubcommandBuilder() + .setName("subscribe") + .setDescription("Subscribe to the news of this server."); + } + + async run(interaction: ChatInputCommandInteraction) { + const newsTable = await getNewsTable(this.db); + const guild = interaction.guild; + const user = interaction.user; + + let subscriptions = await newsTable + ?.get(`${guild.id}.subscriptions`) + .then(subscriptions => subscriptions as string[] ?? [] as string[]) + .catch(() => []) as string[]; + + if (!subscriptions) subscriptions = []; + + const hasSub = subscriptions?.includes(user.id); + const dmChannel = (await interaction.user.createDM().catch(() => null)) as DMChannel | null; + + if (!dmChannel) return await interaction.followUp({ + embeds: [errorEmbed("You need to **enable DMs from server members** to subscribe to the news.")] + }); + + await dmChannel?.send("You have updated the subscription status of \`" + guild.name + "\`.").catch(async () => { + return await interaction.followUp({ + embeds: [errorEmbed("You need to **enable DMs from server members** to subscribe to the news.")] + }); + }); + + await newsTable[!hasSub ? "push" : "pull"](`${guild.id}.subscriptions`, user.id); + + const subscriptionEmbed = new EmbedBuilder() + .setTitle(`✅ • ${hasSub ? "Unsubscribed" : "Subscribed"} ${hasSub ? "from" : "to"} ${guild.name}`) + .setDescription(`You have ${hasSub ? "un" : ""}subscribed to the news of ${guild.name}.`) + .setColor(genColor(100)); + + await interaction.followUp({ embeds: [subscriptionEmbed] }); + } +} diff --git a/src/commands/serverboard.ts b/src/commands/serverboard.ts new file mode 100644 index 0000000..15061e6 --- /dev/null +++ b/src/commands/serverboard.ts @@ -0,0 +1,97 @@ +import { + SlashCommandSubcommandBuilder, ButtonBuilder, ActionRowBuilder, + ButtonStyle, ButtonInteraction, type ChatInputCommandInteraction +} from "discord.js"; +import { quickSort } from "../utils/quickSort.js"; +import { serverEmbed } from "../utils/embeds/serverEmbed.js"; +import { database, getNewsTable, getServerboardTable } from "../utils/database.js"; +import { QuickDB } from "quick.db"; + +export default class Serverboard { + data: SlashCommandSubcommandBuilder; + db: QuickDB; + + constructor(db?: QuickDB) { + this.db = db; + this.data = new SlashCommandSubcommandBuilder() + .setName("serverboard") + .setDescription("Shows the servers that have Nebula.") + .addNumberOption(option => option + .setName("page") + .setDescription("The page you want to see.") + ); + } + + async run(interaction: ChatInputCommandInteraction) { + const guildsMapped = {}; + const shownGuilds = (await (await getServerboardTable(this.db)).all().catch(() => [])) as any[]; + + for (const guild of interaction.client.guilds.cache.values()) { + const shownVal = shownGuilds?.find(shown => shown?.id == guild.id)?.value?.shown; + const isShown = shownVal == null ? null : shownVal; + + if (isShown == false) continue; + if (isShown == null && guild?.rulesChannelId != null) continue; + + guildsMapped[guild.memberCount + ":" + guild.id] = guild; + } + + const guildsSorted = quickSort( + [...Object.keys(guildsMapped).map(i => Number(i.split(":")[0]))], + [[...Object.values(guildsMapped)]], + 0, + Object.keys(guildsMapped).length - 1 + )[1][0].reverse(); + const pages = guildsSorted.length; + const argPage = interaction.options.getNumber("page", false); + let page = (argPage - 1 <= 0 ? 0 : argPage - 1 > pages ? pages - 1 : argPage - 1) || 0; + + let guild = guildsSorted[page]; + let subs = await (await getNewsTable(this.db)) + ?.get(`${guild.id}.subscriptions`) + .then(subs => subs?.length > 0 ? subs as string[] : [] as string[]) + .catch(() => [] as string[]); + + let embed = await serverEmbed({ + guild, page: page + 1, pages, showInvite: true, showSubs: subs.length > 0, subs: subs.length + }); + + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId("left") + .setEmoji("1137330341472915526") + .setStyle(ButtonStyle.Primary), + new ButtonBuilder() + .setCustomId("right") + .setEmoji("1137330125004869702") + .setStyle(ButtonStyle.Primary) + ); + + await interaction.followUp({ embeds: [embed], components: [row] }); + interaction.channel + .createMessageComponentCollector({ filter: i => i.user.id === interaction.user.id, time: 60000 }) + .on("collect", async (interaction: ButtonInteraction) => { + let subs; + + const subscriptions = await (await database()).table("subscriptions").all(); + switch (interaction.customId) { + case "left": + page--; + if (page < 0) page = pages - 1; + subs = subscriptions.filter(sub => (sub.value as string[] ?? [] as string[]).includes(guild.id)); + break; + case "right": + page++; + if (page >= pages) page = 0; + subs = subscriptions.filter(sub => (sub.value as string[]).includes(guild.id)); + break; + } + + guild = guildsSorted[page]; + embed = await serverEmbed({ guild, page: page + 1, pages, showInvite: true, showSubs: subs.length > 0, subs: subs.length }); + + interaction.message.edit({ embeds: [embed], components: [row] }); + interaction.deferUpdate(); + }); + } +} diff --git a/src/commands/settings/command/list.ts b/src/commands/settings/command/list.ts index b63d47e..75873f3 100644 --- a/src/commands/settings/command/list.ts +++ b/src/commands/settings/command/list.ts @@ -1,11 +1,9 @@ import { - SlashCommandSubcommandBuilder, - EmbedBuilder, - PermissionsBitField, - type ChatInputCommandInteraction, + SlashCommandSubcommandBuilder, EmbedBuilder, PermissionsBitField, + type ChatInputCommandInteraction } from "discord.js"; import { genColor } from "../../../utils/colorGen.js"; -import errorEmbed from "../../../utils/embeds/errorEmbed.js"; +import { errorEmbed } from "../../../utils/embeds/errorEmbed.js"; import { QuickDB } from "quick.db"; import { getSettingsTable } from "../../../utils/database.js"; @@ -23,13 +21,11 @@ export default class List { async run(interaction: ChatInputCommandInteraction) { const db = this.db; const settingsTable = await getSettingsTable(db); - const member = interaction.guild.members.cache.get(interaction.user.id); - if (!member.permissions.has(PermissionsBitField.Flags.ManageGuild)) - return await interaction.followUp({ - embeds: [errorEmbed("You need **Manage Server** permissions to list commands.")], - }); + if (!member.permissions.has(PermissionsBitField.Flags.ManageGuild)) return await interaction.followUp({ + embeds: [errorEmbed("You need **Manage Server** permissions to list commands.")], + }); const disabledCommands = await settingsTable ?.get(`${interaction.guild.id}.disabledCommands`) @@ -42,7 +38,7 @@ export default class List { !disabledCommands || disabledCommands?.length == 0 ? "There are no disabled commands." : disabledCommands - .map((command) => { + .map(command => { const [commandName, subcommandName] = command.split("/"); return `/${commandName}${subcommandName ? ` ${subcommandName}` : ""}`; }) diff --git a/src/commands/settings/command/toggle.ts b/src/commands/settings/command/toggle.ts index 0046127..912307c 100644 --- a/src/commands/settings/command/toggle.ts +++ b/src/commands/settings/command/toggle.ts @@ -5,7 +5,7 @@ import { import { genColor } from "../../../utils/colorGen.js"; import Commands from "../../../handlers/commands.js"; import { getSettingsTable } from "../../../utils/database.js"; -import errorEmbed from "../../../utils/embeds/errorEmbed.js"; +import { errorEmbed } from "../../../utils/embeds/errorEmbed.js"; import { QuickDB } from "quick.db"; export default class Toggle { @@ -34,9 +34,10 @@ export default class Toggle { const commandPath = interaction.options.getString("command", true); let [commandName, subcommandName, subcommandGroupName] = commandPath.split(" "); commandName = commandName.replace("/", ""); - const disabledCommands = await settingsTable?.get(`${interaction.guild.id}.disabledCommands`).then( - (disabledCommands) => disabledCommands as any[] ?? [] - ).catch(() => []); + const disabledCommands = await settingsTable + ?.get(`${interaction.guild.id}.disabledCommands`) + .then(disabledCommands => disabledCommands as any[] ?? []) + .catch(() => []); const hasCommand = (name: string, subcommand?: string, subcommandGroup?: string) => { return commands.commands.some(command => @@ -54,14 +55,13 @@ export default class Toggle { ? disabledCommands.filter((cmd) => cmd !== commandName) : [...disabledCommands, commandName]; - if (!member.permissions.has(PermissionsBitField.Flags.ManageGuild)) - return await interaction.followUp({ - embeds: [errorEmbed("You need the **Manage Server** permission to enable commands.")], - }); + if (!member.permissions.has(PermissionsBitField.Flags.ManageGuild)) return await interaction.followUp({ + embeds: [errorEmbed("You need the **Manage Server** permission to enable commands.")], + }); - if (!hasCommand(commandName, subcommandName, subcommandGroupName)) { - return await interaction.followUp({ embeds: [errorEmbed("The specified command doesn't exist.")] }); - } + if (!hasCommand(commandName, subcommandName, subcommandGroupName)) return await interaction.followUp({ + embeds: [errorEmbed("The specified command doesn't exist.")] + }); const embed = new EmbedBuilder() .setTitle(`⌚ • ${isEnabled ? "Disabling" : "Enabling"} ${commandPath}.`) diff --git a/src/commands/settings/leveling/block-channels.ts b/src/commands/settings/leveling/block-channels.ts index 64bd32e..d46d410 100644 --- a/src/commands/settings/leveling/block-channels.ts +++ b/src/commands/settings/leveling/block-channels.ts @@ -6,7 +6,7 @@ import { NonThreadGuildBasedChannel } from "discord.js"; import { genColor } from "../../../utils/colorGen.js"; -import errorEmbed from "../../../utils/embeds/errorEmbed.js"; +import { errorEmbed } from "../../../utils/embeds/errorEmbed.js"; import { getSettingsTable } from "../../../utils/database.js"; import { QuickDB } from "quick.db"; diff --git a/src/commands/settings/leveling/channel.ts b/src/commands/settings/leveling/channel.ts index 32f49cd..b7c5227 100644 --- a/src/commands/settings/leveling/channel.ts +++ b/src/commands/settings/leveling/channel.ts @@ -6,7 +6,7 @@ import { NonThreadGuildBasedChannel } from "discord.js"; import { genColor } from "../../../utils/colorGen.js"; -import errorEmbed from "../../../utils/embeds/errorEmbed.js"; +import { errorEmbed } from "../../../utils/embeds/errorEmbed.js"; import { getSettingsTable } from "../../../utils/database.js"; import { QuickDB } from "quick.db"; diff --git a/src/commands/settings/leveling/rewards.ts b/src/commands/settings/leveling/rewards.ts index 4c9e5da..9b5fd1f 100644 --- a/src/commands/settings/leveling/rewards.ts +++ b/src/commands/settings/leveling/rewards.ts @@ -1,10 +1,9 @@ import { SlashCommandSubcommandBuilder, EmbedBuilder, PermissionsBitField, - type ChatInputCommandInteraction, - Role, + Role, type ChatInputCommandInteraction } from "discord.js"; import { genColor } from "../../../utils/colorGen.js"; -import errorEmbed from "../../../utils/embeds/errorEmbed.js"; +import { errorEmbed } from "../../../utils/embeds/errorEmbed.js"; import { getSettingsTable } from "../../../utils/database.js"; import { QuickDB } from "quick.db"; @@ -35,18 +34,15 @@ export default class Rewards { async run(interaction: ChatInputCommandInteraction) { const db = this.db; const settingsTable = await getSettingsTable(db); - - const inputLevel = interaction.options.getNumber("level", false); + const level = interaction.options.getNumber("level", false); const inputRole = interaction.options.getRole("role", false) as Role | null; - if (!inputLevel && !inputRole) { - // List the rewards + if (!level && !inputRole) { const rewards = await this.getRewards(interaction.guild.id).then(rewards => rewards.sort((a, b) => a.level - b.level)) as Reward[]; - if (rewards.length == 0) { - return await interaction.followUp({ - embeds: [errorEmbed("There are no rewards set for this server.")] - }); - } + + if (rewards.length == 0) return await interaction.followUp({ + embeds: [errorEmbed("There are no rewards set for this server.")] + }); const rewardsEmbed = new EmbedBuilder() .setTitle("🎁 • Rewards") @@ -55,31 +51,20 @@ export default class Rewards { for (const { roleId, level } of rewards) { if (!roleId) continue; - rewardsEmbed.addFields([{ - name: `Level ${level}`, - value: `<@&${roleId}>`, - }]); + rewardsEmbed.addFields({ name: `Level ${level}`, value: `<@&${roleId}>` }); } - return await interaction.followUp({ - embeds: [rewardsEmbed] - }); - } else if (inputLevel && !inputRole) { - // Delete a reward - const level = Number(inputLevel); + return await interaction.followUp({ embeds: [rewardsEmbed] }); + } else if (level && !inputRole) { const rewards = await this.getRewards(interaction.guild.id).then(rewards => rewards.sort((a, b) => a?.level - b?.level)) as Reward[]; - if (rewards.length == 0) { - return await interaction.followUp({ - embeds: [errorEmbed("There are no rewards set for this server.")] - }); - } - const reward = rewards.find(reward => reward?.level == level); - if (!reward) { - return await interaction.followUp({ - embeds: [errorEmbed(`There is no reward set for level ${level}.`)] - }); - } + if (rewards.length == 0) return await interaction.followUp({ + embeds: [errorEmbed("There are no rewards set for this server.")] + }); + + if (!rewards.find(reward => reward?.level == level)) return await interaction.followUp({ + embeds: [errorEmbed(`There is no reward set for level ${level}.`)] + }); const newRewards = rewards.filter(reward => reward.level != level); await settingsTable?.set(`${interaction.guild.id}.leveling.rewards`, newRewards).catch(() => []); @@ -93,37 +78,29 @@ export default class Rewards { }); } - // Set a reward - const level = Number(inputLevel); const role = await interaction.guild.roles.fetch(String(inputRole?.id)).catch(() => null); + const permissions = role?.permissions as PermissionsBitField; - if (!role) { - return await interaction.followUp({ - embeds: [errorEmbed("That role doesn't exist or couldn't be loaded.")] - }); - } + if (!role) return await interaction.followUp({ + embeds: [errorEmbed("That role doesn't exist or couldn't be loaded.")] + }); + + if (level < 0) return await interaction.followUp({ + embeds: [errorEmbed("You can't set a reward for a negative level.")] + }); + + if (level == 0) return await interaction.followUp({ + embeds: [errorEmbed("You can't set a reward for level 0.")] + }); + + if (role.position >= interaction.guild.members.me.roles.highest.position) return await interaction.followUp({ + embeds: [errorEmbed("That role is above mine.")] + }); + + if (role?.name?.includes("everyone")) return await interaction.followUp({ + embeds: [errorEmbed("I can't give out the @everyone role.")] + }); - const permissions = role?.permissions as PermissionsBitField; - if (level < 0) { - return await interaction.followUp({ - embeds: [errorEmbed("You can't set a reward for a negative level.")] - }); - } - if (level == 0) { - return await interaction.followUp({ - embeds: [errorEmbed("You can't set a reward for level 0.")] - }); - } - if (role.position >= interaction.guild.members.me.roles.highest.position) { - return await interaction.followUp({ - embeds: [errorEmbed("That role is above mine.")] - }); - } - if (role?.name?.includes("everyone")) { - return await interaction.followUp({ - embeds: [errorEmbed("I can't give out the @everyone role.")] - }); - } if ( permissions.has(PermissionsBitField.Flags.Administrator) || permissions.has(PermissionsBitField.Flags.ManageRoles) || @@ -134,11 +111,9 @@ export default class Rewards { permissions.has(PermissionsBitField.Flags.ManageMessages) || permissions.has(PermissionsBitField.Flags.ManageThreads) || permissions.has(PermissionsBitField.Flags.ManageNicknames) - ) { - return await interaction.followUp({ - embeds: [errorEmbed("I can't give out a role with dangerous permissions.")] - }); - } + ) return await interaction.followUp({ + embeds: [errorEmbed("I can't give out a role with dangerous permissions.")] + }); const rewards = await this.getRewards(interaction.guild.id).then(rewards => rewards.sort((a, b) => a?.level - b?.level)) as Reward[]; const newRewards = rewards.filter(reward => reward.level != level); @@ -159,14 +134,14 @@ export default class Rewards { async getRewards(guildId: string): Promise { const settingsTable = await getSettingsTable(this.db); - const rewards = new Promise((resolve, reject) => { - settingsTable?.get(`${guildId}.leveling.rewards`).then(rewards => { + const rewards = new Promise(resolve => settingsTable + ?.get(`${guildId}.leveling.rewards`) + .then(rewards => { if (!rewards) return resolve([]); resolve(rewards); - }).catch(() => { - resolve([]); - }); - }); + }) + .catch(() => resolve([]))); + return rewards as Promise; } } diff --git a/src/commands/settings/leveling/set.ts b/src/commands/settings/leveling/set.ts index d7e1e93..9018228 100644 --- a/src/commands/settings/leveling/set.ts +++ b/src/commands/settings/leveling/set.ts @@ -3,7 +3,7 @@ import { type ChatInputCommandInteraction, } from "discord.js"; import { genColor } from "../../../utils/colorGen.js"; -import errorEmbed from "../../../utils/embeds/errorEmbed.js"; +import { errorEmbed } from "../../../utils/embeds/errorEmbed.js"; import { getLevelingTable, getSettingsTable } from "../../../utils/database.js"; import { QuickDB } from "quick.db"; import { Reward } from "./rewards.js"; diff --git a/src/commands/settings/leveling/toggle.ts b/src/commands/settings/leveling/toggle.ts index 9e064dd..6b5c581 100644 --- a/src/commands/settings/leveling/toggle.ts +++ b/src/commands/settings/leveling/toggle.ts @@ -1,11 +1,10 @@ import { - SlashCommandSubcommandBuilder, EmbedBuilder, - type ChatInputCommandInteraction, - PermissionsBitField, + SlashCommandSubcommandBuilder, EmbedBuilder, PermissionsBitField, + type ChatInputCommandInteraction } from "discord.js"; import { genColor } from "../../../utils/colorGen.js"; -import database from "../../../utils/database.js"; -import errorEmbed from "../../../utils/embeds/errorEmbed.js"; +import { database } from "../../../utils/database.js"; +import { errorEmbed } from "../../../utils/embeds/errorEmbed.js"; export default class Toggle { data: SlashCommandSubcommandBuilder; @@ -17,13 +16,14 @@ export default class Toggle { async run(interaction: ChatInputCommandInteraction) { const db = await database(); + const enabled = await db.table("settings") + ?.get(`${interaction.guild.id}.leveling.enabled`) + .then(enabled => !!enabled) + .catch(() => false); - const enabled = await db.table("settings")?.get(`${interaction.guild.id}.leveling.enabled`).then( - (enabled) => !!enabled - ).catch(() => false); await db.table("settings").set(`${interaction.guild.id}.leveling.enabled`, !enabled); - const user = (await interaction.guild.members.me.fetch()) + const user = await interaction.guild.members.me.fetch(); if (!user.permissions.has(PermissionsBitField.Flags.ManageGuild)) { return await interaction.followUp({ embeds: [errorEmbed("You need **Manage Server** permissions to toggle leveling.")] diff --git a/src/commands/settings/moderation/logs.ts b/src/commands/settings/moderation/logs.ts index 0dae559..197bfc7 100644 --- a/src/commands/settings/moderation/logs.ts +++ b/src/commands/settings/moderation/logs.ts @@ -1,12 +1,9 @@ import { - SlashCommandSubcommandBuilder, - EmbedBuilder, - PermissionsBitField, - type ChatInputCommandInteraction, - ChannelType, + SlashCommandSubcommandBuilder, EmbedBuilder, PermissionsBitField, + ChannelType, type ChatInputCommandInteraction } from "discord.js"; import { genColor } from "../../../utils/colorGen.js"; -import errorEmbed from "../../../utils/embeds/errorEmbed.js"; +import { errorEmbed } from "../../../utils/embeds/errorEmbed.js"; import { QuickDB } from "quick.db"; import { getSettingsTable } from "../../../utils/database.js"; @@ -19,29 +16,28 @@ export default class Logs { this.data = new SlashCommandSubcommandBuilder() .setName("logs") .setDescription("Sets/Remove the logs channel.") - .addChannelOption(option => - option.setName("channel") - .setDescription("Where to send logs in. Empty = deleted.") + .addChannelOption(option => option + .setName("channel") + .setDescription("Where to send logs in. Empty = deleted.") ); } async run(interaction: ChatInputCommandInteraction) { const db = this.db; const settingsTable = await getSettingsTable(db); - const member = interaction.guild.members.cache.get(interaction.user.id); + const specifiedChannel = interaction.options.getChannel("channel"); - if (!member.permissions.has(PermissionsBitField.Flags.ManageGuild)) { - return await interaction.followUp({ - embeds: [errorEmbed("You need **Manage Server** permissions to list commands.")], - }); - } + if (!member.permissions.has(PermissionsBitField.Flags.ManageGuild)) return await interaction.followUp({ + embeds: [errorEmbed("You need **Manage Server** permissions to list commands.")], + }); - const specifiedChannel = interaction.options.getChannel("channel"); - if (specifiedChannel?.type != ChannelType.GuildText) return await interaction.followUp({ embeds: [errorEmbed("You must provide a text channel.")] }); + if (specifiedChannel?.type !== ChannelType.GuildText) return await interaction.followUp({ + embeds: [errorEmbed("You must provide a text channel.")] + }); if (!specifiedChannel?.id) await settingsTable?.delete(`${interaction.guild.id}.logChannel`); - else await settingsTable?.set(`${interaction.guild.id}.logChannel`, specifiedChannel?.id) + else await settingsTable?.set(`${interaction.guild.id}.logChannel`, specifiedChannel?.id); const listEmbed = new EmbedBuilder() .setTitle("📃 • Log channel") diff --git a/src/commands/settings/news/channel.ts b/src/commands/settings/news/channel.ts index 5803a7b..327931d 100644 --- a/src/commands/settings/news/channel.ts +++ b/src/commands/settings/news/channel.ts @@ -1,13 +1,10 @@ import { - SlashCommandSubcommandBuilder, - EmbedBuilder, - PermissionsBitField, - type ChatInputCommandInteraction, - ChannelType, + SlashCommandSubcommandBuilder, EmbedBuilder, PermissionsBitField, + ChannelType, type ChatInputCommandInteraction } from "discord.js"; import { getNewsTable } from "../../../utils/database.js"; import { genColor } from "../../../utils/colorGen.js"; -import errorEmbed from "../../../utils/embeds/errorEmbed.js"; +import { errorEmbed } from "../../../utils/embeds/errorEmbed.js"; import { QuickDB } from "quick.db"; export default class Channel { @@ -19,8 +16,12 @@ export default class Channel { this.data = new SlashCommandSubcommandBuilder() .setName("channel") .setDescription("Sets/removes (when no options) a news channel, all news you post will be sent.") - .addChannelOption((option) => option.setName("channel").setDescription("The channel to send news to.")) - .addRoleOption((option) => option.setName("role").setDescription("The role to ping when news are sent.")); + .addChannelOption(option => option + .setName("channel") + .setDescription("The channel to send news to.")) + .addRoleOption(option => option + .setName("role") + .setDescription("The role to ping when news are sent.")); } async run(interaction: ChatInputCommandInteraction) { @@ -30,43 +31,31 @@ export default class Channel { const guild = interaction.guild; const channelOption = interaction.options.getChannel("channel"); const roleOption = interaction.options.getRole("role"); - const channel = guild.channels.cache.get(channelOption?.id); const role = guild.roles.cache.get(roleOption?.id ?? ""); - if (role?.name === "@everyone") { - return await interaction.followUp({ embeds: [errorEmbed("You can't ping @everyone.")] }); - } if (!channelOption && !roleOption) { await newsTable.delete(`${guild.id}.channel`); return await interaction.followUp({ - embeds: [ - new EmbedBuilder().setTitle("✅ • Removed news channel.") - ] + embeds: [ new EmbedBuilder().setTitle("✅ • Removed news channel.") ] }); } - if (!interaction.memberPermissions.has(PermissionsBitField.Flags.ManageGuild)) { - return await interaction.followUp({ - embeds: [errorEmbed("You need **Manage Server** permissions to add news.")], - }); - } - if (channel.type !== ChannelType.GuildText && channel.type !== ChannelType.GuildAnnouncement) { - return await interaction.followUp({ embeds: [errorEmbed("The channel must be a text channel.")] }); - } + if (!interaction.memberPermissions.has(PermissionsBitField.Flags.ManageGuild)) return await interaction.followUp({ + embeds: [errorEmbed("You need **Manage Server** permissions to add news.")], + }); - await newsTable.set(`${guild.id}.channel`, { - channelId: channel.id, - roleId: role?.id ?? "", + if (channel.type !== ChannelType.GuildText && channel.type !== ChannelType.GuildAnnouncement) return await interaction.followUp({ + embeds: [errorEmbed("The channel must be a text channel.")] }); + await newsTable.set(`${guild.id}.channel`, { channelId: channel.id, roleId: role?.id ?? "" }); const embed = new EmbedBuilder() .setTitle("✅ • News channel set!") .setColor(genColor(100)); if (role) embed.setDescription(`The role <@&${role.id}> will be pinged when news are sent.`); else embed.setDescription(`No role will be pinged when news are sent.`); - await interaction.followUp({ embeds: [embed] }); } } diff --git a/src/commands/settings/serverboard/invite.ts b/src/commands/settings/serverboard/invite.ts index 920b2d6..392f6b1 100644 --- a/src/commands/settings/serverboard/invite.ts +++ b/src/commands/settings/serverboard/invite.ts @@ -3,7 +3,7 @@ import { type ChatInputCommandInteraction } from "discord.js"; import { genColor } from "../../../utils/colorGen.js"; -import errorEmbed from "../../../utils/embeds/errorEmbed.js"; +import { errorEmbed } from "../../../utils/embeds/errorEmbed.js"; import { getServerboardTable } from "../../../utils/database.js"; import { QuickDB } from "quick.db"; @@ -21,29 +21,25 @@ export default class Toggle { async run(interaction: ChatInputCommandInteraction) { const db = this.db; const serverbTable = await getServerboardTable(db); - const rulesChannel = interaction.guild?.rulesChannel; const guild = interaction.guild; const member = guild.members.cache.get(interaction.user.id); - if (!member.permissions.has(PermissionsBitField.Flags.ManageGuild)) - return await interaction.followUp({ embeds: [errorEmbed("You need **Manage Server** permissions to add an invite.")] }); + if (!member.permissions.has(PermissionsBitField.Flags.ManageGuild)) return await interaction.followUp({ + embeds: [errorEmbed("You need **Manage Server** permissions to add an invite.")] + }); + + const invite = await serverbTable + ?.get(`${guild.id}.invite`) + .then(invite => String(invite)) + .catch(() => ""); - const invite = await serverbTable?.get(`${guild.id}.invite`).then( - (invite) => String(invite) - ).catch(() => ""); - if (Boolean(invite)) { - // Delete invite + if (invite) { let invites = await rulesChannel?.fetchInvites(); if (!rulesChannel) invites = await guild.invites.fetch(); - - if (invite) { - await invites.find(inv => inv.url == invite) - .delete("Serverboard invite disabled"); - } + if (invite) await invites.find(inv => inv.url === invite).delete("Serverboard invite disabled"); await serverbTable.delete(`${guild.id}.invite`); - const embed = new EmbedBuilder() .setTitle("✅ • Invite deleted!") .setColor(genColor(100)); @@ -51,13 +47,11 @@ export default class Toggle { return await interaction.followUp({ embeds: [embed] }); } - if (!rulesChannel) return await interaction.followUp({ embeds: [errorEmbed("You need a **rules channel** to create an invite.")] }); - - const newInvite = await rulesChannel.createInvite({ - maxAge: 0, - maxUses: 0, - reason: "Serverboard invite enabled" + if (!rulesChannel) return await interaction.followUp({ + embeds: [errorEmbed("You need a **rules channel** to create an invite.")] }); + + const newInvite = await rulesChannel.createInvite({ maxAge: 0, maxUses: 0, reason: "Serverboard invite enabled" }); await serverbTable.set(`${guild.id}.invite`, newInvite.url); const embed = new EmbedBuilder() diff --git a/src/commands/settings/serverboard/reveal.ts b/src/commands/settings/serverboard/reveal.ts index 5718317..491213c 100644 --- a/src/commands/settings/serverboard/reveal.ts +++ b/src/commands/settings/serverboard/reveal.ts @@ -3,8 +3,8 @@ import { type ChatInputCommandInteraction } from "discord.js"; import { genColor } from "../../../utils/colorGen.js"; -import errorEmbed from "../../../utils/embeds/errorEmbed.js"; -import database, { getServerboardTable } from "../../../utils/database.js"; +import { errorEmbed } from "../../../utils/embeds/errorEmbed.js"; +import { getServerboardTable } from "../../../utils/database.js"; import { QuickDB } from "quick.db"; export default class Reveal { diff --git a/src/commands/user/info.ts b/src/commands/user/info.ts new file mode 100644 index 0000000..291e323 --- /dev/null +++ b/src/commands/user/info.ts @@ -0,0 +1,74 @@ +import { + SlashCommandSubcommandBuilder, EmbedBuilder, type ColorResolvable, + type ChatInputCommandInteraction +} from "discord.js"; +import { genColor, genRGBColor } from "../../utils/colorGen.js"; +import { QuickDB } from "quick.db"; +import Vibrant from "node-vibrant"; +import sharp from "sharp"; + +export default class UserInfo { + data: SlashCommandSubcommandBuilder; + db: QuickDB; + + constructor() { + this.data = new SlashCommandSubcommandBuilder() + .setName("info") + .setDescription("Shows your (or another user's) info.") + .addUserOption(option => option + .setName("user") + .setDescription("Select the user.") + ); + } + + async run(interaction: ChatInputCommandInteraction) { + const user = interaction.options.getUser("user"); + const id = user ? user.id : interaction.member.user.id; + const selectedMember = interaction.guild.members.cache.filter(member => member.user.id === id).map(user => user)[0]; + const selectedUser = selectedMember.user; + + let embed = new EmbedBuilder() + .setAuthor({ + name: `• ${selectedMember.nickname == null ? selectedUser.username : selectedMember.nickname}${selectedUser.discriminator == "0" ? "" : `#${selectedUser.discriminator}`}`, + iconURL: selectedMember.displayAvatarURL() + }) + .setFields( + { + name: selectedUser.bot === false ? "👤 • User info" : "🤖 • Bot info", + value: [ + `**Username**: ${selectedUser.username}`, + `**Display name**: ${selectedUser.displayName === selectedUser.username ? "*None*" : selectedUser.displayName}`, + `**Created on** `, + ].join("\n"), + }, + { + name: "👥 • Member info", + value: `**Joined on** `, + } + ) + .setFooter({ text: `User ID: ${selectedMember.id}` }) + .setThumbnail(selectedMember.displayAvatarURL()) + .setColor(genColor(200)); + + try { + const imageBuffer = await (await fetch(selectedMember.displayAvatarURL())).arrayBuffer(); + const image = sharp(imageBuffer).toFormat("jpg"); + const { r, g, b } = (await new Vibrant(await image.toBuffer()).getPalette()).Vibrant; + embed.setColor(genRGBColor(r, g, b) as ColorResolvable); + } catch { } + + const guildRoles = interaction.guild.roles.cache.filter(role => selectedMember.roles.cache.has(role.id)); + const memberRoles = [...guildRoles].sort((role1, role2) => role2[1].position - role1[1].position); + memberRoles.pop(); + + if (memberRoles.length !== 0) embed.addFields({ + name: `🎭 • ${guildRoles.filter(role => selectedMember.roles.cache.has(role.id)).size - 1} ${memberRoles.length === 1 ? "role" : "roles"}`, + value: `${memberRoles + .slice(0, 5) + .map(role => `<@&${role[1].id}>`) + .join(", ")}${memberRoles.length > 5 ? ` **and ${memberRoles.length - 5} more**` : ""}`, + }); + + await interaction.followUp({ embeds: [embed] }); + } +} diff --git a/src/commands/user/level.ts b/src/commands/user/level.ts new file mode 100644 index 0000000..da9a301 --- /dev/null +++ b/src/commands/user/level.ts @@ -0,0 +1,109 @@ +import { SlashCommandSubcommandBuilder, EmbedBuilder, type ChatInputCommandInteraction } from "discord.js"; +import { genColor } from "../../utils/colorGen.js"; +import { getLevelingTable, getSettingsTable } from "../../utils/database.js"; +import { errorEmbed } from "../../utils/embeds/errorEmbed.js"; +import { QuickDB } from "quick.db"; +import { Reward } from "../settings/leveling/rewards.js"; + +export default class Level { + data: SlashCommandSubcommandBuilder; + db: QuickDB; + + constructor(db?: QuickDB) { + this.db = db; + this.data = new SlashCommandSubcommandBuilder() + .setName("level") + .setDescription("Shows your (or another user's) level.") + .addUserOption(option => option + .setName("user") + .setDescription("Select the user.") + ); + } + + async run(interaction: ChatInputCommandInteraction) { + const db = this.db; + const settingsTable = await getSettingsTable(db); + const levelingTable = await getLevelingTable(db); + + const user = interaction.options.getUser("user"); + const member = interaction.member; + const guild = interaction.guild; + + const id = user ? user.id : member.user.id; + const selectedMember = guild.members.cache.filter(member => member.user.id === id).map(user => user)[0]; + const avatarURL = selectedMember.displayAvatarURL(); + + const levelEnabled = await settingsTable + ?.get(`${guild.id}.leveling.enabled`) + .then(data => { + if (!data) return false; + return data; + }) + .catch(() => false); + + if (!levelEnabled) return await interaction.followUp({ + embeds: [errorEmbed("Leveling is disabled for this server.")] + }); + + const { exp, levels } = await levelingTable + ?.get(`${guild.id}.${selectedMember.id}`) + .then(data => { + if (!data) return { exp: 0, level: 0 }; + return { exp: parseInt(data.exp), levels: parseInt(data.levels) }; + }) + .catch(() => { return { exp: 0, levels: 0 } }); + + const formattedExp = exp?.toLocaleString("en-US"); + + if (!exp && !levels) await levelingTable.set(`${guild.id}.${selectedMember.id}`, { levels: 0, exp: 0 }); + + let rewards = []; + let nextReward = null; + const levelRewards = await settingsTable + ?.get(`${interaction.guild.id}.leveling.rewards`) + .then(data => { + if (!data) return [] as Reward[] ?? [] as Reward[]; + return data as Reward[] ?? [] as Reward[]; + }) + .catch(() => [] as Reward[]); + + for (const { roleId, level } of levelRewards) { + const role = await interaction.guild.roles.fetch(roleId).catch(() => {}); + const reward = { roleId, level }; + + if (levels < level) { + if (nextReward) break; + nextReward = reward; + break; + } + + rewards.push(role); + } + + const expUntilLevelup = Math.floor((2 * 50) * 1.25 * ((levels ?? 0) + 1)); + const formattedExpUntilLevelup = expUntilLevelup?.toLocaleString("en-US"); + const levelUpEmbed = new EmbedBuilder() + .setAuthor({ name: `• ${selectedMember.user.username}`, iconURL: avatarURL }) + .setFields( + { + name: `⚡ • Level ${levels ?? 0}`, + value: [ + `**Exp**: ${formattedExp ?? 0}/${formattedExpUntilLevelup} until level up`, + `**Next Level**: ${(levels ?? 0) + 1}` + ].join("\n") + }, + { + name: `🎁 • ${rewards.length} Rewards`, + value: [ + `${rewards.length > 0 ? rewards.map(reward => `<@&${reward.id}>`).join(" ") : "No rewards unlocked"}`, + nextReward ? `**Upcoming reward**: <@&${nextReward.roleId}>` : "**Upcoming reward**: *Cricket, cricket, cricket* - Looks like you claimed everything!" + ].join("\n") + } + ) + .setThumbnail(avatarURL) + .setTimestamp() + .setColor(genColor(200)); + + await interaction.followUp({ embeds: [levelUpEmbed] }); + } +} diff --git a/src/events/easterEggs/Bread.ts b/src/events/easterEggs/Bread.ts index 0f30abc..5d6a36c 100644 --- a/src/events/easterEggs/Bread.ts +++ b/src/events/easterEggs/Bread.ts @@ -1,12 +1,10 @@ import type { Message } from "discord.js"; -import multiReact from "../../utils/multiReact.js"; +import { multiReact } from "../../utils/multiReact.js"; export default class Bread { async run(message: Message) { const gif = "https://tenor.com/bOMAb.gif"; const randomizedChance = Math.round(Math.random() * 100); - let oddsForGif = 0.25; - const breadSplit = message.content.toLowerCase().split("bread"); if (breadSplit[1] == null) return; @@ -14,7 +12,7 @@ export default class Bread { ((breadSplit[0].endsWith(" ") || breadSplit[0].endsWith("")) && breadSplit[1].startsWith(" ")) || message.content.toLowerCase() === "bread" ) { - if (randomizedChance <= oddsForGif) message.channel.send(gif); + if (randomizedChance <= 0.25) message.channel.send(gif); else await multiReact(message, "🍞🇧🇷🇪🇦🇩👍"); } } diff --git a/src/events/easterEggs/Fan.ts b/src/events/easterEggs/Fan.ts index c48099b..80169b9 100644 --- a/src/events/easterEggs/Fan.ts +++ b/src/events/easterEggs/Fan.ts @@ -1,5 +1,5 @@ import type { Message } from "discord.js"; -import randomise from "../../utils/randomise.js"; +import { randomise } from "../../utils/randomise.js"; export default class Fan { async run(message: Message) { diff --git a/src/events/easterEggs/Fireship.ts b/src/events/easterEggs/Fireship.ts index 2fd283c..5317bf1 100644 --- a/src/events/easterEggs/Fireship.ts +++ b/src/events/easterEggs/Fireship.ts @@ -8,9 +8,8 @@ export default class FireShip { content.startsWith("this has been") && content.endsWith("in 100 seconds") && message.content !== "this has been in 100 seconds" - ) - await message.channel.send( - "hit the like button and subscribe if you want to see more short videos like this thanks for watching and I will see you in the next one" - ); + ) await message.channel.send( + "hit the like button and subscribe if you want to see more short videos like this thanks for watching and I will see you in the next one" + ); } } diff --git a/src/events/easterEggs/WhoPinged.ts b/src/events/easterEggs/WhoPinged.ts index 94b5d5d..4fa5152 100644 --- a/src/events/easterEggs/WhoPinged.ts +++ b/src/events/easterEggs/WhoPinged.ts @@ -1,5 +1,5 @@ import type { Message } from "discord.js"; -import randomise from "../../utils/randomise.js"; +import { randomise } from "../../utils/randomise.js"; export default class WhoPinged { async run(message: Message) { diff --git a/src/events/guildCreate.ts b/src/events/guildCreate.ts index 4b707a9..f914978 100644 --- a/src/events/guildCreate.ts +++ b/src/events/guildCreate.ts @@ -1,7 +1,10 @@ -import { EmbedBuilder, type Client, type Guild, DMChannel } from "discord.js"; -import Commands from "../handlers/commands.js"; -import randomise from "../utils/randomise.js"; +import { + EmbedBuilder, type DMChannel, type Client, + type Guild +} from "discord.js"; import { genColor } from "../utils/colorGen.js"; +import { randomise } from "../utils/randomise.js"; +import Commands from "../handlers/commands.js"; export default { name: "guildCreate", @@ -14,23 +17,22 @@ export default { async run(guild: Guild) { const owner = await guild.fetchOwner(); - const dmChannel = (await owner.createDM().catch(() => null)) as DMChannel | null;; + const dmChannel = (await owner.createDM().catch(() => null)) as DMChannel | null; const hearts = ["💖", "💝", "💓", "💗", "💘", "💟", "💕", "💞"]; const embed = new EmbedBuilder() .setTitle("👋 • Welcome to Nebula!") .setDescription([ "Nebula is a multiplatform, multipurpose bot with the ability to add extensions to have additional features.", - "To do things like disabling/enabling commands, use the **/settings** command.", - "As of now, it's in an early stage of development. If you find bugs, please go to our [official server](https://discord.gg/7RdABJhQss) to report them." + "To manage the bot, sign into the [dashboard](https://dash.nebulabot.org).", + "Nebula is in an early stage of development. If you find bugs, please go to our [official server](https://discord.gg/7RdABJhQss) and report them." ].join("\n\n")) .setFooter({ text: `Made by the Nebula team with ${randomise(hearts)}` }) .setColor(genColor(200)); - if (dmChannel) await dmChannel.send({ embeds: [embed] }); - const commands = new Commands(guild.client); await commands.registerCommandsForGuild(guild); + if (dmChannel) await dmChannel.send({ embeds: [embed] }); } } } diff --git a/src/events/guildMemberAdd.ts b/src/events/guildMemberAdd.ts new file mode 100644 index 0000000..927467e --- /dev/null +++ b/src/events/guildMemberAdd.ts @@ -0,0 +1,44 @@ +import { + EmbedBuilder, type Client, type GuildMember, + type TextChannel, type ColorResolvable +} from "discord.js"; +import { genColor, genRGBColor } from "../utils/colorGen.js"; +import Vibrant from "node-vibrant"; +import sharp from "sharp"; + +export default { + name: "guildMemberAdd", + event: class GuildMemberAdd { + client: Client; + + constructor(client: Client) { + this.client = client; + } + + async run(member: GuildMember) { + const id = "1079612083307548704"; + const channel = await member.guild.channels.cache.find(channel => channel.id === id).fetch() as TextChannel; + const avatarURL = member.displayAvatarURL(); + + const embed = new EmbedBuilder() + .setAuthor({ + name: `• ${member.nickname == null ? member.user.username : member.nickname}`, + iconURL: avatarURL + }) + .setTitle("Welcome!") + .setDescription(`Enjoy your stay in **${member.guild.name}**!`) + .setFooter({ text: `User ID: ${member.id}` }) + .setThumbnail(avatarURL) + .setColor(genColor(200)); + + try { + const imageBuffer = await (await fetch(avatarURL)).arrayBuffer(); + const image = sharp(imageBuffer).toFormat("jpg"); + const { r, g, b } = (await new Vibrant(await image.toBuffer()).getPalette()).Vibrant; + embed.setColor(genRGBColor(r, g, b) as ColorResolvable); + } catch {} + + await channel.send({ embeds: [embed] }); + } + } +} diff --git a/src/events/guildMemberRemove.ts b/src/events/guildMemberRemove.ts new file mode 100644 index 0000000..6e1d9a7 --- /dev/null +++ b/src/events/guildMemberRemove.ts @@ -0,0 +1,44 @@ +import { + EmbedBuilder, type Client, type GuildMember, + type TextChannel, type ColorResolvable +} from "discord.js"; +import { genColor, genRGBColor } from "../utils/colorGen.js"; +import Vibrant from "node-vibrant"; +import sharp from "sharp"; + +export default { + name: "guildMemberRemove", + event: class GuildMemberRemove { + client: Client; + + constructor(client: Client) { + this.client = client; + } + + async run(member: GuildMember) { + const id = "1079612083307548704"; + const channel = await member.guild.channels.cache.find(channel => channel.id === id).fetch() as TextChannel; + const avatarURL = member.displayAvatarURL(); + + const embed = new EmbedBuilder() + .setAuthor({ + name: `• ${member.nickname == null ? member.user.username : member.nickname}`, + iconURL: avatarURL + }) + .setTitle("Goodbye!") + .setDescription(`**@${member.user.username}** has left the server 😥`) + .setFooter({ text: `User ID: ${member.id}` }) + .setThumbnail(avatarURL) + .setColor(genColor(200)); + + try { + const imageBuffer = await (await fetch(avatarURL)).arrayBuffer(); + const image = sharp(imageBuffer).toFormat("jpg"); + const { r, g, b } = (await new Vibrant(await image.toBuffer()).getPalette()).Vibrant; + embed.setColor(genRGBColor(r, g, b) as ColorResolvable); + } catch {} + + await channel.send({ embeds: [embed] }); + } + } +} diff --git a/src/events/interactionCreate.ts b/src/events/interactionCreate.ts new file mode 100644 index 0000000..a4f092a --- /dev/null +++ b/src/events/interactionCreate.ts @@ -0,0 +1,54 @@ +import type { CommandInteraction, Client, AutocompleteInteraction } from "discord.js"; +import type { QuickDB } from "quick.db"; +import { pathToFileURL } from "url"; +import { join } from "path"; +import { database } from "../utils/database.js"; + +async function getCommand(interaction: CommandInteraction | AutocompleteInteraction, options: any, db: QuickDB): Promise { + const commandName = interaction.commandName; + const subcommandName = options.getSubcommand(false); + const commandgroupName = options.getSubcommandGroup(false); + + const commandImportPath = join( + join(process.cwd(), "src", "commands"), + `${subcommandName ? `${commandName}/${commandgroupName ? `${commandgroupName}/${subcommandName}` : subcommandName}` : commandName}.ts` + ); + + return new (await import(pathToFileURL(commandImportPath).toString())).default(db); +} + +export default { + name: "interactionCreate", + event: class InteractionCreate { + commands: CommandInteraction; + client: Client; + lastOpenedDb = Date.now(); + db: QuickDB = null; + + constructor(cmds: CommandInteraction, client: Client) { + this.commands = cmds; + this.client = client; + this.lastOpenedDb = Date.now(); + } + + async run(interaction: CommandInteraction | AutocompleteInteraction) { + if (!this.db) this.db = await database(); + if (Date.now() - this.lastOpenedDb > 1000 * 60 * 60) { + this.db = await database(); + this.lastOpenedDb = Date.now(); + } + + if (interaction.isChatInputCommand()) { + const command = await getCommand(interaction, interaction.options, this.db); + if (!command) return; + if (command?.deferred ?? true) await interaction.deferReply(); + command.run(interaction); + } else if (interaction.isAutocomplete()) { + const command = await getCommand(interaction, interaction.options, this.db); + if (!command) return; + if (!command.autocomplete) return; + command.autocomplete(interaction); + } + } + } +} diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts new file mode 100644 index 0000000..7510d05 --- /dev/null +++ b/src/events/messageCreate.ts @@ -0,0 +1,127 @@ +import { EmbedBuilder, type TextChannel, type Message } from "discord.js"; +import { pathToFileURL } from "url"; +import { join } from "path"; +import { readdirSync } from "fs"; +import { genColor } from "../utils/colorGen.js"; +import { database, getLevelingTable, getSettingsTable } from "../utils/database.js"; +import { Reward } from "../commands/settings/leveling/rewards.js"; + +export default { + name: "messageCreate", + event: class MessageCreate { + db = null; + + async run(message: Message) { + const target = message.author; + const guild = message.guild; + + // Easter egg handler + if (target.bot) return; + if (guild.id !== "903852579837059113") return; + const eventsPath = join(process.cwd(), "src", "events", "easterEggs"); + + for (const easterEggFile of readdirSync(eventsPath)) { + const msg = await import(pathToFileURL(join(eventsPath, easterEggFile)).toString()); + new msg.default().run(message, ...message.content); + } + + // Levelling + if (!this.db) this.db = await database(); + const db = this.db; + const levelingTable = await getLevelingTable(db); + const settingsTable = await getSettingsTable(db); + const expPerMessage = 2; + const baseExpForNewLevel = 2 * 50; + const difficultyMultiplier = 1.25; + + const levelingEnabled = await settingsTable + ?.get(`${guild.id}.leveling.enabled`) + .then(enabled => !!enabled) + .catch(() => false); + + if (!levelingEnabled) return; + + const { exp, levels } = await levelingTable + ?.get(`${guild.id}.${target.id}`) + .catch(() => { return { exp: 0, levels: 0 } }); + + const { exp: expGlobal, levels: levelsGlobal } = await levelingTable + ?.get(`global.${target.id}`) + .then(data => { + if (!data) return { exp: 0, levels: 0 }; + return { exp: parseInt(data.exp), levels: parseInt(data.levels) }; + }) + .catch(() => { return { exp: 0, levels: 0 } }); + + const expUntilLevelup = Math.floor(baseExpForNewLevel * difficultyMultiplier * (levels + 1)); + const expUntilLevelupGlobal = Math.floor(baseExpForNewLevel * difficultyMultiplier * (levelsGlobal + 1)); + const expUntilNextLevelup = Math.floor(baseExpForNewLevel * difficultyMultiplier * (levels + 2)); + + const newLevelData = { levels: levels ?? 0, exp: (exp ?? 0) + expPerMessage }; + const newLevelDataGlobal = { levels: levelsGlobal ?? 0, exp: (expGlobal ?? 0) + expPerMessage }; + + if (!(exp >= expUntilLevelup - 1)) { + await levelingTable.set(`global.${target.id}`, newLevelDataGlobal); + return await levelingTable.set(`${guild.id}.${target.id}`, newLevelDataGlobal); + } else if (exp >= expUntilLevelup - 1) { + let leftOverExp = exp - expUntilLevelup; + if (leftOverExp < 0) leftOverExp = 0; + + newLevelData.levels = levels + 1; + newLevelData.exp = leftOverExp ?? 0; + + await levelingTable.set(`${guild.id}.${target.id}`, newLevelData); + } + + if (exp >= expUntilLevelupGlobal - 1) { + let leftOverExpGlobal = exp - expUntilLevelup; + if (leftOverExpGlobal < 0) leftOverExpGlobal = 0; + + newLevelDataGlobal.levels = levels + 1; + newLevelDataGlobal.exp = leftOverExpGlobal + 1; + + await levelingTable.set(`global.${target.id}`, newLevelDataGlobal); + } + + const levelChannelId = await settingsTable + ?.get(`${guild.id}.leveling.channel`) + .then(channelId => String(channelId)) + .catch(() => null); + + if (!levelChannelId) return; + const levelChannel = guild.channels.cache.get(levelChannelId) as TextChannel; + + const leveledEmbed = new EmbedBuilder() + .setAuthor({ name: target.displayName, iconURL: target.avatarURL() }) + .setTitle("⚡ • Level Up!") + .setDescription([ + `**Congratulations <@${target.id}>**!`, + `You made it to **level ${levels + 1}**`, + `You need ${expUntilNextLevelup} exp to level up again.` + ].join("\n")) + .setThumbnail(target.avatarURL()) + .setTimestamp() + .setColor(genColor(200)); + + levelChannel.send({ embeds: [leveledEmbed], content: `<@${target.id}>` }); + + const levelRewards = await settingsTable + ?.get(`${guild.id}.leveling.rewards`) + .then(rewards => rewards as Reward[] ?? [] as Reward[]) + .catch(() => [] as Reward[]); + + const members = await guild.members.fetch(); + for (const { level, roleId } of levelRewards) { + const role = guild.roles.cache.get(roleId); + const targetRoles = members.get(target.id)?.roles; + + if (levels >= level) { + await targetRoles.add(role); + continue; + } + + await targetRoles.remove(role); + } + } + } +} diff --git a/src/handlers/commands.ts b/src/handlers/commands.ts index 6e1e78e..20808a7 100644 --- a/src/handlers/commands.ts +++ b/src/handlers/commands.ts @@ -1,8 +1,11 @@ -import { SlashCommandBuilder, SlashCommandSubcommandGroupBuilder, Guild, type Client } from "discord.js"; +import { + SlashCommandBuilder, SlashCommandSubcommandGroupBuilder, Guild, + type Client +} from "discord.js"; import { pathToFileURL } from "url"; import { join } from "path"; import { readdirSync } from "fs"; -import database, { getSettingsTable } from "../utils/database.js"; +import { database, getSettingsTable } from "../utils/database.js"; import { QuickDB } from "quick.db"; const COMMANDS_PATH = join(process.cwd(), "src", "commands"); @@ -15,29 +18,19 @@ export default class Commands { this.client = client; } - // Load the commands into this.commands private async createSubCommand(name: string, ...disabledCommands: string[]): Promise { const command = new SlashCommandBuilder() .setName(name.toLowerCase()) .setDescription("This command has no description."); - // Fetch the subcommands - const subCommandFiles = readdirSync(join(COMMANDS_PATH, name), { withFileTypes: true }); - - // Add the subcommands to the top command - for (const subCommandFile of subCommandFiles) { + for (const subCommandFile of readdirSync(join(COMMANDS_PATH, name), { withFileTypes: true })) { const subCommandName = subCommandFile.name.replaceAll(".ts", ""); - if ( - disabledCommands?.find( - (command) => command?.split("/")?.[0] == name && command?.split("/")?.[1] == subCommandName - ) - ) continue; + if (disabledCommands?.find(command => command?.split("/")?.[0] == name && command?.split("/")?.[1] == subCommandName)) + continue; if (subCommandFile.isFile()) { const subCommand = await import(pathToFileURL(join(COMMANDS_PATH, name, subCommandFile.name)).toString()); - try { - command.addSubcommand(new subCommand.default().data); - } catch {} + command.addSubcommand(new subCommand.default().data); continue; } @@ -46,17 +39,13 @@ export default class Commands { .setDescription("This subcommand group has no description."); const subCommandGroupFiles = readdirSync(join(COMMANDS_PATH, name, subCommandFile.name), { withFileTypes: true }); - for (const subCommandGroupFile of subCommandGroupFiles) { if (!subCommandGroupFile.isFile()) continue; - if ( - disabledCommands?.find( - (command) => - command?.split("/")?.[0] == name && - command?.split("/")?.[1] == subCommandFile.name.replaceAll(".ts", "") && - command?.split("/")?.[2] == subCommandGroupFile.name.replaceAll(".ts", "") - ) - ) continue; + if (disabledCommands?.find(command => + command?.split("/")?.[0] == name && + command?.split("/")?.[1] == subCommandFile.name.replaceAll(".ts", "") && + command?.split("/")?.[2] == subCommandGroupFile.name.replaceAll(".ts", "") + )) continue; const subCommand = await import( pathToFileURL(join(COMMANDS_PATH, name, subCommandFile.name, subCommandGroupFile.name)).toString() @@ -78,38 +67,34 @@ export default class Commands { if (disabledCommands?.includes(name.replaceAll(".ts", ""))) continue; if (commandFile.isFile()) { - // Add the commands it found to the list const command = await import(pathToFileURL(join(COMMANDS_PATH, name)).toString()); this.commands.push(new command.default().data); continue; } - // Folder found -> Make subcommand - // Create a top subcommand const subCommand = await this.createSubCommand(name, join(COMMANDS_PATH, name), ...disabledCommands); this.commands.push(subCommand); } } - // Register the commands for a specific guild async registerCommandsForGuild(guild: Guild, ...disabledCommands: string[]) { await this.loadCommands(...disabledCommands); await guild.commands.set(this.commands); } - // Register the commands for all guilds async registerCommands(): Promise { const db = await database(); await this.loadCommands(); const settingsTable = await getSettingsTable(db); const guilds = this.client.guilds.cache; - // Adding the commands to the guilds console.log("Adding commands to guilds..."); for (const guildID of guilds.keys()) { - const disabledCommands = await settingsTable?.get(`${guildID}.disabledCommands`).then( - (disabledCommands: string[]) => disabledCommands as string[] ?? [] as string[] - ).catch(() => [] as string[]); + const disabledCommands = await settingsTable + ?.get(`${guildID}.disabledCommands`) + .then((disabledCommands: string[]) => disabledCommands as string[] ?? [] as string[]) + .catch(() => [] as string[]); + if (disabledCommands.length > 0) await this.loadCommands(...disabledCommands); await guilds.get(guildID)?.commands.set(this.commands); } diff --git a/src/handlers/events.ts b/src/handlers/events.ts index c7964f9..3b36224 100644 --- a/src/handlers/events.ts +++ b/src/handlers/events.ts @@ -14,7 +14,7 @@ export default class Events { const eventsPath = join(process.cwd(), "src", "events"); for (const eventFile of readdirSync(eventsPath)) { - if (!eventFile.endsWith("js")) continue; + if (!eventFile.endsWith("ts")) continue; const event = await import(pathToFileURL(join(eventsPath, eventFile)).toString()); const clientEvent = this.client.on(event.default.name, new event.default.event(this.client).run); diff --git a/src/index.ts b/src/index.ts index 969a9d9..e4be99a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,6 @@ import { ShardingManager } from "discord.js"; const manager = new ShardingManager("./src/bot.ts", { token: process.env.TOKEN }); - manager.on("shardCreate", shard => { shard.on("error", err => console.error(err)); console.log(`Launched shard ${shard.id}`); diff --git a/src/utils/colorGen.ts b/src/utils/colorGen.ts index 018a46d..0ebb93c 100644 --- a/src/utils/colorGen.ts +++ b/src/utils/colorGen.ts @@ -33,9 +33,9 @@ export function genRGBColor(r, g, b) { g = g.toString(16); b = b.toString(16); - if (r.length === 1) r = "0" + r; - if (g.length === 1) g = "0" + g; - if (b.length === 1) b = "0" + b; + if (r.length === 1) r = `0${r}`; + if (g.length === 1) g = `0${g}`; + if (b.length === 1) b = `0${b}`; return `#${r}${g}${b}`; } diff --git a/src/utils/database.ts b/src/utils/database.ts index 52e5764..1a2f2fc 100644 --- a/src/utils/database.ts +++ b/src/utils/database.ts @@ -1,10 +1,10 @@ import { QuickDB, MySQLDriver } from "quick.db"; -async function database() { +export async function database() { const mysql = new MySQLDriver({ host: process.env.MYSQL_HOST, user: process.env.MYSQL_USERNAME, - port: Number(process.env.MYSQL_PORT), + port: parseInt(process.env.MYSQL_PORT), password: process.env.MYSQL_PASSWORD, database: process.env.MYSQL_DATABASE, enableKeepAlive: true, @@ -21,5 +21,3 @@ export const getLevelingTable = async (db: QuickDB) => await db.tableAsync( export const getServerboardTable = async (db: QuickDB) => await db.tableAsync("serverboard"); export const getNewsTable = async (db: QuickDB) => await db.tableAsync("news"); export const getModerationTable = async (db: QuickDB) => await db.tableAsync("moderation"); - -export default database; diff --git a/src/utils/embeds/errorEmbed.ts b/src/utils/embeds/errorEmbed.ts index f4255d6..21d51b0 100644 --- a/src/utils/embeds/errorEmbed.ts +++ b/src/utils/embeds/errorEmbed.ts @@ -6,7 +6,7 @@ import { genColor } from "../colorGen.js"; * @param description Description of the error. * @returns Embed with the error description. */ -export default function errorEmbed(description: string) { +export function errorEmbed(description: string) { return new EmbedBuilder() .setTitle("❌ • Error!") .setDescription(description) diff --git a/src/utils/embeds/serverEmbed.ts b/src/utils/embeds/serverEmbed.ts index 589bbab..86004cf 100644 --- a/src/utils/embeds/serverEmbed.ts +++ b/src/utils/embeds/serverEmbed.ts @@ -1,8 +1,8 @@ -import { ColorResolvable, EmbedBuilder, type Guild } from "discord.js"; +import { EmbedBuilder, type ColorResolvable, type Guild } from "discord.js"; import { genColor, genRGBColor } from "../colorGen.js"; +import { database, getServerboardTable } from "../database.js"; import Vibrant from "node-vibrant"; import sharp from "sharp"; -import database, { getServerboardTable } from "../database.js"; type Options = { guild: Guild @@ -19,114 +19,89 @@ type Options = { * @param options Options of the embed. * @returns Embed that contains the guild info. */ -export default async function serverEmbed(options: Options) { - const db = await database(); - +export async function serverEmbed(options: Options) { const page = options.page; const pages = options.pages; - - // Retrieve guild information const guild = options.guild; const { premiumTier: boostTier, premiumSubscriptionCount: boostCount } = guild; - const iconURL = guild.iconURL(); - // Getting the invite table - const showInvite = options.showInvite; - const serverbTable = await getServerboardTable(db); - const invite = await serverbTable?.get(`${guild.id}.invite`).then( - async (invite) => invite ? String(invite) : null - ).catch(() => null); + const invite = (await getServerboardTable(await database())) + ?.get(`${guild.id}.invite`) + .then(async invite => invite ? String(invite) : null) + .catch(() => null); - // Getting different types of members const members = guild.members.cache; const boosters = members.filter(member => member.premiumSince); const onlineMembers = members.filter(member => ["online", "dnd", "idle"].includes(member.presence?.status)).size; const bots = members.filter(member => member.user.bot); - - // Formatting numbers to the American comma format - const formattedMemberCount = guild.memberCount?.toLocaleString("en-US"); - const formattedOnlineMembers = onlineMembers?.toLocaleString("en-US"); const formattedUserCount = (guild.memberCount - bots.size)?.toLocaleString("en-US"); - const formattedBotCount = bots.size?.toLocaleString("en-US"); - // Sorting the roles const roles = guild.roles.cache; - const rolesSorted = [...roles].sort((role1, role2) => role2[1].position - role1[1].position); - rolesSorted.pop(); + const sortedRoles = [...roles].sort((role1, role2) => role2[1].position - role1[1].position); + sortedRoles.pop(); - // Organising the channel sizes const channels = guild.channels.cache; const channelSizes = { text: channels.filter(channel => channel.type === 0 || channel.type === 15 || channel.type === 5).size, voice: channels.filter(channel => channel.type === 2 || channel.type === 13).size, categories: channels.filter(channel => channel.type === 4).size - } + }; - // Create the embed const generalValues = [ - `**Owner**: <@${guild.ownerId}>`, - `**Created on** `, - ] + `**Owned by** ${(await guild.fetchOwner()).user.displayName}`, + `**Created on** ` + ]; if (options.showSubs) generalValues.push(`**Subscribers**: ${options.subs}`); - if (showInvite && invite) generalValues.push(`**Invite link**: ${invite}`); + if (options.showInvite && invite === null) generalValues.push(`**Invite link**: ${await invite}`); const embed = new EmbedBuilder() - .setAuthor({ - name: `${pages ? `#${page} • ` : ""}${guild.name}`, - url: showInvite && invite ? invite : null, - iconURL: iconURL - }) + .setAuthor({ name: `${pages ? `#${page} • ` : ""}${guild.name}`, iconURL: guild.iconURL() }) .setDescription(guild.description ? guild.description : null) - .setFields({ - name: "📃 • General", - value: generalValues.join("\n") - }) + .setFields({ name: "📃 • General", value: generalValues.join("\n") }) .setFooter({ text: `Server ID: ${guild.id}${pages ? ` • Page ${page}/${pages}` : ""}` }) - .setThumbnail(iconURL) + .setThumbnail(guild.iconURL()) .setColor(genColor(200)); - // Set the embed color try { - const imageBuffer = await (await fetch(iconURL)).arrayBuffer(); // Stream the buffer + const imageBuffer = await (await fetch(guild.iconURL())).arrayBuffer(); const image = sharp(imageBuffer).toFormat("jpg"); - const { r, g, b } = (await new Vibrant(await image.toBuffer()).getPalette()).Vibrant; // Get the most vibrant color + const { r, g, b } = (await new Vibrant(await image.toBuffer()).getPalette()).Vibrant; embed.setColor(genRGBColor(r, g, b) as ColorResolvable); - } catch { } + } catch {} - // Adding the fields if (options.roles) embed.addFields({ name: `🎭 • ${roles.size - 1} ${roles.size === 1 ? "role" : "roles"}`, value: roles.size === 1 ? "*None*" - : `${rolesSorted.slice(0, 5).map(role => `<@&${role[0]}>`).join(", ")}${roles.size > 5 ? ` **and ${roles.size - 5} more**` : ""}` - }) + : `${sortedRoles.slice(0, 5).map(role => `<@&${role[0]}>`).join(", ")}${roles.size > 5 ? ` **and ${roles.size - 5} more**` : ""}` + }); embed.addFields( { - name: `👥 • ${formattedMemberCount} members`, + name: `👥 • ${guild.memberCount?.toLocaleString("en-US")} members`, value: [ - `${formattedUserCount} users • ${formattedBotCount} bots`, - `${formattedOnlineMembers} online` + `**${formattedUserCount}** users • **${bots.size?.toLocaleString("en-US")}** bots`, + `**${onlineMembers?.toLocaleString("en-US")}** online` ].join("\n"), inline: true }, { name: `🗨️ • ${channelSizes.text + channelSizes.voice} channels`, value: [ - `${channelSizes.text} text • ${channelSizes.voice} voice`, - `${channelSizes.categories} categories` + `**${channelSizes.text}** text • **${channelSizes.voice}** voice`, + `**${channelSizes.categories}** categories` ].join("\n"), inline: true }, { - name: `🌟 • ${boostCount}${boostTier === 0 ? "/2" : boostTier === 1 ? "/7" : boostTier === 2 ? "/14" : ""} boosts`, + name: `🌟 • ${boostTier == 0 ? "No level" : `Level ${boostTier}`}`, value: [ - boostTier == 0 ? "No boosts" : `Level ${boostTier}`, - `${boosters.size} ${boosters.size === 1 ? "booster" : "boosters"}` + `**${boostCount}**${boostTier === 0 ? "/2" : boostTier === 1 ? "/7" : boostTier === 2 ? "/14" : ""} boosts`, + `**${boosters.size}** ${boosters.size === 1 ? "booster" : "boosters"}` ].join("\n"), inline: true } - ) + ); return embed; } diff --git a/src/utils/multiReact.ts b/src/utils/multiReact.ts index 03d5be5..b91c968 100644 --- a/src/utils/multiReact.ts +++ b/src/utils/multiReact.ts @@ -1,6 +1,6 @@ import type { Message } from "discord.js"; -export default async function multiReact(message: Message, ...reactions) { +export async function multiReact(message: Message, ...reactions) { for (const i of reactions) { if (typeof i === "object") { await message.react(i); diff --git a/src/utils/quickSort.ts b/src/utils/quickSort.ts index 9aa40b2..907735b 100644 --- a/src/utils/quickSort.ts +++ b/src/utils/quickSort.ts @@ -1,18 +1,15 @@ type Corresponding = [...any[]] | null; -// Swap the values function swap(sortItems: number[], corresponding: Corresponding, leftIndex: number, rightIndex: number) { let temp = sortItems[leftIndex]; sortItems[leftIndex] = sortItems[rightIndex]; sortItems[rightIndex] = temp; - // Swap the corresponding values if (!corresponding) return; for (let i = 0; i < corresponding.length; i++) { let subArray = corresponding[i]; if (Array.isArray(subArray)) { - // Swap the corresponding sub-array values let tempValue = subArray[leftIndex]; subArray[leftIndex] = subArray[rightIndex]; subArray[rightIndex] = tempValue; @@ -20,56 +17,39 @@ function swap(sortItems: number[], corresponding: Corresponding, leftIndex: numb } } -// Partition the array -function partition(sortItems: number[], corresponding: Corresponding, leftIndex: number, rightIndex: number): number { - let pivot: number = sortItems[Math.floor((rightIndex + leftIndex) / 2)], // middle element - leftPointer: number = leftIndex, // left pointer - rightPointer: number = rightIndex; // right pointer - - while (leftPointer <= rightPointer) { - while (sortItems[leftPointer] < pivot) leftPointer++; - while (sortItems[rightPointer] > pivot) rightPointer--; - - if (leftPointer <= rightPointer) { - swap(sortItems, corresponding, leftPointer, rightPointer); // swapping two elements - leftPointer++; - rightPointer--; - } - } - - return leftPointer; -} - /** * Sorts an array of items and returns the sorted items and corresponding items * @param sortItems The items to sort - * @param corresponding The corresponding items to sort, but linked to the exact index of the sortItems array (input [[...], [...], ...]) (Optional) + * @param corresponding The corresponding items to sort * @param leftIndex The left index of the sortItems array * @param rightIndex The right index of the sortItems array * @returns The sorted items and corresponding items */ -// Initialize the quicksort function -export default function quickSort( +export function quickSort( sortItems: number[], corresponding: Corresponding, leftIndex: number, rightIndex: number ): [number[], Corresponding] { - let index: number; + let pivot = sortItems[Math.floor((rightIndex + leftIndex) / 2)]; + let leftPointer = leftIndex; + let rightPointer = rightIndex; - if (sortItems.length > 1) { - index = partition(sortItems, corresponding, leftIndex, rightIndex); // index returned from partition + while (leftPointer <= rightPointer) { + while (sortItems[leftPointer] < pivot) leftPointer++; + while (sortItems[rightPointer] > pivot) rightPointer--; - if (leftIndex < index - 1) { - // more elements on the left side of the pivot - quickSort(sortItems, corresponding, leftIndex, index - 1); + if (leftPointer <= rightPointer) { + swap(sortItems, corresponding, leftPointer, rightPointer); + leftPointer++; + rightPointer--; } + } - if (index < rightIndex) { - // more elements on the right side of the pivot - quickSort(sortItems, corresponding, index, rightIndex); - } + if (sortItems.length > 1) { + if (leftIndex < leftPointer - 1) quickSort(sortItems, corresponding, leftIndex, leftPointer - 1); + if (leftPointer < rightIndex) quickSort(sortItems, corresponding, leftPointer, rightIndex); } - return [sortItems, corresponding]; // Return both sorted items and corresponding array + return [sortItems, corresponding]; } diff --git a/src/utils/randomise.ts b/src/utils/randomise.ts index 9df4f35..35d8bad 100644 --- a/src/utils/randomise.ts +++ b/src/utils/randomise.ts @@ -3,6 +3,6 @@ * @param array Array to randomise. * @returns Randomised value from within the array. */ -export default function randomise(array: any[]) { +export function randomise(array: any[]) { return array[Math.floor(Math.random() * array.length)]; } diff --git a/src/utils/sendChannelNews.ts b/src/utils/sendChannelNews.ts index 9bc231e..92682d2 100644 --- a/src/utils/sendChannelNews.ts +++ b/src/utils/sendChannelNews.ts @@ -2,7 +2,7 @@ import { EmbedBuilder, Guild, Role, TextChannel } from "discord.js"; -import database, { getNewsTable } from "./database.js"; +import { database, getNewsTable } from "./database.js"; import { genColor } from "./colorGen.js"; export type News = { @@ -16,12 +16,12 @@ export type News = { messageId?: string } -export default async function sendChannelNews(guild: Guild, news: News, id: string) { +export async function sendChannelNews(guild: Guild, news: News, id: string) { const db = await database(); const newsTable = await getNewsTable(db); const subscribedChannel = await newsTable.get(`${guild.id}.channel`).then( - (channel) => channel as { channelId: string | null, roleId: string | null } + channel => channel as { channelId: string | null, roleId: string | null } ).catch(() => { return { channelId: null as string | null, @@ -50,4 +50,4 @@ export default async function sendChannelNews(guild: Guild, news: News, id: stri const message = await channelToSend.send({ embeds: [newsEmbed], content: roleToSend ? `<@&${roleToSend.id}>` : null }); await newsTable.set(`${guild.id}.news.${id}.messageId`, message.id); return; -} \ No newline at end of file +} diff --git a/src/utils/sendSubscribedNews.ts b/src/utils/sendSubscribedNews.ts index 1ce906f..6dbca4e 100644 --- a/src/utils/sendSubscribedNews.ts +++ b/src/utils/sendSubscribedNews.ts @@ -1,5 +1,5 @@ import { DMChannel, EmbedBuilder, Guild } from "discord.js"; -import database, { getNewsTable } from "./database.js"; +import { database, getNewsTable } from "./database.js"; import { genColor } from "./colorGen.js"; export type News = { @@ -13,12 +13,12 @@ export type News = { messageId?: string } -export default async function sendSubscribedNews(guild: Guild, news: News) { +export async function sendSubscribedNews(guild: Guild, news: News) { const db = await database(); const newsTable = await getNewsTable(db); const subscriptions = await newsTable.get(`${guild.id}.subscriptions`).then( - (subscriptions) => subscriptions as string[] ?? [] as string[] + subscriptions => subscriptions as string[] ?? [] as string[] ).catch(() => [] as string[]); const members = await guild.members.fetch(); const subscribed = members.filter((member) => subscriptions.includes(member.id)); diff --git a/tsconfig.json b/tsconfig.json index e4ce5f9..fdb1bff 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,5 +16,5 @@ "strict": false, "types": ["bun-types"] }, - "exclude": ["node_modules"], + "exclude": ["node_modules"] } From 651717a06fb79dab4a665a9b024a167583201483 Mon Sep 17 00:00:00 2001 From: Serge Date: Thu, 11 Jan 2024 09:34:34 +0600 Subject: [PATCH 002/127] 0.2 -> 0.1 (no dashboard to count as 0.2) --- package.json | 2 +- src/commands/about.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 1c8478f..d4bc0a5 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "The Nebula team", "The GitHub contributors" ], - "version": "0.2.0", + "version": "0.1.0", "main": "./src/index.ts", "type": "module", "scripts": { diff --git a/src/commands/about.ts b/src/commands/about.ts index e2c28dc..249f697 100644 --- a/src/commands/about.ts +++ b/src/commands/about.ts @@ -25,7 +25,7 @@ export default class About { { name: "📃 • General", value: [ - "**Version** 0.2, *Dasshubodo update*", + "**Version** 0.1", `**${guilds.size}** guild${guilds.size === 1 ? "" : "s"} • **${shards}** shard${shards === 1 ? "" : "s"}` ].join("\n") }, From 27cb35f557ab6589d8bf98271002e4a48862078e Mon Sep 17 00:00:00 2001 From: Serge Date: Thu, 11 Jan 2024 09:37:22 +0600 Subject: [PATCH 003/127] excess commands moment --- src/commands/info/news.ts | 112 ----------------- src/commands/info/server.ts | 17 --- src/commands/info/subscribe.ts | 58 --------- src/commands/info/user.ts | 80 ------------ src/commands/leveling/leaderboard.ts | 53 -------- src/commands/leveling/level.ts | 128 ------------------- src/commands/leveling/rewards.ts | 66 ---------- src/commands/moderation/unwarn.ts | 98 --------------- src/commands/nebula/about.ts | 43 ------- src/commands/nebula/apply.ts | 43 ------- src/commands/nebula/donate.ts | 33 ----- src/commands/nebula/news.ts | 100 --------------- src/commands/nebula/serverboard.ts | 147 ---------------------- src/commands/nebula/subscribe.ts | 55 --------- src/commands/settings/news/add.ts | 125 ------------------- src/commands/settings/news/edit.ts | 177 --------------------------- src/commands/settings/news/remove.ts | 65 ---------- 17 files changed, 1400 deletions(-) delete mode 100644 src/commands/info/news.ts delete mode 100644 src/commands/info/server.ts delete mode 100644 src/commands/info/subscribe.ts delete mode 100644 src/commands/info/user.ts delete mode 100644 src/commands/leveling/leaderboard.ts delete mode 100644 src/commands/leveling/level.ts delete mode 100644 src/commands/leveling/rewards.ts delete mode 100644 src/commands/moderation/unwarn.ts delete mode 100644 src/commands/nebula/about.ts delete mode 100644 src/commands/nebula/apply.ts delete mode 100644 src/commands/nebula/donate.ts delete mode 100644 src/commands/nebula/news.ts delete mode 100644 src/commands/nebula/serverboard.ts delete mode 100644 src/commands/nebula/subscribe.ts delete mode 100644 src/commands/settings/news/add.ts delete mode 100644 src/commands/settings/news/edit.ts delete mode 100644 src/commands/settings/news/remove.ts diff --git a/src/commands/info/news.ts b/src/commands/info/news.ts deleted file mode 100644 index 603a771..0000000 --- a/src/commands/info/news.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { - SlashCommandSubcommandBuilder, EmbedBuilder, ActionRowBuilder, - ButtonBuilder, ButtonStyle, type ChatInputCommandInteraction -} from "discord.js"; -import { genColor } from "../../utils/colorGen.js"; -import { getNewsTable } from "../../utils/database.js"; -import errorEmbed from "../../utils/embeds/errorEmbed.js"; -import { QuickDB } from "quick.db"; - -export default class News { - data: SlashCommandSubcommandBuilder; - db: QuickDB; - - constructor(db?: QuickDB) { - this.db = db; - this.data = new SlashCommandSubcommandBuilder() - .setName("news") - .setDescription("The news of the current server you're in.") - .addNumberOption(option => option - .setName("page") - .setDescription("The page of the news you want to see") - ); - } - - async run(interaction: ChatInputCommandInteraction) { - const db = this.db; - const newsletterTable = await getNewsTable(db); - - const guild = interaction.guild; - let page = interaction.options.getNumber("page") ?? 1; - - const news = await newsletterTable?.get(`${guild.id}.news`).then( - (news: any) => news as any[] ?? [] - ).catch(() => []); - if (!news) return await interaction.followUp({ - embeds: [errorEmbed("No news found.\nAdmins can add news with the **/settings news add** command.")] - }); - - const newsSorted = (Object.values(news).map((newsItem: any, i) => { - return { - id: Object.keys(news)[i], - ...newsItem - } - }) as any[])?.sort((a, b) => b.createdAt - a.createdAt); - if (!newsSorted) return await interaction.followUp({ - embeds: [errorEmbed("No news found.\nAdmins can add news with the **/settings news add** command.")] - }); - - if (newsSorted.length == 0) { - return await interaction.followUp({ - embeds: [errorEmbed("No news found.\nAdmins can add news with the **/settings news add** command.")] - }); - } - - if (page > newsSorted.length) page = newsSorted.length; - if (page < 1) page = 1; - - let currentNews = newsSorted[page - 1]; - let newsEmbed = new EmbedBuilder() - .setAuthor({ name: currentNews.author, iconURL: currentNews.authorPfp ?? null }) - .setTitle(currentNews.title) - .setDescription(currentNews.body) - .setImage(currentNews.imageURL || null) - .setTimestamp(parseInt(currentNews.updatedAt)) - .setFooter({ text: `Page ${page} of ${newsSorted.length} • ID: ${currentNews.id}` }) - .setColor(genColor(200)); - - const row = new ActionRowBuilder().addComponents( - new ButtonBuilder() - .setCustomId("left") - .setEmoji("1137330341472915526") - .setStyle(ButtonStyle.Primary), - new ButtonBuilder() - .setCustomId("right") - .setEmoji("1137330125004869702") - .setStyle(ButtonStyle.Primary) - ); - - await interaction.followUp({ embeds: [newsEmbed], components: [row] }); - - const buttonCollector = interaction.channel.createMessageComponentCollector({ - filter: i => i.user.id === interaction.user.id, - time: 60000 - }); - - buttonCollector.on("collect", async i => { - if (!i.isButton()) return; - const id = i.customId; - - if (id == "left") { - page--; - if (page < 1) page = newsSorted.length; - } else if (id == "right") { - page++; - if (page > newsSorted.length) page = 1; - } - - currentNews = newsSorted[page - 1]; - newsEmbed = new EmbedBuilder() - .setAuthor({ name: currentNews.author, iconURL: currentNews.authorPfp ?? null }) - .setTitle(currentNews.title) - .setDescription(currentNews.body) - .setImage(currentNews.imageURL || null) - .setTimestamp(parseInt(currentNews.updatedAt)) - .setFooter({ text: `Page ${page} of ${newsSorted.length} • ID: ${currentNews.id}` }) - .setColor(genColor(200)); - - await interaction.editReply({ embeds: [newsEmbed], components: [row] }); - await i.deferUpdate(); - }); - } -} diff --git a/src/commands/info/server.ts b/src/commands/info/server.ts deleted file mode 100644 index eaa0482..0000000 --- a/src/commands/info/server.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { SlashCommandSubcommandBuilder, type ChatInputCommandInteraction } from "discord.js"; -import serverEmbed from "../../utils/embeds/serverEmbed.js"; - -export default class Server { - data: SlashCommandSubcommandBuilder; - constructor() { - this.data = new SlashCommandSubcommandBuilder() - .setName("server") - .setDescription("Shows this server's info."); - } - - async run(interaction: ChatInputCommandInteraction) { - const embed = await serverEmbed({ guild: interaction.guild, roles: true }); - - await interaction.followUp({ embeds: [embed] }); - } -} diff --git a/src/commands/info/subscribe.ts b/src/commands/info/subscribe.ts deleted file mode 100644 index 0239fd2..0000000 --- a/src/commands/info/subscribe.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { - SlashCommandSubcommandBuilder, EmbedBuilder, type ChatInputCommandInteraction, DMChannel -} from "discord.js"; -import { genColor } from "../../utils/colorGen.js"; -import { getNewsTable } from "../../utils/database.js"; -import errorEmbed from "../../utils/embeds/errorEmbed.js"; -import { QuickDB } from "quick.db"; - -type SubscriptionType = string[]; - -export default class Subscribe { - data: SlashCommandSubcommandBuilder; - db: QuickDB; - - constructor(db?: QuickDB) { - this.db = db; - this.data = new SlashCommandSubcommandBuilder() - .setName("subscribe") - .setDescription("Subscribe to the news of the current server you're in."); - } - - async run(interaction: ChatInputCommandInteraction) { - const db = this.db; - const newsTable = await getNewsTable(db);; - - const guild = interaction.guild; - const user = interaction.user; - - let subscriptions = await newsTable?.get(`${guild.id}.subscriptions`).then( - (subscriptions: any) => subscriptions as SubscriptionType ?? [] as SubscriptionType - ) - .catch(() => []) as SubscriptionType; - if (!subscriptions) subscriptions = []; - - const hasSub = subscriptions?.includes(user.id); - - const dmChannel = (await interaction.user.createDM().catch(() => null)) as DMChannel | null; - if (!dmChannel) return await interaction.followUp({ - embeds: [errorEmbed("You need to **enable DMs from server members** to subscribe to the news.")] - }); - const sendDms = await dmChannel?.send("You have updated the subscription status of \`" + guild.name + "\`.").catch(() => { }); - if (!sendDms) { - await newsTable.pull(`${guild.id}.subscriptions`, user.id); - return await interaction.followUp({ - embeds: [errorEmbed("You need to **enable DMs from server members** to subscribe to the news.")] - }); - } - - await newsTable[!hasSub ? "push" : "pull"](`${guild.id}.subscriptions`, user.id); - - const subscriptionEmbed = new EmbedBuilder() - .setTitle(`✅ • ${hasSub ? "Unsubscribed" : "Subscribed"} ${hasSub ? "from" : "to"} ${guild.name}`) - .setDescription(`You have ${hasSub ? "un" : ""}subscribed to the news of ${guild.name}.`) - .setColor(genColor(100)); - - await interaction.followUp({ embeds: [subscriptionEmbed] }); - } -} diff --git a/src/commands/info/user.ts b/src/commands/info/user.ts deleted file mode 100644 index fadb42c..0000000 --- a/src/commands/info/user.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { - ColorResolvable, - SlashCommandSubcommandBuilder, - EmbedBuilder, - type ChatInputCommandInteraction, -} from "discord.js"; -import { genColor, genRGBColor } from "../../utils/colorGen.js"; -import Vibrant from "node-vibrant"; -import sharp from "sharp"; - -export default class User { - data: SlashCommandSubcommandBuilder; - constructor() { - this.data = new SlashCommandSubcommandBuilder() - .setName("user") - .setDescription("Shows your (or another user's) info.") - .addUserOption((option) => option.setName("user").setDescription("Select the user.")); - } - - async run(interaction: ChatInputCommandInteraction) { - const user = interaction.options.getUser("user"); - const member = interaction.member; - const guild = interaction.guild; - - const id = user ? user.id : member.user.id; - const selectedMember = guild.members.cache.filter((member) => member.user.id === id).map((user) => user)[0]; - const avatarURL = selectedMember.displayAvatarURL(); - const selectedUser = selectedMember.user; - - // Sorting the roles - const guildRoles = guild.roles.cache.filter((role) => selectedMember.roles.cache.has(role.id)); - const memberRoles = [...guildRoles].sort((role1, role2) => role2[1].position - role1[1].position); - memberRoles.pop(); - const rolesOrRole = memberRoles.length === 1 ? "role" : "roles"; - - let embed = new EmbedBuilder() - .setAuthor({ - name: `• ${selectedMember.nickname == null ? selectedUser.username : selectedMember.nickname}${selectedUser.discriminator == "0" ? "" : `#${selectedUser.discriminator}` - }`, - iconURL: avatarURL, - }) - .setFields( - { - name: selectedUser.bot === false ? "👤 • User info" : "🤖 • Bot info", - value: [ - `**Username**: ${selectedUser.username}`, - `**Display name**: ${selectedUser.displayName === selectedUser.username ? "*None*" : selectedUser.displayName - }`, - `**Created on** `, - ].join("\n"), - }, - { - name: "👥 • Member info", - value: `**Joined on** `, - } - ) - .setFooter({ text: `User ID: ${selectedMember.id}` }) - .setThumbnail(avatarURL) - .setColor(genColor(200)); - - try { - const imageBuffer = await (await fetch(avatarURL)).arrayBuffer(); - const image = sharp(imageBuffer).toFormat("jpg"); - const { r, g, b } = (await new Vibrant(await image.toBuffer()).getPalette()).Vibrant; - embed.setColor(genRGBColor(r, g, b) as ColorResolvable); - } catch { } - - if (memberRoles.length != 0) { - embed.addFields({ - name: `🎭 • ${guildRoles.filter((role) => selectedMember.roles.cache.has(role.id)).size - 1} ${rolesOrRole}`, - value: `${memberRoles - .slice(0, 5) - .map((role) => `<@&${role[1].id}>`) - .join(", ")}${memberRoles.length > 5 ? ` **and ${memberRoles.length - 5} more**` : ""}`, - }); - } - - await interaction.followUp({ embeds: [embed] }); - } -} diff --git a/src/commands/leveling/leaderboard.ts b/src/commands/leveling/leaderboard.ts deleted file mode 100644 index 233a4d2..0000000 --- a/src/commands/leveling/leaderboard.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { SlashCommandSubcommandBuilder, EmbedBuilder, type ChatInputCommandInteraction } from "discord.js"; -import { genColor } from "../../utils/colorGen.js"; -import { getLevelingTable, getSettingsTable } from "../../utils/database.js"; -import { BASE_EXP_FOR_NEW_LEVEL, DIFFICULTY_MULTIPLIER } from "../../events/leveling.js"; -import errorEmbed from "../../utils/embeds/errorEmbed.js"; -import { QuickDB } from "quick.db"; - -export default class Leaderboard { - data: SlashCommandSubcommandBuilder; - db: QuickDB; - - constructor(db?: QuickDB) { - this.db = db; - this.data = new SlashCommandSubcommandBuilder() - .setName("leaderboard") - .setDescription("Shows the server's leaderboard in levels."); - } - - async run(interaction: ChatInputCommandInteraction) { - const db = this.db; - const settingsTable = await getSettingsTable(db); - const levelingTable = await getLevelingTable(db); - - // Under maintenance - return await interaction.followUp({ - embeds: [errorEmbed("This command is under maintenance.")] - }); - - const guild = interaction.guild; - const levelEnabled = await settingsTable?.get(`${guild.id}.leveling.enabled`).catch(() => { }); - const levels = await levelingTable?.get(`${guild.id}`).catch(() => { }); - const levelKeys = Object.keys(levels) - const convertLevelsAndExpToExp = (levels: any) => { - const exp = Object.keys(levels).map(level => levels[level].exp); - return exp.reduce((a, b) => a + b); - }; - - if (!levelEnabled) - return await interaction.followUp({ embeds: [errorEmbed("Leveling is disabled for this server.")] }); - - const levelUpEmbed = new EmbedBuilder() - .setTitle("⚡ • Top 10 active members") - .setDescription( - levelKeys.slice(0, 10).map(level => - `#${Object.keys(levels).indexOf(level) + 1} • <@${level}>\n**Level ${levels[level].levels}** - Next Level: ${levels[level].levels + 1}\n**Exp**: ${levels[level].exp}/${Math.floor(BASE_EXP_FOR_NEW_LEVEL * DIFFICULTY_MULTIPLIER * (levels[level].levels + 1))} until level up` - ).join("\n\n") - ) - .setColor(genColor(200)) - .setTimestamp(); - - await interaction.followUp({ embeds: [levelUpEmbed] }); - } -} diff --git a/src/commands/leveling/level.ts b/src/commands/leveling/level.ts deleted file mode 100644 index ddd5c21..0000000 --- a/src/commands/leveling/level.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { - ColorResolvable, - SlashCommandSubcommandBuilder, - EmbedBuilder, - type ChatInputCommandInteraction -} from "discord.js"; -import { genColor, genRGBColor } from "../../utils/colorGen.js"; -import Vibrant from "node-vibrant"; -import sharp from "sharp"; -import { getLevelingTable, getSettingsTable } from "../../utils/database.js"; -import { BASE_EXP_FOR_NEW_LEVEL, DIFFICULTY_MULTIPLIER } from "../../events/leveling.js"; -import errorEmbed from "../../utils/embeds/errorEmbed.js"; -import { QuickDB } from "quick.db"; -import { Reward } from "../settings/leveling/rewards.js"; - -export default class Level { - data: SlashCommandSubcommandBuilder; - db: QuickDB; - - constructor(db?: QuickDB) { - this.db = db; - this.data = new SlashCommandSubcommandBuilder() - .setName("level") - .setDescription("Shows your (or another user's) level.") - .addUserOption((option) => option.setName("user").setDescription("Select the user.")); - } - - async run(interaction: ChatInputCommandInteraction) { - const db = this.db; - const settingsTable = await getSettingsTable(db); - const levelingTable = await getLevelingTable(db); - - const user = interaction.options.getUser("user"); - const member = interaction.member; - const guild = interaction.guild; - - const id = user ? user.id : member.user.id; - const selectedMember = guild.members.cache.filter((member) => member.user.id === id).map((user) => user)[0]; - const avatarURL = selectedMember.displayAvatarURL(); - - const levelEnabled = await settingsTable?.get(`${guild.id}.leveling.enabled`).then( - (data) => { - if (!data) return false; - return Boolean(data); - } - ).catch(() => false); - if (!levelEnabled) { - return await interaction.followUp({ - embeds: [errorEmbed("Leveling is disabled for this server.")] - }); - } - - const { exp, levels } = await levelingTable?.get(`${guild.id}.${selectedMember.id}`).then( - (data) => { - if (!data) return { exp: 0, level: 0 }; - return { exp: Number(data.exp), levels: Number(data.levels) }; - } - ).catch(() => { - return { exp: 0, levels: 0 }; - }); - const formattedExp = exp?.toLocaleString("en-US"); - - if (!exp && !levels) { - await levelingTable.set(`${guild.id}.${selectedMember.id}`, { - levels: 0, - exp: 0 - }); - } - - let rewards = []; - let nextReward = null; - const levelRewards = await settingsTable?.get(`${interaction.guild.id}.leveling.rewards`).then( - (data) => { - if (!data) return [] as Reward[] ?? [] as Reward[]; - return data as Reward[] ?? [] as Reward[]; - } - ).catch(() => [] as Reward[]); - - for (const { roleId, level } of levelRewards) { - const role = await interaction.guild.roles.fetch(roleId).catch(() => { }); - const reward = { roleId, level }; - - if (levels < level) { - if (nextReward) break; - nextReward = reward; - break; - } - rewards.push(role); - } - - const expUntilLevelup = Math.floor(BASE_EXP_FOR_NEW_LEVEL * DIFFICULTY_MULTIPLIER * ((levels ?? 0) + 1)); - const formattedExpUntilLevelup = expUntilLevelup?.toLocaleString("en-US"); - const levelUpEmbed = new EmbedBuilder() - .setFields([ - { - name: `⚡ • Level ${levels ?? 0}`, - value: [ - `**Exp**: ${formattedExp ?? 0}/${formattedExpUntilLevelup} until level up`, - `**Next Level**: ${(levels ?? 0) + 1}` - ].join("\n") - }, - { - name: `🎁 • ${rewards.length} Rewards`, - value: [ - `${rewards.length > 0 ? rewards.map(reward => `<@&${reward.id}>`).join(" ") : "No rewards unlocked" - }`, - nextReward ? `**Upcoming reward**: <@&${nextReward.roleId}>` : "**Upcoming reward**: *Cricket, cricket, cricket* - Looks like you claimed everything!" - ].join("\n") - } - ]) - .setAuthor({ - name: `• ${selectedMember.user.username}`, - iconURL: avatarURL - }) - .setThumbnail(avatarURL) - .setColor(genColor(200)) - .setTimestamp(); - - try { - const imageBuffer = await (await fetch(avatarURL)).arrayBuffer(); - const image = sharp(imageBuffer).toFormat("jpg"); - const { r, g, b } = (await new Vibrant(await image.toBuffer()).getPalette()).Vibrant; - levelUpEmbed.setColor(genRGBColor(r, g, b) as ColorResolvable); - } catch { } - - await interaction.followUp({ embeds: [levelUpEmbed] }); - } -} diff --git a/src/commands/leveling/rewards.ts b/src/commands/leveling/rewards.ts deleted file mode 100644 index 513f539..0000000 --- a/src/commands/leveling/rewards.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { - SlashCommandSubcommandBuilder, EmbedBuilder, PermissionsBitField, - type ChatInputCommandInteraction, - Role, -} from "discord.js"; -import { genColor } from "../../utils/colorGen.js"; -import errorEmbed from "../../utils/embeds/errorEmbed.js"; -import { getSettingsTable } from "../../utils/database.js"; -import { QuickDB } from "quick.db"; - -export type Reward = { - roleId: string, - level: number, -} - -export default class Rewards { - data: SlashCommandSubcommandBuilder; - db: QuickDB; - - constructor(db?: QuickDB) { - this.db = db; - this.data = new SlashCommandSubcommandBuilder() - .setName("rewards") - .setDescription("Sets/gets reward roles for each level -> No options = shows."); - } - - async run(interaction: ChatInputCommandInteraction) { - // List the rewards - const rewards = await this.getRewards(interaction.guild.id).then(rewards => rewards.sort((a, b) => a.level - b.level)) as Reward[]; - if (rewards.length == 0) { - return await interaction.followUp({ - embeds: [errorEmbed("There are no rewards set for this server.")] - }); - } - - const rewardsEmbed = new EmbedBuilder() - .setTitle("🎁 • Rewards") - .setDescription("Here are the rewards in this server.") - .setColor(genColor(100)); - - for (const { roleId, level } of rewards) { - if (!roleId) continue; - rewardsEmbed.addFields([{ - name: `Level ${level}`, - value: `<@&${roleId}>`, - }]); - } - - return await interaction.followUp({ - embeds: [rewardsEmbed] - }); - } - - async getRewards(guildId: string): Promise { - const settingsTable = await getSettingsTable(this.db); - const rewards = new Promise((resolve, reject) => { - settingsTable?.get(`${guildId}.leveling.rewards`).then(rewards => { - if (!rewards) return resolve([]); - resolve(rewards); - }).catch(() => { - resolve([]); - }); - }); - return rewards as Promise; - } -} diff --git a/src/commands/moderation/unwarn.ts b/src/commands/moderation/unwarn.ts deleted file mode 100644 index 1e69b0f..0000000 --- a/src/commands/moderation/unwarn.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { - SlashCommandSubcommandBuilder, EmbedBuilder, PermissionsBitField, - type ChatInputCommandInteraction, TextChannel, DMChannel, - ChannelType, Channel -} from "discord.js"; -import { genColor } from "../../utils/colorGen.js"; -import errorEmbed from "../../utils/embeds/errorEmbed.js"; -import { QuickDB } from "quick.db"; -import { getModerationTable, getSettingsTable } from "../../utils/database.js"; - -export default class Unwarn { - data: SlashCommandSubcommandBuilder; - db: QuickDB; - - constructor(db: QuickDB) { - this.db = db; - this.data = new SlashCommandSubcommandBuilder() - .setName("unwarn") - .setDescription("Warns a user.") - .addUserOption(user => user - .setName("user") - .setDescription("The user that you want to warn.") - .setRequired(true) - ) - .addNumberOption(string => string - .setName("id") - .setDescription("The id of the warn.") - .setRequired(true) - ); - } - - async run(interaction: ChatInputCommandInteraction) { - const db = this.db; - const modTable = await getModerationTable(db); - - const user = interaction.options.getUser("user"); - const members = interaction.guild.members.cache; - const member = members.get(interaction.member.user.id); - const selectedMember = members.get(user.id); - const name = selectedMember.nickname ?? user.username; - const dmChannel = (await user.createDM().catch(() => null)) as DMChannel | null;; - const id = interaction.options.getNumber("id", true); - const warns = await modTable?.get(`${interaction.guild.id}.${user.id}.warns`).then( - warns => warns as any[] ?? [] - ).catch(() => []); - const newWarns = warns.filter(warn => warn.id !== id); - if (newWarns.length === warns.length) - return await interaction.followUp({ embeds: [errorEmbed(`There is no warn with the id of ${id}.`)] }); - - const unwarnEmbed = new EmbedBuilder() - .setTitle(`✅ • Removed warning`) - .setDescription([ - `**Moderator**: <@${member.id}>`, - `**Original Reason**: ${newWarns.find(warn => warn.id === id)?.reason ?? "No reason provided"}` - ].join("\n")) - .setFooter({ text: `User ID: ${user.id}` }) - .setThumbnail(user.displayAvatarURL()) - .setAuthor({ name: `• ${user.username}`, iconURL: user.displayAvatarURL() }) - .setColor(genColor(100)); - const embedDM = new EmbedBuilder() - .setTitle(`🤝 • You were unwarned`) - .setDescription([ - `**Moderator**: ${member.user.username}`, - `**Original Reason**: ${newWarns.find(warn => warn.id === id)?.reason ?? "No reason provided"}` - ].join("\n")) - .setThumbnail(user.displayAvatarURL()) - .setAuthor({ name: `• ${user.username}`, iconURL: user.displayAvatarURL() }) - .setFooter({ text: `User ID: ${user.id}` }) - .setColor(genColor(100)); - - if (!member.permissions.has(PermissionsBitField.Flags.ModerateMembers)) - return await interaction.followUp({ embeds: [errorEmbed("You need the **Moderate Members** permission to execute this command.")] }); - if (selectedMember === member) - return await interaction.followUp({ embeds: [errorEmbed("You can't unwarn yourself.")] }); - if (!selectedMember.manageable) - return await interaction.followUp({ embeds: [errorEmbed(`You can't unwarn ${name}, because they have a higher role position than Nebula.`)] }); - if (member.roles.highest.position < selectedMember.roles.highest.position) - return await interaction.followUp({ embeds: [errorEmbed(`You can't unwarn ${name}, because they have a higher role position than you.`)] }); - - const settingsTable = await getSettingsTable(db); - const logChannel = await settingsTable?.get(`${interaction.guild.id}.logChannel`).then( - (channel: string | null) => channel - ).catch(() => null); - if (logChannel) { - const channel = await interaction.guild.channels.cache.get(logChannel).fetch().then( - (channel: Channel) => { - if (channel.type != ChannelType.GuildText) return null; - return channel as TextChannel; - } - ).catch(() => null); - if (channel) await channel.send({ embeds: [unwarnEmbed] }); - } - - if (dmChannel) await dmChannel.send({ embeds: [embedDM] }); - await modTable?.set(`${interaction.guild.id}.${user.id}.warns`, newWarns); - await interaction.followUp({ embeds: [unwarnEmbed] }); - } -} diff --git a/src/commands/nebula/about.ts b/src/commands/nebula/about.ts deleted file mode 100644 index a43f228..0000000 --- a/src/commands/nebula/about.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { SlashCommandSubcommandBuilder, EmbedBuilder, type ChatInputCommandInteraction } from "discord.js"; -import { genColor } from "../../utils/colorGen.js"; -import randomise from "../../utils/randomise.js"; - -export default class About { - data: SlashCommandSubcommandBuilder; - constructor() { - this.data = new SlashCommandSubcommandBuilder() - .setName("about") - .setDescription("Shows information about the bot."); - } - - async run(interaction: ChatInputCommandInteraction) { - const client = interaction.client; - const hearts = ["💖", "💝", "💓", "💗", "💘", "💟", "💕", "💞"]; - - const aboutEmbed = new EmbedBuilder() - .setAuthor({ name: "• About", iconURL: client.user.displayAvatarURL() }) - .setDescription("Nebula is a multiplatform, multipurpose bot with the ability to add extensions to have additional features.") - .setFields( - { - name: "📃 • General", - value: ["**Version**: v0.1-alpha", `**Guild count**: ${client.guilds.cache.size}`].join("\n"), - inline: true - }, - { - name: "🌌 • Entities involved", - value: [ - "**Head developer**: Goos", - "**Developers**: Golem64, Pigpot, ThatBOI", - "**Designers**: ArtyH, Optix, proJM, Slider_on_the_black", - "**Translators**: Dimkauzh, Golem64, Optix, SaFire, ThatBOI", - "And **YOU**, for using Nebula." - ].join("\n") - } - ) - .setFooter({ text: `Made by the Nebula team with ${randomise(hearts)}` }) - .setThumbnail(client.user.displayAvatarURL()) - .setColor(genColor(270)); - - await interaction.followUp({ embeds: [aboutEmbed] }); - } -} diff --git a/src/commands/nebula/apply.ts b/src/commands/nebula/apply.ts deleted file mode 100644 index 6a72777..0000000 --- a/src/commands/nebula/apply.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { SlashCommandSubcommandBuilder, EmbedBuilder, type ChatInputCommandInteraction } from "discord.js"; -import { genColor } from "../../utils/colorGen.js"; -import randomise from "../../utils/randomise.js"; - -export default class About { - data: SlashCommandSubcommandBuilder; - constructor() { - this.data = new SlashCommandSubcommandBuilder() - .setName("apply") - .setDescription("Apply as a team member in Nebula!"); - } - - async run(interaction: ChatInputCommandInteraction) { - const client = interaction.client; - const hearts = ["💖", "💝", "💓", "💗", "💘", "💟", "💕", "💞", "❣️", "❤️‍🔥"]; - - const applyEmbed = new EmbedBuilder() - .setAuthor({ name: `• Apply for team!`, iconURL: client.user.displayAvatarURL() }) - .setDescription("We're currently looking for people that could extend our team to help us make Nebula faster and better!") - .setFields( - { - name: "👥 • Who we're looking for", - value: [ - "- **Developers**: Code and implement our back-end in [TypeScript](https://www.typescriptlang.org/).", - "- **Front-End Devs**: Make great experiences for our users by making the designs interactive using [Svelte](https://svelte.dev/).", - "- **UI/UX/Icon designers**: Help us make Nebula not only work great, but also look great." - ].join("\n") - }, - { - name: "❓ • How to apply", - value: [ - "Join our [team applicants server](https://discord.gg/5hkHwGCbju) to find information on applying, resources + getting interviewed at.", - `Thank you for your interest at joining Nebula, we appreciate it ${randomise(hearts)}` - ].join("\n\n") - } - ) - .setFooter({ text: `Made by the Nebula team with ${randomise(hearts)}` }) - .setThumbnail(client.user.displayAvatarURL()) - .setColor(genColor(270)); - - await interaction.followUp({ embeds: [applyEmbed] }); - } -} diff --git a/src/commands/nebula/donate.ts b/src/commands/nebula/donate.ts deleted file mode 100644 index b86025c..0000000 --- a/src/commands/nebula/donate.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { SlashCommandSubcommandBuilder, EmbedBuilder, type ChatInputCommandInteraction } from "discord.js"; -import { genColor } from "../../utils/colorGen.js"; - -export default class Donate { - data: SlashCommandSubcommandBuilder; - constructor() { - this.data = new SlashCommandSubcommandBuilder() - .setName("donate") - .setDescription("Helps us keep up Nebula!"); - } - - async run(interaction: ChatInputCommandInteraction) { - const donationEmbed = new EmbedBuilder() - .setTitle("Donate") - .setDescription([ - "Thanks for being interested into donating for Nebula.", - "We'll be grateful for any donation and use it towards paying our server costs and software costs.", - "[Click here](https://nebula.fyreblitz.com/) to donate via PayPal!" - ].join("\n")) - .setFields({ - name: "🤔 • Why should I donate?", - value: [ - "By donating, you help the Nebula team to continue existing and keep production going as well as keeping our servers alive - Nebula is completely free and, as of now, doesn't have any other way to have upkeep or funding, and donating is a great way to help us.", - "If you donated, you can reach out to us to request a special donators role to flex to your friends.", - "Additionally, you're helping us keep up the servers and make more amazing features :)." - ].join("\n\n") - }) - .setThumbnail(interaction.client.user.avatarURL()) - .setColor(genColor(270)); - - await interaction.followUp({ embeds: [donationEmbed] }); - } -} diff --git a/src/commands/nebula/news.ts b/src/commands/nebula/news.ts deleted file mode 100644 index a5f3fd8..0000000 --- a/src/commands/nebula/news.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { - SlashCommandSubcommandBuilder, EmbedBuilder, ActionRowBuilder, - ButtonBuilder, ButtonStyle, type ChatInputCommandInteraction -} from "discord.js"; -import { genColor } from "../../utils/colorGen.js"; -import { getNewsTable } from "../../utils/database.js"; -import errorEmbed from "../../utils/embeds/errorEmbed.js"; -import { QuickDB } from "quick.db"; - -export default class News { - data: SlashCommandSubcommandBuilder; - db: QuickDB; - - constructor(db?: QuickDB) { - this.db = db; - this.data = new SlashCommandSubcommandBuilder() - .setName("news") - .setDescription("The news of Nebula.") - .addNumberOption(option => option - .setName("page") - .setDescription("The page of the news you want to see") - ); - } - - async run(interaction: ChatInputCommandInteraction) { - const db = this.db; - const newsletterTable = await getNewsTable(db); - - const nebulaId = "903852579837059113" - let page = interaction.options.getNumber("page") ?? 1; - - const news = await newsletterTable.get(`${nebulaId}.news`).then( - (news: any) => news as any[] ?? [] - ).catch(() => []); - const newsSorted = (Object.values(news) as any[])?.sort((a, b) => b.createdAt - a.createdAt); - - if (newsSorted.length == 0) { - return await interaction.followUp({ - embeds: [errorEmbed("No news found.\nAdmins can add news with the **/settings news add** command.")] - }); - } - - if (page > newsSorted.length) page = newsSorted.length; - if (page < 1) page = 1; - - let currentNews = newsSorted[page - 1]; - let newsEmbed = new EmbedBuilder() - .setAuthor({ name: currentNews.author, iconURL: currentNews.authorPfp ?? null }) - .setTitle(currentNews.title) - .setDescription(currentNews.body) - .setImage(currentNews.imageURL || null) - .setTimestamp(parseInt(currentNews.updatedAt)) - .setFooter({ text: `Page ${page} of ${newsSorted.length} • ID: ${currentNews.id}` }) - .setColor(genColor(270)); - - const row = new ActionRowBuilder().addComponents( - new ButtonBuilder() - .setCustomId("left") - .setEmoji("1137330341472915526") - .setStyle(ButtonStyle.Primary), - new ButtonBuilder() - .setCustomId("right") - .setEmoji("1137330125004869702") - .setStyle(ButtonStyle.Primary) - ); - - await interaction.followUp({ embeds: [newsEmbed], components: [row] }); - - const buttonCollector = interaction.channel.createMessageComponentCollector({ - filter: i => i.user.id === interaction.user.id, - time: 60000 - }); - - buttonCollector.on("collect", async i => { - if (!i.isButton()) return; - const id = i.customId; - - if (id == "left") { - page--; - if (page < 1) page = newsSorted.length; - } else if (id == "right") { - page++; - if (page > newsSorted.length) page = 1; - } - - currentNews = newsSorted[page - 1]; - newsEmbed = new EmbedBuilder() - .setAuthor({ name: currentNews.author, iconURL: currentNews.authorPfp ?? null }) - .setTitle(currentNews.title) - .setDescription(currentNews.body) - .setImage(currentNews.imageURL || null) - .setTimestamp(parseInt(currentNews.updatedAt)) - .setFooter({ text: `Page ${page} of ${newsSorted.length} • ID: ${currentNews.id}` }) - .setColor(genColor(270)); - - await interaction.editReply({ embeds: [newsEmbed], components: [row] }); - await i.deferUpdate(); - }); - } -} diff --git a/src/commands/nebula/serverboard.ts b/src/commands/nebula/serverboard.ts deleted file mode 100644 index 60f8d42..0000000 --- a/src/commands/nebula/serverboard.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { - SlashCommandSubcommandBuilder, ButtonBuilder, ActionRowBuilder, - ButtonStyle, type ChatInputCommandInteraction, ButtonInteraction, - CacheType, StringSelectMenuInteraction, UserSelectMenuInteraction, - RoleSelectMenuInteraction, MentionableSelectMenuInteraction, ChannelSelectMenuInteraction -} from "discord.js"; -import quickSort from "../../utils/quickSort.js"; -import serverEmbed from "../../utils/embeds/serverEmbed.js"; -import database, { getNewsTable, getServerboardTable } from "../../utils/database.js"; -import errorEmbed from "../../utils/embeds/errorEmbed.js"; -import { QuickDB } from "quick.db"; - -export default class Serverboard { - data: SlashCommandSubcommandBuilder; - db: QuickDB; - - constructor(db?: QuickDB) { - this.db = db; - this.data = new SlashCommandSubcommandBuilder() - .setName("serverboard") - .setDescription("Shows the servers that have Nebula.") - .addNumberOption(option => option - .setName("page") - .setDescription("The page you want to see.") - ); - } - - async run(interaction: ChatInputCommandInteraction) { - const db = this.db; - const serverbTable = await getServerboardTable(db); - - // Receiving necessary data - const guilds = interaction.client.guilds.cache; - const guildsMapped = {}; - - // Simplifying the data - const shownGuilds = (await serverbTable.all().catch(() => [])) as any[]; - for (const guild of guilds.values()) { - const shownVal = shownGuilds?.find(shown => shown?.id == guild.id)?.value?.shown; - const isShown = shownVal == null ? null : Boolean(shownVal); - const isCommunity = guild?.rulesChannelId != null; - - if (isShown == false) continue; - if (isShown == null && !isCommunity) continue; - - const members = guild.memberCount + ":" + guild.id; - guildsMapped[members] = guild; - } - - if (Object.keys(guildsMapped).length == 0) - return await interaction.followUp({ - embeds: [errorEmbed("There are no servers with Nebula in them that are shown.")] - }); - - // Sorting the data - const sortedGuilds = quickSort( - [...Object.keys(guildsMapped).map(i => Number(i.split(":")[0]))], - [[...Object.values(guildsMapped)]], - 0, - Object.keys(guildsMapped).length - 1 - ); - - // Additional data - const guildsSorted = sortedGuilds[1][0].reverse(); - const pages = guildsSorted.length; - const argPage = interaction.options.getNumber("page", false); - let page = (argPage - 1 <= 0 ? 0 : argPage - 1 > pages ? pages - 1 : argPage - 1) || 0; - - // Creating the embed - const guild = guildsSorted[page]; - const subscriptionsTable = await getNewsTable(db); - const subs = await subscriptionsTable?.get(`${guild.id}.subscriptions`).then( - subs => subs?.length > 0 ? subs as string[] : [] as string[] - ).catch(() => [] as string[]); - - let embed = await serverEmbed({ - guild, - page: page + 1, - pages, - showInvite: true, - showSubs: subs.length > 0, - subs: subs.length - }); - - // Sending the embed - const row = new ActionRowBuilder().addComponents( - new ButtonBuilder() - .setCustomId("left") - .setEmoji("1137330341472915526") - .setStyle(ButtonStyle.Primary), - new ButtonBuilder() - .setCustomId("right") - .setEmoji("1137330125004869702") - .setStyle(ButtonStyle.Primary) - ); - - await interaction.followUp({ embeds: [embed], components: [row] }); - - // Listening for button events - const collectorFilter = i => i.user.id === interaction.user.id; - const collector = interaction.channel.createMessageComponentCollector({ filter: collectorFilter, time: 60000 }); - collector.on("collect", async interaction => { - page = await handlePageUpdate(interaction, page, pages, row, guildsSorted); - }); - } -} - -async function handlePageUpdate( - interaction: StringSelectMenuInteraction | - UserSelectMenuInteraction | - RoleSelectMenuInteraction | - MentionableSelectMenuInteraction | - ChannelSelectMenuInteraction | - ButtonInteraction, page: number, pages: number, row, guildsSorted: any[] -) { - const db = await database(); - - let embed; - let guild; - let subs; - - const subscriptionsTable = db.table("subscriptions"); - const subscriptions = await subscriptionsTable.all(); - - switch (interaction.customId) { - case "left": - page--; - if (page < 0) page = pages - 1; - guild = guildsSorted[page]; - subs = subscriptions.filter(sub => (sub.value as string[] ?? [] as string[]).includes(guild.id)); - - embed = await serverEmbed({ guild, page: page + 1, pages, showInvite: true, showSubs: subs.length > 0, subs: subs.length }); - break; - case "right": - page++; - if (page >= pages) page = 0; - guild = guildsSorted[page]; - subs = subscriptions.filter(sub => (sub.value as string[]).includes(guild.id)); - - embed = await serverEmbed({ guild, page: page + 1, pages, showInvite: true, showSubs: subs.length > 0, subs: subs.length }); - break; - } - - interaction.message.edit({ embeds: [embed], components: [row] }); - interaction.deferUpdate(); - return page; -} diff --git a/src/commands/nebula/subscribe.ts b/src/commands/nebula/subscribe.ts deleted file mode 100644 index d04dfe9..0000000 --- a/src/commands/nebula/subscribe.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { - SlashCommandSubcommandBuilder, EmbedBuilder, type ChatInputCommandInteraction, DMChannel -} from "discord.js"; -import { genColor } from "../../utils/colorGen.js"; -import { getNewsTable } from "../../utils/database.js"; -import errorEmbed from "../../utils/embeds/errorEmbed.js"; -import { QuickDB } from "quick.db"; - -type SubscriptionType = string[]; - -export default class Subscribe { - data: SlashCommandSubcommandBuilder; - db: QuickDB; - - constructor(db?: QuickDB) { - this.db = db; - this.data = new SlashCommandSubcommandBuilder() - .setName("subscribe") - .setDescription("Subscribe to the news of Nebula."); - } - - async run(interaction: ChatInputCommandInteraction) { - const db = this.db; - const newsTable = await getNewsTable(db);; - - const user = interaction.user; - - const nebulaId = "903852579837059113"; - const subscriptions = await newsTable?.get(`${nebulaId}.subscriptions`).then( - (subscriptions: any) => subscriptions as SubscriptionType ?? [] as SubscriptionType - ).catch(() => [] as SubscriptionType); - const hasSub = subscriptions?.includes(user.id); - - const dmChannel = (await interaction.user.createDM().catch(() => null)) as DMChannel | null; - if (!dmChannel) return await interaction.followUp({ - embeds: [errorEmbed("You need to **enable DMs from server members** to subscribe to the news.")] - }); - const sendDms = await dmChannel?.send("You have updated the subscription status of \`Nebula\`.").catch(() => null); - if (!sendDms) { - await newsTable.pull(`${nebulaId}.subscriptions`, user.id); - return await interaction.followUp({ - embeds: [errorEmbed("You need to **enable DMs from server members** to subscribe to the news.")] - }); - } - - await newsTable[!hasSub ? "push" : "pull"](`${nebulaId}.subscriptions`, user.id); - - const subscriptionEmbed = new EmbedBuilder() - .setTitle(`✅ • ${hasSub ? "Unsubscribed" : "Subscribed"} ${hasSub ? "from" : "to"} Nebula`) - .setDescription(`You have ${hasSub ? "un" : ""}subscribed to the news of Nebula.`) - .setColor(genColor(100)); - - await interaction.followUp({ embeds: [subscriptionEmbed] }); - } -} diff --git a/src/commands/settings/news/add.ts b/src/commands/settings/news/add.ts deleted file mode 100644 index e9072d4..0000000 --- a/src/commands/settings/news/add.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { - SlashCommandSubcommandBuilder, EmbedBuilder, PermissionsBitField, - type ChatInputCommandInteraction, ModalBuilder, TextInputBuilder, - ActionRowBuilder, TextInputStyle -} from "discord.js"; -import { getNewsTable } from "../../../utils/database.js"; -import { genColor } from "../../../utils/colorGen.js"; -import errorEmbed from "../../../utils/embeds/errorEmbed.js"; -import sendSubscribedNews, { News } from "../../../utils/sendSubscribedNews.js"; -import sendChannelNews from "../../../utils/sendChannelNews.js"; -import validateURL from "../../../utils/validateURL.js"; -import { QuickDB } from "quick.db"; - -export default class Add { - data: SlashCommandSubcommandBuilder; - deferred: boolean = false; - db: QuickDB; - - constructor(db?: QuickDB) { - this.db = db; - this.data = new SlashCommandSubcommandBuilder() - .setName("add") - .setDescription("Adds news to your guild."); - } - - async run(interaction: ChatInputCommandInteraction) { - const db = this.db; - const newsTable = await getNewsTable(db); - - const user = interaction.user; - const guild = interaction.guild; - - const author = user.displayName ?? user.username; - const timestamp = Date.now().toString(); - const member = guild.members.cache.get(user.id); - - if (!member.permissions.has(PermissionsBitField.Flags.ManageGuild)) { - return await interaction.reply({ - embeds: [errorEmbed("You need **Manage Server** permissions to add news.")], - }); - } - - const newsModal = new ModalBuilder() - .setCustomId("addnews") - .setTitle("Create new News for your server/project"); - - const titleInput = new TextInputBuilder() - .setCustomId("title") - .setPlaceholder("Title") - .setStyle(TextInputStyle.Short) - .setMaxLength(100) - .setLabel("Title") - .setRequired(true); - - const bodyInput = new TextInputBuilder() - .setCustomId("body") - .setPlaceholder("Content (markdown)") - .setMaxLength(4000) - .setStyle(TextInputStyle.Paragraph) - .setLabel("Content (markdown)") - .setRequired(true); - - const imageURLInput = new TextInputBuilder() - .setCustomId("imageurl") - .setPlaceholder("Big image URL (bottom)") - .setStyle(TextInputStyle.Short) - .setMaxLength(1000) - .setLabel("Big image URL (bottom)") - .setRequired(false); - - const firstActionRow = new ActionRowBuilder().addComponents(titleInput) as ActionRowBuilder; - const secondActionRow = new ActionRowBuilder().addComponents(bodyInput) as ActionRowBuilder; - const thirdActionRow = new ActionRowBuilder().addComponents(imageURLInput) as ActionRowBuilder; - - newsModal.addComponents(firstActionRow, secondActionRow, thirdActionRow); - await interaction.showModal(newsModal).catch((err) => { - console.error(err); - }); - - interaction.client.once("interactionCreate", async (interaction) => { - if (!interaction.isModalSubmit()) return; - if (interaction.customId !== "addnews") return; - - const title = interaction.fields.getTextInputValue("title") as string; - const body = interaction.fields.getTextInputValue("body") as string; - const imageURL = interaction.fields.getTextInputValue("imageurl") as string | undefined; - let validURL = false; - if (imageURL) validURL = validateURL(imageURL); - - if (!validURL && imageURL) { - await interaction.reply({ - embeds: [errorEmbed("The image URL you provided is invalid.")], - }); - return; - } - - const id = crypto.randomUUID(); - const news = { - id, - title, - body, - imageURL, - author, - authorPfp: interaction.user.avatarURL(), - createdAt: timestamp, - updatedAt: timestamp, - messageId: null - }; - - sendSubscribedNews(interaction.guild, news as News).catch((err) => { - console.error(err); - }); - sendChannelNews(interaction.guild, news as News, id).catch((err) => { - console.error(err); - }); - - const embed = new EmbedBuilder() - .setTitle("✅ • News sent!") - .setColor(genColor(100)); - - await newsTable.set(`${guild.id}.news.${id}`, news); - await interaction.reply({ embeds: [embed] }); - }); - } -} diff --git a/src/commands/settings/news/edit.ts b/src/commands/settings/news/edit.ts deleted file mode 100644 index b6ed09a..0000000 --- a/src/commands/settings/news/edit.ts +++ /dev/null @@ -1,177 +0,0 @@ -import { - SlashCommandSubcommandBuilder, - EmbedBuilder, - PermissionsBitField, - type ChatInputCommandInteraction, - ModalBuilder, - TextInputBuilder, - ActionRowBuilder, - TextInputStyle, - TextChannel, - Message, -} from "discord.js"; -import { getNewsTable } from "../../../utils/database.js"; -import { genColor } from "../../../utils/colorGen.js"; -import errorEmbed from "../../../utils/embeds/errorEmbed.js"; -import sendSubscribedNewsletter, { News } from "../../../utils/sendSubscribedNews.js"; -import validateURL from "../../../utils/validateURL.js"; -import { QuickDB } from "quick.db"; - -export default class Edit { - data: SlashCommandSubcommandBuilder; - deferred: boolean = false; - db: QuickDB; - - constructor(db?: QuickDB) { - this.db = db; - this.data = new SlashCommandSubcommandBuilder() - .setName("edit") - .setDescription("Edits new of your guild.") - .addStringOption((option) => - option - .setName("id") - .setDescription("The ID of the news you want to edit.") - .setRequired(true) - ); - } - - async run(interaction: ChatInputCommandInteraction) { - const db = this.db; - const newsTable = await getNewsTable(db); - - const user = interaction.user; - const guild = interaction.guild; - const id = interaction.options.getString("id", true).trim(); - const news = await newsTable?.get(`${guild.id}.news.${id}`).then( - (news) => news as News - ).catch(() => null as News | null); - - if (!news) return await interaction.followUp({ embeds: [errorEmbed("The specified news doesn't exist.")] }); - - const author = user.displayName ?? user.username; - const timestamp = Date.now().toString(); - const member = guild.members.cache.get(user.id); - - if (!member.permissions.has(PermissionsBitField.Flags.ManageGuild)) { - return await interaction.followUp({ - embeds: [errorEmbed("You need **Manage Server** permissions to add news.")], - }); - } - - const editModal = new ModalBuilder() - .setCustomId("editnews") - .setTitle("Edit News: " + news.title); - - const titleInput = new TextInputBuilder() - .setCustomId("title") - .setPlaceholder("Title") - .setStyle(TextInputStyle.Short) - .setMaxLength(100) - .setLabel("Title") - .setValue(news.title) - .setRequired(true); - - const bodyInput = new TextInputBuilder() - .setCustomId("body") - .setPlaceholder("Content (markdown)") - .setMaxLength(4000) - .setStyle(TextInputStyle.Paragraph) - .setLabel("Content (markdown)") - .setValue(news.body) - .setRequired(true); - - const imageURLInput = new TextInputBuilder() - .setCustomId("imageurl") - .setPlaceholder("Big image URL (bottom)") - .setStyle(TextInputStyle.Short) - .setMaxLength(1000) - .setLabel("Big image URL (bottom)") - .setValue(news.imageURL) - .setRequired(false); - - const firstActionRow = new ActionRowBuilder().addComponents(titleInput) as ActionRowBuilder; - const secondActionRow = new ActionRowBuilder().addComponents(bodyInput) as ActionRowBuilder; - const thirdActionRow = new ActionRowBuilder().addComponents(imageURLInput) as ActionRowBuilder; - - editModal.addComponents(firstActionRow, secondActionRow, thirdActionRow); - await interaction.showModal(editModal).catch((err) => { - console.error(err); - }); - - interaction.client.once("interactionCreate", async (interaction) => { - if (!interaction.isModalSubmit()) return; - if (interaction.customId !== "editnews") return; - - const title = interaction.fields.getTextInputValue("title") as string; - const body = interaction.fields.getTextInputValue("body") as string; - const imageURL = interaction.fields.getTextInputValue("imageurl") as string | undefined; - let validURL = false; - if (imageURL) validURL = validateURL(imageURL); - - if (!validURL && imageURL) { - await interaction.reply({ - embeds: [errorEmbed("The image URL you provided is invalid.")], - }); - return; - } - - const newNews = { - ...news, - title, - body, - imageURL, - author, - authorPfp: user.avatarURL(), - updatedAt: timestamp - }; - - sendSubscribedNewsletter(guild, { - ...newNews, - title: `Updated: ${newNews.title}`, - } as News).catch((err) => { - console.error(err); - }); - - const newsEmbed = new EmbedBuilder() - .setAuthor({ name: newNews.author, iconURL: newNews.authorPfp ?? null }) - .setTitle(newNews.title) - .setDescription(newNews.body) - .setImage(newNews.imageURL || null) - .setTimestamp(parseInt(newNews.updatedAt)) - .setFooter({ text: `Updated news from ${guild.name}` }) - .setColor(genColor(200)); - - const subscribedNewsChannel = await newsTable?.get(`${guild.id}.channel`).then( - (channel) => channel as { channelId: string; roleId: string } | null - ).catch(() => { - return { - channelId: null as string | null, - roleId: null as string | null - }; - }); - if (Boolean(subscribedNewsChannel.channelId)) { - const messageId = newNews?.messageId; - const newsChannel = (await guild.channels.fetch(subscribedNewsChannel?.channelId ?? "").catch(() => { })) as TextChannel | null; - - if (!messageId && newsChannel.id) { - newNews.messageId = ((await newsChannel?.send({ - embeds: [newsEmbed], - content: subscribedNewsChannel.roleId ? `<@&${subscribedNewsChannel.roleId}>` : null - }).catch(() => { })) as Message | null)?.id; - } else if (newsChannel.id) { - await newsChannel?.messages.edit(messageId, { - embeds: [newsEmbed], - content: subscribedNewsChannel.roleId ? `<@&${subscribedNewsChannel.roleId}>` : null - }).catch(() => { }); - } - } - - const embed = new EmbedBuilder() - .setTitle("✅ • News edited!") - .setColor(genColor(100)); - - await newsTable.set(`${guild.id}.news.${id}`, newNews); - await interaction.reply({ embeds: [embed] }); - }); - } -} diff --git a/src/commands/settings/news/remove.ts b/src/commands/settings/news/remove.ts deleted file mode 100644 index 934fc3a..0000000 --- a/src/commands/settings/news/remove.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { - SlashCommandSubcommandBuilder, - EmbedBuilder, - PermissionsBitField, - type ChatInputCommandInteraction, - TextChannel, -} from "discord.js"; -import { getNewsTable } from "../../../utils/database.js"; -import { genColor } from "../../../utils/colorGen.js"; -import errorEmbed from "../../../utils/embeds/errorEmbed.js"; -import { QuickDB } from "quick.db"; - -export default class Remove { - data: SlashCommandSubcommandBuilder; - db: QuickDB; - - constructor(db?: QuickDB) { - this.db = db; - this.data = new SlashCommandSubcommandBuilder() - .setName("remove") - .setDescription("Removes news from your guild.") - .addStringOption((option) => - option - .setName("id") - .setDescription("The id of the news (can be found in the footer of the news).") - .setRequired(true) - ); - } - - async run(interaction: ChatInputCommandInteraction) { - const db = this.db; - const newsTable = await getNewsTable(db); - - const user = interaction.user; - const guild = interaction.guild; - const providedId = interaction.options.getString("id"); - const member = guild.members.cache.get(user.id); - - if (!member.permissions.has(PermissionsBitField.Flags.ManageGuild)) { - return await interaction.followUp({ - embeds: [errorEmbed("You need **Manage Server** permissions to delete news.")], - }); - } - - const embed = new EmbedBuilder().setTitle("✅ • News deleted!").setColor(genColor(100)); - - const subscribedChannel = await newsTable?.get(`${guild.id}.channel`).then( - (channel) => channel as { channelId: string, roleId: string } - ).catch(() => { - return { - channelId: null, - roleId: null - } - }); - const news = await newsTable?.get(providedId).catch(() => null); - if (!news) return await interaction.followUp({ embeds: [errorEmbed("The specified news doesn't exist.")] }); - - const messageId = news?.messageId; - const newsChannel = (await interaction.guild.channels.fetch(subscribedChannel?.channelId ?? "").catch(() => null)) as TextChannel | null; - if (newsChannel) await newsChannel?.messages.delete(messageId).catch(() => null); - - await newsTable?.delete(`${guild.id}.news.${providedId}`).catch(() => null); - await interaction.followUp({ embeds: [embed] }); - } -} From b9f48c4fe168014899395045e06b5ac4c8a2019e Mon Sep 17 00:00:00 2001 From: Serge Date: Thu, 11 Jan 2024 09:42:38 +0600 Subject: [PATCH 004/127] okay now this is done --- src/events/easterEggs.ts | 20 ----- src/events/error.ts | 16 ---- src/events/leveling.ts | 147 ------------------------------------- src/events/loadCommands.ts | 68 ----------------- src/utils/validateURL.ts | 4 - 5 files changed, 255 deletions(-) delete mode 100644 src/events/easterEggs.ts delete mode 100644 src/events/error.ts delete mode 100644 src/events/leveling.ts delete mode 100644 src/events/loadCommands.ts delete mode 100644 src/utils/validateURL.ts diff --git a/src/events/easterEggs.ts b/src/events/easterEggs.ts deleted file mode 100644 index 6046dde..0000000 --- a/src/events/easterEggs.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { Message } from "discord.js"; -import { pathToFileURL } from "url"; -import { join } from "path"; -import { readdirSync } from "fs"; - -export default { - name: "messageCreate", - event: class MessageCreate { - async run(message: Message) { - if (message.author.bot) return; - if (message.guildId !== "903852579837059113" && message.guildId !== "986268144446341142") return; - const eventsPath = join(process.cwd(), "src", "events"); - - for (const easterEggFile of readdirSync(join(eventsPath, "easterEggs"))) { - const msg = await import(pathToFileURL(join(eventsPath, "easterEggs", easterEggFile)).toString()); - new msg.default().run(message, ...message.content); - } - } - }, -}; diff --git a/src/events/error.ts b/src/events/error.ts deleted file mode 100644 index b593e5b..0000000 --- a/src/events/error.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { type Client } from "discord.js"; - -export default { - name: "error", - event: class Error { - client: Client; - - constructor(client: Client) { - this.client = client; - } - - async run(err) { - console.error(err); - } - } -} diff --git a/src/events/leveling.ts b/src/events/leveling.ts deleted file mode 100644 index 54eccac..0000000 --- a/src/events/leveling.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { ColorResolvable, EmbedBuilder, TextChannel, type Message } from "discord.js"; -import Vibrant from "node-vibrant"; -import sharp from "sharp"; -import { genColor, genRGBColor } from "../utils/colorGen.js"; -import database, { getLevelingTable, getSettingsTable } from "../utils/database.js"; -import { Reward } from "../commands/settings/leveling/rewards.js"; - -export const EXP_PER_MESSAGE = 10; // 10 exp per message -export const BASE_EXP_FOR_NEW_LEVEL = 10 * 100; // 1000 messages to level up -export const DIFFICULTY_MULTIPLIER = 1.25; // 1.25x harder to level up each level - -export default { - name: "messageCreate", - event: class MessageCreate { - db = null; - - async run(message: Message) { - if (!this.db) this.db = await database(); - const db = this.db; - const levelingTable = await getLevelingTable(db); - const settingsTable = await getSettingsTable(db); - const target = message.author; - - if (message.author.bot) return; - - const levelingEnabled = await settingsTable?.get(`${message.guild.id}.leveling.enabled`).then( - (enabled) => !!enabled - ).catch(() => false); - - if (!levelingEnabled) return; - - const { exp, levels } = await levelingTable?.get(`${message.guild.id}.${target.id}`).catch(() => { - return { - exp: 0, - levels: 0 - }; - }); - const { exp: expGlobal, levels: levelsGlobal } = await levelingTable?.get(`global.${target.id}`).then( - (data) => { - if (!data) return { - exp: 0, - levels: 0 - }; - return { - exp: Number(data.exp), - levels: Number(data.levels) - }; - } - ).catch(() => { - return { - exp: 0, - levels: 0 - }; - }); - - const expUntilLevelup = Math.floor(BASE_EXP_FOR_NEW_LEVEL * DIFFICULTY_MULTIPLIER * (levels + 1)); - const expUntilLevelupGlobal = Math.floor(BASE_EXP_FOR_NEW_LEVEL * DIFFICULTY_MULTIPLIER * (levelsGlobal + 1)); - const expUntilNextLevelup = Math.floor(BASE_EXP_FOR_NEW_LEVEL * DIFFICULTY_MULTIPLIER * (levels + 2)); - - const newLevelData = { - levels: levels ?? 0, - exp: (exp ?? 0) + EXP_PER_MESSAGE - }; - - const newLevelDataGlobal = { - levels: levelsGlobal ?? 0, - exp: (expGlobal ?? 0) + EXP_PER_MESSAGE - }; - - if (!(exp >= expUntilLevelup - 1)) { - await levelingTable.set(`global.${target.id}`, newLevelDataGlobal); - return await levelingTable.set(`${message.guild.id}.${target.id}`, newLevelDataGlobal); - } else if (exp >= expUntilLevelup - 1) { - let leftOverExp = exp - expUntilLevelup; - if (leftOverExp < 0) leftOverExp = 0; - - newLevelData.levels = levels + 1; - newLevelData.exp = leftOverExp ?? 0; - - await levelingTable.set(`${message.guild.id}.${target.id}`, newLevelData); - } - - if (exp >= expUntilLevelupGlobal - 1) { - let leftOverExpGlobal = exp - expUntilLevelup; - if (leftOverExpGlobal < 0) leftOverExpGlobal = 0; - - newLevelDataGlobal.levels = levels + 1; - newLevelDataGlobal.exp = leftOverExpGlobal + 1; - - await levelingTable.set(`global.${target.id}`, newLevelDataGlobal); - } - - // Check if there's a level up channel - const levelChannelId = await settingsTable?.get(`${message.guild.id}.leveling.channel`).then( - (channelId) => String(channelId) - ).catch(() => null); - if (!levelChannelId) return; - const levelChannel = message.guild.channels.cache.get(levelChannelId) as TextChannel - - // Level up embed - const leveledEmbed = new EmbedBuilder() - .setTitle("⚡ • Level Up!") - .setDescription([ - `**Congratulations <@${target.id}>**!`, - `You made it to **level ${levels + 1}**`, - `You need ${expUntilNextLevelup} exp to level up again.` - ].join("\n")) - .setAuthor({ - name: target.displayName, - url: target.avatarURL() - }) - .setThumbnail(target.avatarURL()) - .setColor(genColor(200)) - .setTimestamp(); - - // Get vibrant color - try { - const imageBuffer = await (await fetch(target.avatarURL())).arrayBuffer(); - const image = sharp(imageBuffer).toFormat("jpg"); - const { r, g, b } = (await new Vibrant(await image.toBuffer()).getPalette()).Vibrant; - leveledEmbed.setColor(genRGBColor(r, g, b) as ColorResolvable); - } catch { } - - // Sending the level up - levelChannel.send({ - embeds: [leveledEmbed], - content: `<@${target.id}>` - }); - - // Checking if there is a level reward - const levelRewards = await settingsTable?.get(`${message.guild.id}.leveling.rewards`).then( - (rewards) => rewards as Reward[] ?? [] as Reward[] - ).catch(() => [] as Reward[]); - const members = await message.guild.members.fetch(); - for (const { level, roleId } of levelRewards) { - const role = message.guild.roles.cache.get(roleId); - - if (levels >= level) { - await members.get(target.id)?.roles.add(role); - continue; - } - - await members.get(target.id)?.roles.remove(role); - } - } - } -} diff --git a/src/events/loadCommands.ts b/src/events/loadCommands.ts deleted file mode 100644 index 2530122..0000000 --- a/src/events/loadCommands.ts +++ /dev/null @@ -1,68 +0,0 @@ -import type { CommandInteraction, Interaction, Client, AutocompleteInteraction } from "discord.js"; -import { pathToFileURL } from "url"; -import { join } from "path"; -import database from "../utils/database.js"; -import { QuickDB } from "quick.db"; - -const COMMANDS_PATH = join(process.cwd(), "src", "commands"); - -async function getCommand(interaction: CommandInteraction | AutocompleteInteraction, options: any, db: QuickDB): Promise { - const commandName = interaction.commandName; - const subcommandName = options.getSubcommand(false); - const commandgroupName = options.getSubcommandGroup(false); - - const commandImportPath = join( - COMMANDS_PATH, - `${subcommandName ? - `${commandName}/${commandgroupName ? - `${commandgroupName}/${subcommandName}` : - subcommandName}` : - commandName}.ts` - ); - const command = new (await import(pathToFileURL(commandImportPath).toString())).default(db); - return command; -} - -export default { - name: "interactionCreate", - event: class InteractionCreate { - commands: CommandInteraction; - client: Client; - lastOpenedDb = Date.now(); - db: QuickDB = null; - - constructor(cmds: CommandInteraction, client: Client) { - this.commands = cmds; - this.client = client; - this.lastOpenedDb = Date.now(); - } - - async run(interaction: Interaction) { - if (!this.db) this.db = await database(); - if (Date.now() - this.lastOpenedDb > 1000 * 60 * 60) { - this.db = await database(); - this.lastOpenedDb = Date.now(); - } - - if (interaction.isChatInputCommand()) { - const options = interaction.options - - // Command names - const command = await getCommand(interaction, options, this.db).catch(() => null); - if (!command) return; - - const deferred = command?.deferred ?? true; - if (deferred) await interaction.deferReply(); - - command.run(interaction); - } else if (interaction.isAutocomplete()) { - const options = interaction.options; - - const command = await getCommand(interaction, options, this.db).catch(() => null); - if (!command) return; - if (!command.autocomplete) return; - command.autocomplete(interaction); - } - } - } -}; diff --git a/src/utils/validateURL.ts b/src/utils/validateURL.ts deleted file mode 100644 index 7b6857e..0000000 --- a/src/utils/validateURL.ts +++ /dev/null @@ -1,4 +0,0 @@ -export default function validateURL(url) { - if (/^(http(s):\/\/.)[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)$/g.test(url)) return true; - return false; -} From e951e0c2ab6f494fa790661b4888866a84b9a4cf Mon Sep 17 00:00:00 2001 From: froxcey Date: Sat, 13 Jan 2024 00:57:05 +0800 Subject: [PATCH 005/127] Add sqlite querys --- .gitignore | 1 + src/utils/database.ts | 23 ------- src/utils/database/disabledCommands.ts | 35 ++++++++++ src/utils/database/index.ts | 14 ++++ src/utils/database/levelBlockedChannels.ts | 40 +++++++++++ src/utils/database/levelRewards.ts | 54 +++++++++++++++ src/utils/database/leveling.ts | 38 +++++++++++ src/utils/database/moderation.ts | 68 +++++++++++++++++++ src/utils/database/news.ts | 78 ++++++++++++++++++++++ src/utils/database/newsCategories.ts | 62 +++++++++++++++++ src/utils/database/settings.ts | 61 +++++++++++++++++ src/utils/database/types.ts | 25 +++++++ 12 files changed, 476 insertions(+), 23 deletions(-) delete mode 100644 src/utils/database.ts create mode 100644 src/utils/database/disabledCommands.ts create mode 100644 src/utils/database/index.ts create mode 100644 src/utils/database/levelBlockedChannels.ts create mode 100644 src/utils/database/levelRewards.ts create mode 100644 src/utils/database/leveling.ts create mode 100644 src/utils/database/moderation.ts create mode 100644 src/utils/database/news.ts create mode 100644 src/utils/database/newsCategories.ts create mode 100644 src/utils/database/settings.ts create mode 100644 src/utils/database/types.ts diff --git a/.gitignore b/.gitignore index 713d500..5a00c13 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules/ .env +.DS_Store diff --git a/src/utils/database.ts b/src/utils/database.ts deleted file mode 100644 index 1a2f2fc..0000000 --- a/src/utils/database.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { QuickDB, MySQLDriver } from "quick.db"; - -export async function database() { - const mysql = new MySQLDriver({ - host: process.env.MYSQL_HOST, - user: process.env.MYSQL_USERNAME, - port: parseInt(process.env.MYSQL_PORT), - password: process.env.MYSQL_PASSWORD, - database: process.env.MYSQL_DATABASE, - enableKeepAlive: true, - compress: true - }); - - await mysql.connect(); - const db = new QuickDB({ driver: mysql }); - return db; -} - -export const getSettingsTable = async (db: QuickDB) => await db.tableAsync("settings"); -export const getLevelingTable = async (db: QuickDB) => await db.tableAsync("leveling"); -export const getServerboardTable = async (db: QuickDB) => await db.tableAsync("serverboard"); -export const getNewsTable = async (db: QuickDB) => await db.tableAsync("news"); -export const getModerationTable = async (db: QuickDB) => await db.tableAsync("moderation"); diff --git a/src/utils/database/disabledCommands.ts b/src/utils/database/disabledCommands.ts new file mode 100644 index 0000000..542ed83 --- /dev/null +++ b/src/utils/database/disabledCommands.ts @@ -0,0 +1,35 @@ +import { getDatabase } from "."; +import { TableDefinition, TypeOfDefinition } from "./types"; + +const tableDefinition = { + name: "disabledCommands", + definition: { + guild: "INTEGER", + command: "TEXT", + }, +} satisfies TableDefinition; + +const database = getDatabase(tableDefinition); + +const getQuery = database.query( + "SELECT * FROM disabledCommands WHERE guild = $1;", +); +export function getDisabledCommands(guildID: string) { + return ( + getQuery.all(guildID) as TypeOfDefinition[] + ).map((val) => val.command); +} + +const addQuery = database.query( + "INSERT INTO disabledCommands (guild, command) VALUES (?1, ?2);", +); +export function disableCommands(guildID: string, command: string) { + addQuery.run(guildID, command); +} + +const removeQuery = database.query( + "DELETE FROM disabledCommands WHERE guild = $1 AND command = $2", +); +export function enableCommands(guildID: string, command: string) { + removeQuery.run(guildID, command); +} diff --git a/src/utils/database/index.ts b/src/utils/database/index.ts new file mode 100644 index 0000000..65cefa9 --- /dev/null +++ b/src/utils/database/index.ts @@ -0,0 +1,14 @@ +import { Database } from "bun:sqlite"; +import { TableDefinition } from "./types"; + +// Get (or create) SQLite database +const database = new Database("data.db", { create: true }); + +export function getDatabase(definition: TableDefinition) { + // Create table if not exist + const defStr = Object.entries(definition.definition) + .map(([field, type]) => field.concat(" ", type)) + .join(", "); + database.run(`CREATE TABLE IF NOT EXISTS ${definition.name} (${defStr});`); + return database; +} diff --git a/src/utils/database/levelBlockedChannels.ts b/src/utils/database/levelBlockedChannels.ts new file mode 100644 index 0000000..175e5e2 --- /dev/null +++ b/src/utils/database/levelBlockedChannels.ts @@ -0,0 +1,40 @@ +import { getDatabase } from "."; +import { TableDefinition, TypeOfDefinition } from "./types"; + +const tableDefinition = { + name: "levelBlockedChannels", + definition: { + guild: "INTEGER", + channel: "INTEGER", + }, +} satisfies TableDefinition; + +const database = getDatabase(tableDefinition); + +const getQuery = database.query( + "SELECT * FROM levelBlockedChannels WHERE guild = $1 AND channel = $2;", +); +export function getBlockedChannels(guildID: string, channelID: string) { + return getQuery.all(guildID, channelID).length == 0; +} + +const listQuery = database.query("SELECT * FROM levelBlockedChannels WHERE guild = $1;"); +export function listBlockedChannels(guildID: string) { + return ( + listQuery.all(guildID) as TypeOfDefinition[] + ).map((val) => val.channel); +} + +const addQuery = database.query( + "INSERT INTO levelBlockedChannels (guild, channel) VALUES (?1, ?2);", +); +export function blockChannel(guildID: string, channelID: string) { + addQuery.run(guildID, channelID); +} + +const removeQuery = database.query( + "DELETE FROM levelBlockedChannels WHERE guild = $1 AND channel = $2;", +); +export function unblockChannel(guildID: string, channelID: string) { + removeQuery.run(guildID, channelID); +} diff --git a/src/utils/database/levelRewards.ts b/src/utils/database/levelRewards.ts new file mode 100644 index 0000000..fd96fd5 --- /dev/null +++ b/src/utils/database/levelRewards.ts @@ -0,0 +1,54 @@ +// TODO: Implement logic + +import { getDatabase } from "."; +import { TableDefinition, TypeOfDefinition } from "./types"; + +const tableDefinition = { + name: "levelRewards", + definition: { + guild: "INTEGER", + role: "INTEGER", + level: "INTEGER", + }, +} satisfies TableDefinition; + +const database = getDatabase(tableDefinition); + +const getQuery = database.query("SELECT * FROM levelRewards WHERE guild = $1;"); +export function get(guildID: string) { + return getQuery.all(guildID) as TypeOfDefinition[]; +} + +const addQuery = database.query( + "INSERT INTO levelRewards (guild, role, level) VALUES (?1, ?2, ?3);", +); +export function addReward( + guildID: string, + role: number | string, + level: number, +) { + return addQuery.all(guildID, level, role) as TypeOfDefinition< + typeof tableDefinition + >[]; +} + +/** + * Update the level requirement for a reward + */ +const updateQuery = database.query( + "UPDATE levelRewards SET level = $3 WHERE guild = $1 AND role = $2", +); +export function updateReward( + guildID: string, + role: number | string, + level: number, +) { + updateQuery.run(guildID, role, level); +} + +const removeQuery = database.query( + "DELETE FROM levelRewards WHERE guild = $1 AND role = $2", +); +export function removeReward(guildID: number | string, role: number | string) { + removeQuery.run(guildID, role); +} diff --git a/src/utils/database/leveling.ts b/src/utils/database/leveling.ts new file mode 100644 index 0000000..5635ef6 --- /dev/null +++ b/src/utils/database/leveling.ts @@ -0,0 +1,38 @@ +import { getDatabase } from "."; +import { TableDefinition, TypeOfDefinition } from "./types"; + +const tableDefinition = { + name: "leveling", + definition: { + guild: "INTEGER", + user: "INTEGER", + level: "INTEGER", + exp: "INTEGER", + }, +} satisfies TableDefinition; + +const database = getDatabase(tableDefinition); + +const getQuery = database.query( + "SELECT * FROM leveling WHERE guild = $1 AND user = $2;", +); +export function getLevel(guildID: string, userID: string): [number, number] { + const res = getQuery.all(guildID, userID) as TypeOfDefinition< + typeof tableDefinition + >[]; + if (res.length == 0) return [0, 0]; + return [res[0].level, res[0].exp]; +} + +const setQuery = database.query("UPDATE leveling SET level = $3, exp = $4 WHERE guild = $1 AND user = $2;"); +const insertQuery = database.query("INSERT INTO leveling (guild, user, level, exp) VALUES (?1, ?2, ?3, ?4);"); +export function setLevel( + guildID: string | number, + userID: string, + level: number, + exp: number, +) { + getQuery.all(guildID, userID).length == 0 + ? insertQuery.run(guildID, userID, level, exp) + : setQuery.run(guildID, userID, level, exp); +} diff --git a/src/utils/database/moderation.ts b/src/utils/database/moderation.ts new file mode 100644 index 0000000..2c5bbe6 --- /dev/null +++ b/src/utils/database/moderation.ts @@ -0,0 +1,68 @@ +import { getDatabase } from "."; +import { TableDefinition, TypeOfDefinition } from "./types"; + +const definition = { + name: "moderation", + definition: { + guild: "INTEGER", + user: "INTEGER", + type: "TEXT", + moderator: "INTEGER", + reason: "TEXT", + public: "BOOL", + id: "TEXT", + timestamp: "TIMESTAMP", + }, +} satisfies TableDefinition; + +type modType = "MUTE" | "WARN" | "KICK" | "BAN"; + +const database = getDatabase(definition); + +const addQuery = database.query( + "INSERT INTO moderation (guild, user, type, moderator, reason, public, id, timestamp) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8);", +); +export function addModeration( + guildID: string | number, + userID: string, + type: modType, + moderator: string, + reason = "", + pub = false, +) { + const id = crypto.randomUUID(); + addQuery.run(guildID, userID, type, moderator, reason, pub, id, Date.now()); + return id; +} + +const listUserQuery = database.query( + "SELECT * FROM moderation WHERE guild = $1 AND user = $2 AND type = $3;", +); +export function listUserModeration( + guildID: number | string, + userID: number | string, + type: modType, +) { + return listUserQuery.all(guildID, userID, type) as TypeOfDefinition< + typeof definition + >[]; +} + +const listModQuery = database.query( + "SELECT * FROM moderation WHERE guild = $1 AND moderator = $2;", +); +export function listModeratorLog( + guildID: number | string, + moderator: number | string, +) { + return listModQuery.all(guildID, moderator) as TypeOfDefinition< + typeof definition + >[]; +} + +const removeQuery = database.query( + "DELETE FROM moderation WHERE guild = $1 AND id = $2", +); +export function removeModeration(guildID: string | number, id: string) { + removeQuery.run(guildID, id); +} diff --git a/src/utils/database/news.ts b/src/utils/database/news.ts new file mode 100644 index 0000000..31bc66a --- /dev/null +++ b/src/utils/database/news.ts @@ -0,0 +1,78 @@ +import { getDatabase } from "."; +import { TableDefinition, TypeOfDefinition } from "./types"; + +const definition = { + name: "news", + definition: { + guild: "INTEGER", + title: "TEXT", + body: "TEXT", + imageURL: "TEXT", + author: "TEXT", + authorPFP: "TEXT", + createdAt: "TIMESTAMP", + updatedAt: "TIMESTAMP", + messageID: "INTEGER", + categoryID: "TEXT", + id: "TEXT", + }, +} satisfies TableDefinition; + +const database = getDatabase(definition); + +const addQuery = database.query(""); +export function addNews( + guildID: number | string, + title: string, + body: string, + imageURL: string | null, + author: string, + authorPFP: string, + messageID: string | number, + categoryID: string, +) { + addQuery.run( + guildID, + title, + body, + imageURL, + author, + authorPFP, + Date.now(), + 0, + messageID, + categoryID, + crypto.randomUUID(), + ); +} + +const listAllQuery = database.query("SELECT * FROM news WHERE guild = $1;"); +export function listAllNews(guildID: string | number) { + return listAllQuery.all(guildID) as TypeOfDefinition[]; +} + +const listCategoryQuery = database.query( + "SELECT * FROM news WHERE guild = $1 AND categoryID = $2;", +); +export function listCategoryNews(guildID: string | number, categoryID: string) { + return listCategoryQuery.all(guildID, guildID) as TypeOfDefinition< + typeof definition + >[]; +} + +const updateQuery = database.query( + "UPDATE news SET title = $2, body = $3, imageURL = $4 WHERE id = $1", +); +export function updateNews( + id: string, + title: string, + body: string, + imageURL: string, +) { + updateQuery.run(id, title, body, imageURL); +} + +const deleteQuery = database.query("DELETE FROM news WHERE id = $1"); +export function deleteNews(id: string) { + deleteQuery.run(id); +} diff --git a/src/utils/database/newsCategories.ts b/src/utils/database/newsCategories.ts new file mode 100644 index 0000000..a40b023 --- /dev/null +++ b/src/utils/database/newsCategories.ts @@ -0,0 +1,62 @@ +// TODO: +import { getDatabase } from "."; +import { TableDefinition, TypeOfDefinition } from "./types"; + +const definition = { + name: "newsCategories", + definition: { + guild: "INTEGER", + name: "TEXT", + role: "INTEGER", + channel: "INTEGER", + id: "TEXT", + }, +} satisfies TableDefinition; + +const database = getDatabase(definition); + +const createQuery = database.query( + "INSERT INTO newsCategories (guild, name, role, channel, id) VALUES (?1, ?2, ?3, ?4, ?5);", +); +export function createCategory( + guildID: number | string, + name: string, + role: number | string, + channel: number | string, +) { + createQuery.run(guildID, name, role, channel, crypto.randomUUID()); +} + +const listQuery = database.query( + "SELECT * FROM newsCategories WHERE guild = $1;", +); +export function listCategories(guildID: number | string) { + return listQuery.all(guildID) as TypeOfDefinition[]; +} + +const findNameQuery = database.query( + "SELECT * FROM newsCategories WHERE guild = $1 AND name = $2;", +); +export function findWithName(guildID: number | string, name: string) { + return findNameQuery.get(guildID, name) as TypeOfDefinition< + typeof definition + >[]; +} + +const updateQuery = database.query( + "UPDATE newsCategories SET name = $3 WHERE guild = $1 AND name = $2", +); +export function updateCategory( + guildID: number | string, + name: string, + newName: string, +) { + updateQuery.run(guildID, name, newName); +} + +const deleteQuery = database.query( + "DELETE FROM newsCategories WHERE guild = $1 AND name = $2", +); +export function deleteCategory(guildID: number | string, name: string) { + deleteQuery.run(guildID, name); +} diff --git a/src/utils/database/settings.ts b/src/utils/database/settings.ts new file mode 100644 index 0000000..c0c82e4 --- /dev/null +++ b/src/utils/database/settings.ts @@ -0,0 +1,61 @@ +// TODO: Add more settings + +import { getDatabase } from "."; +import { FieldData, SqlType, TableDefinition, TypeOfDefinition } from "./types"; + +// Define table structure +const tableDefinition = { + name: "settings", + definition: { + guild: "INTEGER", + key: "TEXT", + value: "TEXT", + }, +} satisfies TableDefinition; + +// Define type of settings +const settingsDefinition = { + "leveling.enabled": "BOOL", + "leveling.channel": "INTEGER", + "leveling.persistence": "BOOL", + "log.channel": "INTEGER", + "serverboard.inviteLink": "TEXT", + "serverboard.shown": "BOOL", +} satisfies Record; + +export const settingKeys = Object.keys( + settingsDefinition, +) as (keyof typeof settingsDefinition)[]; + +const database = getDatabase(tableDefinition); + +const getQuery = database.query( + "SELECT * FROM settings WHERE guild = $1 AND key = $2;", +); +export function get( + guild: string, + key: K, +): TypeOfKey | null { + let res = getQuery.all(guild, key) as TypeOfDefinition< + typeof tableDefinition + >[]; + if (res.length == 0) return null; + if (settingsDefinition[key] == "TEXT") return res[0].value; + return JSON.parse(res[0].value); +} + +let setQuery = database.query( + "UPDATE settings SET value = $3 WHERE guild = $1 AND key = $2;", +); +export function set( + guild: string, + key: K, + value: TypeOfKey, +) { + setQuery.run(guild, key, JSON.stringify(value)); +} + +// Utility type +type TypeOfKey = SqlType< + (typeof settingsDefinition)[T] +>; diff --git a/src/utils/database/types.ts b/src/utils/database/types.ts new file mode 100644 index 0000000..c589505 --- /dev/null +++ b/src/utils/database/types.ts @@ -0,0 +1,25 @@ +export type FieldData = + | "TEXT" + | "INTEGER" + | "FLOAT" + | "BOOL" + | "TIMESTAMP" + | "JSON"; + +export type TableDefinition = { + name: string; + definition: Record; +}; + +export type SqlType = { + BOOL: boolean; + INTEGER: number; + FLOAT: number; + TEXT: string; + TIMESTAMP: Date; + JSON: any; +}[T]; + +export type TypeOfDefinition = { + [K in keyof T["definition"]]: SqlType; +}; From f35f3030bb4624ec19256eb81a99589da8823a8a Mon Sep 17 00:00:00 2001 From: Serge Date: Sun, 14 Jan 2024 00:14:31 +0600 Subject: [PATCH 006/127] started fixing things --- bun.lockb | Bin 60561 -> 55882 bytes package.json | 2 - src/bot.ts | 4 +- src/commands/about.ts | 6 +- src/commands/news.ts | 2 +- src/commands/server/leaderboard.ts | 6 +- src/events/guildCreate.ts | 6 +- src/events/guildMemberAdd.ts | 44 ------- src/events/guildMemberRemove.ts | 44 ------- src/events/interactionCreate.ts | 24 +--- src/events/messageCreate.ts | 110 ++++++------------ src/handlers/commands.ts | 33 +++--- src/handlers/events.ts | 2 +- src/utils/colorGen.ts | 8 +- src/utils/database/disabledCommands.ts | 20 +--- src/utils/database/index.ts | 5 +- src/utils/database/levelBlockedChannels.ts | 22 ++-- src/utils/database/levelRewards.ts | 40 ++----- .../database/{leveling.ts => levelling.ts} | 25 ++-- src/utils/database/moderation.ts | 42 ++----- src/utils/database/news.ts | 33 ++---- src/utils/database/newsCategories.ts | 36 ++---- src/utils/database/settings.ts | 47 +++----- src/utils/multiReact.ts | 2 +- src/utils/sendChannelNews.ts | 9 +- src/utils/sendSubscribedNews.ts | 20 ++-- tsconfig.json | 30 ++--- 27 files changed, 178 insertions(+), 444 deletions(-) delete mode 100644 src/events/guildMemberAdd.ts delete mode 100644 src/events/guildMemberRemove.ts rename src/utils/database/{leveling.ts => levelling.ts} (65%) diff --git a/bun.lockb b/bun.lockb index a6d4250375c527ed9001036d147e1e700250a064..0f508ce8113890e241f6b47e883dade01271e0ad 100644 GIT binary patch delta 7775 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 n#J4?ltvIXJs { }); const levels = await levelingTable?.get(`${interaction.guild.id}`).catch(() => { }); diff --git a/src/events/guildCreate.ts b/src/events/guildCreate.ts index f914978..393dccb 100644 --- a/src/events/guildCreate.ts +++ b/src/events/guildCreate.ts @@ -2,9 +2,9 @@ import { EmbedBuilder, type DMChannel, type Client, type Guild } from "discord.js"; -import { genColor } from "../utils/colorGen.js"; -import { randomise } from "../utils/randomise.js"; -import Commands from "../handlers/commands.js"; +import { genColor } from "../utils/colorGen"; +import { randomise } from "../utils/randomise"; +import Commands from "../handlers/commands"; export default { name: "guildCreate", diff --git a/src/events/guildMemberAdd.ts b/src/events/guildMemberAdd.ts deleted file mode 100644 index 927467e..0000000 --- a/src/events/guildMemberAdd.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { - EmbedBuilder, type Client, type GuildMember, - type TextChannel, type ColorResolvable -} from "discord.js"; -import { genColor, genRGBColor } from "../utils/colorGen.js"; -import Vibrant from "node-vibrant"; -import sharp from "sharp"; - -export default { - name: "guildMemberAdd", - event: class GuildMemberAdd { - client: Client; - - constructor(client: Client) { - this.client = client; - } - - async run(member: GuildMember) { - const id = "1079612083307548704"; - const channel = await member.guild.channels.cache.find(channel => channel.id === id).fetch() as TextChannel; - const avatarURL = member.displayAvatarURL(); - - const embed = new EmbedBuilder() - .setAuthor({ - name: `• ${member.nickname == null ? member.user.username : member.nickname}`, - iconURL: avatarURL - }) - .setTitle("Welcome!") - .setDescription(`Enjoy your stay in **${member.guild.name}**!`) - .setFooter({ text: `User ID: ${member.id}` }) - .setThumbnail(avatarURL) - .setColor(genColor(200)); - - try { - const imageBuffer = await (await fetch(avatarURL)).arrayBuffer(); - const image = sharp(imageBuffer).toFormat("jpg"); - const { r, g, b } = (await new Vibrant(await image.toBuffer()).getPalette()).Vibrant; - embed.setColor(genRGBColor(r, g, b) as ColorResolvable); - } catch {} - - await channel.send({ embeds: [embed] }); - } - } -} diff --git a/src/events/guildMemberRemove.ts b/src/events/guildMemberRemove.ts deleted file mode 100644 index 6e1d9a7..0000000 --- a/src/events/guildMemberRemove.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { - EmbedBuilder, type Client, type GuildMember, - type TextChannel, type ColorResolvable -} from "discord.js"; -import { genColor, genRGBColor } from "../utils/colorGen.js"; -import Vibrant from "node-vibrant"; -import sharp from "sharp"; - -export default { - name: "guildMemberRemove", - event: class GuildMemberRemove { - client: Client; - - constructor(client: Client) { - this.client = client; - } - - async run(member: GuildMember) { - const id = "1079612083307548704"; - const channel = await member.guild.channels.cache.find(channel => channel.id === id).fetch() as TextChannel; - const avatarURL = member.displayAvatarURL(); - - const embed = new EmbedBuilder() - .setAuthor({ - name: `• ${member.nickname == null ? member.user.username : member.nickname}`, - iconURL: avatarURL - }) - .setTitle("Goodbye!") - .setDescription(`**@${member.user.username}** has left the server 😥`) - .setFooter({ text: `User ID: ${member.id}` }) - .setThumbnail(avatarURL) - .setColor(genColor(200)); - - try { - const imageBuffer = await (await fetch(avatarURL)).arrayBuffer(); - const image = sharp(imageBuffer).toFormat("jpg"); - const { r, g, b } = (await new Vibrant(await image.toBuffer()).getPalette()).Vibrant; - embed.setColor(genRGBColor(r, g, b) as ColorResolvable); - } catch {} - - await channel.send({ embeds: [embed] }); - } - } -} diff --git a/src/events/interactionCreate.ts b/src/events/interactionCreate.ts index a4f092a..6769d37 100644 --- a/src/events/interactionCreate.ts +++ b/src/events/interactionCreate.ts @@ -1,20 +1,17 @@ import type { CommandInteraction, Client, AutocompleteInteraction } from "discord.js"; -import type { QuickDB } from "quick.db"; import { pathToFileURL } from "url"; import { join } from "path"; -import { database } from "../utils/database.js"; -async function getCommand(interaction: CommandInteraction | AutocompleteInteraction, options: any, db: QuickDB): Promise { +async function getCommand(interaction: CommandInteraction | AutocompleteInteraction, options: any): Promise { const commandName = interaction.commandName; const subcommandName = options.getSubcommand(false); - const commandgroupName = options.getSubcommandGroup(false); - + const commandGroupName = options.getSubcommandGroup(false); const commandImportPath = join( join(process.cwd(), "src", "commands"), - `${subcommandName ? `${commandName}/${commandgroupName ? `${commandgroupName}/${subcommandName}` : subcommandName}` : commandName}.ts` + `${subcommandName ? `${commandName}/${commandGroupName ? `${commandGroupName}/${subcommandName}` : subcommandName}` : commandName}.ts` ); - return new (await import(pathToFileURL(commandImportPath).toString())).default(db); + return new (await import(pathToFileURL(commandImportPath).toString())); } export default { @@ -22,29 +19,20 @@ export default { event: class InteractionCreate { commands: CommandInteraction; client: Client; - lastOpenedDb = Date.now(); - db: QuickDB = null; constructor(cmds: CommandInteraction, client: Client) { this.commands = cmds; this.client = client; - this.lastOpenedDb = Date.now(); } async run(interaction: CommandInteraction | AutocompleteInteraction) { - if (!this.db) this.db = await database(); - if (Date.now() - this.lastOpenedDb > 1000 * 60 * 60) { - this.db = await database(); - this.lastOpenedDb = Date.now(); - } - if (interaction.isChatInputCommand()) { - const command = await getCommand(interaction, interaction.options, this.db); + const command = await getCommand(interaction, interaction.options); if (!command) return; if (command?.deferred ?? true) await interaction.deferReply(); command.run(interaction); } else if (interaction.isAutocomplete()) { - const command = await getCommand(interaction, interaction.options, this.db); + const command = await getCommand(interaction, interaction.options); if (!command) return; if (!command.autocomplete) return; command.autocomplete(interaction); diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts index 7510d05..9d600a5 100644 --- a/src/events/messageCreate.ts +++ b/src/events/messageCreate.ts @@ -2,22 +2,21 @@ import { EmbedBuilder, type TextChannel, type Message } from "discord.js"; import { pathToFileURL } from "url"; import { join } from "path"; import { readdirSync } from "fs"; -import { genColor } from "../utils/colorGen.js"; -import { database, getLevelingTable, getSettingsTable } from "../utils/database.js"; -import { Reward } from "../commands/settings/leveling/rewards.js"; +import { genColor } from "../utils/colorGen"; +import { get } from "../utils/database/settings"; +import { getLevel, setLevel } from "../utils/database/levelling"; +import { get as getLevelRewards } from "../utils/database/levelRewards"; export default { name: "messageCreate", event: class MessageCreate { - db = null; - async run(message: Message) { - const target = message.author; + const author = message.author; const guild = message.guild; // Easter egg handler - if (target.bot) return; - if (guild.id !== "903852579837059113") return; + if (author.bot) return; + if (guild?.id !== "903852579837059113") return; const eventsPath = join(process.cwd(), "src", "events", "easterEggs"); for (const easterEggFile of readdirSync(eventsPath)) { @@ -26,101 +25,64 @@ export default { } // Levelling - if (!this.db) this.db = await database(); - const db = this.db; - const levelingTable = await getLevelingTable(db); - const settingsTable = await getSettingsTable(db); + const levelChannelId = get(guild.id, "levelling.channel"); + if (!levelChannelId) return; + if (!get(guild.id, "levelling.enabled")) return; + const expPerMessage = 2; const baseExpForNewLevel = 2 * 50; const difficultyMultiplier = 1.25; - const levelingEnabled = await settingsTable - ?.get(`${guild.id}.leveling.enabled`) - .then(enabled => !!enabled) - .catch(() => false); - - if (!levelingEnabled) return; - - const { exp, levels } = await levelingTable - ?.get(`${guild.id}.${target.id}`) - .catch(() => { return { exp: 0, levels: 0 } }); - - const { exp: expGlobal, levels: levelsGlobal } = await levelingTable - ?.get(`global.${target.id}`) - .then(data => { - if (!data) return { exp: 0, levels: 0 }; - return { exp: parseInt(data.exp), levels: parseInt(data.levels) }; - }) - .catch(() => { return { exp: 0, levels: 0 } }); - - const expUntilLevelup = Math.floor(baseExpForNewLevel * difficultyMultiplier * (levels + 1)); - const expUntilLevelupGlobal = Math.floor(baseExpForNewLevel * difficultyMultiplier * (levelsGlobal + 1)); - const expUntilNextLevelup = Math.floor(baseExpForNewLevel * difficultyMultiplier * (levels + 2)); - - const newLevelData = { levels: levels ?? 0, exp: (exp ?? 0) + expPerMessage }; - const newLevelDataGlobal = { levels: levelsGlobal ?? 0, exp: (expGlobal ?? 0) + expPerMessage }; + const [exp, level] = getLevel(guild.id, author.id); + const [globalExp, globalLevel] = getLevel("0", author.id); + const expUntilLevelup = Math.floor(baseExpForNewLevel * difficultyMultiplier * (level + 1)); + const newLevelData = { level: level ?? 0, exp: (exp ?? 0) + expPerMessage }; + const newLevelDataGlobal = { level: globalLevel ?? 0, exp: (globalExp ?? 0) + expPerMessage }; if (!(exp >= expUntilLevelup - 1)) { - await levelingTable.set(`global.${target.id}`, newLevelDataGlobal); - return await levelingTable.set(`${guild.id}.${target.id}`, newLevelDataGlobal); + setLevel(0, author.id, newLevelDataGlobal.level, newLevelDataGlobal.exp); + return setLevel(guild.id, author.id, newLevelDataGlobal.level, newLevelDataGlobal.exp); } else if (exp >= expUntilLevelup - 1) { let leftOverExp = exp - expUntilLevelup; if (leftOverExp < 0) leftOverExp = 0; - newLevelData.levels = levels + 1; + newLevelData.level = level + 1; newLevelData.exp = leftOverExp ?? 0; - - await levelingTable.set(`${guild.id}.${target.id}`, newLevelData); + setLevel(guild.id, author.id, newLevelData.level, newLevelData.exp); } - if (exp >= expUntilLevelupGlobal - 1) { + if (exp >= Math.floor(baseExpForNewLevel * difficultyMultiplier * (globalLevel + 1)) - 1) { let leftOverExpGlobal = exp - expUntilLevelup; if (leftOverExpGlobal < 0) leftOverExpGlobal = 0; - newLevelDataGlobal.levels = levels + 1; + newLevelDataGlobal.level = level + 1; newLevelDataGlobal.exp = leftOverExpGlobal + 1; - - await levelingTable.set(`global.${target.id}`, newLevelDataGlobal); + setLevel(0, author.id, newLevelDataGlobal.level, newLevelDataGlobal.exp); } - const levelChannelId = await settingsTable - ?.get(`${guild.id}.leveling.channel`) - .then(channelId => String(channelId)) - .catch(() => null); - - if (!levelChannelId) return; - const levelChannel = guild.channels.cache.get(levelChannelId) as TextChannel; - - const leveledEmbed = new EmbedBuilder() - .setAuthor({ name: target.displayName, iconURL: target.avatarURL() }) + const embed = new EmbedBuilder() + .setAuthor({ name: author.displayName, iconURL: author.avatarURL() || undefined }) .setTitle("⚡ • Level Up!") .setDescription([ - `**Congratulations <@${target.id}>**!`, - `You made it to **level ${levels + 1}**`, - `You need ${expUntilNextLevelup} exp to level up again.` + `**Congratulations, ${author.displayName}**!`, + `You made it to **level ${level + 1}**`, + `You need ${Math.floor(baseExpForNewLevel * difficultyMultiplier * (level + 2))} EXP to level up again.` ].join("\n")) - .setThumbnail(target.avatarURL()) + .setThumbnail(author.avatarURL()) .setTimestamp() .setColor(genColor(200)); - levelChannel.send({ embeds: [leveledEmbed], content: `<@${target.id}>` }); - - const levelRewards = await settingsTable - ?.get(`${guild.id}.leveling.rewards`) - .then(rewards => rewards as Reward[] ?? [] as Reward[]) - .catch(() => [] as Reward[]); - - const members = await guild.members.fetch(); - for (const { level, roleId } of levelRewards) { - const role = guild.roles.cache.get(roleId); - const targetRoles = members.get(target.id)?.roles; + (guild.channels.cache.get(`${levelChannelId}`) as TextChannel).send({ embeds: [embed], content: `<@${author.id}>` }); + for (const { level, roleID } of getLevelRewards(guild.id)) { + const role = guild.roles.cache.get(`${roleID}`); + const authorRoles = (await guild.members.fetch()).get(author.id)?.roles; - if (levels >= level) { - await targetRoles.add(role); + if (level >= level) { + await authorRoles?.add(role); continue; } - await targetRoles.remove(role); + await authorRoles?.remove(role); } } } diff --git a/src/handlers/commands.ts b/src/handlers/commands.ts index 20808a7..67e82f5 100644 --- a/src/handlers/commands.ts +++ b/src/handlers/commands.ts @@ -1,35 +1,33 @@ import { SlashCommandBuilder, SlashCommandSubcommandGroupBuilder, Guild, - type Client + type Client } from "discord.js"; import { pathToFileURL } from "url"; import { join } from "path"; import { readdirSync } from "fs"; -import { database, getSettingsTable } from "../utils/database.js"; -import { QuickDB } from "quick.db"; +import { getDisabledCommands } from "../utils/database/disabledCommands"; -const COMMANDS_PATH = join(process.cwd(), "src", "commands"); export default class Commands { client: Client; commands: any[] = []; - db: QuickDB; - constructor(client?: Client) { + constructor(client: Client) { this.client = client; } private async createSubCommand(name: string, ...disabledCommands: string[]): Promise { + const commandsPath = join(process.cwd(), "src", "commands"); const command = new SlashCommandBuilder() .setName(name.toLowerCase()) .setDescription("This command has no description."); - for (const subCommandFile of readdirSync(join(COMMANDS_PATH, name), { withFileTypes: true })) { + for (const subCommandFile of readdirSync(join(commandsPath, name), { withFileTypes: true })) { const subCommandName = subCommandFile.name.replaceAll(".ts", ""); if (disabledCommands?.find(command => command?.split("/")?.[0] == name && command?.split("/")?.[1] == subCommandName)) continue; if (subCommandFile.isFile()) { - const subCommand = await import(pathToFileURL(join(COMMANDS_PATH, name, subCommandFile.name)).toString()); + const subCommand = await import(pathToFileURL(join(commandsPath, name, subCommandFile.name)).toString()); command.addSubcommand(new subCommand.default().data); continue; } @@ -38,7 +36,7 @@ export default class Commands { .setName(subCommandName.toLowerCase()) .setDescription("This subcommand group has no description."); - const subCommandGroupFiles = readdirSync(join(COMMANDS_PATH, name, subCommandFile.name), { withFileTypes: true }); + const subCommandGroupFiles = readdirSync(join(commandsPath, name, subCommandFile.name), { withFileTypes: true }); for (const subCommandGroupFile of subCommandGroupFiles) { if (!subCommandGroupFile.isFile()) continue; if (disabledCommands?.find(command => @@ -48,7 +46,7 @@ export default class Commands { )) continue; const subCommand = await import( - pathToFileURL(join(COMMANDS_PATH, name, subCommandFile.name, subCommandGroupFile.name)).toString() + pathToFileURL(join(commandsPath, name, subCommandFile.name, subCommandGroupFile.name)).toString() ); subCommandGroup.addSubcommand(new subCommand.default().data); } @@ -59,20 +57,21 @@ export default class Commands { } async loadCommands(...disabledCommands: string[]) { + const commandsPath = join(process.cwd(), "src", "commands"); this.commands = []; - const commandFiles = readdirSync(COMMANDS_PATH, { withFileTypes: true }); + const commandFiles = readdirSync(commandsPath, { withFileTypes: true }); for (const commandFile of commandFiles) { const name = commandFile.name; if (disabledCommands?.includes(name.replaceAll(".ts", ""))) continue; if (commandFile.isFile()) { - const command = await import(pathToFileURL(join(COMMANDS_PATH, name)).toString()); + const command = await import(pathToFileURL(join(commandsPath, name)).toString()); this.commands.push(new command.default().data); continue; } - const subCommand = await this.createSubCommand(name, join(COMMANDS_PATH, name), ...disabledCommands); + const subCommand = await this.createSubCommand(name, join(commandsPath, name), ...disabledCommands); this.commands.push(subCommand); } } @@ -83,18 +82,12 @@ export default class Commands { } async registerCommands(): Promise { - const db = await database(); await this.loadCommands(); - const settingsTable = await getSettingsTable(db); const guilds = this.client.guilds.cache; console.log("Adding commands to guilds..."); for (const guildID of guilds.keys()) { - const disabledCommands = await settingsTable - ?.get(`${guildID}.disabledCommands`) - .then((disabledCommands: string[]) => disabledCommands as string[] ?? [] as string[]) - .catch(() => [] as string[]); - + const disabledCommands = getDisabledCommands(guildID); if (disabledCommands.length > 0) await this.loadCommands(...disabledCommands); await guilds.get(guildID)?.commands.set(this.commands); } diff --git a/src/handlers/events.ts b/src/handlers/events.ts index 3b36224..ff15b72 100644 --- a/src/handlers/events.ts +++ b/src/handlers/events.ts @@ -17,7 +17,7 @@ export default class Events { if (!eventFile.endsWith("ts")) continue; const event = await import(pathToFileURL(join(eventsPath, eventFile)).toString()); - const clientEvent = this.client.on(event.default.name, new event.default.event(this.client).run); + const clientEvent = client.on(event.default.name, new event.default.event(client).run); this.events.push({ name: event.default.name, event: clientEvent }); } diff --git a/src/utils/colorGen.ts b/src/utils/colorGen.ts index 0ebb93c..438c5ff 100644 --- a/src/utils/colorGen.ts +++ b/src/utils/colorGen.ts @@ -28,10 +28,10 @@ export function genColor(hue: number) { * @param b Blue. * @returns Color in HEX. */ -export function genRGBColor(r, g, b) { - r = r.toString(16); - g = g.toString(16); - b = b.toString(16); +export function genRGBColor(r: string | any[], g: string | any[], b: string | any[]) { + r = r.toString(); + g = g.toString(); + b = b.toString(); if (r.length === 1) r = `0${r}`; if (g.length === 1) g = `0${g}`; diff --git a/src/utils/database/disabledCommands.ts b/src/utils/database/disabledCommands.ts index 542ed83..a91edf2 100644 --- a/src/utils/database/disabledCommands.ts +++ b/src/utils/database/disabledCommands.ts @@ -5,31 +5,23 @@ const tableDefinition = { name: "disabledCommands", definition: { guild: "INTEGER", - command: "TEXT", - }, + command: "TEXT" + } } satisfies TableDefinition; const database = getDatabase(tableDefinition); +const getQuery = database.query("SELECT * FROM disabledCommands WHERE guild = $1;"); +const addQuery = database.query("INSERT INTO disabledCommands (guild, command) VALUES (?1, ?2);"); +const removeQuery = database.query("DELETE FROM disabledCommands WHERE guild = $1 AND command = $2"); -const getQuery = database.query( - "SELECT * FROM disabledCommands WHERE guild = $1;", -); export function getDisabledCommands(guildID: string) { - return ( - getQuery.all(guildID) as TypeOfDefinition[] - ).map((val) => val.command); + return (getQuery.all(guildID) as TypeOfDefinition[]).map(val => val.command); } -const addQuery = database.query( - "INSERT INTO disabledCommands (guild, command) VALUES (?1, ?2);", -); export function disableCommands(guildID: string, command: string) { addQuery.run(guildID, command); } -const removeQuery = database.query( - "DELETE FROM disabledCommands WHERE guild = $1 AND command = $2", -); export function enableCommands(guildID: string, command: string) { removeQuery.run(guildID, command); } diff --git a/src/utils/database/index.ts b/src/utils/database/index.ts index 65cefa9..f5240a7 100644 --- a/src/utils/database/index.ts +++ b/src/utils/database/index.ts @@ -1,14 +1,15 @@ import { Database } from "bun:sqlite"; import { TableDefinition } from "./types"; -// Get (or create) SQLite database +// Get (or create) an SQLite database const database = new Database("data.db", { create: true }); export function getDatabase(definition: TableDefinition) { - // Create table if not exist + // Create table if it doesn't exist const defStr = Object.entries(definition.definition) .map(([field, type]) => field.concat(" ", type)) .join(", "); + database.run(`CREATE TABLE IF NOT EXISTS ${definition.name} (${defStr});`); return database; } diff --git a/src/utils/database/levelBlockedChannels.ts b/src/utils/database/levelBlockedChannels.ts index 175e5e2..3eb610b 100644 --- a/src/utils/database/levelBlockedChannels.ts +++ b/src/utils/database/levelBlockedChannels.ts @@ -5,36 +5,28 @@ const tableDefinition = { name: "levelBlockedChannels", definition: { guild: "INTEGER", - channel: "INTEGER", - }, + channel: "INTEGER" + } } satisfies TableDefinition; const database = getDatabase(tableDefinition); +const getQuery = database.query("SELECT * FROM levelBlockedChannels WHERE guild = $1 AND channel = $2;"); +const listQuery = database.query("SELECT * FROM levelBlockedChannels WHERE guild = $1;"); +const addQuery = database.query("INSERT INTO levelBlockedChannels (guild, channel) VALUES (?1, ?2);"); +const removeQuery = database.query("DELETE FROM levelBlockedChannels WHERE guild = $1 AND channel = $2;"); -const getQuery = database.query( - "SELECT * FROM levelBlockedChannels WHERE guild = $1 AND channel = $2;", -); export function getBlockedChannels(guildID: string, channelID: string) { return getQuery.all(guildID, channelID).length == 0; } -const listQuery = database.query("SELECT * FROM levelBlockedChannels WHERE guild = $1;"); export function listBlockedChannels(guildID: string) { - return ( - listQuery.all(guildID) as TypeOfDefinition[] - ).map((val) => val.channel); + return (listQuery.all(guildID) as TypeOfDefinition[]).map(val => val.channel); } -const addQuery = database.query( - "INSERT INTO levelBlockedChannels (guild, channel) VALUES (?1, ?2);", -); export function blockChannel(guildID: string, channelID: string) { addQuery.run(guildID, channelID); } -const removeQuery = database.query( - "DELETE FROM levelBlockedChannels WHERE guild = $1 AND channel = $2;", -); export function unblockChannel(guildID: string, channelID: string) { removeQuery.run(guildID, channelID); } diff --git a/src/utils/database/levelRewards.ts b/src/utils/database/levelRewards.ts index fd96fd5..5588158 100644 --- a/src/utils/database/levelRewards.ts +++ b/src/utils/database/levelRewards.ts @@ -1,5 +1,4 @@ // TODO: Implement logic - import { getDatabase } from "."; import { TableDefinition, TypeOfDefinition } from "./types"; @@ -7,48 +6,29 @@ const tableDefinition = { name: "levelRewards", definition: { guild: "INTEGER", - role: "INTEGER", - level: "INTEGER", - }, + roleID: "INTEGER", + level: "INTEGER" + } } satisfies TableDefinition; const database = getDatabase(tableDefinition); - const getQuery = database.query("SELECT * FROM levelRewards WHERE guild = $1;"); +const addQuery = database.query("INSERT INTO levelRewards (guild, role, level) VALUES (?1, ?2, ?3);"); +const updateQuery = database.query("UPDATE levelRewards SET level = $3 WHERE guild = $1 AND role = $2"); +const removeQuery = database.query("DELETE FROM levelRewards WHERE guild = $1 AND role = $2"); + export function get(guildID: string) { return getQuery.all(guildID) as TypeOfDefinition[]; } -const addQuery = database.query( - "INSERT INTO levelRewards (guild, role, level) VALUES (?1, ?2, ?3);", -); -export function addReward( - guildID: string, - role: number | string, - level: number, -) { - return addQuery.all(guildID, level, role) as TypeOfDefinition< - typeof tableDefinition - >[]; +export function addReward(guildID: string, role: number | string, level: number) { + return addQuery.all(guildID, level, role) as TypeOfDefinition[]; } -/** - * Update the level requirement for a reward - */ -const updateQuery = database.query( - "UPDATE levelRewards SET level = $3 WHERE guild = $1 AND role = $2", -); -export function updateReward( - guildID: string, - role: number | string, - level: number, -) { +export function updateReward(guildID: string, role: number | string, level: number) { updateQuery.run(guildID, role, level); } -const removeQuery = database.query( - "DELETE FROM levelRewards WHERE guild = $1 AND role = $2", -); export function removeReward(guildID: number | string, role: number | string) { removeQuery.run(guildID, role); } diff --git a/src/utils/database/leveling.ts b/src/utils/database/levelling.ts similarity index 65% rename from src/utils/database/leveling.ts rename to src/utils/database/levelling.ts index 5635ef6..24449e8 100644 --- a/src/utils/database/leveling.ts +++ b/src/utils/database/levelling.ts @@ -2,36 +2,27 @@ import { getDatabase } from "."; import { TableDefinition, TypeOfDefinition } from "./types"; const tableDefinition = { - name: "leveling", + name: "levelling", definition: { guild: "INTEGER", user: "INTEGER", level: "INTEGER", - exp: "INTEGER", - }, + exp: "INTEGER" + } } satisfies TableDefinition; const database = getDatabase(tableDefinition); +const getQuery = database.query("SELECT * FROM levelling WHERE guild = $1 AND user = $2;"); +const setQuery = database.query("UPDATE leveling SET level = $3, exp = $4 WHERE guild = $1 AND user = $2;"); +const insertQuery = database.query("INSERT INTO levelling (guild, user, level, exp) VALUES (?1, ?2, ?3, ?4);"); -const getQuery = database.query( - "SELECT * FROM leveling WHERE guild = $1 AND user = $2;", -); export function getLevel(guildID: string, userID: string): [number, number] { - const res = getQuery.all(guildID, userID) as TypeOfDefinition< - typeof tableDefinition - >[]; + const res = getQuery.all(guildID, userID) as TypeOfDefinition[]; if (res.length == 0) return [0, 0]; return [res[0].level, res[0].exp]; } -const setQuery = database.query("UPDATE leveling SET level = $3, exp = $4 WHERE guild = $1 AND user = $2;"); -const insertQuery = database.query("INSERT INTO leveling (guild, user, level, exp) VALUES (?1, ?2, ?3, ?4);"); -export function setLevel( - guildID: string | number, - userID: string, - level: number, - exp: number, -) { +export function setLevel(guildID: string | number, userID: string, level: number, exp: number) { getQuery.all(guildID, userID).length == 0 ? insertQuery.run(guildID, userID, level, exp) : setQuery.run(guildID, userID, level, exp); diff --git a/src/utils/database/moderation.ts b/src/utils/database/moderation.ts index 2c5bbe6..4ff7084 100644 --- a/src/utils/database/moderation.ts +++ b/src/utils/database/moderation.ts @@ -11,58 +11,38 @@ const definition = { reason: "TEXT", public: "BOOL", id: "TEXT", - timestamp: "TIMESTAMP", - }, + timestamp: "TIMESTAMP" + } } satisfies TableDefinition; type modType = "MUTE" | "WARN" | "KICK" | "BAN"; - const database = getDatabase(definition); +const addQuery = database.query("INSERT INTO moderation (guild, user, type, moderator, reason, public, id, timestamp) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8);"); +const listUserQuery = database.query("SELECT * FROM moderation WHERE guild = $1 AND user = $2 AND type = $3;"); +const listModQuery = database.query("SELECT * FROM moderation WHERE guild = $1 AND moderator = $2;"); +const removeQuery = database.query("DELETE FROM moderation WHERE guild = $1 AND id = $2"); -const addQuery = database.query( - "INSERT INTO moderation (guild, user, type, moderator, reason, public, id, timestamp) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8);", -); export function addModeration( guildID: string | number, userID: string, type: modType, moderator: string, reason = "", - pub = false, + pub = false ) { const id = crypto.randomUUID(); addQuery.run(guildID, userID, type, moderator, reason, pub, id, Date.now()); return id; } -const listUserQuery = database.query( - "SELECT * FROM moderation WHERE guild = $1 AND user = $2 AND type = $3;", -); -export function listUserModeration( - guildID: number | string, - userID: number | string, - type: modType, -) { - return listUserQuery.all(guildID, userID, type) as TypeOfDefinition< - typeof definition - >[]; +export function listUserModeration(guildID: number | string, userID: number | string, type: modType) { + return listUserQuery.all(guildID, userID, type) as TypeOfDefinition[]; } -const listModQuery = database.query( - "SELECT * FROM moderation WHERE guild = $1 AND moderator = $2;", -); -export function listModeratorLog( - guildID: number | string, - moderator: number | string, -) { - return listModQuery.all(guildID, moderator) as TypeOfDefinition< - typeof definition - >[]; +export function listModeratorLog(guildID: number | string, moderator: number | string) { + return listModQuery.all(guildID, moderator) as TypeOfDefinition[]; } -const removeQuery = database.query( - "DELETE FROM moderation WHERE guild = $1 AND id = $2", -); export function removeModeration(guildID: string | number, id: string) { removeQuery.run(guildID, id); } diff --git a/src/utils/database/news.ts b/src/utils/database/news.ts index 31bc66a..3288606 100644 --- a/src/utils/database/news.ts +++ b/src/utils/database/news.ts @@ -14,13 +14,17 @@ const definition = { updatedAt: "TIMESTAMP", messageID: "INTEGER", categoryID: "TEXT", - id: "TEXT", - }, + id: "TEXT" + } } satisfies TableDefinition; const database = getDatabase(definition); - const addQuery = database.query(""); +const listAllQuery = database.query("SELECT * FROM news WHERE guild = $1;"); +const listCategoryQuery = database.query("SELECT * FROM news WHERE guild = $1 AND categoryID = $2;"); +const updateQuery = database.query("UPDATE news SET title = $2, body = $3, imageURL = $4 WHERE id = $1"); +const deleteQuery = database.query("DELETE FROM news WHERE id = $1"); + export function addNews( guildID: number | string, title: string, @@ -29,7 +33,7 @@ export function addNews( author: string, authorPFP: string, messageID: string | number, - categoryID: string, + categoryID: string ) { addQuery.run( guildID, @@ -42,37 +46,22 @@ export function addNews( 0, messageID, categoryID, - crypto.randomUUID(), + crypto.randomUUID() ); } -const listAllQuery = database.query("SELECT * FROM news WHERE guild = $1;"); export function listAllNews(guildID: string | number) { return listAllQuery.all(guildID) as TypeOfDefinition[]; } -const listCategoryQuery = database.query( - "SELECT * FROM news WHERE guild = $1 AND categoryID = $2;", -); export function listCategoryNews(guildID: string | number, categoryID: string) { - return listCategoryQuery.all(guildID, guildID) as TypeOfDefinition< - typeof definition - >[]; + return listCategoryQuery.all(guildID, categoryID) as TypeOfDefinition[]; } -const updateQuery = database.query( - "UPDATE news SET title = $2, body = $3, imageURL = $4 WHERE id = $1", -); -export function updateNews( - id: string, - title: string, - body: string, - imageURL: string, -) { +export function updateNews(id: string, title: string, body: string, imageURL: string) { updateQuery.run(id, title, body, imageURL); } -const deleteQuery = database.query("DELETE FROM news WHERE id = $1"); export function deleteNews(id: string) { deleteQuery.run(id); } diff --git a/src/utils/database/newsCategories.ts b/src/utils/database/newsCategories.ts index a40b023..9933283 100644 --- a/src/utils/database/newsCategories.ts +++ b/src/utils/database/newsCategories.ts @@ -1,4 +1,3 @@ -// TODO: import { getDatabase } from "."; import { TableDefinition, TypeOfDefinition } from "./types"; @@ -9,43 +8,29 @@ const definition = { name: "TEXT", role: "INTEGER", channel: "INTEGER", - id: "TEXT", - }, + id: "TEXT" + } } satisfies TableDefinition; const database = getDatabase(definition); +const createQuery = database.query("INSERT INTO newsCategories (guild, name, role, channel, id) VALUES (?1, ?2, ?3, ?4, ?5);"); +const listQuery = database.query("SELECT * FROM newsCategories WHERE guild = $1;"); +const findNameQuery = database.query("SELECT * FROM newsCategories WHERE guild = $1 AND name = $2;"); +const updateQuery = database.query("UPDATE newsCategories SET name = $3 WHERE guild = $1 AND name = $2"); +const deleteQuery = database.query("DELETE FROM newsCategories WHERE guild = $1 AND name = $2"); -const createQuery = database.query( - "INSERT INTO newsCategories (guild, name, role, channel, id) VALUES (?1, ?2, ?3, ?4, ?5);", -); -export function createCategory( - guildID: number | string, - name: string, - role: number | string, - channel: number | string, -) { +export function createCategory(guildID: number | string, name: string, role: number | string, channel: number | string) { createQuery.run(guildID, name, role, channel, crypto.randomUUID()); } -const listQuery = database.query( - "SELECT * FROM newsCategories WHERE guild = $1;", -); export function listCategories(guildID: number | string) { return listQuery.all(guildID) as TypeOfDefinition[]; } -const findNameQuery = database.query( - "SELECT * FROM newsCategories WHERE guild = $1 AND name = $2;", -); export function findWithName(guildID: number | string, name: string) { - return findNameQuery.get(guildID, name) as TypeOfDefinition< - typeof definition - >[]; + return findNameQuery.get(guildID, name) as TypeOfDefinition[]; } -const updateQuery = database.query( - "UPDATE newsCategories SET name = $3 WHERE guild = $1 AND name = $2", -); export function updateCategory( guildID: number | string, name: string, @@ -54,9 +39,6 @@ export function updateCategory( updateQuery.run(guildID, name, newName); } -const deleteQuery = database.query( - "DELETE FROM newsCategories WHERE guild = $1 AND name = $2", -); export function deleteCategory(guildID: number | string, name: string) { deleteQuery.run(guildID, name); } diff --git a/src/utils/database/settings.ts b/src/utils/database/settings.ts index c0c82e4..e381d3f 100644 --- a/src/utils/database/settings.ts +++ b/src/utils/database/settings.ts @@ -1,61 +1,40 @@ // TODO: Add more settings - import { getDatabase } from "."; import { FieldData, SqlType, TableDefinition, TypeOfDefinition } from "./types"; -// Define table structure const tableDefinition = { name: "settings", definition: { guild: "INTEGER", key: "TEXT", - value: "TEXT", - }, + value: "TEXT" + } } satisfies TableDefinition; -// Define type of settings const settingsDefinition = { - "leveling.enabled": "BOOL", - "leveling.channel": "INTEGER", - "leveling.persistence": "BOOL", + "levelling.enabled": "BOOL", + "levelling.channel": "INTEGER", + "levelling.persistence": "BOOL", "log.channel": "INTEGER", "serverboard.inviteLink": "TEXT", "serverboard.shown": "BOOL", } satisfies Record; -export const settingKeys = Object.keys( - settingsDefinition, -) as (keyof typeof settingsDefinition)[]; - +export const settingKeys = Object.keys(settingsDefinition) as (keyof typeof settingsDefinition)[]; const database = getDatabase(tableDefinition); +const getQuery = database.query("SELECT * FROM settings WHERE guild = $1 AND key = $2;"); +const setQuery = database.query("UPDATE settings SET value = $3 WHERE guild = $1 AND key = $2;"); -const getQuery = database.query( - "SELECT * FROM settings WHERE guild = $1 AND key = $2;", -); -export function get( - guild: string, - key: K, -): TypeOfKey | null { - let res = getQuery.all(guild, key) as TypeOfDefinition< - typeof tableDefinition - >[]; +export function get(guild: string, key: K): TypeOfKey | null { + let res = getQuery.all(guild, key) as TypeOfDefinition[]; if (res.length == 0) return null; - if (settingsDefinition[key] == "TEXT") return res[0].value; + if (settingsDefinition[key] == "TEXT") return res[0].value as TypeOfKey; return JSON.parse(res[0].value); } -let setQuery = database.query( - "UPDATE settings SET value = $3 WHERE guild = $1 AND key = $2;", -); -export function set( - guild: string, - key: K, - value: TypeOfKey, -) { +export function set(guild: string, key: K, value: TypeOfKey) { setQuery.run(guild, key, JSON.stringify(value)); } // Utility type -type TypeOfKey = SqlType< - (typeof settingsDefinition)[T] ->; +type TypeOfKey = SqlType<(typeof settingsDefinition)[T]>; diff --git a/src/utils/multiReact.ts b/src/utils/multiReact.ts index b91c968..c2f5027 100644 --- a/src/utils/multiReact.ts +++ b/src/utils/multiReact.ts @@ -1,6 +1,6 @@ import type { Message } from "discord.js"; -export async function multiReact(message: Message, ...reactions) { +export async function multiReact(message: Message, ...reactions: string[]) { for (const i of reactions) { if (typeof i === "object") { await message.react(i); diff --git a/src/utils/sendChannelNews.ts b/src/utils/sendChannelNews.ts index 92682d2..d738d40 100644 --- a/src/utils/sendChannelNews.ts +++ b/src/utils/sendChannelNews.ts @@ -2,8 +2,7 @@ import { EmbedBuilder, Guild, Role, TextChannel } from "discord.js"; -import { database, getNewsTable } from "./database.js"; -import { genColor } from "./colorGen.js"; +import { genColor } from "./colorGen"; export type News = { title: string @@ -21,7 +20,7 @@ export async function sendChannelNews(guild: Guild, news: News, id: string) { const newsTable = await getNewsTable(db); const subscribedChannel = await newsTable.get(`${guild.id}.channel`).then( - channel => channel as { channelId: string | null, roleId: string | null } + (channel: { channelId: string | null; roleId: string | null; }) => channel as { channelId: string | null, roleId: string | null } ).catch(() => { return { channelId: null as string | null, @@ -35,7 +34,7 @@ export async function sendChannelNews(guild: Guild, news: News, id: string) { if (!channelToSend) return; const role = subscribedChannel.roleId; - let roleToSend: Role | null; + let roleToSend: Role | undefined; if (role) roleToSend = guild.roles.cache.get(role); const newsEmbed = new EmbedBuilder() @@ -47,7 +46,7 @@ export async function sendChannelNews(guild: Guild, news: News, id: string) { .setFooter({ text: `Latest news from ${guild.name}` }) .setColor(genColor(200)); - const message = await channelToSend.send({ embeds: [newsEmbed], content: roleToSend ? `<@&${roleToSend.id}>` : null }); + const message = await channelToSend.send({ embeds: [newsEmbed], content: roleToSend ? `<@&${roleToSend.id}>` : undefined }); await newsTable.set(`${guild.id}.news.${id}.messageId`, message.id); return; } diff --git a/src/utils/sendSubscribedNews.ts b/src/utils/sendSubscribedNews.ts index 6dbca4e..be08cdc 100644 --- a/src/utils/sendSubscribedNews.ts +++ b/src/utils/sendSubscribedNews.ts @@ -1,6 +1,5 @@ import { DMChannel, EmbedBuilder, Guild } from "discord.js"; -import { database, getNewsTable } from "./database.js"; -import { genColor } from "./colorGen.js"; +import { genColor } from "./colorGen"; export type News = { title: string @@ -14,16 +13,13 @@ export type News = { } export async function sendSubscribedNews(guild: Guild, news: News) { - const db = await database(); - const newsTable = await getNewsTable(db); + const subscriptions = await (await getNewsTable(await database())) + .get(`${guild.id}.subscriptions`) + .then((subscriptions: string[]) => subscriptions as string[] ?? [] as string[]); - const subscriptions = await newsTable.get(`${guild.id}.subscriptions`).then( - subscriptions => subscriptions as string[] ?? [] as string[] - ).catch(() => [] as string[]); - const members = await guild.members.fetch(); - const subscribed = members.filter((member) => subscriptions.includes(member.id)); - const memberDMs = (await Promise.all(subscribed.map((member) => member.createDM().catch(() => null)))) as DMChannel[] | null;; - const memberDMsOpen = memberDMs.filter((dm) => dm !== null); + const subscribed = (await guild.members.fetch()).filter(member => subscriptions.includes(member.id)); + const memberDMs = (await Promise.all(subscribed.map(member => member.createDM().catch(() => null)))) as DMChannel[] | null; + const memberDMsOpen = memberDMs?.filter(dm => dm !== null); const newsEmbed = new EmbedBuilder() .setAuthor({ name: news.author, iconURL: news.authorPfp ?? null }) @@ -34,6 +30,6 @@ export async function sendSubscribedNews(guild: Guild, news: News) { .setFooter({ text: `Latest news from ${guild.name}` }) .setColor(genColor(200)); - await Promise.all(memberDMsOpen.map((dm) => dm.send({ embeds: [newsEmbed] }).catch(() => null))); + await Promise.all(memberDMsOpen?.map(dm => dm.send({ embeds: [newsEmbed] }).catch(() => null))); return; } diff --git a/tsconfig.json b/tsconfig.json index fdb1bff..1a7a764 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,20 +1,20 @@ { "compilerOptions": { - "target": "ESNext", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "noImplicitAny": false, - "removeComments": true, - "preserveConstEnums": true, - "forceConsistentCasingInFileNames": true, - "esModuleInterop": true, + "lib": ["ESNext"], + "module": "esnext", + "target": "esnext", + "moduleResolution": "Node", + "moduleDetection": "force", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "noEmit": true, + "composite": true, + "strict": true, + "downlevelIteration": true, "skipLibCheck": true, "allowSyntheticDefaultImports": true, - "noFallthroughCasesInSwitch": true, - "resolveJsonModule": true, - "isolatedModules": true, - "strict": false, + "forceConsistentCasingInFileNames": true, + "allowJs": true, "types": ["bun-types"] - }, - "exclude": ["node_modules"] -} + } +} \ No newline at end of file From f55f17313cbe5a30328412767eea9762fd20c8e5 Mon Sep 17 00:00:00 2001 From: Serge Date: Sun, 14 Jan 2024 17:44:43 +0600 Subject: [PATCH 007/127] some more changes --- .gitignore | 1 + src/commands/about.ts | 2 +- src/commands/news.ts | 48 +++++++-------- src/commands/serverboard.ts | 12 ++-- src/commands/user/info.ts | 16 +++-- src/commands/user/level.ts | 95 +++++++++--------------------- src/events/easterEggs/Bread.ts | 8 +-- src/events/easterEggs/Fan.ts | 4 +- src/events/easterEggs/Fireship.ts | 1 - src/events/easterEggs/Honk.ts | 2 +- src/events/easterEggs/WhoPinged.ts | 4 +- src/events/guildCreate.ts | 12 ++-- src/events/messageCreate.ts | 51 ++++++---------- src/utils/embeds/errorEmbed.ts | 2 +- src/utils/embeds/serverEmbed.ts | 14 ++--- src/utils/sendChannelNews.ts | 4 +- src/utils/sendSubscribedNews.ts | 5 +- 17 files changed, 105 insertions(+), 176 deletions(-) diff --git a/.gitignore b/.gitignore index 5a00c13..023011d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules/ .env .DS_Store +data.db diff --git a/src/commands/about.ts b/src/commands/about.ts index 6e86855..76240e6 100644 --- a/src/commands/about.ts +++ b/src/commands/about.ts @@ -35,7 +35,7 @@ export default class About { "**Head developer**: Goos", "**Developers**: Golem64, Pigpot, ThatBOI", "**Designers**: ArtyH, Optix, proJM, Slider_on_the_black", - "**Translators**: Dimkauzh, Golem64, Optix, Sungi, SaFire, ThatBOI", + "**Translators**: Dimkauzh, Golem64, Optix, SaFire, ThatBOI", "And **YOU**, for using Nebula." ].join("\n") }, diff --git a/src/commands/news.ts b/src/commands/news.ts index be75cd7..8246dc3 100644 --- a/src/commands/news.ts +++ b/src/commands/news.ts @@ -3,15 +3,12 @@ import { ButtonBuilder, ButtonStyle, type ChatInputCommandInteraction } from "discord.js"; import { genColor } from "../utils/colorGen"; -import { getNewsTable } from "../utils/database.js"; -import { QuickDB } from "quick.db"; +import { listAllNews } from "../utils/database/news"; export default class News { data: SlashCommandSubcommandBuilder; - db: QuickDB; - constructor(db?: QuickDB) { - this.db = db; + constructor() { this.data = new SlashCommandSubcommandBuilder() .setName("news") .setDescription("The news of Nebula.") @@ -23,17 +20,13 @@ export default class News { async run(interaction: ChatInputCommandInteraction) { let page = interaction.options.getNumber("page") ?? 1; - const news = await (await getNewsTable(this.db)) - .get(`903852579837059113.news`) // News of the Nebula server - .then(news => news as any[] ?? []) - .catch(() => []); + const newsSorted = (Object.values(listAllNews("903852579837059113")) as any[])?.sort((a, b) => b.createdAt - a.createdAt); - const newsSorted = (Object.values(news) as any[])?.sort((a, b) => b.createdAt - a.createdAt); if (page > newsSorted.length) page = newsSorted.length; if (page < 1) page = 1; let currentNews = newsSorted[page - 1]; - let newsEmbed = new EmbedBuilder() + let embed = new EmbedBuilder() .setAuthor({ name: currentNews.author, iconURL: currentNews.authorPfp ?? null }) .setTitle(currentNews.title) .setDescription(currentNews.body) @@ -53,25 +46,24 @@ export default class News { .setStyle(ButtonStyle.Primary) ); - await interaction.followUp({ embeds: [newsEmbed], components: [row] }); + await interaction.followUp({ embeds: [embed], components: [row] }); interaction.channel - .createMessageComponentCollector({ filter: i => i.user.id === interaction.user.id, time: 60000 }) - .on("collect", async i => { - if (!i.isButton()) return; + ?.createMessageComponentCollector({ filter: (i: { user: { id: string; }; }) => i.user.id === interaction.user.id, time: 60000 }) + .on("collect", async (i: { isButton: () => any; customId: string; deferUpdate: () => any; }) => { + if (!i.isButton()) return; + if (i.customId === "left") { + page--; + if (page < 1) page = newsSorted.length; + } else if (i.customId === "right") { + page++; + if (page > newsSorted.length) page = 1; + } - if (i.customId === "left") { - page--; - if (page < 1) page = newsSorted.length; - } else if (i.customId === "right") { - page++; - if (page > newsSorted.length) page = 1; - } + currentNews = currentNews; + embed = embed; - currentNews = currentNews; - newsEmbed = newsEmbed; - - await interaction.editReply({ embeds: [newsEmbed], components: [row] }); - await i.deferUpdate(); - }); + await interaction.editReply({ embeds: [embed], components: [row] }); + await i.deferUpdate(); + }); } } diff --git a/src/commands/serverboard.ts b/src/commands/serverboard.ts index 15061e6..c1c4f9d 100644 --- a/src/commands/serverboard.ts +++ b/src/commands/serverboard.ts @@ -2,8 +2,8 @@ import { SlashCommandSubcommandBuilder, ButtonBuilder, ActionRowBuilder, ButtonStyle, ButtonInteraction, type ChatInputCommandInteraction } from "discord.js"; -import { quickSort } from "../utils/quickSort.js"; -import { serverEmbed } from "../utils/embeds/serverEmbed.js"; +import { quickSort } from "../utils/quickSort"; +import { serverEmbed } from "../utils/embeds/serverEmbed"; import { database, getNewsTable, getServerboardTable } from "../utils/database.js"; import { QuickDB } from "quick.db"; @@ -49,7 +49,7 @@ export default class Serverboard { let guild = guildsSorted[page]; let subs = await (await getNewsTable(this.db)) ?.get(`${guild.id}.subscriptions`) - .then(subs => subs?.length > 0 ? subs as string[] : [] as string[]) + .then((subs: string | any[]) => subs?.length > 0 ? subs as string[] : [] as string[]) .catch(() => [] as string[]); let embed = await serverEmbed({ @@ -69,7 +69,7 @@ export default class Serverboard { await interaction.followUp({ embeds: [embed], components: [row] }); interaction.channel - .createMessageComponentCollector({ filter: i => i.user.id === interaction.user.id, time: 60000 }) + ?.createMessageComponentCollector({ filter: i => i.user.id === interaction.user.id, time: 60000 }) .on("collect", async (interaction: ButtonInteraction) => { let subs; @@ -78,12 +78,12 @@ export default class Serverboard { case "left": page--; if (page < 0) page = pages - 1; - subs = subscriptions.filter(sub => (sub.value as string[] ?? [] as string[]).includes(guild.id)); + subs = subscriptions.filter((sub: { value: string[]; }) => (sub.value as string[] ?? [] as string[]).includes(guild.id)); break; case "right": page++; if (page >= pages) page = 0; - subs = subscriptions.filter(sub => (sub.value as string[]).includes(guild.id)); + subs = subscriptions.filter((sub: { value: string[]; }) => (sub.value as string[]).includes(guild.id)); break; } diff --git a/src/commands/user/info.ts b/src/commands/user/info.ts index 291e323..a47733f 100644 --- a/src/commands/user/info.ts +++ b/src/commands/user/info.ts @@ -3,13 +3,11 @@ import { type ChatInputCommandInteraction } from "discord.js"; import { genColor, genRGBColor } from "../../utils/colorGen.js"; -import { QuickDB } from "quick.db"; import Vibrant from "node-vibrant"; import sharp from "sharp"; export default class UserInfo { data: SlashCommandSubcommandBuilder; - db: QuickDB; constructor() { this.data = new SlashCommandSubcommandBuilder() @@ -23,18 +21,18 @@ export default class UserInfo { async run(interaction: ChatInputCommandInteraction) { const user = interaction.options.getUser("user"); - const id = user ? user.id : interaction.member.user.id; - const selectedMember = interaction.guild.members.cache.filter(member => member.user.id === id).map(user => user)[0]; - const selectedUser = selectedMember.user; + const id = user ? user.id : interaction.member?.user.id; + const selectedMember = interaction.guild?.members.cache.filter(member => member.user.id === id).map(user => user)[0]; + const selectedUser = selectedMember?.user; let embed = new EmbedBuilder() .setAuthor({ - name: `• ${selectedMember.nickname == null ? selectedUser.username : selectedMember.nickname}${selectedUser.discriminator == "0" ? "" : `#${selectedUser.discriminator}`}`, - iconURL: selectedMember.displayAvatarURL() + name: `• ${selectedMember?.nickname == null ? selectedUser?.username : selectedMember.nickname}${selectedUser?.discriminator == "0" ? "" : `#${selectedUser?.discriminator}`}`, + iconURL: selectedMember?.displayAvatarURL() }) .setFields( { - name: selectedUser.bot === false ? "👤 • User info" : "🤖 • Bot info", + name: selectedUser?.bot === false ? "👤 • User info" : "🤖 • Bot info", value: [ `**Username**: ${selectedUser.username}`, `**Display name**: ${selectedUser.displayName === selectedUser.username ? "*None*" : selectedUser.displayName}`, @@ -43,7 +41,7 @@ export default class UserInfo { }, { name: "👥 • Member info", - value: `**Joined on** `, + value: `**Joined on** `, } ) .setFooter({ text: `User ID: ${selectedMember.id}` }) diff --git a/src/commands/user/level.ts b/src/commands/user/level.ts index da9a301..03aae7b 100644 --- a/src/commands/user/level.ts +++ b/src/commands/user/level.ts @@ -1,16 +1,13 @@ import { SlashCommandSubcommandBuilder, EmbedBuilder, type ChatInputCommandInteraction } from "discord.js"; -import { genColor } from "../../utils/colorGen.js"; -import { getLevelingTable, getSettingsTable } from "../../utils/database.js"; -import { errorEmbed } from "../../utils/embeds/errorEmbed.js"; -import { QuickDB } from "quick.db"; -import { Reward } from "../settings/leveling/rewards.js"; +import { genColor } from "../../utils/colorGen"; +import { errorEmbed } from "../../utils/embeds/errorEmbed"; +import { get as getLevelRewards } from "../../utils/database/levelRewards"; +import { get } from "../../utils/database/settings"; +import { getLevel, setLevel } from "../../utils/database/levelling"; export default class Level { data: SlashCommandSubcommandBuilder; - db: QuickDB; - - constructor(db?: QuickDB) { - this.db = db; + constructor() { this.data = new SlashCommandSubcommandBuilder() .setName("level") .setDescription("Shows your (or another user's) level.") @@ -21,89 +18,55 @@ export default class Level { } async run(interaction: ChatInputCommandInteraction) { - const db = this.db; - const settingsTable = await getSettingsTable(db); - const levelingTable = await getLevelingTable(db); - - const user = interaction.options.getUser("user"); - const member = interaction.member; const guild = interaction.guild; - - const id = user ? user.id : member.user.id; - const selectedMember = guild.members.cache.filter(member => member.user.id === id).map(user => user)[0]; - const avatarURL = selectedMember.displayAvatarURL(); - - const levelEnabled = await settingsTable - ?.get(`${guild.id}.leveling.enabled`) - .then(data => { - if (!data) return false; - return data; - }) - .catch(() => false); - - if (!levelEnabled) return await interaction.followUp({ + if (!get(`${guild?.id}`, "levelling.enabled")) return await interaction.followUp({ embeds: [errorEmbed("Leveling is disabled for this server.")] }); - const { exp, levels } = await levelingTable - ?.get(`${guild.id}.${selectedMember.id}`) - .then(data => { - if (!data) return { exp: 0, level: 0 }; - return { exp: parseInt(data.exp), levels: parseInt(data.levels) }; - }) - .catch(() => { return { exp: 0, levels: 0 } }); - - const formattedExp = exp?.toLocaleString("en-US"); - - if (!exp && !levels) await levelingTable.set(`${guild.id}.${selectedMember.id}`, { levels: 0, exp: 0 }); - + const user = interaction.options.getUser("user"); + const id = user ? user.id : interaction.member?.user.id; + const selectedMember = guild?.members.cache.filter(member => member.user.id === id).map(user => user)[0]; + const [guildExp, guildLevel] = getLevel(`${guild?.id}`, `${selectedMember?.id}`); + if (!guildExp && !guildLevel) setLevel(`${guild?.id}`, `${selectedMember?.id}`, 0, 0); + + const avatarURL = selectedMember?.displayAvatarURL(); + const formattedExp = guildExp?.toLocaleString("en-US"); + const formattedExpUntilLevelup = Math.floor(100 * 1.25 * ((guildLevel ?? 0) + 1))?.toLocaleString("en-US"); let rewards = []; - let nextReward = null; - const levelRewards = await settingsTable - ?.get(`${interaction.guild.id}.leveling.rewards`) - .then(data => { - if (!data) return [] as Reward[] ?? [] as Reward[]; - return data as Reward[] ?? [] as Reward[]; - }) - .catch(() => [] as Reward[]); - - for (const { roleId, level } of levelRewards) { - const role = await interaction.guild.roles.fetch(roleId).catch(() => {}); - const reward = { roleId, level }; + let nextReward; - if (levels < level) { + for (const { roleID, level } of getLevelRewards(`${guild?.id}`)) { + if (guildLevel < level) { if (nextReward) break; - nextReward = reward; + nextReward = { roleID, level }; break; } - rewards.push(role); + rewards.push(await guild?.roles.fetch(roleID)?.catch(() => {})); } - const expUntilLevelup = Math.floor((2 * 50) * 1.25 * ((levels ?? 0) + 1)); - const formattedExpUntilLevelup = expUntilLevelup?.toLocaleString("en-US"); - const levelUpEmbed = new EmbedBuilder() - .setAuthor({ name: `• ${selectedMember.user.username}`, iconURL: avatarURL }) + const embed = new EmbedBuilder() + .setAuthor({ name: `• ${selectedMember?.user.username}`, iconURL: avatarURL }) .setFields( { - name: `⚡ • Level ${levels ?? 0}`, + name: `⚡ • Level ${guildLevel ?? 0}`, value: [ `**Exp**: ${formattedExp ?? 0}/${formattedExpUntilLevelup} until level up`, - `**Next Level**: ${(levels ?? 0) + 1}` + `**Next Level**: ${(guildLevel ?? 0) + 1}` ].join("\n") }, { name: `🎁 • ${rewards.length} Rewards`, value: [ - `${rewards.length > 0 ? rewards.map(reward => `<@&${reward.id}>`).join(" ") : "No rewards unlocked"}`, - nextReward ? `**Upcoming reward**: <@&${nextReward.roleId}>` : "**Upcoming reward**: *Cricket, cricket, cricket* - Looks like you claimed everything!" + `${rewards.length > 0 ? rewards.map(reward => `<@&${reward?.id}>`).join(" ") : "No rewards unlocked"}`, + nextReward ? `**Upcoming reward**: <@&${nextReward.roleID}>` : "**Upcoming reward**: *Cricket, cricket, cricket* - Looks like you claimed everything!" ].join("\n") } ) - .setThumbnail(avatarURL) + .setThumbnail(avatarURL || null) .setTimestamp() .setColor(genColor(200)); - await interaction.followUp({ embeds: [levelUpEmbed] }); + await interaction.followUp({ embeds: [embed] }); } } diff --git a/src/events/easterEggs/Bread.ts b/src/events/easterEggs/Bread.ts index 5d6a36c..3c23a30 100644 --- a/src/events/easterEggs/Bread.ts +++ b/src/events/easterEggs/Bread.ts @@ -1,18 +1,16 @@ import type { Message } from "discord.js"; -import { multiReact } from "../../utils/multiReact.js"; +import { multiReact } from "../../utils/multiReact"; export default class Bread { async run(message: Message) { - const gif = "https://tenor.com/bOMAb.gif"; - const randomizedChance = Math.round(Math.random() * 100); const breadSplit = message.content.toLowerCase().split("bread"); - if (breadSplit[1] == null) return; + if (breadSplit[1] == null) return; if ( ((breadSplit[0].endsWith(" ") || breadSplit[0].endsWith("")) && breadSplit[1].startsWith(" ")) || message.content.toLowerCase() === "bread" ) { - if (randomizedChance <= 0.25) message.channel.send(gif); + if (Math.round(Math.random() * 100) <= 0.25) message.channel.send("https://tenor.com/bOMAb.gif"); else await multiReact(message, "🍞🇧🇷🇪🇦🇩👍"); } } diff --git a/src/events/easterEggs/Fan.ts b/src/events/easterEggs/Fan.ts index 80169b9..1e46211 100644 --- a/src/events/easterEggs/Fan.ts +++ b/src/events/easterEggs/Fan.ts @@ -1,5 +1,5 @@ import type { Message } from "discord.js"; -import { randomise } from "../../utils/randomise.js"; +import { randomise } from "../../utils/randomise"; export default class Fan { async run(message: Message) { @@ -7,7 +7,7 @@ export default class Fan { const gifs = randomise([ "https://tenor.com/bC37i.gif", "https://tenor.com/view/fan-gif-20757784", - "https://tenor.com/view/below-deck-im-your-biggest-fan-biggest-fan-kate-kate-chastain-gif-15861715", + "https://tenor.com/view/below-deck-im-your-biggest-fan-biggest-fan-kate-kate-chastain-gif-15861715" ]); await message.channel.send(gifs); diff --git a/src/events/easterEggs/Fireship.ts b/src/events/easterEggs/Fireship.ts index 5317bf1..8a0e24e 100644 --- a/src/events/easterEggs/Fireship.ts +++ b/src/events/easterEggs/Fireship.ts @@ -3,7 +3,6 @@ import type { Message } from "discord.js"; export default class FireShip { async run(message: Message) { const content = message.content.toLowerCase(); - if ( content.startsWith("this has been") && content.endsWith("in 100 seconds") && diff --git a/src/events/easterEggs/Honk.ts b/src/events/easterEggs/Honk.ts index 1003288..bbc7567 100644 --- a/src/events/easterEggs/Honk.ts +++ b/src/events/easterEggs/Honk.ts @@ -2,7 +2,7 @@ import type { Message } from "discord.js"; export default class Honk { async run(message: Message) { - const honks: string[] = ["hnok", "hokn", "hkon", "onk", "hon", "honhk", "hhonk", "honkk"]; + const honks = ["hnok", "hokn", "hkon", "onk", "hon", "honhk", "hhonk", "honkk"]; if (honks.includes(message.content.toLowerCase())) message.channel.send("https://tenor.com/bW8sm.gif"); } } diff --git a/src/events/easterEggs/WhoPinged.ts b/src/events/easterEggs/WhoPinged.ts index 4fa5152..7fab89b 100644 --- a/src/events/easterEggs/WhoPinged.ts +++ b/src/events/easterEggs/WhoPinged.ts @@ -1,5 +1,5 @@ import type { Message } from "discord.js"; -import { randomise } from "../../utils/randomise.js"; +import { randomise } from "../../utils/randomise"; export default class WhoPinged { async run(message: Message) { @@ -12,7 +12,7 @@ export default class WhoPinged { "https://tenor.com/view/me-when-someone-pings-me-sad-cursed-emoji-crying-gif-22784322", "https://tenor.com/view/discord-triggered-notification-angry-dog-noises-dog-girl-gif-11710406", "https://tenor.com/view/tense-table-smash-mad-gif-13656077", - "https://tenor.com/view/yakuza-kazuma-kiryu-pissed-off-gif-14586175", + "https://tenor.com/view/yakuza-kazuma-kiryu-pissed-off-gif-14586175" ]); await message.channel.send(gifs); diff --git a/src/events/guildCreate.ts b/src/events/guildCreate.ts index 393dccb..4da596d 100644 --- a/src/events/guildCreate.ts +++ b/src/events/guildCreate.ts @@ -16,22 +16,18 @@ export default { } async run(guild: Guild) { - const owner = await guild.fetchOwner(); - const dmChannel = (await owner.createDM().catch(() => null)) as DMChannel | null; - const hearts = ["💖", "💝", "💓", "💗", "💘", "💟", "💕", "💞"]; - + const dmChannel = (await (await guild.fetchOwner()).createDM().catch(() => null)) as DMChannel | null; const embed = new EmbedBuilder() .setTitle("👋 • Welcome to Nebula!") .setDescription([ "Nebula is a multiplatform, multipurpose bot with the ability to add extensions to have additional features.", - "To manage the bot, sign into the [dashboard](https://dash.nebulabot.org).", + "To manage the bot, use the /settings command.", "Nebula is in an early stage of development. If you find bugs, please go to our [official server](https://discord.gg/7RdABJhQss) and report them." ].join("\n\n")) - .setFooter({ text: `Made by the Nebula team with ${randomise(hearts)}` }) + .setFooter({ text: `Made by the Nebula team with ${randomise(["💖", "💝", "💓", "💗", "💘", "💟", "💕", "💞"])}` }) .setColor(genColor(200)); - const commands = new Commands(guild.client); - await commands.registerCommandsForGuild(guild); + await new Commands(guild.client).registerCommandsForGuild(guild); if (dmChannel) await dmChannel.send({ embeds: [embed] }); } } diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts index 9d600a5..5357754 100644 --- a/src/events/messageCreate.ts +++ b/src/events/messageCreate.ts @@ -19,45 +19,31 @@ export default { if (guild?.id !== "903852579837059113") return; const eventsPath = join(process.cwd(), "src", "events", "easterEggs"); - for (const easterEggFile of readdirSync(eventsPath)) { - const msg = await import(pathToFileURL(join(eventsPath, easterEggFile)).toString()); - new msg.default().run(message, ...message.content); - } + for (const easterEggFile of readdirSync(eventsPath)) + new (await import(pathToFileURL(join(eventsPath, easterEggFile)).toString())).default().run(message, ...message.content); // Levelling const levelChannelId = get(guild.id, "levelling.channel"); if (!levelChannelId) return; if (!get(guild.id, "levelling.enabled")) return; - const expPerMessage = 2; - const baseExpForNewLevel = 2 * 50; - const difficultyMultiplier = 1.25; - - const [exp, level] = getLevel(guild.id, author.id); + const [guildExp, guildLevel] = getLevel(guild.id, author.id); const [globalExp, globalLevel] = getLevel("0", author.id); - const expUntilLevelup = Math.floor(baseExpForNewLevel * difficultyMultiplier * (level + 1)); - const newLevelData = { level: level ?? 0, exp: (exp ?? 0) + expPerMessage }; - const newLevelDataGlobal = { level: globalLevel ?? 0, exp: (globalExp ?? 0) + expPerMessage }; + const expUntilLevelup = Math.floor(100 * 1.25 * (guildLevel + 1)); - if (!(exp >= expUntilLevelup - 1)) { - setLevel(0, author.id, newLevelDataGlobal.level, newLevelDataGlobal.exp); - return setLevel(guild.id, author.id, newLevelDataGlobal.level, newLevelDataGlobal.exp); - } else if (exp >= expUntilLevelup - 1) { - let leftOverExp = exp - expUntilLevelup; + if (!(guildExp >= expUntilLevelup - 1)) { + setLevel(0, author.id, globalLevel ?? 0, (globalExp ?? 0) + 2); + return setLevel(guild.id, author.id, globalLevel ?? 0, (globalExp ?? 0) + 2); + } else if (guildExp >= expUntilLevelup - 1) { + let leftOverExp = guildExp - expUntilLevelup; if (leftOverExp < 0) leftOverExp = 0; - - newLevelData.level = level + 1; - newLevelData.exp = leftOverExp ?? 0; - setLevel(guild.id, author.id, newLevelData.level, newLevelData.exp); + setLevel(guild.id, author.id, guildLevel + 1, leftOverExp ?? 0); } - if (exp >= Math.floor(baseExpForNewLevel * difficultyMultiplier * (globalLevel + 1)) - 1) { - let leftOverExpGlobal = exp - expUntilLevelup; - if (leftOverExpGlobal < 0) leftOverExpGlobal = 0; - - newLevelDataGlobal.level = level + 1; - newLevelDataGlobal.exp = leftOverExpGlobal + 1; - setLevel(0, author.id, newLevelDataGlobal.level, newLevelDataGlobal.exp); + if (guildExp >= Math.floor(100 * 1.25 * (globalLevel + 1)) - 1) { + let globalLeftOverExp = guildExp - expUntilLevelup; + if (globalLeftOverExp < 0) globalLeftOverExp = 0; + setLevel(0, author.id, guildLevel + 1, globalLeftOverExp + 1); } const embed = new EmbedBuilder() @@ -65,8 +51,8 @@ export default { .setTitle("⚡ • Level Up!") .setDescription([ `**Congratulations, ${author.displayName}**!`, - `You made it to **level ${level + 1}**`, - `You need ${Math.floor(baseExpForNewLevel * difficultyMultiplier * (level + 2))} EXP to level up again.` + `You made it to **level ${guildLevel + 1}**`, + `You need ${Math.floor(100 * 1.25 * (guildLevel + 2))} EXP to level up again.` ].join("\n")) .setThumbnail(author.avatarURL()) .setTimestamp() @@ -75,9 +61,10 @@ export default { (guild.channels.cache.get(`${levelChannelId}`) as TextChannel).send({ embeds: [embed], content: `<@${author.id}>` }); for (const { level, roleID } of getLevelRewards(guild.id)) { const role = guild.roles.cache.get(`${roleID}`); - const authorRoles = (await guild.members.fetch()).get(author.id)?.roles; + if (!role) continue; - if (level >= level) { + const authorRoles = (await guild.members.fetch()).get(author.id)?.roles; + if (guildLevel >= level) { await authorRoles?.add(role); continue; } diff --git a/src/utils/embeds/errorEmbed.ts b/src/utils/embeds/errorEmbed.ts index 21d51b0..453678f 100644 --- a/src/utils/embeds/errorEmbed.ts +++ b/src/utils/embeds/errorEmbed.ts @@ -1,5 +1,5 @@ import { EmbedBuilder } from "discord.js"; -import { genColor } from "../colorGen.js"; +import { genColor } from "../colorGen"; /** * Sends the embed containing an error. diff --git a/src/utils/embeds/serverEmbed.ts b/src/utils/embeds/serverEmbed.ts index 86004cf..e1402d6 100644 --- a/src/utils/embeds/serverEmbed.ts +++ b/src/utils/embeds/serverEmbed.ts @@ -1,6 +1,7 @@ import { EmbedBuilder, type ColorResolvable, type Guild } from "discord.js"; -import { genColor, genRGBColor } from "../colorGen.js"; +import { genColor, genRGBColor } from "../colorGen"; import { database, getServerboardTable } from "../database.js"; +import { get } from "../database/settings"; import Vibrant from "node-vibrant"; import sharp from "sharp"; @@ -24,12 +25,7 @@ export async function serverEmbed(options: Options) { const pages = options.pages; const guild = options.guild; const { premiumTier: boostTier, premiumSubscriptionCount: boostCount } = guild; - - const invite = (await getServerboardTable(await database())) - ?.get(`${guild.id}.invite`) - .then(async invite => invite ? String(invite) : null) - .catch(() => null); - + const invite = get(guild.id, "serverboard.inviteLink"); const members = guild.members.cache; const boosters = members.filter(member => member.premiumSince); const onlineMembers = members.filter(member => ["online", "dnd", "idle"].includes(member.presence?.status)).size; @@ -52,10 +48,10 @@ export async function serverEmbed(options: Options) { `**Created on** ` ]; if (options.showSubs) generalValues.push(`**Subscribers**: ${options.subs}`); - if (options.showInvite && invite === null) generalValues.push(`**Invite link**: ${await invite}`); + if (options.showInvite && invite === null) generalValues.push(`**Invite link**: ${invite}`); const embed = new EmbedBuilder() - .setAuthor({ name: `${pages ? `#${page} • ` : ""}${guild.name}`, iconURL: guild.iconURL() }) + .setAuthor({ name: `${pages ? `#${page} • ` : ""}${guild.name}`, iconURL: guild.iconURL() || undefined }) .setDescription(guild.description ? guild.description : null) .setFields({ name: "📃 • General", value: generalValues.join("\n") }) .setFooter({ text: `Server ID: ${guild.id}${pages ? ` • Page ${page}/${pages}` : ""}` }) diff --git a/src/utils/sendChannelNews.ts b/src/utils/sendChannelNews.ts index d738d40..f9e724e 100644 --- a/src/utils/sendChannelNews.ts +++ b/src/utils/sendChannelNews.ts @@ -37,7 +37,7 @@ export async function sendChannelNews(guild: Guild, news: News, id: string) { let roleToSend: Role | undefined; if (role) roleToSend = guild.roles.cache.get(role); - const newsEmbed = new EmbedBuilder() + const embed = new EmbedBuilder() .setAuthor({ name: news.author, iconURL: news.authorPfp ?? null }) .setTitle(news.title) .setDescription(news.body) @@ -46,7 +46,7 @@ export async function sendChannelNews(guild: Guild, news: News, id: string) { .setFooter({ text: `Latest news from ${guild.name}` }) .setColor(genColor(200)); - const message = await channelToSend.send({ embeds: [newsEmbed], content: roleToSend ? `<@&${roleToSend.id}>` : undefined }); + const message = await channelToSend.send({ embeds: [embed], content: roleToSend ? `<@&${roleToSend.id}>` : undefined }); await newsTable.set(`${guild.id}.news.${id}.messageId`, message.id); return; } diff --git a/src/utils/sendSubscribedNews.ts b/src/utils/sendSubscribedNews.ts index be08cdc..ed90580 100644 --- a/src/utils/sendSubscribedNews.ts +++ b/src/utils/sendSubscribedNews.ts @@ -20,8 +20,7 @@ export async function sendSubscribedNews(guild: Guild, news: News) { const subscribed = (await guild.members.fetch()).filter(member => subscriptions.includes(member.id)); const memberDMs = (await Promise.all(subscribed.map(member => member.createDM().catch(() => null)))) as DMChannel[] | null; const memberDMsOpen = memberDMs?.filter(dm => dm !== null); - - const newsEmbed = new EmbedBuilder() + const embed = new EmbedBuilder() .setAuthor({ name: news.author, iconURL: news.authorPfp ?? null }) .setTitle(news.title) .setDescription(news.body) @@ -30,6 +29,6 @@ export async function sendSubscribedNews(guild: Guild, news: News) { .setFooter({ text: `Latest news from ${guild.name}` }) .setColor(genColor(200)); - await Promise.all(memberDMsOpen?.map(dm => dm.send({ embeds: [newsEmbed] }).catch(() => null))); + await Promise.all(memberDMsOpen?.map(dm => dm.send({ embeds: [embed] }).catch(() => null))); return; } From b8d8e4698c0ab8d1506d91f5e3fc57402b96d74b Mon Sep 17 00:00:00 2001 From: Serge Date: Thu, 18 Jan 2024 23:10:55 +0600 Subject: [PATCH 008/127] goofy --- CONTRIBUTING.md | 17 +- README.md | 4 +- bun.lockb | Bin 55882 -> 59108 bytes package.json | 2 +- src/commands/about.ts | 7 +- src/commands/moderation/ban.ts | 48 ++---- src/commands/moderation/delwarn.ts | 66 +++---- src/commands/news.ts | 21 ++- src/commands/server/info.ts | 2 +- src/commands/server/news.ts | 73 ++++++++ src/commands/server/news/add.ts | 102 ----------- src/commands/server/news/edit.ts | 161 ------------------ src/commands/server/news/remove.ts | 61 ------- src/commands/server/news/view.ts | 107 ------------ src/commands/serverboard.ts | 11 +- src/commands/settings/command/list.ts | 51 ------ src/commands/settings/command/toggle.ts | 82 --------- .../settings/leveling/block-channels.ts | 91 ---------- src/commands/settings/leveling/channel.ts | 82 --------- src/commands/settings/leveling/rewards.ts | 147 ---------------- src/commands/settings/leveling/set.ts | 78 --------- src/commands/settings/leveling/toggle.ts | 42 ----- src/commands/settings/moderation/logs.ts | 49 ------ src/commands/settings/news/channel.ts | 61 ------- src/commands/settings/serverboard/invite.ts | 63 ------- src/commands/settings/serverboard/reveal.ts | 55 ------ src/commands/user/info.ts | 16 +- src/commands/user/level.ts | 7 +- src/utils/colorGen.ts | 8 +- src/utils/database/index.ts | 5 +- src/utils/database/levelRewards.ts | 6 +- src/utils/database/levelling.ts | 2 +- src/utils/multiReact.ts | 1 - 33 files changed, 156 insertions(+), 1372 deletions(-) create mode 100644 src/commands/server/news.ts delete mode 100644 src/commands/server/news/add.ts delete mode 100644 src/commands/server/news/edit.ts delete mode 100644 src/commands/server/news/remove.ts delete mode 100644 src/commands/server/news/view.ts delete mode 100644 src/commands/settings/command/list.ts delete mode 100644 src/commands/settings/command/toggle.ts delete mode 100644 src/commands/settings/leveling/block-channels.ts delete mode 100644 src/commands/settings/leveling/channel.ts delete mode 100644 src/commands/settings/leveling/rewards.ts delete mode 100644 src/commands/settings/leveling/set.ts delete mode 100644 src/commands/settings/leveling/toggle.ts delete mode 100644 src/commands/settings/moderation/logs.ts delete mode 100644 src/commands/settings/news/channel.ts delete mode 100644 src/commands/settings/serverboard/invite.ts delete mode 100644 src/commands/settings/serverboard/reveal.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 52b1d59..7f50bf2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,9 +2,9 @@ ## Prerequisites - Basic knowledge of [TypeScript](https://typescriptlang.org/) and [discord.js](https://discord.js.org/). -- [Bun](https://bun.sh) and [MySQL](https://mysql.com/) installed. +- [Bun](https://bun.sh) installed. -## Get started with developing +## Get started with contributing ### Getting the code - Make a fork of this repository. - Clone your fork. @@ -14,18 +14,13 @@ - Invite your bot to your server. - Reset and then copy your bot's token. -### Setting up your database -We use MySQL for the database. You need to set one up to be able to run the bot. - -- Create a database file called `json.db` -- It is recommended to create a specific user for Nebula only. See [the docs](https://dev.mysql.com/doc/refman/8.0/en/creating-accounts.html) for more information. - ### Setting up .env -- Copy the `example.env` file and replace the content with your own credentials inside a file called `.env`. +- Copy the `example.env` file. +- Paste it into a `.env` file and replace the token with your own. ### Running -- Run `bun i` to install all the modules needed by the bot. -- Run `bun start` +- Run `bun i` to install all the dependencies needed by the bot. +- Run `bun start`. Be sure to open a pull request when you're ready to push your changes. Be descriptive of the changes you've made. diff --git a/README.md b/README.md index 3596e2f..43a1426 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,7 @@ # About Nebula is a multiplatform, multipurpose bot with the ability to add extensions to have additional features. -**Please note that Nebula is currently in an alpha preview state and only usable within Discord with limited functionality.** +**Please note that Nebula is currently unstable and is only usable within Discord.** # Contributing While we're developing the multiplatform version of the bot, you can still [help us](CONTRIBUTING.MD) if you find any bugs. - -Only bug fixes are accepted, **no new features!** diff --git a/bun.lockb b/bun.lockb index 0f508ce8113890e241f6b47e883dade01271e0ad..b66eff0ca37048daa75c3e1a0751ee0de4203879 100644 GIT binary patch delta 10180 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 diff --git a/package.json b/package.json index 75f4506..008c5b3 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ }, "devDependencies": { "bun-types": "0.8.1", - "typescript": "^5.2.2" + "typescript": "5.2.2" }, "trustedDependencies": ["sharp"] } diff --git a/src/commands/about.ts b/src/commands/about.ts index 76240e6..a432eec 100644 --- a/src/commands/about.ts +++ b/src/commands/about.ts @@ -5,7 +5,6 @@ import { randomise } from "../utils/randomise"; export default class About { data: SlashCommandSubcommandBuilder; deferred: boolean = false; - constructor() { this.data = new SlashCommandSubcommandBuilder() .setName("about") @@ -16,8 +15,6 @@ export default class About { const client = interaction.client; const guilds = client.guilds.cache; const shards = client.shard?.count; - const hearts = ["💖", "💝", "💓", "💗", "💘", "💟", "💕", "💞"]; - const embed = new EmbedBuilder() .setAuthor({ name: "• About", iconURL: client.user.displayAvatarURL() }) .setDescription("Nebula is a multiplatform, multipurpose bot with the ability to add extensions to have additional features.") @@ -25,7 +22,7 @@ export default class About { { name: "📃 • General", value: [ - "**Version** 0.1", + "**Version** 0.1, *Dasshubodo update*", `**${guilds.size}** guild${guilds.size === 1 ? "" : "s"} • **${shards}** shard${shards === 1 ? "" : "s"}` ].join("\n") }, @@ -44,7 +41,7 @@ export default class About { value: "[GitHub](https://www.github.com/NebulaTheBot)・[YouTube](https://www.youtube.com/@NebulaTheBot)・[Instagram](https://instagram.com/NebulaTheBot)・[Mastodon](https://mastodon.online/@NebulaTheBot@mastodon.social)・[Guilded](https://guilded.gg/Nebula)・[Revolt](https://rvlt.gg/28TS9aXy)" } ) - .setFooter({ text: `Made by the Nebula team with ${randomise(hearts)}` }) + .setFooter({ text: `Made by the Nebula team with ${randomise(["💖", "💝", "💓", "💗", "💘", "💟", "💕", "💞"])}` }) .setThumbnail(client.user.displayAvatarURL()) .setColor(genColor(270)); diff --git a/src/commands/moderation/ban.ts b/src/commands/moderation/ban.ts index 8985521..80db033 100644 --- a/src/commands/moderation/ban.ts +++ b/src/commands/moderation/ban.ts @@ -3,18 +3,14 @@ import { TextChannel, DMChannel, ChannelType, type Channel, type ChatInputCommandInteraction } from "discord.js"; -import { genColor } from "../../utils/colorGen.js"; -import { errorEmbed } from "../../utils/embeds/errorEmbed.js"; -import { QuickDB } from "quick.db"; -import { getSettingsTable } from "../../utils/database.js"; +import { genColor } from "../../utils/colorGen"; +import { errorEmbed } from "../../utils/embeds/errorEmbed"; +import { get } from "../../utils/database/settings"; export default class Ban { data: SlashCommandSubcommandBuilder; deferred: boolean = false; - db: QuickDB; - - constructor(db: QuickDB) { - this.db = db; + constructor() { this.data = new SlashCommandSubcommandBuilder() .setName("ban") .setDescription("Bans a user.") @@ -30,10 +26,11 @@ export default class Ban { } async run(interaction: ChatInputCommandInteraction) { - const user = interaction.options.getUser("user"); - const members = interaction.guild.members.cache; - const member = members.get(interaction.member.user.id); - const selectedMember = members.get(user.id); + const user = interaction.options.getUser("user")!; + const guild = interaction.guild!; + const members = guild.members.cache!; + const member = members.get(interaction.member?.user.id!)!; + const selectedMember = members.get(user.id)!; const name = selectedMember.nickname ?? user.username; if (!member.permissions.has(PermissionsBitField.Flags.BanMembers)) return await interaction.reply({ @@ -60,34 +57,19 @@ export default class Ban { const embed = new EmbedBuilder() .setAuthor({ name: `• ${user.username}`, iconURL: user.displayAvatarURL() }) .setTitle(`✅ • Banned ${user.username}`) - .setDescription([ - `**Moderator**: <@${interaction.user.id}>`, - `**Reason**: ${reason ?? "No reason provided"}` - ].join("\n")) - .setThumbnail(user.displayAvatarURL()) - .setFooter({ text: `User ID: ${user.id}` }) - .setColor(genColor(100)); - - const embedDM = new EmbedBuilder() - .setAuthor({ name: `• ${user.username}`, iconURL: user.displayAvatarURL() }) - .setTitle(`🔨 • You were banned`) .setDescription([ `**Moderator**: ${interaction.user.username}`, `**Reason**: ${reason ?? "No reason provided"}` ].join("\n")) .setThumbnail(user.displayAvatarURL()) .setFooter({ text: `User ID: ${user.id}` }) - .setColor(genColor(0)); - - const logChannel = await (await getSettingsTable(this.db)) - ?.get(`${interaction.guild.id}.logChannel`) - .then((channel: string | null) => channel ?? null) - .catch(() => null); + .setColor(genColor(100)); + const logChannel = get(guild.id, "log.channel"); if (logChannel) { - const channel = await interaction.guild.channels.cache - .get(logChannel) - .fetch() + const channel = await guild.channels.cache + .get(`${logChannel}`) + ?.fetch() .then((channel: Channel) => { if (channel.type != ChannelType.GuildText) return null; return channel as TextChannel; @@ -98,7 +80,7 @@ export default class Ban { } const dmChannel = (await user.createDM().catch(() => null)) as DMChannel | null; - if (dmChannel) await dmChannel.send({ embeds: [embedDM] }); + if (dmChannel) await dmChannel.send({ embeds: [embed.setTitle("🔨 • You were banned").setColor(genColor(0))] }); await selectedMember.ban({ reason: reason ?? undefined }); await interaction.reply({ embeds: [embed] }); } diff --git a/src/commands/moderation/delwarn.ts b/src/commands/moderation/delwarn.ts index e8d8ea7..0958ee1 100644 --- a/src/commands/moderation/delwarn.ts +++ b/src/commands/moderation/delwarn.ts @@ -3,17 +3,14 @@ import { TextChannel, DMChannel, ChannelType, type Channel, type ChatInputCommandInteraction } from "discord.js"; -import { genColor } from "../../utils/colorGen.js"; -import { errorEmbed } from "../../utils/embeds/errorEmbed.js"; -import { QuickDB } from "quick.db"; -import { getModerationTable, getSettingsTable } from "../../utils/database.js"; +import { genColor } from "../../utils/colorGen"; +import { errorEmbed } from "../../utils/embeds/errorEmbed"; +import { listUserModeration, removeModeration } from "../../utils/database/moderation"; +import { get } from "../../utils/database/settings"; export default class Delwarn { data: SlashCommandSubcommandBuilder; - db: QuickDB; - - constructor(db: QuickDB) { - this.db = db; + constructor() { this.data = new SlashCommandSubcommandBuilder() .setName("delwarn") .setDescription("Removes a warning from a user.") @@ -30,23 +27,23 @@ export default class Delwarn { } async run(interaction: ChatInputCommandInteraction) { - const user = interaction.options.getUser("user"); - const members = interaction.guild.members.cache; - const member = members.get(interaction.member.user.id); - const selectedMember = members.get(user.id); + const user = interaction.options.getUser("user")!; + const guild = interaction.guild!; + const members = guild.members.cache!; + const member = members.get(interaction.member?.user.id!); + const selectedMember = members.get(user.id)!; const name = selectedMember.nickname ?? user.username; const id = interaction.options.getNumber("id", true); - const warns = await (await getModerationTable(this.db)) - ?.get(`${interaction.guild.id}.${user.id}.warns`) - .then(warns => warns as any[] ?? []) - .catch(() => []); - const newWarns = warns.filter(warn => warn.id !== id); + const warns = listUserModeration(guild.id, user.id, "WARN"); + const newWarns = warns.filter(warn => warn.id !== `${id}`); - if (!member.permissions.has(PermissionsBitField.Flags.ModerateMembers)) return await interaction.followUp({ + if (!member?.permissions.has(PermissionsBitField.Flags.ModerateMembers)) return await interaction.followUp({ embeds: [errorEmbed("You need the **Moderate Members** permission to execute this command.")] }); - if (selectedMember === member) return await interaction.followUp({ embeds: [errorEmbed("You can't remove a warn from yourself.")] }); + if (selectedMember === member) return await interaction.followUp({ + embeds: [errorEmbed("You can't remove a warn from yourself.")] + }); if (newWarns.length === warns.length) return await interaction.followUp({ embeds: [errorEmbed(`There is no warn with the id of ${id}.`)] @@ -64,33 +61,18 @@ export default class Delwarn { .setAuthor({ name: `• ${user.username}`, iconURL: user.displayAvatarURL() }) .setTitle(`✅ • Removed warning`) .setDescription([ - `**Moderator**: <@${member.id}>`, - `**Original reason**: ${newWarns.find(warn => warn.id === id)?.reason ?? "No reason provided"}` - ].join("\n")) - .setThumbnail(user.displayAvatarURL()) - .setFooter({ text: `User ID: ${user.id}` }) - .setColor(genColor(100)); - - const embedDM = new EmbedBuilder() - .setAuthor({ name: `• ${user.username}`, iconURL: user.displayAvatarURL() }) - .setTitle(`🤝 • Your warning was removed`) - .setDescription([ - `**Moderator**: ${member.user.username}`, - `**Original reason**: ${newWarns.find(warn => warn.id === id)?.reason ?? "No reason provided"}` + `**Moderator**: ${interaction.user.username}`, + `**Original reason**: ${newWarns.find(warn => warn.id === `${id}`)?.reason ?? "No reason provided"}` ].join("\n")) .setThumbnail(user.displayAvatarURL()) .setFooter({ text: `User ID: ${user.id}` }) .setColor(genColor(100)); - const logChannel = await (await getSettingsTable(this.db)) - ?.get(`${interaction.guild.id}.logChannel`) - .then((channel: string | null) => channel) - .catch(() => null); - + const logChannel = get(guild.id, "log.channel"); if (logChannel) { - const channel = await interaction.guild.channels.cache - .get(logChannel) - .fetch() + const channel = await guild.channels.cache + .get(`${logChannel}`) + ?.fetch() .then((channel: Channel) => { if (channel.type != ChannelType.GuildText) return null; return channel as TextChannel; @@ -101,8 +83,8 @@ export default class Delwarn { } const dmChannel = (await user.createDM().catch(() => null)) as DMChannel | null; - if (dmChannel) await dmChannel.send({ embeds: [embedDM] }); - await (await getModerationTable(this.db))?.set(`${interaction.guild.id}.${user.id}.warns`, newWarns); + if (dmChannel) await dmChannel.send({ embeds: [embed.setTitle("🤝 • Your warning was removed")] }); + removeModeration(guild.id, `${id}`); await interaction.followUp({ embeds: [embed] }); } } diff --git a/src/commands/news.ts b/src/commands/news.ts index 8246dc3..890afd2 100644 --- a/src/commands/news.ts +++ b/src/commands/news.ts @@ -7,32 +7,31 @@ import { listAllNews } from "../utils/database/news"; export default class News { data: SlashCommandSubcommandBuilder; - constructor() { this.data = new SlashCommandSubcommandBuilder() .setName("news") - .setDescription("The news of Nebula.") + .setDescription("View the news of Nebula.") .addNumberOption(option => option .setName("page") - .setDescription("The page of the news you want to see") + .setDescription("The page of the news that you want to see.") ); } async run(interaction: ChatInputCommandInteraction) { let page = interaction.options.getNumber("page") ?? 1; - const newsSorted = (Object.values(listAllNews("903852579837059113")) as any[])?.sort((a, b) => b.createdAt - a.createdAt); + const sortedNews = (Object.values(listAllNews("903852579837059113")) as any[])?.sort((a, b) => b.createdAt - a.createdAt); + let currentNews = sortedNews[page - 1]; - if (page > newsSorted.length) page = newsSorted.length; + if (page > sortedNews.length) page = sortedNews.length; if (page < 1) page = 1; - let currentNews = newsSorted[page - 1]; let embed = new EmbedBuilder() .setAuthor({ name: currentNews.author, iconURL: currentNews.authorPfp ?? null }) .setTitle(currentNews.title) .setDescription(currentNews.body) .setImage(currentNews.imageURL || null) .setTimestamp(parseInt(currentNews.updatedAt)) - .setFooter({ text: `Page ${page} of ${newsSorted.length} • ID: ${currentNews.id}` }) + .setFooter({ text: `Page ${page} of ${sortedNews.length} • ID: ${currentNews.id}` }) .setColor(genColor(270)); const row = new ActionRowBuilder().addComponents( @@ -48,15 +47,15 @@ export default class News { await interaction.followUp({ embeds: [embed], components: [row] }); interaction.channel - ?.createMessageComponentCollector({ filter: (i: { user: { id: string; }; }) => i.user.id === interaction.user.id, time: 60000 }) - .on("collect", async (i: { isButton: () => any; customId: string; deferUpdate: () => any; }) => { + ?.createMessageComponentCollector({ filter: i => i.user.id === interaction.user.id, time: 60000 }) + .on("collect", async i => { if (!i.isButton()) return; if (i.customId === "left") { page--; - if (page < 1) page = newsSorted.length; + if (page < 1) page = sortedNews.length; } else if (i.customId === "right") { page++; - if (page > newsSorted.length) page = 1; + if (page > sortedNews.length) page = 1; } currentNews = currentNews; diff --git a/src/commands/server/info.ts b/src/commands/server/info.ts index 26de1ad..9da46a4 100644 --- a/src/commands/server/info.ts +++ b/src/commands/server/info.ts @@ -10,7 +10,7 @@ export default class ServerInfo { } async run(interaction: ChatInputCommandInteraction) { - const embed = await serverEmbed({ guild: interaction.guild, roles: true }); + const embed = await serverEmbed({ guild: interaction.guild!, roles: true }); await interaction.followUp({ embeds: [embed] }); } } diff --git a/src/commands/server/news.ts b/src/commands/server/news.ts new file mode 100644 index 0000000..c11db9d --- /dev/null +++ b/src/commands/server/news.ts @@ -0,0 +1,73 @@ +import { + SlashCommandSubcommandBuilder, EmbedBuilder, ActionRowBuilder, + ButtonBuilder, ButtonStyle, type ChatInputCommandInteraction +} from "discord.js"; +import { genColor } from "../../utils/colorGen"; +import { errorEmbed } from "../../utils/embeds/errorEmbed"; +import { listAllNews } from "../../utils/database/news"; + +export default class News { + data: SlashCommandSubcommandBuilder; + constructor() { + this.data = new SlashCommandSubcommandBuilder() + .setName("news") + .setDescription("View the news of this server.") + .addNumberOption(option => option + .setName("page") + .setDescription("The page of the news that you want to see.") + ); + } + + async run(interaction: ChatInputCommandInteraction) { + let page = interaction.options.getNumber("page") ?? 1; + const news = listAllNews(interaction.guild?.id!); + const sortedNews = (Object.values(news) as any[])?.sort((a, b) => b.createdAt - a.createdAt); + let currentNews = sortedNews[page - 1]; + + if (!news || !sortedNews || sortedNews.length == 0) return await interaction.followUp({ + embeds: [errorEmbed("No news found.\nAdmins can add news with the **/settings news add** command.")] + }); + if (page > sortedNews.length) page = sortedNews.length; + if (page < 1) page = 1; + + let embed = new EmbedBuilder() + .setAuthor({ name: currentNews.author, iconURL: currentNews.authorPFP }) + .setTitle(currentNews.title) + .setDescription(currentNews.body) + .setImage(currentNews.imageURL || null) + .setTimestamp(parseInt(currentNews.updatedAt)) + .setFooter({ text: `Page ${page} of ${sortedNews.length} • ID: ${currentNews.id}` }) + .setColor(genColor(200)); + + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId("left") + .setEmoji("1137330341472915526") + .setStyle(ButtonStyle.Primary), + new ButtonBuilder() + .setCustomId("right") + .setEmoji("1137330125004869702") + .setStyle(ButtonStyle.Primary) + ); + + await interaction.followUp({ embeds: [embed], components: [row] }); + interaction.channel + ?.createMessageComponentCollector({ filter: i => i.user.id === interaction.user.id, time: 60000 }) + .on("collect", async i => { + if (!i.isButton()) return; + if (i.customId === "left") { + page--; + if (page < 1) page = sortedNews.length; + } else if (i.customId === "right") { + page++; + if (page > sortedNews.length) page = 1; + } + + currentNews = currentNews; + embed = embed; + + await interaction.editReply({ embeds: [embed], components: [row] }); + await i.deferUpdate(); + }); + } +} diff --git a/src/commands/server/news/add.ts b/src/commands/server/news/add.ts deleted file mode 100644 index dbd8b26..0000000 --- a/src/commands/server/news/add.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { - SlashCommandSubcommandBuilder, EmbedBuilder, PermissionsBitField, - ModalBuilder, TextInputBuilder, ActionRowBuilder, - TextInputStyle, type ChatInputCommandInteraction -} from "discord.js"; -import { getNewsTable } from "../../../utils/database.js"; -import { genColor } from "../../../utils/colorGen.js"; -import { errorEmbed } from "../../../utils/embeds/errorEmbed.js"; -import { sendSubscribedNews, News } from "../../../utils/sendSubscribedNews.js"; -import { sendChannelNews } from "../../../utils/sendChannelNews.js"; -import { QuickDB } from "quick.db"; - -export default class Add { - data: SlashCommandSubcommandBuilder; - deferred: boolean = false; - db: QuickDB; - - constructor(db?: QuickDB) { - this.db = db; - this.data = new SlashCommandSubcommandBuilder() - .setName("add") - .setDescription("Adds news to your guild."); - } - - async run(interaction: ChatInputCommandInteraction) { - const member = interaction.guild.members.cache.get(interaction.user.id); - if (!member.permissions.has(PermissionsBitField.Flags.ManageGuild)) return await interaction.reply({ - embeds: [errorEmbed("You need **Manage Server** permissions to add news.")], - }); - - const newsModal = new ModalBuilder() - .setCustomId("addnews") - .setTitle("Create new News for your server/project"); - - const titleInput = new TextInputBuilder() - .setCustomId("title") - .setPlaceholder("Write a title") - .setStyle(TextInputStyle.Short) - .setMaxLength(100) - .setLabel("Title") - .setRequired(true); - - const bodyInput = new TextInputBuilder() - .setCustomId("body") - .setPlaceholder("Insert your content here") - .setMaxLength(4000) - .setStyle(TextInputStyle.Paragraph) - .setLabel("Content (supports Markdown)") - .setRequired(true); - - const imageURLInput = new TextInputBuilder() - .setCustomId("imageurl") - .setPlaceholder("Place a link to your image") - .setStyle(TextInputStyle.Short) - .setMaxLength(1000) - .setLabel("Image URL (placed at the bottom)") - .setRequired(false); - - const firstActionRow = new ActionRowBuilder().addComponents(titleInput) as ActionRowBuilder; - const secondActionRow = new ActionRowBuilder().addComponents(bodyInput) as ActionRowBuilder; - const thirdActionRow = new ActionRowBuilder().addComponents(imageURLInput) as ActionRowBuilder; - - newsModal.addComponents(firstActionRow, secondActionRow, thirdActionRow); - await interaction.showModal(newsModal).catch(err => console.error(err)); - - interaction.client.once("interactionCreate", async interaction => { - if (!interaction.isModalSubmit()) return; - if (interaction.customId !== "addnews") return; - - const imageURL = interaction.fields.getTextInputValue("imageurl") as string | undefined; - if (imageURL) { - await interaction.reply({ - embeds: [errorEmbed("The image URL you provided is invalid.")], - }); - return; - } - - const id = crypto.randomUUID(); - const news = { - id, - title: interaction.fields.getTextInputValue("title") as string, - body: interaction.fields.getTextInputValue("body") as string, - imageURL, - author: interaction.user.displayName ?? interaction.user.username, - authorPfp: interaction.user.avatarURL(), - createdAt: Date.now().toString(), - updatedAt: Date.now().toString(), - messageId: null - }; - - sendSubscribedNews(interaction.guild, news as News).catch(err => console.error(err)); - sendChannelNews(interaction.guild, news as News, id).catch(err => console.error(err)); - - const embed = new EmbedBuilder() - .setTitle("✅ • News sent!") - .setColor(genColor(100)); - - await (await getNewsTable(this.db)).set(`${interaction.guild.id}.news.${id}`, news); - await interaction.reply({ embeds: [embed] }); - }); - } -} diff --git a/src/commands/server/news/edit.ts b/src/commands/server/news/edit.ts deleted file mode 100644 index 3373fe6..0000000 --- a/src/commands/server/news/edit.ts +++ /dev/null @@ -1,161 +0,0 @@ -import { - SlashCommandSubcommandBuilder, EmbedBuilder, PermissionsBitField, - ModalBuilder, TextInputBuilder, ActionRowBuilder, - TextInputStyle, TextChannel, Message, - type ChatInputCommandInteraction -} from "discord.js"; -import { getNewsTable } from "../../../utils/database.js"; -import { genColor } from "../../../utils/colorGen.js"; -import { errorEmbed } from "../../../utils/embeds/errorEmbed.js"; -import { sendSubscribedNews, News } from "../../../utils/sendSubscribedNews.js"; -import { QuickDB } from "quick.db"; - -export default class Edit { - data: SlashCommandSubcommandBuilder; - deferred: boolean = false; - db: QuickDB; - - constructor(db?: QuickDB) { - this.db = db; - this.data = new SlashCommandSubcommandBuilder() - .setName("edit") - .setDescription("Edits new of your guild.") - .addStringOption(option => option - .setName("id") - .setDescription("The ID of the news you want to edit.") - .setRequired(true) - ); - } - - async run(interaction: ChatInputCommandInteraction) { - const db = this.db; - const newsTable = await getNewsTable(db); - - const user = interaction.user; - const guild = interaction.guild; - const id = interaction.options.getString("id", true).trim(); - const news = await newsTable - ?.get(`${guild.id}.news.${id}`) - .then(news => news as News) - .catch(() => null as News | null); - - if (!news) return await interaction.followUp({ - embeds: [errorEmbed("The specified news doesn't exist.")] - }); - - const author = user.displayName ?? user.username; - const timestamp = Date.now().toString(); - const member = guild.members.cache.get(user.id); - - if (!member.permissions.has(PermissionsBitField.Flags.ManageGuild)) return await interaction.followUp({ - embeds: [errorEmbed("You need **Manage Server** permissions to add news.")], - }); - - const editModal = new ModalBuilder() - .setCustomId("editnews") - .setTitle("Edit News: " + news.title); - - const titleInput = new TextInputBuilder() - .setCustomId("title") - .setPlaceholder("Title") - .setStyle(TextInputStyle.Short) - .setMaxLength(100) - .setLabel("Title") - .setValue(news.title) - .setRequired(true); - - const bodyInput = new TextInputBuilder() - .setCustomId("body") - .setPlaceholder("Content (markdown)") - .setMaxLength(4000) - .setStyle(TextInputStyle.Paragraph) - .setLabel("Content (markdown)") - .setValue(news.body) - .setRequired(true); - - const imageURLInput = new TextInputBuilder() - .setCustomId("imageurl") - .setPlaceholder("Big image URL (bottom)") - .setStyle(TextInputStyle.Short) - .setMaxLength(1000) - .setLabel("Big image URL (bottom)") - .setValue(news.imageURL) - .setRequired(false); - - const firstActionRow = new ActionRowBuilder().addComponents(titleInput) as ActionRowBuilder; - const secondActionRow = new ActionRowBuilder().addComponents(bodyInput) as ActionRowBuilder; - const thirdActionRow = new ActionRowBuilder().addComponents(imageURLInput) as ActionRowBuilder; - - editModal.addComponents(firstActionRow, secondActionRow, thirdActionRow); - await interaction.showModal(editModal).catch(err => console.error(err)); - - interaction.client.once("interactionCreate", async (interaction) => { - if (!interaction.isModalSubmit()) return; - if (interaction.customId !== "editnews") return; - - const title = interaction.fields.getTextInputValue("title") as string; - const body = interaction.fields.getTextInputValue("body") as string; - const imageURL = interaction.fields.getTextInputValue("imageurl") as string | undefined; - - if (imageURL) await interaction.reply({ - embeds: [errorEmbed("The image URL you provided is invalid.")], - }); - - const newNews = { - ...news, - title, - body, - imageURL, - author, - authorPfp: user.avatarURL(), - updatedAt: timestamp - }; - - sendSubscribedNews(guild, {...newNews, title: `Updated: ${newNews.title}`} as News) - .catch(err => console.error(err)); - - const newsEmbed = new EmbedBuilder() - .setAuthor({ name: newNews.author, iconURL: newNews.authorPfp ?? null }) - .setTitle(newNews.title) - .setDescription(newNews.body) - .setImage(newNews.imageURL || null) - .setTimestamp(parseInt(newNews.updatedAt)) - .setFooter({ text: `Updated news from ${guild.name}` }) - .setColor(genColor(200)); - - const subscribedNewsChannel = await newsTable - ?.get(`${guild.id}.channel`) - .then(channel => channel as { channelId: string; roleId: string } | null) - .catch(() => { - return { channelId: null as string | null, roleId: null as string | null }; - }); - - if (subscribedNewsChannel.channelId) { - const messageId = newNews?.messageId; - const newsChannel = (await guild.channels.fetch(subscribedNewsChannel?.channelId ?? "").catch(() => { })) as TextChannel | null; - - if (!messageId && newsChannel.id) newNews.messageId = ((await newsChannel - ?.send({ - embeds: [newsEmbed], - content: subscribedNewsChannel.roleId ? `<@&${subscribedNewsChannel.roleId}>` : null - }) - .catch(() => { })) as Message | null - )?.id; - - else if (newsChannel.id) await newsChannel?.messages - .edit(messageId, { - embeds: [newsEmbed], - content: subscribedNewsChannel.roleId ? `<@&${subscribedNewsChannel.roleId}>` : null - }) - .catch(() => { }); - } - - const embed = new EmbedBuilder() - .setTitle("✅ • News edited!") - .setColor(genColor(100)); - - await newsTable.set(`${guild.id}.news.${id}`, newNews); - await interaction.reply({ embeds: [embed] }); - }); - } -} diff --git a/src/commands/server/news/remove.ts b/src/commands/server/news/remove.ts deleted file mode 100644 index c196d29..0000000 --- a/src/commands/server/news/remove.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { - SlashCommandSubcommandBuilder, EmbedBuilder, PermissionsBitField, - TextChannel, type ChatInputCommandInteraction -} from "discord.js"; -import { getNewsTable } from "../../../utils/database.js"; -import { genColor } from "../../../utils/colorGen.js"; -import { errorEmbed } from "../../../utils/embeds/errorEmbed.js"; -import { QuickDB } from "quick.db"; - -export default class Remove { - data: SlashCommandSubcommandBuilder; - db: QuickDB; - - constructor(db?: QuickDB) { - this.db = db; - this.data = new SlashCommandSubcommandBuilder() - .setName("remove") - .setDescription("Removes news from your guild.") - .addStringOption(option => option - .setName("id") - .setDescription("The ID of the news. Found in the footer of the news.") - .setRequired(true) - ); - } - - async run(interaction: ChatInputCommandInteraction) { - const db = this.db; - const newsTable = await getNewsTable(db); - const user = interaction.user; - const guild = interaction.guild; - const providedId = interaction.options.getString("id"); - const member = guild.members.cache.get(user.id); - - if (!member.permissions.has(PermissionsBitField.Flags.ManageGuild)) return await interaction.followUp({ - embeds: [errorEmbed("You need **Manage Server** permissions to delete news.")], - }); - - const embed = new EmbedBuilder() - .setTitle("✅ • News deleted!") - .setColor(genColor(100)); - - const subscribedChannel = await newsTable - ?.get(`${guild.id}.channel`) - .then(channel => channel as { channelId: string, roleId: string }) - .catch(() => { - return { channelId: null, roleId: null }; - }); - - const news = await newsTable?.get(providedId).catch(() => null); - if (!news) return await interaction.followUp({ - embeds: [errorEmbed("The specified news doesn't exist.")] - }); - - const messageId = news?.messageId; - const newsChannel = (await interaction.guild.channels.fetch(subscribedChannel?.channelId ?? "").catch(() => null)) as TextChannel | null; - if (newsChannel) await newsChannel?.messages.delete(messageId).catch(() => null); - - await newsTable?.delete(`${guild.id}.news.${providedId}`).catch(() => null); - await interaction.followUp({ embeds: [embed] }); - } -} diff --git a/src/commands/server/news/view.ts b/src/commands/server/news/view.ts deleted file mode 100644 index 8904206..0000000 --- a/src/commands/server/news/view.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { - SlashCommandSubcommandBuilder, EmbedBuilder, ActionRowBuilder, - ButtonBuilder, ButtonStyle, type ChatInputCommandInteraction -} from "discord.js"; -import { genColor } from "../../../utils/colorGen.js"; -import { getNewsTable } from "../../../utils/database.js"; -import { QuickDB } from "quick.db"; -import { errorEmbed } from "../../../utils/embeds/errorEmbed.js"; - -export default class View { - data: SlashCommandSubcommandBuilder; - db: QuickDB; - - constructor(db?: QuickDB) { - this.db = db; - this.data = new SlashCommandSubcommandBuilder() - .setName("view") - .setDescription("View the news of this server.") - .addNumberOption(option => option - .setName("page") - .setDescription("The page of the news you want to see.") - ); - } - - async run(interaction: ChatInputCommandInteraction) { - const db = this.db; - const newsletterTable = await getNewsTable(db); - const guild = interaction.guild; - let page = interaction.options.getNumber("page") ?? 1; - - const news = await newsletterTable - ?.get(`${guild.id}.news`) - .then(news => news as any[] ?? []) - .catch(() => []); - - const newsSorted = ( - Object.values(news).map((newsItem, i) => { - return { - id: Object.keys(news)[i], - ...newsItem - } - }) as any[] - )?.sort((a, b) => b.createdAt - a.createdAt); - - if (!news) return await interaction.followUp({ - embeds: [errorEmbed("No news found.\nAdmins can add news with the **/settings news add** command.")] - }); - - if (!newsSorted) return await interaction.followUp({ - embeds: [errorEmbed("No news found.\nAdmins can add news with the **/settings news add** command.")] - }); - - if (newsSorted.length == 0) return await interaction.followUp({ - embeds: [errorEmbed("No news found.\nAdmins can add news with the **/settings news add** command.")] - }); - - if (page > newsSorted.length) page = newsSorted.length; - if (page < 1) page = 1; - - let currentNews = newsSorted[page - 1]; - let newsEmbed = new EmbedBuilder() - .setAuthor({ name: currentNews.author, iconURL: currentNews.authorPfp ?? null }) - .setTitle(currentNews.title) - .setDescription(currentNews.body) - .setImage(currentNews.imageURL || null) - .setTimestamp(parseInt(currentNews.updatedAt)) - .setFooter({ text: `Page ${page} of ${newsSorted.length} • ID: ${currentNews.id}` }) - .setColor(genColor(200)); - - const row = new ActionRowBuilder().addComponents( - new ButtonBuilder() - .setCustomId("left") - .setEmoji("1137330341472915526") - .setStyle(ButtonStyle.Primary), - new ButtonBuilder() - .setCustomId("right") - .setEmoji("1137330125004869702") - .setStyle(ButtonStyle.Primary) - ); - - await interaction.followUp({ embeds: [newsEmbed], components: [row] }); - - const buttonCollector = interaction.channel.createMessageComponentCollector({ - filter: i => i.user.id === interaction.user.id, - time: 60000 - }); - - buttonCollector.on("collect", async i => { - if (!i.isButton()) return; - const id = i.customId; - - if (id == "left") { - page--; - if (page < 1) page = newsSorted.length; - } else if (id == "right") { - page++; - if (page > newsSorted.length) page = 1; - } - - currentNews = currentNews; - newsEmbed = newsEmbed; - - await interaction.editReply({ embeds: [newsEmbed], components: [row] }); - await i.deferUpdate(); - }); - } -} diff --git a/src/commands/serverboard.ts b/src/commands/serverboard.ts index c1c4f9d..58ce2f4 100644 --- a/src/commands/serverboard.ts +++ b/src/commands/serverboard.ts @@ -4,15 +4,10 @@ import { } from "discord.js"; import { quickSort } from "../utils/quickSort"; import { serverEmbed } from "../utils/embeds/serverEmbed"; -import { database, getNewsTable, getServerboardTable } from "../utils/database.js"; -import { QuickDB } from "quick.db"; export default class Serverboard { data: SlashCommandSubcommandBuilder; - db: QuickDB; - - constructor(db?: QuickDB) { - this.db = db; + constructor() { this.data = new SlashCommandSubcommandBuilder() .setName("serverboard") .setDescription("Shows the servers that have Nebula.") @@ -41,9 +36,9 @@ export default class Serverboard { [[...Object.values(guildsMapped)]], 0, Object.keys(guildsMapped).length - 1 - )[1][0].reverse(); + )[1]![0].reverse(); const pages = guildsSorted.length; - const argPage = interaction.options.getNumber("page", false); + const argPage = interaction.options.getNumber("page", false)!; let page = (argPage - 1 <= 0 ? 0 : argPage - 1 > pages ? pages - 1 : argPage - 1) || 0; let guild = guildsSorted[page]; diff --git a/src/commands/settings/command/list.ts b/src/commands/settings/command/list.ts deleted file mode 100644 index 75873f3..0000000 --- a/src/commands/settings/command/list.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { - SlashCommandSubcommandBuilder, EmbedBuilder, PermissionsBitField, - type ChatInputCommandInteraction -} from "discord.js"; -import { genColor } from "../../../utils/colorGen.js"; -import { errorEmbed } from "../../../utils/embeds/errorEmbed.js"; -import { QuickDB } from "quick.db"; -import { getSettingsTable } from "../../../utils/database.js"; - -export default class List { - data: SlashCommandSubcommandBuilder; - db: QuickDB; - - constructor(db?: QuickDB) { - this.db = db; - this.data = new SlashCommandSubcommandBuilder() - .setName("list") - .setDescription("Lists all disabled commands."); - } - - async run(interaction: ChatInputCommandInteraction) { - const db = this.db; - const settingsTable = await getSettingsTable(db); - const member = interaction.guild.members.cache.get(interaction.user.id); - - if (!member.permissions.has(PermissionsBitField.Flags.ManageGuild)) return await interaction.followUp({ - embeds: [errorEmbed("You need **Manage Server** permissions to list commands.")], - }); - - const disabledCommands = await settingsTable - ?.get(`${interaction.guild.id}.disabledCommands`) - .then((disabledCommands) => disabledCommands as any[] ?? []) - .catch(() => []); - - const listEmbed = new EmbedBuilder() - .setTitle("📃 • Disabled commands") - .setDescription( - !disabledCommands || disabledCommands?.length == 0 - ? "There are no disabled commands." - : disabledCommands - .map(command => { - const [commandName, subcommandName] = command.split("/"); - return `/${commandName}${subcommandName ? ` ${subcommandName}` : ""}`; - }) - .join("\n") - ) - .setColor(genColor(100)); - - return await interaction.followUp({ embeds: [listEmbed] }); - } -} diff --git a/src/commands/settings/command/toggle.ts b/src/commands/settings/command/toggle.ts deleted file mode 100644 index 912307c..0000000 --- a/src/commands/settings/command/toggle.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { - SlashCommandSubcommandBuilder, EmbedBuilder, PermissionsBitField, - type ChatInputCommandInteraction -} from "discord.js"; -import { genColor } from "../../../utils/colorGen.js"; -import Commands from "../../../handlers/commands.js"; -import { getSettingsTable } from "../../../utils/database.js"; -import { errorEmbed } from "../../../utils/embeds/errorEmbed.js"; -import { QuickDB } from "quick.db"; - -export default class Toggle { - data: SlashCommandSubcommandBuilder; - db: QuickDB; - - constructor(db?: QuickDB) { - this.db = db; - this.data = new SlashCommandSubcommandBuilder() - .setName("toggle") - .setDescription("Enables/disables a command.") - .addStringOption(option => option - .setName("command") - .setDescription("Layout: \"/topcommand (subcommand) (group)\".") - .setRequired(true) - ); - } - - async run(interaction: ChatInputCommandInteraction) { - const db = this.db; - const settingsTable = await getSettingsTable(db); - - const commands = new Commands(interaction.client); - await commands.loadCommands(); - - const commandPath = interaction.options.getString("command", true); - let [commandName, subcommandName, subcommandGroupName] = commandPath.split(" "); - commandName = commandName.replace("/", ""); - const disabledCommands = await settingsTable - ?.get(`${interaction.guild.id}.disabledCommands`) - .then(disabledCommands => disabledCommands as any[] ?? []) - .catch(() => []); - - const hasCommand = (name: string, subcommand?: string, subcommandGroup?: string) => { - return commands.commands.some(command => - command.name === name && - (!subcommand || command.options.some(opt => opt.name === subcommand)) && - (!subcommandGroup || command.options.some( - opt => opt.type === "SUB_COMMAND_GROUP" && opt.options?.some(opt => opt.name === subcommandGroup) - )) - ); - }; - - const member = interaction.guild.members.cache.get(interaction.user.id); - const isEnabled = !disabledCommands.includes(commandName); - const updatedDisabledCommands = !isEnabled - ? disabledCommands.filter((cmd) => cmd !== commandName) - : [...disabledCommands, commandName]; - - if (!member.permissions.has(PermissionsBitField.Flags.ManageGuild)) return await interaction.followUp({ - embeds: [errorEmbed("You need the **Manage Server** permission to enable commands.")], - }); - - if (!hasCommand(commandName, subcommandName, subcommandGroupName)) return await interaction.followUp({ - embeds: [errorEmbed("The specified command doesn't exist.")] - }); - - const embed = new EmbedBuilder() - .setTitle(`⌚ • ${isEnabled ? "Disabling" : "Enabling"} ${commandPath}.`) - .setDescription("The command hasn't been updated yet, we will edit this message once it has.") - .setColor(genColor(100)); - - interaction.followUp({ embeds: [embed] }); - await settingsTable.set(`${interaction.guild.id}.disabledCommands`, updatedDisabledCommands); - - commands.registerCommandsForGuild(interaction.guild, ...updatedDisabledCommands).then(() => { - embed - .setTitle(`✅ • ${isEnabled ? "Disabled" : "Enabled"} ${commandPath}.`) - .setDescription(`The command has been ${isEnabled ? "disabled." : "enabled."}`); - - interaction.editReply({ embeds: [embed] }); - }); - } -} diff --git a/src/commands/settings/leveling/block-channels.ts b/src/commands/settings/leveling/block-channels.ts deleted file mode 100644 index d46d410..0000000 --- a/src/commands/settings/leveling/block-channels.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { - SlashCommandSubcommandBuilder, EmbedBuilder, PermissionsBitField, - type ChatInputCommandInteraction, - ChannelType, - Collection, - NonThreadGuildBasedChannel -} from "discord.js"; -import { genColor } from "../../../utils/colorGen.js"; -import { errorEmbed } from "../../../utils/embeds/errorEmbed.js"; -import { getSettingsTable } from "../../../utils/database.js"; -import { QuickDB } from "quick.db"; - -export default class BlockChannels { - data: SlashCommandSubcommandBuilder; - db: QuickDB; - - constructor(db?: QuickDB) { - this.db = db; - this.data = new SlashCommandSubcommandBuilder() - .setName("block-channels") - .setDescription("Toggles blocked channels for leveling, no options = view blocked.") - .addChannelOption(option => option - .setName("channel") - .setDescription("A channel to toggle blocked status in.") - ); - } - - async run(interaction: ChatInputCommandInteraction) { - const db = this.db; - const settingsTable = await getSettingsTable(db); - - const guild = interaction.guild; - const channel = interaction.options.getChannel("channel"); - const member = interaction.guild.members.cache.get(interaction.user.id); - - if (!member.permissions.has(PermissionsBitField.Flags.ManageGuild)) { - return await interaction.followUp({ - embeds: [errorEmbed("You need **Manage Server** permissions to set a channel.")] - }); - } - - const blocked = await settingsTable?.get(`${guild.id}.leveling.blockedchannels`).then( - blocked => blocked as any[] ?? [] - ).catch(() => []); - if (!channel) { - return await interaction.followUp({ - embeds: [ - new EmbedBuilder() - .setTitle("📃 • Blocked Leveling Channels") - .setDescription( - blocked?.length == 0 ? - "There are no blocked channels." : - `${blocked?.map(id => `- <#${id}>`).join("\n")}` - ) - .setColor(genColor(100)) - ] - }); - } - - if (channel?.type != ChannelType.GuildText) { - return await interaction.followUp({ - embeds: [errorEmbed("You must provide a text channel.")] - }); - } - - const perms = (await interaction.guild.channels.fetch().then( - (channels: Collection) => channels as Collection - )).get(channel.id)?.permissionsFor(guild.members.me); - if (!perms.has(PermissionsBitField.Flags.SendMessages)) { - return await interaction.followUp({ - embeds: [errorEmbed("I don't have permission to send messages in that channel.")] - }); - } - - const isBlocked = blocked?.includes(channel.id); - await settingsTable[isBlocked ? "pull" : "push"](`${interaction.guild.id}.leveling.blockedchannels`, channel.id); - - await interaction.followUp({ - embeds: [ - new EmbedBuilder() - .setTitle("✅ • Blocked Leveling Channels") - .setDescription( - isBlocked ? - `Unblocked <#${channel.id}>.` : - `Blocked <#${channel.id}>.` - ) - .setColor(genColor(100)) - ] - }); - } -} diff --git a/src/commands/settings/leveling/channel.ts b/src/commands/settings/leveling/channel.ts deleted file mode 100644 index b7c5227..0000000 --- a/src/commands/settings/leveling/channel.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { - SlashCommandSubcommandBuilder, EmbedBuilder, PermissionsBitField, - type ChatInputCommandInteraction, - ChannelType, - Collection, - NonThreadGuildBasedChannel -} from "discord.js"; -import { genColor } from "../../../utils/colorGen.js"; -import { errorEmbed } from "../../../utils/embeds/errorEmbed.js"; -import { getSettingsTable } from "../../../utils/database.js"; -import { QuickDB } from "quick.db"; - -export default class Channel { - data: SlashCommandSubcommandBuilder; - db: QuickDB; - - constructor(db?: QuickDB) { - this.db = db; - this.data = new SlashCommandSubcommandBuilder() - .setName("channel") - .setDescription("Sets the channel for level up messages (no channel = no messages).") - .addChannelOption(option => option - .setName("channel") - .setDescription("The channel where level up messages are sent in.") - ); - } - - async run(interaction: ChatInputCommandInteraction) { - const db = this.db; - const settingsTable = await getSettingsTable(db); - - const guild = interaction.guild; - const user = interaction.user; - const channel = interaction.options.getChannel("channel"); - const member = interaction.guild.members.cache.get(user.id); - - if (!member.permissions.has(PermissionsBitField.Flags.ManageGuild)) { - return await interaction.followUp({ - embeds: [errorEmbed("You need **Manage Server** permissions to set a channel.")] - }); - } - - if (!channel) { - await settingsTable?.delete(`${guild.id}.leveling.channel`).catch(() => ""); - - return await interaction.followUp({ - embeds: [ - new EmbedBuilder() - .setTitle("✅ • Leveling Channel") - .setDescription("Level up messages will no longer be sent.") - .setColor(genColor(100)) - ] - }); - } - - if (channel?.type != ChannelType.GuildText) { - return await interaction.followUp({ - embeds: [errorEmbed("You must provide a text channel.")] - }); - } - - const perms = (await guild.channels.fetch().then( - (channels: Collection) => channels as Collection - )).get(channel.id).permissionsFor(guild.members.me); - if (!perms.has(PermissionsBitField.Flags.SendMessages)) { - return await interaction.followUp({ - embeds: [errorEmbed("I don't have permission to send messages in that channel.")] - }); - } - - await settingsTable.set(`${guild.id}.leveling.channel`, channel.id).catch(() => ""); - - await interaction.followUp({ - embeds: [ - new EmbedBuilder() - .setTitle("✅ • Leveling Channel") - .setDescription(`Level up messages will now be sent in <#${channel.id}>.`) - .setColor(genColor(100)) - ] - }); - } -} diff --git a/src/commands/settings/leveling/rewards.ts b/src/commands/settings/leveling/rewards.ts deleted file mode 100644 index 9b5fd1f..0000000 --- a/src/commands/settings/leveling/rewards.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { - SlashCommandSubcommandBuilder, EmbedBuilder, PermissionsBitField, - Role, type ChatInputCommandInteraction -} from "discord.js"; -import { genColor } from "../../../utils/colorGen.js"; -import { errorEmbed } from "../../../utils/embeds/errorEmbed.js"; -import { getSettingsTable } from "../../../utils/database.js"; -import { QuickDB } from "quick.db"; - -export type Reward = { - roleId: string, - level: number, -} - -export default class Rewards { - data: SlashCommandSubcommandBuilder; - db: QuickDB; - - constructor(db?: QuickDB) { - this.db = db; - this.data = new SlashCommandSubcommandBuilder() - .setName("rewards") - .setDescription("Sets/gets reward roles for each level -> No options = shows.") - .addNumberOption(option => option - .setName("level") - .setDescription("The level to set the reward role for. When set without role option => deletes reward.") - ) - .addRoleOption(option => option - .setName("role") - .setDescription("The role that should be awarded for the level => leaving empty = deletes reward for specified level.") - ); - } - - async run(interaction: ChatInputCommandInteraction) { - const db = this.db; - const settingsTable = await getSettingsTable(db); - const level = interaction.options.getNumber("level", false); - const inputRole = interaction.options.getRole("role", false) as Role | null; - - if (!level && !inputRole) { - const rewards = await this.getRewards(interaction.guild.id).then(rewards => rewards.sort((a, b) => a.level - b.level)) as Reward[]; - - if (rewards.length == 0) return await interaction.followUp({ - embeds: [errorEmbed("There are no rewards set for this server.")] - }); - - const rewardsEmbed = new EmbedBuilder() - .setTitle("🎁 • Rewards") - .setDescription("Here are the rewards set for this server.") - .setColor(genColor(100)); - - for (const { roleId, level } of rewards) { - if (!roleId) continue; - rewardsEmbed.addFields({ name: `Level ${level}`, value: `<@&${roleId}>` }); - } - - return await interaction.followUp({ embeds: [rewardsEmbed] }); - } else if (level && !inputRole) { - const rewards = await this.getRewards(interaction.guild.id).then(rewards => rewards.sort((a, b) => a?.level - b?.level)) as Reward[]; - - if (rewards.length == 0) return await interaction.followUp({ - embeds: [errorEmbed("There are no rewards set for this server.")] - }); - - if (!rewards.find(reward => reward?.level == level)) return await interaction.followUp({ - embeds: [errorEmbed(`There is no reward set for level ${level}.`)] - }); - - const newRewards = rewards.filter(reward => reward.level != level); - await settingsTable?.set(`${interaction.guild.id}.leveling.rewards`, newRewards).catch(() => []); - - return await interaction.followUp({ - embeds: [new EmbedBuilder() - .setTitle("🎁 • Rewards") - .setDescription(`Deleted the reward for level ${level}.`) - .setColor(genColor(100)) - ] - }); - } - - const role = await interaction.guild.roles.fetch(String(inputRole?.id)).catch(() => null); - const permissions = role?.permissions as PermissionsBitField; - - if (!role) return await interaction.followUp({ - embeds: [errorEmbed("That role doesn't exist or couldn't be loaded.")] - }); - - if (level < 0) return await interaction.followUp({ - embeds: [errorEmbed("You can't set a reward for a negative level.")] - }); - - if (level == 0) return await interaction.followUp({ - embeds: [errorEmbed("You can't set a reward for level 0.")] - }); - - if (role.position >= interaction.guild.members.me.roles.highest.position) return await interaction.followUp({ - embeds: [errorEmbed("That role is above mine.")] - }); - - if (role?.name?.includes("everyone")) return await interaction.followUp({ - embeds: [errorEmbed("I can't give out the @everyone role.")] - }); - - if ( - permissions.has(PermissionsBitField.Flags.Administrator) || - permissions.has(PermissionsBitField.Flags.ManageRoles) || - permissions.has(PermissionsBitField.Flags.ManageGuild) || - permissions.has(PermissionsBitField.Flags.ManageChannels) || - permissions.has(PermissionsBitField.Flags.ManageWebhooks) || - permissions.has(PermissionsBitField.Flags.ManageGuildExpressions) || - permissions.has(PermissionsBitField.Flags.ManageMessages) || - permissions.has(PermissionsBitField.Flags.ManageThreads) || - permissions.has(PermissionsBitField.Flags.ManageNicknames) - ) return await interaction.followUp({ - embeds: [errorEmbed("I can't give out a role with dangerous permissions.")] - }); - - const rewards = await this.getRewards(interaction.guild.id).then(rewards => rewards.sort((a, b) => a?.level - b?.level)) as Reward[]; - const newRewards = rewards.filter(reward => reward.level != level); - newRewards.push({ - roleId: role?.id, - level: level - }); - await settingsTable?.set(`${interaction.guild.id}.leveling.rewards`, newRewards).catch(() => ""); - - return await interaction.followUp({ - embeds: [new EmbedBuilder() - .setTitle("🎁 • Rewards") - .setDescription(`Set the reward for level ${level} to <@&${role?.id}>.`) - .setColor(genColor(100)) - ] - }); - } - - async getRewards(guildId: string): Promise { - const settingsTable = await getSettingsTable(this.db); - const rewards = new Promise(resolve => settingsTable - ?.get(`${guildId}.leveling.rewards`) - .then(rewards => { - if (!rewards) return resolve([]); - resolve(rewards); - }) - .catch(() => resolve([]))); - - return rewards as Promise; - } -} diff --git a/src/commands/settings/leveling/set.ts b/src/commands/settings/leveling/set.ts deleted file mode 100644 index 9018228..0000000 --- a/src/commands/settings/leveling/set.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { - SlashCommandSubcommandBuilder, EmbedBuilder, - type ChatInputCommandInteraction, -} from "discord.js"; -import { genColor } from "../../../utils/colorGen.js"; -import { errorEmbed } from "../../../utils/embeds/errorEmbed.js"; -import { getLevelingTable, getSettingsTable } from "../../../utils/database.js"; -import { QuickDB } from "quick.db"; -import { Reward } from "./rewards.js"; - -export default class Set { - data: SlashCommandSubcommandBuilder; - db: QuickDB; - - constructor(db?: QuickDB) { - this.db = db; - this.data = new SlashCommandSubcommandBuilder() - .setName("set") - .setDescription("Sets the levels for a user.") - .addUserOption(option => option - .setName("user") - .setDescription("The user to set the levels for.") - .setRequired(true) - ) - .addNumberOption(option => option - .setName("levels") - .setDescription("The amount of levels to set the user to.") - .setRequired(true) - ); - } - - async run(interaction: ChatInputCommandInteraction) { - const db = this.db; - const levelingTable = await getLevelingTable(db); - const settingsTable = await getSettingsTable(db); - - const target = interaction.options.getUser("user", true); - const level = interaction.options.getNumber("levels", true); - - if (level < 0) { - return await interaction.followUp({ - embeds: [errorEmbed("You can't set a user's levels to a negative number.")] - }); - } - - await levelingTable.set(`${interaction.guild.id}.${target.id}`, { - levels: level, - exp: 0 - }).catch(() => ""); - - const levelRewards = await settingsTable?.get(`${interaction.guild.id}.leveling.rewards`).then( - (data) => { - if (!data) return [] as Reward[]; - return data as Reward[] ?? [] as Reward[]; - } - ).catch(() => [] as Reward[]); - const members = await interaction.guild.members.fetch(); - for (const { level: rewardLevel, roleId } of levelRewards) { - const role = interaction.guild.roles.cache.get(roleId); - - if (level >= rewardLevel) { - await members.get(target.id)?.roles.add(role); - continue; - } - - await members.get(target.id)?.roles.remove(role); - } - - await interaction.followUp({ - embeds: [ - new EmbedBuilder() - .setTitle("✅ • Levels set!") - .setDescription(`Set ${target}'s levels to ${level}.`) - .setColor(genColor(100)) - ] - }); - } -} diff --git a/src/commands/settings/leveling/toggle.ts b/src/commands/settings/leveling/toggle.ts deleted file mode 100644 index 6b5c581..0000000 --- a/src/commands/settings/leveling/toggle.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { - SlashCommandSubcommandBuilder, EmbedBuilder, PermissionsBitField, - type ChatInputCommandInteraction -} from "discord.js"; -import { genColor } from "../../../utils/colorGen.js"; -import { database } from "../../../utils/database.js"; -import { errorEmbed } from "../../../utils/embeds/errorEmbed.js"; - -export default class Toggle { - data: SlashCommandSubcommandBuilder; - constructor() { - this.data = new SlashCommandSubcommandBuilder() - .setName("toggle") - .setDescription("Toggles if leveling is enabled."); - } - - async run(interaction: ChatInputCommandInteraction) { - const db = await database(); - const enabled = await db.table("settings") - ?.get(`${interaction.guild.id}.leveling.enabled`) - .then(enabled => !!enabled) - .catch(() => false); - - await db.table("settings").set(`${interaction.guild.id}.leveling.enabled`, !enabled); - - const user = await interaction.guild.members.me.fetch(); - if (!user.permissions.has(PermissionsBitField.Flags.ManageGuild)) { - return await interaction.followUp({ - embeds: [errorEmbed("You need **Manage Server** permissions to toggle leveling.")] - }); - } - - await interaction.followUp({ - embeds: [ - new EmbedBuilder() - .setTitle("✅ • Leveling toggled!") - .setDescription(`Leveling is now ${enabled ? "disabled" : "enabled"}.`) - .setColor(genColor(100)) - ] - }); - } -} diff --git a/src/commands/settings/moderation/logs.ts b/src/commands/settings/moderation/logs.ts deleted file mode 100644 index 197bfc7..0000000 --- a/src/commands/settings/moderation/logs.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { - SlashCommandSubcommandBuilder, EmbedBuilder, PermissionsBitField, - ChannelType, type ChatInputCommandInteraction -} from "discord.js"; -import { genColor } from "../../../utils/colorGen.js"; -import { errorEmbed } from "../../../utils/embeds/errorEmbed.js"; -import { QuickDB } from "quick.db"; -import { getSettingsTable } from "../../../utils/database.js"; - -export default class Logs { - data: SlashCommandSubcommandBuilder; - db: QuickDB; - - constructor(db?: QuickDB) { - this.db = db; - this.data = new SlashCommandSubcommandBuilder() - .setName("logs") - .setDescription("Sets/Remove the logs channel.") - .addChannelOption(option => option - .setName("channel") - .setDescription("Where to send logs in. Empty = deleted.") - ); - } - - async run(interaction: ChatInputCommandInteraction) { - const db = this.db; - const settingsTable = await getSettingsTable(db); - const member = interaction.guild.members.cache.get(interaction.user.id); - const specifiedChannel = interaction.options.getChannel("channel"); - - if (!member.permissions.has(PermissionsBitField.Flags.ManageGuild)) return await interaction.followUp({ - embeds: [errorEmbed("You need **Manage Server** permissions to list commands.")], - }); - - if (specifiedChannel?.type !== ChannelType.GuildText) return await interaction.followUp({ - embeds: [errorEmbed("You must provide a text channel.")] - }); - - if (!specifiedChannel?.id) await settingsTable?.delete(`${interaction.guild.id}.logChannel`); - else await settingsTable?.set(`${interaction.guild.id}.logChannel`, specifiedChannel?.id); - - const listEmbed = new EmbedBuilder() - .setTitle("📃 • Log channel") - .setDescription(`The log channel has been ${specifiedChannel?.id ? `set to <#${specifiedChannel.id}>` : "deleted"}.`) - .setColor(genColor(100)); - - return await interaction.followUp({ embeds: [listEmbed] }); - } -} diff --git a/src/commands/settings/news/channel.ts b/src/commands/settings/news/channel.ts deleted file mode 100644 index 327931d..0000000 --- a/src/commands/settings/news/channel.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { - SlashCommandSubcommandBuilder, EmbedBuilder, PermissionsBitField, - ChannelType, type ChatInputCommandInteraction -} from "discord.js"; -import { getNewsTable } from "../../../utils/database.js"; -import { genColor } from "../../../utils/colorGen.js"; -import { errorEmbed } from "../../../utils/embeds/errorEmbed.js"; -import { QuickDB } from "quick.db"; - -export default class Channel { - data: SlashCommandSubcommandBuilder; - db: QuickDB; - - constructor(db?: QuickDB) { - this.db = db; - this.data = new SlashCommandSubcommandBuilder() - .setName("channel") - .setDescription("Sets/removes (when no options) a news channel, all news you post will be sent.") - .addChannelOption(option => option - .setName("channel") - .setDescription("The channel to send news to.")) - .addRoleOption(option => option - .setName("role") - .setDescription("The role to ping when news are sent.")); - } - - async run(interaction: ChatInputCommandInteraction) { - const db = this.db; - const newsTable = await getNewsTable(db); - - const guild = interaction.guild; - const channelOption = interaction.options.getChannel("channel"); - const roleOption = interaction.options.getRole("role"); - const channel = guild.channels.cache.get(channelOption?.id); - const role = guild.roles.cache.get(roleOption?.id ?? ""); - - if (!channelOption && !roleOption) { - await newsTable.delete(`${guild.id}.channel`); - return await interaction.followUp({ - embeds: [ new EmbedBuilder().setTitle("✅ • Removed news channel.") ] - }); - } - - if (!interaction.memberPermissions.has(PermissionsBitField.Flags.ManageGuild)) return await interaction.followUp({ - embeds: [errorEmbed("You need **Manage Server** permissions to add news.")], - }); - - if (channel.type !== ChannelType.GuildText && channel.type !== ChannelType.GuildAnnouncement) return await interaction.followUp({ - embeds: [errorEmbed("The channel must be a text channel.")] - }); - - await newsTable.set(`${guild.id}.channel`, { channelId: channel.id, roleId: role?.id ?? "" }); - const embed = new EmbedBuilder() - .setTitle("✅ • News channel set!") - .setColor(genColor(100)); - - if (role) embed.setDescription(`The role <@&${role.id}> will be pinged when news are sent.`); - else embed.setDescription(`No role will be pinged when news are sent.`); - await interaction.followUp({ embeds: [embed] }); - } -} diff --git a/src/commands/settings/serverboard/invite.ts b/src/commands/settings/serverboard/invite.ts deleted file mode 100644 index 392f6b1..0000000 --- a/src/commands/settings/serverboard/invite.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { - SlashCommandSubcommandBuilder, EmbedBuilder, PermissionsBitField, - type ChatInputCommandInteraction -} from "discord.js"; -import { genColor } from "../../../utils/colorGen.js"; -import { errorEmbed } from "../../../utils/embeds/errorEmbed.js"; -import { getServerboardTable } from "../../../utils/database.js"; -import { QuickDB } from "quick.db"; - -export default class Toggle { - data: SlashCommandSubcommandBuilder; - db: QuickDB; - - constructor(db?: QuickDB) { - this.db = db; - this.data = new SlashCommandSubcommandBuilder() - .setName("invite") - .setDescription("Toggles the invite link to your server in /info serverboard (Auto generates/deletes invites)."); - } - - async run(interaction: ChatInputCommandInteraction) { - const db = this.db; - const serverbTable = await getServerboardTable(db); - const rulesChannel = interaction.guild?.rulesChannel; - const guild = interaction.guild; - const member = guild.members.cache.get(interaction.user.id); - - if (!member.permissions.has(PermissionsBitField.Flags.ManageGuild)) return await interaction.followUp({ - embeds: [errorEmbed("You need **Manage Server** permissions to add an invite.")] - }); - - const invite = await serverbTable - ?.get(`${guild.id}.invite`) - .then(invite => String(invite)) - .catch(() => ""); - - if (invite) { - let invites = await rulesChannel?.fetchInvites(); - if (!rulesChannel) invites = await guild.invites.fetch(); - if (invite) await invites.find(inv => inv.url === invite).delete("Serverboard invite disabled"); - - await serverbTable.delete(`${guild.id}.invite`); - const embed = new EmbedBuilder() - .setTitle("✅ • Invite deleted!") - .setColor(genColor(100)); - - return await interaction.followUp({ embeds: [embed] }); - } - - if (!rulesChannel) return await interaction.followUp({ - embeds: [errorEmbed("You need a **rules channel** to create an invite.")] - }); - - const newInvite = await rulesChannel.createInvite({ maxAge: 0, maxUses: 0, reason: "Serverboard invite enabled" }); - await serverbTable.set(`${guild.id}.invite`, newInvite.url); - - const embed = new EmbedBuilder() - .setTitle("✅ • Invite created!") - .setColor(genColor(100)); - - await interaction.followUp({ embeds: [embed] }); - } -} diff --git a/src/commands/settings/serverboard/reveal.ts b/src/commands/settings/serverboard/reveal.ts deleted file mode 100644 index 491213c..0000000 --- a/src/commands/settings/serverboard/reveal.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { - SlashCommandSubcommandBuilder, EmbedBuilder, PermissionsBitField, - type ChatInputCommandInteraction -} from "discord.js"; -import { genColor } from "../../../utils/colorGen.js"; -import { errorEmbed } from "../../../utils/embeds/errorEmbed.js"; -import { getServerboardTable } from "../../../utils/database.js"; -import { QuickDB } from "quick.db"; - -export default class Reveal { - data: SlashCommandSubcommandBuilder; - db: QuickDB; - - constructor(db?: QuickDB) { - this.db = db; - this.data = new SlashCommandSubcommandBuilder() - .setName("reveal") - .setDescription("Toggles the shown status of your server, community = shown, non = hidden (default)."); - } - - async run(interaction: ChatInputCommandInteraction) { - const db = this.db; - const serverbTable = await getServerboardTable(db); - - const guild = interaction.guild; - const isCommunity = guild?.rulesChannelId != null; - - const shown = await serverbTable?.get(`${guild?.id}.shown`).then( - (shown) => !!shown - ).catch(() => false); - const member = guild?.members.cache.get(interaction.user.id); - - if (!member?.permissions.has(PermissionsBitField.Flags.ManageGuild)) { - return await interaction.followUp({ - embeds: [errorEmbed("You need **Manage Server** permissions to toggle the shown status of your server.")], - }); - } - - if (!isCommunity) - await serverbTable.set(`${guild?.id}.shown`, !!shown).catch(() => { }); - else - await serverbTable.set(`${guild?.id}.shown`, !shown).catch(() => { }); - - const newShown = await serverbTable?.get(`${guild?.id}.shown`).then( - (shown) => !!shown - ).catch(() => false); - - const embed = new EmbedBuilder() - .setTitle("✅ • Shown status toggled!") - .setDescription(`Your server is now ${newShown ? "shown" : "hidden"}.`) - .setColor(genColor(100)); - - await interaction.followUp({ embeds: [embed] }); - } -} diff --git a/src/commands/user/info.ts b/src/commands/user/info.ts index a47733f..2d82336 100644 --- a/src/commands/user/info.ts +++ b/src/commands/user/info.ts @@ -41,26 +41,26 @@ export default class UserInfo { }, { name: "👥 • Member info", - value: `**Joined on** `, + value: `**Joined on** `, } ) - .setFooter({ text: `User ID: ${selectedMember.id}` }) - .setThumbnail(selectedMember.displayAvatarURL()) + .setFooter({ text: `User ID: ${selectedMember?.id}` }) + .setThumbnail(selectedMember?.displayAvatarURL()!) .setColor(genColor(200)); try { - const imageBuffer = await (await fetch(selectedMember.displayAvatarURL())).arrayBuffer(); + const imageBuffer = await (await fetch(selectedMember?.displayAvatarURL()!)).arrayBuffer(); const image = sharp(imageBuffer).toFormat("jpg"); - const { r, g, b } = (await new Vibrant(await image.toBuffer()).getPalette()).Vibrant; - embed.setColor(genRGBColor(r, g, b) as ColorResolvable); + const yes = (await new Vibrant(await image.toBuffer()).getPalette()).Vibrant; + embed.setColor(genRGBColor(yes?.r, yes?.g, yes?.b) as ColorResolvable); } catch { } - const guildRoles = interaction.guild.roles.cache.filter(role => selectedMember.roles.cache.has(role.id)); + const guildRoles = interaction.guild?.roles.cache.filter(role => selectedMember?.roles.cache.has(role.id)); const memberRoles = [...guildRoles].sort((role1, role2) => role2[1].position - role1[1].position); memberRoles.pop(); if (memberRoles.length !== 0) embed.addFields({ - name: `🎭 • ${guildRoles.filter(role => selectedMember.roles.cache.has(role.id)).size - 1} ${memberRoles.length === 1 ? "role" : "roles"}`, + name: `🎭 • ${guildRoles?.filter(role => selectedMember?.roles.cache.has(role.id)).size! - 1} ${memberRoles.length === 1 ? "role" : "roles"}`, value: `${memberRoles .slice(0, 5) .map(role => `<@&${role[1].id}>`) diff --git a/src/commands/user/level.ts b/src/commands/user/level.ts index 03aae7b..d796dfa 100644 --- a/src/commands/user/level.ts +++ b/src/commands/user/level.ts @@ -30,7 +30,6 @@ export default class Level { if (!guildExp && !guildLevel) setLevel(`${guild?.id}`, `${selectedMember?.id}`, 0, 0); const avatarURL = selectedMember?.displayAvatarURL(); - const formattedExp = guildExp?.toLocaleString("en-US"); const formattedExpUntilLevelup = Math.floor(100 * 1.25 * ((guildLevel ?? 0) + 1))?.toLocaleString("en-US"); let rewards = []; let nextReward; @@ -42,7 +41,7 @@ export default class Level { break; } - rewards.push(await guild?.roles.fetch(roleID)?.catch(() => {})); + rewards.push(await guild?.roles.fetch(`${roleID}`)?.catch(() => {})); } const embed = new EmbedBuilder() @@ -51,8 +50,8 @@ export default class Level { { name: `⚡ • Level ${guildLevel ?? 0}`, value: [ - `**Exp**: ${formattedExp ?? 0}/${formattedExpUntilLevelup} until level up`, - `**Next Level**: ${(guildLevel ?? 0) + 1}` + `**${guildExp?.toLocaleString("en-US") ?? 0}/${formattedExpUntilLevelup}** EXP until level up`, + `**Next level**: ${(guildLevel ?? 0) + 1}` ].join("\n") }, { diff --git a/src/utils/colorGen.ts b/src/utils/colorGen.ts index 438c5ff..b6544e6 100644 --- a/src/utils/colorGen.ts +++ b/src/utils/colorGen.ts @@ -28,10 +28,10 @@ export function genColor(hue: number) { * @param b Blue. * @returns Color in HEX. */ -export function genRGBColor(r: string | any[], g: string | any[], b: string | any[]) { - r = r.toString(); - g = g.toString(); - b = b.toString(); +export function genRGBColor(r: any, g: any, b: any) { + r = r.toString(16); + g = g.toString(16); + b = b.toString(16); if (r.length === 1) r = `0${r}`; if (g.length === 1) g = `0${g}`; diff --git a/src/utils/database/index.ts b/src/utils/database/index.ts index f5240a7..e815a45 100644 --- a/src/utils/database/index.ts +++ b/src/utils/database/index.ts @@ -6,10 +6,7 @@ const database = new Database("data.db", { create: true }); export function getDatabase(definition: TableDefinition) { // Create table if it doesn't exist - const defStr = Object.entries(definition.definition) - .map(([field, type]) => field.concat(" ", type)) - .join(", "); - + const defStr = Object.entries(definition.definition).map(([field, type]) => field.concat(" ", type)).join(", "); database.run(`CREATE TABLE IF NOT EXISTS ${definition.name} (${defStr});`); return database; } diff --git a/src/utils/database/levelRewards.ts b/src/utils/database/levelRewards.ts index 5588158..131b715 100644 --- a/src/utils/database/levelRewards.ts +++ b/src/utils/database/levelRewards.ts @@ -13,9 +13,9 @@ const tableDefinition = { const database = getDatabase(tableDefinition); const getQuery = database.query("SELECT * FROM levelRewards WHERE guild = $1;"); -const addQuery = database.query("INSERT INTO levelRewards (guild, role, level) VALUES (?1, ?2, ?3);"); -const updateQuery = database.query("UPDATE levelRewards SET level = $3 WHERE guild = $1 AND role = $2"); -const removeQuery = database.query("DELETE FROM levelRewards WHERE guild = $1 AND role = $2"); +const addQuery = database.query("INSERT INTO levelRewards (guild, roleID, level) VALUES (?1, ?2, ?3);"); +const updateQuery = database.query("UPDATE levelRewards SET level = $3 WHERE guild = $1 AND roleID = $2"); +const removeQuery = database.query("DELETE FROM levelRewards WHERE guild = $1 AND roleID = $2"); export function get(guildID: string) { return getQuery.all(guildID) as TypeOfDefinition[]; diff --git a/src/utils/database/levelling.ts b/src/utils/database/levelling.ts index 24449e8..361d5a9 100644 --- a/src/utils/database/levelling.ts +++ b/src/utils/database/levelling.ts @@ -13,7 +13,7 @@ const tableDefinition = { const database = getDatabase(tableDefinition); const getQuery = database.query("SELECT * FROM levelling WHERE guild = $1 AND user = $2;"); -const setQuery = database.query("UPDATE leveling SET level = $3, exp = $4 WHERE guild = $1 AND user = $2;"); +const setQuery = database.query("UPDATE levelling SET level = $3, exp = $4 WHERE guild = $1 AND user = $2;"); const insertQuery = database.query("INSERT INTO levelling (guild, user, level, exp) VALUES (?1, ?2, ?3, ?4);"); export function getLevel(guildID: string, userID: string): [number, number] { diff --git a/src/utils/multiReact.ts b/src/utils/multiReact.ts index c2f5027..48b3274 100644 --- a/src/utils/multiReact.ts +++ b/src/utils/multiReact.ts @@ -6,7 +6,6 @@ export async function multiReact(message: Message, ...reactions: string[]) { await message.react(i); continue; } - for (const reaction of i) if (reaction !== " ") await message.react(reaction); } } From b492edcbe6a1f7afe0531ccc7590a265f6d9d2f1 Mon Sep 17 00:00:00 2001 From: Serge Date: Sat, 20 Jan 2024 00:03:03 +0600 Subject: [PATCH 009/127] fixed moderation commands Co-authored-by: Mart Zielman --- bun.lockb | Bin 59108 -> 59052 bytes package.json | 13 +++-- src/commands/about.ts | 1 - src/commands/moderation/ban.ts | 23 ++++----- src/commands/moderation/delwarn.ts | 37 ++++++-------- src/commands/moderation/kick.ts | 68 +++++++++---------------- src/commands/moderation/mute.ts | 73 ++++++++++----------------- src/commands/moderation/purge.ts | 49 ++++++++---------- src/commands/moderation/unban.ts | 65 ++++++++++-------------- src/commands/moderation/unmute.ts | 56 ++++++++------------- src/commands/moderation/warn.ts | 77 +++++++++++------------------ src/commands/moderation/warns.ts | 45 ++++++----------- src/commands/news.ts | 3 +- src/commands/server/info.ts | 4 +- src/commands/server/leaderboard.ts | 25 +++------- src/commands/server/news.ts | 5 +- src/commands/server/subscribe.ts | 41 +++------------ src/commands/serverboard.ts | 12 ++--- src/commands/user/info.ts | 29 ++++++----- src/commands/user/level.ts | 25 +++++----- src/events/interactionCreate.ts | 2 +- src/events/messageCreate.ts | 6 +-- src/utils/database/news.ts | 2 +- src/utils/database/settings.ts | 9 +++- src/utils/embeds/serverEmbed.ts | 11 ++--- 25 files changed, 259 insertions(+), 422 deletions(-) diff --git a/bun.lockb b/bun.lockb index b66eff0ca37048daa75c3e1a0751ee0de4203879..837348f217dfc0b3425c0ea124d052a931390795 100644 GIT binary patch delta 7939 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 delta 8311 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 W;)$wdH2SU8kyHDq;0C&#mi0dcp{BI} diff --git a/package.json b/package.json index 008c5b3..532baa9 100644 --- a/package.json +++ b/package.json @@ -6,20 +6,19 @@ "The GitHub contributors" ], "version": "0.1.0", - "main": "./src/index.ts", + "main": "./src/bot.ts", "type": "module", "scripts": { - "start": "bun ./src/index.ts" + "start": "bun ./src/bot.ts" }, "dependencies": { "discord.js": "^14.14.1", "ms": "^2.1.3", "node-vibrant": "^3.2.1-alpha.1", - "sharp": "v0.33.0-alpha.11" + "sharp": "^0.33.2" }, "devDependencies": { - "bun-types": "0.8.1", - "typescript": "5.2.2" - }, - "trustedDependencies": ["sharp"] + "bun-types": "^1.0.23", + "typescript": "^5.3.3" + } } diff --git a/src/commands/about.ts b/src/commands/about.ts index a432eec..d6b2bb4 100644 --- a/src/commands/about.ts +++ b/src/commands/about.ts @@ -4,7 +4,6 @@ import { randomise } from "../utils/randomise"; export default class About { data: SlashCommandSubcommandBuilder; - deferred: boolean = false; constructor() { this.data = new SlashCommandSubcommandBuilder() .setName("about") diff --git a/src/commands/moderation/ban.ts b/src/commands/moderation/ban.ts index 80db033..3fe5fc3 100644 --- a/src/commands/moderation/ban.ts +++ b/src/commands/moderation/ban.ts @@ -5,11 +5,10 @@ import { } from "discord.js"; import { genColor } from "../../utils/colorGen"; import { errorEmbed } from "../../utils/embeds/errorEmbed"; -import { get } from "../../utils/database/settings"; +import { getSetting } from "../../utils/database/settings"; export default class Ban { data: SlashCommandSubcommandBuilder; - deferred: boolean = false; constructor() { this.data = new SlashCommandSubcommandBuilder() .setName("ban") @@ -28,28 +27,26 @@ export default class Ban { async run(interaction: ChatInputCommandInteraction) { const user = interaction.options.getUser("user")!; const guild = interaction.guild!; - const members = guild.members.cache!; + const members = guild.members.cache; const member = members.get(interaction.member?.user.id!)!; - const selectedMember = members.get(user.id)!; - const name = selectedMember.nickname ?? user.username; + const target = members.get(user.id)!; + const name = target.nickname ?? user.username; if (!member.permissions.has(PermissionsBitField.Flags.BanMembers)) return await interaction.reply({ embeds: [errorEmbed("You need the **Ban Members** permission to execute this command.")] }); - if (selectedMember === member) return await interaction.reply({ - embeds: [errorEmbed("You can't ban yourself.")] - }); + if (target === member) return await interaction.reply({ embeds: [errorEmbed("You can't ban yourself.")] }); - if (selectedMember.user.id === interaction.client.user.id) return await interaction.reply({ + if (target.user.id === interaction.client.user.id) return await interaction.reply({ embeds: [errorEmbed("You can't ban Nebula.")] }); - if (!selectedMember.manageable) return await interaction.reply({ + if (!target.manageable) return await interaction.reply({ embeds: [errorEmbed(`You can't ban ${name}, because they have a higher role position than Nebula.`)] }); - if (member.roles.highest.position < selectedMember.roles.highest.position) return await interaction.reply({ + if (member.roles.highest.position < target.roles.highest.position) return await interaction.reply({ embeds: [errorEmbed(`You can't ban ${name}, because they have a higher role position than you.`)] }); @@ -65,7 +62,7 @@ export default class Ban { .setFooter({ text: `User ID: ${user.id}` }) .setColor(genColor(100)); - const logChannel = get(guild.id, "log.channel"); + const logChannel = getSetting(guild.id, "log.channel"); if (logChannel) { const channel = await guild.channels.cache .get(`${logChannel}`) @@ -79,9 +76,9 @@ export default class Ban { if (channel) await channel.send({ embeds: [embed] }); } + await target.ban({ reason: reason ?? undefined }); const dmChannel = (await user.createDM().catch(() => null)) as DMChannel | null; if (dmChannel) await dmChannel.send({ embeds: [embed.setTitle("🔨 • You were banned").setColor(genColor(0))] }); - await selectedMember.ban({ reason: reason ?? undefined }); await interaction.reply({ embeds: [embed] }); } } diff --git a/src/commands/moderation/delwarn.ts b/src/commands/moderation/delwarn.ts index 0958ee1..f266208 100644 --- a/src/commands/moderation/delwarn.ts +++ b/src/commands/moderation/delwarn.ts @@ -6,7 +6,7 @@ import { import { genColor } from "../../utils/colorGen"; import { errorEmbed } from "../../utils/embeds/errorEmbed"; import { listUserModeration, removeModeration } from "../../utils/database/moderation"; -import { get } from "../../utils/database/settings"; +import { getSetting } from "../../utils/database/settings"; export default class Delwarn { data: SlashCommandSubcommandBuilder; @@ -29,46 +29,41 @@ export default class Delwarn { async run(interaction: ChatInputCommandInteraction) { const user = interaction.options.getUser("user")!; const guild = interaction.guild!; - const members = guild.members.cache!; - const member = members.get(interaction.member?.user.id!); - const selectedMember = members.get(user.id)!; - const name = selectedMember.nickname ?? user.username; + const members = guild.members.cache; + const member = members.get(interaction.member?.user.id!)!; + const target = members.get(user.id)!; + const name = target.nickname ?? user.username; const id = interaction.options.getNumber("id", true); const warns = listUserModeration(guild.id, user.id, "WARN"); const newWarns = warns.filter(warn => warn.id !== `${id}`); - if (!member?.permissions.has(PermissionsBitField.Flags.ModerateMembers)) return await interaction.followUp({ + if (!member.permissions.has(PermissionsBitField.Flags.ModerateMembers)) return await interaction.reply({ embeds: [errorEmbed("You need the **Moderate Members** permission to execute this command.")] }); - if (selectedMember === member) return await interaction.followUp({ - embeds: [errorEmbed("You can't remove a warn from yourself.")] - }); + if (target === member) return await interaction.reply({ embeds: [errorEmbed("You can't remove a warn from yourself.")] }); - if (newWarns.length === warns.length) return await interaction.followUp({ + if (newWarns.length === warns.length) return await interaction.reply({ embeds: [errorEmbed(`There is no warn with the id of ${id}.`)] }); - if (!selectedMember.manageable) return await interaction.followUp({ - embeds: [errorEmbed(`You can't unwarn ${name}, because they have a higher role position than Nebula.`)] + if (!target.manageable) return await interaction.reply({ + embeds: [errorEmbed(`You can't delete a warn from ${name}, because they have a higher role position than Nebula.`)] }); - if (member.roles.highest.position < selectedMember.roles.highest.position) return await interaction.followUp({ - embeds: [errorEmbed(`You can't unwarn ${name}, because they have a higher role position than you.`)] + if (member.roles.highest.position < target.roles.highest.position) return await interaction.reply({ + embeds: [errorEmbed(`You can't delete a warn from ${name}, because they have a higher role position than you.`)] }); const embed = new EmbedBuilder() .setAuthor({ name: `• ${user.username}`, iconURL: user.displayAvatarURL() }) .setTitle(`✅ • Removed warning`) - .setDescription([ - `**Moderator**: ${interaction.user.username}`, - `**Original reason**: ${newWarns.find(warn => warn.id === `${id}`)?.reason ?? "No reason provided"}` - ].join("\n")) + .setDescription(`**Moderator**: ${interaction.user.username}`) .setThumbnail(user.displayAvatarURL()) .setFooter({ text: `User ID: ${user.id}` }) .setColor(genColor(100)); - const logChannel = get(guild.id, "log.channel"); + const logChannel = getSetting(guild.id, "log.channel"); if (logChannel) { const channel = await guild.channels.cache .get(`${logChannel}`) @@ -82,9 +77,9 @@ export default class Delwarn { if (channel) await channel.send({ embeds: [embed] }); } + removeModeration(guild.id, `${id}`); const dmChannel = (await user.createDM().catch(() => null)) as DMChannel | null; if (dmChannel) await dmChannel.send({ embeds: [embed.setTitle("🤝 • Your warning was removed")] }); - removeModeration(guild.id, `${id}`); - await interaction.followUp({ embeds: [embed] }); + await interaction.reply({ embeds: [embed] }); } } diff --git a/src/commands/moderation/kick.ts b/src/commands/moderation/kick.ts index fdbfdef..af21a4c 100644 --- a/src/commands/moderation/kick.ts +++ b/src/commands/moderation/kick.ts @@ -3,17 +3,13 @@ import { TextChannel, DMChannel, ChannelType, type Channel, type ChatInputCommandInteraction } from "discord.js"; -import { genColor } from "../../utils/colorGen.js"; -import { errorEmbed } from "../../utils/embeds/errorEmbed.js"; -import { QuickDB } from "quick.db"; -import { getSettingsTable } from "../../utils/database.js"; +import { genColor } from "../../utils/colorGen"; +import { errorEmbed } from "../../utils/embeds/errorEmbed"; +import { getSetting } from "../../utils/database/settings"; export default class Kick { data: SlashCommandSubcommandBuilder; - db: QuickDB; - - constructor(db: QuickDB) { - this.db = db; + constructor() { this.data = new SlashCommandSubcommandBuilder() .setName("kick") .setDescription("Kicks a user.") @@ -29,29 +25,28 @@ export default class Kick { } async run(interaction: ChatInputCommandInteraction) { - const user = interaction.options.getUser("user"); - const members = interaction.guild.members.cache; - const member = members.get(interaction.member.user.id); - const selectedMember = members.get(user.id); - const name = selectedMember.nickname ?? user.username; + const user = interaction.options.getUser("user")!; + const guild = interaction.guild!; + const members = guild.members.cache!; + const member = members.get(interaction.member?.user.id!)!; + const target = members.get(user.id)!; + const name = target.nickname ?? user.username; - if (!member.permissions.has(PermissionsBitField.Flags.KickMembers)) return await interaction.followUp({ + if (!member.permissions.has(PermissionsBitField.Flags.KickMembers)) return await interaction.reply({ embeds: [errorEmbed("You need the **Kick Members** permission to execute this command.")] }); - if (selectedMember === member) return await interaction.followUp({ - embeds: [errorEmbed("You can't kick yourself.")] - }); + if (target === member) return await interaction.reply({ embeds: [errorEmbed("You can't kick yourself.")] }); - if (selectedMember.user.id === interaction.client.user.id) return await interaction.followUp({ + if (target.user.id === interaction.client.user.id) return await interaction.reply({ embeds: [errorEmbed("You can't kick Nebula.")] }); - if (!selectedMember.manageable) return await interaction.followUp({ + if (!target.manageable) return await interaction.reply({ embeds: [errorEmbed(`You can't kick ${name}, because they have a higher role position than Nebula.`)] }); - if (member.roles.highest.position < selectedMember.roles.highest.position) return await interaction.followUp({ + if (member.roles.highest.position < target.roles.highest.position) return await interaction.reply({ embeds: [errorEmbed(`You can't kick ${name}, because they have a higher role position than you.`)] }); @@ -59,35 +54,20 @@ export default class Kick { const embed = new EmbedBuilder() .setAuthor({ name: `• ${user.username}`, iconURL: user.displayAvatarURL() }) .setTitle(`✅ • Kicked <@${user.id}>`) - .setDescription([ - `**Moderator**: <@${interaction.user.id}>`, - `**Reason**: ${reason ?? "No reason provided"}` - ].join("\n")) - .setThumbnail(user.displayAvatarURL()) - .setFooter({ text: `User ID: ${user.id}` }) - .setColor(genColor(100)); - - const embedDM = new EmbedBuilder() - .setAuthor({ name: `• ${user.username}`, iconURL: user.displayAvatarURL() }) - .setTitle(`🥾 • You were kicked`) .setDescription([ `**Moderator**: ${interaction.user.username}`, `**Reason**: ${reason ?? "No reason provided"}` ].join("\n")) .setThumbnail(user.displayAvatarURL()) .setFooter({ text: `User ID: ${user.id}` }) - .setColor(genColor(0)); - - const logChannel = await (await getSettingsTable(this.db)) - ?.get(`${interaction.guild.id}.logChannel`) - .then((channel: string | null) => channel ?? null) - .catch(() => null); + .setColor(genColor(100)); + const logChannel = getSetting(guild.id, "log.channel"); if (logChannel) { - const channel = await interaction.guild.channels.cache - .get(logChannel) - .fetch() - .then((channel: Channel | null) => { + const channel = await guild.channels.cache + .get(`${logChannel}`) + ?.fetch() + .then((channel: Channel) => { if (channel.type != ChannelType.GuildText) return null; return channel as TextChannel; }) @@ -96,9 +76,9 @@ export default class Kick { if (channel) await channel.send({ embeds: [embed] }); } + await target.kick(reason ?? undefined); const dmChannel = (await user.createDM().catch(() => null)) as DMChannel | null; - if (dmChannel) await dmChannel.send({ embeds: [embedDM] }); - await selectedMember.kick(reason ?? undefined); - await interaction.followUp({ embeds: [embed] }); + if (dmChannel) await dmChannel.send({ embeds: [embed.setTitle("🥾 • You were kicked").setColor(genColor(0))] }); + await interaction.reply({ embeds: [embed] }); } } diff --git a/src/commands/moderation/mute.ts b/src/commands/moderation/mute.ts index 9ac51da..685871c 100644 --- a/src/commands/moderation/mute.ts +++ b/src/commands/moderation/mute.ts @@ -3,18 +3,14 @@ import { TextChannel, DMChannel, ChannelType, type Channel, type ChatInputCommandInteraction } from "discord.js"; -import { genColor } from "../../utils/colorGen.js"; -import { errorEmbed } from "../../utils/embeds/errorEmbed.js"; -import { getSettingsTable } from "../../utils/database.js"; -import { QuickDB } from "quick.db"; +import { genColor } from "../../utils/colorGen"; +import { errorEmbed } from "../../utils/embeds/errorEmbed"; +import { getSetting } from "../../utils/database/settings"; import ms from "ms"; export default class Mute { data: SlashCommandSubcommandBuilder; - db: QuickDB; - - constructor(db: QuickDB) { - this.db = db; + constructor() { this.data = new SlashCommandSubcommandBuilder() .setName("mute") .setDescription("Mutes a user.") @@ -35,34 +31,33 @@ export default class Mute { } async run(interaction: ChatInputCommandInteraction) { - const user = interaction.options.getUser("user"); - const duration = interaction.options.getString("duration"); - const members = interaction.guild.members.cache; - const member = members.get(interaction.member.user.id); - const selectedMember = members.get(user.id); - const name = selectedMember.nickname ?? user.username; + const user = interaction.options.getUser("user")!; + const duration = interaction.options.getString("duration")!; + const guild = interaction.guild!; + const members = guild.members.cache!; + const member = members.get(interaction.member?.user.id!)!; + const target = members.get(user.id)!; + const name = target.nickname ?? user.username; - if (!member.permissions.has(PermissionsBitField.Flags.MuteMembers)) return await interaction.followUp({ + if (!member.permissions.has(PermissionsBitField.Flags.MuteMembers)) return await interaction.reply({ embeds: [errorEmbed("You need the **Mute Members** permission to execute this command.")] }); - if (selectedMember === member) return await interaction.followUp({ - embeds: [errorEmbed("You can't mute yourself.")] - }); + if (target === member) return await interaction.reply({ embeds: [errorEmbed("You can't mute yourself.")] }); - if (selectedMember.user.bot) return await interaction.followUp({ - embeds: [errorEmbed(`You can't mute ${name}, because it's a bot.`)] + if (target.user.id === interaction.client.user.id) return await interaction.reply({ + embeds: [errorEmbed("You can't mute Nebula.")] }); - if (!selectedMember.manageable) return await interaction.followUp({ + if (!target.manageable) return await interaction.reply({ embeds: [errorEmbed(`You can't mute ${name}, because they have a higher role position than Nebula.`)] }); - if (member.roles.highest.position < selectedMember.roles.highest.position) return await interaction.followUp({ + if (member.roles.highest.position < target.roles.highest.position) return await interaction.reply({ embeds: [errorEmbed(`You can't mute ${name}, because they have a higher role position than you.`)] }); - if (!ms(duration) || ms(duration) > ms("28d")) return await interaction.followUp({ + if (!ms(duration) || ms(duration) > ms("28d")) return await interaction.reply({ embeds: [errorEmbed("The duration is invalid or is above the 28 day limit.")] }); @@ -71,7 +66,7 @@ export default class Mute { .setAuthor({ name: `• ${user.username}`, iconURL: user.displayAvatarURL() }) .setTitle(`✅ • Muted ${user.username}`) .setDescription([ - `**Moderator**: <@${member.id}>`, + `**Moderator**: ${interaction.user.username}`, `**Duration**: ${ms(ms(duration), { long: true })}`, `**Reason**: ${interaction.options.getString("reason") ?? "No reason provided"}` ].join("\n")) @@ -79,27 +74,11 @@ export default class Mute { .setThumbnail(user.displayAvatarURL()) .setColor(genColor(100)); - const embedDM = new EmbedBuilder() - .setAuthor({ name: `• ${user.username}`, iconURL: user.displayAvatarURL() }) - .setTitle(`🤐 • You were muted`) - .setDescription([ - `**Moderator**: ${member.user.username}`, - `**Duration**: ${ms(ms(duration), { long: true })}`, - `**Reason**: ${interaction.options.getString("reason") ?? "No reason provided"}` - ].join("\n")) - .setThumbnail(user.displayAvatarURL()) - .setFooter({ text: `User ID: ${user.id}` }) - .setColor(genColor(0)); - - const logChannel = await (await getSettingsTable(this.db)) - ?.get(`${interaction.guild.id}.logChannel`) - .then((channel: string | null) => channel) - .catch(() => null); - + const logChannel = getSetting(interaction.guildId!, "log.channel"); if (logChannel) { - const channel = await interaction.guild.channels.cache - .get(logChannel) - .fetch() + const channel = await guild.channels.cache + .get(`${logChannel}`) + ?.fetch() .then((channel: Channel) => { if (channel.type != ChannelType.GuildText) return null; return channel as TextChannel; @@ -109,9 +88,9 @@ export default class Mute { if (channel) await channel.send({ embeds: [embed] }); } + await target.edit({ communicationDisabledUntil: time }); const dmChannel = (await user.createDM().catch(() => null)) as DMChannel | null; - if (dmChannel) await dmChannel.send({ embeds: [embedDM] }); - await selectedMember.edit({ communicationDisabledUntil: time }); - await interaction.followUp({ embeds: [embed] }); + if (dmChannel) await dmChannel.send({ embeds: [embed.setTitle("🤐 • You were muted").setColor(genColor(0))] }); + await interaction.reply({ embeds: [embed] }); } } diff --git a/src/commands/moderation/purge.ts b/src/commands/moderation/purge.ts index a97275c..8750f9b 100644 --- a/src/commands/moderation/purge.ts +++ b/src/commands/moderation/purge.ts @@ -3,17 +3,13 @@ import { ChannelType, TextChannel, type Channel, type ChatInputCommandInteraction } from "discord.js"; -import { genColor } from "../../utils/colorGen.js"; -import { errorEmbed } from "../../utils/embeds/errorEmbed.js"; -import { getSettingsTable } from "../../utils/database.js"; -import { QuickDB } from "quick.db"; +import { genColor } from "../../utils/colorGen"; +import { errorEmbed } from "../../utils/embeds/errorEmbed"; +import { getSetting } from "../../utils/database/settings"; export default class Purge { data: SlashCommandSubcommandBuilder; - db: QuickDB; - - constructor(db: QuickDB) { - this.db = db; + constructor() { this.data = new SlashCommandSubcommandBuilder() .setName("purge") .setDescription("Purges messages.") @@ -30,27 +26,28 @@ export default class Purge { } async run(interaction: ChatInputCommandInteraction) { - const amount = interaction.options.getNumber("amount"); - const member = interaction.guild.members.cache.get(interaction.member.user.id); + const guild = interaction.guild!; + const amount = interaction.options.getNumber("amount")!; + const member = guild.members.cache.get(interaction.member?.user.id!)!; - if (amount > 100) return await interaction.followUp({ - embeds: [errorEmbed("You can only purge up to 100 messages at a time.")], + if (!member.permissions.has(PermissionsBitField.Flags.ManageMessages)) return await interaction.reply({ + embeds: [errorEmbed("You need the **Manage Messages** permission to execute this command.")], }); - if (amount < 1) return await interaction.followUp({ - embeds: [errorEmbed("You must purge at least 1 message.")], + if (amount > 100) return await interaction.reply({ + embeds: [errorEmbed("You can only purge up to 100 messages at a time.")], }); - if (!member.permissions.has(PermissionsBitField.Flags.ManageMessages)) return await interaction.followUp({ - embeds: [errorEmbed("You need the **Manage Messages** permission to execute this command.")], + if (amount < 1) return await interaction.reply({ + embeds: [errorEmbed("You must purge at least 1 message.")], }); - const channelOption = interaction.options.getChannel("channel"); - const channel = interaction.guild.channels.cache.get(interaction.channel.id ?? channelOption.id); + const channelOption = interaction.options.getChannel("channel")!; + const channel = guild.channels.cache.get(interaction.channel?.id ?? channelOption.id)!; const embed = new EmbedBuilder() .setTitle(`✅ • Purged ${amount} messages.`) .setDescription([ - `**Moderator**: <@${member.id}>`, + `**Moderator**: ${interaction.user.username}`, `**Channel**: ${channelOption ?? `<#${channel.id}>`}`, ].join("\n")) .setColor(genColor(100)); @@ -59,15 +56,11 @@ export default class Purge { ? await channel.bulkDelete(amount + 1, true) : await channel.bulkDelete(amount, true); - const logChannel = await (await getSettingsTable(this.db)) - ?.get(`${interaction.guild.id}.logChannel`) - .then((channel: string | null) => channel) - .catch(() => null); - + const logChannel = getSetting(guild.id, "log.channel"); if (logChannel) { - const channel = await interaction.guild.channels.cache - .get(logChannel) - .fetch() + const channel = await guild.channels.cache + .get(`${logChannel}`) + ?.fetch() .then((channel: Channel) => { if (channel.type != ChannelType.GuildText) return null; return channel as TextChannel; @@ -77,6 +70,6 @@ export default class Purge { if (channel) await channel.send({ embeds: [embed] }); } - await interaction.followUp({ embeds: [embed] }); + await interaction.reply({ embeds: [embed] }); } } diff --git a/src/commands/moderation/unban.ts b/src/commands/moderation/unban.ts index bbcafe3..4e1c35a 100644 --- a/src/commands/moderation/unban.ts +++ b/src/commands/moderation/unban.ts @@ -3,65 +3,50 @@ import { TextChannel, DMChannel, ChannelType, type Channel, type ChatInputCommandInteraction } from "discord.js"; -import { genColor } from "../../utils/colorGen.js"; -import { errorEmbed } from "../../utils/embeds/errorEmbed.js"; -import { getSettingsTable } from "../../utils/database.js"; -import { QuickDB } from "quick.db"; +import { genColor } from "../../utils/colorGen"; +import { errorEmbed } from "../../utils/embeds/errorEmbed"; +import { getSetting } from "../../utils/database/settings"; export default class Unban { data: SlashCommandSubcommandBuilder; - db: QuickDB; - - constructor(db: QuickDB) { - this.db = db; + constructor() { this.data = new SlashCommandSubcommandBuilder() .setName("unban") .setDescription("Unbans a user.") .addStringOption(string => string - .setName("user") + .setName("id") .setDescription("The ID of the user that you want to unban.") .setRequired(true) - ) + ); } async run(interaction: ChatInputCommandInteraction) { - const userID = interaction.options.getString("user"); - const member = interaction.guild.members.cache.get(interaction.member.user.id); - const selectedBannedMember = (interaction.guild.bans.cache.map(user => user.user)).filter(user => user.id === userID)[0]; + const id = interaction.options.getString("id")!; + const guild = interaction.guild!; + const member = guild.members.cache.get(interaction.member?.user.id!)!; + const target = (guild.bans.cache.map(user => user.user)).filter(user => user.id === id)[0]!; - if (!member.permissions.has(PermissionsBitField.Flags.BanMembers)) return await interaction.followUp({ + if (!member.permissions.has(PermissionsBitField.Flags.BanMembers)) return await interaction.reply({ embeds: [errorEmbed("You need the **Ban Members** permission to execute this command.")] }); - if (selectedBannedMember == undefined) return await interaction.followUp({ + if (target == undefined) return await interaction.reply({ embeds: [errorEmbed("You can't unban this user because they were never banned.")] }); const embed = new EmbedBuilder() - .setAuthor({ name: member.user.username, iconURL: member.user.displayAvatarURL() }) - .setTitle(`✅ • Unbanned ${member.user.username}`) - .setDescription(`**Moderator**: <@${member.user.id}>`) - .setThumbnail(selectedBannedMember.displayAvatarURL()) - .setFooter({ text: `User ID: ${userID}` }) - .setColor(genColor(100)); - - const embedDM = new EmbedBuilder() - .setAuthor({ name: member.user.username, iconURL: member.user.displayAvatarURL() }) - .setTitle(`🤝 • You were unbanned`) - .setDescription(`**Moderator**: ${member.user.username}`) - .setThumbnail(selectedBannedMember.displayAvatarURL()) - .setFooter({ text: `User ID: ${userID}` }) + .setAuthor({ name: target.username, iconURL: target.displayAvatarURL() }) + .setTitle(`✅ • Unbanned ${target.username}`) + .setDescription(`**Moderator**: ${interaction.user.username}`) + .setThumbnail(target.displayAvatarURL()) + .setFooter({ text: `User ID: ${id}` }) .setColor(genColor(100)); - const logChannel = await (await getSettingsTable(this.db)) - ?.get(`${interaction.guild.id}.logChannel`) - .then((channel: string | null) => channel) - .catch(() => null); - + const logChannel = getSetting(guild.id, "log.channel"); if (logChannel) { - const channel = await interaction.guild.channels.cache - .get(logChannel) - .fetch() + const channel = await guild.channels.cache + .get(`${logChannel}`) + ?.fetch() .then((channel: Channel) => { if (channel.type != ChannelType.GuildText) return null; return channel as TextChannel; @@ -71,9 +56,9 @@ export default class Unban { if (channel) await channel.send({ embeds: [embed] }); } - const dmChannel = (await selectedBannedMember.createDM().catch(() => null)) as DMChannel | null; - if (dmChannel) await dmChannel.send({ embeds: [embedDM] }); - await interaction.guild.members.unban(userID); - await interaction.followUp({ embeds: [embed] }); + await guild.members.unban(id); + const dmChannel = (await target.createDM().catch(() => null)) as DMChannel | null; + if (dmChannel) await dmChannel.send({ embeds: [embed.setTitle("🤝 • You were unbanned")] }); + await interaction.reply({ embeds: [embed] }); } } diff --git a/src/commands/moderation/unmute.ts b/src/commands/moderation/unmute.ts index 9215c9b..d00c2ea 100644 --- a/src/commands/moderation/unmute.ts +++ b/src/commands/moderation/unmute.ts @@ -3,17 +3,13 @@ import { TextChannel, DMChannel, ChannelType, type Channel, type ChatInputCommandInteraction } from "discord.js"; -import { genColor } from "../../utils/colorGen.js"; -import { errorEmbed } from "../../utils/embeds/errorEmbed.js"; -import { getSettingsTable } from "../../utils/database.js"; -import { QuickDB } from "quick.db"; +import { genColor } from "../../utils/colorGen"; +import { errorEmbed } from "../../utils/embeds/errorEmbed"; +import { getSetting } from "../../utils/database/settings"; export default class Unmute { data: SlashCommandSubcommandBuilder; - db: QuickDB; - - constructor(db: QuickDB) { - this.db = db; + constructor() { this.data = new SlashCommandSubcommandBuilder() .setName("unmute") .setDescription("Unmutes a user.") @@ -25,27 +21,21 @@ export default class Unmute { } async run(interaction: ChatInputCommandInteraction) { - const members = interaction.guild.members.cache; - if (!members.get(interaction.member.user.id).permissions.has(PermissionsBitField.Flags.MuteMembers)) - return await interaction.followUp({ + const guild = interaction.guild!; + const members = guild.members.cache!; + if (!members.get(interaction.member!.user.id)!.permissions.has(PermissionsBitField.Flags.MuteMembers)) + return await interaction.reply({ embeds: [errorEmbed("You need the **Mute Members** permission to execute this command.")] }); - const user = interaction.options.getUser("user"); - const embed = new EmbedBuilder() - .setAuthor({ name: `• ${members.get(user.id).nickname ?? user.username}`, iconURL: user.displayAvatarURL() }) - .setTitle(`✅ • Unmuted ${user.username}`) - .setDescription([ - `**Moderator**: <@${interaction.user.id}>`, - `**Date**: ` - ].join("\n")) - .setThumbnail(user.displayAvatarURL()) - .setFooter({ text: `User ID: ${user.id}` }) - .setColor(genColor(100)); + const user = interaction.options.getUser("user")!; + if (members.get(user.id)?.communicationDisabledUntil === null) return await interaction.reply({ + embeds: [errorEmbed("You can't unmute this user because they were never muted.")] + }); - const embedDM = new EmbedBuilder() + const embed = new EmbedBuilder() .setAuthor({ name: `• ${user.username}`, iconURL: user.displayAvatarURL() }) - .setTitle(`🤝 • You were unmuted`) + .setTitle(`✅ • Unmuted ${user.username}`) .setDescription([ `**Moderator**: ${interaction.user.username}`, `**Date**: ` @@ -54,15 +44,11 @@ export default class Unmute { .setFooter({ text: `User ID: ${user.id}` }) .setColor(genColor(100)); - const logChannel = await (await getSettingsTable(this.db)) - ?.get(`${interaction.guild.id}.logChannel`) - .then((channel: string | null) => channel) - .catch(() => null); - + const logChannel = getSetting(guild.id, "log.channel"); if (logChannel) { - const channel = await interaction.guild.channels.cache - .get(logChannel) - .fetch() + const channel = await guild.channels.cache + .get(`${logChannel}`) + ?.fetch() .then((channel: Channel) => { if (channel.type != ChannelType.GuildText) return null; return channel as TextChannel; @@ -72,9 +58,9 @@ export default class Unmute { if (channel) await channel.send({ embeds: [embed] }); } + await members.get(user.id)!.edit({ communicationDisabledUntil: null }); const dmChannel = (await user.createDM().catch(() => null)) as DMChannel | null; - if (dmChannel) await dmChannel.send({ embeds: [embedDM] }); - await members.get(user.id).edit({ communicationDisabledUntil: null }); - await interaction.followUp({ embeds: [embed] }); + if (dmChannel) await dmChannel.send({ embeds: [embed.setTitle("🤝 • You were unmuted")] }); + await interaction.reply({ embeds: [embed] }); } } diff --git a/src/commands/moderation/warn.ts b/src/commands/moderation/warn.ts index 56abc22..e0d956a 100644 --- a/src/commands/moderation/warn.ts +++ b/src/commands/moderation/warn.ts @@ -3,17 +3,14 @@ import { TextChannel, DMChannel, ChannelType, type Channel, type ChatInputCommandInteraction } from "discord.js"; -import { genColor } from "../../utils/colorGen.js"; -import { errorEmbed } from "../../utils/embeds/errorEmbed.js"; -import { QuickDB } from "quick.db"; -import { getModerationTable, getSettingsTable } from "../../utils/database.js"; +import { genColor } from "../../utils/colorGen"; +import { errorEmbed } from "../../utils/embeds/errorEmbed"; +import { getSetting } from "../../utils/database/settings"; +import { addModeration } from "../../utils/database/moderation"; export default class Warn { data: SlashCommandSubcommandBuilder; - db: QuickDB; - - constructor(db: QuickDB) { - this.db = db; + constructor() { this.data = new SlashCommandSubcommandBuilder() .setName("warn") .setDescription("Warns a user.") @@ -29,64 +26,48 @@ export default class Warn { } async run(interaction: ChatInputCommandInteraction) { - const user = interaction.options.getUser("user"); - const members = interaction.guild.members.cache; - const member = members.get(interaction.member.user.id); - const selectedMember = members.get(user.id); - const name = selectedMember.nickname ?? user.username; + const user = interaction.options.getUser("user")!; + const guild = interaction.guild!; + const members = guild.members.cache; + const member = members.get(interaction.member?.user.id!)!; + const target = members.get(user.id)!; + const name = target.nickname ?? user.username; - if (!member.permissions.has(PermissionsBitField.Flags.ModerateMembers)) return await interaction.followUp({ + if (!member.permissions.has(PermissionsBitField.Flags.ModerateMembers)) return await interaction.reply({ embeds: [errorEmbed("You need the **Moderate Members** permission to execute this command.")] }); - if (selectedMember === member) return await interaction.followUp({ embeds: [errorEmbed("You can't warn yourself.")] }); + if (target === member) return await interaction.reply({ embeds: [errorEmbed("You can't warn yourself.")] }); + + if (target.user.id === interaction.client.user.id) return await interaction.reply({ + embeds: [errorEmbed("You can't warn Nebula.")] + }); - if (!selectedMember.manageable) return await interaction.followUp({ + if (!target.manageable) return await interaction.reply({ embeds: [errorEmbed(`You can't warn ${name}, because they have a higher role position than Nebula.`)] }); - if (member.roles.highest.position < selectedMember.roles.highest.position) return await interaction.followUp({ + if (member.roles.highest.position < target.roles.highest.position) return await interaction.reply({ embeds: [errorEmbed(`You can't warn ${name}, because they have a higher role position than you.`)] }); - const newWarn = { - id: Date.now(), - userId: user.id, - moderator: member.id, - reason: interaction.options.getString("reason") ?? "No reason provided" - }; - + const reason = interaction.options.getString("reason"); const embed = new EmbedBuilder() .setAuthor({ name: `• ${user.username}`, iconURL: user.displayAvatarURL() }) .setTitle(`✅ • Warned ${user.username}`) .setDescription([ - `**Moderator**: <@${member.id}>`, - `**Reason**: ${interaction.options.getString("reason") ?? "No reason provided"}` - ].join("\n")) - .setThumbnail(user.displayAvatarURL()) - .setFooter({ text: `User ID: ${user.id}` }) - .setColor(genColor(100)); - - const embedDM = new EmbedBuilder() - .setAuthor({ name: `• ${user.username}`, iconURL: user.displayAvatarURL() }) - .setTitle("⚠️ • You were warned") - .setDescription([ - `**Moderator**: <@${member.id}>`, - `**Reason**: ${interaction.options.getString("reason") ?? "No reason provided"}` + `**Moderator**: ${interaction.user.username}`, + `**Reason**: ${reason ?? "No reason provided"}` ].join("\n")) .setThumbnail(user.displayAvatarURL()) .setFooter({ text: `User ID: ${user.id}` }) .setColor(genColor(100)); - const logChannel = await (await getSettingsTable(this.db)) - ?.get(`${interaction.guild.id}.logChannel`) - .then((channel: string | null) => channel) - .catch(() => null); - + const logChannel = getSetting(guild.id, "log.channel"); if (logChannel) { - const channel = await interaction.guild.channels.cache - .get(logChannel) - .fetch() + const channel = await guild.channels.cache + .get(`${logChannel}`) + ?.fetch() .then((channel: Channel) => { if (channel.type != ChannelType.GuildText) return null; return channel as TextChannel; @@ -95,9 +76,9 @@ export default class Warn { if (channel) await channel.send({ embeds: [embed] }); } + addModeration(guild.id, user.id, "WARN", member.id, reason ?? undefined); const dmChannel = (await user.createDM().catch(() => null)) as DMChannel | null; - if (dmChannel) await dmChannel.send({ embeds: [embedDM] }); - await (await getModerationTable(this.db))?.push(`${interaction.guild.id}.${user.id}.warns`, newWarn); - await interaction.followUp({ embeds: [embed] }); + if (dmChannel) await dmChannel.send({ embeds: [embed.setTitle("⚠️ • You were warned").setColor(genColor(0))] }); + await interaction.reply({ embeds: [embed] }); } } diff --git a/src/commands/moderation/warns.ts b/src/commands/moderation/warns.ts index 5813c09..7828399 100644 --- a/src/commands/moderation/warns.ts +++ b/src/commands/moderation/warns.ts @@ -2,23 +2,13 @@ import { SlashCommandSubcommandBuilder, EmbedBuilder, PermissionsBitField, type ChatInputCommandInteraction } from "discord.js"; -import { genColor } from "../../utils/colorGen.js"; -import { QuickDB } from "quick.db"; -import { getModerationTable } from "../../utils/database.js"; -import { errorEmbed } from "../../utils/embeds/errorEmbed.js"; - -type Warn = { - id: number; - moderator: string; - reason: string; -} +import { genColor } from "../../utils/colorGen"; +import { errorEmbed } from "../../utils/embeds/errorEmbed"; +import { listUserModeration } from "../../utils/database/moderation"; export default class Warns { data: SlashCommandSubcommandBuilder; - db: QuickDB; - - constructor(db: QuickDB) { - this.db = db; + constructor() { this.data = new SlashCommandSubcommandBuilder() .setName("warns") .setDescription("Warns of a user.") @@ -30,21 +20,14 @@ export default class Warns { } async run(interaction: ChatInputCommandInteraction) { - const member = interaction.guild.members.cache.get(interaction.member.user.id); - - if (!member.permissions.has(PermissionsBitField.Flags.ModerateMembers)) return await interaction.followUp({ - embeds: [errorEmbed("You need the **Moderate Members** permission to execute this command.")] - }); - - const user = interaction.options.getUser("user"); - const warns = await (await getModerationTable(this.db)) - .get(`${interaction.guild.id}.${user.id}.warns`) - .then(warns => { - if (!warns) return [] as Warn[]; - return warns as Warn[] ?? [] as Warn[]; - }) - .catch(() => [] as Warn[]); - + const guild = interaction.guild!; + if (!(guild.members.cache.get(interaction.member?.user.id!))?.permissions.has(PermissionsBitField.Flags.ModerateMembers)) + return await interaction.reply({ + embeds: [errorEmbed("You need the **Moderate Members** permission to execute this command.")] + }); + + const user = interaction.options.getUser("user")!; + const warns = listUserModeration(guild.id, user.id, "WARN"); const embed = new EmbedBuilder() .setAuthor({ name: `• ${user.username}`, iconURL: user.displayAvatarURL() }) .setTitle(`✅ • Warns of ${user.username}`) @@ -54,7 +37,7 @@ export default class Warns { value: [ `**Moderator**: <@${warn.moderator}>`, `**Reason**: ${warn.reason}`, - `**Date**: `, + `**Date**: ` ].join("\n") }; }) : [{ name: "No warns", value: "This user has no warns." }]) @@ -62,6 +45,6 @@ export default class Warns { .setFooter({ text: `User ID: ${user.id}` }) .setColor(genColor(100)); - await interaction.followUp({ embeds: [embed] }); + await interaction.reply({ embeds: [embed] }); } } diff --git a/src/commands/news.ts b/src/commands/news.ts index 890afd2..11b9f6c 100644 --- a/src/commands/news.ts +++ b/src/commands/news.ts @@ -45,7 +45,7 @@ export default class News { .setStyle(ButtonStyle.Primary) ); - await interaction.followUp({ embeds: [embed], components: [row] }); + await interaction.reply({ embeds: [embed], components: [row] }); interaction.channel ?.createMessageComponentCollector({ filter: i => i.user.id === interaction.user.id, time: 60000 }) .on("collect", async i => { @@ -62,7 +62,6 @@ export default class News { embed = embed; await interaction.editReply({ embeds: [embed], components: [row] }); - await i.deferUpdate(); }); } } diff --git a/src/commands/server/info.ts b/src/commands/server/info.ts index 9da46a4..6402b38 100644 --- a/src/commands/server/info.ts +++ b/src/commands/server/info.ts @@ -1,5 +1,5 @@ import { SlashCommandSubcommandBuilder, type ChatInputCommandInteraction } from "discord.js"; -import { serverEmbed } from "../../utils/embeds/serverEmbed.js"; +import { serverEmbed } from "../../utils/embeds/serverEmbed"; export default class ServerInfo { data: SlashCommandSubcommandBuilder; @@ -11,6 +11,6 @@ export default class ServerInfo { async run(interaction: ChatInputCommandInteraction) { const embed = await serverEmbed({ guild: interaction.guild!, roles: true }); - await interaction.followUp({ embeds: [embed] }); + await interaction.reply({ embeds: [embed] }); } } diff --git a/src/commands/server/leaderboard.ts b/src/commands/server/leaderboard.ts index 490e64f..45cfc4c 100644 --- a/src/commands/server/leaderboard.ts +++ b/src/commands/server/leaderboard.ts @@ -1,30 +1,19 @@ import { SlashCommandSubcommandBuilder, EmbedBuilder, type ChatInputCommandInteraction } from "discord.js"; -import { genColor } from "../../utils/colorGen.js"; -import { getLevelingTable, getSettingsTable } from "../../utils/database.js"; -import { errorEmbed } from "../../utils/embeds/errorEmbed.js"; -import { QuickDB } from "quick.db"; +import { genColor } from "../../utils/colorGen"; +import { errorEmbed } from "../../utils/embeds/errorEmbed"; +import { getSetting } from "../../utils/database/settings"; export default class Leaderboard { data: SlashCommandSubcommandBuilder; - db: QuickDB; - - constructor(db?: QuickDB) { - this.db = db; + constructor() { this.data = new SlashCommandSubcommandBuilder() .setName("leaderboard") .setDescription("Shows the server's leaderboard in levels."); } async run(interaction: ChatInputCommandInteraction) { - const settingsTable = await getSettingsTable(this.db); - const levelingTable = await getLevelingTable(this.db); - - return await interaction.followUp({ - embeds: [errorEmbed("This command is under maintenance.")] - }); - - const levelEnabled = await settingsTable?.get(`${interaction.guild.id}.leveling.enabled`).catch(() => { }); - const levels = await levelingTable?.get(`${interaction.guild.id}`).catch(() => { }); + const levelEnabled = getSetting(interaction.guild?.id!, "levelling.enabled"); + const levels = await levelingTable?.get(`${interaction.guild?.id}`).catch(() => { }); const levelKeys = Object.keys(levels) const convertLevelsAndExpToExp = (levels: any) => { const exp = Object.keys(levels).map(level => levels[level].exp); @@ -48,6 +37,6 @@ export default class Leaderboard { .setColor(genColor(200)) .setTimestamp(); - await interaction.followUp({ embeds: [embed] }); + await interaction.reply({ embeds: [embed] }); } } diff --git a/src/commands/server/news.ts b/src/commands/server/news.ts index c11db9d..925eb3d 100644 --- a/src/commands/server/news.ts +++ b/src/commands/server/news.ts @@ -24,7 +24,7 @@ export default class News { const sortedNews = (Object.values(news) as any[])?.sort((a, b) => b.createdAt - a.createdAt); let currentNews = sortedNews[page - 1]; - if (!news || !sortedNews || sortedNews.length == 0) return await interaction.followUp({ + if (!news || !sortedNews || sortedNews.length == 0) return await interaction.reply({ embeds: [errorEmbed("No news found.\nAdmins can add news with the **/settings news add** command.")] }); if (page > sortedNews.length) page = sortedNews.length; @@ -50,7 +50,7 @@ export default class News { .setStyle(ButtonStyle.Primary) ); - await interaction.followUp({ embeds: [embed], components: [row] }); + await interaction.reply({ embeds: [embed], components: [row] }); interaction.channel ?.createMessageComponentCollector({ filter: i => i.user.id === interaction.user.id, time: 60000 }) .on("collect", async i => { @@ -67,7 +67,6 @@ export default class News { embed = embed; await interaction.editReply({ embeds: [embed], components: [row] }); - await i.deferUpdate(); }); } } diff --git a/src/commands/server/subscribe.ts b/src/commands/server/subscribe.ts index 230ee60..b42106a 100644 --- a/src/commands/server/subscribe.ts +++ b/src/commands/server/subscribe.ts @@ -1,55 +1,30 @@ -import { - SlashCommandSubcommandBuilder, EmbedBuilder, DMChannel, - type ChatInputCommandInteraction -} from "discord.js"; -import { genColor } from "../../utils/colorGen.js"; -import { getNewsTable } from "../../utils/database.js"; -import { QuickDB } from "quick.db"; -import { errorEmbed } from "../../utils/embeds/errorEmbed.js"; +import { SlashCommandSubcommandBuilder, EmbedBuilder, type ChatInputCommandInteraction } from "discord.js"; +import { genColor } from "../../utils/colorGen"; export default class Subscribe { data: SlashCommandSubcommandBuilder; - db: QuickDB; - - constructor(db?: QuickDB) { - this.db = db; + constructor() { this.data = new SlashCommandSubcommandBuilder() .setName("subscribe") .setDescription("Subscribe to the news of this server."); } async run(interaction: ChatInputCommandInteraction) { - const newsTable = await getNewsTable(this.db); - const guild = interaction.guild; - const user = interaction.user; - + const guild = interaction.guild!; let subscriptions = await newsTable ?.get(`${guild.id}.subscriptions`) .then(subscriptions => subscriptions as string[] ?? [] as string[]) .catch(() => []) as string[]; if (!subscriptions) subscriptions = []; + const hasSub = subscriptions?.includes(interaction.user.id); + await newsTable[!hasSub ? "push" : "pull"](`${guild.id}.subscriptions`, interaction.user.id); - const hasSub = subscriptions?.includes(user.id); - const dmChannel = (await interaction.user.createDM().catch(() => null)) as DMChannel | null; - - if (!dmChannel) return await interaction.followUp({ - embeds: [errorEmbed("You need to **enable DMs from server members** to subscribe to the news.")] - }); - - await dmChannel?.send("You have updated the subscription status of \`" + guild.name + "\`.").catch(async () => { - return await interaction.followUp({ - embeds: [errorEmbed("You need to **enable DMs from server members** to subscribe to the news.")] - }); - }); - - await newsTable[!hasSub ? "push" : "pull"](`${guild.id}.subscriptions`, user.id); - - const subscriptionEmbed = new EmbedBuilder() + const embed = new EmbedBuilder() .setTitle(`✅ • ${hasSub ? "Unsubscribed" : "Subscribed"} ${hasSub ? "from" : "to"} ${guild.name}`) .setDescription(`You have ${hasSub ? "un" : ""}subscribed to the news of ${guild.name}.`) .setColor(genColor(100)); - await interaction.followUp({ embeds: [subscriptionEmbed] }); + await interaction.reply({ embeds: [embed] }); } } diff --git a/src/commands/serverboard.ts b/src/commands/serverboard.ts index 58ce2f4..750d644 100644 --- a/src/commands/serverboard.ts +++ b/src/commands/serverboard.ts @@ -4,6 +4,7 @@ import { } from "discord.js"; import { quickSort } from "../utils/quickSort"; import { serverEmbed } from "../utils/embeds/serverEmbed"; +import { listPublicServers } from "../utils/database/settings"; export default class Serverboard { data: SlashCommandSubcommandBuilder; @@ -19,15 +20,10 @@ export default class Serverboard { async run(interaction: ChatInputCommandInteraction) { const guildsMapped = {}; - const shownGuilds = (await (await getServerboardTable(this.db)).all().catch(() => [])) as any[]; - - for (const guild of interaction.client.guilds.cache.values()) { - const shownVal = shownGuilds?.find(shown => shown?.id == guild.id)?.value?.shown; - const isShown = shownVal == null ? null : shownVal; - - if (isShown == false) continue; - if (isShown == null && guild?.rulesChannelId != null) continue; + const shownGuilds = listPublicServers(); + for (const shownGuild of shownGuilds) { + const guild = interaction.client.guilds.cache.find(guild => (guild.id == shownGuilds)) guildsMapped[guild.memberCount + ":" + guild.id] = guild; } diff --git a/src/commands/user/info.ts b/src/commands/user/info.ts index 2d82336..e4a5f31 100644 --- a/src/commands/user/info.ts +++ b/src/commands/user/info.ts @@ -2,13 +2,12 @@ import { SlashCommandSubcommandBuilder, EmbedBuilder, type ColorResolvable, type ChatInputCommandInteraction } from "discord.js"; -import { genColor, genRGBColor } from "../../utils/colorGen.js"; +import { genColor, genRGBColor } from "../../utils/colorGen"; import Vibrant from "node-vibrant"; import sharp from "sharp"; export default class UserInfo { data: SlashCommandSubcommandBuilder; - constructor() { this.data = new SlashCommandSubcommandBuilder() .setName("info") @@ -22,13 +21,13 @@ export default class UserInfo { async run(interaction: ChatInputCommandInteraction) { const user = interaction.options.getUser("user"); const id = user ? user.id : interaction.member?.user.id; - const selectedMember = interaction.guild?.members.cache.filter(member => member.user.id === id).map(user => user)[0]; - const selectedUser = selectedMember?.user; + const target = interaction.guild?.members.cache.filter(member => member.user.id === id).map(user => user)[0]!; + const selectedUser = target.user!; let embed = new EmbedBuilder() .setAuthor({ - name: `• ${selectedMember?.nickname == null ? selectedUser?.username : selectedMember.nickname}${selectedUser?.discriminator == "0" ? "" : `#${selectedUser?.discriminator}`}`, - iconURL: selectedMember?.displayAvatarURL() + name: `• ${target.nickname == null ? selectedUser.username : target.nickname}${selectedUser.discriminator == "0" ? "" : `#${selectedUser.discriminator}`}`, + iconURL: target.displayAvatarURL() }) .setFields( { @@ -41,32 +40,32 @@ export default class UserInfo { }, { name: "👥 • Member info", - value: `**Joined on** `, + value: `**Joined on** `, } ) - .setFooter({ text: `User ID: ${selectedMember?.id}` }) - .setThumbnail(selectedMember?.displayAvatarURL()!) + .setFooter({ text: `User ID: ${target.id}` }) + .setThumbnail(target.displayAvatarURL()!) .setColor(genColor(200)); try { - const imageBuffer = await (await fetch(selectedMember?.displayAvatarURL()!)).arrayBuffer(); + const imageBuffer = await (await fetch(target.displayAvatarURL()!)).arrayBuffer(); const image = sharp(imageBuffer).toFormat("jpg"); - const yes = (await new Vibrant(await image.toBuffer()).getPalette()).Vibrant; - embed.setColor(genRGBColor(yes?.r, yes?.g, yes?.b) as ColorResolvable); + const { r, g, b } = (await new Vibrant(await image.toBuffer()).getPalette()).Vibrant!; + embed.setColor(genRGBColor(r, g, b) as ColorResolvable); } catch { } - const guildRoles = interaction.guild?.roles.cache.filter(role => selectedMember?.roles.cache.has(role.id)); + const guildRoles = interaction.guild?.roles.cache.filter(role => target.roles.cache.has(role.id))!; const memberRoles = [...guildRoles].sort((role1, role2) => role2[1].position - role1[1].position); memberRoles.pop(); if (memberRoles.length !== 0) embed.addFields({ - name: `🎭 • ${guildRoles?.filter(role => selectedMember?.roles.cache.has(role.id)).size! - 1} ${memberRoles.length === 1 ? "role" : "roles"}`, + name: `🎭 • ${guildRoles.filter(role => target.roles.cache.has(role.id)).size! - 1} ${memberRoles.length === 1 ? "role" : "roles"}`, value: `${memberRoles .slice(0, 5) .map(role => `<@&${role[1].id}>`) .join(", ")}${memberRoles.length > 5 ? ` **and ${memberRoles.length - 5} more**` : ""}`, }); - await interaction.followUp({ embeds: [embed] }); + await interaction.reply({ embeds: [embed] }); } } diff --git a/src/commands/user/level.ts b/src/commands/user/level.ts index d796dfa..5482cc4 100644 --- a/src/commands/user/level.ts +++ b/src/commands/user/level.ts @@ -2,7 +2,7 @@ import { SlashCommandSubcommandBuilder, EmbedBuilder, type ChatInputCommandInter import { genColor } from "../../utils/colorGen"; import { errorEmbed } from "../../utils/embeds/errorEmbed"; import { get as getLevelRewards } from "../../utils/database/levelRewards"; -import { get } from "../../utils/database/settings"; +import { getSetting } from "../../utils/database/settings"; import { getLevel, setLevel } from "../../utils/database/levelling"; export default class Level { @@ -18,39 +18,38 @@ export default class Level { } async run(interaction: ChatInputCommandInteraction) { - const guild = interaction.guild; - if (!get(`${guild?.id}`, "levelling.enabled")) return await interaction.followUp({ + const guild = interaction.guild!; + if (!getSetting(`${guild.id}`, "levelling.enabled")) return await interaction.reply({ embeds: [errorEmbed("Leveling is disabled for this server.")] }); const user = interaction.options.getUser("user"); const id = user ? user.id : interaction.member?.user.id; - const selectedMember = guild?.members.cache.filter(member => member.user.id === id).map(user => user)[0]; - const [guildExp, guildLevel] = getLevel(`${guild?.id}`, `${selectedMember?.id}`); - if (!guildExp && !guildLevel) setLevel(`${guild?.id}`, `${selectedMember?.id}`, 0, 0); + const target = guild.members.cache.filter(member => member.user.id === id).map(user => user)[0]!; + const [guildExp, guildLevel] = getLevel(`${guild.id}`, `${target.id}`)!; + if (!guildExp && !guildLevel) setLevel(`${guild.id}`, `${target.id}`, 0, 0); - const avatarURL = selectedMember?.displayAvatarURL(); const formattedExpUntilLevelup = Math.floor(100 * 1.25 * ((guildLevel ?? 0) + 1))?.toLocaleString("en-US"); let rewards = []; let nextReward; - for (const { roleID, level } of getLevelRewards(`${guild?.id}`)) { + for (const { roleID, level } of getLevelRewards(`${guild.id}`)) { if (guildLevel < level) { if (nextReward) break; nextReward = { roleID, level }; break; } - rewards.push(await guild?.roles.fetch(`${roleID}`)?.catch(() => {})); + rewards.push(await guild.roles.fetch(`${roleID}`)?.catch(() => { })); } const embed = new EmbedBuilder() - .setAuthor({ name: `• ${selectedMember?.user.username}`, iconURL: avatarURL }) + .setAuthor({ name: `• ${target.user.username}`, iconURL: target.displayAvatarURL() }) .setFields( { name: `⚡ • Level ${guildLevel ?? 0}`, value: [ - `**${guildExp?.toLocaleString("en-US") ?? 0}/${formattedExpUntilLevelup}** EXP until level up`, + `**${guildExp.toLocaleString("en-US") ?? 0}/${formattedExpUntilLevelup}** EXP until level up`, `**Next level**: ${(guildLevel ?? 0) + 1}` ].join("\n") }, @@ -62,10 +61,10 @@ export default class Level { ].join("\n") } ) - .setThumbnail(avatarURL || null) + .setThumbnail(target.displayAvatarURL()) .setTimestamp() .setColor(genColor(200)); - await interaction.followUp({ embeds: [embed] }); + await interaction.reply({ embeds: [embed] }); } } diff --git a/src/events/interactionCreate.ts b/src/events/interactionCreate.ts index 6769d37..5eef580 100644 --- a/src/events/interactionCreate.ts +++ b/src/events/interactionCreate.ts @@ -29,7 +29,7 @@ export default { if (interaction.isChatInputCommand()) { const command = await getCommand(interaction, interaction.options); if (!command) return; - if (command?.deferred ?? true) await interaction.deferReply(); + if (command.deferred === true) await interaction.deferReply(); command.run(interaction); } else if (interaction.isAutocomplete()) { const command = await getCommand(interaction, interaction.options); diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts index 5357754..52ce580 100644 --- a/src/events/messageCreate.ts +++ b/src/events/messageCreate.ts @@ -3,7 +3,7 @@ import { pathToFileURL } from "url"; import { join } from "path"; import { readdirSync } from "fs"; import { genColor } from "../utils/colorGen"; -import { get } from "../utils/database/settings"; +import { getSetting } from "../utils/database/settings"; import { getLevel, setLevel } from "../utils/database/levelling"; import { get as getLevelRewards } from "../utils/database/levelRewards"; @@ -23,9 +23,9 @@ export default { new (await import(pathToFileURL(join(eventsPath, easterEggFile)).toString())).default().run(message, ...message.content); // Levelling - const levelChannelId = get(guild.id, "levelling.channel"); + const levelChannelId = getSetting(guild.id, "levelling.channel"); if (!levelChannelId) return; - if (!get(guild.id, "levelling.enabled")) return; + if (!getSetting(guild.id, "levelling.enabled")) return; const [guildExp, guildLevel] = getLevel(guild.id, author.id); const [globalExp, globalLevel] = getLevel("0", author.id); diff --git a/src/utils/database/news.ts b/src/utils/database/news.ts index 3288606..29da269 100644 --- a/src/utils/database/news.ts +++ b/src/utils/database/news.ts @@ -19,7 +19,7 @@ const definition = { } satisfies TableDefinition; const database = getDatabase(definition); -const addQuery = database.query(""); +const addQuery = database.query("INSERT INTO news (guild, title, body, imageURL, author, authorPFP, createdAt, updatedAt, messageID, categoryID, id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11);"); const listAllQuery = database.query("SELECT * FROM news WHERE guild = $1;"); const listCategoryQuery = database.query("SELECT * FROM news WHERE guild = $1 AND categoryID = $2;"); const updateQuery = database.query("UPDATE news SET title = $2, body = $3, imageURL = $4 WHERE id = $1"); diff --git a/src/utils/database/settings.ts b/src/utils/database/settings.ts index e381d3f..3215c35 100644 --- a/src/utils/database/settings.ts +++ b/src/utils/database/settings.ts @@ -23,18 +23,23 @@ const settingsDefinition = { export const settingKeys = Object.keys(settingsDefinition) as (keyof typeof settingsDefinition)[]; const database = getDatabase(tableDefinition); const getQuery = database.query("SELECT * FROM settings WHERE guild = $1 AND key = $2;"); +const listPublicQuery = database.query("SELECT * FROM settings WHERE serverboard.shown = TRUE;"); const setQuery = database.query("UPDATE settings SET value = $3 WHERE guild = $1 AND key = $2;"); -export function get(guild: string, key: K): TypeOfKey | null { +export function getSetting(guild: string, key: K): TypeOfKey | null { let res = getQuery.all(guild, key) as TypeOfDefinition[]; if (res.length == 0) return null; if (settingsDefinition[key] == "TEXT") return res[0].value as TypeOfKey; return JSON.parse(res[0].value); } -export function set(guild: string, key: K, value: TypeOfKey) { +export function setSetting(guild: string, key: K, value: TypeOfKey) { setQuery.run(guild, key, JSON.stringify(value)); } +export function listPublicServers() { + return (listPublicQuery.all() as TypeOfDefinition[]).map(entry => (entry.guild)); +} + // Utility type type TypeOfKey = SqlType<(typeof settingsDefinition)[T]>; diff --git a/src/utils/embeds/serverEmbed.ts b/src/utils/embeds/serverEmbed.ts index e1402d6..33e9733 100644 --- a/src/utils/embeds/serverEmbed.ts +++ b/src/utils/embeds/serverEmbed.ts @@ -1,7 +1,6 @@ import { EmbedBuilder, type ColorResolvable, type Guild } from "discord.js"; import { genColor, genRGBColor } from "../colorGen"; -import { database, getServerboardTable } from "../database.js"; -import { get } from "../database/settings"; +import { getSetting } from "../database/settings"; import Vibrant from "node-vibrant"; import sharp from "sharp"; @@ -25,10 +24,10 @@ export async function serverEmbed(options: Options) { const pages = options.pages; const guild = options.guild; const { premiumTier: boostTier, premiumSubscriptionCount: boostCount } = guild; - const invite = get(guild.id, "serverboard.inviteLink"); + const invite = getSetting(guild.id, "serverboard.inviteLink"); const members = guild.members.cache; const boosters = members.filter(member => member.premiumSince); - const onlineMembers = members.filter(member => ["online", "dnd", "idle"].includes(member.presence?.status)).size; + const onlineMembers = members.filter(member => ["online", "dnd", "idle"].includes(member.presence?.status!)).size; const bots = members.filter(member => member.user.bot); const formattedUserCount = (guild.memberCount - bots.size)?.toLocaleString("en-US"); @@ -59,9 +58,9 @@ export async function serverEmbed(options: Options) { .setColor(genColor(200)); try { - const imageBuffer = await (await fetch(guild.iconURL())).arrayBuffer(); + const imageBuffer = await (await fetch(guild.iconURL()!)).arrayBuffer(); const image = sharp(imageBuffer).toFormat("jpg"); - const { r, g, b } = (await new Vibrant(await image.toBuffer()).getPalette()).Vibrant; + const { r, g, b } = (await new Vibrant(await image.toBuffer()).getPalette()).Vibrant!; embed.setColor(genRGBColor(r, g, b) as ColorResolvable); } catch {} From 7954e0f03799a70cdea15cbf24dd04176b704441 Mon Sep 17 00:00:00 2001 From: froxcey Date: Sat, 20 Jan 2024 03:39:11 +0800 Subject: [PATCH 010/127] Fix serverboard query --- src/utils/database/settings.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/utils/database/settings.ts b/src/utils/database/settings.ts index 3215c35..c0e3a88 100644 --- a/src/utils/database/settings.ts +++ b/src/utils/database/settings.ts @@ -22,8 +22,9 @@ const settingsDefinition = { export const settingKeys = Object.keys(settingsDefinition) as (keyof typeof settingsDefinition)[]; const database = getDatabase(tableDefinition); + const getQuery = database.query("SELECT * FROM settings WHERE guild = $1 AND key = $2;"); -const listPublicQuery = database.query("SELECT * FROM settings WHERE serverboard.shown = TRUE;"); +const listPublicQuery = database.query("SELECT * FROM settings WHERE key = 'serverboard.shown' AND value = 'true';"); const setQuery = database.query("UPDATE settings SET value = $3 WHERE guild = $1 AND key = $2;"); export function getSetting(guild: string, key: K): TypeOfKey | null { From 94cf212a4b8fbc533687db7968e4f24d86187e6a Mon Sep 17 00:00:00 2001 From: froxcey Date: Sat, 20 Jan 2024 16:15:35 +0800 Subject: [PATCH 011/127] Fix import --- src/events/interactionCreate.ts | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/events/interactionCreate.ts b/src/events/interactionCreate.ts index 5eef580..9cee018 100644 --- a/src/events/interactionCreate.ts +++ b/src/events/interactionCreate.ts @@ -1,17 +1,26 @@ -import type { CommandInteraction, Client, AutocompleteInteraction } from "discord.js"; +import type { + CommandInteraction, + Client, + AutocompleteInteraction, +} from "discord.js"; import { pathToFileURL } from "url"; import { join } from "path"; -async function getCommand(interaction: CommandInteraction | AutocompleteInteraction, options: any): Promise { +async function getCommand( + interaction: CommandInteraction | AutocompleteInteraction, + options: any, +): Promise { const commandName = interaction.commandName; const subcommandName = options.getSubcommand(false); const commandGroupName = options.getSubcommandGroup(false); const commandImportPath = join( join(process.cwd(), "src", "commands"), - `${subcommandName ? `${commandName}/${commandGroupName ? `${commandGroupName}/${subcommandName}` : subcommandName}` : commandName}.ts` + `${subcommandName ? `${commandName}/${commandGroupName ? `${commandGroupName}/${subcommandName}` : subcommandName}` : commandName}.ts`, ); - return new (await import(pathToFileURL(commandImportPath).toString())); + return new ( + await import(pathToFileURL(commandImportPath).toString()) + ).default(); } export default { @@ -38,5 +47,5 @@ export default { command.autocomplete(interaction); } } - } -} + }, +}; From afc7db91b47ce516ed81e4f6213934c011162655 Mon Sep 17 00:00:00 2001 From: Serge Date: Sat, 20 Jan 2024 16:11:17 +0600 Subject: [PATCH 012/127] re-added welcome/goodbye messages --- src/commands/about.ts | 2 +- src/commands/server/leaderboard.ts | 2 +- src/commands/server/subscribe.ts | 30 ----------------- src/commands/serverboard.ts | 35 ++++++-------------- src/commands/user/info.ts | 2 +- src/events/guildMemberAdd.ts | 40 +++++++++++++++++++++++ src/events/guildMemberRemove.ts | 40 +++++++++++++++++++++++ src/events/interactionCreate.ts | 15 ++------- src/utils/embeds/serverEmbed.ts | 3 -- src/utils/sendChannelNews.ts | 52 ------------------------------ src/utils/sendSubscribedNews.ts | 34 ------------------- 11 files changed, 95 insertions(+), 160 deletions(-) delete mode 100644 src/commands/server/subscribe.ts create mode 100644 src/events/guildMemberAdd.ts create mode 100644 src/events/guildMemberRemove.ts delete mode 100644 src/utils/sendChannelNews.ts delete mode 100644 src/utils/sendSubscribedNews.ts diff --git a/src/commands/about.ts b/src/commands/about.ts index d6b2bb4..a5fb60a 100644 --- a/src/commands/about.ts +++ b/src/commands/about.ts @@ -22,7 +22,7 @@ export default class About { name: "📃 • General", value: [ "**Version** 0.1, *Dasshubodo update*", - `**${guilds.size}** guild${guilds.size === 1 ? "" : "s"} • **${shards}** shard${shards === 1 ? "" : "s"}` + `**${guilds.size}** guild${guilds.size === 1 ? "" : "s"} ${shards == undefined ? "" : `• **${shards}** shard${shards === 1 ? "" : "s"}`}` ].join("\n") }, { diff --git a/src/commands/server/leaderboard.ts b/src/commands/server/leaderboard.ts index 45cfc4c..6de80b2 100644 --- a/src/commands/server/leaderboard.ts +++ b/src/commands/server/leaderboard.ts @@ -20,7 +20,7 @@ export default class Leaderboard { return exp.reduce((a, b) => a + b); }; - if (!levelEnabled) return await interaction.followUp({ + if (!levelEnabled) return await interaction.reply({ embeds: [errorEmbed("Leveling is disabled for this server.")] }); diff --git a/src/commands/server/subscribe.ts b/src/commands/server/subscribe.ts deleted file mode 100644 index b42106a..0000000 --- a/src/commands/server/subscribe.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { SlashCommandSubcommandBuilder, EmbedBuilder, type ChatInputCommandInteraction } from "discord.js"; -import { genColor } from "../../utils/colorGen"; - -export default class Subscribe { - data: SlashCommandSubcommandBuilder; - constructor() { - this.data = new SlashCommandSubcommandBuilder() - .setName("subscribe") - .setDescription("Subscribe to the news of this server."); - } - - async run(interaction: ChatInputCommandInteraction) { - const guild = interaction.guild!; - let subscriptions = await newsTable - ?.get(`${guild.id}.subscriptions`) - .then(subscriptions => subscriptions as string[] ?? [] as string[]) - .catch(() => []) as string[]; - - if (!subscriptions) subscriptions = []; - const hasSub = subscriptions?.includes(interaction.user.id); - await newsTable[!hasSub ? "push" : "pull"](`${guild.id}.subscriptions`, interaction.user.id); - - const embed = new EmbedBuilder() - .setTitle(`✅ • ${hasSub ? "Unsubscribed" : "Subscribed"} ${hasSub ? "from" : "to"} ${guild.name}`) - .setDescription(`You have ${hasSub ? "un" : ""}subscribed to the news of ${guild.name}.`) - .setColor(genColor(100)); - - await interaction.reply({ embeds: [embed] }); - } -} diff --git a/src/commands/serverboard.ts b/src/commands/serverboard.ts index 750d644..9d0a352 100644 --- a/src/commands/serverboard.ts +++ b/src/commands/serverboard.ts @@ -20,10 +20,8 @@ export default class Serverboard { async run(interaction: ChatInputCommandInteraction) { const guildsMapped = {}; - const shownGuilds = listPublicServers(); - - for (const shownGuild of shownGuilds) { - const guild = interaction.client.guilds.cache.find(guild => (guild.id == shownGuilds)) + for (const shownGuild of listPublicServers()) { + const guild = interaction.client.guilds.cache.find(guild => (guild.id == shownGuild)); guildsMapped[guild.memberCount + ":" + guild.id] = guild; } @@ -33,19 +31,12 @@ export default class Serverboard { 0, Object.keys(guildsMapped).length - 1 )[1]![0].reverse(); + const pages = guildsSorted.length; const argPage = interaction.options.getNumber("page", false)!; let page = (argPage - 1 <= 0 ? 0 : argPage - 1 > pages ? pages - 1 : argPage - 1) || 0; - let guild = guildsSorted[page]; - let subs = await (await getNewsTable(this.db)) - ?.get(`${guild.id}.subscriptions`) - .then((subs: string | any[]) => subs?.length > 0 ? subs as string[] : [] as string[]) - .catch(() => [] as string[]); - - let embed = await serverEmbed({ - guild, page: page + 1, pages, showInvite: true, showSubs: subs.length > 0, subs: subs.length - }); + let embed = await serverEmbed({ guild, page: page + 1, pages, showInvite: true }); const row = new ActionRowBuilder().addComponents( new ButtonBuilder() @@ -58,31 +49,23 @@ export default class Serverboard { .setStyle(ButtonStyle.Primary) ); - await interaction.followUp({ embeds: [embed], components: [row] }); + await interaction.reply({ embeds: [embed], components: [row] }); interaction.channel ?.createMessageComponentCollector({ filter: i => i.user.id === interaction.user.id, time: 60000 }) - .on("collect", async (interaction: ButtonInteraction) => { - let subs; - - const subscriptions = await (await database()).table("subscriptions").all(); - switch (interaction.customId) { + .on("collect", async (i: ButtonInteraction) => { + switch (i.customId) { case "left": page--; if (page < 0) page = pages - 1; - subs = subscriptions.filter((sub: { value: string[]; }) => (sub.value as string[] ?? [] as string[]).includes(guild.id)); - break; case "right": page++; if (page >= pages) page = 0; - subs = subscriptions.filter((sub: { value: string[]; }) => (sub.value as string[]).includes(guild.id)); - break; } guild = guildsSorted[page]; - embed = await serverEmbed({ guild, page: page + 1, pages, showInvite: true, showSubs: subs.length > 0, subs: subs.length }); + embed = await serverEmbed({ guild, page: page + 1, pages, showInvite: true }); - interaction.message.edit({ embeds: [embed], components: [row] }); - interaction.deferUpdate(); + i.message.edit({ embeds: [embed], components: [row] }); }); } } diff --git a/src/commands/user/info.ts b/src/commands/user/info.ts index e4a5f31..f0d7980 100644 --- a/src/commands/user/info.ts +++ b/src/commands/user/info.ts @@ -52,7 +52,7 @@ export default class UserInfo { const image = sharp(imageBuffer).toFormat("jpg"); const { r, g, b } = (await new Vibrant(await image.toBuffer()).getPalette()).Vibrant!; embed.setColor(genRGBColor(r, g, b) as ColorResolvable); - } catch { } + } catch {} const guildRoles = interaction.guild?.roles.cache.filter(role => target.roles.cache.has(role.id))!; const memberRoles = [...guildRoles].sort((role1, role2) => role2[1].position - role1[1].position); diff --git a/src/events/guildMemberAdd.ts b/src/events/guildMemberAdd.ts new file mode 100644 index 0000000..c045f74 --- /dev/null +++ b/src/events/guildMemberAdd.ts @@ -0,0 +1,40 @@ +import { + EmbedBuilder, type Client, type GuildMember, + type TextChannel, type ColorResolvable +} from "discord.js"; +import { genColor, genRGBColor } from "../utils/colorGen"; +import Vibrant from "node-vibrant"; +import sharp from "sharp"; + +export default { + name: "guildMemberAdd", + event: class GuildMemberAdd { + client: Client; + + constructor(client: Client) { + this.client = client; + } + + async run(member: GuildMember) { + const id = "1079612083307548704"; + const channel = await member.guild.channels.cache.find(channel => channel.id === id)!.fetch() as TextChannel; + const avatarURL = member.displayAvatarURL(); + const embed = new EmbedBuilder() + .setAuthor({ name: `• ${member.nickname == null ? member.user.username : member.nickname}`, iconURL: avatarURL }) + .setTitle("Welcome!") + .setDescription(`Enjoy your stay in **${member.guild.name}**!`) + .setFooter({ text: `User ID: ${member.id}` }) + .setThumbnail(avatarURL) + .setColor(genColor(200)); + + try { + const imageBuffer = await (await fetch(avatarURL)).arrayBuffer(); + const image = sharp(imageBuffer).toFormat("jpg"); + const { r, g, b } = (await new Vibrant(await image.toBuffer()).getPalette()).Vibrant!; + embed.setColor(genRGBColor(r, g, b) as ColorResolvable); + } catch {} + + await channel.send({ embeds: [embed] }); + } + } +} diff --git a/src/events/guildMemberRemove.ts b/src/events/guildMemberRemove.ts new file mode 100644 index 0000000..3f251dc --- /dev/null +++ b/src/events/guildMemberRemove.ts @@ -0,0 +1,40 @@ +import { + EmbedBuilder, type Client, type GuildMember, + type TextChannel, type ColorResolvable +} from "discord.js"; +import { genColor, genRGBColor } from "../utils/colorGen"; +import Vibrant from "node-vibrant"; +import sharp from "sharp"; + +export default { + name: "guildMemberRemove", + event: class GuildMemberRemove { + client: Client; + + constructor(client: Client) { + this.client = client; + } + + async run(member: GuildMember) { + const id = "1079612083307548704"; + const channel = await member.guild.channels.cache.find(channel => channel.id === id)!.fetch() as TextChannel; + const avatarURL = member.displayAvatarURL(); + const embed = new EmbedBuilder() + .setAuthor({ name: `• ${member.nickname == null ? member.user.username : member.nickname}`, iconURL: avatarURL }) + .setTitle("Goodbye!") + .setDescription(`**@${member.user.username}** has left the server 😥`) + .setFooter({ text: `User ID: ${member.id}` }) + .setThumbnail(avatarURL) + .setColor(genColor(200)); + + try { + const imageBuffer = await (await fetch(avatarURL)).arrayBuffer(); + const image = sharp(imageBuffer).toFormat("jpg"); + const { r, g, b } = (await new Vibrant(await image.toBuffer()).getPalette()).Vibrant!; + embed.setColor(genRGBColor(r, g, b) as ColorResolvable); + } catch {} + + await channel.send({ embeds: [embed] }); + } + } +} diff --git a/src/events/interactionCreate.ts b/src/events/interactionCreate.ts index 9cee018..589d2ae 100644 --- a/src/events/interactionCreate.ts +++ b/src/events/interactionCreate.ts @@ -1,15 +1,8 @@ -import type { - CommandInteraction, - Client, - AutocompleteInteraction, -} from "discord.js"; +import type { CommandInteraction, Client, AutocompleteInteraction } from "discord.js"; import { pathToFileURL } from "url"; import { join } from "path"; -async function getCommand( - interaction: CommandInteraction | AutocompleteInteraction, - options: any, -): Promise { +async function getCommand(interaction: CommandInteraction | AutocompleteInteraction, options: any): Promise { const commandName = interaction.commandName; const subcommandName = options.getSubcommand(false); const commandGroupName = options.getSubcommandGroup(false); @@ -18,9 +11,7 @@ async function getCommand( `${subcommandName ? `${commandName}/${commandGroupName ? `${commandGroupName}/${subcommandName}` : subcommandName}` : commandName}.ts`, ); - return new ( - await import(pathToFileURL(commandImportPath).toString()) - ).default(); + return new (await import(pathToFileURL(commandImportPath).toString())).default(); } export default { diff --git a/src/utils/embeds/serverEmbed.ts b/src/utils/embeds/serverEmbed.ts index 33e9733..9384d22 100644 --- a/src/utils/embeds/serverEmbed.ts +++ b/src/utils/embeds/serverEmbed.ts @@ -10,8 +10,6 @@ type Options = { showInvite?: boolean page?: number pages?: number - showSubs?: boolean - subs?: number }; /** @@ -46,7 +44,6 @@ export async function serverEmbed(options: Options) { `**Owned by** ${(await guild.fetchOwner()).user.displayName}`, `**Created on** ` ]; - if (options.showSubs) generalValues.push(`**Subscribers**: ${options.subs}`); if (options.showInvite && invite === null) generalValues.push(`**Invite link**: ${invite}`); const embed = new EmbedBuilder() diff --git a/src/utils/sendChannelNews.ts b/src/utils/sendChannelNews.ts deleted file mode 100644 index f9e724e..0000000 --- a/src/utils/sendChannelNews.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { - EmbedBuilder, Guild, Role, - TextChannel -} from "discord.js"; -import { genColor } from "./colorGen"; - -export type News = { - title: string - body: string - imageURL: string - author: string - authorPfp: string - createdAt: string - updatedAt: string - messageId?: string -} - -export async function sendChannelNews(guild: Guild, news: News, id: string) { - const db = await database(); - const newsTable = await getNewsTable(db); - - const subscribedChannel = await newsTable.get(`${guild.id}.channel`).then( - (channel: { channelId: string | null; roleId: string | null; }) => channel as { channelId: string | null, roleId: string | null } - ).catch(() => { - return { - channelId: null as string | null, - roleId: null as string | null - }; - }); - - if (!subscribedChannel) return; - const channel = subscribedChannel.channelId; - const channelToSend = guild.channels.cache.get(channel) as TextChannel; - if (!channelToSend) return; - - const role = subscribedChannel.roleId; - let roleToSend: Role | undefined; - if (role) roleToSend = guild.roles.cache.get(role); - - const embed = new EmbedBuilder() - .setAuthor({ name: news.author, iconURL: news.authorPfp ?? null }) - .setTitle(news.title) - .setDescription(news.body) - .setImage(news.imageURL || null) - .setTimestamp(parseInt(news.updatedAt)) - .setFooter({ text: `Latest news from ${guild.name}` }) - .setColor(genColor(200)); - - const message = await channelToSend.send({ embeds: [embed], content: roleToSend ? `<@&${roleToSend.id}>` : undefined }); - await newsTable.set(`${guild.id}.news.${id}.messageId`, message.id); - return; -} diff --git a/src/utils/sendSubscribedNews.ts b/src/utils/sendSubscribedNews.ts deleted file mode 100644 index ed90580..0000000 --- a/src/utils/sendSubscribedNews.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { DMChannel, EmbedBuilder, Guild } from "discord.js"; -import { genColor } from "./colorGen"; - -export type News = { - title: string - body: string - imageURL: string - author: string - authorPfp: string - createdAt: string - updatedAt: string - messageId?: string -} - -export async function sendSubscribedNews(guild: Guild, news: News) { - const subscriptions = await (await getNewsTable(await database())) - .get(`${guild.id}.subscriptions`) - .then((subscriptions: string[]) => subscriptions as string[] ?? [] as string[]); - - const subscribed = (await guild.members.fetch()).filter(member => subscriptions.includes(member.id)); - const memberDMs = (await Promise.all(subscribed.map(member => member.createDM().catch(() => null)))) as DMChannel[] | null; - const memberDMsOpen = memberDMs?.filter(dm => dm !== null); - const embed = new EmbedBuilder() - .setAuthor({ name: news.author, iconURL: news.authorPfp ?? null }) - .setTitle(news.title) - .setDescription(news.body) - .setImage(news.imageURL || null) - .setTimestamp(parseInt(news.updatedAt)) - .setFooter({ text: `Latest news from ${guild.name}` }) - .setColor(genColor(200)); - - await Promise.all(memberDMsOpen?.map(dm => dm.send({ embeds: [embed] }).catch(() => null))); - return; -} From 25640c8413a48e9068d341a166d1f8bf90dbce7d Mon Sep 17 00:00:00 2001 From: Serge Date: Sun, 21 Jan 2024 00:03:17 +0600 Subject: [PATCH 013/127] errory serverboard.ts --- bun.lockb | Bin 59052 -> 66484 bytes package.json | 3 ++- src/commands/moderation/warns.ts | 2 +- src/commands/serverboard.ts | 16 +++++++++------- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/bun.lockb b/bun.lockb index 837348f217dfc0b3425c0ea124d052a931390795..427d99330868e24062d5c693a9c807f628a4ddbc 100644 GIT binary patch delta 14999 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 OhcCWa|1q5_n)g45^u3t? delta 10628 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, `**Reason**: ${warn.reason}`, - `**Date**: ` + `**Date**: ` ].join("\n") }; }) : [{ name: "No warns", value: "This user has no warns." }]) diff --git a/src/commands/serverboard.ts b/src/commands/serverboard.ts index 9d0a352..25e5284 100644 --- a/src/commands/serverboard.ts +++ b/src/commands/serverboard.ts @@ -1,6 +1,7 @@ import { SlashCommandSubcommandBuilder, ButtonBuilder, ActionRowBuilder, - ButtonStyle, ButtonInteraction, type ChatInputCommandInteraction + ButtonStyle, ButtonInteraction, type Guild, + type ChatInputCommandInteraction } from "discord.js"; import { quickSort } from "../utils/quickSort"; import { serverEmbed } from "../utils/embeds/serverEmbed"; @@ -19,10 +20,10 @@ export default class Serverboard { } async run(interaction: ChatInputCommandInteraction) { - const guildsMapped = {}; + const guildsMapped: Record = {}; for (const shownGuild of listPublicServers()) { - const guild = interaction.client.guilds.cache.find(guild => (guild.id == shownGuild)); - guildsMapped[guild.memberCount + ":" + guild.id] = guild; + const guild = interaction.client.guilds.cache.find(guild => (guild.id === `${shownGuild}`))!; + guildsMapped[`${guild.memberCount}:${guild.id}`] = guild; } const guildsSorted = quickSort( @@ -31,6 +32,7 @@ export default class Serverboard { 0, Object.keys(guildsMapped).length - 1 )[1]![0].reverse(); + console.log(guildsSorted); const pages = guildsSorted.length; const argPage = interaction.options.getNumber("page", false)!; @@ -62,10 +64,10 @@ export default class Serverboard { if (page >= pages) page = 0; } - guild = guildsSorted[page]; - embed = await serverEmbed({ guild, page: page + 1, pages, showInvite: true }); + guild = guild; + embed = embed; - i.message.edit({ embeds: [embed], components: [row] }); + await interaction.editReply({ embeds: [embed], components: [row] }); }); } } From 2f16013ed83d97cf2fbde8271f4da9ee9ee33833 Mon Sep 17 00:00:00 2001 From: froxcey Date: Sun, 21 Jan 2024 02:33:19 +0800 Subject: [PATCH 014/127] Improve serverboard handling --- src/commands/serverboard.ts | 5 +- src/utils/embeds/serverEmbed.ts | 94 +++++++++++++++++++++------------ 2 files changed, 63 insertions(+), 36 deletions(-) diff --git a/src/commands/serverboard.ts b/src/commands/serverboard.ts index 25e5284..debb333 100644 --- a/src/commands/serverboard.ts +++ b/src/commands/serverboard.ts @@ -6,6 +6,7 @@ import { import { quickSort } from "../utils/quickSort"; import { serverEmbed } from "../utils/embeds/serverEmbed"; import { listPublicServers } from "../utils/database/settings"; +import {errorEmbed} from "../utils/embeds/errorEmbed"; export default class Serverboard { data: SlashCommandSubcommandBuilder; @@ -26,13 +27,15 @@ export default class Serverboard { guildsMapped[`${guild.memberCount}:${guild.id}`] = guild; } + if (Object.keys(guildsMapped).length == 0) + return interaction.reply({"embeds": [errorEmbed("No public server found")]}) + const guildsSorted = quickSort( [...Object.keys(guildsMapped).map(i => Number(i.split(":")[0]))], [[...Object.values(guildsMapped)]], 0, Object.keys(guildsMapped).length - 1 )[1]![0].reverse(); - console.log(guildsSorted); const pages = guildsSorted.length; const argPage = interaction.options.getNumber("page", false)!; diff --git a/src/utils/embeds/serverEmbed.ts b/src/utils/embeds/serverEmbed.ts index 9384d22..e95d30d 100644 --- a/src/utils/embeds/serverEmbed.ts +++ b/src/utils/embeds/serverEmbed.ts @@ -5,11 +5,11 @@ import Vibrant from "node-vibrant"; import sharp from "sharp"; type Options = { - guild: Guild - roles?: boolean - showInvite?: boolean - page?: number - pages?: number + guild: Guild; + roles?: boolean; + showInvite?: boolean; + page?: number; + pages?: number; }; /** @@ -18,81 +18,105 @@ type Options = { * @returns Embed that contains the guild info. */ export async function serverEmbed(options: Options) { - const page = options.page; - const pages = options.pages; - const guild = options.guild; - const { premiumTier: boostTier, premiumSubscriptionCount: boostCount } = guild; + const { page, pages, guild } = options; + const { premiumTier: boostTier, premiumSubscriptionCount: boostCount } = + guild; const invite = getSetting(guild.id, "serverboard.inviteLink"); const members = guild.members.cache; - const boosters = members.filter(member => member.premiumSince); - const onlineMembers = members.filter(member => ["online", "dnd", "idle"].includes(member.presence?.status!)).size; - const bots = members.filter(member => member.user.bot); - const formattedUserCount = (guild.memberCount - bots.size)?.toLocaleString("en-US"); + const boosters = members.filter((member) => member.premiumSince); + const onlineMembers = members.filter((member) => + ["online", "dnd", "idle"].includes(member.presence?.status!), + ).size; + const bots = members.filter((member) => member.user.bot); + const formattedUserCount = (guild.memberCount - bots.size)?.toLocaleString( + "en-US", + ); const roles = guild.roles.cache; - const sortedRoles = [...roles].sort((role1, role2) => role2[1].position - role1[1].position); + const sortedRoles = [...roles].sort( + (role1, role2) => role2[1].position - role1[1].position, + ); sortedRoles.pop(); const channels = guild.channels.cache; const channelSizes = { - text: channels.filter(channel => channel.type === 0 || channel.type === 15 || channel.type === 5).size, - voice: channels.filter(channel => channel.type === 2 || channel.type === 13).size, - categories: channels.filter(channel => channel.type === 4).size + text: channels.filter( + (channel) => + channel.type === 0 || channel.type === 15 || channel.type === 5, + ).size, + voice: channels.filter( + (channel) => channel.type === 2 || channel.type === 13, + ).size, + categories: channels.filter((channel) => channel.type === 4).size, }; const generalValues = [ `**Owned by** ${(await guild.fetchOwner()).user.displayName}`, - `**Created on** ` + `**Created on** `, ]; - if (options.showInvite && invite === null) generalValues.push(`**Invite link**: ${invite}`); + if (options.showInvite && invite === null) + generalValues.push(`**Invite link**: ${invite}`); const embed = new EmbedBuilder() - .setAuthor({ name: `${pages ? `#${page} • ` : ""}${guild.name}`, iconURL: guild.iconURL() || undefined }) + .setAuthor({ + name: `${pages ? `#${page} • ` : ""}${guild.name}`, + iconURL: guild.iconURL() || undefined, + }) .setDescription(guild.description ? guild.description : null) .setFields({ name: "📃 • General", value: generalValues.join("\n") }) - .setFooter({ text: `Server ID: ${guild.id}${pages ? ` • Page ${page}/${pages}` : ""}` }) + .setFooter({ + text: `Server ID: ${guild.id}${pages ? ` • Page ${page}/${pages}` : ""}`, + }) .setThumbnail(guild.iconURL()) .setColor(genColor(200)); try { const imageBuffer = await (await fetch(guild.iconURL()!)).arrayBuffer(); const image = sharp(imageBuffer).toFormat("jpg"); - const { r, g, b } = (await new Vibrant(await image.toBuffer()).getPalette()).Vibrant!; + const { r, g, b } = (await new Vibrant(await image.toBuffer()).getPalette()) + .Vibrant!; embed.setColor(genRGBColor(r, g, b) as ColorResolvable); } catch {} - if (options.roles) embed.addFields({ - name: `🎭 • ${roles.size - 1} ${roles.size === 1 ? "role" : "roles"}`, - value: roles.size === 1 - ? "*None*" - : `${sortedRoles.slice(0, 5).map(role => `<@&${role[0]}>`).join(", ")}${roles.size > 5 ? ` **and ${roles.size - 5} more**` : ""}` - }); + if (options.roles) + embed.addFields({ + name: `🎭 • ${roles.size - 1} ${roles.size === 1 ? "role" : "roles"}`, + value: + roles.size === 1 + ? "*None*" + : `${sortedRoles + .slice(0, 5) + .map((role) => `<@&${role[0]}>`) + .join( + ", ", + )}${roles.size > 5 ? ` **and ${roles.size - 5} more**` : ""}`, + }); embed.addFields( { name: `👥 • ${guild.memberCount?.toLocaleString("en-US")} members`, value: [ `**${formattedUserCount}** users • **${bots.size?.toLocaleString("en-US")}** bots`, - `**${onlineMembers?.toLocaleString("en-US")}** online` + `**${onlineMembers?.toLocaleString("en-US")}** online`, ].join("\n"), - inline: true + inline: true, }, { name: `🗨️ • ${channelSizes.text + channelSizes.voice} channels`, value: [ `**${channelSizes.text}** text • **${channelSizes.voice}** voice`, - `**${channelSizes.categories}** categories` + `**${channelSizes.categories}** categories`, ].join("\n"), - inline: true + inline: true, }, { name: `🌟 • ${boostTier == 0 ? "No level" : `Level ${boostTier}`}`, value: [ `**${boostCount}**${boostTier === 0 ? "/2" : boostTier === 1 ? "/7" : boostTier === 2 ? "/14" : ""} boosts`, - `**${boosters.size}** ${boosters.size === 1 ? "booster" : "boosters"}` + `**${boosters.size}** ${boosters.size === 1 ? "booster" : "boosters"}`, ].join("\n"), - inline: true - } + inline: true, + }, ); return embed; From f57519ce685ff9219dda05df7d3c75c7e2b5bf9f Mon Sep 17 00:00:00 2001 From: froxcey Date: Sun, 21 Jan 2024 03:19:31 +0800 Subject: [PATCH 015/127] Fix serverboard --- src/commands/serverboard.ts | 54 +++++++++++++++++++++++++------------ 1 file changed, 37 insertions(+), 17 deletions(-) diff --git a/src/commands/serverboard.ts b/src/commands/serverboard.ts index debb333..8679ff6 100644 --- a/src/commands/serverboard.ts +++ b/src/commands/serverboard.ts @@ -1,12 +1,17 @@ import { - SlashCommandSubcommandBuilder, ButtonBuilder, ActionRowBuilder, - ButtonStyle, ButtonInteraction, type Guild, - type ChatInputCommandInteraction + SlashCommandSubcommandBuilder, + ButtonBuilder, + ActionRowBuilder, + ButtonStyle, + ButtonInteraction, + type Guild, + type ChatInputCommandInteraction, + ComponentType, } from "discord.js"; import { quickSort } from "../utils/quickSort"; import { serverEmbed } from "../utils/embeds/serverEmbed"; import { listPublicServers } from "../utils/database/settings"; -import {errorEmbed} from "../utils/embeds/errorEmbed"; +import { errorEmbed } from "../utils/embeds/errorEmbed"; export default class Serverboard { data: SlashCommandSubcommandBuilder; @@ -14,34 +19,44 @@ export default class Serverboard { this.data = new SlashCommandSubcommandBuilder() .setName("serverboard") .setDescription("Shows the servers that have Nebula.") - .addNumberOption(option => option - .setName("page") - .setDescription("The page you want to see.") + .addNumberOption((option) => + option.setName("page").setDescription("The page you want to see."), ); } async run(interaction: ChatInputCommandInteraction) { const guildsMapped: Record = {}; for (const shownGuild of listPublicServers()) { - const guild = interaction.client.guilds.cache.find(guild => (guild.id === `${shownGuild}`))!; + const guild = interaction.client.guilds.cache.find( + (guild) => (guild.id = shownGuild + ""), + )!; guildsMapped[`${guild.memberCount}:${guild.id}`] = guild; } if (Object.keys(guildsMapped).length == 0) - return interaction.reply({"embeds": [errorEmbed("No public server found")]}) + return interaction.reply({ + embeds: [errorEmbed("No public server found")], + }); const guildsSorted = quickSort( - [...Object.keys(guildsMapped).map(i => Number(i.split(":")[0]))], + [...Object.keys(guildsMapped).map((i) => Number(i.split(":")[0]))], [[...Object.values(guildsMapped)]], 0, - Object.keys(guildsMapped).length - 1 + Object.keys(guildsMapped).length - 1, )[1]![0].reverse(); const pages = guildsSorted.length; const argPage = interaction.options.getNumber("page", false)!; - let page = (argPage - 1 <= 0 ? 0 : argPage - 1 > pages ? pages - 1 : argPage - 1) || 0; + let page = + (argPage - 1 <= 0 ? 0 : argPage - 1 > pages ? pages - 1 : argPage - 1) || + 0; let guild = guildsSorted[page]; - let embed = await serverEmbed({ guild, page: page + 1, pages, showInvite: true }); + let embed = await serverEmbed({ + guild, + page: page + 1, + pages, + showInvite: true, + }); const row = new ActionRowBuilder().addComponents( new ButtonBuilder() @@ -51,12 +66,17 @@ export default class Serverboard { new ButtonBuilder() .setCustomId("right") .setEmoji("1137330125004869702") - .setStyle(ButtonStyle.Primary) + .setStyle(ButtonStyle.Primary), ); - await interaction.reply({ embeds: [embed], components: [row] }); - interaction.channel - ?.createMessageComponentCollector({ filter: i => i.user.id === interaction.user.id, time: 60000 }) + const reply = await interaction.reply({ + embeds: [embed], + components: [row], + }); + reply.createMessageComponentCollector({ + componentType: ComponentType.Button, + time: 60000, + }) .on("collect", async (i: ButtonInteraction) => { switch (i.customId) { case "left": From c6d475c3a084cb5af15eae97fb9fa2b90418cdd7 Mon Sep 17 00:00:00 2001 From: froxcey Date: Sun, 21 Jan 2024 13:48:05 +0800 Subject: [PATCH 016/127] Fix serverboard --- src/commands/serverboard.ts | 54 +++++++++------------- src/utils/database/disabledCommands.ts | 2 +- src/utils/database/levelBlockedChannels.ts | 4 +- src/utils/database/levelRewards.ts | 4 +- src/utils/database/levelling.ts | 4 +- src/utils/database/moderation.ts | 6 +-- src/utils/database/news.ts | 4 +- src/utils/database/newsCategories.ts | 6 +-- src/utils/database/settings.ts | 3 +- 9 files changed, 40 insertions(+), 47 deletions(-) diff --git a/src/commands/serverboard.ts b/src/commands/serverboard.ts index 8679ff6..6a97858 100644 --- a/src/commands/serverboard.ts +++ b/src/commands/serverboard.ts @@ -4,11 +4,9 @@ import { ActionRowBuilder, ButtonStyle, ButtonInteraction, - type Guild, type ChatInputCommandInteraction, ComponentType, } from "discord.js"; -import { quickSort } from "../utils/quickSort"; import { serverEmbed } from "../utils/embeds/serverEmbed"; import { listPublicServers } from "../utils/database/settings"; import { errorEmbed } from "../utils/embeds/errorEmbed"; @@ -25,38 +23,33 @@ export default class Serverboard { } async run(interaction: ChatInputCommandInteraction) { - const guildsMapped: Record = {}; - for (const shownGuild of listPublicServers()) { - const guild = interaction.client.guilds.cache.find( - (guild) => (guild.id = shownGuild + ""), - )!; - guildsMapped[`${guild.memberCount}:${guild.id}`] = guild; - } + const guildList = ( + await Promise.all( + listPublicServers().map((id) => + interaction.client.guilds.fetch(id), + ), + ) + ).sort((a, b) => b.memberCount - a.memberCount); + + const pages = guildList.length; - if (Object.keys(guildsMapped).length == 0) + if (pages == 0) return interaction.reply({ embeds: [errorEmbed("No public server found")], }); - const guildsSorted = quickSort( - [...Object.keys(guildsMapped).map((i) => Number(i.split(":")[0]))], - [[...Object.values(guildsMapped)]], - 0, - Object.keys(guildsMapped).length - 1, - )[1]![0].reverse(); - - const pages = guildsSorted.length; const argPage = interaction.options.getNumber("page", false)!; let page = (argPage - 1 <= 0 ? 0 : argPage - 1 > pages ? pages - 1 : argPage - 1) || 0; - let guild = guildsSorted[page]; - let embed = await serverEmbed({ - guild, - page: page + 1, - pages, - showInvite: true, - }); + async function getEmbed() { + return await serverEmbed({ + guild: guildList[page], + page: page + 1, + pages, + showInvite: true, + }); + } const row = new ActionRowBuilder().addComponents( new ButtonBuilder() @@ -70,10 +63,11 @@ export default class Serverboard { ); const reply = await interaction.reply({ - embeds: [embed], + embeds: [await getEmbed()], components: [row], }); - reply.createMessageComponentCollector({ + reply + .createMessageComponentCollector({ componentType: ComponentType.Button, time: 60000, }) @@ -87,10 +81,8 @@ export default class Serverboard { if (page >= pages) page = 0; } - guild = guild; - embed = embed; - - await interaction.editReply({ embeds: [embed], components: [row] }); + await reply.edit({ embeds: [await getEmbed()], components: [row] }); + i.update({}); }); } } diff --git a/src/utils/database/disabledCommands.ts b/src/utils/database/disabledCommands.ts index a91edf2..fc96454 100644 --- a/src/utils/database/disabledCommands.ts +++ b/src/utils/database/disabledCommands.ts @@ -4,7 +4,7 @@ import { TableDefinition, TypeOfDefinition } from "./types"; const tableDefinition = { name: "disabledCommands", definition: { - guild: "INTEGER", + guild: "TEXT", command: "TEXT" } } satisfies TableDefinition; diff --git a/src/utils/database/levelBlockedChannels.ts b/src/utils/database/levelBlockedChannels.ts index 3eb610b..379bab6 100644 --- a/src/utils/database/levelBlockedChannels.ts +++ b/src/utils/database/levelBlockedChannels.ts @@ -4,8 +4,8 @@ import { TableDefinition, TypeOfDefinition } from "./types"; const tableDefinition = { name: "levelBlockedChannels", definition: { - guild: "INTEGER", - channel: "INTEGER" + guild: "TEXT", + channel: "TEXT" } } satisfies TableDefinition; diff --git a/src/utils/database/levelRewards.ts b/src/utils/database/levelRewards.ts index 131b715..def0875 100644 --- a/src/utils/database/levelRewards.ts +++ b/src/utils/database/levelRewards.ts @@ -5,8 +5,8 @@ import { TableDefinition, TypeOfDefinition } from "./types"; const tableDefinition = { name: "levelRewards", definition: { - guild: "INTEGER", - roleID: "INTEGER", + guild: "TEXT", + roleID: "TEXT", level: "INTEGER" } } satisfies TableDefinition; diff --git a/src/utils/database/levelling.ts b/src/utils/database/levelling.ts index 361d5a9..965f430 100644 --- a/src/utils/database/levelling.ts +++ b/src/utils/database/levelling.ts @@ -4,8 +4,8 @@ import { TableDefinition, TypeOfDefinition } from "./types"; const tableDefinition = { name: "levelling", definition: { - guild: "INTEGER", - user: "INTEGER", + guild: "TEXT", + user: "TEXT", level: "INTEGER", exp: "INTEGER" } diff --git a/src/utils/database/moderation.ts b/src/utils/database/moderation.ts index 4ff7084..c3a3ed4 100644 --- a/src/utils/database/moderation.ts +++ b/src/utils/database/moderation.ts @@ -4,10 +4,10 @@ import { TableDefinition, TypeOfDefinition } from "./types"; const definition = { name: "moderation", definition: { - guild: "INTEGER", - user: "INTEGER", + guild: "TEXT", + user: "TEXT", type: "TEXT", - moderator: "INTEGER", + moderator: "TEXT", reason: "TEXT", public: "BOOL", id: "TEXT", diff --git a/src/utils/database/news.ts b/src/utils/database/news.ts index 29da269..fddf356 100644 --- a/src/utils/database/news.ts +++ b/src/utils/database/news.ts @@ -4,7 +4,7 @@ import { TableDefinition, TypeOfDefinition } from "./types"; const definition = { name: "news", definition: { - guild: "INTEGER", + guild: "TEXT", title: "TEXT", body: "TEXT", imageURL: "TEXT", @@ -12,7 +12,7 @@ const definition = { authorPFP: "TEXT", createdAt: "TIMESTAMP", updatedAt: "TIMESTAMP", - messageID: "INTEGER", + messageID: "TEXT", categoryID: "TEXT", id: "TEXT" } diff --git a/src/utils/database/newsCategories.ts b/src/utils/database/newsCategories.ts index 9933283..41b2949 100644 --- a/src/utils/database/newsCategories.ts +++ b/src/utils/database/newsCategories.ts @@ -4,10 +4,10 @@ import { TableDefinition, TypeOfDefinition } from "./types"; const definition = { name: "newsCategories", definition: { - guild: "INTEGER", + guild: "TEXT", name: "TEXT", - role: "INTEGER", - channel: "INTEGER", + role: "TEXT", + channel: "TEXT", id: "TEXT" } } satisfies TableDefinition; diff --git a/src/utils/database/settings.ts b/src/utils/database/settings.ts index c0e3a88..e303414 100644 --- a/src/utils/database/settings.ts +++ b/src/utils/database/settings.ts @@ -5,7 +5,7 @@ import { FieldData, SqlType, TableDefinition, TypeOfDefinition } from "./types"; const tableDefinition = { name: "settings", definition: { - guild: "INTEGER", + guild: "TEXT", key: "TEXT", value: "TEXT" } @@ -39,6 +39,7 @@ export function setSetting(guild: str } export function listPublicServers() { + return ["1144855477449142302", "1079612082636472420"] return (listPublicQuery.all() as TypeOfDefinition[]).map(entry => (entry.guild)); } From 2fdb074a574ccabcf1441094d958e460f9120046 Mon Sep 17 00:00:00 2001 From: Serge Date: Sun, 21 Jan 2024 21:55:17 +0600 Subject: [PATCH 017/127] prettierrc + errorEmbed edit --- .prettierrc | 7 ++++ package.json | 1 + src/commands/moderation/ban.ts | 12 +++--- src/commands/serverboard.ts | 52 +++++++------------------ src/utils/database/settings.ts | 1 - src/utils/embeds/errorEmbed.ts | 7 ++-- src/utils/embeds/serverEmbed.ts | 69 ++++++++++++--------------------- 7 files changed, 56 insertions(+), 93 deletions(-) create mode 100644 .prettierrc diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..9488af4 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "tabWidth": 2, + "useTabs": false, + "arrowParens": "avoid", + "trailingComma": "none", + "printWidth": 120 +} \ No newline at end of file diff --git a/package.json b/package.json index 1997b11..3a2484d 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "version": "0.1.0", "main": "./src/bot.ts", "type": "module", + "prettier": "./.prettierrc", "scripts": { "start": "bun ./src/bot.ts" }, diff --git a/src/commands/moderation/ban.ts b/src/commands/moderation/ban.ts index 3fe5fc3..c6502ef 100644 --- a/src/commands/moderation/ban.ts +++ b/src/commands/moderation/ban.ts @@ -30,7 +30,7 @@ export default class Ban { const members = guild.members.cache; const member = members.get(interaction.member?.user.id!)!; const target = members.get(user.id)!; - const name = target.nickname ?? user.username; + const name = user.displayName; if (!member.permissions.has(PermissionsBitField.Flags.BanMembers)) return await interaction.reply({ embeds: [errorEmbed("You need the **Ban Members** permission to execute this command.")] @@ -43,19 +43,19 @@ export default class Ban { }); if (!target.manageable) return await interaction.reply({ - embeds: [errorEmbed(`You can't ban ${name}, because they have a higher role position than Nebula.`)] + embeds: [errorEmbed(`You can't ban ${name}`, "The member has a higher role position than Nebula.")] }); if (member.roles.highest.position < target.roles.highest.position) return await interaction.reply({ - embeds: [errorEmbed(`You can't ban ${name}, because they have a higher role position than you.`)] + embeds: [errorEmbed(`You can't ban ${name}`, "The member has a higher role position than you.")] }); const reason = interaction.options.getString("reason"); const embed = new EmbedBuilder() - .setAuthor({ name: `• ${user.username}`, iconURL: user.displayAvatarURL() }) - .setTitle(`✅ • Banned ${user.username}`) + .setAuthor({ name: `• ${name}`, iconURL: user.displayAvatarURL() }) + .setTitle(`✅ • Banned ${name}`) .setDescription([ - `**Moderator**: ${interaction.user.username}`, + `**Moderator**: ${interaction.user.displayName}`, `**Reason**: ${reason ?? "No reason provided"}` ].join("\n")) .setThumbnail(user.displayAvatarURL()) diff --git a/src/commands/serverboard.ts b/src/commands/serverboard.ts index 6a97858..40025a6 100644 --- a/src/commands/serverboard.ts +++ b/src/commands/serverboard.ts @@ -1,11 +1,7 @@ import { - SlashCommandSubcommandBuilder, - ButtonBuilder, - ActionRowBuilder, - ButtonStyle, - ButtonInteraction, - type ChatInputCommandInteraction, - ComponentType, + SlashCommandSubcommandBuilder, ButtonBuilder, ActionRowBuilder, + ButtonStyle, ButtonInteraction, ComponentType, + type ChatInputCommandInteraction } from "discord.js"; import { serverEmbed } from "../utils/embeds/serverEmbed"; import { listPublicServers } from "../utils/database/settings"; @@ -17,38 +13,24 @@ export default class Serverboard { this.data = new SlashCommandSubcommandBuilder() .setName("serverboard") .setDescription("Shows the servers that have Nebula.") - .addNumberOption((option) => - option.setName("page").setDescription("The page you want to see."), + .addNumberOption(option => option + .setName("page") + .setDescription("The page you want to see.") ); } async run(interaction: ChatInputCommandInteraction) { - const guildList = ( - await Promise.all( - listPublicServers().map((id) => - interaction.client.guilds.fetch(id), - ), - ) - ).sort((a, b) => b.memberCount - a.memberCount); + const guildList = (await Promise.all(listPublicServers().map(id => interaction.client.guilds.fetch(id)))) + .sort((a, b) => b.memberCount - a.memberCount); const pages = guildList.length; - - if (pages == 0) - return interaction.reply({ - embeds: [errorEmbed("No public server found")], - }); + if (pages == 0) return interaction.reply({ embeds: [errorEmbed("No public server found.")] }); const argPage = interaction.options.getNumber("page", false)!; - let page = - (argPage - 1 <= 0 ? 0 : argPage - 1 > pages ? pages - 1 : argPage - 1) || - 0; + let page = (argPage - 1 <= 0 ? 0 : argPage - 1 > pages ? pages - 1 : argPage - 1) || 0; + async function getEmbed() { - return await serverEmbed({ - guild: guildList[page], - page: page + 1, - pages, - showInvite: true, - }); + return await serverEmbed({ guild: guildList[page], page: page + 1, pages, showInvite: true }); } const row = new ActionRowBuilder().addComponents( @@ -62,15 +44,9 @@ export default class Serverboard { .setStyle(ButtonStyle.Primary), ); - const reply = await interaction.reply({ - embeds: [await getEmbed()], - components: [row], - }); + const reply = await interaction.reply({ embeds: [await getEmbed()], components: [row] }); reply - .createMessageComponentCollector({ - componentType: ComponentType.Button, - time: 60000, - }) + .createMessageComponentCollector({ componentType: ComponentType.Button, time: 60000 }) .on("collect", async (i: ButtonInteraction) => { switch (i.customId) { case "left": diff --git a/src/utils/database/settings.ts b/src/utils/database/settings.ts index e303414..f3228d3 100644 --- a/src/utils/database/settings.ts +++ b/src/utils/database/settings.ts @@ -39,7 +39,6 @@ export function setSetting(guild: str } export function listPublicServers() { - return ["1144855477449142302", "1079612082636472420"] return (listPublicQuery.all() as TypeOfDefinition[]).map(entry => (entry.guild)); } diff --git a/src/utils/embeds/errorEmbed.ts b/src/utils/embeds/errorEmbed.ts index 453678f..6a6975b 100644 --- a/src/utils/embeds/errorEmbed.ts +++ b/src/utils/embeds/errorEmbed.ts @@ -3,12 +3,13 @@ import { genColor } from "../colorGen"; /** * Sends the embed containing an error. - * @param description Description of the error. + * @param error The error. + * @param reason The reason of the error. * @returns Embed with the error description. */ -export function errorEmbed(description: string) { +export function errorEmbed(error: string, reason: string) { return new EmbedBuilder() .setTitle("❌ • Error!") - .setDescription(description) + .setDescription([`**${error}**`, `Reason: ${reason}`].join("\n")) .setColor(genColor(0)); } diff --git a/src/utils/embeds/serverEmbed.ts b/src/utils/embeds/serverEmbed.ts index e95d30d..1c84653 100644 --- a/src/utils/embeds/serverEmbed.ts +++ b/src/utils/embeds/serverEmbed.ts @@ -19,62 +19,43 @@ type Options = { */ export async function serverEmbed(options: Options) { const { page, pages, guild } = options; - const { premiumTier: boostTier, premiumSubscriptionCount: boostCount } = - guild; + const { premiumTier: boostTier, premiumSubscriptionCount: boostCount } = guild; const invite = getSetting(guild.id, "serverboard.inviteLink"); const members = guild.members.cache; - const boosters = members.filter((member) => member.premiumSince); - const onlineMembers = members.filter((member) => - ["online", "dnd", "idle"].includes(member.presence?.status!), - ).size; - const bots = members.filter((member) => member.user.bot); - const formattedUserCount = (guild.memberCount - bots.size)?.toLocaleString( - "en-US", - ); + const boosters = members.filter(member => member.premiumSince); + const onlineMembers = members.filter(member => ["online", "dnd", "idle"].includes(member.presence?.status!)).size; + const bots = members.filter(member => member.user.bot); + const formattedUserCount = (guild.memberCount - bots.size)?.toLocaleString("en-US"); const roles = guild.roles.cache; - const sortedRoles = [...roles].sort( - (role1, role2) => role2[1].position - role1[1].position, - ); + const sortedRoles = [...roles].sort((role1, role2) => role2[1].position - role1[1].position); sortedRoles.pop(); const channels = guild.channels.cache; const channelSizes = { - text: channels.filter( - (channel) => - channel.type === 0 || channel.type === 15 || channel.type === 5, - ).size, - voice: channels.filter( - (channel) => channel.type === 2 || channel.type === 13, - ).size, - categories: channels.filter((channel) => channel.type === 4).size, + text: channels.filter(channel => channel.type === 0 || channel.type === 15 || channel.type === 5).size, + voice: channels.filter(channel => channel.type === 2 || channel.type === 13).size, + categories: channels.filter(channel => channel.type === 4).size }; const generalValues = [ - `**Owned by** ${(await guild.fetchOwner()).user.displayName}`, - `**Created on** `, + `Owned by **${(await guild.fetchOwner()).user.displayName}**`, + `Created on ****` ]; - if (options.showInvite && invite === null) - generalValues.push(`**Invite link**: ${invite}`); + if (options.showInvite && invite !== null) generalValues.push(`**Invite link**: ${invite}`); const embed = new EmbedBuilder() - .setAuthor({ - name: `${pages ? `#${page} • ` : ""}${guild.name}`, - iconURL: guild.iconURL() || undefined, - }) + .setAuthor({ name: `${pages ? `#${page} • ` : ""}${guild.name}`, iconURL: guild.iconURL()! }) .setDescription(guild.description ? guild.description : null) .setFields({ name: "📃 • General", value: generalValues.join("\n") }) - .setFooter({ - text: `Server ID: ${guild.id}${pages ? ` • Page ${page}/${pages}` : ""}`, - }) + .setFooter({ text: `Server ID: ${guild.id}${pages ? ` • Page ${page}/${pages}` : ""}` }) .setThumbnail(guild.iconURL()) .setColor(genColor(200)); try { const imageBuffer = await (await fetch(guild.iconURL()!)).arrayBuffer(); const image = sharp(imageBuffer).toFormat("jpg"); - const { r, g, b } = (await new Vibrant(await image.toBuffer()).getPalette()) - .Vibrant!; + const { r, g, b } = (await new Vibrant(await image.toBuffer()).getPalette()).Vibrant!; embed.setColor(genRGBColor(r, g, b) as ColorResolvable); } catch {} @@ -86,10 +67,8 @@ export async function serverEmbed(options: Options) { ? "*None*" : `${sortedRoles .slice(0, 5) - .map((role) => `<@&${role[0]}>`) - .join( - ", ", - )}${roles.size > 5 ? ` **and ${roles.size - 5} more**` : ""}`, + .map(role => `<@&${role[0]}>`) + .join(", ")}${roles.size > 5 ? ` **and ${roles.size - 5} more**` : ""}` }); embed.addFields( @@ -97,26 +76,26 @@ export async function serverEmbed(options: Options) { name: `👥 • ${guild.memberCount?.toLocaleString("en-US")} members`, value: [ `**${formattedUserCount}** users • **${bots.size?.toLocaleString("en-US")}** bots`, - `**${onlineMembers?.toLocaleString("en-US")}** online`, + `**${onlineMembers?.toLocaleString("en-US")}** online` ].join("\n"), - inline: true, + inline: true }, { name: `🗨️ • ${channelSizes.text + channelSizes.voice} channels`, value: [ `**${channelSizes.text}** text • **${channelSizes.voice}** voice`, - `**${channelSizes.categories}** categories`, + `**${channelSizes.categories}** categories` ].join("\n"), - inline: true, + inline: true }, { name: `🌟 • ${boostTier == 0 ? "No level" : `Level ${boostTier}`}`, value: [ `**${boostCount}**${boostTier === 0 ? "/2" : boostTier === 1 ? "/7" : boostTier === 2 ? "/14" : ""} boosts`, - `**${boosters.size}** ${boosters.size === 1 ? "booster" : "boosters"}`, + `**${boosters.size}** ${boosters.size === 1 ? "booster" : "boosters"}` ].join("\n"), - inline: true, - }, + inline: true + } ); return embed; From bc1902cb1988fd576f0beba315e147b0404ebbd7 Mon Sep 17 00:00:00 2001 From: Serge Date: Sun, 21 Jan 2024 21:55:55 +0600 Subject: [PATCH 018/127] 120 -> 100 --- .prettierrc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.prettierrc b/.prettierrc index 9488af4..9d5a120 100644 --- a/.prettierrc +++ b/.prettierrc @@ -3,5 +3,5 @@ "useTabs": false, "arrowParens": "avoid", "trailingComma": "none", - "printWidth": 120 -} \ No newline at end of file + "printWidth": 100 +} From da49452d4a54478a5dc4a0f8c5e44cbae0b847fb Mon Sep 17 00:00:00 2001 From: froxcey Date: Mon, 22 Jan 2024 00:42:24 +0800 Subject: [PATCH 019/127] Add settings --- src/commands/server/settings.ts | 73 +++++++++++++++++++++++++++++ src/handlers/commands.ts | 81 +++++++++++++++++++++++++-------- src/utils/database/settings.ts | 66 ++++++++++++++++++++------- 3 files changed, 186 insertions(+), 34 deletions(-) create mode 100644 src/commands/server/settings.ts diff --git a/src/commands/server/settings.ts b/src/commands/server/settings.ts new file mode 100644 index 0000000..5311418 --- /dev/null +++ b/src/commands/server/settings.ts @@ -0,0 +1,73 @@ +import { + Client, + InteractionType, + SlashCommandStringOption, + SlashCommandSubcommandBuilder, + type ChatInputCommandInteraction, +} from "discord.js"; +import { + getSetting, + setSetting, + settingsDefinition, + settingsKeys, +} from "../../utils/database/settings"; + +export default class ServerInfo { + data: SlashCommandSubcommandBuilder; + constructor() { + this.data = new SlashCommandSubcommandBuilder() + .setName("settings") + .setDescription("Configure the bot") + .addStringOption( + new SlashCommandStringOption() + .setName("key") + .setDescription("The setting key to set") + .addChoices(...settingsKeys.map((key) => ({ name: key, value: key }))) + .setRequired(true), + ) + .addStringOption( + new SlashCommandStringOption() + .setName("value") + .setDescription( + "The value you want to set this option to, or blank for view", + ) + .setAutocomplete(true), + ); + } + + async run(interaction: ChatInputCommandInteraction) { + const key = interaction.options.get("key")! + .value as keyof typeof settingsDefinition; + const value = interaction.options.get("value")?.value; + if (value == undefined) + return interaction.reply( + `\`${key}\` is currently \`${JSON.stringify(getSetting(interaction.guildId!, key))}\``, + ); + + setSetting(interaction.guildId!, key, value); + interaction.reply(`\`${key}\` has been set to \`${value}\``); + } + + autocompleteHandler(client: Client) { + client.on("interactionCreate", (interaction) => { + if (interaction.type != InteractionType.ApplicationCommandAutocomplete) + return; + if (interaction.options.getSubcommand() != this.data.name) return; + switch ( + settingsDefinition[ + interaction.options.get("key")! + .value as keyof typeof settingsDefinition + ] + ) { + case "BOOL": + interaction.respond( + ["TRUE", "FALSE"].map((choice) => ({ + name: choice, + value: choice, + })), + ); + break; + } + }); + } +} diff --git a/src/handlers/commands.ts b/src/handlers/commands.ts index 67e82f5..9e114fd 100644 --- a/src/handlers/commands.ts +++ b/src/handlers/commands.ts @@ -1,6 +1,8 @@ import { - SlashCommandBuilder, SlashCommandSubcommandGroupBuilder, Guild, - type Client + SlashCommandBuilder, + SlashCommandSubcommandGroupBuilder, + Guild, + type Client, } from "discord.js"; import { pathToFileURL } from "url"; import { join } from "path"; @@ -15,20 +17,40 @@ export default class Commands { this.client = client; } - private async createSubCommand(name: string, ...disabledCommands: string[]): Promise { + private async createSubCommand( + name: string, + ...disabledCommands: string[] + ): Promise { const commandsPath = join(process.cwd(), "src", "commands"); const command = new SlashCommandBuilder() .setName(name.toLowerCase()) .setDescription("This command has no description."); - for (const subCommandFile of readdirSync(join(commandsPath, name), { withFileTypes: true })) { + for (const subCommandFile of readdirSync(join(commandsPath, name), { + withFileTypes: true, + })) { const subCommandName = subCommandFile.name.replaceAll(".ts", ""); - if (disabledCommands?.find(command => command?.split("/")?.[0] == name && command?.split("/")?.[1] == subCommandName)) + if ( + disabledCommands?.find( + (command) => + command?.split("/")?.[0] == name && + command?.split("/")?.[1] == subCommandName, + ) + ) continue; if (subCommandFile.isFile()) { - const subCommand = await import(pathToFileURL(join(commandsPath, name, subCommandFile.name)).toString()); - command.addSubcommand(new subCommand.default().data); + const subCommandModule = await import( + pathToFileURL( + join(commandsPath, name, subCommandFile.name), + ).toString() + ); + const subCommand = new subCommandModule.default(); + + command.addSubcommand(subCommand.data); + if ("autocompleteHandler" in subCommand) { + subCommand.autocompleteHandler(this.client); + } continue; } @@ -36,17 +58,33 @@ export default class Commands { .setName(subCommandName.toLowerCase()) .setDescription("This subcommand group has no description."); - const subCommandGroupFiles = readdirSync(join(commandsPath, name, subCommandFile.name), { withFileTypes: true }); + const subCommandGroupFiles = readdirSync( + join(commandsPath, name, subCommandFile.name), + { withFileTypes: true }, + ); for (const subCommandGroupFile of subCommandGroupFiles) { if (!subCommandGroupFile.isFile()) continue; - if (disabledCommands?.find(command => - command?.split("/")?.[0] == name && - command?.split("/")?.[1] == subCommandFile.name.replaceAll(".ts", "") && - command?.split("/")?.[2] == subCommandGroupFile.name.replaceAll(".ts", "") - )) continue; + if ( + disabledCommands?.find( + (command) => + command?.split("/")?.[0] == name && + command?.split("/")?.[1] == + subCommandFile.name.replaceAll(".ts", "") && + command?.split("/")?.[2] == + subCommandGroupFile.name.replaceAll(".ts", ""), + ) + ) + continue; const subCommand = await import( - pathToFileURL(join(commandsPath, name, subCommandFile.name, subCommandGroupFile.name)).toString() + pathToFileURL( + join( + commandsPath, + name, + subCommandFile.name, + subCommandGroupFile.name, + ), + ).toString() ); subCommandGroup.addSubcommand(new subCommand.default().data); } @@ -66,12 +104,18 @@ export default class Commands { if (disabledCommands?.includes(name.replaceAll(".ts", ""))) continue; if (commandFile.isFile()) { - const command = await import(pathToFileURL(join(commandsPath, name)).toString()); - this.commands.push(new command.default().data); + const commandImport = await import( + pathToFileURL(join(commandsPath, name)).toString() + ); + this.commands.push(new commandImport.default().data); continue; } - const subCommand = await this.createSubCommand(name, join(commandsPath, name), ...disabledCommands); + const subCommand = await this.createSubCommand( + name, + join(commandsPath, name), + ...disabledCommands, + ); this.commands.push(subCommand); } } @@ -88,7 +132,8 @@ export default class Commands { console.log("Adding commands to guilds..."); for (const guildID of guilds.keys()) { const disabledCommands = getDisabledCommands(guildID); - if (disabledCommands.length > 0) await this.loadCommands(...disabledCommands); + if (disabledCommands.length > 0) + await this.loadCommands(...disabledCommands); await guilds.get(guildID)?.commands.set(this.commands); } diff --git a/src/utils/database/settings.ts b/src/utils/database/settings.ts index e303414..3c45ee3 100644 --- a/src/utils/database/settings.ts +++ b/src/utils/database/settings.ts @@ -7,11 +7,11 @@ const tableDefinition = { definition: { guild: "TEXT", key: "TEXT", - value: "TEXT" - } + value: "TEXT", + }, } satisfies TableDefinition; -const settingsDefinition = { +export const settingsDefinition = { "levelling.enabled": "BOOL", "levelling.channel": "INTEGER", "levelling.persistence": "BOOL", @@ -20,28 +20,62 @@ const settingsDefinition = { "serverboard.shown": "BOOL", } satisfies Record; -export const settingKeys = Object.keys(settingsDefinition) as (keyof typeof settingsDefinition)[]; +export const settingsKeys = Object.keys( + settingsDefinition, +) as (keyof typeof settingsDefinition)[]; const database = getDatabase(tableDefinition); -const getQuery = database.query("SELECT * FROM settings WHERE guild = $1 AND key = $2;"); -const listPublicQuery = database.query("SELECT * FROM settings WHERE key = 'serverboard.shown' AND value = 'true';"); -const setQuery = database.query("UPDATE settings SET value = $3 WHERE guild = $1 AND key = $2;"); +const getQuery = database.query( + "SELECT * FROM settings WHERE guild = $1 AND key = $2;", +); +const listPublicQuery = database.query( + "SELECT * FROM settings WHERE key = 'serverboard.shown' AND value = 'TRUE';", +); +const removeQuery = database.query( + "DELETE FROM settings WHERE guild = $1 AND key = $2;", +); +const insertQuery = database.query( + "INSERT INTO settings (guild, key, value) VALUES (?1, ?2, ?3);", +); -export function getSetting(guild: string, key: K): TypeOfKey | null { - let res = getQuery.all(guild, key) as TypeOfDefinition[]; +export function getSetting( + guild: string, + key: K, +): TypeOfKey | null { + let res = getQuery.all(JSON.stringify(guild), key) as TypeOfDefinition< + typeof tableDefinition + >[]; if (res.length == 0) return null; - if (settingsDefinition[key] == "TEXT") return res[0].value as TypeOfKey; - return JSON.parse(res[0].value); + switch (settingsDefinition[key]) { + case "TEXT": + return res[0].value as TypeOfKey; + case "BOOL": + return (res[0].value == "TRUE") as TypeOfKey; + default: + // TODO: Implement more data types + return "WIP"; + } } -export function setSetting(guild: string, key: K, value: TypeOfKey) { - setQuery.run(guild, key, JSON.stringify(value)); +export function setSetting( + guild: string, + key: K, + value: TypeOfKey, +) { + const doInsert = getSetting(guild, key) == null; + if (!doInsert) { + removeQuery.all(JSON.stringify(guild), key); + } + insertQuery.run(JSON.stringify(guild), key, value); } export function listPublicServers() { - return ["1144855477449142302", "1079612082636472420"] - return (listPublicQuery.all() as TypeOfDefinition[]).map(entry => (entry.guild)); + return ( + listPublicQuery.all() as TypeOfDefinition[] + ).map((entry) => JSON.parse(entry.guild)); } // Utility type -type TypeOfKey = SqlType<(typeof settingsDefinition)[T]>; +type TypeOfKey = SqlType< + (typeof settingsDefinition)[T] +>; From 1d3d04e1d724ed866f985024e30ef70b7b536705 Mon Sep 17 00:00:00 2001 From: froxcey Date: Mon, 22 Jan 2024 08:51:42 +0800 Subject: [PATCH 020/127] Improve error --- src/commands/serverboard.ts | 4 ++-- src/utils/embeds/errorEmbed.ts | 13 +++++++++---- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/commands/serverboard.ts b/src/commands/serverboard.ts index 40025a6..96ee99e 100644 --- a/src/commands/serverboard.ts +++ b/src/commands/serverboard.ts @@ -24,9 +24,9 @@ export default class Serverboard { .sort((a, b) => b.memberCount - a.memberCount); const pages = guildList.length; - if (pages == 0) return interaction.reply({ embeds: [errorEmbed("No public server found.")] }); + if (pages == 0) return interaction.reply({ embeds: [errorEmbed("No public server found", "By some magical miracle, all the servers using Nebula turned off their visibility. Use /server settings key:`serverboard.shown` value:`TRUE` to make your server publicly visible.")] }); - const argPage = interaction.options.getNumber("page", false)!; + const argPage = interaction.options.get("page", false)!.value as number; let page = (argPage - 1 <= 0 ? 0 : argPage - 1 > pages ? pages - 1 : argPage - 1) || 0; async function getEmbed() { diff --git a/src/utils/embeds/errorEmbed.ts b/src/utils/embeds/errorEmbed.ts index 6a6975b..f596500 100644 --- a/src/utils/embeds/errorEmbed.ts +++ b/src/utils/embeds/errorEmbed.ts @@ -3,13 +3,18 @@ import { genColor } from "../colorGen"; /** * Sends the embed containing an error. - * @param error The error. + * @param title The error. * @param reason The reason of the error. * @returns Embed with the error description. */ -export function errorEmbed(error: string, reason: string) { +export function errorEmbed(title: string, reason?: string) { + + const content = [`**${title}**`] + + if (reason != undefined) content.push(reason) + return new EmbedBuilder() - .setTitle("❌ • Error!") - .setDescription([`**${error}**`, `Reason: ${reason}`].join("\n")) + .setTitle("❌ • Something went wrong!") + .setDescription(content.join("\n")) .setColor(genColor(0)); } From 1fa636b21881fec9c31da4a3f51f66e43e06e632 Mon Sep 17 00:00:00 2001 From: Serge Date: Fri, 26 Jan 2024 17:58:04 +0600 Subject: [PATCH 021/127] prettified all files + fixed some bugs --- .prettierrc => .prettierrc.json | 0 package.json | 1 - src/bot.ts | 2 +- src/commands/about.ts | 24 ++++-- src/commands/moderation/ban.ts | 72 +++++++++------- src/commands/moderation/delwarn.ts | 72 ++++++++++------ src/commands/moderation/kick.ts | 74 ++++++++++------- src/commands/moderation/mute.ts | 96 +++++++++++++--------- src/commands/moderation/purge.ts | 76 +++++++++++------ src/commands/moderation/unban.ts | 38 +++++---- src/commands/moderation/unmute.ts | 40 +++++---- src/commands/moderation/warn.ts | 79 +++++++++++------- src/commands/moderation/warns.ts | 44 ++++++---- src/commands/news.ts | 22 +++-- src/commands/server/leaderboard.ts | 42 ---------- src/commands/server/news.ts | 27 ++++-- src/commands/server/settings.ts | 50 ++++++----- src/commands/serverboard.ts | 32 +++++--- src/commands/user/info.ts | 54 +++++++----- src/commands/user/level.ts | 42 ++++++---- src/events/easterEggs/Bread.ts | 6 +- src/events/easterEggs/Fireship.ts | 7 +- src/events/easterEggs/Honk.ts | 3 +- src/events/guildCreate.ts | 27 +++--- src/events/guildMemberAdd.ts | 40 --------- src/events/guildMemberRemove.ts | 40 --------- src/events/interactionCreate.ts | 15 +++- src/events/messageCreate.ts | 37 +++++---- src/handlers/commands.ts | 43 ++++------ src/utils/database/disabledCommands.ts | 8 +- src/utils/database/index.ts | 4 +- src/utils/database/levelBlockedChannels.ts | 16 +++- src/utils/database/levelRewards.ts | 8 +- src/utils/database/levelling.ts | 8 +- src/utils/database/moderation.ts | 18 +++- src/utils/database/news.ts | 12 ++- src/utils/database/newsCategories.ts | 25 ++++-- src/utils/database/settings.ts | 44 ++++------ src/utils/database/types.ts | 8 +- src/utils/embeds/errorEmbed.ts | 6 +- src/utils/embeds/serverEmbed.ts | 12 ++- src/utils/quickSort.ts | 10 ++- 42 files changed, 710 insertions(+), 574 deletions(-) rename .prettierrc => .prettierrc.json (100%) delete mode 100644 src/commands/server/leaderboard.ts delete mode 100644 src/events/guildMemberAdd.ts delete mode 100644 src/events/guildMemberRemove.ts diff --git a/.prettierrc b/.prettierrc.json similarity index 100% rename from .prettierrc rename to .prettierrc.json diff --git a/package.json b/package.json index 3a2484d..1997b11 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,6 @@ "version": "0.1.0", "main": "./src/bot.ts", "type": "module", - "prettier": "./.prettierrc", "scripts": { "start": "bun ./src/bot.ts" }, diff --git a/src/bot.ts b/src/bot.ts index f84ed93..8a818f7 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -4,7 +4,7 @@ import Events from "./handlers/events"; const client = new Client({ presence: { - activities: [{ name: "with /settings!", type: ActivityType.Playing }] + activities: [{ name: "with the dashboard!", type: ActivityType.Playing }] }, intents: [ "Guilds", diff --git a/src/commands/about.ts b/src/commands/about.ts index a5fb60a..b395742 100644 --- a/src/commands/about.ts +++ b/src/commands/about.ts @@ -1,4 +1,8 @@ -import { SlashCommandSubcommandBuilder, EmbedBuilder, type ChatInputCommandInteraction } from "discord.js"; +import { + SlashCommandSubcommandBuilder, + EmbedBuilder, + type ChatInputCommandInteraction +} from "discord.js"; import { genColor } from "../utils/colorGen"; import { randomise } from "../utils/randomise"; @@ -14,22 +18,27 @@ export default class About { const client = interaction.client; const guilds = client.guilds.cache; const shards = client.shard?.count; + const hearts = ["💖", "💝", "💓", "💗", "💘", "💟", "💕", "💞"]; const embed = new EmbedBuilder() .setAuthor({ name: "• About", iconURL: client.user.displayAvatarURL() }) - .setDescription("Nebula is a multiplatform, multipurpose bot with the ability to add extensions to have additional features.") + .setDescription( + "Nebula is a multiplatform, multipurpose bot with the ability to add extensions to have additional features." + ) .setFields( { name: "📃 • General", value: [ - "**Version** 0.1, *Dasshubodo update*", - `**${guilds.size}** guild${guilds.size === 1 ? "" : "s"} ${shards == undefined ? "" : `• **${shards}** shard${shards === 1 ? "" : "s"}`}` + "**Version** 0.1-pre", + `**${guilds.size}** guild${guilds.size === 1 ? "" : "s"} ${ + shards == undefined ? "" : `• **${shards}** shard${shards === 1 ? "" : "s"}` + }` ].join("\n") }, { name: "🌌 • Entities involved", value: [ "**Head developer**: Goos", - "**Developers**: Golem64, Pigpot, ThatBOI", + "**Developers**: Froxcey, Golem64, Pigpot, ThatBOI", "**Designers**: ArtyH, Optix, proJM, Slider_on_the_black", "**Translators**: Dimkauzh, Golem64, Optix, SaFire, ThatBOI", "And **YOU**, for using Nebula." @@ -37,10 +46,11 @@ export default class About { }, { name: "🔗 • Links", - value: "[GitHub](https://www.github.com/NebulaTheBot)・[YouTube](https://www.youtube.com/@NebulaTheBot)・[Instagram](https://instagram.com/NebulaTheBot)・[Mastodon](https://mastodon.online/@NebulaTheBot@mastodon.social)・[Guilded](https://guilded.gg/Nebula)・[Revolt](https://rvlt.gg/28TS9aXy)" + value: + "[GitHub](https://www.github.com/NebulaTheBot)・[YouTube](https://www.youtube.com/@NebulaTheBot)・[Instagram](https://instagram.com/NebulaTheBot)・[Mastodon](https://mastodon.online/@NebulaTheBot@mastodon.social)・[Guilded](https://guilded.gg/Nebula)・[Revolt](https://rvlt.gg/28TS9aXy)" } ) - .setFooter({ text: `Made by the Nebula team with ${randomise(["💖", "💝", "💓", "💗", "💘", "💟", "💕", "💞"])}` }) + .setFooter({ text: `Made by the Nebula team with ${randomise(hearts)}` }) .setThumbnail(client.user.displayAvatarURL()) .setColor(genColor(270)); diff --git a/src/commands/moderation/ban.ts b/src/commands/moderation/ban.ts index c6502ef..bca5dd8 100644 --- a/src/commands/moderation/ban.ts +++ b/src/commands/moderation/ban.ts @@ -1,7 +1,12 @@ import { - SlashCommandSubcommandBuilder, EmbedBuilder, PermissionsBitField, - TextChannel, DMChannel, ChannelType, - type Channel, type ChatInputCommandInteraction + SlashCommandSubcommandBuilder, + EmbedBuilder, + PermissionsBitField, + TextChannel, + DMChannel, + ChannelType, + type Channel, + type ChatInputCommandInteraction } from "discord.js"; import { genColor } from "../../utils/colorGen"; import { errorEmbed } from "../../utils/embeds/errorEmbed"; @@ -13,14 +18,11 @@ export default class Ban { this.data = new SlashCommandSubcommandBuilder() .setName("ban") .setDescription("Bans a user.") - .addUserOption(option => option - .setName("user") - .setDescription("The user that you want to ban.") - .setRequired(true) + .addUserOption(user => + user.setName("user").setDescription("The user that you want to ban.").setRequired(true) ) - .addStringOption(option => option - .setName("reason") - .setDescription("The reason for the ban.") + .addStringOption(string => + string.setName("reason").setDescription("The reason for the ban.") ); } @@ -32,32 +34,43 @@ export default class Ban { const target = members.get(user.id)!; const name = user.displayName; - if (!member.permissions.has(PermissionsBitField.Flags.BanMembers)) return await interaction.reply({ - embeds: [errorEmbed("You need the **Ban Members** permission to execute this command.")] - }); + if (!member.permissions.has(PermissionsBitField.Flags.BanMembers)) + return await interaction.reply({ + embeds: [errorEmbed("You can't execute this command.", "You need the **Ban Members** permission to execute this command.")] + }); - if (target === member) return await interaction.reply({ embeds: [errorEmbed("You can't ban yourself.")] }); + if (target === member) + return await interaction.reply({ embeds: [errorEmbed(`You can't ban ${name}.`, "The member is **you**.\n# WHY")] }); - if (target.user.id === interaction.client.user.id) return await interaction.reply({ - embeds: [errorEmbed("You can't ban Nebula.")] - }); + if (target.user.id === interaction.client.user.id) + return await interaction.reply({ + embeds: [errorEmbed(`You can't ban ${name}.`, "The member is Nebula (why do you want to ban Nebula D:)")] + }); - if (!target.manageable) return await interaction.reply({ - embeds: [errorEmbed(`You can't ban ${name}`, "The member has a higher role position than Nebula.")] - }); + if (!target.manageable) + return await interaction.reply({ + embeds: [ + errorEmbed(`You can't ban ${name}.`, "The member has a higher role position than Nebula.") + ] + }); - if (member.roles.highest.position < target.roles.highest.position) return await interaction.reply({ - embeds: [errorEmbed(`You can't ban ${name}`, "The member has a higher role position than you.")] - }); + if (member.roles.highest.position < target.roles.highest.position) + return await interaction.reply({ + embeds: [ + errorEmbed(`You can't ban ${name}.`, "The member has a higher role position than you.") + ] + }); const reason = interaction.options.getString("reason"); const embed = new EmbedBuilder() .setAuthor({ name: `• ${name}`, iconURL: user.displayAvatarURL() }) .setTitle(`✅ • Banned ${name}`) - .setDescription([ - `**Moderator**: ${interaction.user.displayName}`, - `**Reason**: ${reason ?? "No reason provided"}` - ].join("\n")) + .setDescription( + [ + `**Moderator**: ${interaction.user.displayName}`, + `**Reason**: ${reason ?? "No reason provided"}` + ].join("\n") + ) .setThumbnail(user.displayAvatarURL()) .setFooter({ text: `User ID: ${user.id}` }) .setColor(genColor(100)); @@ -78,7 +91,10 @@ export default class Ban { await target.ban({ reason: reason ?? undefined }); const dmChannel = (await user.createDM().catch(() => null)) as DMChannel | null; - if (dmChannel) await dmChannel.send({ embeds: [embed.setTitle("🔨 • You were banned").setColor(genColor(0))] }); + if (dmChannel) + await dmChannel.send({ + embeds: [embed.setTitle("🔨 • You were banned").setColor(genColor(0))] + }); await interaction.reply({ embeds: [embed] }); } } diff --git a/src/commands/moderation/delwarn.ts b/src/commands/moderation/delwarn.ts index f266208..3ea2ebf 100644 --- a/src/commands/moderation/delwarn.ts +++ b/src/commands/moderation/delwarn.ts @@ -1,7 +1,12 @@ import { - SlashCommandSubcommandBuilder, EmbedBuilder, PermissionsBitField, - TextChannel, DMChannel, ChannelType, - type Channel, type ChatInputCommandInteraction + SlashCommandSubcommandBuilder, + EmbedBuilder, + PermissionsBitField, + TextChannel, + DMChannel, + ChannelType, + type Channel, + type ChatInputCommandInteraction } from "discord.js"; import { genColor } from "../../utils/colorGen"; import { errorEmbed } from "../../utils/embeds/errorEmbed"; @@ -14,15 +19,14 @@ export default class Delwarn { this.data = new SlashCommandSubcommandBuilder() .setName("delwarn") .setDescription("Removes a warning from a user.") - .addUserOption(user => user - .setName("user") - .setDescription("The user that you want to free from the warning.") - .setRequired(true) + .addUserOption(user => + user + .setName("user") + .setDescription("The user that you want to free from the warning.") + .setRequired(true) ) - .addNumberOption(string => string - .setName("id") - .setDescription("The id of the warn.") - .setRequired(true) + .addNumberOption(number => + number.setName("id").setDescription("The id of the warn.").setRequired(true) ); } @@ -37,23 +41,40 @@ export default class Delwarn { const warns = listUserModeration(guild.id, user.id, "WARN"); const newWarns = warns.filter(warn => warn.id !== `${id}`); - if (!member.permissions.has(PermissionsBitField.Flags.ModerateMembers)) return await interaction.reply({ - embeds: [errorEmbed("You need the **Moderate Members** permission to execute this command.")] - }); + if (!member.permissions.has(PermissionsBitField.Flags.ModerateMembers)) + return await interaction.reply({ + embeds: [ + errorEmbed("You need the **Moderate Members** permission to execute this command.") + ] + }); - if (target === member) return await interaction.reply({ embeds: [errorEmbed("You can't remove a warn from yourself.")] }); + if (target === member) + return await interaction.reply({ + embeds: [errorEmbed("You can't remove a warn from yourself.")] + }); - if (newWarns.length === warns.length) return await interaction.reply({ - embeds: [errorEmbed(`There is no warn with the id of ${id}.`)] - }); + if (newWarns.length === warns.length) + return await interaction.reply({ + embeds: [errorEmbed(`There is no warn with the id of ${id}.`)] + }); - if (!target.manageable) return await interaction.reply({ - embeds: [errorEmbed(`You can't delete a warn from ${name}, because they have a higher role position than Nebula.`)] - }); + if (!target.manageable) + return await interaction.reply({ + embeds: [ + errorEmbed( + `You can't delete a warn from ${name}, because they have a higher role position than Nebula.` + ) + ] + }); - if (member.roles.highest.position < target.roles.highest.position) return await interaction.reply({ - embeds: [errorEmbed(`You can't delete a warn from ${name}, because they have a higher role position than you.`)] - }); + if (member.roles.highest.position < target.roles.highest.position) + return await interaction.reply({ + embeds: [ + errorEmbed( + `You can't delete a warn from ${name}, because they have a higher role position than you.` + ) + ] + }); const embed = new EmbedBuilder() .setAuthor({ name: `• ${user.username}`, iconURL: user.displayAvatarURL() }) @@ -79,7 +100,8 @@ export default class Delwarn { removeModeration(guild.id, `${id}`); const dmChannel = (await user.createDM().catch(() => null)) as DMChannel | null; - if (dmChannel) await dmChannel.send({ embeds: [embed.setTitle("🤝 • Your warning was removed")] }); + if (dmChannel) + await dmChannel.send({ embeds: [embed.setTitle("🤝 • Your warning was removed")] }); await interaction.reply({ embeds: [embed] }); } } diff --git a/src/commands/moderation/kick.ts b/src/commands/moderation/kick.ts index af21a4c..161d615 100644 --- a/src/commands/moderation/kick.ts +++ b/src/commands/moderation/kick.ts @@ -1,7 +1,12 @@ import { - SlashCommandSubcommandBuilder, EmbedBuilder, PermissionsBitField, - TextChannel, DMChannel, ChannelType, - type Channel, type ChatInputCommandInteraction + SlashCommandSubcommandBuilder, + EmbedBuilder, + PermissionsBitField, + TextChannel, + DMChannel, + ChannelType, + type Channel, + type ChatInputCommandInteraction } from "discord.js"; import { genColor } from "../../utils/colorGen"; import { errorEmbed } from "../../utils/embeds/errorEmbed"; @@ -13,14 +18,11 @@ export default class Kick { this.data = new SlashCommandSubcommandBuilder() .setName("kick") .setDescription("Kicks a user.") - .addUserOption(option => option - .setName("user") - .setDescription("The user that you want to kick.") - .setRequired(true) + .addUserOption(user => + user.setName("user").setDescription("The user that you want to kick.").setRequired(true) ) - .addStringOption(option => option - .setName("reason") - .setDescription("The reason for the kick.") + .addStringOption(string => + string.setName("reason").setDescription("The reason for the kick.") ); } @@ -32,32 +34,45 @@ export default class Kick { const target = members.get(user.id)!; const name = target.nickname ?? user.username; - if (!member.permissions.has(PermissionsBitField.Flags.KickMembers)) return await interaction.reply({ - embeds: [errorEmbed("You need the **Kick Members** permission to execute this command.")] - }); + if (!member.permissions.has(PermissionsBitField.Flags.KickMembers)) + return await interaction.reply({ + embeds: [errorEmbed("You need the **Kick Members** permission to execute this command.")] + }); - if (target === member) return await interaction.reply({ embeds: [errorEmbed("You can't kick yourself.")] }); + if (target === member) + return await interaction.reply({ embeds: [errorEmbed("You can't kick yourself.")] }); - if (target.user.id === interaction.client.user.id) return await interaction.reply({ - embeds: [errorEmbed("You can't kick Nebula.")] - }); + if (target.user.id === interaction.client.user.id) + return await interaction.reply({ + embeds: [errorEmbed("You can't kick Nebula.")] + }); - if (!target.manageable) return await interaction.reply({ - embeds: [errorEmbed(`You can't kick ${name}, because they have a higher role position than Nebula.`)] - }); + if (!target.manageable) + return await interaction.reply({ + embeds: [ + errorEmbed( + `You can't kick ${name}, because they have a higher role position than Nebula.` + ) + ] + }); - if (member.roles.highest.position < target.roles.highest.position) return await interaction.reply({ - embeds: [errorEmbed(`You can't kick ${name}, because they have a higher role position than you.`)] - }); + if (member.roles.highest.position < target.roles.highest.position) + return await interaction.reply({ + embeds: [ + errorEmbed(`You can't kick ${name}, because they have a higher role position than you.`) + ] + }); const reason = interaction.options.getString("reason"); const embed = new EmbedBuilder() .setAuthor({ name: `• ${user.username}`, iconURL: user.displayAvatarURL() }) .setTitle(`✅ • Kicked <@${user.id}>`) - .setDescription([ - `**Moderator**: ${interaction.user.username}`, - `**Reason**: ${reason ?? "No reason provided"}` - ].join("\n")) + .setDescription( + [ + `**Moderator**: ${interaction.user.username}`, + `**Reason**: ${reason ?? "No reason provided"}` + ].join("\n") + ) .setThumbnail(user.displayAvatarURL()) .setFooter({ text: `User ID: ${user.id}` }) .setColor(genColor(100)); @@ -78,7 +93,10 @@ export default class Kick { await target.kick(reason ?? undefined); const dmChannel = (await user.createDM().catch(() => null)) as DMChannel | null; - if (dmChannel) await dmChannel.send({ embeds: [embed.setTitle("🥾 • You were kicked").setColor(genColor(0))] }); + if (dmChannel) + await dmChannel.send({ + embeds: [embed.setTitle("🥾 • You were kicked").setColor(genColor(0))] + }); await interaction.reply({ embeds: [embed] }); } } diff --git a/src/commands/moderation/mute.ts b/src/commands/moderation/mute.ts index 685871c..378085a 100644 --- a/src/commands/moderation/mute.ts +++ b/src/commands/moderation/mute.ts @@ -1,7 +1,12 @@ import { - SlashCommandSubcommandBuilder, EmbedBuilder, PermissionsBitField, - TextChannel, DMChannel, ChannelType, - type Channel, type ChatInputCommandInteraction + SlashCommandSubcommandBuilder, + EmbedBuilder, + PermissionsBitField, + TextChannel, + DMChannel, + ChannelType, + type Channel, + type ChatInputCommandInteraction } from "discord.js"; import { genColor } from "../../utils/colorGen"; import { errorEmbed } from "../../utils/embeds/errorEmbed"; @@ -14,19 +19,17 @@ export default class Mute { this.data = new SlashCommandSubcommandBuilder() .setName("mute") .setDescription("Mutes a user.") - .addUserOption(user => user - .setName("user") - .setDescription("The user that you want to mute.") - .setRequired(true) + .addUserOption(user => + user.setName("user").setDescription("The user that you want to mute.").setRequired(true) ) - .addStringOption(string => string - .setName("duration") - .setDescription("The duration of the mute (e.g 30m, 1d, 2h)") - .setRequired(true) + .addStringOption(string => + string + .setName("duration") + .setDescription("The duration of the mute (e.g 30m, 1d, 2h)") + .setRequired(true) ) - .addStringOption(string => string - .setName("reason") - .setDescription("The reason for the mute.") + .addStringOption(string => + string.setName("reason").setDescription("The reason for the mute.") ); } @@ -39,37 +42,53 @@ export default class Mute { const target = members.get(user.id)!; const name = target.nickname ?? user.username; - if (!member.permissions.has(PermissionsBitField.Flags.MuteMembers)) return await interaction.reply({ - embeds: [errorEmbed("You need the **Mute Members** permission to execute this command.")] - }); + if (!member.permissions.has(PermissionsBitField.Flags.MuteMembers)) + return await interaction.reply({ + embeds: [errorEmbed("You need the **Mute Members** permission to execute this command.")] + }); - if (target === member) return await interaction.reply({ embeds: [errorEmbed("You can't mute yourself.")] }); + if (target === member) + return await interaction.reply({ embeds: [errorEmbed("You can't mute yourself.")] }); - if (target.user.id === interaction.client.user.id) return await interaction.reply({ - embeds: [errorEmbed("You can't mute Nebula.")] - }); + if (target.user.id === interaction.client.user.id) + return await interaction.reply({ + embeds: [errorEmbed("You can't mute Nebula.")] + }); - if (!target.manageable) return await interaction.reply({ - embeds: [errorEmbed(`You can't mute ${name}, because they have a higher role position than Nebula.`)] - }); + if (!target.manageable) + return await interaction.reply({ + embeds: [ + errorEmbed( + `You can't mute ${name}, because they have a higher role position than Nebula.` + ) + ] + }); - if (member.roles.highest.position < target.roles.highest.position) return await interaction.reply({ - embeds: [errorEmbed(`You can't mute ${name}, because they have a higher role position than you.`)] - }); + if (member.roles.highest.position < target.roles.highest.position) + return await interaction.reply({ + embeds: [ + errorEmbed(`You can't mute ${name}, because they have a higher role position than you.`) + ] + }); - if (!ms(duration) || ms(duration) > ms("28d")) return await interaction.reply({ - embeds: [errorEmbed("The duration is invalid or is above the 28 day limit.")] - }); + if (!ms(duration) || ms(duration) > ms("28d")) + return await interaction.reply({ + embeds: [errorEmbed("The duration is invalid or is above the 28 day limit.")] + }); - const time = new Date(Date.parse(new Date().toISOString()) + Date.parse(new Date(ms(duration)).toISOString())).toISOString(); + const time = new Date( + Date.parse(new Date().toISOString()) + Date.parse(new Date(ms(duration)).toISOString()) + ).toISOString(); const embed = new EmbedBuilder() .setAuthor({ name: `• ${user.username}`, iconURL: user.displayAvatarURL() }) .setTitle(`✅ • Muted ${user.username}`) - .setDescription([ - `**Moderator**: ${interaction.user.username}`, - `**Duration**: ${ms(ms(duration), { long: true })}`, - `**Reason**: ${interaction.options.getString("reason") ?? "No reason provided"}` - ].join("\n")) + .setDescription( + [ + `**Moderator**: ${interaction.user.username}`, + `**Duration**: ${ms(ms(duration), { long: true })}`, + `**Reason**: ${interaction.options.getString("reason") ?? "No reason provided"}` + ].join("\n") + ) .setFooter({ text: `User ID: ${user.id}` }) .setThumbnail(user.displayAvatarURL()) .setColor(genColor(100)); @@ -90,7 +109,10 @@ export default class Mute { await target.edit({ communicationDisabledUntil: time }); const dmChannel = (await user.createDM().catch(() => null)) as DMChannel | null; - if (dmChannel) await dmChannel.send({ embeds: [embed.setTitle("🤐 • You were muted").setColor(genColor(0))] }); + if (dmChannel) + await dmChannel.send({ + embeds: [embed.setTitle("🤐 • You were muted").setColor(genColor(0))] + }); await interaction.reply({ embeds: [embed] }); } } diff --git a/src/commands/moderation/purge.ts b/src/commands/moderation/purge.ts index 8750f9b..ff60019 100644 --- a/src/commands/moderation/purge.ts +++ b/src/commands/moderation/purge.ts @@ -1,6 +1,10 @@ import { - SlashCommandSubcommandBuilder, EmbedBuilder, PermissionsBitField, - ChannelType, TextChannel, type Channel, + SlashCommandSubcommandBuilder, + EmbedBuilder, + PermissionsBitField, + ChannelType, + TextChannel, + type Channel, type ChatInputCommandInteraction } from "discord.js"; import { genColor } from "../../utils/colorGen"; @@ -13,15 +17,23 @@ export default class Purge { this.data = new SlashCommandSubcommandBuilder() .setName("purge") .setDescription("Purges messages.") - .addNumberOption(number => number - .setName("amount") - .setDescription("The amount of messages that you want to purge.") - .setRequired(true) + .addNumberOption(number => + number + .setName("amount") + .setDescription("The amount of messages that you want to purge.") + .setRequired(true) ) - .addChannelOption(channel => channel - .setName("channel") - .setDescription("The channel that you want to purge. Leave empty if you want to purge the current channel.") - .addChannelTypes(ChannelType.GuildText, ChannelType.PublicThread, ChannelType.PrivateThread) + .addChannelOption(channel => + channel + .setName("channel") + .setDescription( + "The channel that you want to purge. Leave empty if you want to purge the current channel." + ) + .addChannelTypes( + ChannelType.GuildText, + ChannelType.PublicThread, + ChannelType.PrivateThread + ) ); } @@ -30,33 +42,43 @@ export default class Purge { const amount = interaction.options.getNumber("amount")!; const member = guild.members.cache.get(interaction.member?.user.id!)!; - if (!member.permissions.has(PermissionsBitField.Flags.ManageMessages)) return await interaction.reply({ - embeds: [errorEmbed("You need the **Manage Messages** permission to execute this command.")], - }); + if (!member.permissions.has(PermissionsBitField.Flags.ManageMessages)) + return await interaction.reply({ + embeds: [errorEmbed("You need the **Manage Messages** permission to execute this command.")] + }); - if (amount > 100) return await interaction.reply({ - embeds: [errorEmbed("You can only purge up to 100 messages at a time.")], - }); + if (amount > 100) + return await interaction.reply({ + embeds: [errorEmbed("You can only purge up to 100 messages at a time.")] + }); - if (amount < 1) return await interaction.reply({ - embeds: [errorEmbed("You must purge at least 1 message.")], - }); + if (amount < 1) + return await interaction.reply({ + embeds: [errorEmbed("You must purge at least 1 message.")] + }); const channelOption = interaction.options.getChannel("channel")!; const channel = guild.channels.cache.get(interaction.channel?.id ?? channelOption.id)!; const embed = new EmbedBuilder() .setTitle(`✅ • Purged ${amount} messages.`) - .setDescription([ - `**Moderator**: ${interaction.user.username}`, - `**Channel**: ${channelOption ?? `<#${channel.id}>`}`, - ].join("\n")) + .setDescription( + [ + `**Moderator**: ${interaction.user.username}`, + `**Channel**: ${channelOption ?? `<#${channel.id}>`}` + ].join("\n") + ) .setColor(genColor(100)); - if (channel.type === ChannelType.GuildText && ChannelType.PublicThread && ChannelType.PrivateThread) channel == interaction.channel - ? await channel.bulkDelete(amount + 1, true) - : await channel.bulkDelete(amount, true); + if ( + channel.type === ChannelType.GuildText && + ChannelType.PublicThread && + ChannelType.PrivateThread + ) + channel == interaction.channel + ? await channel.bulkDelete(amount + 1, true) + : await channel.bulkDelete(amount, true); - const logChannel = getSetting(guild.id, "log.channel"); + const logChannel = getSetting(guild.id, "log.channel"); if (logChannel) { const channel = await guild.channels.cache .get(`${logChannel}`) diff --git a/src/commands/moderation/unban.ts b/src/commands/moderation/unban.ts index 4e1c35a..6d9e8ce 100644 --- a/src/commands/moderation/unban.ts +++ b/src/commands/moderation/unban.ts @@ -1,7 +1,12 @@ import { - PermissionsBitField, EmbedBuilder, SlashCommandSubcommandBuilder, - TextChannel, DMChannel, ChannelType, - type Channel, type ChatInputCommandInteraction + PermissionsBitField, + EmbedBuilder, + SlashCommandSubcommandBuilder, + TextChannel, + DMChannel, + ChannelType, + type Channel, + type ChatInputCommandInteraction } from "discord.js"; import { genColor } from "../../utils/colorGen"; import { errorEmbed } from "../../utils/embeds/errorEmbed"; @@ -13,26 +18,29 @@ export default class Unban { this.data = new SlashCommandSubcommandBuilder() .setName("unban") .setDescription("Unbans a user.") - .addStringOption(string => string - .setName("id") - .setDescription("The ID of the user that you want to unban.") - .setRequired(true) - ); + .addStringOption(string => + string + .setName("id") + .setDescription("The ID of the user that you want to unban.") + .setRequired(true) + ); } async run(interaction: ChatInputCommandInteraction) { const id = interaction.options.getString("id")!; const guild = interaction.guild!; const member = guild.members.cache.get(interaction.member?.user.id!)!; - const target = (guild.bans.cache.map(user => user.user)).filter(user => user.id === id)[0]!; + const target = guild.bans.cache.map(user => user.user).filter(user => user.id === id)[0]!; - if (!member.permissions.has(PermissionsBitField.Flags.BanMembers)) return await interaction.reply({ - embeds: [errorEmbed("You need the **Ban Members** permission to execute this command.")] - }); + if (!member.permissions.has(PermissionsBitField.Flags.BanMembers)) + return await interaction.reply({ + embeds: [errorEmbed("You need the **Ban Members** permission to execute this command.")] + }); - if (target == undefined) return await interaction.reply({ - embeds: [errorEmbed("You can't unban this user because they were never banned.")] - }); + if (target == undefined) + return await interaction.reply({ + embeds: [errorEmbed("You can't unban this user because they were never banned.")] + }); const embed = new EmbedBuilder() .setAuthor({ name: target.username, iconURL: target.displayAvatarURL() }) diff --git a/src/commands/moderation/unmute.ts b/src/commands/moderation/unmute.ts index d00c2ea..2bdf81f 100644 --- a/src/commands/moderation/unmute.ts +++ b/src/commands/moderation/unmute.ts @@ -1,7 +1,12 @@ import { - SlashCommandSubcommandBuilder, EmbedBuilder, PermissionsBitField, - TextChannel, DMChannel, ChannelType, - type Channel, type ChatInputCommandInteraction + SlashCommandSubcommandBuilder, + EmbedBuilder, + PermissionsBitField, + TextChannel, + DMChannel, + ChannelType, + type Channel, + type ChatInputCommandInteraction } from "discord.js"; import { genColor } from "../../utils/colorGen"; import { errorEmbed } from "../../utils/embeds/errorEmbed"; @@ -13,33 +18,38 @@ export default class Unmute { this.data = new SlashCommandSubcommandBuilder() .setName("unmute") .setDescription("Unmutes a user.") - .addUserOption(user => user - .setName("user") - .setDescription("The user that you want to unmute.") - .setRequired(true) + .addUserOption(user => + user.setName("user").setDescription("The user that you want to unmute.").setRequired(true) ); } async run(interaction: ChatInputCommandInteraction) { const guild = interaction.guild!; const members = guild.members.cache!; - if (!members.get(interaction.member!.user.id)!.permissions.has(PermissionsBitField.Flags.MuteMembers)) + if ( + !members + .get(interaction.member!.user.id)! + .permissions.has(PermissionsBitField.Flags.MuteMembers) + ) return await interaction.reply({ embeds: [errorEmbed("You need the **Mute Members** permission to execute this command.")] }); const user = interaction.options.getUser("user")!; - if (members.get(user.id)?.communicationDisabledUntil === null) return await interaction.reply({ - embeds: [errorEmbed("You can't unmute this user because they were never muted.")] - }); + if (members.get(user.id)?.communicationDisabledUntil === null) + return await interaction.reply({ + embeds: [errorEmbed("You can't unmute this user because they were never muted.")] + }); const embed = new EmbedBuilder() .setAuthor({ name: `• ${user.username}`, iconURL: user.displayAvatarURL() }) .setTitle(`✅ • Unmuted ${user.username}`) - .setDescription([ - `**Moderator**: ${interaction.user.username}`, - `**Date**: ` - ].join("\n")) + .setDescription( + [ + `**Moderator**: ${interaction.user.username}`, + `**Date**: ` + ].join("\n") + ) .setThumbnail(user.displayAvatarURL()) .setFooter({ text: `User ID: ${user.id}` }) .setColor(genColor(100)); diff --git a/src/commands/moderation/warn.ts b/src/commands/moderation/warn.ts index e0d956a..34ce9ae 100644 --- a/src/commands/moderation/warn.ts +++ b/src/commands/moderation/warn.ts @@ -1,7 +1,12 @@ import { - SlashCommandSubcommandBuilder, EmbedBuilder, PermissionsBitField, - TextChannel, DMChannel, ChannelType, - type Channel, type ChatInputCommandInteraction + SlashCommandSubcommandBuilder, + EmbedBuilder, + PermissionsBitField, + TextChannel, + DMChannel, + ChannelType, + type Channel, + type ChatInputCommandInteraction } from "discord.js"; import { genColor } from "../../utils/colorGen"; import { errorEmbed } from "../../utils/embeds/errorEmbed"; @@ -14,14 +19,11 @@ export default class Warn { this.data = new SlashCommandSubcommandBuilder() .setName("warn") .setDescription("Warns a user.") - .addUserOption(user => user - .setName("user") - .setDescription("The user that you want to warn.") - .setRequired(true) + .addUserOption(user => + user.setName("user").setDescription("The user that you want to warn.").setRequired(true) ) - .addStringOption(string => string - .setName("reason") - .setDescription("The reason for the warn.") + .addStringOption(string => + string.setName("reason").setDescription("The reason for the warn.") ); } @@ -33,32 +35,47 @@ export default class Warn { const target = members.get(user.id)!; const name = target.nickname ?? user.username; - if (!member.permissions.has(PermissionsBitField.Flags.ModerateMembers)) return await interaction.reply({ - embeds: [errorEmbed("You need the **Moderate Members** permission to execute this command.")] - }); + if (!member.permissions.has(PermissionsBitField.Flags.ModerateMembers)) + return await interaction.reply({ + embeds: [ + errorEmbed("You need the **Moderate Members** permission to execute this command.") + ] + }); - if (target === member) return await interaction.reply({ embeds: [errorEmbed("You can't warn yourself.")] }); + if (target === member) + return await interaction.reply({ embeds: [errorEmbed("You can't warn yourself.")] }); - if (target.user.id === interaction.client.user.id) return await interaction.reply({ - embeds: [errorEmbed("You can't warn Nebula.")] - }); + if (target.user.id === interaction.client.user.id) + return await interaction.reply({ + embeds: [errorEmbed("You can't warn Nebula.")] + }); - if (!target.manageable) return await interaction.reply({ - embeds: [errorEmbed(`You can't warn ${name}, because they have a higher role position than Nebula.`)] - }); + if (!target.manageable) + return await interaction.reply({ + embeds: [ + errorEmbed( + `You can't warn ${name}, because they have a higher role position than Nebula.` + ) + ] + }); - if (member.roles.highest.position < target.roles.highest.position) return await interaction.reply({ - embeds: [errorEmbed(`You can't warn ${name}, because they have a higher role position than you.`)] - }); + if (member.roles.highest.position < target.roles.highest.position) + return await interaction.reply({ + embeds: [ + errorEmbed(`You can't warn ${name}, because they have a higher role position than you.`) + ] + }); const reason = interaction.options.getString("reason"); const embed = new EmbedBuilder() .setAuthor({ name: `• ${user.username}`, iconURL: user.displayAvatarURL() }) .setTitle(`✅ • Warned ${user.username}`) - .setDescription([ - `**Moderator**: ${interaction.user.username}`, - `**Reason**: ${reason ?? "No reason provided"}` - ].join("\n")) + .setDescription( + [ + `**Moderator**: ${interaction.user.username}`, + `**Reason**: ${reason ?? "No reason provided"}` + ].join("\n") + ) .setThumbnail(user.displayAvatarURL()) .setFooter({ text: `User ID: ${user.id}` }) .setColor(genColor(100)); @@ -71,14 +88,18 @@ export default class Warn { .then((channel: Channel) => { if (channel.type != ChannelType.GuildText) return null; return channel as TextChannel; - }).catch(() => null); + }) + .catch(() => null); if (channel) await channel.send({ embeds: [embed] }); } addModeration(guild.id, user.id, "WARN", member.id, reason ?? undefined); const dmChannel = (await user.createDM().catch(() => null)) as DMChannel | null; - if (dmChannel) await dmChannel.send({ embeds: [embed.setTitle("⚠️ • You were warned").setColor(genColor(0))] }); + if (dmChannel) + await dmChannel.send({ + embeds: [embed.setTitle("⚠️ • You were warned").setColor(genColor(0))] + }); await interaction.reply({ embeds: [embed] }); } } diff --git a/src/commands/moderation/warns.ts b/src/commands/moderation/warns.ts index af59ed7..7328e25 100644 --- a/src/commands/moderation/warns.ts +++ b/src/commands/moderation/warns.ts @@ -1,5 +1,7 @@ import { - SlashCommandSubcommandBuilder, EmbedBuilder, PermissionsBitField, + SlashCommandSubcommandBuilder, + EmbedBuilder, + PermissionsBitField, type ChatInputCommandInteraction } from "discord.js"; import { genColor } from "../../utils/colorGen"; @@ -12,18 +14,22 @@ export default class Warns { this.data = new SlashCommandSubcommandBuilder() .setName("warns") .setDescription("Warns of a user.") - .addUserOption(user => user - .setName("user") - .setDescription("The user that you want to see.") - .setRequired(true) + .addUserOption(user => + user.setName("user").setDescription("The user that you want to see.").setRequired(true) ); } async run(interaction: ChatInputCommandInteraction) { const guild = interaction.guild!; - if (!(guild.members.cache.get(interaction.member?.user.id!))?.permissions.has(PermissionsBitField.Flags.ModerateMembers)) + if ( + !guild.members.cache + .get(interaction.member?.user.id!) + ?.permissions.has(PermissionsBitField.Flags.ModerateMembers) + ) return await interaction.reply({ - embeds: [errorEmbed("You need the **Moderate Members** permission to execute this command.")] + embeds: [ + errorEmbed("You need the **Moderate Members** permission to execute this command.") + ] }); const user = interaction.options.getUser("user")!; @@ -31,16 +37,20 @@ export default class Warns { const embed = new EmbedBuilder() .setAuthor({ name: `• ${user.username}`, iconURL: user.displayAvatarURL() }) .setTitle(`✅ • Warns of ${user.username}`) - .setFields(warns.length > 0 ? warns.map(warn => { - return { - name: `#${warn.id}`, - value: [ - `**Moderator**: <@${warn.moderator}>`, - `**Reason**: ${warn.reason}`, - `**Date**: ` - ].join("\n") - }; - }) : [{ name: "No warns", value: "This user has no warns." }]) + .setFields( + warns.length > 0 + ? warns.map(warn => { + return { + name: `#${warn.id}`, + value: [ + `**Moderator**: <@${warn.moderator}>`, + `**Reason**: ${warn.reason}`, + `**Date**: ` + ].join("\n") + }; + }) + : [{ name: "No warns", value: "This user has no warns." }] + ) .setThumbnail(user.displayAvatarURL()) .setFooter({ text: `User ID: ${user.id}` }) .setColor(genColor(100)); diff --git a/src/commands/news.ts b/src/commands/news.ts index 11b9f6c..582edc3 100644 --- a/src/commands/news.ts +++ b/src/commands/news.ts @@ -1,6 +1,10 @@ import { - SlashCommandSubcommandBuilder, EmbedBuilder, ActionRowBuilder, - ButtonBuilder, ButtonStyle, type ChatInputCommandInteraction + SlashCommandSubcommandBuilder, + EmbedBuilder, + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + type ChatInputCommandInteraction } from "discord.js"; import { genColor } from "../utils/colorGen"; import { listAllNews } from "../utils/database/news"; @@ -11,15 +15,16 @@ export default class News { this.data = new SlashCommandSubcommandBuilder() .setName("news") .setDescription("View the news of Nebula.") - .addNumberOption(option => option - .setName("page") - .setDescription("The page of the news that you want to see.") + .addNumberOption(number => + number.setName("page").setDescription("The page of the news that you want to see.") ); } async run(interaction: ChatInputCommandInteraction) { let page = interaction.options.getNumber("page") ?? 1; - const sortedNews = (Object.values(listAllNews("903852579837059113")) as any[])?.sort((a, b) => b.createdAt - a.createdAt); + const sortedNews = (Object.values(listAllNews("903852579837059113")) as any[])?.sort( + (a, b) => b.createdAt - a.createdAt + ); let currentNews = sortedNews[page - 1]; if (page > sortedNews.length) page = sortedNews.length; @@ -47,7 +52,10 @@ export default class News { await interaction.reply({ embeds: [embed], components: [row] }); interaction.channel - ?.createMessageComponentCollector({ filter: i => i.user.id === interaction.user.id, time: 60000 }) + ?.createMessageComponentCollector({ + filter: i => i.user.id === interaction.user.id, + time: 60000 + }) .on("collect", async i => { if (!i.isButton()) return; if (i.customId === "left") { diff --git a/src/commands/server/leaderboard.ts b/src/commands/server/leaderboard.ts deleted file mode 100644 index 6de80b2..0000000 --- a/src/commands/server/leaderboard.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { SlashCommandSubcommandBuilder, EmbedBuilder, type ChatInputCommandInteraction } from "discord.js"; -import { genColor } from "../../utils/colorGen"; -import { errorEmbed } from "../../utils/embeds/errorEmbed"; -import { getSetting } from "../../utils/database/settings"; - -export default class Leaderboard { - data: SlashCommandSubcommandBuilder; - constructor() { - this.data = new SlashCommandSubcommandBuilder() - .setName("leaderboard") - .setDescription("Shows the server's leaderboard in levels."); - } - - async run(interaction: ChatInputCommandInteraction) { - const levelEnabled = getSetting(interaction.guild?.id!, "levelling.enabled"); - const levels = await levelingTable?.get(`${interaction.guild?.id}`).catch(() => { }); - const levelKeys = Object.keys(levels) - const convertLevelsAndExpToExp = (levels: any) => { - const exp = Object.keys(levels).map(level => levels[level].exp); - return exp.reduce((a, b) => a + b); - }; - - if (!levelEnabled) return await interaction.reply({ - embeds: [errorEmbed("Leveling is disabled for this server.")] - }); - - const embed = new EmbedBuilder() - .setTitle("⚡ • Top 10 active members") - .setDescription(levelKeys - .slice(0, 10) - .map(level => [ - `#${Object.keys(levels).indexOf(level) + 1} • <@${level}>`, - `**Level ${levels[level].levels}** - Next Level: ${levels[level].levels + 1}`, - `**Exp**: ${levels[level].exp}/${Math.floor((2 * 50) * 1.25 * (levels[level].levels + 1))} until level up` - ]) - .join("\n\n")) - .setColor(genColor(200)) - .setTimestamp(); - - await interaction.reply({ embeds: [embed] }); - } -} diff --git a/src/commands/server/news.ts b/src/commands/server/news.ts index 925eb3d..3d86b27 100644 --- a/src/commands/server/news.ts +++ b/src/commands/server/news.ts @@ -1,6 +1,10 @@ import { - SlashCommandSubcommandBuilder, EmbedBuilder, ActionRowBuilder, - ButtonBuilder, ButtonStyle, type ChatInputCommandInteraction + SlashCommandSubcommandBuilder, + EmbedBuilder, + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + type ChatInputCommandInteraction } from "discord.js"; import { genColor } from "../../utils/colorGen"; import { errorEmbed } from "../../utils/embeds/errorEmbed"; @@ -12,9 +16,8 @@ export default class News { this.data = new SlashCommandSubcommandBuilder() .setName("news") .setDescription("View the news of this server.") - .addNumberOption(option => option - .setName("page") - .setDescription("The page of the news that you want to see.") + .addNumberOption(number => + number.setName("page").setDescription("The page of the news that you want to see.") ); } @@ -24,9 +27,12 @@ export default class News { const sortedNews = (Object.values(news) as any[])?.sort((a, b) => b.createdAt - a.createdAt); let currentNews = sortedNews[page - 1]; - if (!news || !sortedNews || sortedNews.length == 0) return await interaction.reply({ - embeds: [errorEmbed("No news found.\nAdmins can add news with the **/settings news add** command.")] - }); + if (!news || !sortedNews || sortedNews.length == 0) + return await interaction.reply({ + embeds: [ + errorEmbed("No news found.\nAdmins can add news with the **/settings news add** command.") + ] + }); if (page > sortedNews.length) page = sortedNews.length; if (page < 1) page = 1; @@ -52,7 +58,10 @@ export default class News { await interaction.reply({ embeds: [embed], components: [row] }); interaction.channel - ?.createMessageComponentCollector({ filter: i => i.user.id === interaction.user.id, time: 60000 }) + ?.createMessageComponentCollector({ + filter: i => i.user.id === interaction.user.id, + time: 60000 + }) .on("collect", async i => { if (!i.isButton()) return; if (i.customId === "left") { diff --git a/src/commands/server/settings.ts b/src/commands/server/settings.ts index 5311418..af2bd8c 100644 --- a/src/commands/server/settings.ts +++ b/src/commands/server/settings.ts @@ -1,7 +1,7 @@ import { Client, InteractionType, - SlashCommandStringOption, + EmbedBuilder, SlashCommandSubcommandBuilder, type ChatInputCommandInteraction, } from "discord.js"; @@ -9,8 +9,9 @@ import { getSetting, setSetting, settingsDefinition, - settingsKeys, + settingsKeys } from "../../utils/database/settings"; +import { genColor } from "../../utils/colorGen"; export default class ServerInfo { data: SlashCommandSubcommandBuilder; @@ -18,53 +19,50 @@ export default class ServerInfo { this.data = new SlashCommandSubcommandBuilder() .setName("settings") .setDescription("Configure the bot") - .addStringOption( - new SlashCommandStringOption() + .addStringOption(string => + string .setName("key") .setDescription("The setting key to set") - .addChoices(...settingsKeys.map((key) => ({ name: key, value: key }))) - .setRequired(true), + .addChoices(...settingsKeys.map(key => ({ name: key, value: key }))) + .setRequired(true) ) - .addStringOption( - new SlashCommandStringOption() + .addStringOption(string => + string .setName("value") - .setDescription( - "The value you want to set this option to, or blank for view", - ) - .setAutocomplete(true), + .setDescription("The value you want to set this option to, or blank for view") + .setAutocomplete(true) ); } async run(interaction: ChatInputCommandInteraction) { - const key = interaction.options.get("key")! - .value as keyof typeof settingsDefinition; + const key = interaction.options.get("key")!.value as keyof typeof settingsDefinition; const value = interaction.options.get("value")?.value; if (value == undefined) return interaction.reply( - `\`${key}\` is currently \`${JSON.stringify(getSetting(interaction.guildId!, key))}\``, + `\`${key}\` is currently \`${JSON.stringify(getSetting(interaction.guildId!, key))}\`` ); + const embed = new EmbedBuilder() + .setTitle(`\`${key}\` has been set to \`${value}\``) + .setColor(genColor(100)); + setSetting(interaction.guildId!, key, value); - interaction.reply(`\`${key}\` has been set to \`${value}\``); + interaction.reply({ embeds: [embed] }); } autocompleteHandler(client: Client) { - client.on("interactionCreate", (interaction) => { - if (interaction.type != InteractionType.ApplicationCommandAutocomplete) - return; + client.on("interactionCreate", interaction => { + if (interaction.type != InteractionType.ApplicationCommandAutocomplete) return; if (interaction.options.getSubcommand() != this.data.name) return; switch ( - settingsDefinition[ - interaction.options.get("key")! - .value as keyof typeof settingsDefinition - ] + settingsDefinition[interaction.options.get("key")!.value as keyof typeof settingsDefinition] ) { case "BOOL": interaction.respond( - ["TRUE", "FALSE"].map((choice) => ({ + ["TRUE", "FALSE"].map(choice => ({ name: choice, - value: choice, - })), + value: choice + })) ); break; } diff --git a/src/commands/serverboard.ts b/src/commands/serverboard.ts index 96ee99e..650dcdb 100644 --- a/src/commands/serverboard.ts +++ b/src/commands/serverboard.ts @@ -1,6 +1,10 @@ import { - SlashCommandSubcommandBuilder, ButtonBuilder, ActionRowBuilder, - ButtonStyle, ButtonInteraction, ComponentType, + SlashCommandSubcommandBuilder, + ButtonBuilder, + ActionRowBuilder, + ButtonStyle, + ButtonInteraction, + ComponentType, type ChatInputCommandInteraction } from "discord.js"; import { serverEmbed } from "../utils/embeds/serverEmbed"; @@ -13,20 +17,28 @@ export default class Serverboard { this.data = new SlashCommandSubcommandBuilder() .setName("serverboard") .setDescription("Shows the servers that have Nebula.") - .addNumberOption(option => option - .setName("page") - .setDescription("The page you want to see.") + .addNumberOption(number => + number.setName("page").setDescription("The page you want to see.") ); } async run(interaction: ChatInputCommandInteraction) { - const guildList = (await Promise.all(listPublicServers().map(id => interaction.client.guilds.fetch(id)))) - .sort((a, b) => b.memberCount - a.memberCount); + const guildList = ( + await Promise.all(listPublicServers().map(id => interaction.client.guilds.fetch(id))) + ).sort((a, b) => b.memberCount - a.memberCount); const pages = guildList.length; - if (pages == 0) return interaction.reply({ embeds: [errorEmbed("No public server found", "By some magical miracle, all the servers using Nebula turned off their visibility. Use /server settings key:`serverboard.shown` value:`TRUE` to make your server publicly visible.")] }); + if (pages == 0) + return interaction.reply({ + embeds: [ + errorEmbed( + "No public server found", + "By some magical miracle, all the servers using Nebula turned off their visibility. Use /server settings key:`serverboard.shown` value:`TRUE` to make your server publicly visible." + ) + ] + }); - const argPage = interaction.options.get("page", false)!.value as number; + const argPage = interaction.options.get("page")?.value as number; let page = (argPage - 1 <= 0 ? 0 : argPage - 1 > pages ? pages - 1 : argPage - 1) || 0; async function getEmbed() { @@ -41,7 +53,7 @@ export default class Serverboard { new ButtonBuilder() .setCustomId("right") .setEmoji("1137330125004869702") - .setStyle(ButtonStyle.Primary), + .setStyle(ButtonStyle.Primary) ); const reply = await interaction.reply({ embeds: [await getEmbed()], components: [row] }); diff --git a/src/commands/user/info.ts b/src/commands/user/info.ts index f0d7980..077d7b4 100644 --- a/src/commands/user/info.ts +++ b/src/commands/user/info.ts @@ -1,5 +1,7 @@ import { - SlashCommandSubcommandBuilder, EmbedBuilder, type ColorResolvable, + SlashCommandSubcommandBuilder, + EmbedBuilder, + type ColorResolvable, type ChatInputCommandInteraction } from "discord.js"; import { genColor, genRGBColor } from "../../utils/colorGen"; @@ -12,21 +14,22 @@ export default class UserInfo { this.data = new SlashCommandSubcommandBuilder() .setName("info") .setDescription("Shows your (or another user's) info.") - .addUserOption(option => option - .setName("user") - .setDescription("Select the user.") - ); + .addUserOption(user => user.setName("user").setDescription("Select the user.")); } async run(interaction: ChatInputCommandInteraction) { const user = interaction.options.getUser("user"); const id = user ? user.id : interaction.member?.user.id; - const target = interaction.guild?.members.cache.filter(member => member.user.id === id).map(user => user)[0]!; + const target = interaction.guild?.members.cache + .filter(member => member.user.id === id) + .map(user => user)[0]!; const selectedUser = target.user!; let embed = new EmbedBuilder() .setAuthor({ - name: `• ${target.nickname == null ? selectedUser.username : target.nickname}${selectedUser.discriminator == "0" ? "" : `#${selectedUser.discriminator}`}`, + name: `• ${target.nickname == null ? selectedUser.username : target.nickname}${ + selectedUser.discriminator == "0" ? "" : `#${selectedUser.discriminator}` + }`, iconURL: target.displayAvatarURL() }) .setFields( @@ -34,13 +37,17 @@ export default class UserInfo { name: selectedUser?.bot === false ? "👤 • User info" : "🤖 • Bot info", value: [ `**Username**: ${selectedUser.username}`, - `**Display name**: ${selectedUser.displayName === selectedUser.username ? "*None*" : selectedUser.displayName}`, - `**Created on** `, - ].join("\n"), + `**Display name**: ${ + selectedUser.displayName === selectedUser.username + ? "*None*" + : selectedUser.displayName + }`, + `**Created on** ` + ].join("\n") }, { name: "👥 • Member info", - value: `**Joined on** `, + value: `**Joined on** ` } ) .setFooter({ text: `User ID: ${target.id}` }) @@ -54,17 +61,24 @@ export default class UserInfo { embed.setColor(genRGBColor(r, g, b) as ColorResolvable); } catch {} - const guildRoles = interaction.guild?.roles.cache.filter(role => target.roles.cache.has(role.id))!; - const memberRoles = [...guildRoles].sort((role1, role2) => role2[1].position - role1[1].position); + const guildRoles = interaction.guild?.roles.cache.filter(role => + target.roles.cache.has(role.id) + )!; + const memberRoles = [...guildRoles].sort( + (role1, role2) => role2[1].position - role1[1].position + ); memberRoles.pop(); - if (memberRoles.length !== 0) embed.addFields({ - name: `🎭 • ${guildRoles.filter(role => target.roles.cache.has(role.id)).size! - 1} ${memberRoles.length === 1 ? "role" : "roles"}`, - value: `${memberRoles - .slice(0, 5) - .map(role => `<@&${role[1].id}>`) - .join(", ")}${memberRoles.length > 5 ? ` **and ${memberRoles.length - 5} more**` : ""}`, - }); + if (memberRoles.length !== 0) + embed.addFields({ + name: `🎭 • ${guildRoles.filter(role => target.roles.cache.has(role.id)).size! - 1} ${ + memberRoles.length === 1 ? "role" : "roles" + }`, + value: `${memberRoles + .slice(0, 5) + .map(role => `<@&${role[1].id}>`) + .join(", ")}${memberRoles.length > 5 ? ` **and ${memberRoles.length - 5} more**` : ""}` + }); await interaction.reply({ embeds: [embed] }); } diff --git a/src/commands/user/level.ts b/src/commands/user/level.ts index 5482cc4..85a84aa 100644 --- a/src/commands/user/level.ts +++ b/src/commands/user/level.ts @@ -1,4 +1,8 @@ -import { SlashCommandSubcommandBuilder, EmbedBuilder, type ChatInputCommandInteraction } from "discord.js"; +import { + SlashCommandSubcommandBuilder, + EmbedBuilder, + type ChatInputCommandInteraction +} from "discord.js"; import { genColor } from "../../utils/colorGen"; import { errorEmbed } from "../../utils/embeds/errorEmbed"; import { get as getLevelRewards } from "../../utils/database/levelRewards"; @@ -11,25 +15,27 @@ export default class Level { this.data = new SlashCommandSubcommandBuilder() .setName("level") .setDescription("Shows your (or another user's) level.") - .addUserOption(option => option - .setName("user") - .setDescription("Select the user.") - ); + .addUserOption(user => user.setName("user").setDescription("Select the user.")); } async run(interaction: ChatInputCommandInteraction) { const guild = interaction.guild!; - if (!getSetting(`${guild.id}`, "levelling.enabled")) return await interaction.reply({ - embeds: [errorEmbed("Leveling is disabled for this server.")] - }); + if (!getSetting(`${guild.id}`, "levelling.enabled")) + return await interaction.reply({ + embeds: [errorEmbed("Leveling is disabled for this server.")] + }); const user = interaction.options.getUser("user"); const id = user ? user.id : interaction.member?.user.id; - const target = guild.members.cache.filter(member => member.user.id === id).map(user => user)[0]!; + const target = guild.members.cache + .filter(member => member.user.id === id) + .map(user => user)[0]!; const [guildExp, guildLevel] = getLevel(`${guild.id}`, `${target.id}`)!; if (!guildExp && !guildLevel) setLevel(`${guild.id}`, `${target.id}`, 0, 0); - const formattedExpUntilLevelup = Math.floor(100 * 1.25 * ((guildLevel ?? 0) + 1))?.toLocaleString("en-US"); + const formattedExpUntilLevelup = Math.floor( + 100 * 1.25 * ((guildLevel ?? 0) + 1) + )?.toLocaleString("en-US"); let rewards = []; let nextReward; @@ -40,7 +46,7 @@ export default class Level { break; } - rewards.push(await guild.roles.fetch(`${roleID}`)?.catch(() => { })); + rewards.push(await guild.roles.fetch(`${roleID}`)?.catch(() => {})); } const embed = new EmbedBuilder() @@ -49,15 +55,23 @@ export default class Level { { name: `⚡ • Level ${guildLevel ?? 0}`, value: [ - `**${guildExp.toLocaleString("en-US") ?? 0}/${formattedExpUntilLevelup}** EXP until level up`, + `**${ + guildExp.toLocaleString("en-US") ?? 0 + }/${formattedExpUntilLevelup}** EXP until level up`, `**Next level**: ${(guildLevel ?? 0) + 1}` ].join("\n") }, { name: `🎁 • ${rewards.length} Rewards`, value: [ - `${rewards.length > 0 ? rewards.map(reward => `<@&${reward?.id}>`).join(" ") : "No rewards unlocked"}`, - nextReward ? `**Upcoming reward**: <@&${nextReward.roleID}>` : "**Upcoming reward**: *Cricket, cricket, cricket* - Looks like you claimed everything!" + `${ + rewards.length > 0 + ? rewards.map(reward => `<@&${reward?.id}>`).join(" ") + : "No rewards unlocked" + }`, + nextReward + ? `**Upcoming reward**: <@&${nextReward.roleID}>` + : "**Upcoming reward**: *Cricket, cricket, cricket* - Looks like you claimed everything!" ].join("\n") } ) diff --git a/src/events/easterEggs/Bread.ts b/src/events/easterEggs/Bread.ts index 3c23a30..0c8e9d0 100644 --- a/src/events/easterEggs/Bread.ts +++ b/src/events/easterEggs/Bread.ts @@ -7,10 +7,12 @@ export default class Bread { if (breadSplit[1] == null) return; if ( - ((breadSplit[0].endsWith(" ") || breadSplit[0].endsWith("")) && breadSplit[1].startsWith(" ")) || + ((breadSplit[0].endsWith(" ") || breadSplit[0].endsWith("")) && + breadSplit[1].startsWith(" ")) || message.content.toLowerCase() === "bread" ) { - if (Math.round(Math.random() * 100) <= 0.25) message.channel.send("https://tenor.com/bOMAb.gif"); + if (Math.round(Math.random() * 100) <= 0.25) + message.channel.send("https://tenor.com/bOMAb.gif"); else await multiReact(message, "🍞🇧🇷🇪🇦🇩👍"); } } diff --git a/src/events/easterEggs/Fireship.ts b/src/events/easterEggs/Fireship.ts index 8a0e24e..2d00d84 100644 --- a/src/events/easterEggs/Fireship.ts +++ b/src/events/easterEggs/Fireship.ts @@ -7,8 +7,9 @@ export default class FireShip { content.startsWith("this has been") && content.endsWith("in 100 seconds") && message.content !== "this has been in 100 seconds" - ) await message.channel.send( - "hit the like button and subscribe if you want to see more short videos like this thanks for watching and I will see you in the next one" - ); + ) + await message.channel.send( + "hit the like button and subscribe if you want to see more short videos like this thanks for watching and I will see you in the next one" + ); } } diff --git a/src/events/easterEggs/Honk.ts b/src/events/easterEggs/Honk.ts index bbc7567..4db639b 100644 --- a/src/events/easterEggs/Honk.ts +++ b/src/events/easterEggs/Honk.ts @@ -3,6 +3,7 @@ import type { Message } from "discord.js"; export default class Honk { async run(message: Message) { const honks = ["hnok", "hokn", "hkon", "onk", "hon", "honhk", "hhonk", "honkk"]; - if (honks.includes(message.content.toLowerCase())) message.channel.send("https://tenor.com/bW8sm.gif"); + if (honks.includes(message.content.toLowerCase())) + message.channel.send("https://tenor.com/bW8sm.gif"); } } diff --git a/src/events/guildCreate.ts b/src/events/guildCreate.ts index 4da596d..4603542 100644 --- a/src/events/guildCreate.ts +++ b/src/events/guildCreate.ts @@ -1,7 +1,4 @@ -import { - EmbedBuilder, type DMChannel, type Client, - type Guild -} from "discord.js"; +import { EmbedBuilder, type DMChannel, type Client, type Guild } from "discord.js"; import { genColor } from "../utils/colorGen"; import { randomise } from "../utils/randomise"; import Commands from "../handlers/commands"; @@ -10,25 +7,29 @@ export default { name: "guildCreate", event: class GuildCreate { client: Client; - constructor(client: Client) { this.client = client; } async run(guild: Guild) { - const dmChannel = (await (await guild.fetchOwner()).createDM().catch(() => null)) as DMChannel | null; + const hearts = ["💖", "💝", "💓", "💗", "💘", "💟", "💕", "💞"]; + const dmChannel = (await (await guild.fetchOwner()) + .createDM() + .catch(() => null)) as DMChannel | null; const embed = new EmbedBuilder() .setTitle("👋 • Welcome to Nebula!") - .setDescription([ - "Nebula is a multiplatform, multipurpose bot with the ability to add extensions to have additional features.", - "To manage the bot, use the /settings command.", - "Nebula is in an early stage of development. If you find bugs, please go to our [official server](https://discord.gg/7RdABJhQss) and report them." - ].join("\n\n")) - .setFooter({ text: `Made by the Nebula team with ${randomise(["💖", "💝", "💓", "💗", "💘", "💟", "💕", "💞"])}` }) + .setDescription( + [ + "Nebula is a multiplatform, multipurpose bot with the ability to add extensions to have additional features.", + "To manage the bot, log into the [dashboard](https://dash.nebulabot.org/). Alternatively, you can use the /settings command.", + "Nebula is in an early stage of development. If you find bugs, please go to our [official server](https://discord.gg/7RdABJhQss) and report them." + ].join("\n\n") + ) + .setFooter({ text: `Made by the Nebula team with ${randomise(hearts)}` }) .setColor(genColor(200)); await new Commands(guild.client).registerCommandsForGuild(guild); if (dmChannel) await dmChannel.send({ embeds: [embed] }); } } -} +}; diff --git a/src/events/guildMemberAdd.ts b/src/events/guildMemberAdd.ts deleted file mode 100644 index c045f74..0000000 --- a/src/events/guildMemberAdd.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { - EmbedBuilder, type Client, type GuildMember, - type TextChannel, type ColorResolvable -} from "discord.js"; -import { genColor, genRGBColor } from "../utils/colorGen"; -import Vibrant from "node-vibrant"; -import sharp from "sharp"; - -export default { - name: "guildMemberAdd", - event: class GuildMemberAdd { - client: Client; - - constructor(client: Client) { - this.client = client; - } - - async run(member: GuildMember) { - const id = "1079612083307548704"; - const channel = await member.guild.channels.cache.find(channel => channel.id === id)!.fetch() as TextChannel; - const avatarURL = member.displayAvatarURL(); - const embed = new EmbedBuilder() - .setAuthor({ name: `• ${member.nickname == null ? member.user.username : member.nickname}`, iconURL: avatarURL }) - .setTitle("Welcome!") - .setDescription(`Enjoy your stay in **${member.guild.name}**!`) - .setFooter({ text: `User ID: ${member.id}` }) - .setThumbnail(avatarURL) - .setColor(genColor(200)); - - try { - const imageBuffer = await (await fetch(avatarURL)).arrayBuffer(); - const image = sharp(imageBuffer).toFormat("jpg"); - const { r, g, b } = (await new Vibrant(await image.toBuffer()).getPalette()).Vibrant!; - embed.setColor(genRGBColor(r, g, b) as ColorResolvable); - } catch {} - - await channel.send({ embeds: [embed] }); - } - } -} diff --git a/src/events/guildMemberRemove.ts b/src/events/guildMemberRemove.ts deleted file mode 100644 index 3f251dc..0000000 --- a/src/events/guildMemberRemove.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { - EmbedBuilder, type Client, type GuildMember, - type TextChannel, type ColorResolvable -} from "discord.js"; -import { genColor, genRGBColor } from "../utils/colorGen"; -import Vibrant from "node-vibrant"; -import sharp from "sharp"; - -export default { - name: "guildMemberRemove", - event: class GuildMemberRemove { - client: Client; - - constructor(client: Client) { - this.client = client; - } - - async run(member: GuildMember) { - const id = "1079612083307548704"; - const channel = await member.guild.channels.cache.find(channel => channel.id === id)!.fetch() as TextChannel; - const avatarURL = member.displayAvatarURL(); - const embed = new EmbedBuilder() - .setAuthor({ name: `• ${member.nickname == null ? member.user.username : member.nickname}`, iconURL: avatarURL }) - .setTitle("Goodbye!") - .setDescription(`**@${member.user.username}** has left the server 😥`) - .setFooter({ text: `User ID: ${member.id}` }) - .setThumbnail(avatarURL) - .setColor(genColor(200)); - - try { - const imageBuffer = await (await fetch(avatarURL)).arrayBuffer(); - const image = sharp(imageBuffer).toFormat("jpg"); - const { r, g, b } = (await new Vibrant(await image.toBuffer()).getPalette()).Vibrant!; - embed.setColor(genRGBColor(r, g, b) as ColorResolvable); - } catch {} - - await channel.send({ embeds: [embed] }); - } - } -} diff --git a/src/events/interactionCreate.ts b/src/events/interactionCreate.ts index 589d2ae..b238625 100644 --- a/src/events/interactionCreate.ts +++ b/src/events/interactionCreate.ts @@ -2,13 +2,22 @@ import type { CommandInteraction, Client, AutocompleteInteraction } from "discor import { pathToFileURL } from "url"; import { join } from "path"; -async function getCommand(interaction: CommandInteraction | AutocompleteInteraction, options: any): Promise { +async function getCommand( + interaction: CommandInteraction | AutocompleteInteraction, + options: any +): Promise { const commandName = interaction.commandName; const subcommandName = options.getSubcommand(false); const commandGroupName = options.getSubcommandGroup(false); const commandImportPath = join( join(process.cwd(), "src", "commands"), - `${subcommandName ? `${commandName}/${commandGroupName ? `${commandGroupName}/${subcommandName}` : subcommandName}` : commandName}.ts`, + `${ + subcommandName + ? `${commandName}/${ + commandGroupName ? `${commandGroupName}/${subcommandName}` : subcommandName + }` + : commandName + }.ts` ); return new (await import(pathToFileURL(commandImportPath).toString())).default(); @@ -38,5 +47,5 @@ export default { command.autocomplete(interaction); } } - }, + } }; diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts index 52ce580..cc846bb 100644 --- a/src/events/messageCreate.ts +++ b/src/events/messageCreate.ts @@ -12,15 +12,9 @@ export default { event: class MessageCreate { async run(message: Message) { const author = message.author; - const guild = message.guild; + const guild = message.guild!; - // Easter egg handler if (author.bot) return; - if (guild?.id !== "903852579837059113") return; - const eventsPath = join(process.cwd(), "src", "events", "easterEggs"); - - for (const easterEggFile of readdirSync(eventsPath)) - new (await import(pathToFileURL(join(eventsPath, easterEggFile)).toString())).default().run(message, ...message.content); // Levelling const levelChannelId = getSetting(guild.id, "levelling.channel"); @@ -49,16 +43,21 @@ export default { const embed = new EmbedBuilder() .setAuthor({ name: author.displayName, iconURL: author.avatarURL() || undefined }) .setTitle("⚡ • Level Up!") - .setDescription([ - `**Congratulations, ${author.displayName}**!`, - `You made it to **level ${guildLevel + 1}**`, - `You need ${Math.floor(100 * 1.25 * (guildLevel + 2))} EXP to level up again.` - ].join("\n")) + .setDescription( + [ + `**Congratulations, ${author.displayName}**!`, + `You made it to **level ${guildLevel + 1}**`, + `You need ${Math.floor(100 * 1.25 * (guildLevel + 2))} EXP to level up again.` + ].join("\n") + ) .setThumbnail(author.avatarURL()) .setTimestamp() .setColor(genColor(200)); - (guild.channels.cache.get(`${levelChannelId}`) as TextChannel).send({ embeds: [embed], content: `<@${author.id}>` }); + (guild.channels.cache.get(`${levelChannelId}`) as TextChannel).send({ + embeds: [embed], + content: `<@${author.id}>` + }); for (const { level, roleID } of getLevelRewards(guild.id)) { const role = guild.roles.cache.get(`${roleID}`); if (!role) continue; @@ -71,6 +70,16 @@ export default { await authorRoles?.remove(role); } + + // Easter egg handler + if (guild?.id !== "903852579837059113") return; + const eventsPath = join(process.cwd(), "src", "events", "easterEggs"); + + for (const easterEggFile of readdirSync(eventsPath)) + new (await import(pathToFileURL(join(eventsPath, easterEggFile)).toString())).default().run( + message, + ...message.content + ); } } -} +}; diff --git a/src/handlers/commands.ts b/src/handlers/commands.ts index 9e114fd..d282885 100644 --- a/src/handlers/commands.ts +++ b/src/handlers/commands.ts @@ -2,7 +2,7 @@ import { SlashCommandBuilder, SlashCommandSubcommandGroupBuilder, Guild, - type Client, + type Client } from "discord.js"; import { pathToFileURL } from "url"; import { join } from "path"; @@ -27,23 +27,19 @@ export default class Commands { .setDescription("This command has no description."); for (const subCommandFile of readdirSync(join(commandsPath, name), { - withFileTypes: true, + withFileTypes: true })) { const subCommandName = subCommandFile.name.replaceAll(".ts", ""); if ( disabledCommands?.find( - (command) => - command?.split("/")?.[0] == name && - command?.split("/")?.[1] == subCommandName, + command => command?.split("/")?.[0] == name && command?.split("/")?.[1] == subCommandName ) ) continue; if (subCommandFile.isFile()) { const subCommandModule = await import( - pathToFileURL( - join(commandsPath, name, subCommandFile.name), - ).toString() + pathToFileURL(join(commandsPath, name, subCommandFile.name)).toString() ); const subCommand = new subCommandModule.default(); @@ -58,32 +54,24 @@ export default class Commands { .setName(subCommandName.toLowerCase()) .setDescription("This subcommand group has no description."); - const subCommandGroupFiles = readdirSync( - join(commandsPath, name, subCommandFile.name), - { withFileTypes: true }, - ); + const subCommandGroupFiles = readdirSync(join(commandsPath, name, subCommandFile.name), { + withFileTypes: true + }); for (const subCommandGroupFile of subCommandGroupFiles) { if (!subCommandGroupFile.isFile()) continue; if ( disabledCommands?.find( - (command) => + command => command?.split("/")?.[0] == name && - command?.split("/")?.[1] == - subCommandFile.name.replaceAll(".ts", "") && - command?.split("/")?.[2] == - subCommandGroupFile.name.replaceAll(".ts", ""), + command?.split("/")?.[1] == subCommandFile.name.replaceAll(".ts", "") && + command?.split("/")?.[2] == subCommandGroupFile.name.replaceAll(".ts", "") ) ) continue; const subCommand = await import( pathToFileURL( - join( - commandsPath, - name, - subCommandFile.name, - subCommandGroupFile.name, - ), + join(commandsPath, name, subCommandFile.name, subCommandGroupFile.name) ).toString() ); subCommandGroup.addSubcommand(new subCommand.default().data); @@ -104,9 +92,7 @@ export default class Commands { if (disabledCommands?.includes(name.replaceAll(".ts", ""))) continue; if (commandFile.isFile()) { - const commandImport = await import( - pathToFileURL(join(commandsPath, name)).toString() - ); + const commandImport = await import(pathToFileURL(join(commandsPath, name)).toString()); this.commands.push(new commandImport.default().data); continue; } @@ -114,7 +100,7 @@ export default class Commands { const subCommand = await this.createSubCommand( name, join(commandsPath, name), - ...disabledCommands, + ...disabledCommands ); this.commands.push(subCommand); } @@ -132,8 +118,7 @@ export default class Commands { console.log("Adding commands to guilds..."); for (const guildID of guilds.keys()) { const disabledCommands = getDisabledCommands(guildID); - if (disabledCommands.length > 0) - await this.loadCommands(...disabledCommands); + if (disabledCommands.length > 0) await this.loadCommands(...disabledCommands); await guilds.get(guildID)?.commands.set(this.commands); } diff --git a/src/utils/database/disabledCommands.ts b/src/utils/database/disabledCommands.ts index fc96454..d0e05dd 100644 --- a/src/utils/database/disabledCommands.ts +++ b/src/utils/database/disabledCommands.ts @@ -12,10 +12,14 @@ const tableDefinition = { const database = getDatabase(tableDefinition); const getQuery = database.query("SELECT * FROM disabledCommands WHERE guild = $1;"); const addQuery = database.query("INSERT INTO disabledCommands (guild, command) VALUES (?1, ?2);"); -const removeQuery = database.query("DELETE FROM disabledCommands WHERE guild = $1 AND command = $2"); +const removeQuery = database.query( + "DELETE FROM disabledCommands WHERE guild = $1 AND command = $2" +); export function getDisabledCommands(guildID: string) { - return (getQuery.all(guildID) as TypeOfDefinition[]).map(val => val.command); + return (getQuery.all(guildID) as TypeOfDefinition[]).map( + val => val.command + ); } export function disableCommands(guildID: string, command: string) { diff --git a/src/utils/database/index.ts b/src/utils/database/index.ts index e815a45..27713ac 100644 --- a/src/utils/database/index.ts +++ b/src/utils/database/index.ts @@ -6,7 +6,9 @@ const database = new Database("data.db", { create: true }); export function getDatabase(definition: TableDefinition) { // Create table if it doesn't exist - const defStr = Object.entries(definition.definition).map(([field, type]) => field.concat(" ", type)).join(", "); + const defStr = Object.entries(definition.definition) + .map(([field, type]) => field.concat(" ", type)) + .join(", "); database.run(`CREATE TABLE IF NOT EXISTS ${definition.name} (${defStr});`); return database; } diff --git a/src/utils/database/levelBlockedChannels.ts b/src/utils/database/levelBlockedChannels.ts index 379bab6..3bcf30a 100644 --- a/src/utils/database/levelBlockedChannels.ts +++ b/src/utils/database/levelBlockedChannels.ts @@ -10,17 +10,25 @@ const tableDefinition = { } satisfies TableDefinition; const database = getDatabase(tableDefinition); -const getQuery = database.query("SELECT * FROM levelBlockedChannels WHERE guild = $1 AND channel = $2;"); +const getQuery = database.query( + "SELECT * FROM levelBlockedChannels WHERE guild = $1 AND channel = $2;" +); const listQuery = database.query("SELECT * FROM levelBlockedChannels WHERE guild = $1;"); -const addQuery = database.query("INSERT INTO levelBlockedChannels (guild, channel) VALUES (?1, ?2);"); -const removeQuery = database.query("DELETE FROM levelBlockedChannels WHERE guild = $1 AND channel = $2;"); +const addQuery = database.query( + "INSERT INTO levelBlockedChannels (guild, channel) VALUES (?1, ?2);" +); +const removeQuery = database.query( + "DELETE FROM levelBlockedChannels WHERE guild = $1 AND channel = $2;" +); export function getBlockedChannels(guildID: string, channelID: string) { return getQuery.all(guildID, channelID).length == 0; } export function listBlockedChannels(guildID: string) { - return (listQuery.all(guildID) as TypeOfDefinition[]).map(val => val.channel); + return (listQuery.all(guildID) as TypeOfDefinition[]).map( + val => val.channel + ); } export function blockChannel(guildID: string, channelID: string) { diff --git a/src/utils/database/levelRewards.ts b/src/utils/database/levelRewards.ts index def0875..e874690 100644 --- a/src/utils/database/levelRewards.ts +++ b/src/utils/database/levelRewards.ts @@ -13,8 +13,12 @@ const tableDefinition = { const database = getDatabase(tableDefinition); const getQuery = database.query("SELECT * FROM levelRewards WHERE guild = $1;"); -const addQuery = database.query("INSERT INTO levelRewards (guild, roleID, level) VALUES (?1, ?2, ?3);"); -const updateQuery = database.query("UPDATE levelRewards SET level = $3 WHERE guild = $1 AND roleID = $2"); +const addQuery = database.query( + "INSERT INTO levelRewards (guild, roleID, level) VALUES (?1, ?2, ?3);" +); +const updateQuery = database.query( + "UPDATE levelRewards SET level = $3 WHERE guild = $1 AND roleID = $2" +); const removeQuery = database.query("DELETE FROM levelRewards WHERE guild = $1 AND roleID = $2"); export function get(guildID: string) { diff --git a/src/utils/database/levelling.ts b/src/utils/database/levelling.ts index 965f430..ca2e70e 100644 --- a/src/utils/database/levelling.ts +++ b/src/utils/database/levelling.ts @@ -13,8 +13,12 @@ const tableDefinition = { const database = getDatabase(tableDefinition); const getQuery = database.query("SELECT * FROM levelling WHERE guild = $1 AND user = $2;"); -const setQuery = database.query("UPDATE levelling SET level = $3, exp = $4 WHERE guild = $1 AND user = $2;"); -const insertQuery = database.query("INSERT INTO levelling (guild, user, level, exp) VALUES (?1, ?2, ?3, ?4);"); +const setQuery = database.query( + "UPDATE levelling SET level = $3, exp = $4 WHERE guild = $1 AND user = $2;" +); +const insertQuery = database.query( + "INSERT INTO levelling (guild, user, level, exp) VALUES (?1, ?2, ?3, ?4);" +); export function getLevel(guildID: string, userID: string): [number, number] { const res = getQuery.all(guildID, userID) as TypeOfDefinition[]; diff --git a/src/utils/database/moderation.ts b/src/utils/database/moderation.ts index c3a3ed4..2be8e5c 100644 --- a/src/utils/database/moderation.ts +++ b/src/utils/database/moderation.ts @@ -17,9 +17,15 @@ const definition = { type modType = "MUTE" | "WARN" | "KICK" | "BAN"; const database = getDatabase(definition); -const addQuery = database.query("INSERT INTO moderation (guild, user, type, moderator, reason, public, id, timestamp) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8);"); -const listUserQuery = database.query("SELECT * FROM moderation WHERE guild = $1 AND user = $2 AND type = $3;"); -const listModQuery = database.query("SELECT * FROM moderation WHERE guild = $1 AND moderator = $2;"); +const addQuery = database.query( + "INSERT INTO moderation (guild, user, type, moderator, reason, public, id, timestamp) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8);" +); +const listUserQuery = database.query( + "SELECT * FROM moderation WHERE guild = $1 AND user = $2 AND type = $3;" +); +const listModQuery = database.query( + "SELECT * FROM moderation WHERE guild = $1 AND moderator = $2;" +); const removeQuery = database.query("DELETE FROM moderation WHERE guild = $1 AND id = $2"); export function addModeration( @@ -35,7 +41,11 @@ export function addModeration( return id; } -export function listUserModeration(guildID: number | string, userID: number | string, type: modType) { +export function listUserModeration( + guildID: number | string, + userID: number | string, + type: modType +) { return listUserQuery.all(guildID, userID, type) as TypeOfDefinition[]; } diff --git a/src/utils/database/news.ts b/src/utils/database/news.ts index fddf356..e305217 100644 --- a/src/utils/database/news.ts +++ b/src/utils/database/news.ts @@ -19,10 +19,16 @@ const definition = { } satisfies TableDefinition; const database = getDatabase(definition); -const addQuery = database.query("INSERT INTO news (guild, title, body, imageURL, author, authorPFP, createdAt, updatedAt, messageID, categoryID, id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11);"); +const addQuery = database.query( + "INSERT INTO news (guild, title, body, imageURL, author, authorPFP, createdAt, updatedAt, messageID, categoryID, id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11);" +); const listAllQuery = database.query("SELECT * FROM news WHERE guild = $1;"); -const listCategoryQuery = database.query("SELECT * FROM news WHERE guild = $1 AND categoryID = $2;"); -const updateQuery = database.query("UPDATE news SET title = $2, body = $3, imageURL = $4 WHERE id = $1"); +const listCategoryQuery = database.query( + "SELECT * FROM news WHERE guild = $1 AND categoryID = $2;" +); +const updateQuery = database.query( + "UPDATE news SET title = $2, body = $3, imageURL = $4 WHERE id = $1" +); const deleteQuery = database.query("DELETE FROM news WHERE id = $1"); export function addNews( diff --git a/src/utils/database/newsCategories.ts b/src/utils/database/newsCategories.ts index 41b2949..2294886 100644 --- a/src/utils/database/newsCategories.ts +++ b/src/utils/database/newsCategories.ts @@ -13,13 +13,24 @@ const definition = { } satisfies TableDefinition; const database = getDatabase(definition); -const createQuery = database.query("INSERT INTO newsCategories (guild, name, role, channel, id) VALUES (?1, ?2, ?3, ?4, ?5);"); +const createQuery = database.query( + "INSERT INTO newsCategories (guild, name, role, channel, id) VALUES (?1, ?2, ?3, ?4, ?5);" +); const listQuery = database.query("SELECT * FROM newsCategories WHERE guild = $1;"); -const findNameQuery = database.query("SELECT * FROM newsCategories WHERE guild = $1 AND name = $2;"); -const updateQuery = database.query("UPDATE newsCategories SET name = $3 WHERE guild = $1 AND name = $2"); +const findNameQuery = database.query( + "SELECT * FROM newsCategories WHERE guild = $1 AND name = $2;" +); +const updateQuery = database.query( + "UPDATE newsCategories SET name = $3 WHERE guild = $1 AND name = $2" +); const deleteQuery = database.query("DELETE FROM newsCategories WHERE guild = $1 AND name = $2"); -export function createCategory(guildID: number | string, name: string, role: number | string, channel: number | string) { +export function createCategory( + guildID: number | string, + name: string, + role: number | string, + channel: number | string +) { createQuery.run(guildID, name, role, channel, crypto.randomUUID()); } @@ -31,11 +42,7 @@ export function findWithName(guildID: number | string, name: string) { return findNameQuery.get(guildID, name) as TypeOfDefinition[]; } -export function updateCategory( - guildID: number | string, - name: string, - newName: string, -) { +export function updateCategory(guildID: number | string, name: string, newName: string) { updateQuery.run(guildID, name, newName); } diff --git a/src/utils/database/settings.ts b/src/utils/database/settings.ts index 3c45ee3..e7bfc7f 100644 --- a/src/utils/database/settings.ts +++ b/src/utils/database/settings.ts @@ -7,8 +7,8 @@ const tableDefinition = { definition: { guild: "TEXT", key: "TEXT", - value: "TEXT", - }, + value: "TEXT" + } } satisfies TableDefinition; export const settingsDefinition = { @@ -17,34 +17,24 @@ export const settingsDefinition = { "levelling.persistence": "BOOL", "log.channel": "INTEGER", "serverboard.inviteLink": "TEXT", - "serverboard.shown": "BOOL", + "serverboard.shown": "BOOL" } satisfies Record; -export const settingsKeys = Object.keys( - settingsDefinition, -) as (keyof typeof settingsDefinition)[]; +export const settingsKeys = Object.keys(settingsDefinition) as (keyof typeof settingsDefinition)[]; const database = getDatabase(tableDefinition); -const getQuery = database.query( - "SELECT * FROM settings WHERE guild = $1 AND key = $2;", -); +const getQuery = database.query("SELECT * FROM settings WHERE guild = $1 AND key = $2;"); const listPublicQuery = database.query( - "SELECT * FROM settings WHERE key = 'serverboard.shown' AND value = 'TRUE';", -); -const removeQuery = database.query( - "DELETE FROM settings WHERE guild = $1 AND key = $2;", -); -const insertQuery = database.query( - "INSERT INTO settings (guild, key, value) VALUES (?1, ?2, ?3);", + "SELECT * FROM settings WHERE key = 'serverboard.shown' AND value = 'TRUE';" ); +const removeQuery = database.query("DELETE FROM settings WHERE guild = $1 AND key = $2;"); +const insertQuery = database.query("INSERT INTO settings (guild, key, value) VALUES (?1, ?2, ?3);"); export function getSetting( guild: string, - key: K, + key: K ): TypeOfKey | null { - let res = getQuery.all(JSON.stringify(guild), key) as TypeOfDefinition< - typeof tableDefinition - >[]; + let res = getQuery.all(JSON.stringify(guild), key) as TypeOfDefinition[]; if (res.length == 0) return null; switch (settingsDefinition[key]) { case "TEXT": @@ -53,14 +43,14 @@ export function getSetting( return (res[0].value == "TRUE") as TypeOfKey; default: // TODO: Implement more data types - return "WIP"; + return "WIP" as TypeOfKey; } } export function setSetting( guild: string, key: K, - value: TypeOfKey, + value: TypeOfKey ) { const doInsert = getSetting(guild, key) == null; if (!doInsert) { @@ -70,12 +60,10 @@ export function setSetting( } export function listPublicServers() { - return ( - listPublicQuery.all() as TypeOfDefinition[] - ).map((entry) => JSON.parse(entry.guild)); + return (listPublicQuery.all() as TypeOfDefinition[]).map(entry => + JSON.parse(entry.guild) + ); } // Utility type -type TypeOfKey = SqlType< - (typeof settingsDefinition)[T] ->; +type TypeOfKey = SqlType<(typeof settingsDefinition)[T]>; diff --git a/src/utils/database/types.ts b/src/utils/database/types.ts index c589505..38f4c08 100644 --- a/src/utils/database/types.ts +++ b/src/utils/database/types.ts @@ -1,10 +1,4 @@ -export type FieldData = - | "TEXT" - | "INTEGER" - | "FLOAT" - | "BOOL" - | "TIMESTAMP" - | "JSON"; +export type FieldData = "TEXT" | "INTEGER" | "FLOAT" | "BOOL" | "TIMESTAMP" | "JSON"; export type TableDefinition = { name: string; diff --git a/src/utils/embeds/errorEmbed.ts b/src/utils/embeds/errorEmbed.ts index f596500..488acc4 100644 --- a/src/utils/embeds/errorEmbed.ts +++ b/src/utils/embeds/errorEmbed.ts @@ -8,10 +8,8 @@ import { genColor } from "../colorGen"; * @returns Embed with the error description. */ export function errorEmbed(title: string, reason?: string) { - - const content = [`**${title}**`] - - if (reason != undefined) content.push(reason) + const content = [`**${title}**`]; + if (reason != undefined) content.push(reason); return new EmbedBuilder() .setTitle("❌ • Something went wrong!") diff --git a/src/utils/embeds/serverEmbed.ts b/src/utils/embeds/serverEmbed.ts index 1c84653..459b28b 100644 --- a/src/utils/embeds/serverEmbed.ts +++ b/src/utils/embeds/serverEmbed.ts @@ -23,7 +23,9 @@ export async function serverEmbed(options: Options) { const invite = getSetting(guild.id, "serverboard.inviteLink"); const members = guild.members.cache; const boosters = members.filter(member => member.premiumSince); - const onlineMembers = members.filter(member => ["online", "dnd", "idle"].includes(member.presence?.status!)).size; + const onlineMembers = members.filter(member => + ["online", "dnd", "idle"].includes(member.presence?.status!) + ).size; const bots = members.filter(member => member.user.bot); const formattedUserCount = (guild.memberCount - bots.size)?.toLocaleString("en-US"); @@ -33,7 +35,9 @@ export async function serverEmbed(options: Options) { const channels = guild.channels.cache; const channelSizes = { - text: channels.filter(channel => channel.type === 0 || channel.type === 15 || channel.type === 5).size, + text: channels.filter( + channel => channel.type === 0 || channel.type === 15 || channel.type === 5 + ).size, voice: channels.filter(channel => channel.type === 2 || channel.type === 13).size, categories: channels.filter(channel => channel.type === 4).size }; @@ -91,7 +95,9 @@ export async function serverEmbed(options: Options) { { name: `🌟 • ${boostTier == 0 ? "No level" : `Level ${boostTier}`}`, value: [ - `**${boostCount}**${boostTier === 0 ? "/2" : boostTier === 1 ? "/7" : boostTier === 2 ? "/14" : ""} boosts`, + `**${boostCount}**${ + boostTier === 0 ? "/2" : boostTier === 1 ? "/7" : boostTier === 2 ? "/14" : "" + } boosts`, `**${boosters.size}** ${boosters.size === 1 ? "booster" : "boosters"}` ].join("\n"), inline: true diff --git a/src/utils/quickSort.ts b/src/utils/quickSort.ts index 907735b..205660e 100644 --- a/src/utils/quickSort.ts +++ b/src/utils/quickSort.ts @@ -1,6 +1,11 @@ type Corresponding = [...any[]] | null; -function swap(sortItems: number[], corresponding: Corresponding, leftIndex: number, rightIndex: number) { +function swap( + sortItems: number[], + corresponding: Corresponding, + leftIndex: number, + rightIndex: number +) { let temp = sortItems[leftIndex]; sortItems[leftIndex] = sortItems[rightIndex]; sortItems[rightIndex] = temp; @@ -47,7 +52,8 @@ export function quickSort( } if (sortItems.length > 1) { - if (leftIndex < leftPointer - 1) quickSort(sortItems, corresponding, leftIndex, leftPointer - 1); + if (leftIndex < leftPointer - 1) + quickSort(sortItems, corresponding, leftIndex, leftPointer - 1); if (leftPointer < rightIndex) quickSort(sortItems, corresponding, leftPointer, rightIndex); } From fd08b5b4d43bd178baa69fb83ab7fef05c4ee2b3 Mon Sep 17 00:00:00 2001 From: Serge Date: Fri, 26 Jan 2024 17:59:44 +0600 Subject: [PATCH 022/127] removing references to the dashboard (0.1-pre doesnt have it) --- src/bot.ts | 2 +- src/events/guildCreate.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bot.ts b/src/bot.ts index 8a818f7..f84ed93 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -4,7 +4,7 @@ import Events from "./handlers/events"; const client = new Client({ presence: { - activities: [{ name: "with the dashboard!", type: ActivityType.Playing }] + activities: [{ name: "with /settings!", type: ActivityType.Playing }] }, intents: [ "Guilds", diff --git a/src/events/guildCreate.ts b/src/events/guildCreate.ts index 4603542..e2e084f 100644 --- a/src/events/guildCreate.ts +++ b/src/events/guildCreate.ts @@ -21,7 +21,7 @@ export default { .setDescription( [ "Nebula is a multiplatform, multipurpose bot with the ability to add extensions to have additional features.", - "To manage the bot, log into the [dashboard](https://dash.nebulabot.org/). Alternatively, you can use the /settings command.", + "To manage the bot, use the /server settings command.", "Nebula is in an early stage of development. If you find bugs, please go to our [official server](https://discord.gg/7RdABJhQss) and report them." ].join("\n\n") ) From 4b8b5428490bcfdb81cebbe44556c7e41284d9f7 Mon Sep 17 00:00:00 2001 From: Serge Date: Fri, 26 Jan 2024 21:12:27 +0600 Subject: [PATCH 023/127] errors updated --- bun.lockb | Bin 66484 -> 59844 bytes package.json | 9 ++++----- src/commands/moderation/ban.ts | 8 +++++--- src/commands/moderation/delwarn.ts | 11 ++++++++--- src/commands/moderation/kick.ts | 9 ++++++--- src/commands/moderation/mute.ts | 9 ++++++--- src/commands/moderation/purge.ts | 7 ++++++- src/commands/moderation/unban.ts | 6 ++++-- src/commands/moderation/unmute.ts | 6 ++++-- src/commands/moderation/warn.ts | 10 +++++++--- src/commands/moderation/warns.ts | 5 ++++- src/commands/server/news.ts | 2 +- src/commands/server/settings.ts | 2 +- src/commands/user/level.ts | 2 +- src/events/interactionCreate.ts | 1 - src/handlers/commands.ts | 1 - src/handlers/events.ts | 1 - src/utils/database/settings.ts | 1 - 18 files changed, 57 insertions(+), 33 deletions(-) diff --git a/bun.lockb b/bun.lockb index 427d99330868e24062d5c693a9c807f628a4ddbc..010f37d64c002dd89ed60e033816bbad21e54a1d 100644 GIT binary patch delta 11216 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 ag!19TX7>3~DZ#m1`R^p6urAC^T>C#4=_~dC delta 15060 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 100) diff --git a/src/commands/moderation/unban.ts b/src/commands/moderation/unban.ts index 6d9e8ce..e8dd9b3 100644 --- a/src/commands/moderation/unban.ts +++ b/src/commands/moderation/unban.ts @@ -34,12 +34,14 @@ export default class Unban { if (!member.permissions.has(PermissionsBitField.Flags.BanMembers)) return await interaction.reply({ - embeds: [errorEmbed("You need the **Ban Members** permission to execute this command.")] + embeds: [ + errorEmbed("You can't execute this command.", "You need the **Ban Members** permission.") + ] }); if (target == undefined) return await interaction.reply({ - embeds: [errorEmbed("You can't unban this user because they were never banned.")] + embeds: [errorEmbed("You can't unban this user.", "The user was never banned.")] }); const embed = new EmbedBuilder() diff --git a/src/commands/moderation/unmute.ts b/src/commands/moderation/unmute.ts index 2bdf81f..31c6964 100644 --- a/src/commands/moderation/unmute.ts +++ b/src/commands/moderation/unmute.ts @@ -32,13 +32,15 @@ export default class Unmute { .permissions.has(PermissionsBitField.Flags.MuteMembers) ) return await interaction.reply({ - embeds: [errorEmbed("You need the **Mute Members** permission to execute this command.")] + embeds: [ + errorEmbed("You can't execute this command.", "You need the **Mute Members** permission.") + ] }); const user = interaction.options.getUser("user")!; if (members.get(user.id)?.communicationDisabledUntil === null) return await interaction.reply({ - embeds: [errorEmbed("You can't unmute this user because they were never muted.")] + embeds: [errorEmbed("You can't unmute this user.", "The user was never muted.")] }); const embed = new EmbedBuilder() diff --git a/src/commands/moderation/warn.ts b/src/commands/moderation/warn.ts index 34ce9ae..c51f9ff 100644 --- a/src/commands/moderation/warn.ts +++ b/src/commands/moderation/warn.ts @@ -38,7 +38,10 @@ export default class Warn { if (!member.permissions.has(PermissionsBitField.Flags.ModerateMembers)) return await interaction.reply({ embeds: [ - errorEmbed("You need the **Moderate Members** permission to execute this command.") + errorEmbed( + "You can't execute this command.", + "You need the **Moderate Members** permission." + ) ] }); @@ -54,7 +57,8 @@ export default class Warn { return await interaction.reply({ embeds: [ errorEmbed( - `You can't warn ${name}, because they have a higher role position than Nebula.` + `You can't warn ${name}.`, + "The member has a higher role position than Nebula." ) ] }); @@ -62,7 +66,7 @@ export default class Warn { if (member.roles.highest.position < target.roles.highest.position) return await interaction.reply({ embeds: [ - errorEmbed(`You can't warn ${name}, because they have a higher role position than you.`) + errorEmbed(`You can't warn ${name}.`, "The member has a higher role position than you.") ] }); diff --git a/src/commands/moderation/warns.ts b/src/commands/moderation/warns.ts index 7328e25..f4ae9c8 100644 --- a/src/commands/moderation/warns.ts +++ b/src/commands/moderation/warns.ts @@ -28,7 +28,10 @@ export default class Warns { ) return await interaction.reply({ embeds: [ - errorEmbed("You need the **Moderate Members** permission to execute this command.") + errorEmbed( + "You can't execute this command.", + "You need the **Moderate Members** permission." + ) ] }); diff --git a/src/commands/server/news.ts b/src/commands/server/news.ts index 3d86b27..f587971 100644 --- a/src/commands/server/news.ts +++ b/src/commands/server/news.ts @@ -30,7 +30,7 @@ export default class News { if (!news || !sortedNews || sortedNews.length == 0) return await interaction.reply({ embeds: [ - errorEmbed("No news found.\nAdmins can add news with the **/settings news add** command.") + errorEmbed("No news found.", "Admins can add news with the **/settings news add** command.") ] }); if (page > sortedNews.length) page = sortedNews.length; diff --git a/src/commands/server/settings.ts b/src/commands/server/settings.ts index af2bd8c..494da8a 100644 --- a/src/commands/server/settings.ts +++ b/src/commands/server/settings.ts @@ -3,7 +3,7 @@ import { InteractionType, EmbedBuilder, SlashCommandSubcommandBuilder, - type ChatInputCommandInteraction, + type ChatInputCommandInteraction } from "discord.js"; import { getSetting, diff --git a/src/commands/user/level.ts b/src/commands/user/level.ts index 85a84aa..6773eec 100644 --- a/src/commands/user/level.ts +++ b/src/commands/user/level.ts @@ -22,7 +22,7 @@ export default class Level { const guild = interaction.guild!; if (!getSetting(`${guild.id}`, "levelling.enabled")) return await interaction.reply({ - embeds: [errorEmbed("Leveling is disabled for this server.")] + embeds: [errorEmbed("Levelling is disabled for this server.")] }); const user = interaction.options.getUser("user"); diff --git a/src/events/interactionCreate.ts b/src/events/interactionCreate.ts index b238625..26fbed7 100644 --- a/src/events/interactionCreate.ts +++ b/src/events/interactionCreate.ts @@ -28,7 +28,6 @@ export default { event: class InteractionCreate { commands: CommandInteraction; client: Client; - constructor(cmds: CommandInteraction, client: Client) { this.commands = cmds; this.client = client; diff --git a/src/handlers/commands.ts b/src/handlers/commands.ts index d282885..ef126f0 100644 --- a/src/handlers/commands.ts +++ b/src/handlers/commands.ts @@ -12,7 +12,6 @@ import { getDisabledCommands } from "../utils/database/disabledCommands"; export default class Commands { client: Client; commands: any[] = []; - constructor(client: Client) { this.client = client; } diff --git a/src/handlers/events.ts b/src/handlers/events.ts index ff15b72..e07fd37 100644 --- a/src/handlers/events.ts +++ b/src/handlers/events.ts @@ -6,7 +6,6 @@ import { readdirSync } from "fs"; export default class Events { client: Client; events: any[] = []; - constructor(client: Client) { this.client = client; diff --git a/src/utils/database/settings.ts b/src/utils/database/settings.ts index e7bfc7f..441a073 100644 --- a/src/utils/database/settings.ts +++ b/src/utils/database/settings.ts @@ -14,7 +14,6 @@ const tableDefinition = { export const settingsDefinition = { "levelling.enabled": "BOOL", "levelling.channel": "INTEGER", - "levelling.persistence": "BOOL", "log.channel": "INTEGER", "serverboard.inviteLink": "TEXT", "serverboard.shown": "BOOL" From 3204ec6dc1a610b7f6fba19112d827a2cb9e827e Mon Sep 17 00:00:00 2001 From: Serge Date: Fri, 26 Jan 2024 22:38:53 +0600 Subject: [PATCH 024/127] =?UTF-8?q?=F0=9F=90=9Bgy=20stuff,=20please=20fix?= =?UTF-8?q?=20=F0=9F=98=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/commands/news.ts | 75 -------- src/commands/server/news/add.ts | 113 +++++++++++ src/commands/server/news/edit.ts | 180 ++++++++++++++++++ src/commands/server/news/remove.ts | 67 +++++++ src/commands/server/{news.ts => news/view.ts} | 15 +- src/commands/{user/info.ts => user.ts} | 56 +++++- src/commands/user/level.ts | 84 -------- src/utils/database/news.ts | 14 +- src/utils/database/newsCategories.ts | 51 ----- src/utils/sendChannelNews.ts | 52 +++++ 10 files changed, 470 insertions(+), 237 deletions(-) delete mode 100644 src/commands/news.ts create mode 100644 src/commands/server/news/add.ts create mode 100644 src/commands/server/news/edit.ts create mode 100644 src/commands/server/news/remove.ts rename src/commands/server/{news.ts => news/view.ts} (87%) rename src/commands/{user/info.ts => user.ts} (61%) delete mode 100644 src/commands/user/level.ts delete mode 100644 src/utils/database/newsCategories.ts create mode 100644 src/utils/sendChannelNews.ts diff --git a/src/commands/news.ts b/src/commands/news.ts deleted file mode 100644 index 582edc3..0000000 --- a/src/commands/news.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { - SlashCommandSubcommandBuilder, - EmbedBuilder, - ActionRowBuilder, - ButtonBuilder, - ButtonStyle, - type ChatInputCommandInteraction -} from "discord.js"; -import { genColor } from "../utils/colorGen"; -import { listAllNews } from "../utils/database/news"; - -export default class News { - data: SlashCommandSubcommandBuilder; - constructor() { - this.data = new SlashCommandSubcommandBuilder() - .setName("news") - .setDescription("View the news of Nebula.") - .addNumberOption(number => - number.setName("page").setDescription("The page of the news that you want to see.") - ); - } - - async run(interaction: ChatInputCommandInteraction) { - let page = interaction.options.getNumber("page") ?? 1; - const sortedNews = (Object.values(listAllNews("903852579837059113")) as any[])?.sort( - (a, b) => b.createdAt - a.createdAt - ); - let currentNews = sortedNews[page - 1]; - - if (page > sortedNews.length) page = sortedNews.length; - if (page < 1) page = 1; - - let embed = new EmbedBuilder() - .setAuthor({ name: currentNews.author, iconURL: currentNews.authorPfp ?? null }) - .setTitle(currentNews.title) - .setDescription(currentNews.body) - .setImage(currentNews.imageURL || null) - .setTimestamp(parseInt(currentNews.updatedAt)) - .setFooter({ text: `Page ${page} of ${sortedNews.length} • ID: ${currentNews.id}` }) - .setColor(genColor(270)); - - const row = new ActionRowBuilder().addComponents( - new ButtonBuilder() - .setCustomId("left") - .setEmoji("1137330341472915526") - .setStyle(ButtonStyle.Primary), - new ButtonBuilder() - .setCustomId("right") - .setEmoji("1137330125004869702") - .setStyle(ButtonStyle.Primary) - ); - - await interaction.reply({ embeds: [embed], components: [row] }); - interaction.channel - ?.createMessageComponentCollector({ - filter: i => i.user.id === interaction.user.id, - time: 60000 - }) - .on("collect", async i => { - if (!i.isButton()) return; - if (i.customId === "left") { - page--; - if (page < 1) page = sortedNews.length; - } else if (i.customId === "right") { - page++; - if (page > sortedNews.length) page = 1; - } - - currentNews = currentNews; - embed = embed; - - await interaction.editReply({ embeds: [embed], components: [row] }); - }); - } -} diff --git a/src/commands/server/news/add.ts b/src/commands/server/news/add.ts new file mode 100644 index 0000000..5eb58db --- /dev/null +++ b/src/commands/server/news/add.ts @@ -0,0 +1,113 @@ +import { + SlashCommandSubcommandBuilder, + EmbedBuilder, + PermissionsBitField, + ModalBuilder, + TextInputBuilder, + ActionRowBuilder, + TextInputStyle, + type ChatInputCommandInteraction +} from "discord.js"; +import { genColor } from "../../../utils/colorGen"; +import { errorEmbed } from "../../../utils/embeds/errorEmbed"; +import { sendChannelNews, News } from "../../../utils/sendChannelNews"; +import { addNews } from "../../../utils/database/news"; + +export default class Add { + data: SlashCommandSubcommandBuilder; + constructor() { + this.data = new SlashCommandSubcommandBuilder() + .setName("add") + .setDescription("Adds news to your guild."); + } + + async run(interaction: ChatInputCommandInteraction) { + const guild = interaction.guild!; + const member = guild.members.cache.get(interaction.user.id)!; + if (!member.permissions.has(PermissionsBitField.Flags.ManageGuild)) + return await interaction.reply({ + embeds: [errorEmbed("You need **Manage Server** permissions to add news.")] + }); + + const newsModal = new ModalBuilder() + .setCustomId("addnews") + .setTitle("Create new News for your server/project"); + + const titleInput = new TextInputBuilder() + .setCustomId("title") + .setPlaceholder("Write a title") + .setStyle(TextInputStyle.Short) + .setMaxLength(100) + .setLabel("Title") + .setRequired(true); + + const bodyInput = new TextInputBuilder() + .setCustomId("body") + .setPlaceholder("Insert your content here") + .setMaxLength(4000) + .setStyle(TextInputStyle.Paragraph) + .setLabel("Content (supports Markdown)") + .setRequired(true); + + const imageURLInput = new TextInputBuilder() + .setCustomId("imageurl") + .setPlaceholder("Place a link to your image") + .setStyle(TextInputStyle.Short) + .setMaxLength(1000) + .setLabel("Image URL (placed at the bottom)") + .setRequired(false); + + const firstActionRow = new ActionRowBuilder().addComponents( + titleInput + ) as ActionRowBuilder; + const secondActionRow = new ActionRowBuilder().addComponents( + bodyInput + ) as ActionRowBuilder; + const thirdActionRow = new ActionRowBuilder().addComponents( + imageURLInput + ) as ActionRowBuilder; + + newsModal.addComponents(firstActionRow, secondActionRow, thirdActionRow); + await interaction.showModal(newsModal).catch(err => console.error(err)); + + interaction.client.once("interactionCreate", async interaction => { + if (!interaction.isModalSubmit()) return; + if (interaction.customId !== "addnews") return; + + const imageURL = interaction.fields.getTextInputValue("imageurl") as string | undefined; + if (imageURL) { + await interaction.reply({ + embeds: [errorEmbed("The image URL you provided is invalid.")] + }); + return; + } + + const id = crypto.randomUUID(); + const news = { + id, + title: interaction.fields.getTextInputValue("title") as string, + body: interaction.fields.getTextInputValue("body") as string, + imageURL, + author: interaction.user.displayName, + authorPfp: interaction.user.avatarURL(), + createdAt: Date.now().toString(), + updatedAt: Date.now().toString(), + messageId: null + }; + + sendChannelNews(guild, news as unknown as News, id).catch(err => console.error(err)); + addNews( + guild.id, + interaction.fields.getTextInputValue("title") as string, + interaction.fields.getTextInputValue("body") as string, + imageURL!, + interaction.user.displayName, + interaction.user.avatarURL()!, + null! + ); + await interaction.reply({ + embeds: [new EmbedBuilder().setTitle("✅ • News sent!").setColor(genColor(100))] + }); + }); + } +} diff --git a/src/commands/server/news/edit.ts b/src/commands/server/news/edit.ts new file mode 100644 index 0000000..a412d27 --- /dev/null +++ b/src/commands/server/news/edit.ts @@ -0,0 +1,180 @@ +import { + SlashCommandSubcommandBuilder, + EmbedBuilder, + PermissionsBitField, + ModalBuilder, + TextInputBuilder, + ActionRowBuilder, + TextInputStyle, + TextChannel, + Message, + type ChatInputCommandInteraction +} from "discord.js"; +import { getNewsTable } from "../../../utils/database.js"; +import { genColor } from "../../../utils/colorGen.js"; +import { errorEmbed } from "../../../utils/embeds/errorEmbed.js"; +import { sendSubscribedNews, News } from "../../../utils/sendSubscribedNews.js"; +import { QuickDB } from "quick.db"; + +export default class Edit { + data: SlashCommandSubcommandBuilder; + deferred: boolean = false; + db: QuickDB; + + constructor(db?: QuickDB) { + this.db = db; + this.data = new SlashCommandSubcommandBuilder() + .setName("edit") + .setDescription("Edits new of your guild.") + .addStringOption(option => + option + .setName("id") + .setDescription("The ID of the news you want to edit.") + .setRequired(true) + ); + } + + async run(interaction: ChatInputCommandInteraction) { + const db = this.db; + const newsTable = await getNewsTable(db); + + const user = interaction.user; + const guild = interaction.guild; + const id = interaction.options.getString("id", true).trim(); + const news = await newsTable + ?.get(`${guild.id}.news.${id}`) + .then(news => news as News) + .catch(() => null as News | null); + + if (!news) + return await interaction.followUp({ + embeds: [errorEmbed("The specified news doesn't exist.")] + }); + + const author = user.displayName ?? user.username; + const timestamp = Date.now().toString(); + const member = guild.members.cache.get(user.id); + + if (!member.permissions.has(PermissionsBitField.Flags.ManageGuild)) + return await interaction.followUp({ + embeds: [errorEmbed("You need **Manage Server** permissions to add news.")] + }); + + const editModal = new ModalBuilder() + .setCustomId("editnews") + .setTitle("Edit News: " + news.title); + + const titleInput = new TextInputBuilder() + .setCustomId("title") + .setPlaceholder("Title") + .setStyle(TextInputStyle.Short) + .setMaxLength(100) + .setLabel("Title") + .setValue(news.title) + .setRequired(true); + + const bodyInput = new TextInputBuilder() + .setCustomId("body") + .setPlaceholder("Content (markdown)") + .setMaxLength(4000) + .setStyle(TextInputStyle.Paragraph) + .setLabel("Content (markdown)") + .setValue(news.body) + .setRequired(true); + + const imageURLInput = new TextInputBuilder() + .setCustomId("imageurl") + .setPlaceholder("Big image URL (bottom)") + .setStyle(TextInputStyle.Short) + .setMaxLength(1000) + .setLabel("Big image URL (bottom)") + .setValue(news.imageURL) + .setRequired(false); + + const firstActionRow = new ActionRowBuilder().addComponents( + titleInput + ) as ActionRowBuilder; + const secondActionRow = new ActionRowBuilder().addComponents( + bodyInput + ) as ActionRowBuilder; + const thirdActionRow = new ActionRowBuilder().addComponents( + imageURLInput + ) as ActionRowBuilder; + + editModal.addComponents(firstActionRow, secondActionRow, thirdActionRow); + await interaction.showModal(editModal).catch(err => console.error(err)); + + interaction.client.once("interactionCreate", async interaction => { + if (!interaction.isModalSubmit()) return; + if (interaction.customId !== "editnews") return; + + const title = interaction.fields.getTextInputValue("title") as string; + const body = interaction.fields.getTextInputValue("body") as string; + const imageURL = interaction.fields.getTextInputValue("imageurl") as string | undefined; + + if (imageURL) + await interaction.reply({ + embeds: [errorEmbed("The image URL you provided is invalid.")] + }); + + const newNews = { + ...news, + title, + body, + imageURL, + author, + authorPfp: user.avatarURL(), + updatedAt: timestamp + }; + + sendSubscribedNews(guild, { ...newNews, title: `Updated: ${newNews.title}` } as News).catch( + err => console.error(err) + ); + + const newsEmbed = new EmbedBuilder() + .setAuthor({ name: newNews.author, iconURL: newNews.authorPfp ?? null }) + .setTitle(newNews.title) + .setDescription(newNews.body) + .setImage(newNews.imageURL || null) + .setTimestamp(parseInt(newNews.updatedAt)) + .setFooter({ text: `Updated news from ${guild.name}` }) + .setColor(genColor(200)); + + const subscribedNewsChannel = await newsTable + ?.get(`${guild.id}.channel`) + .then(channel => channel as { channelId: string; roleId: string } | null) + .catch(() => { + return { channelId: null as string | null, roleId: null as string | null }; + }); + + if (subscribedNewsChannel.channelId) { + const messageId = newNews?.messageId; + const newsChannel = (await guild.channels + .fetch(subscribedNewsChannel?.channelId ?? "") + .catch(() => {})) as TextChannel | null; + + if (!messageId && newsChannel.id) + newNews.messageId = ( + (await newsChannel + ?.send({ + embeds: [newsEmbed], + content: subscribedNewsChannel.roleId ? `<@&${subscribedNewsChannel.roleId}>` : null + }) + .catch(() => {})) as Message | null + )?.id; + else if (newsChannel.id) + await newsChannel?.messages + .edit(messageId, { + embeds: [newsEmbed], + content: subscribedNewsChannel.roleId ? `<@&${subscribedNewsChannel.roleId}>` : null + }) + .catch(() => {}); + } + + const embed = new EmbedBuilder().setTitle("✅ • News edited!").setColor(genColor(100)); + + await newsTable.set(`${guild.id}.news.${id}`, newNews); + await interaction.reply({ embeds: [embed] }); + }); + } +} diff --git a/src/commands/server/news/remove.ts b/src/commands/server/news/remove.ts new file mode 100644 index 0000000..0272fdf --- /dev/null +++ b/src/commands/server/news/remove.ts @@ -0,0 +1,67 @@ +import { + SlashCommandSubcommandBuilder, + EmbedBuilder, + PermissionsBitField, + TextChannel, + type ChatInputCommandInteraction +} from "discord.js"; +import { getNewsTable } from "../../../utils/database.js"; +import { genColor } from "../../../utils/colorGen.js"; +import { errorEmbed } from "../../../utils/embeds/errorEmbed.js"; +import { QuickDB } from "quick.db"; + +export default class Remove { + data: SlashCommandSubcommandBuilder; + db: QuickDB; + + constructor(db?: QuickDB) { + this.db = db; + this.data = new SlashCommandSubcommandBuilder() + .setName("remove") + .setDescription("Removes news from your guild.") + .addStringOption(option => + option + .setName("id") + .setDescription("The ID of the news. Found in the footer of the news.") + .setRequired(true) + ); + } + + async run(interaction: ChatInputCommandInteraction) { + const db = this.db; + const newsTable = await getNewsTable(db); + const user = interaction.user; + const guild = interaction.guild; + const providedId = interaction.options.getString("id"); + const member = guild.members.cache.get(user.id); + + if (!member.permissions.has(PermissionsBitField.Flags.ManageGuild)) + return await interaction.followUp({ + embeds: [errorEmbed("You need **Manage Server** permissions to delete news.")] + }); + + const embed = new EmbedBuilder().setTitle("✅ • News deleted!").setColor(genColor(100)); + + const subscribedChannel = await newsTable + ?.get(`${guild.id}.channel`) + .then(channel => channel as { channelId: string; roleId: string }) + .catch(() => { + return { channelId: null, roleId: null }; + }); + + const news = await newsTable?.get(providedId).catch(() => null); + if (!news) + return await interaction.followUp({ + embeds: [errorEmbed("The specified news doesn't exist.")] + }); + + const messageId = news?.messageId; + const newsChannel = (await interaction.guild.channels + .fetch(subscribedChannel?.channelId ?? "") + .catch(() => null)) as TextChannel | null; + if (newsChannel) await newsChannel?.messages.delete(messageId).catch(() => null); + + await newsTable?.delete(`${guild.id}.news.${providedId}`).catch(() => null); + await interaction.followUp({ embeds: [embed] }); + } +} diff --git a/src/commands/server/news.ts b/src/commands/server/news/view.ts similarity index 87% rename from src/commands/server/news.ts rename to src/commands/server/news/view.ts index f587971..8e06498 100644 --- a/src/commands/server/news.ts +++ b/src/commands/server/news/view.ts @@ -6,15 +6,15 @@ import { ButtonStyle, type ChatInputCommandInteraction } from "discord.js"; -import { genColor } from "../../utils/colorGen"; -import { errorEmbed } from "../../utils/embeds/errorEmbed"; -import { listAllNews } from "../../utils/database/news"; +import { genColor } from "../../../utils/colorGen"; +import { errorEmbed } from "../../../utils/embeds/errorEmbed"; +import { listAllNews } from "../../../utils/database/news"; -export default class News { +export default class View { data: SlashCommandSubcommandBuilder; constructor() { this.data = new SlashCommandSubcommandBuilder() - .setName("news") + .setName("view") .setDescription("View the news of this server.") .addNumberOption(number => number.setName("page").setDescription("The page of the news that you want to see.") @@ -30,7 +30,10 @@ export default class News { if (!news || !sortedNews || sortedNews.length == 0) return await interaction.reply({ embeds: [ - errorEmbed("No news found.", "Admins can add news with the **/settings news add** command.") + errorEmbed( + "No news found.", + "Admins can add news with the **/server news add** command." + ) ] }); if (page > sortedNews.length) page = sortedNews.length; diff --git a/src/commands/user/info.ts b/src/commands/user.ts similarity index 61% rename from src/commands/user/info.ts rename to src/commands/user.ts index 077d7b4..6d026c8 100644 --- a/src/commands/user/info.ts +++ b/src/commands/user.ts @@ -4,23 +4,27 @@ import { type ColorResolvable, type ChatInputCommandInteraction } from "discord.js"; -import { genColor, genRGBColor } from "../../utils/colorGen"; +import { genColor, genRGBColor } from "../utils/colorGen"; +import { get as getLevelRewards } from "../utils/database/levelRewards"; +import { getSetting } from "../utils/database/settings"; +import { getLevel, setLevel } from "../utils/database/levelling"; import Vibrant from "node-vibrant"; import sharp from "sharp"; -export default class UserInfo { +export default class User { data: SlashCommandSubcommandBuilder; constructor() { this.data = new SlashCommandSubcommandBuilder() - .setName("info") + .setName("user") .setDescription("Shows your (or another user's) info.") .addUserOption(user => user.setName("user").setDescription("Select the user.")); } async run(interaction: ChatInputCommandInteraction) { + const guild = interaction.guild!; const user = interaction.options.getUser("user"); const id = user ? user.id : interaction.member?.user.id; - const target = interaction.guild?.members.cache + const target = guild.members.cache .filter(member => member.user.id === id) .map(user => user)[0]!; const selectedUser = target.user!; @@ -43,11 +47,13 @@ export default class UserInfo { : selectedUser.displayName }`, `**Created on** ` - ].join("\n") + ].join("\n"), + inline: true }, { name: "👥 • Member info", - value: `**Joined on** ` + value: `**Joined on** `, + inline: true } ) .setFooter({ text: `User ID: ${target.id}` }) @@ -61,9 +67,41 @@ export default class UserInfo { embed.setColor(genRGBColor(r, g, b) as ColorResolvable); } catch {} - const guildRoles = interaction.guild?.roles.cache.filter(role => - target.roles.cache.has(role.id) - )!; + if (getSetting(`${guild.id}`, "levelling.enabled")) { + const [guildExp, guildLevel] = getLevel(`${guild.id}`, `${target.id}`)!; + if (!guildExp && !guildLevel) setLevel(`${guild.id}`, `${target.id}`, 0, 0); + + const formattedExpUntilLevelup = Math.floor( + 100 * 1.25 * ((guildLevel ?? 0) + 1) + )?.toLocaleString("en-US"); + let rewards = []; + let nextReward; + + for (const { roleID, level } of getLevelRewards(`${guild.id}`)) { + if (guildLevel < level) { + if (nextReward) break; + nextReward = { roleID, level }; + break; + } + + rewards.push(await guild.roles.fetch(`${roleID}`)?.catch(() => {})); + } + + embed.addFields({ + name: `⚡ • Level ${guildLevel ?? 0}`, + value: [ + `**${guildExp.toLocaleString("en-US") ?? 0}/${formattedExpUntilLevelup}** EXP`, + `**Next level**: ${(guildLevel ?? 0) + 1}`, + `${ + rewards.length > 0 + ? rewards.map(reward => `<@&${reward?.id}>`).join(" ") + : "*No rewards unlocked*" + }` + ].join("\n") + }); + } + + const guildRoles = guild.roles.cache.filter(role => target.roles.cache.has(role.id))!; const memberRoles = [...guildRoles].sort( (role1, role2) => role2[1].position - role1[1].position ); diff --git a/src/commands/user/level.ts b/src/commands/user/level.ts deleted file mode 100644 index 6773eec..0000000 --- a/src/commands/user/level.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { - SlashCommandSubcommandBuilder, - EmbedBuilder, - type ChatInputCommandInteraction -} from "discord.js"; -import { genColor } from "../../utils/colorGen"; -import { errorEmbed } from "../../utils/embeds/errorEmbed"; -import { get as getLevelRewards } from "../../utils/database/levelRewards"; -import { getSetting } from "../../utils/database/settings"; -import { getLevel, setLevel } from "../../utils/database/levelling"; - -export default class Level { - data: SlashCommandSubcommandBuilder; - constructor() { - this.data = new SlashCommandSubcommandBuilder() - .setName("level") - .setDescription("Shows your (or another user's) level.") - .addUserOption(user => user.setName("user").setDescription("Select the user.")); - } - - async run(interaction: ChatInputCommandInteraction) { - const guild = interaction.guild!; - if (!getSetting(`${guild.id}`, "levelling.enabled")) - return await interaction.reply({ - embeds: [errorEmbed("Levelling is disabled for this server.")] - }); - - const user = interaction.options.getUser("user"); - const id = user ? user.id : interaction.member?.user.id; - const target = guild.members.cache - .filter(member => member.user.id === id) - .map(user => user)[0]!; - const [guildExp, guildLevel] = getLevel(`${guild.id}`, `${target.id}`)!; - if (!guildExp && !guildLevel) setLevel(`${guild.id}`, `${target.id}`, 0, 0); - - const formattedExpUntilLevelup = Math.floor( - 100 * 1.25 * ((guildLevel ?? 0) + 1) - )?.toLocaleString("en-US"); - let rewards = []; - let nextReward; - - for (const { roleID, level } of getLevelRewards(`${guild.id}`)) { - if (guildLevel < level) { - if (nextReward) break; - nextReward = { roleID, level }; - break; - } - - rewards.push(await guild.roles.fetch(`${roleID}`)?.catch(() => {})); - } - - const embed = new EmbedBuilder() - .setAuthor({ name: `• ${target.user.username}`, iconURL: target.displayAvatarURL() }) - .setFields( - { - name: `⚡ • Level ${guildLevel ?? 0}`, - value: [ - `**${ - guildExp.toLocaleString("en-US") ?? 0 - }/${formattedExpUntilLevelup}** EXP until level up`, - `**Next level**: ${(guildLevel ?? 0) + 1}` - ].join("\n") - }, - { - name: `🎁 • ${rewards.length} Rewards`, - value: [ - `${ - rewards.length > 0 - ? rewards.map(reward => `<@&${reward?.id}>`).join(" ") - : "No rewards unlocked" - }`, - nextReward - ? `**Upcoming reward**: <@&${nextReward.roleID}>` - : "**Upcoming reward**: *Cricket, cricket, cricket* - Looks like you claimed everything!" - ].join("\n") - } - ) - .setThumbnail(target.displayAvatarURL()) - .setTimestamp() - .setColor(genColor(200)); - - await interaction.reply({ embeds: [embed] }); - } -} diff --git a/src/utils/database/news.ts b/src/utils/database/news.ts index e305217..941b0b2 100644 --- a/src/utils/database/news.ts +++ b/src/utils/database/news.ts @@ -13,19 +13,15 @@ const definition = { createdAt: "TIMESTAMP", updatedAt: "TIMESTAMP", messageID: "TEXT", - categoryID: "TEXT", id: "TEXT" } } satisfies TableDefinition; const database = getDatabase(definition); const addQuery = database.query( - "INSERT INTO news (guild, title, body, imageURL, author, authorPFP, createdAt, updatedAt, messageID, categoryID, id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11);" + "INSERT INTO news (guild, title, body, imageURL, author, authorPFP, createdAt, updatedAt, messageID, id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11);" ); const listAllQuery = database.query("SELECT * FROM news WHERE guild = $1;"); -const listCategoryQuery = database.query( - "SELECT * FROM news WHERE guild = $1 AND categoryID = $2;" -); const updateQuery = database.query( "UPDATE news SET title = $2, body = $3, imageURL = $4 WHERE id = $1" ); @@ -38,8 +34,7 @@ export function addNews( imageURL: string | null, author: string, authorPFP: string, - messageID: string | number, - categoryID: string + messageID: string | number ) { addQuery.run( guildID, @@ -51,7 +46,6 @@ export function addNews( Date.now(), 0, messageID, - categoryID, crypto.randomUUID() ); } @@ -60,10 +54,6 @@ export function listAllNews(guildID: string | number) { return listAllQuery.all(guildID) as TypeOfDefinition[]; } -export function listCategoryNews(guildID: string | number, categoryID: string) { - return listCategoryQuery.all(guildID, categoryID) as TypeOfDefinition[]; -} - export function updateNews(id: string, title: string, body: string, imageURL: string) { updateQuery.run(id, title, body, imageURL); } diff --git a/src/utils/database/newsCategories.ts b/src/utils/database/newsCategories.ts deleted file mode 100644 index 2294886..0000000 --- a/src/utils/database/newsCategories.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { getDatabase } from "."; -import { TableDefinition, TypeOfDefinition } from "./types"; - -const definition = { - name: "newsCategories", - definition: { - guild: "TEXT", - name: "TEXT", - role: "TEXT", - channel: "TEXT", - id: "TEXT" - } -} satisfies TableDefinition; - -const database = getDatabase(definition); -const createQuery = database.query( - "INSERT INTO newsCategories (guild, name, role, channel, id) VALUES (?1, ?2, ?3, ?4, ?5);" -); -const listQuery = database.query("SELECT * FROM newsCategories WHERE guild = $1;"); -const findNameQuery = database.query( - "SELECT * FROM newsCategories WHERE guild = $1 AND name = $2;" -); -const updateQuery = database.query( - "UPDATE newsCategories SET name = $3 WHERE guild = $1 AND name = $2" -); -const deleteQuery = database.query("DELETE FROM newsCategories WHERE guild = $1 AND name = $2"); - -export function createCategory( - guildID: number | string, - name: string, - role: number | string, - channel: number | string -) { - createQuery.run(guildID, name, role, channel, crypto.randomUUID()); -} - -export function listCategories(guildID: number | string) { - return listQuery.all(guildID) as TypeOfDefinition[]; -} - -export function findWithName(guildID: number | string, name: string) { - return findNameQuery.get(guildID, name) as TypeOfDefinition[]; -} - -export function updateCategory(guildID: number | string, name: string, newName: string) { - updateQuery.run(guildID, name, newName); -} - -export function deleteCategory(guildID: number | string, name: string) { - deleteQuery.run(guildID, name); -} diff --git a/src/utils/sendChannelNews.ts b/src/utils/sendChannelNews.ts new file mode 100644 index 0000000..f9e724e --- /dev/null +++ b/src/utils/sendChannelNews.ts @@ -0,0 +1,52 @@ +import { + EmbedBuilder, Guild, Role, + TextChannel +} from "discord.js"; +import { genColor } from "./colorGen"; + +export type News = { + title: string + body: string + imageURL: string + author: string + authorPfp: string + createdAt: string + updatedAt: string + messageId?: string +} + +export async function sendChannelNews(guild: Guild, news: News, id: string) { + const db = await database(); + const newsTable = await getNewsTable(db); + + const subscribedChannel = await newsTable.get(`${guild.id}.channel`).then( + (channel: { channelId: string | null; roleId: string | null; }) => channel as { channelId: string | null, roleId: string | null } + ).catch(() => { + return { + channelId: null as string | null, + roleId: null as string | null + }; + }); + + if (!subscribedChannel) return; + const channel = subscribedChannel.channelId; + const channelToSend = guild.channels.cache.get(channel) as TextChannel; + if (!channelToSend) return; + + const role = subscribedChannel.roleId; + let roleToSend: Role | undefined; + if (role) roleToSend = guild.roles.cache.get(role); + + const embed = new EmbedBuilder() + .setAuthor({ name: news.author, iconURL: news.authorPfp ?? null }) + .setTitle(news.title) + .setDescription(news.body) + .setImage(news.imageURL || null) + .setTimestamp(parseInt(news.updatedAt)) + .setFooter({ text: `Latest news from ${guild.name}` }) + .setColor(genColor(200)); + + const message = await channelToSend.send({ embeds: [embed], content: roleToSend ? `<@&${roleToSend.id}>` : undefined }); + await newsTable.set(`${guild.id}.news.${id}.messageId`, message.id); + return; +} From 892efd1d91933c32fd46d59c09aa4b85de823e88 Mon Sep 17 00:00:00 2001 From: Froxcey Date: Wed, 31 Jan 2024 01:20:28 +0800 Subject: [PATCH 025/127] Remove UPDATE Query --- src/commands/server/settings.ts | 2 +- src/utils/database/levelRewards.ts | 10 +++------- src/utils/database/levelling.ts | 14 +++++--------- src/utils/database/news.ts | 15 ++++++++++++++- src/utils/database/settings.ts | 2 +- 5 files changed, 24 insertions(+), 19 deletions(-) diff --git a/src/commands/server/settings.ts b/src/commands/server/settings.ts index 494da8a..16a41bd 100644 --- a/src/commands/server/settings.ts +++ b/src/commands/server/settings.ts @@ -59,7 +59,7 @@ export default class ServerInfo { ) { case "BOOL": interaction.respond( - ["TRUE", "FALSE"].map(choice => ({ + ["true", "false"].map(choice => ({ name: choice, value: choice })) diff --git a/src/utils/database/levelRewards.ts b/src/utils/database/levelRewards.ts index e874690..a020e83 100644 --- a/src/utils/database/levelRewards.ts +++ b/src/utils/database/levelRewards.ts @@ -13,12 +13,7 @@ const tableDefinition = { const database = getDatabase(tableDefinition); const getQuery = database.query("SELECT * FROM levelRewards WHERE guild = $1;"); -const addQuery = database.query( - "INSERT INTO levelRewards (guild, roleID, level) VALUES (?1, ?2, ?3);" -); -const updateQuery = database.query( - "UPDATE levelRewards SET level = $3 WHERE guild = $1 AND roleID = $2" -); +const addQuery = database.query("INSERT INTO levelRewards (guild, roleID, level) VALUES (?1, ?2, ?3);"); const removeQuery = database.query("DELETE FROM levelRewards WHERE guild = $1 AND roleID = $2"); export function get(guildID: string) { @@ -30,7 +25,8 @@ export function addReward(guildID: string, role: number | string, level: number) } export function updateReward(guildID: string, role: number | string, level: number) { - updateQuery.run(guildID, role, level); + removeQuery.run(guildID, role); + addQuery.all(guildID, level, role); } export function removeReward(guildID: number | string, role: number | string) { diff --git a/src/utils/database/levelling.ts b/src/utils/database/levelling.ts index ca2e70e..71c577c 100644 --- a/src/utils/database/levelling.ts +++ b/src/utils/database/levelling.ts @@ -13,12 +13,8 @@ const tableDefinition = { const database = getDatabase(tableDefinition); const getQuery = database.query("SELECT * FROM levelling WHERE guild = $1 AND user = $2;"); -const setQuery = database.query( - "UPDATE levelling SET level = $3, exp = $4 WHERE guild = $1 AND user = $2;" -); -const insertQuery = database.query( - "INSERT INTO levelling (guild, user, level, exp) VALUES (?1, ?2, ?3, ?4);" -); +const deleteQuery = database.query("DELETE FROM levelling WHERE guild = $1 AND user = $2;"); +const insertQuery = database.query("INSERT INTO levelling (guild, user, level, exp) VALUES (?1, ?2, ?3, ?4);"); export function getLevel(guildID: string, userID: string): [number, number] { const res = getQuery.all(guildID, userID) as TypeOfDefinition[]; @@ -27,7 +23,7 @@ export function getLevel(guildID: string, userID: string): [number, number] { } export function setLevel(guildID: string | number, userID: string, level: number, exp: number) { - getQuery.all(guildID, userID).length == 0 - ? insertQuery.run(guildID, userID, level, exp) - : setQuery.run(guildID, userID, level, exp); + if (getQuery.all(guildID, userID).length != 0) + deleteQuery.run(guildID, userID, level, exp); + insertQuery.run(guildID, userID, level, exp); } diff --git a/src/utils/database/news.ts b/src/utils/database/news.ts index 941b0b2..c23f6ec 100644 --- a/src/utils/database/news.ts +++ b/src/utils/database/news.ts @@ -50,11 +50,24 @@ export function addNews( ); } -export function listAllNews(guildID: string | number) { +export function listAllNews(guildID: string) { return listAllQuery.all(guildID) as TypeOfDefinition[]; } export function updateNews(id: string, title: string, body: string, imageURL: string) { + const lastElem = deleteQuery.get(id) as TypeOfDefinition; + addQuery.run( + lastElem.guild, + title, + body, + imageURL, + lastElem.author, + lastElem.authorPFP, + Date.now(), + 0, + lastElem.messageID, + id + ); updateQuery.run(id, title, body, imageURL); } diff --git a/src/utils/database/settings.ts b/src/utils/database/settings.ts index 441a073..b57d0e5 100644 --- a/src/utils/database/settings.ts +++ b/src/utils/database/settings.ts @@ -39,7 +39,7 @@ export function getSetting( case "TEXT": return res[0].value as TypeOfKey; case "BOOL": - return (res[0].value == "TRUE") as TypeOfKey; + return (res[0].value == "true") as TypeOfKey; default: // TODO: Implement more data types return "WIP" as TypeOfKey; From 966c4319ebb64c3c50a31a2ba4a5e9a1ad46328f Mon Sep 17 00:00:00 2001 From: Froxcey Date: Thu, 1 Feb 2024 00:27:09 +0800 Subject: [PATCH 026/127] Get news by id --- src/utils/database/news.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/utils/database/news.ts b/src/utils/database/news.ts index c23f6ec..7821d1c 100644 --- a/src/utils/database/news.ts +++ b/src/utils/database/news.ts @@ -22,6 +22,7 @@ const addQuery = database.query( "INSERT INTO news (guild, title, body, imageURL, author, authorPFP, createdAt, updatedAt, messageID, id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11);" ); const listAllQuery = database.query("SELECT * FROM news WHERE guild = $1;"); +const getIdQuery = database.query("SELECT * FROM news WHERE id = $1;"); const updateQuery = database.query( "UPDATE news SET title = $2, body = $3, imageURL = $4 WHERE id = $1" ); @@ -54,6 +55,10 @@ export function listAllNews(guildID: string) { return listAllQuery.all(guildID) as TypeOfDefinition[]; } +export function get(id: string) { + return getIdQuery.get(id) as TypeOfDefinition | null; +} + export function updateNews(id: string, title: string, body: string, imageURL: string) { const lastElem = deleteQuery.get(id) as TypeOfDefinition; addQuery.run( @@ -66,7 +71,7 @@ export function updateNews(id: string, title: string, body: string, imageURL: st Date.now(), 0, lastElem.messageID, - id + id ); updateQuery.run(id, title, body, imageURL); } From e01cdf0f7643c9142da3fcc6889dadd153677c2c Mon Sep 17 00:00:00 2001 From: Serge Date: Wed, 31 Jan 2024 22:55:04 +0600 Subject: [PATCH 027/127] small progress --- src/commands/server/news/add.ts | 3 +- src/commands/server/news/edit.ts | 122 +++++++---------------------- src/commands/server/news/remove.ts | 31 +++----- src/utils/database/news.ts | 4 +- src/utils/sendChannelNews.ts | 51 ++++++------ 5 files changed, 68 insertions(+), 143 deletions(-) diff --git a/src/commands/server/news/add.ts b/src/commands/server/news/add.ts index 5eb58db..2d9a246 100644 --- a/src/commands/server/news/add.ts +++ b/src/commands/server/news/add.ts @@ -72,9 +72,8 @@ export default class Add { interaction.client.once("interactionCreate", async interaction => { if (!interaction.isModalSubmit()) return; - if (interaction.customId !== "addnews") return; - const imageURL = interaction.fields.getTextInputValue("imageurl") as string | undefined; + const imageURL = interaction.fields.getTextInputValue("imageurl"); if (imageURL) { await interaction.reply({ embeds: [errorEmbed("The image URL you provided is invalid.")] diff --git a/src/commands/server/news/edit.ts b/src/commands/server/news/edit.ts index a412d27..1b9b9ad 100644 --- a/src/commands/server/news/edit.ts +++ b/src/commands/server/news/edit.ts @@ -6,26 +6,18 @@ import { TextInputBuilder, ActionRowBuilder, TextInputStyle, - TextChannel, - Message, type ChatInputCommandInteraction } from "discord.js"; -import { getNewsTable } from "../../../utils/database.js"; -import { genColor } from "../../../utils/colorGen.js"; -import { errorEmbed } from "../../../utils/embeds/errorEmbed.js"; -import { sendSubscribedNews, News } from "../../../utils/sendSubscribedNews.js"; -import { QuickDB } from "quick.db"; +import { genColor } from "../../../utils/colorGen"; +import { errorEmbed } from "../../../utils/embeds/errorEmbed"; +import { get, updateNews } from "../../../utils/database/news"; export default class Edit { data: SlashCommandSubcommandBuilder; - deferred: boolean = false; - db: QuickDB; - - constructor(db?: QuickDB) { - this.db = db; + constructor() { this.data = new SlashCommandSubcommandBuilder() .setName("edit") - .setDescription("Edits new of your guild.") + .setDescription("Edits the news of your guild.") .addStringOption(option => option .setName("id") @@ -35,34 +27,25 @@ export default class Edit { } async run(interaction: ChatInputCommandInteraction) { - const db = this.db; - const newsTable = await getNewsTable(db); + if ( + !interaction + .guild!.members.cache.get(interaction.user.id)! + .permissions.has(PermissionsBitField.Flags.ManageGuild) + ) + return await interaction.followUp({ + embeds: [errorEmbed("You need **Manage Server** permissions to add news.")] + }); - const user = interaction.user; - const guild = interaction.guild; const id = interaction.options.getString("id", true).trim(); - const news = await newsTable - ?.get(`${guild.id}.news.${id}`) - .then(news => news as News) - .catch(() => null as News | null); - + const news = get(id); if (!news) return await interaction.followUp({ embeds: [errorEmbed("The specified news doesn't exist.")] }); - const author = user.displayName ?? user.username; - const timestamp = Date.now().toString(); - const member = guild.members.cache.get(user.id); - - if (!member.permissions.has(PermissionsBitField.Flags.ManageGuild)) - return await interaction.followUp({ - embeds: [errorEmbed("You need **Manage Server** permissions to add news.")] - }); - const editModal = new ModalBuilder() .setCustomId("editnews") - .setTitle("Edit News: " + news.title); + .setTitle(`Edit News: ${news.title}`); const titleInput = new TextInputBuilder() .setCustomId("title") @@ -106,75 +89,24 @@ export default class Edit { interaction.client.once("interactionCreate", async interaction => { if (!interaction.isModalSubmit()) return; - if (interaction.customId !== "editnews") return; - - const title = interaction.fields.getTextInputValue("title") as string; - const body = interaction.fields.getTextInputValue("body") as string; - const imageURL = interaction.fields.getTextInputValue("imageurl") as string | undefined; - if (imageURL) + const imageURL = interaction.fields.getTextInputValue("imageurl"); + if (imageURL) { await interaction.reply({ embeds: [errorEmbed("The image URL you provided is invalid.")] }); - - const newNews = { - ...news, - title, - body, - imageURL, - author, - authorPfp: user.avatarURL(), - updatedAt: timestamp - }; - - sendSubscribedNews(guild, { ...newNews, title: `Updated: ${newNews.title}` } as News).catch( - err => console.error(err) - ); - - const newsEmbed = new EmbedBuilder() - .setAuthor({ name: newNews.author, iconURL: newNews.authorPfp ?? null }) - .setTitle(newNews.title) - .setDescription(newNews.body) - .setImage(newNews.imageURL || null) - .setTimestamp(parseInt(newNews.updatedAt)) - .setFooter({ text: `Updated news from ${guild.name}` }) - .setColor(genColor(200)); - - const subscribedNewsChannel = await newsTable - ?.get(`${guild.id}.channel`) - .then(channel => channel as { channelId: string; roleId: string } | null) - .catch(() => { - return { channelId: null as string | null, roleId: null as string | null }; - }); - - if (subscribedNewsChannel.channelId) { - const messageId = newNews?.messageId; - const newsChannel = (await guild.channels - .fetch(subscribedNewsChannel?.channelId ?? "") - .catch(() => {})) as TextChannel | null; - - if (!messageId && newsChannel.id) - newNews.messageId = ( - (await newsChannel - ?.send({ - embeds: [newsEmbed], - content: subscribedNewsChannel.roleId ? `<@&${subscribedNewsChannel.roleId}>` : null - }) - .catch(() => {})) as Message | null - )?.id; - else if (newsChannel.id) - await newsChannel?.messages - .edit(messageId, { - embeds: [newsEmbed], - content: subscribedNewsChannel.roleId ? `<@&${subscribedNewsChannel.roleId}>` : null - }) - .catch(() => {}); + return; } - const embed = new EmbedBuilder().setTitle("✅ • News edited!").setColor(genColor(100)); - - await newsTable.set(`${guild.id}.news.${id}`, newNews); - await interaction.reply({ embeds: [embed] }); + updateNews( + id, + interaction.fields.getTextInputValue("title"), + interaction.fields.getTextInputValue("body"), + imageURL + ); + await interaction.reply({ + embeds: [new EmbedBuilder().setTitle("✅ • News edited!").setColor(genColor(100))] + }); }); } } diff --git a/src/commands/server/news/remove.ts b/src/commands/server/news/remove.ts index 0272fdf..c95e315 100644 --- a/src/commands/server/news/remove.ts +++ b/src/commands/server/news/remove.ts @@ -5,17 +5,13 @@ import { TextChannel, type ChatInputCommandInteraction } from "discord.js"; -import { getNewsTable } from "../../../utils/database.js"; -import { genColor } from "../../../utils/colorGen.js"; -import { errorEmbed } from "../../../utils/embeds/errorEmbed.js"; -import { QuickDB } from "quick.db"; +import { genColor } from "../../../utils/colorGen"; +import { errorEmbed } from "../../../utils/embeds/errorEmbed"; +import { deleteNews } from "../../../utils/database/news"; export default class Remove { data: SlashCommandSubcommandBuilder; - db: QuickDB; - - constructor(db?: QuickDB) { - this.db = db; + constructor() { this.data = new SlashCommandSubcommandBuilder() .setName("remove") .setDescription("Removes news from your guild.") @@ -28,20 +24,16 @@ export default class Remove { } async run(interaction: ChatInputCommandInteraction) { - const db = this.db; - const newsTable = await getNewsTable(db); const user = interaction.user; - const guild = interaction.guild; - const providedId = interaction.options.getString("id"); - const member = guild.members.cache.get(user.id); + const guild = interaction.guild!; + const id = interaction.options.getString("id")!; + const member = guild.members.cache.get(user.id)!; if (!member.permissions.has(PermissionsBitField.Flags.ManageGuild)) return await interaction.followUp({ embeds: [errorEmbed("You need **Manage Server** permissions to delete news.")] }); - const embed = new EmbedBuilder().setTitle("✅ • News deleted!").setColor(genColor(100)); - const subscribedChannel = await newsTable ?.get(`${guild.id}.channel`) .then(channel => channel as { channelId: string; roleId: string }) @@ -49,9 +41,8 @@ export default class Remove { return { channelId: null, roleId: null }; }); - const news = await newsTable?.get(providedId).catch(() => null); if (!news) - return await interaction.followUp({ + return await interaction.reply({ embeds: [errorEmbed("The specified news doesn't exist.")] }); @@ -61,7 +52,9 @@ export default class Remove { .catch(() => null)) as TextChannel | null; if (newsChannel) await newsChannel?.messages.delete(messageId).catch(() => null); - await newsTable?.delete(`${guild.id}.news.${providedId}`).catch(() => null); - await interaction.followUp({ embeds: [embed] }); + deleteNews(id); + await interaction.reply({ + embeds: [new EmbedBuilder().setTitle("✅ • News deleted!").setColor(genColor(100))] + }); } } diff --git a/src/utils/database/news.ts b/src/utils/database/news.ts index 7821d1c..0af16be 100644 --- a/src/utils/database/news.ts +++ b/src/utils/database/news.ts @@ -19,7 +19,7 @@ const definition = { const database = getDatabase(definition); const addQuery = database.query( - "INSERT INTO news (guild, title, body, imageURL, author, authorPFP, createdAt, updatedAt, messageID, id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11);" + "INSERT INTO news (guild, title, body, imageURL, author, authorPFP, createdAt, updatedAt, messageID, id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10);" ); const listAllQuery = database.query("SELECT * FROM news WHERE guild = $1;"); const getIdQuery = database.query("SELECT * FROM news WHERE id = $1;"); @@ -59,7 +59,7 @@ export function get(id: string) { return getIdQuery.get(id) as TypeOfDefinition | null; } -export function updateNews(id: string, title: string, body: string, imageURL: string) { +export function updateNews(id: string, title: string, body: string, imageURL: string | null) { const lastElem = deleteQuery.get(id) as TypeOfDefinition; addQuery.run( lastElem.guild, diff --git a/src/utils/sendChannelNews.ts b/src/utils/sendChannelNews.ts index f9e724e..93e9b12 100644 --- a/src/utils/sendChannelNews.ts +++ b/src/utils/sendChannelNews.ts @@ -1,32 +1,30 @@ -import { - EmbedBuilder, Guild, Role, - TextChannel -} from "discord.js"; +import { EmbedBuilder, Guild, Role, TextChannel } from "discord.js"; import { genColor } from "./colorGen"; export type News = { - title: string - body: string - imageURL: string - author: string - authorPfp: string - createdAt: string - updatedAt: string - messageId?: string -} + title: string; + body: string; + imageURL: string; + author: string; + authorPfp: string; + createdAt: string; + updatedAt: string; + messageId?: string; +}; export async function sendChannelNews(guild: Guild, news: News, id: string) { - const db = await database(); - const newsTable = await getNewsTable(db); - - const subscribedChannel = await newsTable.get(`${guild.id}.channel`).then( - (channel: { channelId: string | null; roleId: string | null; }) => channel as { channelId: string | null, roleId: string | null } - ).catch(() => { - return { - channelId: null as string | null, - roleId: null as string | null - }; - }); + const subscribedChannel = await newsTable + .get(`${guild.id}.channel`) + .then( + (channel: { channelId: string | null; roleId: string | null }) => + channel as { channelId: string | null; roleId: string | null } + ) + .catch(() => { + return { + channelId: null as string | null, + roleId: null as string | null + }; + }); if (!subscribedChannel) return; const channel = subscribedChannel.channelId; @@ -46,7 +44,10 @@ export async function sendChannelNews(guild: Guild, news: News, id: string) { .setFooter({ text: `Latest news from ${guild.name}` }) .setColor(genColor(200)); - const message = await channelToSend.send({ embeds: [embed], content: roleToSend ? `<@&${roleToSend.id}>` : undefined }); + const message = await channelToSend.send({ + embeds: [embed], + content: roleToSend ? `<@&${roleToSend.id}>` : undefined + }); await newsTable.set(`${guild.id}.news.${id}.messageId`, message.id); return; } From 8da700062d26fda8ee7f4de233d75908cd4482b2 Mon Sep 17 00:00:00 2001 From: Serge Date: Thu, 1 Feb 2024 22:31:48 +0600 Subject: [PATCH 028/127] slowly fixing... and killing my sanity --- src/commands/server/news/add.ts | 9 ++--- src/commands/server/news/channel.ts | 63 +++++++++++++++++++++++++++++ src/commands/server/news/edit.ts | 4 +- src/commands/server/news/remove.ts | 21 ++++------ src/utils/database/news.ts | 12 +++++- src/utils/sendChannelNews.ts | 40 +++++------------- 6 files changed, 96 insertions(+), 53 deletions(-) create mode 100644 src/commands/server/news/channel.ts diff --git a/src/commands/server/news/add.ts b/src/commands/server/news/add.ts index 2d9a246..d210c7a 100644 --- a/src/commands/server/news/add.ts +++ b/src/commands/server/news/add.ts @@ -69,7 +69,6 @@ export default class Add { newsModal.addComponents(firstActionRow, secondActionRow, thirdActionRow); await interaction.showModal(newsModal).catch(err => console.error(err)); - interaction.client.once("interactionCreate", async interaction => { if (!interaction.isModalSubmit()) return; @@ -84,8 +83,8 @@ export default class Add { const id = crypto.randomUUID(); const news = { id, - title: interaction.fields.getTextInputValue("title") as string, - body: interaction.fields.getTextInputValue("body") as string, + title: interaction.fields.getTextInputValue("title"), + body: interaction.fields.getTextInputValue("body"), imageURL, author: interaction.user.displayName, authorPfp: interaction.user.avatarURL(), @@ -97,8 +96,8 @@ export default class Add { sendChannelNews(guild, news as unknown as News, id).catch(err => console.error(err)); addNews( guild.id, - interaction.fields.getTextInputValue("title") as string, - interaction.fields.getTextInputValue("body") as string, + interaction.fields.getTextInputValue("title"), + interaction.fields.getTextInputValue("body"), imageURL!, interaction.user.displayName, interaction.user.avatarURL()!, diff --git a/src/commands/server/news/channel.ts b/src/commands/server/news/channel.ts new file mode 100644 index 0000000..f7883a1 --- /dev/null +++ b/src/commands/server/news/channel.ts @@ -0,0 +1,63 @@ +import { + SlashCommandSubcommandBuilder, + EmbedBuilder, + PermissionsBitField, + type ChatInputCommandInteraction, + ChannelType +} from "discord.js"; +import { genColor } from "../../../utils/colorGen"; +import { errorEmbed } from "../../../utils/embeds/errorEmbed"; + +export default class Channel { + data: SlashCommandSubcommandBuilder; + constructor() { + this.data = new SlashCommandSubcommandBuilder() + .setName("channel") + .setDescription( + "Sets/removes (when no options) a news channel, all news you post will be sent." + ) + .addChannelOption(option => + option.setName("channel").setDescription("The channel to send news to.") + ) + .addRoleOption(option => + option.setName("role").setDescription("The role to ping when news are sent.") + ); + } + + async run(interaction: ChatInputCommandInteraction) { + const guild = interaction.guild!; + const channelOption = interaction.options.getChannel("channel")!; + const roleOption = interaction.options.getRole("role"); + const channel = guild.channels.cache.get(channelOption?.id)!; + const role = guild.roles.cache.get(roleOption?.id ?? ""); + + if (!channelOption && !roleOption) { + await newsTable.delete(`${guild.id}.channel`); + return await interaction.reply({ + embeds: [new EmbedBuilder().setTitle("✅ • Removed news channel.")] + }); + } + + if (!interaction.memberPermissions.has(PermissionsBitField.Flags.ManageGuild)) { + return await interaction.reply({ + embeds: [errorEmbed("You need **Manage Server** permissions to add news.")] + }); + } + if (channel.type !== ChannelType.GuildText && channel.type !== ChannelType.GuildAnnouncement) { + return await interaction.reply({ + embeds: [errorEmbed("The channel must be a text channel.")] + }); + } + + await newsTable.set(`${guild.id}.channel`, { + channelId: channel.id, + roleId: role?.id ?? "" + }); + + const embed = new EmbedBuilder().setTitle("✅ • News channel set!").setColor(genColor(100)); + if (role) embed.setDescription(`The role <@&${role.id}> will be pinged when news are sent.`); + await interaction.reply({ + embeds: [new EmbedBuilder().setTitle("✅ • News channel set!").setColor(genColor(100))] + }); + } +} diff --git a/src/commands/server/news/edit.ts b/src/commands/server/news/edit.ts index 1b9b9ad..0c72a48 100644 --- a/src/commands/server/news/edit.ts +++ b/src/commands/server/news/edit.ts @@ -32,14 +32,14 @@ export default class Edit { .guild!.members.cache.get(interaction.user.id)! .permissions.has(PermissionsBitField.Flags.ManageGuild) ) - return await interaction.followUp({ + return await interaction.reply({ embeds: [errorEmbed("You need **Manage Server** permissions to add news.")] }); const id = interaction.options.getString("id", true).trim(); const news = get(id); if (!news) - return await interaction.followUp({ + return await interaction.reply({ embeds: [errorEmbed("The specified news doesn't exist.")] }); diff --git a/src/commands/server/news/remove.ts b/src/commands/server/news/remove.ts index c95e315..00ce8bf 100644 --- a/src/commands/server/news/remove.ts +++ b/src/commands/server/news/remove.ts @@ -7,7 +7,7 @@ import { } from "discord.js"; import { genColor } from "../../../utils/colorGen"; import { errorEmbed } from "../../../utils/embeds/errorEmbed"; -import { deleteNews } from "../../../utils/database/news"; +import { deleteNews, get } from "../../../utils/database/news"; export default class Remove { data: SlashCommandSubcommandBuilder; @@ -30,26 +30,21 @@ export default class Remove { const member = guild.members.cache.get(user.id)!; if (!member.permissions.has(PermissionsBitField.Flags.ManageGuild)) - return await interaction.followUp({ + return await interaction.reply({ embeds: [errorEmbed("You need **Manage Server** permissions to delete news.")] }); - const subscribedChannel = await newsTable - ?.get(`${guild.id}.channel`) - .then(channel => channel as { channelId: string; roleId: string }) - .catch(() => { - return { channelId: null, roleId: null }; - }); - + const news = get(id); if (!news) return await interaction.reply({ - embeds: [errorEmbed("The specified news doesn't exist.")] + embeds: [errorEmbed("The specified news don't exist.")] }); - const messageId = news?.messageId; - const newsChannel = (await interaction.guild.channels - .fetch(subscribedChannel?.channelId ?? "") + const messageId = news?.messageID; + const newsChannel = (await guild.channels + .fetch(news?.channelID ?? "") .catch(() => null)) as TextChannel | null; + if (newsChannel) await newsChannel?.messages.delete(messageId).catch(() => null); deleteNews(id); diff --git a/src/utils/database/news.ts b/src/utils/database/news.ts index 0af16be..4fc11d8 100644 --- a/src/utils/database/news.ts +++ b/src/utils/database/news.ts @@ -13,13 +13,15 @@ const definition = { createdAt: "TIMESTAMP", updatedAt: "TIMESTAMP", messageID: "TEXT", + channelID: "TEXT", + roleID: "TEXT", id: "TEXT" } } satisfies TableDefinition; const database = getDatabase(definition); const addQuery = database.query( - "INSERT INTO news (guild, title, body, imageURL, author, authorPFP, createdAt, updatedAt, messageID, id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10);" + "INSERT INTO news (guild, title, body, imageURL, author, authorPFP, createdAt, updatedAt, messageID, channelID, roleID, id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12);" ); const listAllQuery = database.query("SELECT * FROM news WHERE guild = $1;"); const getIdQuery = database.query("SELECT * FROM news WHERE id = $1;"); @@ -35,7 +37,9 @@ export function addNews( imageURL: string | null, author: string, authorPFP: string, - messageID: string | number + messageID: string | number, + channelID: string | number, + roleID: string | number ) { addQuery.run( guildID, @@ -47,6 +51,8 @@ export function addNews( Date.now(), 0, messageID, + channelID, + roleID, crypto.randomUUID() ); } @@ -71,6 +77,8 @@ export function updateNews(id: string, title: string, body: string, imageURL: st Date.now(), 0, lastElem.messageID, + lastElem.channelID, + lastElem.roleID, id ); updateQuery.run(id, title, body, imageURL); diff --git a/src/utils/sendChannelNews.ts b/src/utils/sendChannelNews.ts index 93e9b12..e595dc1 100644 --- a/src/utils/sendChannelNews.ts +++ b/src/utils/sendChannelNews.ts @@ -1,46 +1,24 @@ import { EmbedBuilder, Guild, Role, TextChannel } from "discord.js"; import { genColor } from "./colorGen"; +import { get } from "./database/news"; -export type News = { - title: string; - body: string; - imageURL: string; - author: string; - authorPfp: string; - createdAt: string; - updatedAt: string; - messageId?: string; -}; +export async function sendChannelNews(guild: Guild, id: string) { + const news = get(id); + if (!news) return; -export async function sendChannelNews(guild: Guild, news: News, id: string) { - const subscribedChannel = await newsTable - .get(`${guild.id}.channel`) - .then( - (channel: { channelId: string | null; roleId: string | null }) => - channel as { channelId: string | null; roleId: string | null } - ) - .catch(() => { - return { - channelId: null as string | null, - roleId: null as string | null - }; - }); - - if (!subscribedChannel) return; - const channel = subscribedChannel.channelId; - const channelToSend = guild.channels.cache.get(channel) as TextChannel; + const channelToSend = guild.channels.cache.get(news.channelID) as TextChannel; if (!channelToSend) return; - const role = subscribedChannel.roleId; + const role = news.roleID; let roleToSend: Role | undefined; if (role) roleToSend = guild.roles.cache.get(role); const embed = new EmbedBuilder() - .setAuthor({ name: news.author, iconURL: news.authorPfp ?? null }) + .setAuthor({ name: news.author, iconURL: news.authorPFP }) .setTitle(news.title) .setDescription(news.body) - .setImage(news.imageURL || null) - .setTimestamp(parseInt(news.updatedAt)) + .setImage(news.imageURL) + .setTimestamp(parseInt(news.updatedAt.toString())) .setFooter({ text: `Latest news from ${guild.name}` }) .setColor(genColor(200)); From 554b9ebeb5f3eadd146772131a76d715c40b0569 Mon Sep 17 00:00:00 2001 From: Serge Date: Fri, 2 Feb 2024 21:33:23 +0600 Subject: [PATCH 029/127] aaaaaaaaaaaaaaaaa --- src/commands/server/news/add.ts | 19 ++++--------------- src/commands/server/news/channel.ts | 9 ++++----- src/utils/database/news.ts | 13 +++++-------- src/utils/database/settings.ts | 2 +- 4 files changed, 14 insertions(+), 29 deletions(-) diff --git a/src/commands/server/news/add.ts b/src/commands/server/news/add.ts index d210c7a..2f4e0f2 100644 --- a/src/commands/server/news/add.ts +++ b/src/commands/server/news/add.ts @@ -10,7 +10,7 @@ import { } from "discord.js"; import { genColor } from "../../../utils/colorGen"; import { errorEmbed } from "../../../utils/embeds/errorEmbed"; -import { sendChannelNews, News } from "../../../utils/sendChannelNews"; +import { sendChannelNews } from "../../../utils/sendChannelNews"; import { addNews } from "../../../utils/database/news"; export default class Add { @@ -80,20 +80,7 @@ export default class Add { return; } - const id = crypto.randomUUID(); - const news = { - id, - title: interaction.fields.getTextInputValue("title"), - body: interaction.fields.getTextInputValue("body"), - imageURL, - author: interaction.user.displayName, - authorPfp: interaction.user.avatarURL(), - createdAt: Date.now().toString(), - updatedAt: Date.now().toString(), - messageId: null - }; - - sendChannelNews(guild, news as unknown as News, id).catch(err => console.error(err)); + sendChannelNews(guild, crypto.randomUUID()).catch(err => console.error(err)); addNews( guild.id, interaction.fields.getTextInputValue("title"), @@ -101,6 +88,8 @@ export default class Add { imageURL!, interaction.user.displayName, interaction.user.avatarURL()!, + null!, + null!, null! ); await interaction.reply({ diff --git a/src/commands/server/news/channel.ts b/src/commands/server/news/channel.ts index f7883a1..b017bfb 100644 --- a/src/commands/server/news/channel.ts +++ b/src/commands/server/news/channel.ts @@ -7,6 +7,7 @@ import { } from "discord.js"; import { genColor } from "../../../utils/colorGen"; import { errorEmbed } from "../../../utils/embeds/errorEmbed"; +import { setSetting as set } from "../../../utils/database/settings"; export default class Channel { data: SlashCommandSubcommandBuilder; @@ -32,13 +33,13 @@ export default class Channel { const role = guild.roles.cache.get(roleOption?.id ?? ""); if (!channelOption && !roleOption) { - await newsTable.delete(`${guild.id}.channel`); + set(guild.id, "news.channel", 0); return await interaction.reply({ embeds: [new EmbedBuilder().setTitle("✅ • Removed news channel.")] }); } - if (!interaction.memberPermissions.has(PermissionsBitField.Flags.ManageGuild)) { + if (!interaction.memberPermissions!.has(PermissionsBitField.Flags.ManageGuild)) { return await interaction.reply({ embeds: [errorEmbed("You need **Manage Server** permissions to add news.")] }); @@ -56,8 +57,6 @@ export default class Channel { const embed = new EmbedBuilder().setTitle("✅ • News channel set!").setColor(genColor(100)); if (role) embed.setDescription(`The role <@&${role.id}> will be pinged when news are sent.`); - await interaction.reply({ - embeds: [new EmbedBuilder().setTitle("✅ • News channel set!").setColor(genColor(100))] - }); + await interaction.reply({ embeds: [embed] }); } } diff --git a/src/utils/database/news.ts b/src/utils/database/news.ts index 4fc11d8..a4c6036 100644 --- a/src/utils/database/news.ts +++ b/src/utils/database/news.ts @@ -25,21 +25,18 @@ const addQuery = database.query( ); const listAllQuery = database.query("SELECT * FROM news WHERE guild = $1;"); const getIdQuery = database.query("SELECT * FROM news WHERE id = $1;"); -const updateQuery = database.query( - "UPDATE news SET title = $2, body = $3, imageURL = $4 WHERE id = $1" -); const deleteQuery = database.query("DELETE FROM news WHERE id = $1"); export function addNews( - guildID: number | string, + guildID: string, title: string, body: string, imageURL: string | null, author: string, authorPFP: string, - messageID: string | number, - channelID: string | number, - roleID: string | number + messageID: string, + channelID: string, + roleID: string ) { addQuery.run( guildID, @@ -67,6 +64,7 @@ export function get(id: string) { export function updateNews(id: string, title: string, body: string, imageURL: string | null) { const lastElem = deleteQuery.get(id) as TypeOfDefinition; + deleteQuery.run(id); addQuery.run( lastElem.guild, title, @@ -81,7 +79,6 @@ export function updateNews(id: string, title: string, body: string, imageURL: st lastElem.roleID, id ); - updateQuery.run(id, title, body, imageURL); } export function deleteNews(id: string) { diff --git a/src/utils/database/settings.ts b/src/utils/database/settings.ts index b57d0e5..9a37957 100644 --- a/src/utils/database/settings.ts +++ b/src/utils/database/settings.ts @@ -15,13 +15,13 @@ export const settingsDefinition = { "levelling.enabled": "BOOL", "levelling.channel": "INTEGER", "log.channel": "INTEGER", + "news.channel": "INTEGER", "serverboard.inviteLink": "TEXT", "serverboard.shown": "BOOL" } satisfies Record; export const settingsKeys = Object.keys(settingsDefinition) as (keyof typeof settingsDefinition)[]; const database = getDatabase(tableDefinition); - const getQuery = database.query("SELECT * FROM settings WHERE guild = $1 AND key = $2;"); const listPublicQuery = database.query( "SELECT * FROM settings WHERE key = 'serverboard.shown' AND value = 'TRUE';" From 79ea0af9c98a90b8bd07802aef879a9e50ecf167 Mon Sep 17 00:00:00 2001 From: Serge Date: Mon, 5 Feb 2024 22:36:56 +0600 Subject: [PATCH 030/127] test --- src/commands/server/news/channel.ts | 9 ++++----- src/utils/database/settings.ts | 3 ++- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/commands/server/news/channel.ts b/src/commands/server/news/channel.ts index b017bfb..2f2d6b9 100644 --- a/src/commands/server/news/channel.ts +++ b/src/commands/server/news/channel.ts @@ -33,7 +33,8 @@ export default class Channel { const role = guild.roles.cache.get(roleOption?.id ?? ""); if (!channelOption && !roleOption) { - set(guild.id, "news.channel", 0); + set(guild.id, "news.channelID", ""); + set(guild.id, "news.roleID", ""); return await interaction.reply({ embeds: [new EmbedBuilder().setTitle("✅ • Removed news channel.")] }); @@ -50,10 +51,8 @@ export default class Channel { }); } - await newsTable.set(`${guild.id}.channel`, { - channelId: channel.id, - roleId: role?.id ?? "" - }); + set(guild.id, "news.channelID", channel.id); + set(guild.id, "news.roleID", role?.id ?? ""); const embed = new EmbedBuilder().setTitle("✅ • News channel set!").setColor(genColor(100)); if (role) embed.setDescription(`The role <@&${role.id}> will be pinged when news are sent.`); diff --git a/src/utils/database/settings.ts b/src/utils/database/settings.ts index 9a37957..08d154f 100644 --- a/src/utils/database/settings.ts +++ b/src/utils/database/settings.ts @@ -15,7 +15,8 @@ export const settingsDefinition = { "levelling.enabled": "BOOL", "levelling.channel": "INTEGER", "log.channel": "INTEGER", - "news.channel": "INTEGER", + "news.channelID": "TEXT", + "news.roleID": "TEXT", "serverboard.inviteLink": "TEXT", "serverboard.shown": "BOOL" } satisfies Record; From 006de1b7bc816b9f0b7039b6e5f419b7bce8ecb4 Mon Sep 17 00:00:00 2001 From: ThatBoiDev Date: Wed, 7 Feb 2024 14:59:35 +0100 Subject: [PATCH 031/127] Started at the poll command and some fixes in the code --- README.md | 2 +- bun.lockb | Bin 59844 -> 59784 bytes src/commands/poll.ts | 136 ++++++++++++++++++ src/events/guildCreate.ts | 2 +- .../extendedSlashCommandSubcommandBuilder.ts | 42 ++++++ 5 files changed, 180 insertions(+), 2 deletions(-) create mode 100644 src/commands/poll.ts create mode 100644 src/utils/extendedSlashCommandSubcommandBuilder.ts diff --git a/README.md b/README.md index 43a1426..8917692 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@

- +

diff --git a/bun.lockb b/bun.lockb index 010f37d64c002dd89ed60e033816bbad21e54a1d..f63c8129d5e73add0bb7e0b6f402be103abb4960 100644 GIT binary patch delta 2452 zcmX?dnYrUK^8`PpA1^ir%*mHb3uS3^#dFH4D|yP@32s)rzpWO0TZi$2m=Eb zFySTuX+CHICPoaTH#d9ou@a%lM@mE}!~*qT<1%wofu=Gf=Va<-6@yqnE9fIuKTsA! z?X9P~k5N5bPrsl>wHRtI$o_$dcbH@9>7C$U7DMd?`kziI9nE5>y`b5U@-(EE8r#C#@%Y@Mvrfs0o(cynd#q|Kyv6 F699L!^%wvE delta 148 zcmeCU%zWfB^8`O8lb0I<=Hzeos5!wnxqyph@{U50&8{^&m_Qr>0TBiUE(V5%1R%`^ zq$eA4X@FH7DFLYpfM^LR6Pf&=nq@LijmGAH8V^=RmdTF7`jZ1{0w#O#fP{i&H?N=R k=`U0gR2p33>XKsck^u}%UQV7hSB>S`{pDvSJJw790E`(j-T(jq diff --git a/src/commands/poll.ts b/src/commands/poll.ts new file mode 100644 index 0000000..e289e29 --- /dev/null +++ b/src/commands/poll.ts @@ -0,0 +1,136 @@ +import { + SlashCommandSubcommandBuilder, + ButtonBuilder, + ActionRowBuilder, + ButtonStyle, + ButtonInteraction, + ComponentType, + PermissionsBitField, + ChannelType, + EmbedBuilder, + type ChatInputCommandInteraction, + type Channel, + type TextChannel +} from "discord.js"; +import { errorEmbed } from "../utils/embeds/errorEmbed"; +import { genColor } from "../utils/colorGen"; +import { ExtendedSlashCommandSubcommandBuilder } from "../utils/extendedSlashCommandSubcommandBuilder"; + +export default class Poll { + numberOfFields: number; + data: ExtendedSlashCommandSubcommandBuilder; + constructor() { + this.numberOfFields = 6; + this.data = new ExtendedSlashCommandSubcommandBuilder() + .setName("poll") + .setDescription("Make a poll") + .addStringOption(option => + option + .setName("question") + .setDescription("What's the question you want to ask?") + .setRequired(true) + ) + .addChannelOption(channel => + channel + .setName("channel") + .setDescription("The channel to send the poll in") + .setRequired(true) + ) + .genNumberFields("Option", this.numberOfFields, 2) + .addAttachmentOption(option => + option + .setName("image") + .setDescription("The image to appear at the bottom of the embed") + .setRequired(false) + ); + } + + async run(interaction: ChatInputCommandInteraction) { + function convertNumEmoji(num: number) { + const numEmojis = ["1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣", "🔟"]; + return numEmojis[num]; + } + + const question = interaction.options.getString("question"); + const channel = interaction.options.getChannel("channel"); + const image = interaction.options.getAttachment("image")!; + let options: string[] = []; + + const guild = interaction.guild!; + const members = guild.members.cache; + const member = members.get(interaction.member?.user.id!)!; + + for (let i = 0; i < this.numberOfFields; i++) { + const option = interaction.options.getString(`option${i + 1}`); + if (option) { + options.push(option); + } + } + + if (!member.permissions.has(PermissionsBitField.Flags.ManageMessages)) { + return await interaction.reply({ + embeds: [ + errorEmbed( + "You can't execute this command.", + "You need the **Manage Messages** permission." + ) + ] + }); + } + + if (!interaction.guild?.members.me?.permissions.has(PermissionsBitField.Flags.SendMessages)) { + return await interaction.reply({ + embeds: [ + errorEmbed( + `Nebula can't execute this command.", "Nebula needs the **Send Messages** permission for ${channel}.` + ) + ] + }); + } + + let successEmbed = new EmbedBuilder() + .setTitle("✅ • Poll has been created successfully") + .setDescription(`Poll is sent to ${channel}`) + .setColor(genColor(200)); + + let embed = new EmbedBuilder() + .setAuthor({ + name: `• Poll by ${member.user.username}`, + iconURL: member.user.displayAvatarURL() + }) + .setTitle(`📊 • ${question}`) + .setFields( + options.map((option, i) => { + return { + name: `Option ${convertNumEmoji(i)}`, + value: option, + inline: false + }; + }) + ) + .setColor(genColor(200)); + + if (image) { + embed.setImage(image.url); + } + + await interaction.reply({ embeds: [successEmbed] }); + const pollChannel = await guild.channels.cache + .get(`${channel}`) + ?.fetch() + .then((channel: Channel) => { + if (channel.type != ChannelType.GuildText) return null; + return channel as TextChannel; + }) + .catch(() => null); + if (pollChannel) { + const pollMsg = await pollChannel.send({ embeds: [embed] }); + console.log(`sent poll to ${pollChannel}:\n${pollMsg}`); + // TODO: rewrite multiReact to use an array instead of a stupid string + const optionEmojis = options.map((_, i) => convertNumEmoji(i)); + for (const emoji of optionEmojis) { + await pollMsg.react(emoji); // Can't use multiReact here because number emojis are fucking stupid and Typescript is weird + } + } + } +} diff --git a/src/events/guildCreate.ts b/src/events/guildCreate.ts index e2e084f..f7ccb41 100644 --- a/src/events/guildCreate.ts +++ b/src/events/guildCreate.ts @@ -22,7 +22,7 @@ export default { [ "Nebula is a multiplatform, multipurpose bot with the ability to add extensions to have additional features.", "To manage the bot, use the /server settings command.", - "Nebula is in an early stage of development. If you find bugs, please go to our [official server](https://discord.gg/7RdABJhQss) and report them." + "Nebula is in an early stage of development. If you find bugs, please go to our [official server](https://discord.gg/c6C25P4BuY) and report them." ].join("\n\n") ) .setFooter({ text: `Made by the Nebula team with ${randomise(hearts)}` }) diff --git a/src/utils/extendedSlashCommandSubcommandBuilder.ts b/src/utils/extendedSlashCommandSubcommandBuilder.ts new file mode 100644 index 0000000..3cf65e8 --- /dev/null +++ b/src/utils/extendedSlashCommandSubcommandBuilder.ts @@ -0,0 +1,42 @@ +/** + * Generates fields and descriptions for commands which serve the same purpose but have to be reused. + * @param {SlashCommandSubcommandBuilder} command The command to add the fields to. + * @param {string} name Reused name of the fields. + * @param {number} number Number of fields to generate. + * @param {number} numRequired Number of required fields. + * @returns Returns the generated fields. + */ + +import { SlashCommandSubcommandBuilder } from "discord.js"; + +export class ExtendedSlashCommandSubcommandBuilder extends SlashCommandSubcommandBuilder { + /** + * Generates fields and descriptions for commands which serve the same purpose but have to be reused. + * @param {SlashCommandSubcommandBuilder} command The command to add the fields to. + * @param {string} name Reused name of the fields. + * @param {number} number Number of fields to generate. + * @param {number} numRequired Number of required fields. + * @returns Returns the generated fields. + */ +genNumberFields(name: string, number: number, numRequired: number = 0) { + for (let i = 0; i < numRequired; i++) { + this.addStringOption(option => + option + .setName(`${name.toLowerCase()}${i + 1}`) + .setDescription(`${name} ${i + 1}`) + .setRequired(true) + ); + } + + for (let i = numRequired; i < number; i++) { + this.addStringOption(option => + option + .setName(`${name.toLowerCase()}${i + 1}`) + .setDescription(`${name} ${i + 1}, not required`) + .setRequired(false) + ); + } + + return this; +} +} \ No newline at end of file From ee1853738d568a8d4abe40b44bd1188122a35219 Mon Sep 17 00:00:00 2001 From: Serge Date: Wed, 7 Feb 2024 22:36:55 +0600 Subject: [PATCH 032/127] fixed /poll --- src/commands/moderation/purge.ts | 4 +- src/commands/poll.ts | 83 ++++++------------- src/commands/server/news/channel.ts | 61 -------------- src/events/messageCreate.ts | 22 ++--- .../extendedSlashCommandSubcommandBuilder.ts | 49 +++++------ 5 files changed, 58 insertions(+), 161 deletions(-) delete mode 100644 src/commands/server/news/channel.ts diff --git a/src/commands/moderation/purge.ts b/src/commands/moderation/purge.ts index 933228f..21e76e8 100644 --- a/src/commands/moderation/purge.ts +++ b/src/commands/moderation/purge.ts @@ -26,9 +26,7 @@ export default class Purge { .addChannelOption(channel => channel .setName("channel") - .setDescription( - "The channel that you want to purge. Leave empty if you want to purge the current channel." - ) + .setDescription("The channel that you want to purge.") .addChannelTypes( ChannelType.GuildText, ChannelType.PublicThread, diff --git a/src/commands/poll.ts b/src/commands/poll.ts index e289e29..7d0c5b9 100644 --- a/src/commands/poll.ts +++ b/src/commands/poll.ts @@ -1,15 +1,8 @@ import { - SlashCommandSubcommandBuilder, - ButtonBuilder, - ActionRowBuilder, - ButtonStyle, - ButtonInteraction, - ComponentType, PermissionsBitField, ChannelType, EmbedBuilder, type ChatInputCommandInteraction, - type Channel, type TextChannel } from "discord.js"; import { errorEmbed } from "../utils/embeds/errorEmbed"; @@ -17,31 +10,31 @@ import { genColor } from "../utils/colorGen"; import { ExtendedSlashCommandSubcommandBuilder } from "../utils/extendedSlashCommandSubcommandBuilder"; export default class Poll { - numberOfFields: number; data: ExtendedSlashCommandSubcommandBuilder; constructor() { - this.numberOfFields = 6; this.data = new ExtendedSlashCommandSubcommandBuilder() .setName("poll") .setDescription("Make a poll") .addStringOption(option => option .setName("question") - .setDescription("What's the question you want to ask?") + .setDescription("The question that you want to ask.") .setRequired(true) ) .addChannelOption(channel => channel .setName("channel") - .setDescription("The channel to send the poll in") + .setDescription("The channel to send the poll in.") .setRequired(true) + .addChannelTypes( + ChannelType.GuildText, + ChannelType.PublicThread, + ChannelType.PrivateThread + ) ) - .genNumberFields("Option", this.numberOfFields, 2) + .genNumberFields("Option", 6, 2) .addAttachmentOption(option => - option - .setName("image") - .setDescription("The image to appear at the bottom of the embed") - .setRequired(false) + option.setName("image").setDescription("The image that appears at the bottom of the embed.") ); } @@ -51,23 +44,18 @@ export default class Poll { return numEmojis[num]; } - const question = interaction.options.getString("question"); - const channel = interaction.options.getChannel("channel"); + const channel = interaction.options.getChannel("channel") as TextChannel; const image = interaction.options.getAttachment("image")!; - let options: string[] = []; - const guild = interaction.guild!; - const members = guild.members.cache; - const member = members.get(interaction.member?.user.id!)!; + const member = guild.members.cache.get(interaction.member?.user.id!)!; + let options: string[] = []; - for (let i = 0; i < this.numberOfFields; i++) { + for (let i = 0; i < 6; i++) { const option = interaction.options.getString(`option${i + 1}`); - if (option) { - options.push(option); - } + if (option) options.push(option); } - if (!member.permissions.has(PermissionsBitField.Flags.ManageMessages)) { + if (!member.permissions.has(PermissionsBitField.Flags.ManageMessages)) return await interaction.reply({ embeds: [ errorEmbed( @@ -76,9 +64,8 @@ export default class Poll { ) ] }); - } - if (!interaction.guild?.members.me?.permissions.has(PermissionsBitField.Flags.SendMessages)) { + if (!interaction.guild?.members.me?.permissions.has(PermissionsBitField.Flags.SendMessages)) return await interaction.reply({ embeds: [ errorEmbed( @@ -86,51 +73,33 @@ export default class Poll { ) ] }); - } - let successEmbed = new EmbedBuilder() - .setTitle("✅ • Poll has been created successfully") + const successEmbed = new EmbedBuilder() + .setTitle("✅ • Poll has been created successfully") .setDescription(`Poll is sent to ${channel}`) - .setColor(genColor(200)); + .setColor(genColor(100)); let embed = new EmbedBuilder() .setAuthor({ name: `• Poll by ${member.user.username}`, iconURL: member.user.displayAvatarURL() }) - .setTitle(`📊 • ${question}`) + .setTitle(`${interaction.options.getString("question")}`) .setFields( options.map((option, i) => { return { name: `Option ${convertNumEmoji(i)}`, - value: option, - inline: false + value: option }; }) ) .setColor(genColor(200)); - if (image) { - embed.setImage(image.url); - } - await interaction.reply({ embeds: [successEmbed] }); - const pollChannel = await guild.channels.cache - .get(`${channel}`) - ?.fetch() - .then((channel: Channel) => { - if (channel.type != ChannelType.GuildText) return null; - return channel as TextChannel; - }) - .catch(() => null); - if (pollChannel) { - const pollMsg = await pollChannel.send({ embeds: [embed] }); - console.log(`sent poll to ${pollChannel}:\n${pollMsg}`); - // TODO: rewrite multiReact to use an array instead of a stupid string - const optionEmojis = options.map((_, i) => convertNumEmoji(i)); - for (const emoji of optionEmojis) { - await pollMsg.react(emoji); // Can't use multiReact here because number emojis are fucking stupid and Typescript is weird - } - } + + if (image) embed.setImage(image.url); + await channel.send({ embeds: [embed] }).then(async message => { + for (const emoji of options.map((_, i) => convertNumEmoji(i))) await message.react(emoji); + }); } } diff --git a/src/commands/server/news/channel.ts b/src/commands/server/news/channel.ts deleted file mode 100644 index 2f2d6b9..0000000 --- a/src/commands/server/news/channel.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { - SlashCommandSubcommandBuilder, - EmbedBuilder, - PermissionsBitField, - type ChatInputCommandInteraction, - ChannelType -} from "discord.js"; -import { genColor } from "../../../utils/colorGen"; -import { errorEmbed } from "../../../utils/embeds/errorEmbed"; -import { setSetting as set } from "../../../utils/database/settings"; - -export default class Channel { - data: SlashCommandSubcommandBuilder; - constructor() { - this.data = new SlashCommandSubcommandBuilder() - .setName("channel") - .setDescription( - "Sets/removes (when no options) a news channel, all news you post will be sent." - ) - .addChannelOption(option => - option.setName("channel").setDescription("The channel to send news to.") - ) - .addRoleOption(option => - option.setName("role").setDescription("The role to ping when news are sent.") - ); - } - - async run(interaction: ChatInputCommandInteraction) { - const guild = interaction.guild!; - const channelOption = interaction.options.getChannel("channel")!; - const roleOption = interaction.options.getRole("role"); - const channel = guild.channels.cache.get(channelOption?.id)!; - const role = guild.roles.cache.get(roleOption?.id ?? ""); - - if (!channelOption && !roleOption) { - set(guild.id, "news.channelID", ""); - set(guild.id, "news.roleID", ""); - return await interaction.reply({ - embeds: [new EmbedBuilder().setTitle("✅ • Removed news channel.")] - }); - } - - if (!interaction.memberPermissions!.has(PermissionsBitField.Flags.ManageGuild)) { - return await interaction.reply({ - embeds: [errorEmbed("You need **Manage Server** permissions to add news.")] - }); - } - if (channel.type !== ChannelType.GuildText && channel.type !== ChannelType.GuildAnnouncement) { - return await interaction.reply({ - embeds: [errorEmbed("The channel must be a text channel.")] - }); - } - - set(guild.id, "news.channelID", channel.id); - set(guild.id, "news.roleID", role?.id ?? ""); - - const embed = new EmbedBuilder().setTitle("✅ • News channel set!").setColor(genColor(100)); - if (role) embed.setDescription(`The role <@&${role.id}> will be pinged when news are sent.`); - await interaction.reply({ embeds: [embed] }); - } -} diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts index cc846bb..48e040b 100644 --- a/src/events/messageCreate.ts +++ b/src/events/messageCreate.ts @@ -13,9 +13,19 @@ export default { async run(message: Message) { const author = message.author; const guild = message.guild!; - if (author.bot) return; + // Easter egg handler + if (guild.id === "1079612082636472420") { + const eventsPath = join(process.cwd(), "src", "events", "easterEggs"); + + for (const easterEggFile of readdirSync(eventsPath)) + new (await import(pathToFileURL(join(eventsPath, easterEggFile)).toString())).default().run( + message, + ...message.content + ); + } + // Levelling const levelChannelId = getSetting(guild.id, "levelling.channel"); if (!levelChannelId) return; @@ -70,16 +80,6 @@ export default { await authorRoles?.remove(role); } - - // Easter egg handler - if (guild?.id !== "903852579837059113") return; - const eventsPath = join(process.cwd(), "src", "events", "easterEggs"); - - for (const easterEggFile of readdirSync(eventsPath)) - new (await import(pathToFileURL(join(eventsPath, easterEggFile)).toString())).default().run( - message, - ...message.content - ); } } }; diff --git a/src/utils/extendedSlashCommandSubcommandBuilder.ts b/src/utils/extendedSlashCommandSubcommandBuilder.ts index 3cf65e8..0ed1df4 100644 --- a/src/utils/extendedSlashCommandSubcommandBuilder.ts +++ b/src/utils/extendedSlashCommandSubcommandBuilder.ts @@ -1,16 +1,7 @@ -/** - * Generates fields and descriptions for commands which serve the same purpose but have to be reused. - * @param {SlashCommandSubcommandBuilder} command The command to add the fields to. - * @param {string} name Reused name of the fields. - * @param {number} number Number of fields to generate. - * @param {number} numRequired Number of required fields. - * @returns Returns the generated fields. - */ - import { SlashCommandSubcommandBuilder } from "discord.js"; export class ExtendedSlashCommandSubcommandBuilder extends SlashCommandSubcommandBuilder { - /** + /** * Generates fields and descriptions for commands which serve the same purpose but have to be reused. * @param {SlashCommandSubcommandBuilder} command The command to add the fields to. * @param {string} name Reused name of the fields. @@ -18,25 +9,25 @@ export class ExtendedSlashCommandSubcommandBuilder extends SlashCommandSubcomman * @param {number} numRequired Number of required fields. * @returns Returns the generated fields. */ -genNumberFields(name: string, number: number, numRequired: number = 0) { - for (let i = 0; i < numRequired; i++) { - this.addStringOption(option => - option - .setName(`${name.toLowerCase()}${i + 1}`) - .setDescription(`${name} ${i + 1}`) - .setRequired(true) - ); - } + genNumberFields(name: string, number: number, numRequired: number = 0) { + for (let i = 0; i < numRequired; i++) { + this.addStringOption(option => + option + .setName(`${name.toLowerCase()}${i + 1}`) + .setDescription(`${name} ${i + 1}`) + .setRequired(true) + ); + } - for (let i = numRequired; i < number; i++) { - this.addStringOption(option => - option - .setName(`${name.toLowerCase()}${i + 1}`) - .setDescription(`${name} ${i + 1}, not required`) - .setRequired(false) - ); - } + for (let i = numRequired; i < number; i++) { + this.addStringOption(option => + option + .setName(`${name.toLowerCase()}${i + 1}`) + .setDescription(`${name} ${i + 1}, not required`) + .setRequired(false) + ); + } - return this; + return this; + } } -} \ No newline at end of file From 605b4c8d83c17fd817e8b1dfa89406c8a53b4f5a Mon Sep 17 00:00:00 2001 From: Serge Date: Wed, 7 Feb 2024 22:50:16 +0600 Subject: [PATCH 033/127] quick fix --- src/commands/poll.ts | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/src/commands/poll.ts b/src/commands/poll.ts index 7d0c5b9..64f3f4f 100644 --- a/src/commands/poll.ts +++ b/src/commands/poll.ts @@ -21,20 +21,19 @@ export default class Poll { .setDescription("The question that you want to ask.") .setRequired(true) ) + .genNumberFields("Option", 6, 2) + .addAttachmentOption(option => + option.setName("image").setDescription("The image that appears at the bottom of the embed.") + ) .addChannelOption(channel => channel .setName("channel") .setDescription("The channel to send the poll in.") - .setRequired(true) .addChannelTypes( ChannelType.GuildText, ChannelType.PublicThread, ChannelType.PrivateThread ) - ) - .genNumberFields("Option", 6, 2) - .addAttachmentOption(option => - option.setName("image").setDescription("The image that appears at the bottom of the embed.") ); } @@ -74,11 +73,6 @@ export default class Poll { ] }); - const successEmbed = new EmbedBuilder() - .setTitle("✅ • Poll has been created successfully") - .setDescription(`Poll is sent to ${channel}`) - .setColor(genColor(100)); - let embed = new EmbedBuilder() .setAuthor({ name: `• Poll by ${member.user.username}`, @@ -95,10 +89,20 @@ export default class Poll { ) .setColor(genColor(200)); - await interaction.reply({ embeds: [successEmbed] }); - if (image) embed.setImage(image.url); - await channel.send({ embeds: [embed] }).then(async message => { + if (channel) { + const successEmbed = new EmbedBuilder() + .setTitle("✅ • Poll has been created successfully") + .setDescription(`Poll is sent to ${channel}`) + .setColor(genColor(100)); + + await interaction.reply({ embeds: [successEmbed] }); + await channel.send({ embeds: [embed] }).then(async message => { + for (const emoji of options.map((_, i) => convertNumEmoji(i))) await message.react(emoji); + }); + } + + await interaction.channel?.send({ embeds: [embed] }).then(async message => { for (const emoji of options.map((_, i) => convertNumEmoji(i))) await message.react(emoji); }); } From a44ea2f634dab4ecc0441cf671418987d13a04d4 Mon Sep 17 00:00:00 2001 From: ThatBoiDev Date: Wed, 7 Feb 2024 18:24:51 +0100 Subject: [PATCH 034/127] Did some more fixes to /poll, work in progress. --- src/commands/poll.ts | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/src/commands/poll.ts b/src/commands/poll.ts index 64f3f4f..9c06320 100644 --- a/src/commands/poll.ts +++ b/src/commands/poll.ts @@ -90,20 +90,35 @@ export default class Poll { .setColor(genColor(200)); if (image) embed.setImage(image.url); - if (channel) { - const successEmbed = new EmbedBuilder() - .setTitle("✅ • Poll has been created successfully") - .setDescription(`Poll is sent to ${channel}`) - .setColor(genColor(100)); - await interaction.reply({ embeds: [successEmbed] }); + const successEmbed = new EmbedBuilder() + .setTitle("✅ • Poll has been created successfully") + .setDescription(`Poll is sent to ${channel ? channel : interaction.channel}`) + .setColor(genColor(100)); + await interaction.reply({ embeds: [successEmbed] }); + + if (channel) { await channel.send({ embeds: [embed] }).then(async message => { + messageId = message.id; for (const emoji of options.map((_, i) => convertNumEmoji(i))) await message.react(emoji); }); } + let messageId: string; await interaction.channel?.send({ embeds: [embed] }).then(async message => { + messageId = message.id; for (const emoji of options.map((_, i) => convertNumEmoji(i))) await message.react(emoji); }); + + interaction.client.on("messageReactionAdd", async (reaction, user) => { + if (!user.bot && reaction.message.id === messageId) { + const userReactions = reaction.message.reactions.cache.filter(reaction => + reaction.users.cache.has(user.id) + ); + if (userReactions.size > 1) await userReactions.last()?.remove(); + } + }); + + return; } } From 092e25ec25dec43539b4b76c976aef7af8eee251 Mon Sep 17 00:00:00 2001 From: ThatBoiDev Date: Thu, 8 Feb 2024 14:33:13 +0100 Subject: [PATCH 035/127] Added a reaction handler for poll --- src/commands/poll.ts | 18 +++--------------- src/events/messageReactionAdd.ts | 24 ++++++++++++++++++++++++ src/utils/database/poll.ts | 29 +++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 15 deletions(-) create mode 100644 src/events/messageReactionAdd.ts create mode 100644 src/utils/database/poll.ts diff --git a/src/commands/poll.ts b/src/commands/poll.ts index 9c06320..15f7b19 100644 --- a/src/commands/poll.ts +++ b/src/commands/poll.ts @@ -8,6 +8,7 @@ import { import { errorEmbed } from "../utils/embeds/errorEmbed"; import { genColor } from "../utils/colorGen"; import { ExtendedSlashCommandSubcommandBuilder } from "../utils/extendedSlashCommandSubcommandBuilder"; +import { addPoll } from "../utils/database/poll"; export default class Poll { data: ExtendedSlashCommandSubcommandBuilder; @@ -99,26 +100,13 @@ export default class Poll { if (channel) { await channel.send({ embeds: [embed] }).then(async message => { - messageId = message.id; for (const emoji of options.map((_, i) => convertNumEmoji(i))) await message.react(emoji); }); } - let messageId: string; - await interaction.channel?.send({ embeds: [embed] }).then(async message => { - messageId = message.id; + return await interaction.channel?.send({ embeds: [embed] }).then(async message => { for (const emoji of options.map((_, i) => convertNumEmoji(i))) await message.react(emoji); + addPoll(interaction.guildId!, message.id); }); - - interaction.client.on("messageReactionAdd", async (reaction, user) => { - if (!user.bot && reaction.message.id === messageId) { - const userReactions = reaction.message.reactions.cache.filter(reaction => - reaction.users.cache.has(user.id) - ); - if (userReactions.size > 1) await userReactions.last()?.remove(); - } - }); - - return; } } diff --git a/src/events/messageReactionAdd.ts b/src/events/messageReactionAdd.ts new file mode 100644 index 0000000..f76a241 --- /dev/null +++ b/src/events/messageReactionAdd.ts @@ -0,0 +1,24 @@ +import { type Client, type Guild, type User, type MessageReaction } from "discord.js"; +import { getPolls } from "../utils/database/poll"; + +export default { + name: "messageReactionAdd", + event: class MessageReactionAdd { + client: Client; + constructor(client: Client) { + this.client = client; + } + + async run(guild: Guild, user: User, reaction: MessageReaction) { + if (user.bot) return; + + // Poll reaction handler + const messageIds = getPolls(guild.id); + if (!messageIds.includes(reaction.message.id)) return; + const userReactions = reaction.message.reactions.cache.filter(reaction => + reaction.users.cache.has(user.id) + ); + if (userReactions.size > 1) await userReactions.last()?.remove(); + } + } +}; diff --git a/src/utils/database/poll.ts b/src/utils/database/poll.ts new file mode 100644 index 0000000..1cafa99 --- /dev/null +++ b/src/utils/database/poll.ts @@ -0,0 +1,29 @@ +import { getDatabase } from "."; +import { TableDefinition, TypeOfDefinition } from "./types"; + +const tableDefinition = { + name: "polls", + definition: { + guild: "TEXT", + message: "TEXT" + } +} satisfies TableDefinition; + +const database = getDatabase(tableDefinition); +const getQuery = database.query("SELECT * FROM polls WHERE guild = $1;"); +const addQuery = database.query("INSERT INTO polls (guild, message) VALUES (?1, ?2);"); +const removeQuery = database.query("DELETE FROM polls WHERE guild = $1 AND message = $2"); + +export function getPolls(guildID: string) { + return (getQuery.all(guildID) as TypeOfDefinition[]).map( + val => val.message + ); +} + +export function addPoll(guildID: string, message: string) { + addQuery.run(guildID, message); +} + +export function removePoll(guildID: string, message: string) { + removeQuery.run(guildID, message); +} From 299684e9b98c6bc839422357c7dc5ff939e077b8 Mon Sep 17 00:00:00 2001 From: Serge Date: Sun, 11 Feb 2024 00:51:54 +0600 Subject: [PATCH 036/127] errorEmbed redo (again, part 124) and some improvements --- src/bot.ts | 8 ++-- src/commands/poll.ts | 42 ++++++++----------- src/commands/server/news/{add.ts => send.ts} | 22 +++++----- src/commands/server/news/view.ts | 35 +++++++++++----- src/commands/serverboard.ts | 13 +++--- src/events/guildCreate.ts | 2 +- ...eactionAdd.ts => messageReactionCreate.ts} | 5 +-- src/utils/database/news.ts | 18 ++++---- src/utils/embeds/errorEmbed.ts | 14 +++++-- src/utils/sendChannelNews.ts | 4 +- 10 files changed, 87 insertions(+), 76 deletions(-) rename src/commands/server/news/{add.ts => send.ts} (87%) rename src/events/{messageReactionAdd.ts => messageReactionCreate.ts} (87%) diff --git a/src/bot.ts b/src/bot.ts index f84ed93..fce003b 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -1,4 +1,4 @@ -import { Client, ActivityType } from "discord.js"; +import { Client, ActivityType, Partials } from "discord.js"; import Commands from "./handlers/commands"; import Events from "./handlers/events"; @@ -12,8 +12,10 @@ const client = new Client({ "GuildMessages", "GuildEmojisAndStickers", "GuildPresences", - "MessageContent" - ] + "MessageContent", + "GuildMessageReactions" + ], + partials: [Partials.Message, Partials.Channel, Partials.Reaction] }); client.on("ready", async () => { diff --git a/src/commands/poll.ts b/src/commands/poll.ts index 15f7b19..a8d57dc 100644 --- a/src/commands/poll.ts +++ b/src/commands/poll.ts @@ -56,23 +56,18 @@ export default class Poll { } if (!member.permissions.has(PermissionsBitField.Flags.ManageMessages)) - return await interaction.reply({ - embeds: [ - errorEmbed( - "You can't execute this command.", - "You need the **Manage Messages** permission." - ) - ] - }); + return errorEmbed( + interaction, + "You can't execute this command.", + "You need the **Manage Messages** permission." + ); if (!interaction.guild?.members.me?.permissions.has(PermissionsBitField.Flags.SendMessages)) - return await interaction.reply({ - embeds: [ - errorEmbed( - `Nebula can't execute this command.", "Nebula needs the **Send Messages** permission for ${channel}.` - ) - ] - }); + return errorEmbed( + interaction, + "Nebula can't execute this command.", + `Nebula needs the **Send Messages** permission for ${channel}.` + ); let embed = new EmbedBuilder() .setAuthor({ @@ -91,20 +86,19 @@ export default class Poll { .setColor(genColor(200)); if (image) embed.setImage(image.url); - - const successEmbed = new EmbedBuilder() - .setTitle("✅ • Poll has been created successfully") - .setDescription(`Poll is sent to ${channel ? channel : interaction.channel}`) - .setColor(genColor(100)); - await interaction.reply({ embeds: [successEmbed] }); - if (channel) { - await channel.send({ embeds: [embed] }).then(async message => { + const successEmbed = new EmbedBuilder() + .setTitle("✅ • Poll has been created successfully") + .setDescription(`Poll is sent to ${channel}.`) + .setColor(genColor(100)); + + await interaction.reply({ embeds: [successEmbed] }); + return await channel.send({ embeds: [embed] }).then(async message => { for (const emoji of options.map((_, i) => convertNumEmoji(i))) await message.react(emoji); }); } - return await interaction.channel?.send({ embeds: [embed] }).then(async message => { + await interaction.reply({ embeds: [embed], fetchReply: true }).then(async message => { for (const emoji of options.map((_, i) => convertNumEmoji(i))) await message.react(emoji); addPoll(interaction.guildId!, message.id); }); diff --git a/src/commands/server/news/add.ts b/src/commands/server/news/send.ts similarity index 87% rename from src/commands/server/news/add.ts rename to src/commands/server/news/send.ts index 2f4e0f2..b5d2d34 100644 --- a/src/commands/server/news/add.ts +++ b/src/commands/server/news/send.ts @@ -11,14 +11,15 @@ import { import { genColor } from "../../../utils/colorGen"; import { errorEmbed } from "../../../utils/embeds/errorEmbed"; import { sendChannelNews } from "../../../utils/sendChannelNews"; -import { addNews } from "../../../utils/database/news"; +import { sendNews } from "../../../utils/database/news"; +import { getSetting } from "../../../utils/database/settings"; -export default class Add { +export default class Send { data: SlashCommandSubcommandBuilder; constructor() { this.data = new SlashCommandSubcommandBuilder() - .setName("add") - .setDescription("Adds news to your guild."); + .setName("send") + .setDescription("Send your news."); } async run(interaction: ChatInputCommandInteraction) { @@ -26,12 +27,10 @@ export default class Add { const member = guild.members.cache.get(interaction.user.id)!; if (!member.permissions.has(PermissionsBitField.Flags.ManageGuild)) return await interaction.reply({ - embeds: [errorEmbed("You need **Manage Server** permissions to add news.")] + embeds: [errorEmbed("You need **Manage Server** permissions to send news.")] }); - const newsModal = new ModalBuilder() - .setCustomId("addnews") - .setTitle("Create new News for your server/project"); + const newsModal = new ModalBuilder().setCustomId("sendnews").setTitle("Write your news out."); const titleInput = new TextInputBuilder() .setCustomId("title") @@ -81,7 +80,7 @@ export default class Add { } sendChannelNews(guild, crypto.randomUUID()).catch(err => console.error(err)); - addNews( + sendNews( guild.id, interaction.fields.getTextInputValue("title"), interaction.fields.getTextInputValue("body"), @@ -89,9 +88,10 @@ export default class Add { interaction.user.displayName, interaction.user.avatarURL()!, null!, - null!, - null! + getSetting(guild.id, "news.channelID")!, + getSetting(guild.id, "news.roleID")! ); + await interaction.reply({ embeds: [new EmbedBuilder().setTitle("✅ • News sent!").setColor(genColor(100))] }); diff --git a/src/commands/server/news/view.ts b/src/commands/server/news/view.ts index 8e06498..8b79a27 100644 --- a/src/commands/server/news/view.ts +++ b/src/commands/server/news/view.ts @@ -28,14 +28,12 @@ export default class View { let currentNews = sortedNews[page - 1]; if (!news || !sortedNews || sortedNews.length == 0) - return await interaction.reply({ - embeds: [ - errorEmbed( - "No news found.", - "Admins can add news with the **/server news add** command." - ) - ] - }); + return errorEmbed( + interaction, + "No news found.", + "Admins can add news with the **/server news add** command." + ); + if (page > sortedNews.length) page = sortedNews.length; if (page < 1) page = 1; @@ -67,6 +65,15 @@ export default class View { }) .on("collect", async i => { if (!i.isButton()) return; + if (i.user.id !== interaction.user.id) { + errorEmbed( + interaction, + "No.", + "You have not sent this command. Type **/server news view** to view news yourself." + ); + return; + } + if (i.customId === "left") { page--; if (page < 1) page = sortedNews.length; @@ -75,10 +82,18 @@ export default class View { if (page > sortedNews.length) page = 1; } - currentNews = currentNews; - embed = embed; + currentNews = sortedNews[page - 1]; + embed = new EmbedBuilder() + .setAuthor({ name: currentNews.author, iconURL: currentNews.authorPFP }) + .setTitle(currentNews.title) + .setDescription(currentNews.body) + .setImage(currentNews.imageURL || null) + .setTimestamp(parseInt(currentNews.updatedAt)) + .setFooter({ text: `Page ${page} of ${sortedNews.length} • ID: ${currentNews.id}` }) + .setColor(genColor(200)); await interaction.editReply({ embeds: [embed], components: [row] }); + await i.deferUpdate(); }); } } diff --git a/src/commands/serverboard.ts b/src/commands/serverboard.ts index 650dcdb..ba06a98 100644 --- a/src/commands/serverboard.ts +++ b/src/commands/serverboard.ts @@ -29,14 +29,11 @@ export default class Serverboard { const pages = guildList.length; if (pages == 0) - return interaction.reply({ - embeds: [ - errorEmbed( - "No public server found", - "By some magical miracle, all the servers using Nebula turned off their visibility. Use /server settings key:`serverboard.shown` value:`TRUE` to make your server publicly visible." - ) - ] - }); + return errorEmbed( + interaction, + "No public server found", + "By some magical miracle, all the servers using Nebula turned off their visibility. Use /server settings key:`serverboard.shown` value:`TRUE` to make your server publicly visible." + ); const argPage = interaction.options.get("page")?.value as number; let page = (argPage - 1 <= 0 ? 0 : argPage - 1 > pages ? pages - 1 : argPage - 1) || 0; diff --git a/src/events/guildCreate.ts b/src/events/guildCreate.ts index f7ccb41..f950b0d 100644 --- a/src/events/guildCreate.ts +++ b/src/events/guildCreate.ts @@ -21,7 +21,7 @@ export default { .setDescription( [ "Nebula is a multiplatform, multipurpose bot with the ability to add extensions to have additional features.", - "To manage the bot, use the /server settings command.", + "To manage the bot, use the **/server settings** command.", "Nebula is in an early stage of development. If you find bugs, please go to our [official server](https://discord.gg/c6C25P4BuY) and report them." ].join("\n\n") ) diff --git a/src/events/messageReactionAdd.ts b/src/events/messageReactionCreate.ts similarity index 87% rename from src/events/messageReactionAdd.ts rename to src/events/messageReactionCreate.ts index f76a241..58b60c1 100644 --- a/src/events/messageReactionAdd.ts +++ b/src/events/messageReactionCreate.ts @@ -2,8 +2,8 @@ import { type Client, type Guild, type User, type MessageReaction } from "discor import { getPolls } from "../utils/database/poll"; export default { - name: "messageReactionAdd", - event: class MessageReactionAdd { + name: "messageReactionCreate", + event: class MessageReactionCreate { client: Client; constructor(client: Client) { this.client = client; @@ -12,7 +12,6 @@ export default { async run(guild: Guild, user: User, reaction: MessageReaction) { if (user.bot) return; - // Poll reaction handler const messageIds = getPolls(guild.id); if (!messageIds.includes(reaction.message.id)) return; const userReactions = reaction.message.reactions.cache.filter(reaction => diff --git a/src/utils/database/news.ts b/src/utils/database/news.ts index a4c6036..b71e873 100644 --- a/src/utils/database/news.ts +++ b/src/utils/database/news.ts @@ -4,7 +4,7 @@ import { TableDefinition, TypeOfDefinition } from "./types"; const definition = { name: "news", definition: { - guild: "TEXT", + guildID: "TEXT", title: "TEXT", body: "TEXT", imageURL: "TEXT", @@ -20,14 +20,14 @@ const definition = { } satisfies TableDefinition; const database = getDatabase(definition); -const addQuery = database.query( - "INSERT INTO news (guild, title, body, imageURL, author, authorPFP, createdAt, updatedAt, messageID, channelID, roleID, id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12);" +const sendQuery = database.query( + "INSERT INTO news (guildID, title, body, imageURL, author, authorPFP, createdAt, updatedAt, messageID, channelID, roleID, id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12);" ); -const listAllQuery = database.query("SELECT * FROM news WHERE guild = $1;"); +const listAllQuery = database.query("SELECT * FROM news WHERE guildID = $1;"); const getIdQuery = database.query("SELECT * FROM news WHERE id = $1;"); const deleteQuery = database.query("DELETE FROM news WHERE id = $1"); -export function addNews( +export function sendNews( guildID: string, title: string, body: string, @@ -38,7 +38,7 @@ export function addNews( channelID: string, roleID: string ) { - addQuery.run( + sendQuery.run( guildID, title, body, @@ -63,10 +63,10 @@ export function get(id: string) { } export function updateNews(id: string, title: string, body: string, imageURL: string | null) { - const lastElem = deleteQuery.get(id) as TypeOfDefinition; + const lastElem = get(id)!; deleteQuery.run(id); - addQuery.run( - lastElem.guild, + sendQuery.run( + lastElem.guildID, title, body, imageURL, diff --git a/src/utils/embeds/errorEmbed.ts b/src/utils/embeds/errorEmbed.ts index 488acc4..2bf2616 100644 --- a/src/utils/embeds/errorEmbed.ts +++ b/src/utils/embeds/errorEmbed.ts @@ -1,18 +1,24 @@ -import { EmbedBuilder } from "discord.js"; +import { EmbedBuilder, type ChatInputCommandInteraction } from "discord.js"; import { genColor } from "../colorGen"; /** * Sends the embed containing an error. + * @param interaction The interaction (slash command). * @param title The error. * @param reason The reason of the error. * @returns Embed with the error description. */ -export function errorEmbed(title: string, reason?: string) { +export function errorEmbed( + interaction: ChatInputCommandInteraction, + title: string, + reason?: string +) { const content = [`**${title}**`]; if (reason != undefined) content.push(reason); - - return new EmbedBuilder() + const embed = new EmbedBuilder() .setTitle("❌ • Something went wrong!") .setDescription(content.join("\n")) .setColor(genColor(0)); + + return interaction.reply({ embeds: [embed], ephemeral: true }); } diff --git a/src/utils/sendChannelNews.ts b/src/utils/sendChannelNews.ts index e595dc1..a012b35 100644 --- a/src/utils/sendChannelNews.ts +++ b/src/utils/sendChannelNews.ts @@ -22,10 +22,8 @@ export async function sendChannelNews(guild: Guild, id: string) { .setFooter({ text: `Latest news from ${guild.name}` }) .setColor(genColor(200)); - const message = await channelToSend.send({ + return await channelToSend.send({ embeds: [embed], content: roleToSend ? `<@&${roleToSend.id}>` : undefined }); - await newsTable.set(`${guild.id}.news.${id}.messageId`, message.id); - return; } From b32f4b0aad16fb696c1fe63c59f7871fccf8532b Mon Sep 17 00:00:00 2001 From: Serge Date: Sun, 11 Feb 2024 14:08:48 +0600 Subject: [PATCH 037/127] finished changing errorEmbed --- src/commands/moderation/ban.ts | 38 +++++++++++------------ src/commands/moderation/delwarn.ts | 49 +++++++++++------------------- src/commands/moderation/kick.ts | 41 +++++++++++-------------- src/commands/moderation/mute.ts | 49 ++++++++++++++---------------- src/commands/moderation/purge.ts | 22 +++++--------- src/commands/moderation/unban.ts | 14 ++++----- src/commands/moderation/unmute.ts | 14 ++++----- src/commands/moderation/warn.ts | 44 +++++++++++---------------- src/commands/moderation/warns.ts | 13 +++----- src/commands/server/news/edit.ts | 28 ++++++++--------- src/commands/server/news/remove.ts | 13 ++++---- src/commands/server/news/send.ts | 26 ++++++++-------- 12 files changed, 146 insertions(+), 205 deletions(-) diff --git a/src/commands/moderation/ban.ts b/src/commands/moderation/ban.ts index 8e1aa5e..4447562 100644 --- a/src/commands/moderation/ban.ts +++ b/src/commands/moderation/ban.ts @@ -35,33 +35,29 @@ export default class Ban { const name = user.displayName; if (!member.permissions.has(PermissionsBitField.Flags.BanMembers)) - return await interaction.reply({ - embeds: [ - errorEmbed("You can't execute this command.", "You need the **Ban Members** permission.") - ] - }); - - if (target === member) - return await interaction.reply({ embeds: [errorEmbed("You can't ban yourself.")] }); + return errorEmbed( + interaction, + "You can't execute this command.", + "You need the **Ban Members** permission." + ); + if (target === member) return errorEmbed(interaction, "You can't ban yourself."); if (target.user.id === interaction.client.user.id) - return await interaction.reply({ - embeds: [errorEmbed("You can't ban Nebula.")] - }); + return errorEmbed(interaction, "You can't ban Nebula."); if (!target.manageable) - return await interaction.reply({ - embeds: [ - errorEmbed(`You can't ban ${name}.`, "The member has a higher role position than Nebula.") - ] - }); + return errorEmbed( + interaction, + `You can't ban ${name}.`, + "The member has a higher role position than Nebula." + ); if (member.roles.highest.position < target.roles.highest.position) - return await interaction.reply({ - embeds: [ - errorEmbed(`You can't ban ${name}.`, "The member has a higher role position than you.") - ] - }); + return errorEmbed( + interaction, + `You can't ban ${name}.`, + "The member has a higher role position than you." + ); const reason = interaction.options.getString("reason"); const embed = new EmbedBuilder() diff --git a/src/commands/moderation/delwarn.ts b/src/commands/moderation/delwarn.ts index d1f95ff..0121369 100644 --- a/src/commands/moderation/delwarn.ts +++ b/src/commands/moderation/delwarn.ts @@ -42,44 +42,29 @@ export default class Delwarn { const newWarns = warns.filter(warn => warn.id !== `${id}`); if (!member.permissions.has(PermissionsBitField.Flags.ModerateMembers)) - return await interaction.reply({ - embeds: [ - errorEmbed( - "You can't execute this command.", - "You need the **Moderate Members** permission." - ) - ] - }); - - if (target === member) - return await interaction.reply({ - embeds: [errorEmbed("You can't remove a warn from yourself.")] - }); + return errorEmbed( + interaction, + "You can't execute this command.", + "You need the **Moderate Members** permission." + ); + if (target === member) return errorEmbed(interaction, "You can't remove a warn from yourself."); if (newWarns.length === warns.length) - return await interaction.reply({ - embeds: [errorEmbed(`There is no warn with the id of ${id}.`)] - }); + return errorEmbed(interaction, `There is no warn with the id of ${id}.`); if (!target.manageable) - return await interaction.reply({ - embeds: [ - errorEmbed( - `You can't delete a warn from ${name}.`, - "The member has a higher role position than Nebula." - ) - ] - }); + return errorEmbed( + interaction, + `You can't delete a warn from ${name}.`, + "The member has a higher role position than Nebula." + ); if (member.roles.highest.position < target.roles.highest.position) - return await interaction.reply({ - embeds: [ - errorEmbed( - `You can't delete a warn from ${name}`, - "The member has a higher role position than you." - ) - ] - }); + return errorEmbed( + interaction, + `You can't delete a warn from ${name}`, + "The member has a higher role position than you." + ); const embed = new EmbedBuilder() .setAuthor({ name: `• ${user.username}`, iconURL: user.displayAvatarURL() }) diff --git a/src/commands/moderation/kick.ts b/src/commands/moderation/kick.ts index 8539e27..e6a9b5b 100644 --- a/src/commands/moderation/kick.ts +++ b/src/commands/moderation/kick.ts @@ -35,36 +35,29 @@ export default class Kick { const name = target.nickname ?? user.username; if (!member.permissions.has(PermissionsBitField.Flags.KickMembers)) - return await interaction.reply({ - embeds: [ - errorEmbed("You can't execute this command.", "You need the **Kick Members** permission.") - ] - }); - - if (target === member) - return await interaction.reply({ embeds: [errorEmbed("You can't kick yourself.")] }); + return errorEmbed( + interaction, + "You can't execute this command.", + "You need the **Kick Members** permission." + ); + if (target === member) return errorEmbed(interaction, "You can't kick yourself."); if (target.user.id === interaction.client.user.id) - return await interaction.reply({ - embeds: [errorEmbed("You can't kick Nebula.")] - }); + return errorEmbed(interaction, "You can't kick Nebula."); if (!target.manageable) - return await interaction.reply({ - embeds: [ - errorEmbed( - `You can't kick ${name}.`, - "The member has a higher role position than Nebula." - ) - ] - }); + return errorEmbed( + interaction, + `You can't kick ${name}.`, + "The member has a higher role position than Nebula." + ); if (member.roles.highest.position < target.roles.highest.position) - return await interaction.reply({ - embeds: [ - errorEmbed(`You can't kick ${name}.`, "The member has a higher role position than you.") - ] - }); + return errorEmbed( + interaction, + `You can't kick ${name}.`, + "The member has a higher role position than you." + ); const reason = interaction.options.getString("reason"); const embed = new EmbedBuilder() diff --git a/src/commands/moderation/mute.ts b/src/commands/moderation/mute.ts index f828d73..4521d92 100644 --- a/src/commands/moderation/mute.ts +++ b/src/commands/moderation/mute.ts @@ -43,41 +43,36 @@ export default class Mute { const name = target.nickname ?? user.username; if (!member.permissions.has(PermissionsBitField.Flags.MuteMembers)) - return await interaction.reply({ - embeds: [ - errorEmbed("You can't execute this command.", "You need the **Mute Members** permission.") - ] - }); - - if (target === member) - return await interaction.reply({ embeds: [errorEmbed("You can't mute yourself.")] }); + return errorEmbed( + interaction, + "You can't execute this command.", + "You need the **Mute Members** permission." + ); + if (target === member) return errorEmbed(interaction, "You can't mute yourself."); if (target.user.id === interaction.client.user.id) - return await interaction.reply({ - embeds: [errorEmbed("You can't mute Nebula.")] - }); + return errorEmbed(interaction, "You can't mute Nebula."); if (!target.manageable) - return await interaction.reply({ - embeds: [ - errorEmbed( - `You can't mute ${name}.`, - "The member has a higher role position than Nebula." - ) - ] - }); + return errorEmbed( + interaction, + `You can't mute ${name}.`, + "The member has a higher role position than Nebula." + ); if (member.roles.highest.position < target.roles.highest.position) - return await interaction.reply({ - embeds: [ - errorEmbed(`You can't mute ${name}.`, "The member has a higher role position than you.") - ] - }); + return errorEmbed( + interaction, + `You can't mute ${name}.`, + "The member has a higher role position than you." + ); if (!ms(duration) || ms(duration) > ms("28d")) - return await interaction.reply({ - embeds: [errorEmbed("The duration is invalid or is above the 28 day limit.")] - }); + return errorEmbed( + interaction, + `You can't mute ${name}`, + "The duration is invalid or is above the 28 day limit." + ); const time = new Date( Date.parse(new Date().toISOString()) + Date.parse(new Date(ms(duration)).toISOString()) diff --git a/src/commands/moderation/purge.ts b/src/commands/moderation/purge.ts index 21e76e8..821194b 100644 --- a/src/commands/moderation/purge.ts +++ b/src/commands/moderation/purge.ts @@ -41,24 +41,16 @@ export default class Purge { const member = guild.members.cache.get(interaction.member?.user.id!)!; if (!member.permissions.has(PermissionsBitField.Flags.ManageMessages)) - return await interaction.reply({ - embeds: [ - errorEmbed( - "You can't execute this command", - "You need the **Manage Messages** permission." - ) - ] - }); + return errorEmbed( + interaction, + "You can't execute this command", + "You need the **Manage Messages** permission." + ); if (amount > 100) - return await interaction.reply({ - embeds: [errorEmbed("You can only purge up to 100 messages at a time.")] - }); + return errorEmbed(interaction, "You can only purge up to 100 messages at a time."); - if (amount < 1) - return await interaction.reply({ - embeds: [errorEmbed("You must purge at least 1 message.")] - }); + if (amount < 1) return errorEmbed(interaction, "You must purge at least 1 message."); const channelOption = interaction.options.getChannel("channel")!; const channel = guild.channels.cache.get(interaction.channel?.id ?? channelOption.id)!; diff --git a/src/commands/moderation/unban.ts b/src/commands/moderation/unban.ts index e8dd9b3..8b09064 100644 --- a/src/commands/moderation/unban.ts +++ b/src/commands/moderation/unban.ts @@ -33,16 +33,14 @@ export default class Unban { const target = guild.bans.cache.map(user => user.user).filter(user => user.id === id)[0]!; if (!member.permissions.has(PermissionsBitField.Flags.BanMembers)) - return await interaction.reply({ - embeds: [ - errorEmbed("You can't execute this command.", "You need the **Ban Members** permission.") - ] - }); + return errorEmbed( + interaction, + "You can't execute this command.", + "You need the **Ban Members** permission." + ); if (target == undefined) - return await interaction.reply({ - embeds: [errorEmbed("You can't unban this user.", "The user was never banned.")] - }); + return errorEmbed(interaction, "You can't unban this user.", "The user was never banned."); const embed = new EmbedBuilder() .setAuthor({ name: target.username, iconURL: target.displayAvatarURL() }) diff --git a/src/commands/moderation/unmute.ts b/src/commands/moderation/unmute.ts index 31c6964..e67600b 100644 --- a/src/commands/moderation/unmute.ts +++ b/src/commands/moderation/unmute.ts @@ -31,17 +31,15 @@ export default class Unmute { .get(interaction.member!.user.id)! .permissions.has(PermissionsBitField.Flags.MuteMembers) ) - return await interaction.reply({ - embeds: [ - errorEmbed("You can't execute this command.", "You need the **Mute Members** permission.") - ] - }); + return errorEmbed( + interaction, + "You can't execute this command.", + "You need the **Mute Members** permission." + ); const user = interaction.options.getUser("user")!; if (members.get(user.id)?.communicationDisabledUntil === null) - return await interaction.reply({ - embeds: [errorEmbed("You can't unmute this user.", "The user was never muted.")] - }); + return errorEmbed(interaction, "You can't unmute this user.", "The user was never muted."); const embed = new EmbedBuilder() .setAuthor({ name: `• ${user.username}`, iconURL: user.displayAvatarURL() }) diff --git a/src/commands/moderation/warn.ts b/src/commands/moderation/warn.ts index c51f9ff..8521401 100644 --- a/src/commands/moderation/warn.ts +++ b/src/commands/moderation/warn.ts @@ -36,39 +36,29 @@ export default class Warn { const name = target.nickname ?? user.username; if (!member.permissions.has(PermissionsBitField.Flags.ModerateMembers)) - return await interaction.reply({ - embeds: [ - errorEmbed( - "You can't execute this command.", - "You need the **Moderate Members** permission." - ) - ] - }); - - if (target === member) - return await interaction.reply({ embeds: [errorEmbed("You can't warn yourself.")] }); + return errorEmbed( + interaction, + "You can't execute this command.", + "You need the **Moderate Members** permission." + ); + if (target === member) return errorEmbed(interaction, "You can't warn yourself."); if (target.user.id === interaction.client.user.id) - return await interaction.reply({ - embeds: [errorEmbed("You can't warn Nebula.")] - }); + return errorEmbed(interaction, "You can't warn Nebula."); if (!target.manageable) - return await interaction.reply({ - embeds: [ - errorEmbed( - `You can't warn ${name}.`, - "The member has a higher role position than Nebula." - ) - ] - }); + return errorEmbed( + interaction, + `You can't warn ${name}.`, + "The member has a higher role position than Nebula." + ); if (member.roles.highest.position < target.roles.highest.position) - return await interaction.reply({ - embeds: [ - errorEmbed(`You can't warn ${name}.`, "The member has a higher role position than you.") - ] - }); + return errorEmbed( + interaction, + `You can't warn ${name}.`, + "The member has a higher role position than you." + ); const reason = interaction.options.getString("reason"); const embed = new EmbedBuilder() diff --git a/src/commands/moderation/warns.ts b/src/commands/moderation/warns.ts index f4ae9c8..33db3dd 100644 --- a/src/commands/moderation/warns.ts +++ b/src/commands/moderation/warns.ts @@ -26,14 +26,11 @@ export default class Warns { .get(interaction.member?.user.id!) ?.permissions.has(PermissionsBitField.Flags.ModerateMembers) ) - return await interaction.reply({ - embeds: [ - errorEmbed( - "You can't execute this command.", - "You need the **Moderate Members** permission." - ) - ] - }); + return errorEmbed( + interaction, + "You can't execute this command.", + "You need the **Moderate Members** permission." + ); const user = interaction.options.getUser("user")!; const warns = listUserModeration(guild.id, user.id, "WARN"); diff --git a/src/commands/server/news/edit.ts b/src/commands/server/news/edit.ts index 0c72a48..604f57e 100644 --- a/src/commands/server/news/edit.ts +++ b/src/commands/server/news/edit.ts @@ -32,16 +32,15 @@ export default class Edit { .guild!.members.cache.get(interaction.user.id)! .permissions.has(PermissionsBitField.Flags.ManageGuild) ) - return await interaction.reply({ - embeds: [errorEmbed("You need **Manage Server** permissions to add news.")] - }); + return errorEmbed( + interaction, + "You can't execute this command.", + "You need the **Manage Server** permission." + ); const id = interaction.options.getString("id", true).trim(); const news = get(id); - if (!news) - return await interaction.reply({ - embeds: [errorEmbed("The specified news doesn't exist.")] - }); + if (!news) return errorEmbed(interaction, "The specified news doesn't exist."); const editModal = new ModalBuilder() .setCustomId("editnews") @@ -87,23 +86,22 @@ export default class Edit { editModal.addComponents(firstActionRow, secondActionRow, thirdActionRow); await interaction.showModal(editModal).catch(err => console.error(err)); - interaction.client.once("interactionCreate", async interaction => { - if (!interaction.isModalSubmit()) return; + interaction.client.once("interactionCreate", async i => { + if (!i.isModalSubmit()) return; - const imageURL = interaction.fields.getTextInputValue("imageurl"); + const imageURL = i.fields.getTextInputValue("imageurl"); if (imageURL) { - await interaction.reply({ - embeds: [errorEmbed("The image URL you provided is invalid.")] - }); + errorEmbed(interaction, "The image URL you provided is invalid."); return; } updateNews( id, - interaction.fields.getTextInputValue("title"), - interaction.fields.getTextInputValue("body"), + i.fields.getTextInputValue("title"), + i.fields.getTextInputValue("body"), imageURL ); + await interaction.reply({ embeds: [new EmbedBuilder().setTitle("✅ • News edited!").setColor(genColor(100))] }); diff --git a/src/commands/server/news/remove.ts b/src/commands/server/news/remove.ts index 00ce8bf..c6ccabb 100644 --- a/src/commands/server/news/remove.ts +++ b/src/commands/server/news/remove.ts @@ -30,15 +30,14 @@ export default class Remove { const member = guild.members.cache.get(user.id)!; if (!member.permissions.has(PermissionsBitField.Flags.ManageGuild)) - return await interaction.reply({ - embeds: [errorEmbed("You need **Manage Server** permissions to delete news.")] - }); + return errorEmbed( + interaction, + "You can't execute this command.", + "You need the **Manage Server** permission." + ); const news = get(id); - if (!news) - return await interaction.reply({ - embeds: [errorEmbed("The specified news don't exist.")] - }); + if (!news) return errorEmbed(interaction, "The specified news don't exist."); const messageId = news?.messageID; const newsChannel = (await guild.channels diff --git a/src/commands/server/news/send.ts b/src/commands/server/news/send.ts index b5d2d34..a6c5ea5 100644 --- a/src/commands/server/news/send.ts +++ b/src/commands/server/news/send.ts @@ -26,9 +26,11 @@ export default class Send { const guild = interaction.guild!; const member = guild.members.cache.get(interaction.user.id)!; if (!member.permissions.has(PermissionsBitField.Flags.ManageGuild)) - return await interaction.reply({ - embeds: [errorEmbed("You need **Manage Server** permissions to send news.")] - }); + return errorEmbed( + interaction, + "You can't execute this command.", + "You need the **Manage Server** permission." + ); const newsModal = new ModalBuilder().setCustomId("sendnews").setTitle("Write your news out."); @@ -68,25 +70,23 @@ export default class Send { newsModal.addComponents(firstActionRow, secondActionRow, thirdActionRow); await interaction.showModal(newsModal).catch(err => console.error(err)); - interaction.client.once("interactionCreate", async interaction => { - if (!interaction.isModalSubmit()) return; + interaction.client.once("interactionCreate", async i => { + if (!i.isModalSubmit()) return; - const imageURL = interaction.fields.getTextInputValue("imageurl"); + const imageURL = i.fields.getTextInputValue("imageurl"); if (imageURL) { - await interaction.reply({ - embeds: [errorEmbed("The image URL you provided is invalid.")] - }); + errorEmbed(interaction, "The image URL you provided is invalid."); return; } sendChannelNews(guild, crypto.randomUUID()).catch(err => console.error(err)); sendNews( guild.id, - interaction.fields.getTextInputValue("title"), - interaction.fields.getTextInputValue("body"), + i.fields.getTextInputValue("title"), + i.fields.getTextInputValue("body"), imageURL!, - interaction.user.displayName, - interaction.user.avatarURL()!, + i.user.displayName, + i.user.avatarURL()!, null!, getSetting(guild.id, "news.channelID")!, getSetting(guild.id, "news.roleID")! From b42ece0793bea46406ef51b1b0a2cca5f0ca89a9 Mon Sep 17 00:00:00 2001 From: Serge Date: Sun, 11 Feb 2024 22:20:15 +0600 Subject: [PATCH 038/127] made /server news send actually send news, more error handling --- src/commands/moderation/ban.ts | 12 ++++--- src/commands/moderation/delwarn.ts | 8 +++-- src/commands/server/news/send.ts | 57 ++++++++++++++---------------- src/utils/database/news.ts | 5 +-- src/utils/sendChannelNews.ts | 4 +-- 5 files changed, 43 insertions(+), 43 deletions(-) diff --git a/src/commands/moderation/ban.ts b/src/commands/moderation/ban.ts index 4447562..35d045d 100644 --- a/src/commands/moderation/ban.ts +++ b/src/commands/moderation/ban.ts @@ -88,11 +88,13 @@ export default class Ban { } await target.ban({ reason: reason ?? undefined }); - const dmChannel = (await user.createDM().catch(() => null)) as DMChannel | null; - if (dmChannel) - await dmChannel.send({ - embeds: [embed.setTitle("🔨 • You were banned").setColor(genColor(0))] - }); await interaction.reply({ embeds: [embed] }); + + const dmChannel = (await user.createDM().catch(() => null)) as DMChannel | null; + if (!dmChannel) return; + if (target.user.bot) return; + await dmChannel.send({ + embeds: [embed.setTitle("🔨 • You were banned").setColor(genColor(0))] + }); } } diff --git a/src/commands/moderation/delwarn.ts b/src/commands/moderation/delwarn.ts index 0121369..eb80c46 100644 --- a/src/commands/moderation/delwarn.ts +++ b/src/commands/moderation/delwarn.ts @@ -89,9 +89,11 @@ export default class Delwarn { } removeModeration(guild.id, `${id}`); - const dmChannel = (await user.createDM().catch(() => null)) as DMChannel | null; - if (dmChannel) - await dmChannel.send({ embeds: [embed.setTitle("🤝 • Your warning was removed")] }); await interaction.reply({ embeds: [embed] }); + + const dmChannel = (await user.createDM().catch(() => null)) as DMChannel | null; + if (!dmChannel) return; + if (target.user.bot) return; + await dmChannel.send({ embeds: [embed.setTitle("🤝 • Your warning was removed")] }); } } diff --git a/src/commands/server/news/send.ts b/src/commands/server/news/send.ts index a6c5ea5..a9786d5 100644 --- a/src/commands/server/news/send.ts +++ b/src/commands/server/news/send.ts @@ -33,39 +33,32 @@ export default class Send { ); const newsModal = new ModalBuilder().setCustomId("sendnews").setTitle("Write your news out."); - - const titleInput = new TextInputBuilder() - .setCustomId("title") - .setPlaceholder("Write a title") - .setStyle(TextInputStyle.Short) - .setMaxLength(100) - .setLabel("Title") - .setRequired(true); - - const bodyInput = new TextInputBuilder() - .setCustomId("body") - .setPlaceholder("Insert your content here") - .setMaxLength(4000) - .setStyle(TextInputStyle.Paragraph) - .setLabel("Content (supports Markdown)") - .setRequired(true); - - const imageURLInput = new TextInputBuilder() - .setCustomId("imageurl") - .setPlaceholder("Place a link to your image") - .setStyle(TextInputStyle.Short) - .setMaxLength(1000) - .setLabel("Image URL (placed at the bottom)") - .setRequired(false); - const firstActionRow = new ActionRowBuilder().addComponents( - titleInput + new TextInputBuilder() + .setCustomId("title") + .setPlaceholder("Write a title") + .setStyle(TextInputStyle.Short) + .setMaxLength(100) + .setLabel("Title") ) as ActionRowBuilder; + const secondActionRow = new ActionRowBuilder().addComponents( - bodyInput + new TextInputBuilder() + .setCustomId("body") + .setPlaceholder("Insert your content here") + .setMaxLength(4000) + .setStyle(TextInputStyle.Paragraph) + .setLabel("Content (supports Markdown)") ) as ActionRowBuilder; + const thirdActionRow = new ActionRowBuilder().addComponents( - imageURLInput + new TextInputBuilder() + .setCustomId("imageurl") + .setPlaceholder("Place a link to your image") + .setStyle(TextInputStyle.Short) + .setMaxLength(1000) + .setLabel("Image URL (placed at the bottom)") + .setRequired(false) ) as ActionRowBuilder; newsModal.addComponents(firstActionRow, secondActionRow, thirdActionRow); @@ -79,7 +72,7 @@ export default class Send { return; } - sendChannelNews(guild, crypto.randomUUID()).catch(err => console.error(err)); + const id = crypto.randomUUID(); sendNews( guild.id, i.fields.getTextInputValue("title"), @@ -89,10 +82,12 @@ export default class Send { i.user.avatarURL()!, null!, getSetting(guild.id, "news.channelID")!, - getSetting(guild.id, "news.roleID")! + getSetting(guild.id, "news.roleID")!, + id ); + sendChannelNews(guild, id).catch(err => console.error(err)); - await interaction.reply({ + await i.reply({ embeds: [new EmbedBuilder().setTitle("✅ • News sent!").setColor(genColor(100))] }); }); diff --git a/src/utils/database/news.ts b/src/utils/database/news.ts index b71e873..45204a0 100644 --- a/src/utils/database/news.ts +++ b/src/utils/database/news.ts @@ -36,7 +36,8 @@ export function sendNews( authorPFP: string, messageID: string, channelID: string, - roleID: string + roleID: string, + id: string ) { sendQuery.run( guildID, @@ -50,7 +51,7 @@ export function sendNews( messageID, channelID, roleID, - crypto.randomUUID() + id ); } diff --git a/src/utils/sendChannelNews.ts b/src/utils/sendChannelNews.ts index a012b35..5c38da9 100644 --- a/src/utils/sendChannelNews.ts +++ b/src/utils/sendChannelNews.ts @@ -4,6 +4,7 @@ import { get } from "./database/news"; export async function sendChannelNews(guild: Guild, id: string) { const news = get(id); + console.log(news); if (!news) return; const channelToSend = guild.channels.cache.get(news.channelID) as TextChannel; @@ -17,8 +18,7 @@ export async function sendChannelNews(guild: Guild, id: string) { .setAuthor({ name: news.author, iconURL: news.authorPFP }) .setTitle(news.title) .setDescription(news.body) - .setImage(news.imageURL) - .setTimestamp(parseInt(news.updatedAt.toString())) + .setTimestamp(parseInt(news.updatedAt.toString()) ?? null) .setFooter({ text: `Latest news from ${guild.name}` }) .setColor(genColor(200)); From b2205acde91520850a8cfdccea93ad99a0a78f80 Mon Sep 17 00:00:00 2001 From: Serge Date: Mon, 12 Feb 2024 22:18:29 +0600 Subject: [PATCH 039/127] finished fixing that one bug --- src/commands/moderation/ban.ts | 2 +- src/commands/moderation/delwarn.ts | 2 +- src/commands/moderation/kick.ts | 12 +++++++----- src/commands/moderation/mute.ts | 12 +++++++----- src/commands/moderation/unban.ts | 7 +++++-- src/commands/moderation/unmute.ts | 7 +++++-- src/commands/moderation/warn.ts | 12 +++++++----- src/commands/server/news/send.ts | 2 +- src/utils/sendChannelNews.ts | 1 + 9 files changed, 35 insertions(+), 22 deletions(-) diff --git a/src/commands/moderation/ban.ts b/src/commands/moderation/ban.ts index 35d045d..b48249e 100644 --- a/src/commands/moderation/ban.ts +++ b/src/commands/moderation/ban.ts @@ -92,7 +92,7 @@ export default class Ban { const dmChannel = (await user.createDM().catch(() => null)) as DMChannel | null; if (!dmChannel) return; - if (target.user.bot) return; + if (user.bot) return; await dmChannel.send({ embeds: [embed.setTitle("🔨 • You were banned").setColor(genColor(0))] }); diff --git a/src/commands/moderation/delwarn.ts b/src/commands/moderation/delwarn.ts index eb80c46..e36cdb8 100644 --- a/src/commands/moderation/delwarn.ts +++ b/src/commands/moderation/delwarn.ts @@ -93,7 +93,7 @@ export default class Delwarn { const dmChannel = (await user.createDM().catch(() => null)) as DMChannel | null; if (!dmChannel) return; - if (target.user.bot) return; + if (user.bot) return; await dmChannel.send({ embeds: [embed.setTitle("🤝 • Your warning was removed")] }); } } diff --git a/src/commands/moderation/kick.ts b/src/commands/moderation/kick.ts index e6a9b5b..26ac0e6 100644 --- a/src/commands/moderation/kick.ts +++ b/src/commands/moderation/kick.ts @@ -88,11 +88,13 @@ export default class Kick { } await target.kick(reason ?? undefined); - const dmChannel = (await user.createDM().catch(() => null)) as DMChannel | null; - if (dmChannel) - await dmChannel.send({ - embeds: [embed.setTitle("🥾 • You were kicked").setColor(genColor(0))] - }); await interaction.reply({ embeds: [embed] }); + + const dmChannel = (await user.createDM().catch(() => null)) as DMChannel | null; + if (!dmChannel) return; + if (user.bot) return; + await dmChannel.send({ + embeds: [embed.setTitle("🥾 • You were kicked").setColor(genColor(0))] + }); } } diff --git a/src/commands/moderation/mute.ts b/src/commands/moderation/mute.ts index 4521d92..37081e9 100644 --- a/src/commands/moderation/mute.ts +++ b/src/commands/moderation/mute.ts @@ -106,11 +106,13 @@ export default class Mute { } await target.edit({ communicationDisabledUntil: time }); - const dmChannel = (await user.createDM().catch(() => null)) as DMChannel | null; - if (dmChannel) - await dmChannel.send({ - embeds: [embed.setTitle("🤐 • You were muted").setColor(genColor(0))] - }); await interaction.reply({ embeds: [embed] }); + + const dmChannel = (await user.createDM().catch(() => null)) as DMChannel | null; + if (!dmChannel) return; + if (user.bot) return; + await dmChannel.send({ + embeds: [embed.setTitle("🤐 • You were muted").setColor(genColor(0))] + }); } } diff --git a/src/commands/moderation/unban.ts b/src/commands/moderation/unban.ts index 8b09064..d39b39d 100644 --- a/src/commands/moderation/unban.ts +++ b/src/commands/moderation/unban.ts @@ -65,8 +65,11 @@ export default class Unban { } await guild.members.unban(id); - const dmChannel = (await target.createDM().catch(() => null)) as DMChannel | null; - if (dmChannel) await dmChannel.send({ embeds: [embed.setTitle("🤝 • You were unbanned")] }); await interaction.reply({ embeds: [embed] }); + + const dmChannel = (await target.createDM().catch(() => null)) as DMChannel | null; + if (!dmChannel) return; + if (target.bot) return; + await dmChannel.send({ embeds: [embed.setTitle("🤝 • You were unbanned")] }); } } diff --git a/src/commands/moderation/unmute.ts b/src/commands/moderation/unmute.ts index e67600b..ab96e61 100644 --- a/src/commands/moderation/unmute.ts +++ b/src/commands/moderation/unmute.ts @@ -69,8 +69,11 @@ export default class Unmute { } await members.get(user.id)!.edit({ communicationDisabledUntil: null }); - const dmChannel = (await user.createDM().catch(() => null)) as DMChannel | null; - if (dmChannel) await dmChannel.send({ embeds: [embed.setTitle("🤝 • You were unmuted")] }); await interaction.reply({ embeds: [embed] }); + + const dmChannel = (await user.createDM().catch(() => null)) as DMChannel | null; + if (!dmChannel) return; + if (user.bot) return; + await dmChannel.send({ embeds: [embed.setTitle("🤝 • You were unmuted")] }); } } diff --git a/src/commands/moderation/warn.ts b/src/commands/moderation/warn.ts index 8521401..d69bb67 100644 --- a/src/commands/moderation/warn.ts +++ b/src/commands/moderation/warn.ts @@ -89,11 +89,13 @@ export default class Warn { } addModeration(guild.id, user.id, "WARN", member.id, reason ?? undefined); - const dmChannel = (await user.createDM().catch(() => null)) as DMChannel | null; - if (dmChannel) - await dmChannel.send({ - embeds: [embed.setTitle("⚠️ • You were warned").setColor(genColor(0))] - }); await interaction.reply({ embeds: [embed] }); + + const dmChannel = (await user.createDM().catch(() => null)) as DMChannel | null; + if (!dmChannel) return; + if (user.bot) return; + await dmChannel.send({ + embeds: [embed.setTitle("⚠️ • You were warned").setColor(genColor(0))] + }); } } diff --git a/src/commands/server/news/send.ts b/src/commands/server/news/send.ts index a9786d5..880e7e6 100644 --- a/src/commands/server/news/send.ts +++ b/src/commands/server/news/send.ts @@ -73,7 +73,7 @@ export default class Send { } const id = crypto.randomUUID(); - sendNews( + let news = sendNews( guild.id, i.fields.getTextInputValue("title"), i.fields.getTextInputValue("body"), diff --git a/src/utils/sendChannelNews.ts b/src/utils/sendChannelNews.ts index 5c38da9..89f8a3d 100644 --- a/src/utils/sendChannelNews.ts +++ b/src/utils/sendChannelNews.ts @@ -22,6 +22,7 @@ export async function sendChannelNews(guild: Guild, id: string) { .setFooter({ text: `Latest news from ${guild.name}` }) .setColor(genColor(200)); + if (news.imageURL !== null) embed.setImage(news.imageURL); return await channelToSend.send({ embeds: [embed], content: roleToSend ? `<@&${roleToSend.id}>` : undefined From 87aa750d10ff12b2499556fbd5d1c4ab064d1c2a Mon Sep 17 00:00:00 2001 From: Serge Date: Mon, 12 Feb 2024 22:45:55 +0600 Subject: [PATCH 040/127] removed imageurl --- src/commands/server/news/send.ts | 28 +++++++--------------------- src/utils/database/news.ts | 8 ++------ src/utils/sendChannelNews.ts | 2 -- 3 files changed, 9 insertions(+), 29 deletions(-) diff --git a/src/commands/server/news/send.ts b/src/commands/server/news/send.ts index 880e7e6..28b1511 100644 --- a/src/commands/server/news/send.ts +++ b/src/commands/server/news/send.ts @@ -11,7 +11,7 @@ import { import { genColor } from "../../../utils/colorGen"; import { errorEmbed } from "../../../utils/embeds/errorEmbed"; import { sendChannelNews } from "../../../utils/sendChannelNews"; -import { sendNews } from "../../../utils/database/news"; +import { get, sendNews } from "../../../utils/database/news"; import { getSetting } from "../../../utils/database/settings"; export default class Send { @@ -51,33 +51,16 @@ export default class Send { .setLabel("Content (supports Markdown)") ) as ActionRowBuilder; - const thirdActionRow = new ActionRowBuilder().addComponents( - new TextInputBuilder() - .setCustomId("imageurl") - .setPlaceholder("Place a link to your image") - .setStyle(TextInputStyle.Short) - .setMaxLength(1000) - .setLabel("Image URL (placed at the bottom)") - .setRequired(false) - ) as ActionRowBuilder; - - newsModal.addComponents(firstActionRow, secondActionRow, thirdActionRow); + newsModal.addComponents(firstActionRow, secondActionRow); await interaction.showModal(newsModal).catch(err => console.error(err)); interaction.client.once("interactionCreate", async i => { if (!i.isModalSubmit()) return; - const imageURL = i.fields.getTextInputValue("imageurl"); - if (imageURL) { - errorEmbed(interaction, "The image URL you provided is invalid."); - return; - } - const id = crypto.randomUUID(); - let news = sendNews( + sendNews( guild.id, i.fields.getTextInputValue("title"), i.fields.getTextInputValue("body"), - imageURL!, i.user.displayName, i.user.avatarURL()!, null!, @@ -85,7 +68,10 @@ export default class Send { getSetting(guild.id, "news.roleID")!, id ); - sendChannelNews(guild, id).catch(err => console.error(err)); + + await sendChannelNews(guild, id) + .catch(err => console.error(err)) + .then(message => (get(id)!.messageID = message?.id!)); await i.reply({ embeds: [new EmbedBuilder().setTitle("✅ • News sent!").setColor(genColor(100))] diff --git a/src/utils/database/news.ts b/src/utils/database/news.ts index 45204a0..9c44f57 100644 --- a/src/utils/database/news.ts +++ b/src/utils/database/news.ts @@ -7,7 +7,6 @@ const definition = { guildID: "TEXT", title: "TEXT", body: "TEXT", - imageURL: "TEXT", author: "TEXT", authorPFP: "TEXT", createdAt: "TIMESTAMP", @@ -21,7 +20,7 @@ const definition = { const database = getDatabase(definition); const sendQuery = database.query( - "INSERT INTO news (guildID, title, body, imageURL, author, authorPFP, createdAt, updatedAt, messageID, channelID, roleID, id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12);" + "INSERT INTO news (guildID, title, body, author, authorPFP, createdAt, updatedAt, messageID, channelID, roleID, id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11);" ); const listAllQuery = database.query("SELECT * FROM news WHERE guildID = $1;"); const getIdQuery = database.query("SELECT * FROM news WHERE id = $1;"); @@ -31,7 +30,6 @@ export function sendNews( guildID: string, title: string, body: string, - imageURL: string | null, author: string, authorPFP: string, messageID: string, @@ -43,7 +41,6 @@ export function sendNews( guildID, title, body, - imageURL, author, authorPFP, Date.now(), @@ -63,14 +60,13 @@ export function get(id: string) { return getIdQuery.get(id) as TypeOfDefinition | null; } -export function updateNews(id: string, title: string, body: string, imageURL: string | null) { +export function updateNews(id: string, title: string, body: string) { const lastElem = get(id)!; deleteQuery.run(id); sendQuery.run( lastElem.guildID, title, body, - imageURL, lastElem.author, lastElem.authorPFP, Date.now(), diff --git a/src/utils/sendChannelNews.ts b/src/utils/sendChannelNews.ts index 89f8a3d..4dc2da8 100644 --- a/src/utils/sendChannelNews.ts +++ b/src/utils/sendChannelNews.ts @@ -4,7 +4,6 @@ import { get } from "./database/news"; export async function sendChannelNews(guild: Guild, id: string) { const news = get(id); - console.log(news); if (!news) return; const channelToSend = guild.channels.cache.get(news.channelID) as TextChannel; @@ -22,7 +21,6 @@ export async function sendChannelNews(guild: Guild, id: string) { .setFooter({ text: `Latest news from ${guild.name}` }) .setColor(genColor(200)); - if (news.imageURL !== null) embed.setImage(news.imageURL); return await channelToSend.send({ embeds: [embed], content: roleToSend ? `<@&${roleToSend.id}>` : undefined From 6fb127e03344f3d0b3156bba39b862e7424556cd Mon Sep 17 00:00:00 2001 From: Serge Date: Fri, 16 Feb 2024 23:32:10 +0600 Subject: [PATCH 041/127] send help please --- src/bot.ts | 6 +- src/commands/about.ts | 2 +- src/commands/server/news/remove.ts | 14 ++--- src/commands/server/news/send.ts | 9 +-- src/commands/server/news/view.ts | 2 +- src/utils/database/news.ts | 95 +++++++++++++----------------- src/utils/database/settings.ts | 26 ++++---- src/utils/sendChannelNews.ts | 33 +++++++---- 8 files changed, 89 insertions(+), 98 deletions(-) diff --git a/src/bot.ts b/src/bot.ts index fce003b..6a505a7 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -1,4 +1,4 @@ -import { Client, ActivityType, Partials } from "discord.js"; +import { Client, ActivityType } from "discord.js"; import Commands from "./handlers/commands"; import Events from "./handlers/events"; @@ -12,10 +12,8 @@ const client = new Client({ "GuildMessages", "GuildEmojisAndStickers", "GuildPresences", - "MessageContent", - "GuildMessageReactions" + "MessageContent" ], - partials: [Partials.Message, Partials.Channel, Partials.Reaction] }); client.on("ready", async () => { diff --git a/src/commands/about.ts b/src/commands/about.ts index b395742..af01d92 100644 --- a/src/commands/about.ts +++ b/src/commands/about.ts @@ -47,7 +47,7 @@ export default class About { { name: "🔗 • Links", value: - "[GitHub](https://www.github.com/NebulaTheBot)・[YouTube](https://www.youtube.com/@NebulaTheBot)・[Instagram](https://instagram.com/NebulaTheBot)・[Mastodon](https://mastodon.online/@NebulaTheBot@mastodon.social)・[Guilded](https://guilded.gg/Nebula)・[Revolt](https://rvlt.gg/28TS9aXy)" + "[GitHub](https://www.github.com/NebulaTheBot) • [YouTube](https://www.youtube.com/@NebulaTheBot) • [Instagram](https://instagram.com/NebulaTheBot) • [Mastodon](https://mastodon.online/@NebulaTheBot@mastodon.social) • [Guilded](https://guilded.gg/Nebula) • [Revolt](https://rvlt.gg/28TS9aXy)" } ) .setFooter({ text: `Made by the Nebula team with ${randomise(hearts)}` }) diff --git a/src/commands/server/news/remove.ts b/src/commands/server/news/remove.ts index c6ccabb..4ba3ff3 100644 --- a/src/commands/server/news/remove.ts +++ b/src/commands/server/news/remove.ts @@ -24,10 +24,9 @@ export default class Remove { } async run(interaction: ChatInputCommandInteraction) { - const user = interaction.user; const guild = interaction.guild!; const id = interaction.options.getString("id")!; - const member = guild.members.cache.get(user.id)!; + const member = guild.members.cache.get(interaction.user.id)!; if (!member.permissions.has(PermissionsBitField.Flags.ManageGuild)) return errorEmbed( @@ -37,15 +36,16 @@ export default class Remove { ); const news = get(id); + console.log(news); if (!news) return errorEmbed(interaction, "The specified news don't exist."); - const messageId = news?.messageID; + const messageID = news.messageID; const newsChannel = (await guild.channels - .fetch(news?.channelID ?? "") - .catch(() => null)) as TextChannel | null; - - if (newsChannel) await newsChannel?.messages.delete(messageId).catch(() => null); + .fetch(news.channelID ?? interaction.channel?.id) + .catch(() => null)) as TextChannel; + console.log(newsChannel); + if (newsChannel) await newsChannel.messages.delete(messageID); deleteNews(id); await interaction.reply({ embeds: [new EmbedBuilder().setTitle("✅ • News deleted!").setColor(genColor(100))] diff --git a/src/commands/server/news/send.ts b/src/commands/server/news/send.ts index 28b1511..a0bef1d 100644 --- a/src/commands/server/news/send.ts +++ b/src/commands/server/news/send.ts @@ -11,7 +11,7 @@ import { import { genColor } from "../../../utils/colorGen"; import { errorEmbed } from "../../../utils/embeds/errorEmbed"; import { sendChannelNews } from "../../../utils/sendChannelNews"; -import { get, sendNews } from "../../../utils/database/news"; +import { sendNews } from "../../../utils/database/news"; import { getSetting } from "../../../utils/database/settings"; export default class Send { @@ -64,15 +64,12 @@ export default class Send { i.user.displayName, i.user.avatarURL()!, null!, - getSetting(guild.id, "news.channelID")!, + getSetting(guild.id, "news.channelID")! ?? interaction.channel?.id, getSetting(guild.id, "news.roleID")!, id ); - await sendChannelNews(guild, id) - .catch(err => console.error(err)) - .then(message => (get(id)!.messageID = message?.id!)); - + await sendChannelNews(guild, id, interaction).catch(err => console.error(err)); await i.reply({ embeds: [new EmbedBuilder().setTitle("✅ • News sent!").setColor(genColor(100))] }); diff --git a/src/commands/server/news/view.ts b/src/commands/server/news/view.ts index 8b79a27..48bf2cd 100644 --- a/src/commands/server/news/view.ts +++ b/src/commands/server/news/view.ts @@ -31,7 +31,7 @@ export default class View { return errorEmbed( interaction, "No news found.", - "Admins can add news with the **/server news add** command." + "Admins can add news with the **/server news send** command." ); if (page > sortedNews.length) page = sortedNews.length; diff --git a/src/utils/database/news.ts b/src/utils/database/news.ts index 9c44f57..e7a5435 100644 --- a/src/utils/database/news.ts +++ b/src/utils/database/news.ts @@ -1,83 +1,68 @@ import { getDatabase } from "."; -import { TableDefinition, TypeOfDefinition } from "./types"; +import { FieldData, SqlType, TableDefinition, TypeOfDefinition } from "./types"; -const definition = { +const tableDefinition = { name: "news", definition: { guildID: "TEXT", - title: "TEXT", - body: "TEXT", - author: "TEXT", - authorPFP: "TEXT", - createdAt: "TIMESTAMP", - updatedAt: "TIMESTAMP", - messageID: "TEXT", - channelID: "TEXT", - roleID: "TEXT", - id: "TEXT" + key: "TEXT", + value: "TEXT" } } satisfies TableDefinition; -const database = getDatabase(definition); -const sendQuery = database.query( - "INSERT INTO news (guildID, title, body, author, authorPFP, createdAt, updatedAt, messageID, channelID, roleID, id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11);" -); +export const newsDefinition = { + title: "TEXT", + body: "TEXT", + author: "TEXT", + authorPFP: "TEXT", + createdAt: "TIMESTAMP", + updatedAt: "TIMESTAMP", + messageID: "TEXT", + channelID: "TEXT", + roleID: "TEXT", + id: "TEXT" +} satisfies Record; + +const database = getDatabase(tableDefinition); +const sendQuery = database.query("INSERT INTO news (guildID, key, value) VALUES (?1, ?2, ?3);"); const listAllQuery = database.query("SELECT * FROM news WHERE guildID = $1;"); -const getIdQuery = database.query("SELECT * FROM news WHERE id = $1;"); +const getQuery = database.query("SELECT * FROM news WHERE id = $1;"); const deleteQuery = database.query("DELETE FROM news WHERE id = $1"); -export function sendNews( +export function sendNews( guildID: string, - title: string, - body: string, - author: string, - authorPFP: string, - messageID: string, - channelID: string, - roleID: string, - id: string + key: K, + value: TypeOfKey ) { - sendQuery.run( - guildID, - title, - body, - author, - authorPFP, - Date.now(), - 0, - messageID, - channelID, - roleID, - id - ); + sendQuery.run(guildID, key, value); } export function listAllNews(guildID: string) { - return listAllQuery.all(guildID) as TypeOfDefinition[]; + return listAllQuery.all(guildID) as TypeOfDefinition[]; +} + +export function get(id: string): TypeOfKey | null { + return getQuery.get(id) as TypeOfKey; } -export function get(id: string) { - return getIdQuery.get(id) as TypeOfDefinition | null; +export function set( + id: string, + key: K, + value: TypeOfKey +) { + deleteQuery.run(id); + sendQuery.run(id, key, value); } export function updateNews(id: string, title: string, body: string) { const lastElem = get(id)!; deleteQuery.run(id); - sendQuery.run( - lastElem.guildID, - title, - body, - lastElem.author, - lastElem.authorPFP, - Date.now(), - 0, - lastElem.messageID, - lastElem.channelID, - lastElem.roleID, - id - ); + sendQuery.run(lastElem.guildID, key, value); } export function deleteNews(id: string) { deleteQuery.run(id); } + +// Utility type +type TypeOfKey = SqlType<(typeof newsDefinition)[T]>; diff --git a/src/utils/database/settings.ts b/src/utils/database/settings.ts index 08d154f..e52030a 100644 --- a/src/utils/database/settings.ts +++ b/src/utils/database/settings.ts @@ -5,7 +5,7 @@ import { FieldData, SqlType, TableDefinition, TypeOfDefinition } from "./types"; const tableDefinition = { name: "settings", definition: { - guild: "TEXT", + guildID: "TEXT", key: "TEXT", value: "TEXT" } @@ -23,18 +23,22 @@ export const settingsDefinition = { export const settingsKeys = Object.keys(settingsDefinition) as (keyof typeof settingsDefinition)[]; const database = getDatabase(tableDefinition); -const getQuery = database.query("SELECT * FROM settings WHERE guild = $1 AND key = $2;"); +const getQuery = database.query("SELECT * FROM settings WHERE guildID = $1 AND key = $2;"); const listPublicQuery = database.query( "SELECT * FROM settings WHERE key = 'serverboard.shown' AND value = 'TRUE';" ); -const removeQuery = database.query("DELETE FROM settings WHERE guild = $1 AND key = $2;"); -const insertQuery = database.query("INSERT INTO settings (guild, key, value) VALUES (?1, ?2, ?3);"); +const deleteQuery = database.query("DELETE FROM settings WHERE guildID = $1 AND key = $2;"); +const insertQuery = database.query( + "INSERT INTO settings (guildID, key, value) VALUES (?1, ?2, ?3);" +); export function getSetting( - guild: string, + guildID: string, key: K ): TypeOfKey | null { - let res = getQuery.all(JSON.stringify(guild), key) as TypeOfDefinition[]; + let res = getQuery.all(JSON.stringify(guildID), key) as TypeOfDefinition< + typeof tableDefinition + >[]; if (res.length == 0) return null; switch (settingsDefinition[key]) { case "TEXT": @@ -48,20 +52,20 @@ export function getSetting( } export function setSetting( - guild: string, + guildID: string, key: K, value: TypeOfKey ) { - const doInsert = getSetting(guild, key) == null; + const doInsert = getSetting(guildID, key) == null; if (!doInsert) { - removeQuery.all(JSON.stringify(guild), key); + deleteQuery.all(JSON.stringify(guildID), key); } - insertQuery.run(JSON.stringify(guild), key, value); + insertQuery.run(JSON.stringify(guildID), key, value); } export function listPublicServers() { return (listPublicQuery.all() as TypeOfDefinition[]).map(entry => - JSON.parse(entry.guild) + JSON.parse(entry.guildID) ); } diff --git a/src/utils/sendChannelNews.ts b/src/utils/sendChannelNews.ts index 4dc2da8..7a3d05e 100644 --- a/src/utils/sendChannelNews.ts +++ b/src/utils/sendChannelNews.ts @@ -1,14 +1,19 @@ -import { EmbedBuilder, Guild, Role, TextChannel } from "discord.js"; +import { + EmbedBuilder, + type Role, + type TextChannel, + type Guild, + type ChatInputCommandInteraction +} from "discord.js"; import { genColor } from "./colorGen"; -import { get } from "./database/news"; - -export async function sendChannelNews(guild: Guild, id: string) { - const news = get(id); - if (!news) return; - - const channelToSend = guild.channels.cache.get(news.channelID) as TextChannel; - if (!channelToSend) return; +import { get, set } from "./database/news"; +export async function sendChannelNews( + guild: Guild, + id: string, + interaction: ChatInputCommandInteraction +) { + const news = get(id)!; const role = news.roleID; let roleToSend: Role | undefined; if (role) roleToSend = guild.roles.cache.get(role); @@ -21,8 +26,10 @@ export async function sendChannelNews(guild: Guild, id: string) { .setFooter({ text: `Latest news from ${guild.name}` }) .setColor(genColor(200)); - return await channelToSend.send({ - embeds: [embed], - content: roleToSend ? `<@&${roleToSend.id}>` : undefined - }); + return (guild.channels.cache.get(news.channelID ?? interaction.channel?.id) as TextChannel) + .send({ + embeds: [embed], + content: roleToSend ? `<@&${roleToSend.id}>` : undefined + }) + .then(message => set(id, "messageID", message?.id!)); } From 0f949b404d4af45b9791f6d463ab9b1607c41be8 Mon Sep 17 00:00:00 2001 From: Serge Date: Sun, 18 Feb 2024 22:33:41 +0600 Subject: [PATCH 042/127] trying to fix news --- src/utils/database/news.ts | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/utils/database/news.ts b/src/utils/database/news.ts index e7a5435..1d27df2 100644 --- a/src/utils/database/news.ts +++ b/src/utils/database/news.ts @@ -4,7 +4,7 @@ import { FieldData, SqlType, TableDefinition, TypeOfDefinition } from "./types"; const tableDefinition = { name: "news", definition: { - guildID: "TEXT", + id: "TEXT", key: "TEXT", value: "TEXT" } @@ -24,17 +24,17 @@ export const newsDefinition = { } satisfies Record; const database = getDatabase(tableDefinition); -const sendQuery = database.query("INSERT INTO news (guildID, key, value) VALUES (?1, ?2, ?3);"); -const listAllQuery = database.query("SELECT * FROM news WHERE guildID = $1;"); +const sendQuery = database.query("INSERT INTO news (id, key, value) VALUES (?1, ?2, ?3);"); +const listAllQuery = database.query("SELECT * FROM news WHERE id = $1;"); const getQuery = database.query("SELECT * FROM news WHERE id = $1;"); const deleteQuery = database.query("DELETE FROM news WHERE id = $1"); export function sendNews( - guildID: string, + id: string, key: K, value: TypeOfKey ) { - sendQuery.run(guildID, key, value); + sendQuery.run((JSON.stringify(id), key, value) as unknown as typeof newsDefinition); } export function listAllNews(guildID: string) { @@ -50,14 +50,16 @@ export function set( key: K, value: TypeOfKey ) { - deleteQuery.run(id); - sendQuery.run(id, key, value); + const doInsert = get(id) == null; + if (!doInsert) { + deleteQuery.all(id, key); + } + sendQuery.run((JSON.stringify(id), key, value) as unknown as typeof newsDefinition); } -export function updateNews(id: string, title: string, body: string) { - const lastElem = get(id)!; +export function updateNews(id: string, key: string, title: string, body: string) { deleteQuery.run(id); - sendQuery.run(lastElem.guildID, key, value); + sendQuery.run(id, key, value); } export function deleteNews(id: string) { From aa1745d328a9b017aebac705e879cd122f4c7ebc Mon Sep 17 00:00:00 2001 From: Serge Date: Mon, 19 Feb 2024 22:47:57 +0600 Subject: [PATCH 043/127] fixed removing news --- src/commands/poll.ts | 2 +- src/commands/server/news/edit.ts | 28 +--------- src/commands/server/news/remove.ts | 5 +- src/commands/server/news/send.ts | 3 - src/utils/database/news.ts | 89 ++++++++++++++---------------- src/utils/database/settings.ts | 1 + src/utils/sendChannelNews.ts | 17 ++++-- 7 files changed, 59 insertions(+), 86 deletions(-) diff --git a/src/commands/poll.ts b/src/commands/poll.ts index a8d57dc..7af0b60 100644 --- a/src/commands/poll.ts +++ b/src/commands/poll.ts @@ -71,7 +71,7 @@ export default class Poll { let embed = new EmbedBuilder() .setAuthor({ - name: `• Poll by ${member.user.username}`, + name: `• ${member.user.username}`, iconURL: member.user.displayAvatarURL() }) .setTitle(`${interaction.options.getString("question")}`) diff --git a/src/commands/server/news/edit.ts b/src/commands/server/news/edit.ts index 604f57e..40dab6a 100644 --- a/src/commands/server/news/edit.ts +++ b/src/commands/server/news/edit.ts @@ -64,43 +64,19 @@ export default class Edit { .setValue(news.body) .setRequired(true); - const imageURLInput = new TextInputBuilder() - .setCustomId("imageurl") - .setPlaceholder("Big image URL (bottom)") - .setStyle(TextInputStyle.Short) - .setMaxLength(1000) - .setLabel("Big image URL (bottom)") - .setValue(news.imageURL) - .setRequired(false); - const firstActionRow = new ActionRowBuilder().addComponents( titleInput ) as ActionRowBuilder; const secondActionRow = new ActionRowBuilder().addComponents( bodyInput ) as ActionRowBuilder; - const thirdActionRow = new ActionRowBuilder().addComponents( - imageURLInput - ) as ActionRowBuilder; - editModal.addComponents(firstActionRow, secondActionRow, thirdActionRow); + editModal.addComponents(firstActionRow, secondActionRow); await interaction.showModal(editModal).catch(err => console.error(err)); - interaction.client.once("interactionCreate", async i => { if (!i.isModalSubmit()) return; - const imageURL = i.fields.getTextInputValue("imageurl"); - if (imageURL) { - errorEmbed(interaction, "The image URL you provided is invalid."); - return; - } - - updateNews( - id, - i.fields.getTextInputValue("title"), - i.fields.getTextInputValue("body"), - imageURL - ); + updateNews(id, i.fields.getTextInputValue("title"), i.fields.getTextInputValue("body")); await interaction.reply({ embeds: [new EmbedBuilder().setTitle("✅ • News edited!").setColor(genColor(100))] diff --git a/src/commands/server/news/remove.ts b/src/commands/server/news/remove.ts index 4ba3ff3..55c2f07 100644 --- a/src/commands/server/news/remove.ts +++ b/src/commands/server/news/remove.ts @@ -8,6 +8,7 @@ import { import { genColor } from "../../../utils/colorGen"; import { errorEmbed } from "../../../utils/embeds/errorEmbed"; import { deleteNews, get } from "../../../utils/database/news"; +import { getSetting } from "../../../utils/database/settings"; export default class Remove { data: SlashCommandSubcommandBuilder; @@ -36,14 +37,12 @@ export default class Remove { ); const news = get(id); - console.log(news); if (!news) return errorEmbed(interaction, "The specified news don't exist."); const messageID = news.messageID; const newsChannel = (await guild.channels - .fetch(news.channelID ?? interaction.channel?.id) + .fetch(getSetting(guild.id, "news.channelID")! ?? interaction.channel?.id) .catch(() => null)) as TextChannel; - console.log(newsChannel); if (newsChannel) await newsChannel.messages.delete(messageID); deleteNews(id); diff --git a/src/commands/server/news/send.ts b/src/commands/server/news/send.ts index a0bef1d..c387120 100644 --- a/src/commands/server/news/send.ts +++ b/src/commands/server/news/send.ts @@ -12,7 +12,6 @@ import { genColor } from "../../../utils/colorGen"; import { errorEmbed } from "../../../utils/embeds/errorEmbed"; import { sendChannelNews } from "../../../utils/sendChannelNews"; import { sendNews } from "../../../utils/database/news"; -import { getSetting } from "../../../utils/database/settings"; export default class Send { data: SlashCommandSubcommandBuilder; @@ -64,8 +63,6 @@ export default class Send { i.user.displayName, i.user.avatarURL()!, null!, - getSetting(guild.id, "news.channelID")! ?? interaction.channel?.id, - getSetting(guild.id, "news.roleID")!, id ); diff --git a/src/utils/database/news.ts b/src/utils/database/news.ts index 1d27df2..1937302 100644 --- a/src/utils/database/news.ts +++ b/src/utils/database/news.ts @@ -1,70 +1,65 @@ import { getDatabase } from "."; -import { FieldData, SqlType, TableDefinition, TypeOfDefinition } from "./types"; +import { TableDefinition, TypeOfDefinition } from "./types"; -const tableDefinition = { +const definition = { name: "news", definition: { - id: "TEXT", - key: "TEXT", - value: "TEXT" + guildID: "TEXT", + title: "TEXT", + body: "TEXT", + author: "TEXT", + authorPFP: "TEXT", + createdAt: "TIMESTAMP", + updatedAt: "TIMESTAMP", + messageID: "TEXT", + id: "TEXT" } } satisfies TableDefinition; -export const newsDefinition = { - title: "TEXT", - body: "TEXT", - author: "TEXT", - authorPFP: "TEXT", - createdAt: "TIMESTAMP", - updatedAt: "TIMESTAMP", - messageID: "TEXT", - channelID: "TEXT", - roleID: "TEXT", - id: "TEXT" -} satisfies Record; - -const database = getDatabase(tableDefinition); -const sendQuery = database.query("INSERT INTO news (id, key, value) VALUES (?1, ?2, ?3);"); -const listAllQuery = database.query("SELECT * FROM news WHERE id = $1;"); -const getQuery = database.query("SELECT * FROM news WHERE id = $1;"); +const database = getDatabase(definition); +const sendQuery = database.query( + "INSERT INTO news (guildID, title, body, author, authorPFP, createdAt, updatedAt, messageID, id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9);" +); +const listAllQuery = database.query("SELECT * FROM news WHERE guildID = $1;"); +const getIdQuery = database.query("SELECT * FROM news WHERE id = $1;"); const deleteQuery = database.query("DELETE FROM news WHERE id = $1"); -export function sendNews( - id: string, - key: K, - value: TypeOfKey +export function sendNews( + guildID: string, + title: string, + body: string, + author: string, + authorPFP: string, + messageID: string, + id: string ) { - sendQuery.run((JSON.stringify(id), key, value) as unknown as typeof newsDefinition); + sendQuery.run(guildID, title, body, author, authorPFP, Date.now(), 0, messageID, id); } export function listAllNews(guildID: string) { - return listAllQuery.all(guildID) as TypeOfDefinition[]; -} - -export function get(id: string): TypeOfKey | null { - return getQuery.get(id) as TypeOfKey; + return listAllQuery.all(guildID) as TypeOfDefinition[]; } -export function set( - id: string, - key: K, - value: TypeOfKey -) { - const doInsert = get(id) == null; - if (!doInsert) { - deleteQuery.all(id, key); - } - sendQuery.run((JSON.stringify(id), key, value) as unknown as typeof newsDefinition); +export function get(id: string) { + return getIdQuery.get(id) as TypeOfDefinition | null; } -export function updateNews(id: string, key: string, title: string, body: string) { +export function updateNews(id: string, title?: string, body?: string, messageID?: string) { + const lastElem = get(id)!; deleteQuery.run(id); - sendQuery.run(id, key, value); + sendQuery.run( + lastElem.guildID, + title ?? lastElem.title, + body ?? lastElem.body, + lastElem.author, + lastElem.authorPFP, + Date.now(), + 0, + messageID ?? lastElem.messageID, + id + ); } export function deleteNews(id: string) { deleteQuery.run(id); } - -// Utility type -type TypeOfKey = SqlType<(typeof newsDefinition)[T]>; diff --git a/src/utils/database/settings.ts b/src/utils/database/settings.ts index e52030a..b203925 100644 --- a/src/utils/database/settings.ts +++ b/src/utils/database/settings.ts @@ -17,6 +17,7 @@ export const settingsDefinition = { "log.channel": "INTEGER", "news.channelID": "TEXT", "news.roleID": "TEXT", + "news.editNews": "BOOL", "serverboard.inviteLink": "TEXT", "serverboard.shown": "BOOL" } satisfies Record; diff --git a/src/utils/sendChannelNews.ts b/src/utils/sendChannelNews.ts index 7a3d05e..2ff8ba0 100644 --- a/src/utils/sendChannelNews.ts +++ b/src/utils/sendChannelNews.ts @@ -6,7 +6,8 @@ import { type ChatInputCommandInteraction } from "discord.js"; import { genColor } from "./colorGen"; -import { get, set } from "./database/news"; +import { get, updateNews } from "./database/news"; +import { getSetting } from "./database/settings"; export async function sendChannelNews( guild: Guild, @@ -14,22 +15,26 @@ export async function sendChannelNews( interaction: ChatInputCommandInteraction ) { const news = get(id)!; - const role = news.roleID; + const role = getSetting(guild.id, "news.roleID"); let roleToSend: Role | undefined; if (role) roleToSend = guild.roles.cache.get(role); const embed = new EmbedBuilder() - .setAuthor({ name: news.author, iconURL: news.authorPFP }) + .setAuthor({ name: `• ${news.author}`, iconURL: news.authorPFP }) .setTitle(news.title) .setDescription(news.body) .setTimestamp(parseInt(news.updatedAt.toString()) ?? null) - .setFooter({ text: `Latest news from ${guild.name}` }) + .setFooter({ text: `Latest news from ${guild.name}\nID: ${news.id}` }) .setColor(genColor(200)); - return (guild.channels.cache.get(news.channelID ?? interaction.channel?.id) as TextChannel) + return ( + guild.channels.cache.get( + getSetting(guild.id, "news.channelID")! ?? interaction.channel?.id + ) as TextChannel + ) .send({ embeds: [embed], content: roleToSend ? `<@&${roleToSend.id}>` : undefined }) - .then(message => set(id, "messageID", message?.id!)); + .then(message => updateNews(id, undefined, undefined, message.id)); } From 0024d13b0eb2cefb7c8c7371ad7cb38ebfaf7e50 Mon Sep 17 00:00:00 2001 From: Serge Date: Tue, 20 Feb 2024 22:46:22 +0600 Subject: [PATCH 044/127] more changes woo --- src/bot.ts | 2 +- src/commands/server/news/edit.ts | 35 +++++++++++++++++++++-- src/commands/server/news/send.ts | 2 +- src/events/guildMemberAdd.ts | 44 +++++++++++++++++++++++++++++ src/events/guildMemberRemove.ts | 44 +++++++++++++++++++++++++++++ src/events/messageReactionCreate.ts | 2 +- src/utils/database/settings.ts | 4 +-- src/utils/sendChannelNews.ts | 8 ++++-- 8 files changed, 130 insertions(+), 11 deletions(-) create mode 100644 src/events/guildMemberAdd.ts create mode 100644 src/events/guildMemberRemove.ts diff --git a/src/bot.ts b/src/bot.ts index 6a505a7..f84ed93 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -13,7 +13,7 @@ const client = new Client({ "GuildEmojisAndStickers", "GuildPresences", "MessageContent" - ], + ] }); client.on("ready", async () => { diff --git a/src/commands/server/news/edit.ts b/src/commands/server/news/edit.ts index 40dab6a..61dd862 100644 --- a/src/commands/server/news/edit.ts +++ b/src/commands/server/news/edit.ts @@ -6,11 +6,15 @@ import { TextInputBuilder, ActionRowBuilder, TextInputStyle, - type ChatInputCommandInteraction + type ChatInputCommandInteraction, + type TextChannel, + type Role } from "discord.js"; import { genColor } from "../../../utils/colorGen"; import { errorEmbed } from "../../../utils/embeds/errorEmbed"; import { get, updateNews } from "../../../utils/database/news"; +import { getSetting } from "../../../utils/database/settings"; +import { sendChannelNews } from "../../../utils/sendChannelNews"; export default class Edit { data: SlashCommandSubcommandBuilder; @@ -38,13 +42,14 @@ export default class Edit { "You need the **Manage Server** permission." ); + const guild = interaction.guild!; const id = interaction.options.getString("id", true).trim(); const news = get(id); if (!news) return errorEmbed(interaction, "The specified news doesn't exist."); const editModal = new ModalBuilder() .setCustomId("editnews") - .setTitle(`Edit News: ${news.title}`); + .setTitle(`Edit news: ${news.title}`); const titleInput = new TextInputBuilder() .setCustomId("title") @@ -76,8 +81,32 @@ export default class Edit { interaction.client.once("interactionCreate", async i => { if (!i.isModalSubmit()) return; - updateNews(id, i.fields.getTextInputValue("title"), i.fields.getTextInputValue("body")); + const role = getSetting(guild.id, "news.roleID"); + let roleToSend: Role | undefined; + if (role) roleToSend = guild.roles.cache.get(role); + const title = i.fields.getTextInputValue("title"); + const body = i.fields.getTextInputValue("body"); + const newsEditable = getSetting(guild.id, "news.editOriginalMessage"); + if (newsEditable === false) await sendChannelNews(guild, id, interaction, title, body); + const embed = new EmbedBuilder() + .setAuthor({ name: `• ${news.author}`, iconURL: news.authorPFP }) + .setTitle(title) + .setDescription(body) + .setTimestamp(parseInt(news.updatedAt.toString()) ?? null) + .setFooter({ text: `Edited news from ${guild.name}\nID: ${news.id}` }) + .setColor(genColor(200)); + + ( + guild.channels.cache.get( + getSetting(guild.id, "news.channelID")! ?? interaction.channel?.id + ) as TextChannel + )?.messages.edit(news.messageID, { + embeds: [embed], + content: roleToSend ? `<@&${roleToSend.id}>` : undefined + }); + + updateNews(id, title, body); await interaction.reply({ embeds: [new EmbedBuilder().setTitle("✅ • News edited!").setColor(genColor(100))] }); diff --git a/src/commands/server/news/send.ts b/src/commands/server/news/send.ts index c387120..17fb7bf 100644 --- a/src/commands/server/news/send.ts +++ b/src/commands/server/news/send.ts @@ -31,7 +31,7 @@ export default class Send { "You need the **Manage Server** permission." ); - const newsModal = new ModalBuilder().setCustomId("sendnews").setTitle("Write your news out."); + const newsModal = new ModalBuilder().setCustomId("sendnews").setTitle("Write your news."); const firstActionRow = new ActionRowBuilder().addComponents( new TextInputBuilder() .setCustomId("title") diff --git a/src/events/guildMemberAdd.ts b/src/events/guildMemberAdd.ts new file mode 100644 index 0000000..75b3600 --- /dev/null +++ b/src/events/guildMemberAdd.ts @@ -0,0 +1,44 @@ +import { + EmbedBuilder, type Client, type GuildMember, + type TextChannel, type ColorResolvable +} from "discord.js"; +import { genColor, genRGBColor } from "../utils/colorGen.js"; +import Vibrant from "node-vibrant"; +import sharp from "sharp"; + +export default { + name: "guildMemberAdd", + event: class GuildMemberAdd { + client: Client; + + constructor(client: Client) { + this.client = client; + } + + async run(member: GuildMember) { + const id = "1079612083307548704"; + const channel = await member.guild.channels.cache.find(channel => channel.id === id)!.fetch() as TextChannel; + const avatarURL = member.displayAvatarURL(); + + const embed = new EmbedBuilder() + .setAuthor({ + name: `• ${member.nickname == null ? member.user.username : member.nickname}`, + iconURL: avatarURL + }) + .setTitle("Welcome!") + .setDescription(`Enjoy your stay in **${member.guild.name}**!`) + .setFooter({ text: `User ID: ${member.id}` }) + .setThumbnail(avatarURL) + .setColor(genColor(200)); + + try { + const imageBuffer = await (await fetch(avatarURL)).arrayBuffer(); + const image = sharp(imageBuffer).toFormat("jpg"); + const { r, g, b } = (await new Vibrant(await image.toBuffer()).getPalette()).Vibrant!; + embed.setColor(genRGBColor(r, g, b) as ColorResolvable); + } catch {} + + await channel.send({ embeds: [embed] }); + } + } +} diff --git a/src/events/guildMemberRemove.ts b/src/events/guildMemberRemove.ts new file mode 100644 index 0000000..39c06e2 --- /dev/null +++ b/src/events/guildMemberRemove.ts @@ -0,0 +1,44 @@ +import { + EmbedBuilder, type Client, type GuildMember, + type TextChannel, type ColorResolvable +} from "discord.js"; +import { genColor, genRGBColor } from "../utils/colorGen.js"; +import Vibrant from "node-vibrant"; +import sharp from "sharp"; + +export default { + name: "guildMemberRemove", + event: class GuildMemberRemove { + client: Client; + + constructor(client: Client) { + this.client = client; + } + + async run(member: GuildMember) { + const id = "1079612083307548704"; + const channel = await member.guild.channels.cache.find(channel => channel.id === id)!.fetch() as TextChannel; + const avatarURL = member.displayAvatarURL(); + + const embed = new EmbedBuilder() + .setAuthor({ + name: `• ${member.nickname == null ? member.user.username : member.nickname}`, + iconURL: avatarURL + }) + .setTitle("Goodbye!") + .setDescription(`**@${member.user.username}** has left the server 😥`) + .setFooter({ text: `User ID: ${member.id}` }) + .setThumbnail(avatarURL) + .setColor(genColor(200)); + + try { + const imageBuffer = await (await fetch(avatarURL)).arrayBuffer(); + const image = sharp(imageBuffer).toFormat("jpg"); + const { r, g, b } = (await new Vibrant(await image.toBuffer()).getPalette()).Vibrant!; + embed.setColor(genRGBColor(r, g, b) as ColorResolvable); + } catch {} + + await channel.send({ embeds: [embed] }); + } + } +} \ No newline at end of file diff --git a/src/events/messageReactionCreate.ts b/src/events/messageReactionCreate.ts index 58b60c1..31cdf26 100644 --- a/src/events/messageReactionCreate.ts +++ b/src/events/messageReactionCreate.ts @@ -1,4 +1,4 @@ -import { type Client, type Guild, type User, type MessageReaction } from "discord.js"; +import type { Client, Guild, User, MessageReaction } from "discord.js"; import { getPolls } from "../utils/database/poll"; export default { diff --git a/src/utils/database/settings.ts b/src/utils/database/settings.ts index b203925..fe43f63 100644 --- a/src/utils/database/settings.ts +++ b/src/utils/database/settings.ts @@ -14,10 +14,10 @@ const tableDefinition = { export const settingsDefinition = { "levelling.enabled": "BOOL", "levelling.channel": "INTEGER", - "log.channel": "INTEGER", + "moderation.channel": "INTEGER", "news.channelID": "TEXT", "news.roleID": "TEXT", - "news.editNews": "BOOL", + "news.editOriginalMessage": "BOOL", "serverboard.inviteLink": "TEXT", "serverboard.shown": "BOOL" } satisfies Record; diff --git a/src/utils/sendChannelNews.ts b/src/utils/sendChannelNews.ts index 2ff8ba0..41b7ad2 100644 --- a/src/utils/sendChannelNews.ts +++ b/src/utils/sendChannelNews.ts @@ -12,7 +12,9 @@ import { getSetting } from "./database/settings"; export async function sendChannelNews( guild: Guild, id: string, - interaction: ChatInputCommandInteraction + interaction: ChatInputCommandInteraction, + title?: string, + body?: string, ) { const news = get(id)!; const role = getSetting(guild.id, "news.roleID"); @@ -21,8 +23,8 @@ export async function sendChannelNews( const embed = new EmbedBuilder() .setAuthor({ name: `• ${news.author}`, iconURL: news.authorPFP }) - .setTitle(news.title) - .setDescription(news.body) + .setTitle(title ?? news.title) + .setDescription(body ?? news.body) .setTimestamp(parseInt(news.updatedAt.toString()) ?? null) .setFooter({ text: `Latest news from ${guild.name}\nID: ${news.id}` }) .setColor(genColor(200)); From c0eeeebe2597871665132f87b209ca1382f1979c Mon Sep 17 00:00:00 2001 From: ThatBoiDev Date: Wed, 21 Feb 2024 15:13:53 +0100 Subject: [PATCH 045/127] feat: settings--types for welcome/goodbye messages --- src/utils/database/settings.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/utils/database/settings.ts b/src/utils/database/settings.ts index 08d154f..a8f7640 100644 --- a/src/utils/database/settings.ts +++ b/src/utils/database/settings.ts @@ -18,7 +18,12 @@ export const settingsDefinition = { "news.channelID": "TEXT", "news.roleID": "TEXT", "serverboard.inviteLink": "TEXT", - "serverboard.shown": "BOOL" + "serverboard.shown": "BOOL", + "welcome.text": "TEXT", + "welcome.goodbyeText": "TEXT", + "welcome.channel": "INTEGER", + "welcome.shown": "BOOL", + "welcome.goodbyeShown": "BOOL" } satisfies Record; export const settingsKeys = Object.keys(settingsDefinition) as (keyof typeof settingsDefinition)[]; From 397b79150180772570a84ff13e8633609f877bb9 Mon Sep 17 00:00:00 2001 From: Serge Date: Wed, 21 Feb 2024 22:57:14 +0600 Subject: [PATCH 046/127] finished welcome/goodbye, fixed unban --- src/bot.ts | 1 + src/commands/about.ts | 2 +- src/commands/moderation/ban.ts | 6 ++--- src/commands/moderation/delwarn.ts | 10 +++---- src/commands/moderation/kick.ts | 10 +++---- src/commands/moderation/mute.ts | 10 +++---- src/commands/moderation/purge.ts | 4 +-- src/commands/moderation/unban.ts | 12 +++++---- src/commands/moderation/unmute.ts | 8 +++--- src/commands/moderation/warn.ts | 10 +++---- src/commands/moderation/warns.ts | 4 +-- src/commands/poll.ts | 4 +-- src/commands/server/news/edit.ts | 4 +-- src/commands/server/news/remove.ts | 2 +- src/commands/server/news/send.ts | 2 +- src/commands/server/news/view.ts | 4 +-- src/commands/server/settings.ts | 4 +-- src/commands/user.ts | 6 ++--- src/events/guildCreate.ts | 2 +- src/events/guildMemberAdd.ts | 42 ++++++++++++++++++++---------- src/events/guildMemberRemove.ts | 39 +++++++++++++++++---------- src/events/messageCreate.ts | 4 +-- src/utils/database/settings.ts | 8 +++--- src/utils/embeds/errorEmbed.ts | 2 +- src/utils/embeds/serverEmbed.ts | 2 +- 25 files changed, 113 insertions(+), 89 deletions(-) diff --git a/src/bot.ts b/src/bot.ts index f84ed93..b0850bb 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -12,6 +12,7 @@ const client = new Client({ "GuildMessages", "GuildEmojisAndStickers", "GuildPresences", + "GuildBans", "MessageContent" ] }); diff --git a/src/commands/about.ts b/src/commands/about.ts index af01d92..f4e0961 100644 --- a/src/commands/about.ts +++ b/src/commands/about.ts @@ -20,7 +20,7 @@ export default class About { const shards = client.shard?.count; const hearts = ["💖", "💝", "💓", "💗", "💘", "💟", "💕", "💞"]; const embed = new EmbedBuilder() - .setAuthor({ name: "• About", iconURL: client.user.displayAvatarURL() }) + .setAuthor({ name: "• About Nebula", iconURL: client.user.displayAvatarURL() }) .setDescription( "Nebula is a multiplatform, multipurpose bot with the ability to add extensions to have additional features." ) diff --git a/src/commands/moderation/ban.ts b/src/commands/moderation/ban.ts index b48249e..66e0cec 100644 --- a/src/commands/moderation/ban.ts +++ b/src/commands/moderation/ban.ts @@ -61,8 +61,8 @@ export default class Ban { const reason = interaction.options.getString("reason"); const embed = new EmbedBuilder() - .setAuthor({ name: `• ${name}`, iconURL: user.displayAvatarURL() }) - .setTitle(`✅ • Banned ${name}`) + .setAuthor({ name: `• ${name}`, iconURL: user.displayAvatarURL() }) + .setTitle(`✅ • Banned ${name}`) .setDescription( [ `**Moderator**: ${interaction.user.displayName}`, @@ -73,7 +73,7 @@ export default class Ban { .setFooter({ text: `User ID: ${user.id}` }) .setColor(genColor(100)); - const logChannel = getSetting(guild.id, "log.channel"); + const logChannel = getSetting(guild.id, "moderation.channel"); if (logChannel) { const channel = await guild.channels.cache .get(`${logChannel}`) diff --git a/src/commands/moderation/delwarn.ts b/src/commands/moderation/delwarn.ts index e36cdb8..517cb70 100644 --- a/src/commands/moderation/delwarn.ts +++ b/src/commands/moderation/delwarn.ts @@ -36,7 +36,7 @@ export default class Delwarn { const members = guild.members.cache; const member = members.get(interaction.member?.user.id!)!; const target = members.get(user.id)!; - const name = target.nickname ?? user.username; + const name = user.displayName; const id = interaction.options.getNumber("id", true); const warns = listUserModeration(guild.id, user.id, "WARN"); const newWarns = warns.filter(warn => warn.id !== `${id}`); @@ -67,14 +67,14 @@ export default class Delwarn { ); const embed = new EmbedBuilder() - .setAuthor({ name: `• ${user.username}`, iconURL: user.displayAvatarURL() }) - .setTitle(`✅ • Removed warning`) - .setDescription(`**Moderator**: ${interaction.user.username}`) + .setAuthor({ name: `• ${name}`, iconURL: user.displayAvatarURL() }) + .setTitle(`✅ • Removed warning from ${name}`) + .setDescription(`**Moderator**: ${interaction.user.displayName}`) .setThumbnail(user.displayAvatarURL()) .setFooter({ text: `User ID: ${user.id}` }) .setColor(genColor(100)); - const logChannel = getSetting(guild.id, "log.channel"); + const logChannel = getSetting(guild.id, "moderation.channel"); if (logChannel) { const channel = await guild.channels.cache .get(`${logChannel}`) diff --git a/src/commands/moderation/kick.ts b/src/commands/moderation/kick.ts index 26ac0e6..0af99d5 100644 --- a/src/commands/moderation/kick.ts +++ b/src/commands/moderation/kick.ts @@ -32,7 +32,7 @@ export default class Kick { const members = guild.members.cache!; const member = members.get(interaction.member?.user.id!)!; const target = members.get(user.id)!; - const name = target.nickname ?? user.username; + const name = user.displayName; if (!member.permissions.has(PermissionsBitField.Flags.KickMembers)) return errorEmbed( @@ -61,11 +61,11 @@ export default class Kick { const reason = interaction.options.getString("reason"); const embed = new EmbedBuilder() - .setAuthor({ name: `• ${user.username}`, iconURL: user.displayAvatarURL() }) - .setTitle(`✅ • Kicked <@${user.id}>`) + .setAuthor({ name: `• ${name}`, iconURL: user.displayAvatarURL() }) + .setTitle(`✅ • Kicked ${name}`) .setDescription( [ - `**Moderator**: ${interaction.user.username}`, + `**Moderator**: ${interaction.user.displayName}`, `**Reason**: ${reason ?? "No reason provided"}` ].join("\n") ) @@ -73,7 +73,7 @@ export default class Kick { .setFooter({ text: `User ID: ${user.id}` }) .setColor(genColor(100)); - const logChannel = getSetting(guild.id, "log.channel"); + const logChannel = getSetting(guild.id, "moderation.channel"); if (logChannel) { const channel = await guild.channels.cache .get(`${logChannel}`) diff --git a/src/commands/moderation/mute.ts b/src/commands/moderation/mute.ts index 37081e9..61cf934 100644 --- a/src/commands/moderation/mute.ts +++ b/src/commands/moderation/mute.ts @@ -40,7 +40,7 @@ export default class Mute { const members = guild.members.cache!; const member = members.get(interaction.member?.user.id!)!; const target = members.get(user.id)!; - const name = target.nickname ?? user.username; + const name = user.displayName; if (!member.permissions.has(PermissionsBitField.Flags.MuteMembers)) return errorEmbed( @@ -78,11 +78,11 @@ export default class Mute { Date.parse(new Date().toISOString()) + Date.parse(new Date(ms(duration)).toISOString()) ).toISOString(); const embed = new EmbedBuilder() - .setAuthor({ name: `• ${user.username}`, iconURL: user.displayAvatarURL() }) - .setTitle(`✅ • Muted ${user.username}`) + .setAuthor({ name: `• ${name}`, iconURL: user.displayAvatarURL() }) + .setTitle(`✅ • Muted ${name}`) .setDescription( [ - `**Moderator**: ${interaction.user.username}`, + `**Moderator**: ${interaction.user.displayName}`, `**Duration**: ${ms(ms(duration), { long: true })}`, `**Reason**: ${interaction.options.getString("reason") ?? "No reason provided"}` ].join("\n") @@ -91,7 +91,7 @@ export default class Mute { .setThumbnail(user.displayAvatarURL()) .setColor(genColor(100)); - const logChannel = getSetting(interaction.guildId!, "log.channel"); + const logChannel = getSetting(interaction.guildId!, "moderation.channel"); if (logChannel) { const channel = await guild.channels.cache .get(`${logChannel}`) diff --git a/src/commands/moderation/purge.ts b/src/commands/moderation/purge.ts index 821194b..eeaa8ca 100644 --- a/src/commands/moderation/purge.ts +++ b/src/commands/moderation/purge.ts @@ -55,7 +55,7 @@ export default class Purge { const channelOption = interaction.options.getChannel("channel")!; const channel = guild.channels.cache.get(interaction.channel?.id ?? channelOption.id)!; const embed = new EmbedBuilder() - .setTitle(`✅ • Purged ${amount} messages.`) + .setTitle(`✅ • Purged ${amount} messages.`) .setDescription( [ `**Moderator**: ${interaction.user.username}`, @@ -73,7 +73,7 @@ export default class Purge { ? await channel.bulkDelete(amount + 1, true) : await channel.bulkDelete(amount, true); - const logChannel = getSetting(guild.id, "log.channel"); + const logChannel = getSetting(guild.id, "moderation.channel"); if (logChannel) { const channel = await guild.channels.cache .get(`${logChannel}`) diff --git a/src/commands/moderation/unban.ts b/src/commands/moderation/unban.ts index d39b39d..43b0086 100644 --- a/src/commands/moderation/unban.ts +++ b/src/commands/moderation/unban.ts @@ -30,7 +30,9 @@ export default class Unban { const id = interaction.options.getString("id")!; const guild = interaction.guild!; const member = guild.members.cache.get(interaction.member?.user.id!)!; - const target = guild.bans.cache.map(user => user.user).filter(user => user.id === id)[0]!; + const target = (await guild.bans.fetch()) + .map(user => user.user) + .filter(user => user.id === id)[0]!; if (!member.permissions.has(PermissionsBitField.Flags.BanMembers)) return errorEmbed( @@ -43,14 +45,14 @@ export default class Unban { return errorEmbed(interaction, "You can't unban this user.", "The user was never banned."); const embed = new EmbedBuilder() - .setAuthor({ name: target.username, iconURL: target.displayAvatarURL() }) - .setTitle(`✅ • Unbanned ${target.username}`) - .setDescription(`**Moderator**: ${interaction.user.username}`) + .setAuthor({ name: `• ${target.displayName}`, iconURL: target.displayAvatarURL() }) + .setTitle(`✅ • Unbanned ${target.displayName}`) + .setDescription(`**Moderator**: ${interaction.user.displayName}`) .setThumbnail(target.displayAvatarURL()) .setFooter({ text: `User ID: ${id}` }) .setColor(genColor(100)); - const logChannel = getSetting(guild.id, "log.channel"); + const logChannel = getSetting(guild.id, "moderation.channel"); if (logChannel) { const channel = await guild.channels.cache .get(`${logChannel}`) diff --git a/src/commands/moderation/unmute.ts b/src/commands/moderation/unmute.ts index ab96e61..a4aada4 100644 --- a/src/commands/moderation/unmute.ts +++ b/src/commands/moderation/unmute.ts @@ -42,11 +42,11 @@ export default class Unmute { return errorEmbed(interaction, "You can't unmute this user.", "The user was never muted."); const embed = new EmbedBuilder() - .setAuthor({ name: `• ${user.username}`, iconURL: user.displayAvatarURL() }) - .setTitle(`✅ • Unmuted ${user.username}`) + .setAuthor({ name: `• ${user.displayName}`, iconURL: user.displayAvatarURL() }) + .setTitle(`✅ • Unmuted ${user.displayName}`) .setDescription( [ - `**Moderator**: ${interaction.user.username}`, + `**Moderator**: ${interaction.user.displayName}`, `**Date**: ` ].join("\n") ) @@ -54,7 +54,7 @@ export default class Unmute { .setFooter({ text: `User ID: ${user.id}` }) .setColor(genColor(100)); - const logChannel = getSetting(guild.id, "log.channel"); + const logChannel = getSetting(guild.id, "moderation.channel"); if (logChannel) { const channel = await guild.channels.cache .get(`${logChannel}`) diff --git a/src/commands/moderation/warn.ts b/src/commands/moderation/warn.ts index d69bb67..b153f2a 100644 --- a/src/commands/moderation/warn.ts +++ b/src/commands/moderation/warn.ts @@ -33,7 +33,7 @@ export default class Warn { const members = guild.members.cache; const member = members.get(interaction.member?.user.id!)!; const target = members.get(user.id)!; - const name = target.nickname ?? user.username; + const name = user.displayName; if (!member.permissions.has(PermissionsBitField.Flags.ModerateMembers)) return errorEmbed( @@ -62,11 +62,11 @@ export default class Warn { const reason = interaction.options.getString("reason"); const embed = new EmbedBuilder() - .setAuthor({ name: `• ${user.username}`, iconURL: user.displayAvatarURL() }) - .setTitle(`✅ • Warned ${user.username}`) + .setAuthor({ name: `• ${name}`, iconURL: user.displayAvatarURL() }) + .setTitle(`✅ • Warned ${name}`) .setDescription( [ - `**Moderator**: ${interaction.user.username}`, + `**Moderator**: ${interaction.user.displayName}`, `**Reason**: ${reason ?? "No reason provided"}` ].join("\n") ) @@ -74,7 +74,7 @@ export default class Warn { .setFooter({ text: `User ID: ${user.id}` }) .setColor(genColor(100)); - const logChannel = getSetting(guild.id, "log.channel"); + const logChannel = getSetting(guild.id, "moderation.channel"); if (logChannel) { const channel = await guild.channels.cache .get(`${logChannel}`) diff --git a/src/commands/moderation/warns.ts b/src/commands/moderation/warns.ts index 33db3dd..d70b562 100644 --- a/src/commands/moderation/warns.ts +++ b/src/commands/moderation/warns.ts @@ -35,8 +35,8 @@ export default class Warns { const user = interaction.options.getUser("user")!; const warns = listUserModeration(guild.id, user.id, "WARN"); const embed = new EmbedBuilder() - .setAuthor({ name: `• ${user.username}`, iconURL: user.displayAvatarURL() }) - .setTitle(`✅ • Warns of ${user.username}`) + .setAuthor({ name: `• ${user.displayName}`, iconURL: user.displayAvatarURL() }) + .setTitle(`✅ • Warns of ${user.displayName}`) .setFields( warns.length > 0 ? warns.map(warn => { diff --git a/src/commands/poll.ts b/src/commands/poll.ts index 7af0b60..65925f8 100644 --- a/src/commands/poll.ts +++ b/src/commands/poll.ts @@ -71,7 +71,7 @@ export default class Poll { let embed = new EmbedBuilder() .setAuthor({ - name: `• ${member.user.username}`, + name: `• ${member.user.displayName}`, iconURL: member.user.displayAvatarURL() }) .setTitle(`${interaction.options.getString("question")}`) @@ -88,7 +88,7 @@ export default class Poll { if (image) embed.setImage(image.url); if (channel) { const successEmbed = new EmbedBuilder() - .setTitle("✅ • Poll has been created successfully") + .setTitle("✅ • Poll has been created successfully") .setDescription(`Poll is sent to ${channel}.`) .setColor(genColor(100)); diff --git a/src/commands/server/news/edit.ts b/src/commands/server/news/edit.ts index 61dd862..47f5d70 100644 --- a/src/commands/server/news/edit.ts +++ b/src/commands/server/news/edit.ts @@ -83,7 +83,7 @@ export default class Edit { const role = getSetting(guild.id, "news.roleID"); let roleToSend: Role | undefined; - if (role) roleToSend = guild.roles.cache.get(role); + if (role) roleToSend = guild.roles.cache.get(role); const title = i.fields.getTextInputValue("title"); const body = i.fields.getTextInputValue("body"); const newsEditable = getSetting(guild.id, "news.editOriginalMessage"); @@ -108,7 +108,7 @@ export default class Edit { updateNews(id, title, body); await interaction.reply({ - embeds: [new EmbedBuilder().setTitle("✅ • News edited!").setColor(genColor(100))] + embeds: [new EmbedBuilder().setTitle("✅ • News edited!").setColor(genColor(100))] }); }); } diff --git a/src/commands/server/news/remove.ts b/src/commands/server/news/remove.ts index 55c2f07..54caa3c 100644 --- a/src/commands/server/news/remove.ts +++ b/src/commands/server/news/remove.ts @@ -47,7 +47,7 @@ export default class Remove { if (newsChannel) await newsChannel.messages.delete(messageID); deleteNews(id); await interaction.reply({ - embeds: [new EmbedBuilder().setTitle("✅ • News deleted!").setColor(genColor(100))] + embeds: [new EmbedBuilder().setTitle("✅ • News deleted!").setColor(genColor(100))] }); } } diff --git a/src/commands/server/news/send.ts b/src/commands/server/news/send.ts index 17fb7bf..b01e8a4 100644 --- a/src/commands/server/news/send.ts +++ b/src/commands/server/news/send.ts @@ -68,7 +68,7 @@ export default class Send { await sendChannelNews(guild, id, interaction).catch(err => console.error(err)); await i.reply({ - embeds: [new EmbedBuilder().setTitle("✅ • News sent!").setColor(genColor(100))] + embeds: [new EmbedBuilder().setTitle("✅ • News sent!").setColor(genColor(100))] }); }); } diff --git a/src/commands/server/news/view.ts b/src/commands/server/news/view.ts index 48bf2cd..5f9d0a9 100644 --- a/src/commands/server/news/view.ts +++ b/src/commands/server/news/view.ts @@ -38,7 +38,7 @@ export default class View { if (page < 1) page = 1; let embed = new EmbedBuilder() - .setAuthor({ name: currentNews.author, iconURL: currentNews.authorPFP }) + .setAuthor({ name: `• ${currentNews.author}`, iconURL: currentNews.authorPFP }) .setTitle(currentNews.title) .setDescription(currentNews.body) .setImage(currentNews.imageURL || null) @@ -84,7 +84,7 @@ export default class View { currentNews = sortedNews[page - 1]; embed = new EmbedBuilder() - .setAuthor({ name: currentNews.author, iconURL: currentNews.authorPFP }) + .setAuthor({ name: `• ${currentNews.author}`, iconURL: currentNews.authorPFP }) .setTitle(currentNews.title) .setDescription(currentNews.body) .setImage(currentNews.imageURL || null) diff --git a/src/commands/server/settings.ts b/src/commands/server/settings.ts index 16a41bd..503020c 100644 --- a/src/commands/server/settings.ts +++ b/src/commands/server/settings.ts @@ -43,10 +43,10 @@ export default class ServerInfo { ); const embed = new EmbedBuilder() - .setTitle(`\`${key}\` has been set to \`${value}\``) + .setTitle(`✅ • \`${key}\` has been set to \`${value}\``) .setColor(genColor(100)); - setSetting(interaction.guildId!, key, value); + setSetting(interaction.guildId!, key, value as keyof typeof settingsDefinition); interaction.reply({ embeds: [embed] }); } diff --git a/src/commands/user.ts b/src/commands/user.ts index 6d026c8..fc05884 100644 --- a/src/commands/user.ts +++ b/src/commands/user.ts @@ -27,13 +27,11 @@ export default class User { const target = guild.members.cache .filter(member => member.user.id === id) .map(user => user)[0]!; - const selectedUser = target.user!; + const selectedUser = target.user!; let embed = new EmbedBuilder() .setAuthor({ - name: `• ${target.nickname == null ? selectedUser.username : target.nickname}${ - selectedUser.discriminator == "0" ? "" : `#${selectedUser.discriminator}` - }`, + name: `• ${target.nickname ?? selectedUser.displayName}`, iconURL: target.displayAvatarURL() }) .setFields( diff --git a/src/events/guildCreate.ts b/src/events/guildCreate.ts index f950b0d..06fb13b 100644 --- a/src/events/guildCreate.ts +++ b/src/events/guildCreate.ts @@ -17,7 +17,7 @@ export default { .createDM() .catch(() => null)) as DMChannel | null; const embed = new EmbedBuilder() - .setTitle("👋 • Welcome to Nebula!") + .setTitle("👋 • Welcome to Nebula!") .setDescription( [ "Nebula is a multiplatform, multipurpose bot with the ability to add extensions to have additional features.", diff --git a/src/events/guildMemberAdd.ts b/src/events/guildMemberAdd.ts index 75b3600..a49619b 100644 --- a/src/events/guildMemberAdd.ts +++ b/src/events/guildMemberAdd.ts @@ -1,8 +1,12 @@ import { - EmbedBuilder, type Client, type GuildMember, - type TextChannel, type ColorResolvable + EmbedBuilder, + type Client, + type GuildMember, + type TextChannel, + type ColorResolvable } from "discord.js"; -import { genColor, genRGBColor } from "../utils/colorGen.js"; +import { genColor, genRGBColor } from "../utils/colorGen"; +import { getSetting } from "../utils/database/settings"; import Vibrant from "node-vibrant"; import sharp from "sharp"; @@ -10,23 +14,33 @@ export default { name: "guildMemberAdd", event: class GuildMemberAdd { client: Client; - constructor(client: Client) { this.client = client; } async run(member: GuildMember) { - const id = "1079612083307548704"; - const channel = await member.guild.channels.cache.find(channel => channel.id === id)!.fetch() as TextChannel; - const avatarURL = member.displayAvatarURL(); + const guildID = member.guild.id; + const id = getSetting(guildID, "welcome.channel"); + if (!id) return; + const channel = (await member.guild.channels.cache + .find(channel => channel.id === id) + ?.fetch()) as TextChannel; + + let text = getSetting(guildID, "welcome.text"); + if (text?.includes("(username)")) + text = text.replaceAll("(username)", member.user.displayName); + if (text?.includes("(usercount)")) + text = text.replaceAll("(usercount)", `${member.guild.memberCount}`); + + const avatarURL = member.displayAvatarURL(); const embed = new EmbedBuilder() - .setAuthor({ - name: `• ${member.nickname == null ? member.user.username : member.nickname}`, - iconURL: avatarURL - }) - .setTitle("Welcome!") - .setDescription(`Enjoy your stay in **${member.guild.name}**!`) + .setAuthor({ name: `• ${member.user.displayName}`, iconURL: avatarURL }) + .setTitle("👋 • Welcome!") + .setDescription( + text ?? + `Welcome, ${member.user.displayName}! Interestingly, you just helped us reach ${member.guild.memberCount} members. Enjoy, and have a nice day!` + ) .setFooter({ text: `User ID: ${member.id}` }) .setThumbnail(avatarURL) .setColor(genColor(200)); @@ -41,4 +55,4 @@ export default { await channel.send({ embeds: [embed] }); } } -} +}; diff --git a/src/events/guildMemberRemove.ts b/src/events/guildMemberRemove.ts index 39c06e2..1e62368 100644 --- a/src/events/guildMemberRemove.ts +++ b/src/events/guildMemberRemove.ts @@ -1,8 +1,12 @@ import { - EmbedBuilder, type Client, type GuildMember, - type TextChannel, type ColorResolvable + EmbedBuilder, + type Client, + type GuildMember, + type TextChannel, + type ColorResolvable } from "discord.js"; -import { genColor, genRGBColor } from "../utils/colorGen.js"; +import { genColor, genRGBColor } from "../utils/colorGen"; +import { getSetting } from "../utils/database/settings"; import Vibrant from "node-vibrant"; import sharp from "sharp"; @@ -10,23 +14,30 @@ export default { name: "guildMemberRemove", event: class GuildMemberRemove { client: Client; - constructor(client: Client) { this.client = client; } async run(member: GuildMember) { - const id = "1079612083307548704"; - const channel = await member.guild.channels.cache.find(channel => channel.id === id)!.fetch() as TextChannel; - const avatarURL = member.displayAvatarURL(); + const guildID = member.guild.id; + const id = getSetting(guildID, "welcome.channel"); + if (!id) return; + const channel = (await member.guild.channels.cache + .find(channel => channel.id === id) + ?.fetch()) as TextChannel; + + let text = getSetting(guildID, "welcome.goodbyeText"); + if (text?.includes("(username)")) + text = text.replaceAll("(username)", member.user.displayName); + if (text?.includes("(usercount)")) + text = text.replaceAll("(usercount)", `${member.guild.memberCount}`); + + const avatarURL = member.displayAvatarURL(); const embed = new EmbedBuilder() - .setAuthor({ - name: `• ${member.nickname == null ? member.user.username : member.nickname}`, - iconURL: avatarURL - }) - .setTitle("Goodbye!") - .setDescription(`**@${member.user.username}** has left the server 😥`) + .setAuthor({ name: `• ${member.user.displayName}`, iconURL: avatarURL }) + .setTitle("🙋‍♂️ • Goodbye!") + .setDescription(text ?? `**@${member.user.username}** has left the server 😥`) .setFooter({ text: `User ID: ${member.id}` }) .setThumbnail(avatarURL) .setColor(genColor(200)); @@ -41,4 +52,4 @@ export default { await channel.send({ embeds: [embed] }); } } -} \ No newline at end of file +}; diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts index 48e040b..0821a73 100644 --- a/src/events/messageCreate.ts +++ b/src/events/messageCreate.ts @@ -51,8 +51,8 @@ export default { } const embed = new EmbedBuilder() - .setAuthor({ name: author.displayName, iconURL: author.avatarURL() || undefined }) - .setTitle("⚡ • Level Up!") + .setAuthor({ name: `• ${author.displayName}`, iconURL: author.avatarURL() || undefined }) + .setTitle("⚡ • Level Up!") .setDescription( [ `**Congratulations, ${author.displayName}**!`, diff --git a/src/utils/database/settings.ts b/src/utils/database/settings.ts index 8ee948c..2f03cdd 100644 --- a/src/utils/database/settings.ts +++ b/src/utils/database/settings.ts @@ -13,8 +13,8 @@ const tableDefinition = { export const settingsDefinition = { "levelling.enabled": "BOOL", - "levelling.channel": "INTEGER", - "moderation.channel": "INTEGER", + "levelling.channel": "TEXT", + "moderation.channel": "TEXT", "news.channelID": "TEXT", "news.roleID": "TEXT", "news.editOriginalMessage": "BOOL", @@ -22,9 +22,7 @@ export const settingsDefinition = { "serverboard.shown": "BOOL", "welcome.text": "TEXT", "welcome.goodbyeText": "TEXT", - "welcome.channel": "INTEGER", - "welcome.shown": "BOOL", - "welcome.goodbyeShown": "BOOL" + "welcome.channel": "TEXT" } satisfies Record; export const settingsKeys = Object.keys(settingsDefinition) as (keyof typeof settingsDefinition)[]; diff --git a/src/utils/embeds/errorEmbed.ts b/src/utils/embeds/errorEmbed.ts index 2bf2616..214d5f7 100644 --- a/src/utils/embeds/errorEmbed.ts +++ b/src/utils/embeds/errorEmbed.ts @@ -16,7 +16,7 @@ export function errorEmbed( const content = [`**${title}**`]; if (reason != undefined) content.push(reason); const embed = new EmbedBuilder() - .setTitle("❌ • Something went wrong!") + .setTitle("❌ • Something went wrong!") .setDescription(content.join("\n")) .setColor(genColor(0)); diff --git a/src/utils/embeds/serverEmbed.ts b/src/utils/embeds/serverEmbed.ts index 459b28b..cbaf3b5 100644 --- a/src/utils/embeds/serverEmbed.ts +++ b/src/utils/embeds/serverEmbed.ts @@ -49,7 +49,7 @@ export async function serverEmbed(options: Options) { if (options.showInvite && invite !== null) generalValues.push(`**Invite link**: ${invite}`); const embed = new EmbedBuilder() - .setAuthor({ name: `${pages ? `#${page} • ` : ""}${guild.name}`, iconURL: guild.iconURL()! }) + .setAuthor({ name: `• ${pages ? `#${page} • ` : ""}${guild.name}`, iconURL: guild.iconURL()! }) .setDescription(guild.description ? guild.description : null) .setFields({ name: "📃 • General", value: generalValues.join("\n") }) .setFooter({ text: `Server ID: ${guild.id}${pages ? ` • Page ${page}/${pages}` : ""}` }) From abe840ad01b65f93a8235bfa62da1e83b0eb39c6 Mon Sep 17 00:00:00 2001 From: Serge Date: Sat, 24 Feb 2024 14:41:44 +0600 Subject: [PATCH 047/127] message logs --- src/commands/about.ts | 13 +++++---- src/events/easterEggs/Fire.ts | 17 +++++++++++ src/events/guildMemberAdd.ts | 17 +++++------ src/events/guildMemberRemove.ts | 17 +++++------ src/events/messageCreate.ts | 4 +-- src/events/messageDelete.ts | 41 ++++++++++++++++++++++++++ src/events/messageUpdate.ts | 51 +++++++++++++++++++++++++++++++++ src/utils/database/settings.ts | 1 + 8 files changed, 137 insertions(+), 24 deletions(-) create mode 100644 src/events/easterEggs/Fire.ts create mode 100644 src/events/messageDelete.ts create mode 100644 src/events/messageUpdate.ts diff --git a/src/commands/about.ts b/src/commands/about.ts index f4e0961..4921623 100644 --- a/src/commands/about.ts +++ b/src/commands/about.ts @@ -20,9 +20,9 @@ export default class About { const shards = client.shard?.count; const hearts = ["💖", "💝", "💓", "💗", "💘", "💟", "💕", "💞"]; const embed = new EmbedBuilder() - .setAuthor({ name: "• About Nebula", iconURL: client.user.displayAvatarURL() }) + .setAuthor({ name: "• About Sokora", iconURL: client.user.displayAvatarURL() }) .setDescription( - "Nebula is a multiplatform, multipurpose bot with the ability to add extensions to have additional features." + "Sokora is a multiplatform, multipurpose bot with the ability to add extensions to have additional features." ) .setFields( { @@ -38,10 +38,11 @@ export default class About { name: "🌌 • Entities involved", value: [ "**Head developer**: Goos", - "**Developers**: Froxcey, Golem64, Pigpot, ThatBOI", + "**Developers**: Froxcey, Golem64, ThatBOI", "**Designers**: ArtyH, Optix, proJM, Slider_on_the_black", - "**Translators**: Dimkauzh, Golem64, Optix, SaFire, ThatBOI", - "And **YOU**, for using Nebula." + "**Translators**: Dimkauzh, Golem64, Optix, SaFire, ThatBOI, Trynera", + "**Testers**: Blaze, Dimkauzh, fishy, flojo, Trynera", + "And **YOU**, for using Sokora." ].join("\n") }, { @@ -50,7 +51,7 @@ export default class About { "[GitHub](https://www.github.com/NebulaTheBot) • [YouTube](https://www.youtube.com/@NebulaTheBot) • [Instagram](https://instagram.com/NebulaTheBot) • [Mastodon](https://mastodon.online/@NebulaTheBot@mastodon.social) • [Guilded](https://guilded.gg/Nebula) • [Revolt](https://rvlt.gg/28TS9aXy)" } ) - .setFooter({ text: `Made by the Nebula team with ${randomise(hearts)}` }) + .setFooter({ text: `Made by the Sokora team with ${randomise(hearts)}` }) .setThumbnail(client.user.displayAvatarURL()) .setColor(genColor(270)); diff --git a/src/events/easterEggs/Fire.ts b/src/events/easterEggs/Fire.ts new file mode 100644 index 0000000..365777d --- /dev/null +++ b/src/events/easterEggs/Fire.ts @@ -0,0 +1,17 @@ +import type { Message } from "discord.js"; +import { randomise } from "../../utils/randomise"; + +export default class Fire { + async run(message: Message) { + if (message.content.toLowerCase().includes("fire in the hole")) { + const gifs = randomise([ + "https://cdn.discordapp.com/attachments/799130520846991370/1080439680199315487/cat-chomp-fireball.gif?ex=65e8485d&is=65d5d35d&hm=cef8b83df8120f1419082f184d835c6af679c9d02d69f97e335eafa82b33489e&", + "https://tenor.com/view/dancing-gif-25178472", + "https://tenor.com/view/fire-in-the-hole-gif-11283103876805231056", + "https://tenor.com/view/fire-in-the-hole-gif-14799466830322850291" + ]); + + await message.channel.send(gifs); + } + } +} diff --git a/src/events/guildMemberAdd.ts b/src/events/guildMemberAdd.ts index a49619b..6b381a1 100644 --- a/src/events/guildMemberAdd.ts +++ b/src/events/guildMemberAdd.ts @@ -22,24 +22,25 @@ export default { const guildID = member.guild.id; const id = getSetting(guildID, "welcome.channel"); if (!id) return; + + let text = getSetting(guildID, "welcome.text"); + const user = member.user; + const guild = member.guild; const channel = (await member.guild.channels.cache .find(channel => channel.id === id) ?.fetch()) as TextChannel; - let text = getSetting(guildID, "welcome.text"); - if (text?.includes("(username)")) - text = text.replaceAll("(username)", member.user.displayName); - - if (text?.includes("(usercount)")) - text = text.replaceAll("(usercount)", `${member.guild.memberCount}`); + if (text?.includes("(name)")) text = text.replaceAll("(name)", user.displayName); + if (text?.includes("(count)")) text = text.replaceAll("(count)", `${guild.memberCount}`); + if (text?.includes("(servername)")) text = text.replaceAll("(servername)", `${guild.name}`); const avatarURL = member.displayAvatarURL(); const embed = new EmbedBuilder() - .setAuthor({ name: `• ${member.user.displayName}`, iconURL: avatarURL }) + .setAuthor({ name: `• ${user.displayName}`, iconURL: avatarURL }) .setTitle("👋 • Welcome!") .setDescription( text ?? - `Welcome, ${member.user.displayName}! Interestingly, you just helped us reach ${member.guild.memberCount} members. Enjoy, and have a nice day!` + `Welcome to ${guild.name}, **${user.displayName}**! Interestingly, you just helped us reach **${guild.memberCount}** members. Enjoy, and have a nice day!` ) .setFooter({ text: `User ID: ${member.id}` }) .setThumbnail(avatarURL) diff --git a/src/events/guildMemberRemove.ts b/src/events/guildMemberRemove.ts index 1e62368..4dadf8c 100644 --- a/src/events/guildMemberRemove.ts +++ b/src/events/guildMemberRemove.ts @@ -22,22 +22,23 @@ export default { const guildID = member.guild.id; const id = getSetting(guildID, "welcome.channel"); if (!id) return; + + let text = getSetting(guildID, "welcome.goodbyeText"); + const user = member.user; + const guild = member.guild; const channel = (await member.guild.channels.cache .find(channel => channel.id === id) ?.fetch()) as TextChannel; - let text = getSetting(guildID, "welcome.goodbyeText"); - if (text?.includes("(username)")) - text = text.replaceAll("(username)", member.user.displayName); - - if (text?.includes("(usercount)")) - text = text.replaceAll("(usercount)", `${member.guild.memberCount}`); + if (text?.includes("(name)")) text = text.replaceAll("(name)", user.displayName); + if (text?.includes("(count)")) text = text.replaceAll("(count)", `${guild.memberCount}`); + if (text?.includes("(servername)")) text = text.replaceAll("(servername)", `${guild.name}`); const avatarURL = member.displayAvatarURL(); const embed = new EmbedBuilder() - .setAuthor({ name: `• ${member.user.displayName}`, iconURL: avatarURL }) + .setAuthor({ name: `• ${user.displayName}`, iconURL: avatarURL }) .setTitle("🙋‍♂️ • Goodbye!") - .setDescription(text ?? `**@${member.user.username}** has left the server 😥`) + .setDescription(text ?? `**@${user.displayName}** has left the server 😥`) .setFooter({ text: `User ID: ${member.id}` }) .setThumbnail(avatarURL) .setColor(genColor(200)); diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts index 0821a73..a94db27 100644 --- a/src/events/messageCreate.ts +++ b/src/events/messageCreate.ts @@ -12,11 +12,11 @@ export default { event: class MessageCreate { async run(message: Message) { const author = message.author; - const guild = message.guild!; if (author.bot) return; + const guild = message.guild!; // Easter egg handler - if (guild.id === "1079612082636472420") { + if (guild.id === "903852579837059113") { const eventsPath = join(process.cwd(), "src", "events", "easterEggs"); for (const easterEggFile of readdirSync(eventsPath)) diff --git a/src/events/messageDelete.ts b/src/events/messageDelete.ts new file mode 100644 index 0000000..9c05afd --- /dev/null +++ b/src/events/messageDelete.ts @@ -0,0 +1,41 @@ +import { codeBlock, EmbedBuilder, type Message, type TextChannel, type Channel } from "discord.js"; +import { genColor } from "../utils/colorGen"; +import { getSetting } from "../utils/database/settings"; + +export default { + name: "messageDelete", + event: class messageDelete { + async run(message: Message) { + const author = message.author; + if (author.bot) return; + + const guild = message.guild!; + const logUpdate = getSetting(guild.id, "moderation.logMessages"); + const logChannel = getSetting(guild.id, "moderation.channel"); + if (!logUpdate) return; + if (!logChannel) return; + + const embed = new EmbedBuilder() + .setAuthor({ name: `• ${author.displayName}`, iconURL: author.displayAvatarURL() }) + .setTitle("🗑️ • Message has been deleted.") + .addFields({ + name: "🗞️ • Deleted message", + value: codeBlock(message.content) + }) + .setFooter({ text: `Message ID: ${message.id}` }) + .setThumbnail(author.displayAvatarURL()) + .setColor(genColor(60)); + + const channel = await guild.channels.cache + .get(`${logChannel}`) + ?.fetch() + .then((channel: Channel) => { + return channel as TextChannel; + }) + .catch(() => null); + + if (!channel) return; + await channel.send({ embeds: [embed] }); + } + } +}; diff --git a/src/events/messageUpdate.ts b/src/events/messageUpdate.ts new file mode 100644 index 0000000..19a074c --- /dev/null +++ b/src/events/messageUpdate.ts @@ -0,0 +1,51 @@ +import { codeBlock, EmbedBuilder, type Message, type TextChannel, type Channel } from "discord.js"; +import { genColor } from "../utils/colorGen"; +import { getSetting } from "../utils/database/settings"; + +export default { + name: "messageUpdate", + event: class MessageUpdate { + async run(oldMessage: Message, newMessage: Message) { + const author = oldMessage.author; + if (author.bot) return; + + const guild = oldMessage.guild!; + const logUpdate = getSetting(guild.id, "moderation.logMessages"); + const logChannel = getSetting(guild.id, "moderation.channel"); + if (!logUpdate) return; + if (!logChannel) return; + + const oldContent = oldMessage.content; + const newContent = newMessage.content; + if (oldContent === newContent) return; + + const embed = new EmbedBuilder() + .setAuthor({ name: `• ${author.displayName}`, iconURL: author.displayAvatarURL() }) + .setTitle("✍ • Message has been edited.") + .addFields( + { + name: "🕰️ • Old message", + value: codeBlock(oldContent) + }, + { + name: "🔄️ • New message", + value: codeBlock(newContent) + } + ) + .setFooter({ text: `Message ID: ${oldMessage.id}` }) + .setThumbnail(author.displayAvatarURL()) + .setColor(genColor(60)); + + const channel = await guild.channels.cache + .get(`${logChannel}`) + ?.fetch() + .then((channel: Channel) => { + return channel as TextChannel; + }) + .catch(() => null); + + if (!channel) return; + await channel.send({ embeds: [embed] }); + } + } +}; diff --git a/src/utils/database/settings.ts b/src/utils/database/settings.ts index 2f03cdd..1287577 100644 --- a/src/utils/database/settings.ts +++ b/src/utils/database/settings.ts @@ -15,6 +15,7 @@ export const settingsDefinition = { "levelling.enabled": "BOOL", "levelling.channel": "TEXT", "moderation.channel": "TEXT", + "moderation.logMessages": "BOOL", "news.channelID": "TEXT", "news.roleID": "TEXT", "news.editOriginalMessage": "BOOL", From 45e0fe98ef95baf45577f962ff18bebe9547a93a Mon Sep 17 00:00:00 2001 From: ThatBoiDev Date: Mon, 26 Feb 2024 19:33:46 +0100 Subject: [PATCH 048/127] todoitem: added a setting and started on LIST type --- src/events/messageReactionCreate.ts | 2 ++ src/utils/database/settings.ts | 4 ++++ src/utils/database/types.ts | 3 ++- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/events/messageReactionCreate.ts b/src/events/messageReactionCreate.ts index 31cdf26..8f38453 100644 --- a/src/events/messageReactionCreate.ts +++ b/src/events/messageReactionCreate.ts @@ -1,5 +1,6 @@ import type { Client, Guild, User, MessageReaction } from "discord.js"; import { getPolls } from "../utils/database/poll"; +import { getSetting } from "../utils/database/settings"; export default { name: "messageReactionCreate", @@ -10,6 +11,7 @@ export default { } async run(guild: Guild, user: User, reaction: MessageReaction) { + if (getSetting(guild.id, "poll.voteOnOneOption") == false) return; if (user.bot) return; const messageIds = getPolls(guild.id); diff --git a/src/utils/database/settings.ts b/src/utils/database/settings.ts index 1287577..f7afde8 100644 --- a/src/utils/database/settings.ts +++ b/src/utils/database/settings.ts @@ -14,6 +14,8 @@ const tableDefinition = { export const settingsDefinition = { "levelling.enabled": "BOOL", "levelling.channel": "TEXT", + "levelling.blockChannelIds": "LIST", // TODO: Add this to the levelling command + "poll.voteOnOneOption": "BOOL", "moderation.channel": "TEXT", "moderation.logMessages": "BOOL", "news.channelID": "TEXT", @@ -50,6 +52,8 @@ export function getSetting( return res[0].value as TypeOfKey; case "BOOL": return (res[0].value == "true") as TypeOfKey; + case "LIST": + return (res[0].value.split(",") as unknown) as TypeOfKey; // TODO: Make this type usable default: // TODO: Implement more data types return "WIP" as TypeOfKey; diff --git a/src/utils/database/types.ts b/src/utils/database/types.ts index 38f4c08..b3623a2 100644 --- a/src/utils/database/types.ts +++ b/src/utils/database/types.ts @@ -1,4 +1,4 @@ -export type FieldData = "TEXT" | "INTEGER" | "FLOAT" | "BOOL" | "TIMESTAMP" | "JSON"; +export type FieldData = "TEXT" | "LIST" | "INTEGER" | "FLOAT" | "BOOL" | "TIMESTAMP" | "JSON"; export type TableDefinition = { name: string; @@ -10,6 +10,7 @@ export type SqlType = { INTEGER: number; FLOAT: number; TEXT: string; + LIST: string[]; TIMESTAMP: Date; JSON: any; }[T]; From 840c4edcff698dc6497c4c99d1fc483fdf47192c Mon Sep 17 00:00:00 2001 From: Serge Date: Thu, 14 Mar 2024 20:26:08 +0500 Subject: [PATCH 049/127] small commit --- bun.lockb | Bin 59784 -> 59352 bytes package.json | 4 +- src/commands/about.ts | 17 ++-- src/commands/moderation/ban.ts | 4 +- src/commands/moderation/delwarn.ts | 4 +- src/commands/moderation/kick.ts | 4 +- src/commands/moderation/mute.ts | 4 +- src/commands/moderation/purge.ts | 2 +- src/commands/moderation/unban.ts | 4 +- src/commands/moderation/unmute.ts | 4 +- src/commands/moderation/warn.ts | 4 +- src/commands/moderation/warns.ts | 2 +- src/commands/poll.ts | 2 +- src/commands/server/settings.ts | 2 +- src/commands/user.ts | 145 ++++++++++++++++++----------- src/events/error.ts | 15 +++ src/events/guildCreate.ts | 15 +-- src/events/guildMemberAdd.ts | 23 +---- src/events/guildMemberRemove.ts | 23 +---- src/events/messageCreate.ts | 2 +- src/events/messageDelete.ts | 6 +- src/events/messageUpdate.ts | 4 +- src/handlers/commands.ts | 1 - src/utils/database/levelling.ts | 7 +- src/utils/database/settings.ts | 6 +- src/utils/embeds/errorEmbed.ts | 2 +- src/utils/embeds/serverEmbed.ts | 22 ++--- src/utils/imageColor.ts | 13 +++ 28 files changed, 189 insertions(+), 152 deletions(-) create mode 100644 src/events/error.ts create mode 100644 src/utils/imageColor.ts diff --git a/bun.lockb b/bun.lockb index f63c8129d5e73add0bb7e0b6f402be103abb4960..5bd5df125a85deff995c6a893c868e2b29418fad 100644 GIT binary patch delta 867 zcmXw$3rJI86vyw~?v`Gc&3t5P4=r;kUN@&DBD9HuP|~2(9vDi6HY}|)Os7r?aw(O5 zO3rPlGYQKwyNYvYDFsC`AI(7X!SaD-l0;ylBArj?AAaBY?z!iD_uT(iV&#quaGx+o z)4hMLx8yfU^@+_TskhC4)p_Q?1ef?LqVa^Wj4|?M+#Io~n)0LroVV4jxq}p|;K3rI z$NfnP$=!oc7VUr{#6ogElF$Jt%gs?3Oc0R-p>~5-C%_9{1fhdj6uTnsqIeugEwR1f zecM}YBo!uG%d`z9Wemq$Jz>mH#t3)jj^-f2s<}y-dj4>9{nRN}zk(^}IJ&*7&2c(; zhO(sHzxAy-XI3g1x?Z)KZnU6@8=gf21*`LK+RBHUs6(;cmh}~DMJeTFjIJ$xUCV#p z@L|!gd|{;hg%exsLN9b^GHuKITLS|c<(?s~@u!ckRT^P3lGxa_K@x-(_S>Q^bIWw1 zcsfS|YYg03Ym0N!Bt8D)Vtl{aM=$N275Aog?DZ@@w{-7eh2^ooY)1CakUc}4 z94)O?AQx7y8X&2R{gfU=a;Yr9gCs2u6uEt3GDqD#cdI**k8?mQNvp zO2jHuaXjq1YY0-PqEMUwr~LNOE7e=WHfvF*R)*HKQANM)<0FXhLj(nM+xM0B$bx?Nk}zUMDn>)~9^5b{6fM-JI^fx{*fU1W9^b-HF(Z*MQnf!p;NFJ5>bted zuh%$aVFVM-NW>`FP^8`K3Q{GY5IvjP!TfDp78GSbD^+$@OOCOjwZSSc^cjhOrs{8SR0SsnO z43}qGKXYTn1s2xVnhXp)n_XEivv6?c+Z=lpIBWN239dV;lTCC@CTkc7FiK8-Sgb7w z($}WTz~IQh(7+00%S<*D)t;Qd#ld?VC~6556#$QE zZk}s!oP`%fc(bJyi;2V1(m89__*{D9;QiY70^@#`*vY3>wp;sFr2BmdI#jnL`o>O? zRUa1q;Y(dPck8`N6Zv^cZ-y7fFm6ieGOAo~Q9XKcZJtE^x0tv<`EzQgpQhBYS!Ty| zuou45)b<$-RtX~sxKrt*L=cV=L2co@4c87?M-jDnY&?H-+wKT z3mN|L|6^oem7fdy?^DA-UTFu8zHN(XMo^nga%HLY=8S4D z*~tq^4Y__m3#1pbChwWyF!|>U`}(&~Lni}iV2K5`?HyEX3RDbKO7DfTr$T8qAblK& zL0}pXgK`c#5T5{I5SRhPATbUgz6Zo07lTSIkR8BW$nXY;f#zB>%!UdAvmwJ;AO?Xs zKnzmI4a7@<7zE}5F-S}TqyY#f%$@8vM+sC3Oujx-k@5ZHuQQi#UNEbLk@3T1#W~Va z_kl7XJPC-G1MvZn0YK2Q5T9Px`BAdLLdPIM!P0kEzp{rus~ee z$Q0~kP(d+YwLwR_H8qKYG0s%aSkHL!qy>pUwt=3pv7XW9-wQg$C;!{VF?rHvAs{WK z15{*Wrf0$ct{Q>bVf7*tV;xkcVHd=@1G^@l+Tk);V5bAu2dL187n`GYX6Z5+>rEEC vqd58bGf5^R)5#zA$xoj2Sf0^ha^qg5$pQC-CMP^*V>Q<+Nw1nbv2+Fi?;$># diff --git a/package.json b/package.json index a601376..9a20dda 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "sharp": "^0.33.2" }, "devDependencies": { - "bun-types": "^1.0.25", - "typescript": "^5.3.3" + "bun-types": "^1.0.30", + "typescript": "^5.4.2" } } diff --git a/src/commands/about.ts b/src/commands/about.ts index 4921623..523c6bb 100644 --- a/src/commands/about.ts +++ b/src/commands/about.ts @@ -17,19 +17,22 @@ export default class About { async run(interaction: ChatInputCommandInteraction) { const client = interaction.client; const guilds = client.guilds.cache; + const members = guilds.map(guild => guild.memberCount).reduce((a, b) => a + b); const shards = client.shard?.count; - const hearts = ["💖", "💝", "💓", "💗", "💘", "💟", "💕", "💞"]; + let emojis = ["💖", "💝", "💓", "💗", "💘", "💟", "💕", "💞"]; + if (Math.round(Math.random() * 100) <= 5) emojis = ["⌨️", "💻", "🖥️"]; + const embed = new EmbedBuilder() .setAuthor({ name: "• About Sokora", iconURL: client.user.displayAvatarURL() }) .setDescription( - "Sokora is a multiplatform, multipurpose bot with the ability to add extensions to have additional features." + "Sokora is a multipurpose Discord bot that lets you manage your servers easily." ) .setFields( { name: "📃 • General", value: [ - "**Version** 0.1-pre", - `**${guilds.size}** guild${guilds.size === 1 ? "" : "s"} ${ + "**Version** 0.1, *the Antei update*", + `**${members}** members • **${guilds.size}** guild${guilds.size === 1 ? "" : "s"} ${ shards == undefined ? "" : `• **${shards}** shard${shards === 1 ? "" : "s"}` }` ].join("\n") @@ -38,10 +41,10 @@ export default class About { name: "🌌 • Entities involved", value: [ "**Head developer**: Goos", - "**Developers**: Froxcey, Golem64, ThatBOI", + "**Developers**: Dimkauzh, Froxcey, Golem64, Spectrum, ThatBOI", "**Designers**: ArtyH, Optix, proJM, Slider_on_the_black", "**Translators**: Dimkauzh, Golem64, Optix, SaFire, ThatBOI, Trynera", - "**Testers**: Blaze, Dimkauzh, fishy, flojo, Trynera", + "**Testers**: Blaze, fishy, flojo, Trynera", "And **YOU**, for using Sokora." ].join("\n") }, @@ -51,7 +54,7 @@ export default class About { "[GitHub](https://www.github.com/NebulaTheBot) • [YouTube](https://www.youtube.com/@NebulaTheBot) • [Instagram](https://instagram.com/NebulaTheBot) • [Mastodon](https://mastodon.online/@NebulaTheBot@mastodon.social) • [Guilded](https://guilded.gg/Nebula) • [Revolt](https://rvlt.gg/28TS9aXy)" } ) - .setFooter({ text: `Made by the Sokora team with ${randomise(hearts)}` }) + .setFooter({ text: `Made with ${randomise(emojis)} by the Sokora team` }) .setThumbnail(client.user.displayAvatarURL()) .setColor(genColor(270)); diff --git a/src/commands/moderation/ban.ts b/src/commands/moderation/ban.ts index 66e0cec..473e8ec 100644 --- a/src/commands/moderation/ban.ts +++ b/src/commands/moderation/ban.ts @@ -62,7 +62,7 @@ export default class Ban { const reason = interaction.options.getString("reason"); const embed = new EmbedBuilder() .setAuthor({ name: `• ${name}`, iconURL: user.displayAvatarURL() }) - .setTitle(`✅ • Banned ${name}`) + .setTitle(`Banned ${name}.`) .setDescription( [ `**Moderator**: ${interaction.user.displayName}`, @@ -94,7 +94,7 @@ export default class Ban { if (!dmChannel) return; if (user.bot) return; await dmChannel.send({ - embeds: [embed.setTitle("🔨 • You were banned").setColor(genColor(0))] + embeds: [embed.setTitle("You got banned.").setColor(genColor(0))] }); } } diff --git a/src/commands/moderation/delwarn.ts b/src/commands/moderation/delwarn.ts index 517cb70..a9c628d 100644 --- a/src/commands/moderation/delwarn.ts +++ b/src/commands/moderation/delwarn.ts @@ -68,7 +68,7 @@ export default class Delwarn { const embed = new EmbedBuilder() .setAuthor({ name: `• ${name}`, iconURL: user.displayAvatarURL() }) - .setTitle(`✅ • Removed warning from ${name}`) + .setTitle(`Removed a warning from ${name}.`) .setDescription(`**Moderator**: ${interaction.user.displayName}`) .setThumbnail(user.displayAvatarURL()) .setFooter({ text: `User ID: ${user.id}` }) @@ -94,6 +94,6 @@ export default class Delwarn { const dmChannel = (await user.createDM().catch(() => null)) as DMChannel | null; if (!dmChannel) return; if (user.bot) return; - await dmChannel.send({ embeds: [embed.setTitle("🤝 • Your warning was removed")] }); + await dmChannel.send({ embeds: [embed.setTitle("Your warning has been removed.")] }); } } diff --git a/src/commands/moderation/kick.ts b/src/commands/moderation/kick.ts index 0af99d5..186373e 100644 --- a/src/commands/moderation/kick.ts +++ b/src/commands/moderation/kick.ts @@ -62,7 +62,7 @@ export default class Kick { const reason = interaction.options.getString("reason"); const embed = new EmbedBuilder() .setAuthor({ name: `• ${name}`, iconURL: user.displayAvatarURL() }) - .setTitle(`✅ • Kicked ${name}`) + .setTitle(`Kicked ${name}.`) .setDescription( [ `**Moderator**: ${interaction.user.displayName}`, @@ -94,7 +94,7 @@ export default class Kick { if (!dmChannel) return; if (user.bot) return; await dmChannel.send({ - embeds: [embed.setTitle("🥾 • You were kicked").setColor(genColor(0))] + embeds: [embed.setTitle("You got kicked.").setColor(genColor(0))] }); } } diff --git a/src/commands/moderation/mute.ts b/src/commands/moderation/mute.ts index 61cf934..b3f39d3 100644 --- a/src/commands/moderation/mute.ts +++ b/src/commands/moderation/mute.ts @@ -79,7 +79,7 @@ export default class Mute { ).toISOString(); const embed = new EmbedBuilder() .setAuthor({ name: `• ${name}`, iconURL: user.displayAvatarURL() }) - .setTitle(`✅ • Muted ${name}`) + .setTitle(`Muted ${name}.`) .setDescription( [ `**Moderator**: ${interaction.user.displayName}`, @@ -112,7 +112,7 @@ export default class Mute { if (!dmChannel) return; if (user.bot) return; await dmChannel.send({ - embeds: [embed.setTitle("🤐 • You were muted").setColor(genColor(0))] + embeds: [embed.setTitle("You got muted.").setColor(genColor(0))] }); } } diff --git a/src/commands/moderation/purge.ts b/src/commands/moderation/purge.ts index eeaa8ca..cdb9799 100644 --- a/src/commands/moderation/purge.ts +++ b/src/commands/moderation/purge.ts @@ -55,7 +55,7 @@ export default class Purge { const channelOption = interaction.options.getChannel("channel")!; const channel = guild.channels.cache.get(interaction.channel?.id ?? channelOption.id)!; const embed = new EmbedBuilder() - .setTitle(`✅ • Purged ${amount} messages.`) + .setTitle(`Purged ${amount} message${amount == 1 ? "" : "s"}.`) .setDescription( [ `**Moderator**: ${interaction.user.username}`, diff --git a/src/commands/moderation/unban.ts b/src/commands/moderation/unban.ts index 43b0086..89bc1ad 100644 --- a/src/commands/moderation/unban.ts +++ b/src/commands/moderation/unban.ts @@ -46,7 +46,7 @@ export default class Unban { const embed = new EmbedBuilder() .setAuthor({ name: `• ${target.displayName}`, iconURL: target.displayAvatarURL() }) - .setTitle(`✅ • Unbanned ${target.displayName}`) + .setTitle(`Unbanned ${target.displayName}.`) .setDescription(`**Moderator**: ${interaction.user.displayName}`) .setThumbnail(target.displayAvatarURL()) .setFooter({ text: `User ID: ${id}` }) @@ -72,6 +72,6 @@ export default class Unban { const dmChannel = (await target.createDM().catch(() => null)) as DMChannel | null; if (!dmChannel) return; if (target.bot) return; - await dmChannel.send({ embeds: [embed.setTitle("🤝 • You were unbanned")] }); + await dmChannel.send({ embeds: [embed.setTitle("You got unbanned.")] }); } } diff --git a/src/commands/moderation/unmute.ts b/src/commands/moderation/unmute.ts index a4aada4..918d3fc 100644 --- a/src/commands/moderation/unmute.ts +++ b/src/commands/moderation/unmute.ts @@ -43,7 +43,7 @@ export default class Unmute { const embed = new EmbedBuilder() .setAuthor({ name: `• ${user.displayName}`, iconURL: user.displayAvatarURL() }) - .setTitle(`✅ • Unmuted ${user.displayName}`) + .setTitle(`Unmuted ${user.displayName}.`) .setDescription( [ `**Moderator**: ${interaction.user.displayName}`, @@ -74,6 +74,6 @@ export default class Unmute { const dmChannel = (await user.createDM().catch(() => null)) as DMChannel | null; if (!dmChannel) return; if (user.bot) return; - await dmChannel.send({ embeds: [embed.setTitle("🤝 • You were unmuted")] }); + await dmChannel.send({ embeds: [embed.setTitle("You got unmuted.")] }); } } diff --git a/src/commands/moderation/warn.ts b/src/commands/moderation/warn.ts index b153f2a..124faaf 100644 --- a/src/commands/moderation/warn.ts +++ b/src/commands/moderation/warn.ts @@ -63,7 +63,7 @@ export default class Warn { const reason = interaction.options.getString("reason"); const embed = new EmbedBuilder() .setAuthor({ name: `• ${name}`, iconURL: user.displayAvatarURL() }) - .setTitle(`✅ • Warned ${name}`) + .setTitle(`Warned ${name}.`) .setDescription( [ `**Moderator**: ${interaction.user.displayName}`, @@ -95,7 +95,7 @@ export default class Warn { if (!dmChannel) return; if (user.bot) return; await dmChannel.send({ - embeds: [embed.setTitle("⚠️ • You were warned").setColor(genColor(0))] + embeds: [embed.setTitle("You got warned.").setColor(genColor(0))] }); } } diff --git a/src/commands/moderation/warns.ts b/src/commands/moderation/warns.ts index d70b562..623049a 100644 --- a/src/commands/moderation/warns.ts +++ b/src/commands/moderation/warns.ts @@ -36,7 +36,7 @@ export default class Warns { const warns = listUserModeration(guild.id, user.id, "WARN"); const embed = new EmbedBuilder() .setAuthor({ name: `• ${user.displayName}`, iconURL: user.displayAvatarURL() }) - .setTitle(`✅ • Warns of ${user.displayName}`) + .setTitle(`Warns of ${user.displayName}.`) .setFields( warns.length > 0 ? warns.map(warn => { diff --git a/src/commands/poll.ts b/src/commands/poll.ts index 65925f8..ac64b83 100644 --- a/src/commands/poll.ts +++ b/src/commands/poll.ts @@ -88,7 +88,7 @@ export default class Poll { if (image) embed.setImage(image.url); if (channel) { const successEmbed = new EmbedBuilder() - .setTitle("✅ • Poll has been created successfully") + .setTitle("Poll has been created.") .setDescription(`Poll is sent to ${channel}.`) .setColor(genColor(100)); diff --git a/src/commands/server/settings.ts b/src/commands/server/settings.ts index 503020c..add6639 100644 --- a/src/commands/server/settings.ts +++ b/src/commands/server/settings.ts @@ -43,7 +43,7 @@ export default class ServerInfo { ); const embed = new EmbedBuilder() - .setTitle(`✅ • \`${key}\` has been set to \`${value}\``) + .setTitle(`\`${key}\` has been set to \`${value}\`.`) .setColor(genColor(100)); setSetting(interaction.guildId!, key, value as keyof typeof settingsDefinition); diff --git a/src/commands/user.ts b/src/commands/user.ts index fc05884..ab5e491 100644 --- a/src/commands/user.ts +++ b/src/commands/user.ts @@ -1,15 +1,19 @@ import { SlashCommandSubcommandBuilder, EmbedBuilder, - type ColorResolvable, - type ChatInputCommandInteraction + ButtonBuilder, + ButtonStyle, + ButtonInteraction, + ActionRowBuilder, + ComponentType, + type ChatInputCommandInteraction, + type Role } from "discord.js"; -import { genColor, genRGBColor } from "../utils/colorGen"; +import { genColor } from "../utils/colorGen"; import { get as getLevelRewards } from "../utils/database/levelRewards"; import { getSetting } from "../utils/database/settings"; import { getLevel, setLevel } from "../utils/database/levelling"; -import Vibrant from "node-vibrant"; -import sharp from "sharp"; +import { imageColor } from "../utils/imageColor"; export default class User { data: SlashCommandSubcommandBuilder; @@ -38,67 +42,25 @@ export default class User { { name: selectedUser?.bot === false ? "👤 • User info" : "🤖 • Bot info", value: [ - `**Username**: ${selectedUser.username}`, - `**Display name**: ${ + `Username's **${selectedUser.username}**`, + `Display name's ${ selectedUser.displayName === selectedUser.username - ? "*None*" - : selectedUser.displayName + ? "*not there*" + : `**${selectedUser.displayName}**` }`, - `**Created on** ` - ].join("\n"), - inline: true + `Created on ****` + ].join("\n") }, { name: "👥 • Member info", - value: `**Joined on** `, - inline: true + value: `Joined on ****` } ) .setFooter({ text: `User ID: ${target.id}` }) .setThumbnail(target.displayAvatarURL()!) .setColor(genColor(200)); - try { - const imageBuffer = await (await fetch(target.displayAvatarURL()!)).arrayBuffer(); - const image = sharp(imageBuffer).toFormat("jpg"); - const { r, g, b } = (await new Vibrant(await image.toBuffer()).getPalette()).Vibrant!; - embed.setColor(genRGBColor(r, g, b) as ColorResolvable); - } catch {} - - if (getSetting(`${guild.id}`, "levelling.enabled")) { - const [guildExp, guildLevel] = getLevel(`${guild.id}`, `${target.id}`)!; - if (!guildExp && !guildLevel) setLevel(`${guild.id}`, `${target.id}`, 0, 0); - - const formattedExpUntilLevelup = Math.floor( - 100 * 1.25 * ((guildLevel ?? 0) + 1) - )?.toLocaleString("en-US"); - let rewards = []; - let nextReward; - - for (const { roleID, level } of getLevelRewards(`${guild.id}`)) { - if (guildLevel < level) { - if (nextReward) break; - nextReward = { roleID, level }; - break; - } - - rewards.push(await guild.roles.fetch(`${roleID}`)?.catch(() => {})); - } - - embed.addFields({ - name: `⚡ • Level ${guildLevel ?? 0}`, - value: [ - `**${guildExp.toLocaleString("en-US") ?? 0}/${formattedExpUntilLevelup}** EXP`, - `**Next level**: ${(guildLevel ?? 0) + 1}`, - `${ - rewards.length > 0 - ? rewards.map(reward => `<@&${reward?.id}>`).join(" ") - : "*No rewards unlocked*" - }` - ].join("\n") - }); - } - + imageColor(embed, undefined, target); const guildRoles = guild.roles.cache.filter(role => target.roles.cache.has(role.id))!; const memberRoles = [...guildRoles].sort( (role1, role2) => role2[1].position - role1[1].position @@ -116,6 +78,77 @@ export default class User { .join(", ")}${memberRoles.length > 5 ? ` **and ${memberRoles.length - 5} more**` : ""}` }); - await interaction.reply({ embeds: [embed] }); + if (!getSetting(`${guild.id}`, "levelling.enabled")) + await interaction.reply({ embeds: [embed] }); + + const [guildExp, guildLevel] = getLevel(`${guild.id}`, `${target.id}`)!; + if (!guildExp && !guildLevel) setLevel(`${guild.id}`, `${target.id}`, 0, 0); + + const formattedExpUntilLevelup = Math.floor( + 100 * 1.25 * ((guildLevel ?? 0) + 1) + )?.toLocaleString("en-US"); + let rewards: (void | Role | null)[] = []; + let nextReward; + + for (const { roleID, level } of getLevelRewards(`${guild.id}`)) { + if (guildLevel < level) { + if (nextReward) break; + nextReward = { roleID, level }; + break; + } + + rewards.push(await guild.roles.fetch(`${roleID}`)?.catch(() => {})); + } + + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId("general") + .setLabel("• General") + .setEmoji("📃") + .setStyle(ButtonStyle.Primary), + new ButtonBuilder() + .setCustomId("level") + .setLabel("• Level") + .setEmoji("⚡") + .setStyle(ButtonStyle.Primary) + ); + + const reply = await interaction.reply({ embeds: [embed], components: [row] }); + reply + .createMessageComponentCollector({ componentType: ComponentType.Button, time: 60000 }) + .on("collect", async (i: ButtonInteraction) => { + let testEmbed: EmbedBuilder = new EmbedBuilder(); + const levelEmbed = new EmbedBuilder() + .setAuthor({ + name: `• ${target.nickname ?? selectedUser.displayName}`, + iconURL: target.displayAvatarURL() + }) + .setFields({ + name: `⚡ • Level ${guildLevel ?? 0}`, + value: [ + `**${guildExp.toLocaleString("en-US") ?? 0}/${formattedExpUntilLevelup}** EXP`, + `**Next level**: ${(guildLevel ?? 0) + 1}`, + `${ + rewards.length > 0 + ? rewards.map(reward => `<@&${reward?.id}>`).join(" ") + : "*No rewards unlocked*" + }` + ].join("\n") + }) + .setFooter({ text: `User ID: ${target.id}` }) + .setThumbnail(target.displayAvatarURL()!) + .setColor(genColor(200)); + + imageColor(levelEmbed, undefined, target); + switch (i.customId) { + case "general": + testEmbed = embed; + case "level": + testEmbed = levelEmbed; + } + + await reply.edit({ embeds: [testEmbed], components: [row] }); + i.update({}); + }); } } diff --git a/src/events/error.ts b/src/events/error.ts new file mode 100644 index 0000000..9805583 --- /dev/null +++ b/src/events/error.ts @@ -0,0 +1,15 @@ +import { DiscordErrorData, type Client } from "discord.js"; + +export default { + name: "error", + event: class Error { + client: Client; + constructor(client: Client) { + this.client = client; + } + + async run(error: DiscordErrorData) { + console.error(error.message); + } + } +}; diff --git a/src/events/guildCreate.ts b/src/events/guildCreate.ts index 06fb13b..9e2bf5a 100644 --- a/src/events/guildCreate.ts +++ b/src/events/guildCreate.ts @@ -12,20 +12,23 @@ export default { } async run(guild: Guild) { - const hearts = ["💖", "💝", "💓", "💗", "💘", "💟", "💕", "💞"]; const dmChannel = (await (await guild.fetchOwner()) .createDM() - .catch(() => null)) as DMChannel | null; + .catch(() => null)) as DMChannel | undefined; + + let emojis = ["💖", "💝", "💓", "💗", "💘", "💟", "💕", "💞"]; + if (Math.round(Math.random() * 100) <= 5) emojis = ["⌨️", "💻", "🖥️"]; + const embed = new EmbedBuilder() - .setTitle("👋 • Welcome to Nebula!") + .setTitle("Welcome to Sokora!") .setDescription( [ - "Nebula is a multiplatform, multipurpose bot with the ability to add extensions to have additional features.", + "Sokora is a multipurpose Discord bot that lets you manage your servers easily.", "To manage the bot, use the **/server settings** command.", - "Nebula is in an early stage of development. If you find bugs, please go to our [official server](https://discord.gg/c6C25P4BuY) and report them." + "Sokora is in an early stage of development. If you find bugs, please go to our [official server](https://discord.gg/c6C25P4BuY) and report them." ].join("\n\n") ) - .setFooter({ text: `Made by the Nebula team with ${randomise(hearts)}` }) + .setFooter({ text: `Made with ${randomise(emojis)} by the Sokora team` }) .setColor(genColor(200)); await new Commands(guild.client).registerCommandsForGuild(guild); diff --git a/src/events/guildMemberAdd.ts b/src/events/guildMemberAdd.ts index 6b381a1..65cccef 100644 --- a/src/events/guildMemberAdd.ts +++ b/src/events/guildMemberAdd.ts @@ -1,14 +1,7 @@ -import { - EmbedBuilder, - type Client, - type GuildMember, - type TextChannel, - type ColorResolvable -} from "discord.js"; -import { genColor, genRGBColor } from "../utils/colorGen"; +import { EmbedBuilder, type Client, type GuildMember, type TextChannel } from "discord.js"; +import { genColor } from "../utils/colorGen"; import { getSetting } from "../utils/database/settings"; -import Vibrant from "node-vibrant"; -import sharp from "sharp"; +import { imageColor } from "../utils/imageColor"; export default { name: "guildMemberAdd", @@ -37,7 +30,7 @@ export default { const avatarURL = member.displayAvatarURL(); const embed = new EmbedBuilder() .setAuthor({ name: `• ${user.displayName}`, iconURL: avatarURL }) - .setTitle("👋 • Welcome!") + .setTitle("Welcome!") .setDescription( text ?? `Welcome to ${guild.name}, **${user.displayName}**! Interestingly, you just helped us reach **${guild.memberCount}** members. Enjoy, and have a nice day!` @@ -46,13 +39,7 @@ export default { .setThumbnail(avatarURL) .setColor(genColor(200)); - try { - const imageBuffer = await (await fetch(avatarURL)).arrayBuffer(); - const image = sharp(imageBuffer).toFormat("jpg"); - const { r, g, b } = (await new Vibrant(await image.toBuffer()).getPalette()).Vibrant!; - embed.setColor(genRGBColor(r, g, b) as ColorResolvable); - } catch {} - + imageColor(embed, undefined, member); await channel.send({ embeds: [embed] }); } } diff --git a/src/events/guildMemberRemove.ts b/src/events/guildMemberRemove.ts index 4dadf8c..fbf9f4f 100644 --- a/src/events/guildMemberRemove.ts +++ b/src/events/guildMemberRemove.ts @@ -1,14 +1,7 @@ -import { - EmbedBuilder, - type Client, - type GuildMember, - type TextChannel, - type ColorResolvable -} from "discord.js"; -import { genColor, genRGBColor } from "../utils/colorGen"; +import { EmbedBuilder, type Client, type GuildMember, type TextChannel } from "discord.js"; +import { genColor } from "../utils/colorGen"; import { getSetting } from "../utils/database/settings"; -import Vibrant from "node-vibrant"; -import sharp from "sharp"; +import { imageColor } from "../utils/imageColor"; export default { name: "guildMemberRemove", @@ -37,19 +30,13 @@ export default { const avatarURL = member.displayAvatarURL(); const embed = new EmbedBuilder() .setAuthor({ name: `• ${user.displayName}`, iconURL: avatarURL }) - .setTitle("🙋‍♂️ • Goodbye!") + .setTitle("Goodbye!") .setDescription(text ?? `**@${user.displayName}** has left the server 😥`) .setFooter({ text: `User ID: ${member.id}` }) .setThumbnail(avatarURL) .setColor(genColor(200)); - try { - const imageBuffer = await (await fetch(avatarURL)).arrayBuffer(); - const image = sharp(imageBuffer).toFormat("jpg"); - const { r, g, b } = (await new Vibrant(await image.toBuffer()).getPalette()).Vibrant!; - embed.setColor(genRGBColor(r, g, b) as ColorResolvable); - } catch {} - + imageColor(embed, undefined, member); await channel.send({ embeds: [embed] }); } } diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts index a94db27..93f32c1 100644 --- a/src/events/messageCreate.ts +++ b/src/events/messageCreate.ts @@ -52,7 +52,7 @@ export default { const embed = new EmbedBuilder() .setAuthor({ name: `• ${author.displayName}`, iconURL: author.avatarURL() || undefined }) - .setTitle("⚡ • Level Up!") + .setTitle("Level up!") .setDescription( [ `**Congratulations, ${author.displayName}**!`, diff --git a/src/events/messageDelete.ts b/src/events/messageDelete.ts index 9c05afd..6df0fea 100644 --- a/src/events/messageDelete.ts +++ b/src/events/messageDelete.ts @@ -4,7 +4,7 @@ import { getSetting } from "../utils/database/settings"; export default { name: "messageDelete", - event: class messageDelete { + event: class MessageDelete { async run(message: Message) { const author = message.author; if (author.bot) return; @@ -17,12 +17,12 @@ export default { const embed = new EmbedBuilder() .setAuthor({ name: `• ${author.displayName}`, iconURL: author.displayAvatarURL() }) - .setTitle("🗑️ • Message has been deleted.") + .setTitle("Message has been deleted.") .addFields({ name: "🗞️ • Deleted message", value: codeBlock(message.content) }) - .setFooter({ text: `Message ID: ${message.id}` }) + .setFooter({ text: `Message ID: ${message.id}\nUser ID: ${message.author.id}` }) .setThumbnail(author.displayAvatarURL()) .setColor(genColor(60)); diff --git a/src/events/messageUpdate.ts b/src/events/messageUpdate.ts index 19a074c..0430cbc 100644 --- a/src/events/messageUpdate.ts +++ b/src/events/messageUpdate.ts @@ -21,7 +21,7 @@ export default { const embed = new EmbedBuilder() .setAuthor({ name: `• ${author.displayName}`, iconURL: author.displayAvatarURL() }) - .setTitle("✍ • Message has been edited.") + .setTitle("Message has been edited.") .addFields( { name: "🕰️ • Old message", @@ -32,7 +32,7 @@ export default { value: codeBlock(newContent) } ) - .setFooter({ text: `Message ID: ${oldMessage.id}` }) + .setFooter({ text: `Message ID: ${oldMessage.id}\nUser ID: ${oldMessage.author.id}` }) .setThumbnail(author.displayAvatarURL()) .setColor(genColor(60)); diff --git a/src/handlers/commands.ts b/src/handlers/commands.ts index ef126f0..c3cbcfe 100644 --- a/src/handlers/commands.ts +++ b/src/handlers/commands.ts @@ -114,7 +114,6 @@ export default class Commands { await this.loadCommands(); const guilds = this.client.guilds.cache; - console.log("Adding commands to guilds..."); for (const guildID of guilds.keys()) { const disabledCommands = getDisabledCommands(guildID); if (disabledCommands.length > 0) await this.loadCommands(...disabledCommands); diff --git a/src/utils/database/levelling.ts b/src/utils/database/levelling.ts index 71c577c..fc70580 100644 --- a/src/utils/database/levelling.ts +++ b/src/utils/database/levelling.ts @@ -14,7 +14,9 @@ const tableDefinition = { const database = getDatabase(tableDefinition); const getQuery = database.query("SELECT * FROM levelling WHERE guild = $1 AND user = $2;"); const deleteQuery = database.query("DELETE FROM levelling WHERE guild = $1 AND user = $2;"); -const insertQuery = database.query("INSERT INTO levelling (guild, user, level, exp) VALUES (?1, ?2, ?3, ?4);"); +const insertQuery = database.query( + "INSERT INTO levelling (guild, user, level, exp) VALUES (?1, ?2, ?3, ?4);" +); export function getLevel(guildID: string, userID: string): [number, number] { const res = getQuery.all(guildID, userID) as TypeOfDefinition[]; @@ -23,7 +25,6 @@ export function getLevel(guildID: string, userID: string): [number, number] { } export function setLevel(guildID: string | number, userID: string, level: number, exp: number) { - if (getQuery.all(guildID, userID).length != 0) - deleteQuery.run(guildID, userID, level, exp); + if (getQuery.all(guildID, userID).length != 0) deleteQuery.run(guildID, userID); insertQuery.run(guildID, userID, level, exp); } diff --git a/src/utils/database/settings.ts b/src/utils/database/settings.ts index f7afde8..d1cb259 100644 --- a/src/utils/database/settings.ts +++ b/src/utils/database/settings.ts @@ -14,7 +14,7 @@ const tableDefinition = { export const settingsDefinition = { "levelling.enabled": "BOOL", "levelling.channel": "TEXT", - "levelling.blockChannelIds": "LIST", // TODO: Add this to the levelling command + //"levelling.blockedChannels": "LIST", // TODO: Add this to the levelling command "poll.voteOnOneOption": "BOOL", "moderation.channel": "TEXT", "moderation.logMessages": "BOOL", @@ -52,8 +52,8 @@ export function getSetting( return res[0].value as TypeOfKey; case "BOOL": return (res[0].value == "true") as TypeOfKey; - case "LIST": - return (res[0].value.split(",") as unknown) as TypeOfKey; // TODO: Make this type usable + // case "LIST": + // return (res[0].value.split(",") as unknown) as TypeOfKey; // TODO: Make this type usable default: // TODO: Implement more data types return "WIP" as TypeOfKey; diff --git a/src/utils/embeds/errorEmbed.ts b/src/utils/embeds/errorEmbed.ts index 214d5f7..fb85e47 100644 --- a/src/utils/embeds/errorEmbed.ts +++ b/src/utils/embeds/errorEmbed.ts @@ -16,7 +16,7 @@ export function errorEmbed( const content = [`**${title}**`]; if (reason != undefined) content.push(reason); const embed = new EmbedBuilder() - .setTitle("❌ • Something went wrong!") + .setTitle("Something went wrong!") .setDescription(content.join("\n")) .setColor(genColor(0)); diff --git a/src/utils/embeds/serverEmbed.ts b/src/utils/embeds/serverEmbed.ts index cbaf3b5..208176e 100644 --- a/src/utils/embeds/serverEmbed.ts +++ b/src/utils/embeds/serverEmbed.ts @@ -1,8 +1,7 @@ -import { EmbedBuilder, type ColorResolvable, type Guild } from "discord.js"; -import { genColor, genRGBColor } from "../colorGen"; +import { EmbedBuilder, type Guild } from "discord.js"; +import { genColor } from "../colorGen"; import { getSetting } from "../database/settings"; -import Vibrant from "node-vibrant"; -import sharp from "sharp"; +import { imageColor } from "../imageColor"; type Options = { guild: Guild; @@ -49,20 +48,17 @@ export async function serverEmbed(options: Options) { if (options.showInvite && invite !== null) generalValues.push(`**Invite link**: ${invite}`); const embed = new EmbedBuilder() - .setAuthor({ name: `• ${pages ? `#${page} • ` : ""}${guild.name}`, iconURL: guild.iconURL()! }) + .setAuthor({ + name: `• ${pages ? `#${page} • ` : ""}${guild.name}`, + iconURL: guild.iconURL()! + }) .setDescription(guild.description ? guild.description : null) .setFields({ name: "📃 • General", value: generalValues.join("\n") }) .setFooter({ text: `Server ID: ${guild.id}${pages ? ` • Page ${page}/${pages}` : ""}` }) .setThumbnail(guild.iconURL()) .setColor(genColor(200)); - try { - const imageBuffer = await (await fetch(guild.iconURL()!)).arrayBuffer(); - const image = sharp(imageBuffer).toFormat("jpg"); - const { r, g, b } = (await new Vibrant(await image.toBuffer()).getPalette()).Vibrant!; - embed.setColor(genRGBColor(r, g, b) as ColorResolvable); - } catch {} - + imageColor(embed, guild); if (options.roles) embed.addFields({ name: `🎭 • ${roles.size - 1} ${roles.size === 1 ? "role" : "roles"}`, @@ -72,7 +68,7 @@ export async function serverEmbed(options: Options) { : `${sortedRoles .slice(0, 5) .map(role => `<@&${role[0]}>`) - .join(", ")}${roles.size > 5 ? ` **and ${roles.size - 5} more**` : ""}` + .join(", ")}${roles.size > 5 ? ` and **${roles.size - 5}** more` : ""}` }); embed.addFields( diff --git a/src/utils/imageColor.ts b/src/utils/imageColor.ts new file mode 100644 index 0000000..8484811 --- /dev/null +++ b/src/utils/imageColor.ts @@ -0,0 +1,13 @@ +import type { Guild, ColorResolvable, EmbedBuilder, GuildMember } from "discord.js"; +import Vibrant from "node-vibrant"; +import sharp from "sharp"; +import { genRGBColor } from "./colorGen"; + +export async function imageColor(embed: EmbedBuilder, guild?: Guild, member?: GuildMember) { + const imageBuffer = await ( + await fetch(guild ? guild.iconURL()! : member?.displayAvatarURL()!) + ).arrayBuffer(); + const image = sharp(imageBuffer).toFormat("jpg"); + const { r, g, b } = (await new Vibrant(await image.toBuffer()).getPalette()).Vibrant!; + return embed.setColor(genRGBColor(r, g, b) as ColorResolvable); +} From 444b4f24ed79e467b4a754b0948cb80d9ad784e7 Mon Sep 17 00:00:00 2001 From: Serge Date: Mon, 25 Mar 2024 21:51:01 +0500 Subject: [PATCH 050/127] some changes --- CONTRIBUTING.md | 2 +- README.md | 4 +- package.json | 6 +- src/commands/about.ts | 10 +-- src/commands/moderation/ban.ts | 4 +- src/commands/moderation/delwarn.ts | 2 +- src/commands/moderation/kick.ts | 4 +- src/commands/moderation/mute.ts | 4 +- src/commands/moderation/warn.ts | 4 +- src/commands/{server => }/news/edit.ts | 10 +-- src/commands/{server => }/news/remove.ts | 8 +- src/commands/{server => }/news/send.ts | 8 +- src/commands/{server => }/news/view.ts | 6 +- src/commands/poll.ts | 106 ----------------------- src/commands/server.ts | 16 ++++ src/commands/server/info.ts | 16 ---- src/commands/serverboard.ts | 10 +-- src/commands/{server => }/settings.ts | 12 +-- src/commands/user.ts | 6 +- src/events/messageCreate.ts | 18 ++-- src/events/messageDelete.ts | 1 + src/events/messageReactionCreate.ts | 25 ------ src/utils/database/poll.ts | 29 ------- src/utils/database/settings.ts | 1 - src/utils/embeds/serverEmbed.ts | 2 +- src/utils/imageColor.ts | 14 +-- 26 files changed, 83 insertions(+), 245 deletions(-) rename src/commands/{server => }/news/edit.ts (92%) rename src/commands/{server => }/news/remove.ts (86%) rename src/commands/{server => }/news/send.ts (90%) rename src/commands/{server => }/news/view.ts (94%) delete mode 100644 src/commands/poll.ts create mode 100644 src/commands/server.ts delete mode 100644 src/commands/server/info.ts rename src/commands/{server => }/settings.ts (88%) delete mode 100644 src/events/messageReactionCreate.ts delete mode 100644 src/utils/database/poll.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7f50bf2..555ab7e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,4 @@ -# Nebula Contributing Guide +# Sokora Contributing Guide ## Prerequisites - Basic knowledge of [TypeScript](https://typescriptlang.org/) and [discord.js](https://discord.js.org/). diff --git a/README.md b/README.md index 8917692..fed9142 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,9 @@ # About -Nebula is a multiplatform, multipurpose bot with the ability to add extensions to have additional features. +Sokora is a multiplatform, multipurpose bot with the ability to add extensions to have additional features. -**Please note that Nebula is currently unstable and is only usable within Discord.** +**Please note that Sokora is currently unstable and is only usable within Discord.** # Contributing While we're developing the multiplatform version of the bot, you can still [help us](CONTRIBUTING.MD) if you find any bugs. diff --git a/package.json b/package.json index 9a20dda..56563f5 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { - "name": "nebula", - "description": "Welcome to Nebula, the multipurpose, multiplatform bot.", + "name": "sokora", + "description": "Welcome to Sokora, the multipurpose, multiplatform bot.", "contributors": [ - "The Nebula team", + "The Sokora team", "The GitHub contributors" ], "version": "0.1.0", diff --git a/src/commands/about.ts b/src/commands/about.ts index 523c6bb..71d7cae 100644 --- a/src/commands/about.ts +++ b/src/commands/about.ts @@ -1,15 +1,11 @@ -import { - SlashCommandSubcommandBuilder, - EmbedBuilder, - type ChatInputCommandInteraction -} from "discord.js"; +import { SlashCommandBuilder, EmbedBuilder, type ChatInputCommandInteraction } from "discord.js"; import { genColor } from "../utils/colorGen"; import { randomise } from "../utils/randomise"; export default class About { - data: SlashCommandSubcommandBuilder; + data: SlashCommandBuilder; constructor() { - this.data = new SlashCommandSubcommandBuilder() + this.data = new SlashCommandBuilder() .setName("about") .setDescription("Shows information about the bot."); } diff --git a/src/commands/moderation/ban.ts b/src/commands/moderation/ban.ts index 473e8ec..3c70a9c 100644 --- a/src/commands/moderation/ban.ts +++ b/src/commands/moderation/ban.ts @@ -43,13 +43,13 @@ export default class Ban { if (target === member) return errorEmbed(interaction, "You can't ban yourself."); if (target.user.id === interaction.client.user.id) - return errorEmbed(interaction, "You can't ban Nebula."); + return errorEmbed(interaction, "You can't ban Sokora."); if (!target.manageable) return errorEmbed( interaction, `You can't ban ${name}.`, - "The member has a higher role position than Nebula." + "The member has a higher role position than Sokora." ); if (member.roles.highest.position < target.roles.highest.position) diff --git a/src/commands/moderation/delwarn.ts b/src/commands/moderation/delwarn.ts index a9c628d..74e5dc6 100644 --- a/src/commands/moderation/delwarn.ts +++ b/src/commands/moderation/delwarn.ts @@ -56,7 +56,7 @@ export default class Delwarn { return errorEmbed( interaction, `You can't delete a warn from ${name}.`, - "The member has a higher role position than Nebula." + "The member has a higher role position than Sokora." ); if (member.roles.highest.position < target.roles.highest.position) diff --git a/src/commands/moderation/kick.ts b/src/commands/moderation/kick.ts index 186373e..14be288 100644 --- a/src/commands/moderation/kick.ts +++ b/src/commands/moderation/kick.ts @@ -43,13 +43,13 @@ export default class Kick { if (target === member) return errorEmbed(interaction, "You can't kick yourself."); if (target.user.id === interaction.client.user.id) - return errorEmbed(interaction, "You can't kick Nebula."); + return errorEmbed(interaction, "You can't kick Sokora."); if (!target.manageable) return errorEmbed( interaction, `You can't kick ${name}.`, - "The member has a higher role position than Nebula." + "The member has a higher role position than Sokora." ); if (member.roles.highest.position < target.roles.highest.position) diff --git a/src/commands/moderation/mute.ts b/src/commands/moderation/mute.ts index b3f39d3..c2b0a5f 100644 --- a/src/commands/moderation/mute.ts +++ b/src/commands/moderation/mute.ts @@ -51,13 +51,13 @@ export default class Mute { if (target === member) return errorEmbed(interaction, "You can't mute yourself."); if (target.user.id === interaction.client.user.id) - return errorEmbed(interaction, "You can't mute Nebula."); + return errorEmbed(interaction, "You can't mute Sokora."); if (!target.manageable) return errorEmbed( interaction, `You can't mute ${name}.`, - "The member has a higher role position than Nebula." + "The member has a higher role position than Sokora." ); if (member.roles.highest.position < target.roles.highest.position) diff --git a/src/commands/moderation/warn.ts b/src/commands/moderation/warn.ts index 124faaf..5a06326 100644 --- a/src/commands/moderation/warn.ts +++ b/src/commands/moderation/warn.ts @@ -44,13 +44,13 @@ export default class Warn { if (target === member) return errorEmbed(interaction, "You can't warn yourself."); if (target.user.id === interaction.client.user.id) - return errorEmbed(interaction, "You can't warn Nebula."); + return errorEmbed(interaction, "You can't warn Sokora."); if (!target.manageable) return errorEmbed( interaction, `You can't warn ${name}.`, - "The member has a higher role position than Nebula." + "The member has a higher role position than Sokora." ); if (member.roles.highest.position < target.roles.highest.position) diff --git a/src/commands/server/news/edit.ts b/src/commands/news/edit.ts similarity index 92% rename from src/commands/server/news/edit.ts rename to src/commands/news/edit.ts index 47f5d70..c3c9a84 100644 --- a/src/commands/server/news/edit.ts +++ b/src/commands/news/edit.ts @@ -10,11 +10,11 @@ import { type TextChannel, type Role } from "discord.js"; -import { genColor } from "../../../utils/colorGen"; -import { errorEmbed } from "../../../utils/embeds/errorEmbed"; -import { get, updateNews } from "../../../utils/database/news"; -import { getSetting } from "../../../utils/database/settings"; -import { sendChannelNews } from "../../../utils/sendChannelNews"; +import { genColor } from "../../utils/colorGen"; +import { errorEmbed } from "../../utils/embeds/errorEmbed"; +import { get, updateNews } from "../../utils/database/news"; +import { getSetting } from "../../utils/database/settings"; +import { sendChannelNews } from "../../utils/sendChannelNews"; export default class Edit { data: SlashCommandSubcommandBuilder; diff --git a/src/commands/server/news/remove.ts b/src/commands/news/remove.ts similarity index 86% rename from src/commands/server/news/remove.ts rename to src/commands/news/remove.ts index 54caa3c..5a3efdb 100644 --- a/src/commands/server/news/remove.ts +++ b/src/commands/news/remove.ts @@ -5,10 +5,10 @@ import { TextChannel, type ChatInputCommandInteraction } from "discord.js"; -import { genColor } from "../../../utils/colorGen"; -import { errorEmbed } from "../../../utils/embeds/errorEmbed"; -import { deleteNews, get } from "../../../utils/database/news"; -import { getSetting } from "../../../utils/database/settings"; +import { genColor } from "../../utils/colorGen"; +import { errorEmbed } from "../../utils/embeds/errorEmbed"; +import { deleteNews, get } from "../../utils/database/news"; +import { getSetting } from "../../utils/database/settings"; export default class Remove { data: SlashCommandSubcommandBuilder; diff --git a/src/commands/server/news/send.ts b/src/commands/news/send.ts similarity index 90% rename from src/commands/server/news/send.ts rename to src/commands/news/send.ts index b01e8a4..dd4c1e6 100644 --- a/src/commands/server/news/send.ts +++ b/src/commands/news/send.ts @@ -8,10 +8,10 @@ import { TextInputStyle, type ChatInputCommandInteraction } from "discord.js"; -import { genColor } from "../../../utils/colorGen"; -import { errorEmbed } from "../../../utils/embeds/errorEmbed"; -import { sendChannelNews } from "../../../utils/sendChannelNews"; -import { sendNews } from "../../../utils/database/news"; +import { genColor } from "../../utils/colorGen"; +import { errorEmbed } from "../../utils/embeds/errorEmbed"; +import { sendChannelNews } from "../../utils/sendChannelNews"; +import { sendNews } from "../../utils/database/news"; export default class Send { data: SlashCommandSubcommandBuilder; diff --git a/src/commands/server/news/view.ts b/src/commands/news/view.ts similarity index 94% rename from src/commands/server/news/view.ts rename to src/commands/news/view.ts index 5f9d0a9..9ccc9b0 100644 --- a/src/commands/server/news/view.ts +++ b/src/commands/news/view.ts @@ -6,9 +6,9 @@ import { ButtonStyle, type ChatInputCommandInteraction } from "discord.js"; -import { genColor } from "../../../utils/colorGen"; -import { errorEmbed } from "../../../utils/embeds/errorEmbed"; -import { listAllNews } from "../../../utils/database/news"; +import { genColor } from "../../utils/colorGen"; +import { errorEmbed } from "../../utils/embeds/errorEmbed"; +import { listAllNews } from "../../utils/database/news"; export default class View { data: SlashCommandSubcommandBuilder; diff --git a/src/commands/poll.ts b/src/commands/poll.ts deleted file mode 100644 index ac64b83..0000000 --- a/src/commands/poll.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { - PermissionsBitField, - ChannelType, - EmbedBuilder, - type ChatInputCommandInteraction, - type TextChannel -} from "discord.js"; -import { errorEmbed } from "../utils/embeds/errorEmbed"; -import { genColor } from "../utils/colorGen"; -import { ExtendedSlashCommandSubcommandBuilder } from "../utils/extendedSlashCommandSubcommandBuilder"; -import { addPoll } from "../utils/database/poll"; - -export default class Poll { - data: ExtendedSlashCommandSubcommandBuilder; - constructor() { - this.data = new ExtendedSlashCommandSubcommandBuilder() - .setName("poll") - .setDescription("Make a poll") - .addStringOption(option => - option - .setName("question") - .setDescription("The question that you want to ask.") - .setRequired(true) - ) - .genNumberFields("Option", 6, 2) - .addAttachmentOption(option => - option.setName("image").setDescription("The image that appears at the bottom of the embed.") - ) - .addChannelOption(channel => - channel - .setName("channel") - .setDescription("The channel to send the poll in.") - .addChannelTypes( - ChannelType.GuildText, - ChannelType.PublicThread, - ChannelType.PrivateThread - ) - ); - } - - async run(interaction: ChatInputCommandInteraction) { - function convertNumEmoji(num: number) { - const numEmojis = ["1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣", "🔟"]; - return numEmojis[num]; - } - - const channel = interaction.options.getChannel("channel") as TextChannel; - const image = interaction.options.getAttachment("image")!; - const guild = interaction.guild!; - const member = guild.members.cache.get(interaction.member?.user.id!)!; - let options: string[] = []; - - for (let i = 0; i < 6; i++) { - const option = interaction.options.getString(`option${i + 1}`); - if (option) options.push(option); - } - - if (!member.permissions.has(PermissionsBitField.Flags.ManageMessages)) - return errorEmbed( - interaction, - "You can't execute this command.", - "You need the **Manage Messages** permission." - ); - - if (!interaction.guild?.members.me?.permissions.has(PermissionsBitField.Flags.SendMessages)) - return errorEmbed( - interaction, - "Nebula can't execute this command.", - `Nebula needs the **Send Messages** permission for ${channel}.` - ); - - let embed = new EmbedBuilder() - .setAuthor({ - name: `• ${member.user.displayName}`, - iconURL: member.user.displayAvatarURL() - }) - .setTitle(`${interaction.options.getString("question")}`) - .setFields( - options.map((option, i) => { - return { - name: `Option ${convertNumEmoji(i)}`, - value: option - }; - }) - ) - .setColor(genColor(200)); - - if (image) embed.setImage(image.url); - if (channel) { - const successEmbed = new EmbedBuilder() - .setTitle("Poll has been created.") - .setDescription(`Poll is sent to ${channel}.`) - .setColor(genColor(100)); - - await interaction.reply({ embeds: [successEmbed] }); - return await channel.send({ embeds: [embed] }).then(async message => { - for (const emoji of options.map((_, i) => convertNumEmoji(i))) await message.react(emoji); - }); - } - - await interaction.reply({ embeds: [embed], fetchReply: true }).then(async message => { - for (const emoji of options.map((_, i) => convertNumEmoji(i))) await message.react(emoji); - addPoll(interaction.guildId!, message.id); - }); - } -} diff --git a/src/commands/server.ts b/src/commands/server.ts new file mode 100644 index 0000000..9c5ac8f --- /dev/null +++ b/src/commands/server.ts @@ -0,0 +1,16 @@ +import { SlashCommandBuilder, type ChatInputCommandInteraction } from "discord.js"; +import { serverEmbed } from "../utils/embeds/serverEmbed"; + +export default class Server { + data: SlashCommandBuilder; + constructor() { + this.data = new SlashCommandBuilder() + .setName("server") + .setDescription("Shows this server's info."); + } + + async run(interaction: ChatInputCommandInteraction) { + const embed = await serverEmbed({ guild: interaction.guild!, roles: true }); + await interaction.reply({ embeds: [embed] }); + } +} diff --git a/src/commands/server/info.ts b/src/commands/server/info.ts deleted file mode 100644 index 6402b38..0000000 --- a/src/commands/server/info.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { SlashCommandSubcommandBuilder, type ChatInputCommandInteraction } from "discord.js"; -import { serverEmbed } from "../../utils/embeds/serverEmbed"; - -export default class ServerInfo { - data: SlashCommandSubcommandBuilder; - constructor() { - this.data = new SlashCommandSubcommandBuilder() - .setName("info") - .setDescription("Shows this server's info."); - } - - async run(interaction: ChatInputCommandInteraction) { - const embed = await serverEmbed({ guild: interaction.guild!, roles: true }); - await interaction.reply({ embeds: [embed] }); - } -} diff --git a/src/commands/serverboard.ts b/src/commands/serverboard.ts index ba06a98..6ec610e 100644 --- a/src/commands/serverboard.ts +++ b/src/commands/serverboard.ts @@ -1,5 +1,5 @@ import { - SlashCommandSubcommandBuilder, + SlashCommandBuilder, ButtonBuilder, ActionRowBuilder, ButtonStyle, @@ -12,11 +12,11 @@ import { listPublicServers } from "../utils/database/settings"; import { errorEmbed } from "../utils/embeds/errorEmbed"; export default class Serverboard { - data: SlashCommandSubcommandBuilder; + data: Omit; constructor() { - this.data = new SlashCommandSubcommandBuilder() + this.data = new SlashCommandBuilder() .setName("serverboard") - .setDescription("Shows the servers that have Nebula.") + .setDescription("Shows the servers that have Sokora.") .addNumberOption(number => number.setName("page").setDescription("The page you want to see.") ); @@ -32,7 +32,7 @@ export default class Serverboard { return errorEmbed( interaction, "No public server found", - "By some magical miracle, all the servers using Nebula turned off their visibility. Use /server settings key:`serverboard.shown` value:`TRUE` to make your server publicly visible." + "By some magical miracle, all the servers using Sokora turned off their visibility. Use /server settings key:`serverboard.shown` value:`TRUE` to make your server publicly visible." ); const argPage = interaction.options.get("page")?.value as number; diff --git a/src/commands/server/settings.ts b/src/commands/settings.ts similarity index 88% rename from src/commands/server/settings.ts rename to src/commands/settings.ts index add6639..e04f661 100644 --- a/src/commands/server/settings.ts +++ b/src/commands/settings.ts @@ -2,7 +2,7 @@ import { Client, InteractionType, EmbedBuilder, - SlashCommandSubcommandBuilder, + SlashCommandBuilder, type ChatInputCommandInteraction } from "discord.js"; import { @@ -10,13 +10,13 @@ import { setSetting, settingsDefinition, settingsKeys -} from "../../utils/database/settings"; -import { genColor } from "../../utils/colorGen"; +} from "../utils/database/settings"; +import { genColor } from "../utils/colorGen"; -export default class ServerInfo { - data: SlashCommandSubcommandBuilder; +export default class Settings { + data: Omit; constructor() { - this.data = new SlashCommandSubcommandBuilder() + this.data = new SlashCommandBuilder() .setName("settings") .setDescription("Configure the bot") .addStringOption(string => diff --git a/src/commands/user.ts b/src/commands/user.ts index ab5e491..2ee7739 100644 --- a/src/commands/user.ts +++ b/src/commands/user.ts @@ -1,5 +1,5 @@ import { - SlashCommandSubcommandBuilder, + SlashCommandBuilder, EmbedBuilder, ButtonBuilder, ButtonStyle, @@ -16,9 +16,9 @@ import { getLevel, setLevel } from "../utils/database/levelling"; import { imageColor } from "../utils/imageColor"; export default class User { - data: SlashCommandSubcommandBuilder; + data: Omit; constructor() { - this.data = new SlashCommandSubcommandBuilder() + this.data = new SlashCommandBuilder() .setName("user") .setDescription("Shows your (or another user's) info.") .addUserOption(user => user.setName("user").setDescription("Select the user.")); diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts index 93f32c1..8d21eef 100644 --- a/src/events/messageCreate.ts +++ b/src/events/messageCreate.ts @@ -20,15 +20,13 @@ export default { const eventsPath = join(process.cwd(), "src", "events", "easterEggs"); for (const easterEggFile of readdirSync(eventsPath)) - new (await import(pathToFileURL(join(eventsPath, easterEggFile)).toString())).default().run( - message, - ...message.content - ); + new ( + await import(pathToFileURL(join(eventsPath, easterEggFile)).toString()) + ).default().run(message, ...message.content); } // Levelling const levelChannelId = getSetting(guild.id, "levelling.channel"); - if (!levelChannelId) return; if (!getSetting(guild.id, "levelling.enabled")) return; const [guildExp, guildLevel] = getLevel(guild.id, author.id); @@ -64,10 +62,12 @@ export default { .setTimestamp() .setColor(genColor(200)); - (guild.channels.cache.get(`${levelChannelId}`) as TextChannel).send({ - embeds: [embed], - content: `<@${author.id}>` - }); + if (levelChannelId) + (guild.channels.cache.get(`${levelChannelId}`) as TextChannel).send({ + embeds: [embed], + content: `<@${author.id}>` + }); + for (const { level, roleID } of getLevelRewards(guild.id)) { const role = guild.roles.cache.get(`${roleID}`); if (!role) continue; diff --git a/src/events/messageDelete.ts b/src/events/messageDelete.ts index 6df0fea..7929e58 100644 --- a/src/events/messageDelete.ts +++ b/src/events/messageDelete.ts @@ -1,3 +1,4 @@ +// TODO: display the person who deleted the message import { codeBlock, EmbedBuilder, type Message, type TextChannel, type Channel } from "discord.js"; import { genColor } from "../utils/colorGen"; import { getSetting } from "../utils/database/settings"; diff --git a/src/events/messageReactionCreate.ts b/src/events/messageReactionCreate.ts deleted file mode 100644 index 8f38453..0000000 --- a/src/events/messageReactionCreate.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { Client, Guild, User, MessageReaction } from "discord.js"; -import { getPolls } from "../utils/database/poll"; -import { getSetting } from "../utils/database/settings"; - -export default { - name: "messageReactionCreate", - event: class MessageReactionCreate { - client: Client; - constructor(client: Client) { - this.client = client; - } - - async run(guild: Guild, user: User, reaction: MessageReaction) { - if (getSetting(guild.id, "poll.voteOnOneOption") == false) return; - if (user.bot) return; - - const messageIds = getPolls(guild.id); - if (!messageIds.includes(reaction.message.id)) return; - const userReactions = reaction.message.reactions.cache.filter(reaction => - reaction.users.cache.has(user.id) - ); - if (userReactions.size > 1) await userReactions.last()?.remove(); - } - } -}; diff --git a/src/utils/database/poll.ts b/src/utils/database/poll.ts deleted file mode 100644 index 1cafa99..0000000 --- a/src/utils/database/poll.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { getDatabase } from "."; -import { TableDefinition, TypeOfDefinition } from "./types"; - -const tableDefinition = { - name: "polls", - definition: { - guild: "TEXT", - message: "TEXT" - } -} satisfies TableDefinition; - -const database = getDatabase(tableDefinition); -const getQuery = database.query("SELECT * FROM polls WHERE guild = $1;"); -const addQuery = database.query("INSERT INTO polls (guild, message) VALUES (?1, ?2);"); -const removeQuery = database.query("DELETE FROM polls WHERE guild = $1 AND message = $2"); - -export function getPolls(guildID: string) { - return (getQuery.all(guildID) as TypeOfDefinition[]).map( - val => val.message - ); -} - -export function addPoll(guildID: string, message: string) { - addQuery.run(guildID, message); -} - -export function removePoll(guildID: string, message: string) { - removeQuery.run(guildID, message); -} diff --git a/src/utils/database/settings.ts b/src/utils/database/settings.ts index d1cb259..91dd44e 100644 --- a/src/utils/database/settings.ts +++ b/src/utils/database/settings.ts @@ -15,7 +15,6 @@ export const settingsDefinition = { "levelling.enabled": "BOOL", "levelling.channel": "TEXT", //"levelling.blockedChannels": "LIST", // TODO: Add this to the levelling command - "poll.voteOnOneOption": "BOOL", "moderation.channel": "TEXT", "moderation.logMessages": "BOOL", "news.channelID": "TEXT", diff --git a/src/utils/embeds/serverEmbed.ts b/src/utils/embeds/serverEmbed.ts index 208176e..1b35555 100644 --- a/src/utils/embeds/serverEmbed.ts +++ b/src/utils/embeds/serverEmbed.ts @@ -49,7 +49,7 @@ export async function serverEmbed(options: Options) { const embed = new EmbedBuilder() .setAuthor({ - name: `• ${pages ? `#${page} • ` : ""}${guild.name}`, + name: ` ${pages ? `#${page} • ` : "• "}${guild.name}`, iconURL: guild.iconURL()! }) .setDescription(guild.description ? guild.description : null) diff --git a/src/utils/imageColor.ts b/src/utils/imageColor.ts index 8484811..ce1022c 100644 --- a/src/utils/imageColor.ts +++ b/src/utils/imageColor.ts @@ -4,10 +4,12 @@ import sharp from "sharp"; import { genRGBColor } from "./colorGen"; export async function imageColor(embed: EmbedBuilder, guild?: Guild, member?: GuildMember) { - const imageBuffer = await ( - await fetch(guild ? guild.iconURL()! : member?.displayAvatarURL()!) - ).arrayBuffer(); - const image = sharp(imageBuffer).toFormat("jpg"); - const { r, g, b } = (await new Vibrant(await image.toBuffer()).getPalette()).Vibrant!; - return embed.setColor(genRGBColor(r, g, b) as ColorResolvable); + try { + const imageBuffer = await ( + await fetch(guild ? guild.iconURL()! : member?.displayAvatarURL()!) + ).arrayBuffer(); + const image = sharp(imageBuffer).toFormat("jpg"); + const { r, g, b } = (await new Vibrant(await image.toBuffer()).getPalette()).Vibrant!; + return embed.setColor(genRGBColor(r, g, b) as ColorResolvable); + } catch {} } From 3aeb346ab3ab063159b372a9db8cd8f7d606f775 Mon Sep 17 00:00:00 2001 From: Serge Date: Mon, 25 Mar 2024 23:20:19 +0500 Subject: [PATCH 051/127] user changes --- src/commands/user.ts | 47 ++++++++++++++++++--------------- src/utils/embeds/serverEmbed.ts | 2 +- 2 files changed, 26 insertions(+), 23 deletions(-) diff --git a/src/commands/user.ts b/src/commands/user.ts index 2ee7739..e695175 100644 --- a/src/commands/user.ts +++ b/src/commands/user.ts @@ -33,6 +33,26 @@ export default class User { .map(user => user)[0]!; const selectedUser = target.user!; + let serverInfo = [`Joined on ****`]; + const guildRoles = guild.roles.cache.filter(role => target.roles.cache.has(role.id))!; + const memberRoles = [...guildRoles].sort( + (role1, role2) => role2[1].position - role1[1].position + ); + memberRoles.pop(); + + if (target.premiumSinceTimestamp != null) + serverInfo.push(`Boosting since **${target.premiumSinceTimestamp}**`); + + if (memberRoles.length !== 0) + serverInfo.push( + `**${guildRoles.filter(role => target.roles.cache.has(role.id)).size! - 1}** ${ + memberRoles.length === 1 ? "role" : "roles" + } • ${memberRoles + .slice(0, 5) + .map(role => `<@&${role[1].id}>`) + .join(", ")}${memberRoles.length > 3 ? ` **and ${memberRoles.length - 4} more**` : ""}` + ); + let embed = new EmbedBuilder() .setAuthor({ name: `• ${target.nickname ?? selectedUser.displayName}`, @@ -40,10 +60,10 @@ export default class User { }) .setFields( { - name: selectedUser?.bot === false ? "👤 • User info" : "🤖 • Bot info", + name: `<:realdiscord:1221878641462345788> • Discord info`, value: [ - `Username's **${selectedUser.username}**`, - `Display name's ${ + `Username is **${selectedUser.username}**`, + `Display name is ${ selectedUser.displayName === selectedUser.username ? "*not there*" : `**${selectedUser.displayName}**` @@ -52,8 +72,8 @@ export default class User { ].join("\n") }, { - name: "👥 • Member info", - value: `Joined on ****` + name: "📒 • Server info", + value: serverInfo.join("\n") } ) .setFooter({ text: `User ID: ${target.id}` }) @@ -61,23 +81,6 @@ export default class User { .setColor(genColor(200)); imageColor(embed, undefined, target); - const guildRoles = guild.roles.cache.filter(role => target.roles.cache.has(role.id))!; - const memberRoles = [...guildRoles].sort( - (role1, role2) => role2[1].position - role1[1].position - ); - memberRoles.pop(); - - if (memberRoles.length !== 0) - embed.addFields({ - name: `🎭 • ${guildRoles.filter(role => target.roles.cache.has(role.id)).size! - 1} ${ - memberRoles.length === 1 ? "role" : "roles" - }`, - value: `${memberRoles - .slice(0, 5) - .map(role => `<@&${role[1].id}>`) - .join(", ")}${memberRoles.length > 5 ? ` **and ${memberRoles.length - 5} more**` : ""}` - }); - if (!getSetting(`${guild.id}`, "levelling.enabled")) await interaction.reply({ embeds: [embed] }); diff --git a/src/utils/embeds/serverEmbed.ts b/src/utils/embeds/serverEmbed.ts index 1b35555..a64facd 100644 --- a/src/utils/embeds/serverEmbed.ts +++ b/src/utils/embeds/serverEmbed.ts @@ -68,7 +68,7 @@ export async function serverEmbed(options: Options) { : `${sortedRoles .slice(0, 5) .map(role => `<@&${role[0]}>`) - .join(", ")}${roles.size > 5 ? ` and **${roles.size - 5}** more` : ""}` + .join(", ")}${roles.size > 5 ? ` and **${roles.size - 6}** more` : ""}` }); embed.addFields( From eb74efa54c71f0894085d0cb4c465468056e9215 Mon Sep 17 00:00:00 2001 From: Serge Date: Sat, 30 Mar 2024 18:25:47 +0500 Subject: [PATCH 052/127] levelling works --- src/commands/about.ts | 2 +- src/commands/news/view.ts | 4 ++-- src/commands/serverboard.ts | 2 +- src/commands/settings.ts | 13 +++++++++++++ src/commands/user.ts | 6 +++--- src/events/guildCreate.ts | 2 +- src/events/messageCreate.ts | 21 ++++++++++++++++----- src/utils/database/settings.ts | 10 +++++----- 8 files changed, 42 insertions(+), 18 deletions(-) diff --git a/src/commands/about.ts b/src/commands/about.ts index 71d7cae..d0247f1 100644 --- a/src/commands/about.ts +++ b/src/commands/about.ts @@ -40,7 +40,7 @@ export default class About { "**Developers**: Dimkauzh, Froxcey, Golem64, Spectrum, ThatBOI", "**Designers**: ArtyH, Optix, proJM, Slider_on_the_black", "**Translators**: Dimkauzh, Golem64, Optix, SaFire, ThatBOI, Trynera", - "**Testers**: Blaze, fishy, flojo, Trynera", + "**Testers**: Blaze, fishy, flojo, GrandSenna, Trynera", "And **YOU**, for using Sokora." ].join("\n") }, diff --git a/src/commands/news/view.ts b/src/commands/news/view.ts index 9ccc9b0..bf4967c 100644 --- a/src/commands/news/view.ts +++ b/src/commands/news/view.ts @@ -31,7 +31,7 @@ export default class View { return errorEmbed( interaction, "No news found.", - "Admins can add news with the **/server news send** command." + "Admins can add news with the **/news send** command." ); if (page > sortedNews.length) page = sortedNews.length; @@ -69,7 +69,7 @@ export default class View { errorEmbed( interaction, "No.", - "You have not sent this command. Type **/server news view** to view news yourself." + "You have not sent this command. Type **/news view** to view news yourself." ); return; } diff --git a/src/commands/serverboard.ts b/src/commands/serverboard.ts index 6ec610e..0e39486 100644 --- a/src/commands/serverboard.ts +++ b/src/commands/serverboard.ts @@ -32,7 +32,7 @@ export default class Serverboard { return errorEmbed( interaction, "No public server found", - "By some magical miracle, all the servers using Sokora turned off their visibility. Use /server settings key:`serverboard.shown` value:`TRUE` to make your server publicly visible." + "By some magical miracle, all the servers using Sokora turned off their visibility. Use /settings key:`serverboard.shown` value:`TRUE` to make your server publicly visible." ); const argPage = interaction.options.get("page")?.value as number; diff --git a/src/commands/settings.ts b/src/commands/settings.ts index e04f661..abc8481 100644 --- a/src/commands/settings.ts +++ b/src/commands/settings.ts @@ -3,6 +3,7 @@ import { InteractionType, EmbedBuilder, SlashCommandBuilder, + PermissionsBitField, type ChatInputCommandInteraction } from "discord.js"; import { @@ -12,6 +13,7 @@ import { settingsKeys } from "../utils/database/settings"; import { genColor } from "../utils/colorGen"; +import { errorEmbed } from "../utils/embeds/errorEmbed"; export default class Settings { data: Omit; @@ -35,6 +37,17 @@ export default class Settings { } async run(interaction: ChatInputCommandInteraction) { + if ( + !interaction.guild?.members.cache + ?.get(interaction.member?.user.id!) + ?.permissions.has(PermissionsBitField.Flags.Administrator) + ) + return errorEmbed( + interaction, + "You can't execute this command.", + "You need the **Administrator** permission." + ); + const key = interaction.options.get("key")!.value as keyof typeof settingsDefinition; const value = interaction.options.get("value")?.value; if (value == undefined) diff --git a/src/commands/user.ts b/src/commands/user.ts index e695175..ef49670 100644 --- a/src/commands/user.ts +++ b/src/commands/user.ts @@ -81,9 +81,9 @@ export default class User { .setColor(genColor(200)); imageColor(embed, undefined, target); - if (!getSetting(`${guild.id}`, "levelling.enabled")) - await interaction.reply({ embeds: [embed] }); + const reply = await interaction.reply({ embeds: [embed], components: [] }); + if (!getSetting(`${guild.id}`, "levelling.enabled")) return; const [guildExp, guildLevel] = getLevel(`${guild.id}`, `${target.id}`)!; if (!guildExp && !guildLevel) setLevel(`${guild.id}`, `${target.id}`, 0, 0); @@ -116,7 +116,7 @@ export default class User { .setStyle(ButtonStyle.Primary) ); - const reply = await interaction.reply({ embeds: [embed], components: [row] }); + reply.edit({ embeds: [embed], components: [row] }); reply .createMessageComponentCollector({ componentType: ComponentType.Button, time: 60000 }) .on("collect", async (i: ButtonInteraction) => { diff --git a/src/events/guildCreate.ts b/src/events/guildCreate.ts index 9e2bf5a..25cc0e2 100644 --- a/src/events/guildCreate.ts +++ b/src/events/guildCreate.ts @@ -24,7 +24,7 @@ export default { .setDescription( [ "Sokora is a multipurpose Discord bot that lets you manage your servers easily.", - "To manage the bot, use the **/server settings** command.", + "To manage the bot, use the **/settings** command.", "Sokora is in an early stage of development. If you find bugs, please go to our [official server](https://discord.gg/c6C25P4BuY) and report them." ].join("\n\n") ) diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts index 8d21eef..e432019 100644 --- a/src/events/messageCreate.ts +++ b/src/events/messageCreate.ts @@ -26,20 +26,31 @@ export default { } // Levelling - const levelChannelId = getSetting(guild.id, "levelling.channel"); if (!getSetting(guild.id, "levelling.enabled")) return; + const blockedChannels = getSetting(guild.id, "levelling.blockChannels")!; + if (blockedChannels != undefined) + for (const channelID of blockedChannels.split(", ")) + if (message.channelId === channelID) return; + + const levelChannelId = getSetting(guild.id, "levelling.channel"); const [guildExp, guildLevel] = getLevel(guild.id, author.id); const [globalExp, globalLevel] = getLevel("0", author.id); + const expPerMessage = 2; const expUntilLevelup = Math.floor(100 * 1.25 * (guildLevel + 1)); + const newLevelData = { level: guildLevel ?? 0, exp: (guildExp ?? 0) + expPerMessage }; + const globalNewLevelData = { level: globalLevel ?? 0, exp: (globalExp ?? 0) + expPerMessage }; - if (!(guildExp >= expUntilLevelup - 1)) { - setLevel(0, author.id, globalLevel ?? 0, (globalExp ?? 0) + 2); - return setLevel(guild.id, author.id, globalLevel ?? 0, (globalExp ?? 0) + 2); + if (guildExp < expUntilLevelup - 1) { + setLevel(0, author.id, globalNewLevelData.exp, globalNewLevelData.level); + return setLevel(guild.id, author.id, globalNewLevelData.exp, globalNewLevelData.level); } else if (guildExp >= expUntilLevelup - 1) { let leftOverExp = guildExp - expUntilLevelup; if (leftOverExp < 0) leftOverExp = 0; - setLevel(guild.id, author.id, guildLevel + 1, leftOverExp ?? 0); + + newLevelData.exp = leftOverExp ?? 0; + newLevelData.level = guildLevel + 1; + setLevel(guild.id, author.id, newLevelData.exp, newLevelData.level); } if (guildExp >= Math.floor(100 * 1.25 * (globalLevel + 1)) - 1) { diff --git a/src/utils/database/settings.ts b/src/utils/database/settings.ts index 91dd44e..b92270e 100644 --- a/src/utils/database/settings.ts +++ b/src/utils/database/settings.ts @@ -1,4 +1,4 @@ -// TODO: Add more settings +// TODO: make the LIST type work import { getDatabase } from "."; import { FieldData, SqlType, TableDefinition, TypeOfDefinition } from "./types"; @@ -14,7 +14,8 @@ const tableDefinition = { export const settingsDefinition = { "levelling.enabled": "BOOL", "levelling.channel": "TEXT", - //"levelling.blockedChannels": "LIST", // TODO: Add this to the levelling command + "levelling.blockChannels": "TEXT", + "levelling.setLevel": "INTEGER", "moderation.channel": "TEXT", "moderation.logMessages": "BOOL", "news.channelID": "TEXT", @@ -47,14 +48,13 @@ export function getSetting( >[]; if (res.length == 0) return null; switch (settingsDefinition[key]) { - case "TEXT": + case "TEXT" || "INTEGER": return res[0].value as TypeOfKey; case "BOOL": return (res[0].value == "true") as TypeOfKey; // case "LIST": - // return (res[0].value.split(",") as unknown) as TypeOfKey; // TODO: Make this type usable + // return (res[0].value.split(",") as unknown) as TypeOfKey; default: - // TODO: Implement more data types return "WIP" as TypeOfKey; } } From 4e39b47fe5729def25beee320cb703f251ae3983 Mon Sep 17 00:00:00 2001 From: Serge Date: Sun, 31 Mar 2024 16:45:39 +0500 Subject: [PATCH 053/127] a yet another small commit --- src/commands/about.ts | 2 +- src/commands/news/edit.ts | 4 +-- src/commands/news/remove.ts | 4 +-- src/commands/settings.ts | 2 +- src/commands/user.ts | 2 +- .../extendedSlashCommandSubcommandBuilder.ts | 33 ------------------- 6 files changed, 7 insertions(+), 40 deletions(-) delete mode 100644 src/utils/extendedSlashCommandSubcommandBuilder.ts diff --git a/src/commands/about.ts b/src/commands/about.ts index d0247f1..1ffd310 100644 --- a/src/commands/about.ts +++ b/src/commands/about.ts @@ -7,7 +7,7 @@ export default class About { constructor() { this.data = new SlashCommandBuilder() .setName("about") - .setDescription("Shows information about the bot."); + .setDescription("Shows information about Sokora."); } async run(interaction: ChatInputCommandInteraction) { diff --git a/src/commands/news/edit.ts b/src/commands/news/edit.ts index c3c9a84..5fbb7df 100644 --- a/src/commands/news/edit.ts +++ b/src/commands/news/edit.ts @@ -22,8 +22,8 @@ export default class Edit { this.data = new SlashCommandSubcommandBuilder() .setName("edit") .setDescription("Edits the news of your guild.") - .addStringOption(option => - option + .addStringOption(string => + string .setName("id") .setDescription("The ID of the news you want to edit.") .setRequired(true) diff --git a/src/commands/news/remove.ts b/src/commands/news/remove.ts index 5a3efdb..dbf4f1f 100644 --- a/src/commands/news/remove.ts +++ b/src/commands/news/remove.ts @@ -16,8 +16,8 @@ export default class Remove { this.data = new SlashCommandSubcommandBuilder() .setName("remove") .setDescription("Removes news from your guild.") - .addStringOption(option => - option + .addStringOption(string => + string .setName("id") .setDescription("The ID of the news. Found in the footer of the news.") .setRequired(true) diff --git a/src/commands/settings.ts b/src/commands/settings.ts index abc8481..b30a97d 100644 --- a/src/commands/settings.ts +++ b/src/commands/settings.ts @@ -20,7 +20,7 @@ export default class Settings { constructor() { this.data = new SlashCommandBuilder() .setName("settings") - .setDescription("Configure the bot") + .setDescription("Configure Sokora to your liking.") .addStringOption(string => string .setName("key") diff --git a/src/commands/user.ts b/src/commands/user.ts index ef49670..e35635e 100644 --- a/src/commands/user.ts +++ b/src/commands/user.ts @@ -83,7 +83,7 @@ export default class User { imageColor(embed, undefined, target); const reply = await interaction.reply({ embeds: [embed], components: [] }); - if (!getSetting(`${guild.id}`, "levelling.enabled")) return; + if (!getSetting(`${guild.id}`, "levelling.enabled" || selectedUser.bot)) return; const [guildExp, guildLevel] = getLevel(`${guild.id}`, `${target.id}`)!; if (!guildExp && !guildLevel) setLevel(`${guild.id}`, `${target.id}`, 0, 0); diff --git a/src/utils/extendedSlashCommandSubcommandBuilder.ts b/src/utils/extendedSlashCommandSubcommandBuilder.ts deleted file mode 100644 index 0ed1df4..0000000 --- a/src/utils/extendedSlashCommandSubcommandBuilder.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { SlashCommandSubcommandBuilder } from "discord.js"; - -export class ExtendedSlashCommandSubcommandBuilder extends SlashCommandSubcommandBuilder { - /** - * Generates fields and descriptions for commands which serve the same purpose but have to be reused. - * @param {SlashCommandSubcommandBuilder} command The command to add the fields to. - * @param {string} name Reused name of the fields. - * @param {number} number Number of fields to generate. - * @param {number} numRequired Number of required fields. - * @returns Returns the generated fields. - */ - genNumberFields(name: string, number: number, numRequired: number = 0) { - for (let i = 0; i < numRequired; i++) { - this.addStringOption(option => - option - .setName(`${name.toLowerCase()}${i + 1}`) - .setDescription(`${name} ${i + 1}`) - .setRequired(true) - ); - } - - for (let i = numRequired; i < number; i++) { - this.addStringOption(option => - option - .setName(`${name.toLowerCase()}${i + 1}`) - .setDescription(`${name} ${i + 1}, not required`) - .setRequired(false) - ); - } - - return this; - } -} From 5c0801924175031b01732644205038d13e74feef Mon Sep 17 00:00:00 2001 From: Serge Date: Thu, 11 Apr 2024 17:35:45 +0500 Subject: [PATCH 054/127] crazy --- src/events/easterEggs/Crazy.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 src/events/easterEggs/Crazy.ts diff --git a/src/events/easterEggs/Crazy.ts b/src/events/easterEggs/Crazy.ts new file mode 100644 index 0000000..0c9bf3f --- /dev/null +++ b/src/events/easterEggs/Crazy.ts @@ -0,0 +1,17 @@ +import type { Message } from "discord.js"; + +export default class Crazy { + async run(message: Message) { + const crazy = message.content.toLowerCase().split("crazy"); + + if (crazy[1] == null) return; + if ( + ((crazy[0].endsWith(" ") || crazy[0].endsWith("")) && crazy[1].startsWith(" ")) || + message.content.toLowerCase() === "crazy" + ) { + await message.channel.send( + "Crazy? I was crazy once.\nThey locked me in a room.\nA rubber room.\nA rubber room with rats.\nAnd rats make me crazy.\nCrazy? I was crazy once..." + ); + } + } +} From dcb7d4dc87687492dc8b124ffbd6d2ec9fa7e6f2 Mon Sep 17 00:00:00 2001 From: Serge Date: Sat, 20 Apr 2024 23:40:57 +0500 Subject: [PATCH 055/127] implement JSON type and stuff like that --- src/commands/about.ts | 2 +- src/commands/user.ts | 75 ++++++++++++++++------------------ src/events/messageCreate.ts | 2 +- src/utils/database/settings.ts | 6 +-- src/utils/database/types.ts | 3 +- 5 files changed, 41 insertions(+), 47 deletions(-) diff --git a/src/commands/about.ts b/src/commands/about.ts index 1ffd310..0867878 100644 --- a/src/commands/about.ts +++ b/src/commands/about.ts @@ -40,7 +40,7 @@ export default class About { "**Developers**: Dimkauzh, Froxcey, Golem64, Spectrum, ThatBOI", "**Designers**: ArtyH, Optix, proJM, Slider_on_the_black", "**Translators**: Dimkauzh, Golem64, Optix, SaFire, ThatBOI, Trynera", - "**Testers**: Blaze, fishy, flojo, GrandSenna, Trynera", + "**Testers**: Blaze, fishy, flojo, Trynera", "And **YOU**, for using Sokora." ].join("\n") }, diff --git a/src/commands/user.ts b/src/commands/user.ts index e35635e..2732b24 100644 --- a/src/commands/user.ts +++ b/src/commands/user.ts @@ -5,12 +5,9 @@ import { ButtonStyle, ButtonInteraction, ActionRowBuilder, - ComponentType, - type ChatInputCommandInteraction, - type Role + type ChatInputCommandInteraction } from "discord.js"; import { genColor } from "../utils/colorGen"; -import { get as getLevelRewards } from "../utils/database/levelRewards"; import { getSetting } from "../utils/database/settings"; import { getLevel, setLevel } from "../utils/database/levelling"; import { imageColor } from "../utils/imageColor"; @@ -81,27 +78,18 @@ export default class User { .setColor(genColor(200)); imageColor(embed, undefined, target); - const reply = await interaction.reply({ embeds: [embed], components: [] }); + await interaction.reply({ embeds: [embed], components: [] }); if (!getSetting(`${guild.id}`, "levelling.enabled" || selectedUser.bot)) return; const [guildExp, guildLevel] = getLevel(`${guild.id}`, `${target.id}`)!; + const [globalExp, globalLevel] = getLevel("0", `${target.id}`)!; if (!guildExp && !guildLevel) setLevel(`${guild.id}`, `${target.id}`, 0, 0); + if (!globalExp && !globalLevel) setLevel("0", `${target.id}`, 0, 0); - const formattedExpUntilLevelup = Math.floor( - 100 * 1.25 * ((guildLevel ?? 0) + 1) - )?.toLocaleString("en-US"); - let rewards: (void | Role | null)[] = []; - let nextReward; - - for (const { roleID, level } of getLevelRewards(`${guild.id}`)) { - if (guildLevel < level) { - if (nextReward) break; - nextReward = { roleID, level }; - break; - } - - rewards.push(await guild.roles.fetch(`${roleID}`)?.catch(() => {})); - } + const nextLevelExp = Math.floor(100 * 1.15 * ((guildLevel ?? 0) + 1))?.toLocaleString("en-US"); + const globalNextLevelExp = Math.floor(100 * 1.15 * ((globalLevel ?? 0) + 1))?.toLocaleString( + "en-US" + ); const row = new ActionRowBuilder().addComponents( new ButtonBuilder() @@ -116,41 +104,48 @@ export default class User { .setStyle(ButtonStyle.Primary) ); - reply.edit({ embeds: [embed], components: [row] }); - reply - .createMessageComponentCollector({ componentType: ComponentType.Button, time: 60000 }) + await interaction.editReply({ embeds: [embed], components: [row] }); + interaction.channel + ?.createMessageComponentCollector({ + filter: i => i.user.id === interaction.user.id, + time: 60000 + }) .on("collect", async (i: ButtonInteraction) => { - let testEmbed: EmbedBuilder = new EmbedBuilder(); const levelEmbed = new EmbedBuilder() .setAuthor({ name: `• ${target.nickname ?? selectedUser.displayName}`, iconURL: target.displayAvatarURL() }) - .setFields({ - name: `⚡ • Level ${guildLevel ?? 0}`, - value: [ - `**${guildExp.toLocaleString("en-US") ?? 0}/${formattedExpUntilLevelup}** EXP`, - `**Next level**: ${(guildLevel ?? 0) + 1}`, - `${ - rewards.length > 0 - ? rewards.map(reward => `<@&${reward?.id}>`).join(" ") - : "*No rewards unlocked*" - }` - ].join("\n") - }) + .setFields( + { + name: `⚡ • Guild level ${guildLevel ?? 0}`, + value: [ + `**${guildExp.toLocaleString("en-US") ?? 0}/${nextLevelExp}** EXP`, + `**Next level**: ${(guildLevel ?? 0) + 1}` + ].join("\n"), + inline: true + }, + { + name: `⛈️ • Global level ${globalLevel ?? 0}`, + value: [ + `**${globalExp.toLocaleString("en-US") ?? 0}/${globalNextLevelExp}** EXP`, + `**Next level**: ${(globalLevel ?? 0) + 1}` + ].join("\n"), + inline: true + } + ) .setFooter({ text: `User ID: ${target.id}` }) - .setThumbnail(target.displayAvatarURL()!) + .setThumbnail(target.displayAvatarURL()) .setColor(genColor(200)); imageColor(levelEmbed, undefined, target); switch (i.customId) { case "general": - testEmbed = embed; + await interaction.editReply({ embeds: [embed], components: [row] }); case "level": - testEmbed = levelEmbed; + await interaction.editReply({ embeds: [levelEmbed], components: [row] }); } - await reply.edit({ embeds: [testEmbed], components: [row] }); i.update({}); }); } diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts index e432019..130094c 100644 --- a/src/events/messageCreate.ts +++ b/src/events/messageCreate.ts @@ -37,7 +37,7 @@ export default { const [guildExp, guildLevel] = getLevel(guild.id, author.id); const [globalExp, globalLevel] = getLevel("0", author.id); const expPerMessage = 2; - const expUntilLevelup = Math.floor(100 * 1.25 * (guildLevel + 1)); + const expUntilLevelup = Math.floor(100 * 1.15 * (guildLevel + 1)); const newLevelData = { level: guildLevel ?? 0, exp: (guildExp ?? 0) + expPerMessage }; const globalNewLevelData = { level: globalLevel ?? 0, exp: (globalExp ?? 0) + expPerMessage }; diff --git a/src/utils/database/settings.ts b/src/utils/database/settings.ts index b92270e..745bd70 100644 --- a/src/utils/database/settings.ts +++ b/src/utils/database/settings.ts @@ -1,4 +1,3 @@ -// TODO: make the LIST type work import { getDatabase } from "."; import { FieldData, SqlType, TableDefinition, TypeOfDefinition } from "./types"; @@ -16,6 +15,7 @@ export const settingsDefinition = { "levelling.channel": "TEXT", "levelling.blockChannels": "TEXT", "levelling.setLevel": "INTEGER", + "levelling.addMultiplier": "JSON", "moderation.channel": "TEXT", "moderation.logMessages": "BOOL", "news.channelID": "TEXT", @@ -52,8 +52,8 @@ export function getSetting( return res[0].value as TypeOfKey; case "BOOL": return (res[0].value == "true") as TypeOfKey; - // case "LIST": - // return (res[0].value.split(",") as unknown) as TypeOfKey; + case "JSON": + return res[0].value.split("," && ", ") as TypeOfKey; default: return "WIP" as TypeOfKey; } diff --git a/src/utils/database/types.ts b/src/utils/database/types.ts index b3623a2..38f4c08 100644 --- a/src/utils/database/types.ts +++ b/src/utils/database/types.ts @@ -1,4 +1,4 @@ -export type FieldData = "TEXT" | "LIST" | "INTEGER" | "FLOAT" | "BOOL" | "TIMESTAMP" | "JSON"; +export type FieldData = "TEXT" | "INTEGER" | "FLOAT" | "BOOL" | "TIMESTAMP" | "JSON"; export type TableDefinition = { name: string; @@ -10,7 +10,6 @@ export type SqlType = { INTEGER: number; FLOAT: number; TEXT: string; - LIST: string[]; TIMESTAMP: Date; JSON: any; }[T]; From a33739c11d7b9441248796969eb3e06af8044e42 Mon Sep 17 00:00:00 2001 From: Serge Date: Sun, 21 Apr 2024 12:40:54 +0500 Subject: [PATCH 056/127] suffer time --- src/events/messageCreate.ts | 13 +++++++++++-- src/utils/colorGen.ts | 1 - src/utils/database/settings.ts | 6 ++---- src/utils/imageColor.ts | 7 +++++++ src/utils/kominator.ts | 8 ++++++++ src/utils/multiReact.ts | 9 +++++++-- 6 files changed, 35 insertions(+), 9 deletions(-) create mode 100644 src/utils/kominator.ts diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts index 130094c..5f21993 100644 --- a/src/events/messageCreate.ts +++ b/src/events/messageCreate.ts @@ -6,6 +6,7 @@ import { genColor } from "../utils/colorGen"; import { getSetting } from "../utils/database/settings"; import { getLevel, setLevel } from "../utils/database/levelling"; import { get as getLevelRewards } from "../utils/database/levelRewards"; +import { kominator } from "../utils/kominator"; export default { name: "messageCreate", @@ -16,7 +17,7 @@ export default { const guild = message.guild!; // Easter egg handler - if (guild.id === "903852579837059113") { + if (guild.id === "1079612082636472420") { const eventsPath = join(process.cwd(), "src", "events", "easterEggs"); for (const easterEggFile of readdirSync(eventsPath)) @@ -28,13 +29,21 @@ export default { // Levelling if (!getSetting(guild.id, "levelling.enabled")) return; + let [guildExp, guildLevel] = getLevel(guild.id, author.id); + const newLevel = getSetting(guild.id, "levelling.setLevel")!; + console.log(kominator(newLevel)); + if (newLevel[0] === author.id) + if (newLevel[1] != null) { + let level = guildLevel.toString(); + level = newLevel; + } + const blockedChannels = getSetting(guild.id, "levelling.blockChannels")!; if (blockedChannels != undefined) for (const channelID of blockedChannels.split(", ")) if (message.channelId === channelID) return; const levelChannelId = getSetting(guild.id, "levelling.channel"); - const [guildExp, guildLevel] = getLevel(guild.id, author.id); const [globalExp, globalLevel] = getLevel("0", author.id); const expPerMessage = 2; const expUntilLevelup = Math.floor(100 * 1.15 * (guildLevel + 1)); diff --git a/src/utils/colorGen.ts b/src/utils/colorGen.ts index b6544e6..f29e83e 100644 --- a/src/utils/colorGen.ts +++ b/src/utils/colorGen.ts @@ -3,7 +3,6 @@ * @param hue Color to randomise. * @returns Color in HEX. */ - export function genColor(hue: number) { const h = hue + 15 * Math.random(); let s = 100; diff --git a/src/utils/database/settings.ts b/src/utils/database/settings.ts index 745bd70..5e0413b 100644 --- a/src/utils/database/settings.ts +++ b/src/utils/database/settings.ts @@ -14,8 +14,8 @@ export const settingsDefinition = { "levelling.enabled": "BOOL", "levelling.channel": "TEXT", "levelling.blockChannels": "TEXT", - "levelling.setLevel": "INTEGER", - "levelling.addMultiplier": "JSON", + "levelling.setLevel": "TEXT", + "levelling.addMultiplier": "TEXT", "moderation.channel": "TEXT", "moderation.logMessages": "BOOL", "news.channelID": "TEXT", @@ -52,8 +52,6 @@ export function getSetting( return res[0].value as TypeOfKey; case "BOOL": return (res[0].value == "true") as TypeOfKey; - case "JSON": - return res[0].value.split("," && ", ") as TypeOfKey; default: return "WIP" as TypeOfKey; } diff --git a/src/utils/imageColor.ts b/src/utils/imageColor.ts index ce1022c..95a96c2 100644 --- a/src/utils/imageColor.ts +++ b/src/utils/imageColor.ts @@ -3,6 +3,13 @@ import Vibrant from "node-vibrant"; import sharp from "sharp"; import { genRGBColor } from "./colorGen"; +/** + * Outputs and sets the most vibrant color from the image. + * @param embed Embed to set the color to. + * @param guild Guild image. + * @param member Member image. + * @returns Embned with the set color. + */ export async function imageColor(embed: EmbedBuilder, guild?: Guild, member?: GuildMember) { try { const imageBuffer = await ( diff --git a/src/utils/kominator.ts b/src/utils/kominator.ts new file mode 100644 index 0000000..afcf17d --- /dev/null +++ b/src/utils/kominator.ts @@ -0,0 +1,8 @@ +/** + * Splits a string using commas. + * @param string String to split. + * @returns An array of strings from the separated string. + */ +export function kominator(string: string): string[] { + return string.split(",").map(str => str.trim()); +} diff --git a/src/utils/multiReact.ts b/src/utils/multiReact.ts index 48b3274..904ead7 100644 --- a/src/utils/multiReact.ts +++ b/src/utils/multiReact.ts @@ -1,7 +1,12 @@ import type { Message } from "discord.js"; -export async function multiReact(message: Message, ...reactions: string[]) { - for (const i of reactions) { +/** + * Reacts to a message with multiple emojis. + * @param message Message to react to. + * @param emojis Emojis that will be used to react. + */ +export async function multiReact(message: Message, ...emojis: string[]) { + for (const i of emojis) { if (typeof i === "object") { await message.react(i); continue; From 4bcb732c89671d141171f5aae2d7780c5436c956 Mon Sep 17 00:00:00 2001 From: Serge Date: Sun, 21 Apr 2024 13:14:02 +0500 Subject: [PATCH 057/127] america ya :D --- src/events/easterEggs/AmericaYa.ts | 15 +++++++++++++++ src/events/messageCreate.ts | 16 ++++++---------- 2 files changed, 21 insertions(+), 10 deletions(-) create mode 100644 src/events/easterEggs/AmericaYa.ts diff --git a/src/events/easterEggs/AmericaYa.ts b/src/events/easterEggs/AmericaYa.ts new file mode 100644 index 0000000..0a9ca51 --- /dev/null +++ b/src/events/easterEggs/AmericaYa.ts @@ -0,0 +1,15 @@ +import type { Message } from "discord.js"; +import { randomise } from "../../utils/randomise"; + +export default class AmericaYa { + async run(message: Message) { + if (message.content.toLowerCase().includes("america ya")) { + const response = randomise([ + "HALLO :D HALLO :D HALLO :D HALLO :D", + "https://tenor.com/view/america-ya-gif-15374592095658975433" + ]); + + await message.channel.send(response); + } + } +} diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts index 5f21993..e4c10ba 100644 --- a/src/events/messageCreate.ts +++ b/src/events/messageCreate.ts @@ -6,7 +6,6 @@ import { genColor } from "../utils/colorGen"; import { getSetting } from "../utils/database/settings"; import { getLevel, setLevel } from "../utils/database/levelling"; import { get as getLevelRewards } from "../utils/database/levelRewards"; -import { kominator } from "../utils/kominator"; export default { name: "messageCreate", @@ -31,12 +30,9 @@ export default { let [guildExp, guildLevel] = getLevel(guild.id, author.id); const newLevel = getSetting(guild.id, "levelling.setLevel")!; - console.log(kominator(newLevel)); - if (newLevel[0] === author.id) - if (newLevel[1] != null) { - let level = guildLevel.toString(); - level = newLevel; - } + const level = parseInt(newLevel[1]); + if (newLevel[1] != null) + setLevel(guild.id, newLevel[0], level, Math.floor(100 * 1.15 * (level + 1))); const blockedChannels = getSetting(guild.id, "levelling.blockChannels")!; if (blockedChannels != undefined) @@ -51,15 +47,15 @@ export default { const globalNewLevelData = { level: globalLevel ?? 0, exp: (globalExp ?? 0) + expPerMessage }; if (guildExp < expUntilLevelup - 1) { - setLevel(0, author.id, globalNewLevelData.exp, globalNewLevelData.level); - return setLevel(guild.id, author.id, globalNewLevelData.exp, globalNewLevelData.level); + setLevel(0, author.id, globalNewLevelData.level, globalNewLevelData.exp); + return setLevel(guild.id, author.id, globalNewLevelData.level, globalNewLevelData.exp); } else if (guildExp >= expUntilLevelup - 1) { let leftOverExp = guildExp - expUntilLevelup; if (leftOverExp < 0) leftOverExp = 0; newLevelData.exp = leftOverExp ?? 0; newLevelData.level = guildLevel + 1; - setLevel(guild.id, author.id, newLevelData.exp, newLevelData.level); + setLevel(guild.id, author.id, newLevelData.level, newLevelData.exp); } if (guildExp >= Math.floor(100 * 1.25 * (globalLevel + 1)) - 1) { From 738a8521ea1cf0fb8aa0c6ce66b57af0997528e4 Mon Sep 17 00:00:00 2001 From: ThatBoiDev Date: Tue, 23 Apr 2024 09:47:39 +0200 Subject: [PATCH 058/127] add descriptions for settings, wip --- src/utils/database/settings.ts | 36 +++++++++++++++++----------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/utils/database/settings.ts b/src/utils/database/settings.ts index 5e0413b..f9d04b0 100644 --- a/src/utils/database/settings.ts +++ b/src/utils/database/settings.ts @@ -11,22 +11,22 @@ const tableDefinition = { } satisfies TableDefinition; export const settingsDefinition = { - "levelling.enabled": "BOOL", - "levelling.channel": "TEXT", - "levelling.blockChannels": "TEXT", - "levelling.setLevel": "TEXT", - "levelling.addMultiplier": "TEXT", - "moderation.channel": "TEXT", - "moderation.logMessages": "BOOL", - "news.channelID": "TEXT", - "news.roleID": "TEXT", - "news.editOriginalMessage": "BOOL", - "serverboard.inviteLink": "TEXT", - "serverboard.shown": "BOOL", - "welcome.text": "TEXT", - "welcome.goodbyeText": "TEXT", - "welcome.channel": "TEXT" -} satisfies Record; + "levelling.enabled": ["BOOL", "Enable or disable the levelling system"], + "levelling.channel": ["TEXT", "The channel ID(s) of the channels where messages are counted for levelling, comma seperated"], + "levelling.blockChannels": ["TEXT", "The channel ID(s) of the channels where messages are not counted for levelling, comma seperated"], + "levelling.setLevel": ["TEXT", "Set the level of an user"], + "levelling.addMultiplier": ["TEXT", "Add an XP multiplier to the levelling system"], + "moderation.channel": ["TEXT", "The channel where moderation logs are sent"], + "moderation.logMessages": ["BOOL", "Whether or not deleted or edited messages should be logged"], + "news.channelID": ["TEXT", "The channel ID(s) of the channels where news messages are sent, comma seperated"], + "news.roleID": ["TEXT", "The role ID(s) of the roles that should be pinged when a news message is sent, comma seperated"], + "news.editOriginalMessage": ["BOOL", "Whether or not the original message should be edited when a news message is updated"], + "serverboard.inviteLink": ["TEXT", "The invite link which should be shown on the serverboard"], + "serverboard.shown": ["BOOL", "Whether or not the server should be shown on the serverboard"], + "welcome.text": ["TEXT", "The welcome message that should be sent when a user joins, leave blank for nothing"], + "welcome.goodbyeText": ["TEXT", "The goodbye message that should be sent when a user leaves, leave blank for nothing"], + "welcome.channel": ["TEXT", "The channel ID(s) of the channels where welcome messages are sent, comma seperated"], +} satisfies Record; export const settingsKeys = Object.keys(settingsDefinition) as (keyof typeof settingsDefinition)[]; const database = getDatabase(tableDefinition); @@ -47,7 +47,7 @@ export function getSetting( typeof tableDefinition >[]; if (res.length == 0) return null; - switch (settingsDefinition[key]) { + switch (settingsDefinition[key][0]) { case "TEXT" || "INTEGER": return res[0].value as TypeOfKey; case "BOOL": @@ -66,7 +66,7 @@ export function setSetting( if (!doInsert) { deleteQuery.all(JSON.stringify(guildID), key); } - insertQuery.run(JSON.stringify(guildID), key, value); + insertQuery.run(JSON.stringify(guildID), key, JSON.stringify(value)); } export function listPublicServers() { From 004d43c1136cc4f27e0799dae5a84d008e597420 Mon Sep 17 00:00:00 2001 From: Serge Date: Tue, 23 Apr 2024 23:03:20 +0500 Subject: [PATCH 059/127] some more cool stuff Co-authored-by: Mart Zielman --- src/commands/moderation/mute.ts | 2 +- src/commands/moderation/purge.ts | 6 +- src/commands/moderation/slowdown.ts | 99 +++++++++++++++++++++++++++++ src/commands/settings.ts | 2 +- src/events/messageCreate.ts | 3 +- src/utils/database/settings.ts | 34 +++++----- src/utils/database/types.ts | 4 +- src/utils/kominator.ts | 2 +- 8 files changed, 127 insertions(+), 25 deletions(-) create mode 100644 src/commands/moderation/slowdown.ts diff --git a/src/commands/moderation/mute.ts b/src/commands/moderation/mute.ts index c2b0a5f..f40374d 100644 --- a/src/commands/moderation/mute.ts +++ b/src/commands/moderation/mute.ts @@ -25,7 +25,7 @@ export default class Mute { .addStringOption(string => string .setName("duration") - .setDescription("The duration of the mute (e.g 30m, 1d, 2h)") + .setDescription("The duration of the mute (e.g 30m, 1d, 2h).") .setRequired(true) ) .addStringOption(string => diff --git a/src/commands/moderation/purge.ts b/src/commands/moderation/purge.ts index cdb9799..aa2573d 100644 --- a/src/commands/moderation/purge.ts +++ b/src/commands/moderation/purge.ts @@ -30,7 +30,8 @@ export default class Purge { .addChannelTypes( ChannelType.GuildText, ChannelType.PublicThread, - ChannelType.PrivateThread + ChannelType.PrivateThread, + ChannelType.GuildVoice ) ); } @@ -67,7 +68,8 @@ export default class Purge { if ( channel.type === ChannelType.GuildText && ChannelType.PublicThread && - ChannelType.PrivateThread + ChannelType.PrivateThread && + ChannelType.GuildVoice ) channel == interaction.channel ? await channel.bulkDelete(amount + 1, true) diff --git a/src/commands/moderation/slowdown.ts b/src/commands/moderation/slowdown.ts new file mode 100644 index 0000000..c7a59d2 --- /dev/null +++ b/src/commands/moderation/slowdown.ts @@ -0,0 +1,99 @@ +import { + SlashCommandSubcommandBuilder, + EmbedBuilder, + PermissionsBitField, + ChannelType, + TextChannel, + type Channel, + type ChatInputCommandInteraction +} from "discord.js"; +import { genColor } from "../../utils/colorGen"; +import { errorEmbed } from "../../utils/embeds/errorEmbed"; +import { getSetting } from "../../utils/database/settings"; +import ms from "ms"; + +export default class Slowdown { + data: SlashCommandSubcommandBuilder; + constructor() { + this.data = new SlashCommandSubcommandBuilder() + .setName("slowdown") + .setDescription("Slows a channel down.") + .addStringOption(string => + string + .setName("time") + .setDescription("Time to slow the channel down to (e.g 30m, 1d, 2h).") + .setRequired(true) + ) + .addStringOption(string => + string.setName("reason").setDescription("The reason for the slowdown.") + ) + .addChannelOption(channel => + channel + .setName("channel") + .setDescription("The channel that you want to slowdown.") + .addChannelTypes( + ChannelType.GuildText, + ChannelType.PublicThread, + ChannelType.PrivateThread, + ChannelType.GuildVoice + ) + ); + } + + async run(interaction: ChatInputCommandInteraction) { + const guild = interaction.guild!; + const time = interaction.options.getString("time")!; + const member = guild.members.cache.get(interaction.member?.user.id!)!; + + if (!member.permissions.has(PermissionsBitField.Flags.ManageMessages)) + return errorEmbed( + interaction, + "You can't execute this command", + "You need the **Manage Channels** permission." + ); + + const channelOption = interaction.options.getChannel("channel")!; + const channel = guild.channels.cache.get(interaction.channel?.id ?? channelOption.id)!; + + let title = `Set a slowdown of \`${channelOption ?? `${channel.name}`}\` to ${ms(ms(time), { + long: true + })}.`; + if (ms(time) === 0) + title = `Removed the slowdown from \`${channelOption ?? `${channel.name}`}\`.`; + + const embed = new EmbedBuilder() + .setTitle(title) + .setDescription( + [ + `**Moderator**: ${interaction.user.username}`, + `**Reason**: ${interaction.options.getString("reason") ?? "No reason provided"}`, + `**Channel**: ${channelOption ?? `<#${channel.id}>`}` + ].join("\n") + ) + .setColor(genColor(100)); + + if ( + channel.type === ChannelType.GuildText && + ChannelType.PublicThread && + ChannelType.PrivateThread && + ChannelType.GuildVoice + ) + await channel.setRateLimitPerUser(ms(time) / 1000, interaction.options.getString("reason")!); + + const logChannel = getSetting(guild.id, "moderation.channel"); + if (logChannel) { + const channel = await guild.channels.cache + .get(`${logChannel}`) + ?.fetch() + .then((channel: Channel) => { + if (channel.type != ChannelType.GuildText) return null; + return channel as TextChannel; + }) + .catch(() => null); + + if (channel) await channel.send({ embeds: [embed] }); + } + + await interaction.reply({ embeds: [embed] }); + } +} diff --git a/src/commands/settings.ts b/src/commands/settings.ts index b30a97d..00ba59d 100644 --- a/src/commands/settings.ts +++ b/src/commands/settings.ts @@ -68,7 +68,7 @@ export default class Settings { if (interaction.type != InteractionType.ApplicationCommandAutocomplete) return; if (interaction.options.getSubcommand() != this.data.name) return; switch ( - settingsDefinition[interaction.options.get("key")!.value as keyof typeof settingsDefinition] + settingsDefinition[interaction.options.get("key")!.value as keyof typeof settingsDefinition][0] ) { case "BOOL": interaction.respond( diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts index e4c10ba..1f65f3a 100644 --- a/src/events/messageCreate.ts +++ b/src/events/messageCreate.ts @@ -39,9 +39,10 @@ export default { for (const channelID of blockedChannels.split(", ")) if (message.channelId === channelID) return; + const cooldown = getSetting(guild.id, "levelling.setCooldown") ?? 4; const levelChannelId = getSetting(guild.id, "levelling.channel"); const [globalExp, globalLevel] = getLevel("0", author.id); - const expPerMessage = 2; + const expPerMessage = getSetting(guild.id, "levelling.setXPGain") ?? 2; const expUntilLevelup = Math.floor(100 * 1.15 * (guildLevel + 1)); const newLevelData = { level: guildLevel ?? 0, exp: (guildExp ?? 0) + expPerMessage }; const globalNewLevelData = { level: globalLevel ?? 0, exp: (globalExp ?? 0) + expPerMessage }; diff --git a/src/utils/database/settings.ts b/src/utils/database/settings.ts index f9d04b0..147da2c 100644 --- a/src/utils/database/settings.ts +++ b/src/utils/database/settings.ts @@ -11,21 +11,23 @@ const tableDefinition = { } satisfies TableDefinition; export const settingsDefinition = { - "levelling.enabled": ["BOOL", "Enable or disable the levelling system"], - "levelling.channel": ["TEXT", "The channel ID(s) of the channels where messages are counted for levelling, comma seperated"], - "levelling.blockChannels": ["TEXT", "The channel ID(s) of the channels where messages are not counted for levelling, comma seperated"], - "levelling.setLevel": ["TEXT", "Set the level of an user"], - "levelling.addMultiplier": ["TEXT", "Add an XP multiplier to the levelling system"], - "moderation.channel": ["TEXT", "The channel where moderation logs are sent"], - "moderation.logMessages": ["BOOL", "Whether or not deleted or edited messages should be logged"], - "news.channelID": ["TEXT", "The channel ID(s) of the channels where news messages are sent, comma seperated"], - "news.roleID": ["TEXT", "The role ID(s) of the roles that should be pinged when a news message is sent, comma seperated"], - "news.editOriginalMessage": ["BOOL", "Whether or not the original message should be edited when a news message is updated"], - "serverboard.inviteLink": ["TEXT", "The invite link which should be shown on the serverboard"], - "serverboard.shown": ["BOOL", "Whether or not the server should be shown on the serverboard"], - "welcome.text": ["TEXT", "The welcome message that should be sent when a user joins, leave blank for nothing"], - "welcome.goodbyeText": ["TEXT", "The goodbye message that should be sent when a user leaves, leave blank for nothing"], - "welcome.channel": ["TEXT", "The channel ID(s) of the channels where welcome messages are sent, comma seperated"], + "levelling.enabled": ["BOOL", "Enable/disable the levelling system."], + "levelling.channel": ["TEXT", "ID of the log channel for levelling-related stuff (i.e someone levelling up)."], + "levelling.blockChannels": ["TEXT", "ID(s) of the channels where messages aren't counted, comma separated."], + "levelling.setLevel": ["TEXT", "Set the level of a user."], + "levelling.addMultiplier": ["TEXT", "Add an XP multiplier to the levelling system. Syntax: multiplier, role/channel (choose), id."], + "levelling.setXPGain": ["INTEGER", "Set the amount of XP a user gains per message."], + "levelling.setCooldown": ["INTEGER", "Set the cooldown between messages that add XP."], + "moderation.channel": ["TEXT", "ID of the log channel for moderation-related stuff (i.e a message being edited)."], + "moderation.logMessages": ["BOOL", "Whether or not edited/deleted messages should be logged."], + "news.channelID": ["TEXT", "ID of the channel where news messages are sent."], + "news.roleID": ["TEXT", "ID of the roles that should be pinged when a news message is sent."], + "news.editOriginalMessage": ["BOOL", "Whether or not the original message should be edited when a news message is updated."], + "serverboard.inviteLink": ["TEXT", "The invite link which is shown on the serverboard."], + "serverboard.shown": ["BOOL", "Whether or not the server should be shown on the serverboard."], + "welcome.text": ["TEXT", "The welcome message that is sent when a user joins."], + "welcome.goodbyeText": ["TEXT", "The goodbye message that is sent when a user leaves."], + "welcome.channel": ["TEXT", "ID of the channel where welcome messages are sent."], } satisfies Record; export const settingsKeys = Object.keys(settingsDefinition) as (keyof typeof settingsDefinition)[]; @@ -76,4 +78,4 @@ export function listPublicServers() { } // Utility type -type TypeOfKey = SqlType<(typeof settingsDefinition)[T]>; +type TypeOfKey = SqlType<(typeof settingsDefinition)[T][0]>; diff --git a/src/utils/database/types.ts b/src/utils/database/types.ts index 38f4c08..8c3daa3 100644 --- a/src/utils/database/types.ts +++ b/src/utils/database/types.ts @@ -1,4 +1,4 @@ -export type FieldData = "TEXT" | "INTEGER" | "FLOAT" | "BOOL" | "TIMESTAMP" | "JSON"; +export type FieldData = "TEXT" | "INTEGER" | "BOOL" | "TIMESTAMP"; export type TableDefinition = { name: string; @@ -8,10 +8,8 @@ export type TableDefinition = { export type SqlType = { BOOL: boolean; INTEGER: number; - FLOAT: number; TEXT: string; TIMESTAMP: Date; - JSON: any; }[T]; export type TypeOfDefinition = { diff --git a/src/utils/kominator.ts b/src/utils/kominator.ts index afcf17d..1542165 100644 --- a/src/utils/kominator.ts +++ b/src/utils/kominator.ts @@ -1,7 +1,7 @@ /** * Splits a string using commas. * @param string String to split. - * @returns An array of strings from the separated string. + * @returns An array of strings from the original string. */ export function kominator(string: string): string[] { return string.split(",").map(str => str.trim()); From c94b7467f7b85ffc49c87bcf46d97e969e235475 Mon Sep 17 00:00:00 2001 From: Serge Date: Thu, 25 Apr 2024 21:17:47 +0500 Subject: [PATCH 060/127] setLevel works --- src/commands/settings.ts | 2 +- src/commands/user.ts | 8 ++++---- src/events/messageCreate.ts | 30 ++++++++++++++++++------------ 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/src/commands/settings.ts b/src/commands/settings.ts index 00ba59d..0bad8ac 100644 --- a/src/commands/settings.ts +++ b/src/commands/settings.ts @@ -52,7 +52,7 @@ export default class Settings { const value = interaction.options.get("value")?.value; if (value == undefined) return interaction.reply( - `\`${key}\` is currently \`${JSON.stringify(getSetting(interaction.guildId!, key))}\`` + `\`${key}\` is currently \`${getSetting(interaction.guildId!, key)}\`` ); const embed = new EmbedBuilder() diff --git a/src/commands/user.ts b/src/commands/user.ts index 2732b24..fb15175 100644 --- a/src/commands/user.ts +++ b/src/commands/user.ts @@ -81,10 +81,10 @@ export default class User { await interaction.reply({ embeds: [embed], components: [] }); if (!getSetting(`${guild.id}`, "levelling.enabled" || selectedUser.bot)) return; - const [guildExp, guildLevel] = getLevel(`${guild.id}`, `${target.id}`)!; - const [globalExp, globalLevel] = getLevel("0", `${target.id}`)!; - if (!guildExp && !guildLevel) setLevel(`${guild.id}`, `${target.id}`, 0, 0); - if (!globalExp && !globalLevel) setLevel("0", `${target.id}`, 0, 0); + const [guildLevel, guildExp] = getLevel(`${guild.id}`, `${target.id}`)!; + const [globalLevel, globalExp] = getLevel("0", `${target.id}`)!; + if (!guildLevel && !guildExp) setLevel(`${guild.id}`, `${target.id}`, 0, 0); + if (!globalLevel && !globalExp) setLevel("0", `${target.id}`, 0, 0); const nextLevelExp = Math.floor(100 * 1.15 * ((guildLevel ?? 0) + 1))?.toLocaleString("en-US"); const globalNextLevelExp = Math.floor(100 * 1.15 * ((globalLevel ?? 0) + 1))?.toLocaleString( diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts index 1f65f3a..30a32bc 100644 --- a/src/events/messageCreate.ts +++ b/src/events/messageCreate.ts @@ -3,9 +3,10 @@ import { pathToFileURL } from "url"; import { join } from "path"; import { readdirSync } from "fs"; import { genColor } from "../utils/colorGen"; -import { getSetting } from "../utils/database/settings"; +import { getSetting, setSetting } from "../utils/database/settings"; import { getLevel, setLevel } from "../utils/database/levelling"; import { get as getLevelRewards } from "../utils/database/levelRewards"; +import { kominator } from "../utils/kominator"; export default { name: "messageCreate", @@ -28,24 +29,29 @@ export default { // Levelling if (!getSetting(guild.id, "levelling.enabled")) return; - let [guildExp, guildLevel] = getLevel(guild.id, author.id); - const newLevel = getSetting(guild.id, "levelling.setLevel")!; - const level = parseInt(newLevel[1]); - if (newLevel[1] != null) - setLevel(guild.id, newLevel[0], level, Math.floor(100 * 1.15 * (level + 1))); + const level = getSetting(guild.id, "levelling.setLevel")!; + if (level != "") { + const newLevel = kominator(level); + console.log(newLevel[0]); + setLevel(guild.id, newLevel[0], +newLevel[1], 100 * +newLevel[1]); + setSetting(guild.id, "levelling.setLevel", ""); + } const blockedChannels = getSetting(guild.id, "levelling.blockChannels")!; if (blockedChannels != undefined) for (const channelID of blockedChannels.split(", ")) if (message.channelId === channelID) return; - const cooldown = getSetting(guild.id, "levelling.setCooldown") ?? 4; + // const cooldown = getSetting(guild.id, "levelling.setCooldown") ?? 4; const levelChannelId = getSetting(guild.id, "levelling.channel"); - const [globalExp, globalLevel] = getLevel("0", author.id); - const expPerMessage = getSetting(guild.id, "levelling.setXPGain") ?? 2; - const expUntilLevelup = Math.floor(100 * 1.15 * (guildLevel + 1)); - const newLevelData = { level: guildLevel ?? 0, exp: (guildExp ?? 0) + expPerMessage }; - const globalNewLevelData = { level: globalLevel ?? 0, exp: (globalExp ?? 0) + expPerMessage }; + const [guildLevel, guildExp] = getLevel(guild.id, author.id); + const [globalLevel, globalExp] = getLevel("0", author.id); + const expUntilLevelup = 100 * 1.15 * (guildLevel + 1); + const newLevelData = { + level: guildLevel ?? 0, + exp: (guildExp ?? 0) + getSetting(guild.id, "levelling.setXPGain")! ?? 2 + }; + const globalNewLevelData = { level: globalLevel ?? 0, exp: (globalExp ?? 0) + 2 }; if (guildExp < expUntilLevelup - 1) { setLevel(0, author.id, globalNewLevelData.level, globalNewLevelData.exp); From 486d43cb2a84282544983a86287da8cf59ce895a Mon Sep 17 00:00:00 2001 From: Serge Date: Sun, 28 Apr 2024 20:27:48 +0500 Subject: [PATCH 061/127] addMultiplier --- src/events/easterEggs/AmericaYa.ts | 13 ++++++------- src/events/easterEggs/Fan.ts | 15 +++++++-------- src/events/easterEggs/Fire.ts | 17 ++++++++--------- src/events/easterEggs/Honk.ts | 4 ++-- src/events/easterEggs/WhoPinged.ts | 25 ++++++++++++------------- src/events/messageCreate.ts | 13 ++++++++++++- src/utils/kominator.ts | 2 +- 7 files changed, 48 insertions(+), 41 deletions(-) diff --git a/src/events/easterEggs/AmericaYa.ts b/src/events/easterEggs/AmericaYa.ts index 0a9ca51..f632037 100644 --- a/src/events/easterEggs/AmericaYa.ts +++ b/src/events/easterEggs/AmericaYa.ts @@ -3,13 +3,12 @@ import { randomise } from "../../utils/randomise"; export default class AmericaYa { async run(message: Message) { - if (message.content.toLowerCase().includes("america ya")) { - const response = randomise([ - "HALLO :D HALLO :D HALLO :D HALLO :D", - "https://tenor.com/view/america-ya-gif-15374592095658975433" - ]); + if (!message.content.toLowerCase().includes("america ya")) return; + const response = randomise([ + "HALLO :D HALLO :D HALLO :D HALLO :D", + "https://tenor.com/view/america-ya-gif-15374592095658975433" + ]); - await message.channel.send(response); - } + await message.channel.send(response); } } diff --git a/src/events/easterEggs/Fan.ts b/src/events/easterEggs/Fan.ts index 1e46211..0af9c3b 100644 --- a/src/events/easterEggs/Fan.ts +++ b/src/events/easterEggs/Fan.ts @@ -3,14 +3,13 @@ import { randomise } from "../../utils/randomise"; export default class Fan { async run(message: Message) { - if (message.content.toLowerCase().includes("i'm a big fan")) { - const gifs = randomise([ - "https://tenor.com/bC37i.gif", - "https://tenor.com/view/fan-gif-20757784", - "https://tenor.com/view/below-deck-im-your-biggest-fan-biggest-fan-kate-kate-chastain-gif-15861715" - ]); + if (!message.content.toLowerCase().includes("i'm a big fan")) return; + const gifs = randomise([ + "https://tenor.com/bC37i.gif", + "https://tenor.com/view/fan-gif-20757784", + "https://tenor.com/view/below-deck-im-your-biggest-fan-biggest-fan-kate-kate-chastain-gif-15861715" + ]); - await message.channel.send(gifs); - } + await message.channel.send(gifs); } } diff --git a/src/events/easterEggs/Fire.ts b/src/events/easterEggs/Fire.ts index 365777d..baf13a4 100644 --- a/src/events/easterEggs/Fire.ts +++ b/src/events/easterEggs/Fire.ts @@ -3,15 +3,14 @@ import { randomise } from "../../utils/randomise"; export default class Fire { async run(message: Message) { - if (message.content.toLowerCase().includes("fire in the hole")) { - const gifs = randomise([ - "https://cdn.discordapp.com/attachments/799130520846991370/1080439680199315487/cat-chomp-fireball.gif?ex=65e8485d&is=65d5d35d&hm=cef8b83df8120f1419082f184d835c6af679c9d02d69f97e335eafa82b33489e&", - "https://tenor.com/view/dancing-gif-25178472", - "https://tenor.com/view/fire-in-the-hole-gif-11283103876805231056", - "https://tenor.com/view/fire-in-the-hole-gif-14799466830322850291" - ]); + if (!message.content.toLowerCase().includes("fire in the hole")) return; + const gifs = randomise([ + "https://cdn.discordapp.com/attachments/799130520846991370/1080439680199315487/cat-chomp-fireball.gif?ex=65e8485d&is=65d5d35d&hm=cef8b83df8120f1419082f184d835c6af679c9d02d69f97e335eafa82b33489e&", + "https://tenor.com/view/dancing-gif-25178472", + "https://tenor.com/view/fire-in-the-hole-gif-11283103876805231056", + "https://tenor.com/view/fire-in-the-hole-gif-14799466830322850291" + ]); - await message.channel.send(gifs); - } + await message.channel.send(gifs); } } diff --git a/src/events/easterEggs/Honk.ts b/src/events/easterEggs/Honk.ts index 4db639b..744f072 100644 --- a/src/events/easterEggs/Honk.ts +++ b/src/events/easterEggs/Honk.ts @@ -3,7 +3,7 @@ import type { Message } from "discord.js"; export default class Honk { async run(message: Message) { const honks = ["hnok", "hokn", "hkon", "onk", "hon", "honhk", "hhonk", "honkk"]; - if (honks.includes(message.content.toLowerCase())) - message.channel.send("https://tenor.com/bW8sm.gif"); + if (!honks.includes(message.content.toLowerCase())) return; + message.channel.send("https://tenor.com/bW8sm.gif"); } } diff --git a/src/events/easterEggs/WhoPinged.ts b/src/events/easterEggs/WhoPinged.ts index 7fab89b..22fb762 100644 --- a/src/events/easterEggs/WhoPinged.ts +++ b/src/events/easterEggs/WhoPinged.ts @@ -3,19 +3,18 @@ import { randomise } from "../../utils/randomise"; export default class WhoPinged { async run(message: Message) { - if (message.content.toLowerCase().includes(`<@${message.client.user.id}>`)) { - const gifs = randomise([ - "https://tenor.com/view/who-pinged-me-ping-discord-up-opening-door-gif-20065356", - "https://tenor.com/view/discord-who-pinged-me-who-pinged-me-gif-25140226", - "https://tenor.com/view/who-pinged-me-ping-discord-discord-ping-undertaker-gif-20399650", - "https://tenor.com/view/who-pinged-me-ping-tudou-mr-potato-cat-gif-22762448", - "https://tenor.com/view/me-when-someone-pings-me-sad-cursed-emoji-crying-gif-22784322", - "https://tenor.com/view/discord-triggered-notification-angry-dog-noises-dog-girl-gif-11710406", - "https://tenor.com/view/tense-table-smash-mad-gif-13656077", - "https://tenor.com/view/yakuza-kazuma-kiryu-pissed-off-gif-14586175" - ]); + if (!message.content.toLowerCase().includes(`<@${message.client.user.id}>`)) return; + const gifs = randomise([ + "https://tenor.com/view/who-pinged-me-ping-discord-up-opening-door-gif-20065356", + "https://tenor.com/view/discord-who-pinged-me-who-pinged-me-gif-25140226", + "https://tenor.com/view/who-pinged-me-ping-discord-discord-ping-undertaker-gif-20399650", + "https://tenor.com/view/who-pinged-me-ping-tudou-mr-potato-cat-gif-22762448", + "https://tenor.com/view/me-when-someone-pings-me-sad-cursed-emoji-crying-gif-22784322", + "https://tenor.com/view/discord-triggered-notification-angry-dog-noises-dog-girl-gif-11710406", + "https://tenor.com/view/tense-table-smash-mad-gif-13656077", + "https://tenor.com/view/yakuza-kazuma-kiryu-pissed-off-gif-14586175" + ]); - await message.channel.send(gifs); - } + await message.channel.send(gifs); } } diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts index 30a32bc..b1efae8 100644 --- a/src/events/messageCreate.ts +++ b/src/events/messageCreate.ts @@ -32,7 +32,6 @@ export default { const level = getSetting(guild.id, "levelling.setLevel")!; if (level != "") { const newLevel = kominator(level); - console.log(newLevel[0]); setLevel(guild.id, newLevel[0], +newLevel[1], 100 * +newLevel[1]); setSetting(guild.id, "levelling.setLevel", ""); } @@ -42,6 +41,18 @@ export default { for (const channelID of blockedChannels.split(", ")) if (message.channelId === channelID) return; + let expGain = getSetting(guild.id, "levelling.setXPGain")! ?? 2; + const multiplier = getSetting(guild.id, "levelling.addMultiplier")!; + if (multiplier != null) { + const expMultiplier = kominator(multiplier); + + if (expMultiplier[1] === "channel") if (message.channelId !== expMultiplier[2]) return; + if (expMultiplier[1] === "role") + if (!message.member?.roles.cache.has(expMultiplier[2])) return; + + expGain = expGain * +expMultiplier[0]; + } + // const cooldown = getSetting(guild.id, "levelling.setCooldown") ?? 4; const levelChannelId = getSetting(guild.id, "levelling.channel"); const [guildLevel, guildExp] = getLevel(guild.id, author.id); diff --git a/src/utils/kominator.ts b/src/utils/kominator.ts index 1542165..55a7e29 100644 --- a/src/utils/kominator.ts +++ b/src/utils/kominator.ts @@ -4,5 +4,5 @@ * @returns An array of strings from the original string. */ export function kominator(string: string): string[] { - return string.split(",").map(str => str.trim()); + return string.split(",").map(str => str.replace("\"", "").trim()); } From cee7735d1d08e0a0c500d1180a20fd69bb00c3d0 Mon Sep 17 00:00:00 2001 From: MrSerge01 <86667481+MrSerge01@users.noreply.github.com> Date: Mon, 10 Jun 2024 23:04:38 +0500 Subject: [PATCH 062/127] modEmbed hell begins now --- bun.lockb | Bin 59352 -> 57166 bytes package.json | 9 ++--- src/commands/about.ts | 6 +-- src/commands/moderation/ban.ts | 47 +++------------------- src/commands/moderation/mute.ts | 50 +++--------------------- src/commands/moderation/slowdown.ts | 4 +- src/utils/embeds/modEmbed.ts | 58 ++++++++++++++++++++++++++++ src/utils/types.ts | 7 ++++ 8 files changed, 84 insertions(+), 97 deletions(-) create mode 100644 src/utils/embeds/modEmbed.ts create mode 100644 src/utils/types.ts diff --git a/bun.lockb b/bun.lockb index 5bd5df125a85deff995c6a893c868e2b29418fad..25701689a124ea992c8a7fb360fad07fffe0cea7 100644 GIT binary patch delta 14026 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 diff --git a/package.json b/package.json index 56563f5..eddddb5 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,7 @@ { "name": "sokora", "description": "Welcome to Sokora, the multipurpose, multiplatform bot.", - "contributors": [ - "The Sokora team", - "The GitHub contributors" - ], + "contributors": ["The Sokora team", "The GitHub contributors"], "version": "0.1.0", "main": "./src/index.ts", "type": "module", @@ -12,10 +9,10 @@ "start": "bun ./src/index.ts" }, "dependencies": { - "discord.js": "^14.14.1", + "discord.js": "^14.15.2", "ms": "^2.1.3", "node-vibrant": "^3.2.1-alpha.1", - "sharp": "^0.33.2" + "sharp": "^0.33.4" }, "devDependencies": { "bun-types": "^1.0.30", diff --git a/src/commands/about.ts b/src/commands/about.ts index 0867878..c1f92d0 100644 --- a/src/commands/about.ts +++ b/src/commands/about.ts @@ -27,7 +27,7 @@ export default class About { { name: "📃 • General", value: [ - "**Version** 0.1, *the Antei update*", + "**Version** 0.1-preview, *Kaishi*", `**${members}** members • **${guilds.size}** guild${guilds.size === 1 ? "" : "s"} ${ shards == undefined ? "" : `• **${shards}** shard${shards === 1 ? "" : "s"}` }` @@ -37,10 +37,10 @@ export default class About { name: "🌌 • Entities involved", value: [ "**Head developer**: Goos", - "**Developers**: Dimkauzh, Froxcey, Golem64, Spectrum, ThatBOI", + "**Developers**: Dimkauzh, Froxcey, Golem64, Spectrum, Nikkerudon, ThatBOI", "**Designers**: ArtyH, Optix, proJM, Slider_on_the_black", "**Translators**: Dimkauzh, Golem64, Optix, SaFire, ThatBOI, Trynera", - "**Testers**: Blaze, fishy, flojo, Trynera", + "**Testers**: Blaze, fishy, flojo, Tech, Trynera", "And **YOU**, for using Sokora." ].join("\n") }, diff --git a/src/commands/moderation/ban.ts b/src/commands/moderation/ban.ts index 3c70a9c..7ae6ea8 100644 --- a/src/commands/moderation/ban.ts +++ b/src/commands/moderation/ban.ts @@ -1,16 +1,10 @@ import { SlashCommandSubcommandBuilder, - EmbedBuilder, PermissionsBitField, - TextChannel, - DMChannel, - ChannelType, - type Channel, type ChatInputCommandInteraction } from "discord.js"; -import { genColor } from "../../utils/colorGen"; import { errorEmbed } from "../../utils/embeds/errorEmbed"; -import { getSetting } from "../../utils/database/settings"; +import { modEmbed } from "../../utils/embeds/modEmbed"; export default class Ban { data: SlashCommandSubcommandBuilder; @@ -21,6 +15,9 @@ export default class Ban { .addUserOption(user => user.setName("user").setDescription("The user that you want to ban.").setRequired(true) ) + .addStringOption(string => + string.setName("duration").setDescription("The duration of the ban (e.g 30m, 1d, 2h).") + ) .addStringOption(string => string.setName("reason").setDescription("The reason for the ban.") ); @@ -60,41 +57,7 @@ export default class Ban { ); const reason = interaction.options.getString("reason"); - const embed = new EmbedBuilder() - .setAuthor({ name: `• ${name}`, iconURL: user.displayAvatarURL() }) - .setTitle(`Banned ${name}.`) - .setDescription( - [ - `**Moderator**: ${interaction.user.displayName}`, - `**Reason**: ${reason ?? "No reason provided"}` - ].join("\n") - ) - .setThumbnail(user.displayAvatarURL()) - .setFooter({ text: `User ID: ${user.id}` }) - .setColor(genColor(100)); - - const logChannel = getSetting(guild.id, "moderation.channel"); - if (logChannel) { - const channel = await guild.channels.cache - .get(`${logChannel}`) - ?.fetch() - .then((channel: Channel) => { - if (channel.type != ChannelType.GuildText) return null; - return channel as TextChannel; - }) - .catch(() => null); - - if (channel) await channel.send({ embeds: [embed] }); - } - await target.ban({ reason: reason ?? undefined }); - await interaction.reply({ embeds: [embed] }); - - const dmChannel = (await user.createDM().catch(() => null)) as DMChannel | null; - if (!dmChannel) return; - if (user.bot) return; - await dmChannel.send({ - embeds: [embed.setTitle("You got banned.").setColor(genColor(0))] - }); + await modEmbed({ interaction, user, action: "Banned", reason }); } } diff --git a/src/commands/moderation/mute.ts b/src/commands/moderation/mute.ts index f40374d..c636314 100644 --- a/src/commands/moderation/mute.ts +++ b/src/commands/moderation/mute.ts @@ -1,16 +1,10 @@ import { SlashCommandSubcommandBuilder, - EmbedBuilder, PermissionsBitField, - TextChannel, - DMChannel, - ChannelType, - type Channel, type ChatInputCommandInteraction } from "discord.js"; -import { genColor } from "../../utils/colorGen"; import { errorEmbed } from "../../utils/embeds/errorEmbed"; -import { getSetting } from "../../utils/database/settings"; +import { modEmbed } from "../../utils/embeds/modEmbed"; import ms from "ms"; export default class Mute { @@ -36,8 +30,8 @@ export default class Mute { async run(interaction: ChatInputCommandInteraction) { const user = interaction.options.getUser("user")!; const duration = interaction.options.getString("duration")!; - const guild = interaction.guild!; - const members = guild.members.cache!; + const reason = interaction.options.getString("reason"); + const members = interaction.guild?.members.cache!; const member = members.get(interaction.member?.user.id!)!; const target = members.get(user.id)!; const name = user.displayName; @@ -77,42 +71,8 @@ export default class Mute { const time = new Date( Date.parse(new Date().toISOString()) + Date.parse(new Date(ms(duration)).toISOString()) ).toISOString(); - const embed = new EmbedBuilder() - .setAuthor({ name: `• ${name}`, iconURL: user.displayAvatarURL() }) - .setTitle(`Muted ${name}.`) - .setDescription( - [ - `**Moderator**: ${interaction.user.displayName}`, - `**Duration**: ${ms(ms(duration), { long: true })}`, - `**Reason**: ${interaction.options.getString("reason") ?? "No reason provided"}` - ].join("\n") - ) - .setFooter({ text: `User ID: ${user.id}` }) - .setThumbnail(user.displayAvatarURL()) - .setColor(genColor(100)); - - const logChannel = getSetting(interaction.guildId!, "moderation.channel"); - if (logChannel) { - const channel = await guild.channels.cache - .get(`${logChannel}`) - ?.fetch() - .then((channel: Channel) => { - if (channel.type != ChannelType.GuildText) return null; - return channel as TextChannel; - }) - .catch(() => null); - - if (channel) await channel.send({ embeds: [embed] }); - } - - await target.edit({ communicationDisabledUntil: time }); - await interaction.reply({ embeds: [embed] }); - const dmChannel = (await user.createDM().catch(() => null)) as DMChannel | null; - if (!dmChannel) return; - if (user.bot) return; - await dmChannel.send({ - embeds: [embed.setTitle("You got muted.").setColor(genColor(0))] - }); + await target.edit({ communicationDisabledUntil: time, reason: reason ?? undefined }); + await modEmbed({ interaction, user, action: "Muted", duration, reason }); } } diff --git a/src/commands/moderation/slowdown.ts b/src/commands/moderation/slowdown.ts index c7a59d2..899bb29 100644 --- a/src/commands/moderation/slowdown.ts +++ b/src/commands/moderation/slowdown.ts @@ -21,7 +21,9 @@ export default class Slowdown { .addStringOption(string => string .setName("time") - .setDescription("Time to slow the channel down to (e.g 30m, 1d, 2h).") + .setDescription( + "Time to slow the channel down to (e.g 30m, 1d, 2h). 0 to remove slowdown." + ) .setRequired(true) ) .addStringOption(string => diff --git a/src/utils/embeds/modEmbed.ts b/src/utils/embeds/modEmbed.ts new file mode 100644 index 0000000..a14ed28 --- /dev/null +++ b/src/utils/embeds/modEmbed.ts @@ -0,0 +1,58 @@ +import { + EmbedBuilder, + TextChannel, + ChannelType, + type Channel, + type ChatInputCommandInteraction, + type User +} from "discord.js"; +import { getSetting } from "../database/settings"; +import { genColor } from "../colorGen"; +import ms from "ms"; + +type Options = { + interaction: ChatInputCommandInteraction; + user: User; + action: string; + duration?: string | null; + reason?: string | null; +}; + +export async function modEmbed(options: Options) { + const { interaction, user, action, duration, reason } = options; + const guild = interaction.guild!; + const name = user.displayName; + const generalValues = [`**Moderator**: ${interaction.user.displayName}`]; + if (duration) generalValues.push(`**Duration**: ${ms(ms(duration), { long: true })}`); + if (reason) generalValues.push(`**Reason**: ${reason}`); + + const embed = new EmbedBuilder() + .setAuthor({ name: `• ${name}`, iconURL: user.displayAvatarURL() }) + .setTitle(`${action} ${name}.`) + .setDescription(generalValues.join("\n")) + .setThumbnail(user.displayAvatarURL()) + .setFooter({ text: `User ID: ${user.id}` }) + .setColor(genColor(100)); + + const logChannel = getSetting(guild.id, "moderation.channel"); + if (logChannel) { + const channel = await guild.channels.cache + .get(`${logChannel}`) + ?.fetch() + .then((channel: Channel) => { + if (channel.type != ChannelType.GuildText) return null; + return channel as TextChannel; + }) + .catch(() => null); + + if (channel) await channel.send({ embeds: [embed] }); + } + + await interaction.reply({ embeds: [embed] }); + if (user.bot) return; + (await user.createDM()) + .send({ + embeds: [embed.setTitle(`You got ${action.toLowerCase()}.`).setColor(genColor(0))] + }) + .catch(() => null); +} diff --git a/src/utils/types.ts b/src/utils/types.ts new file mode 100644 index 0000000..f300e26 --- /dev/null +++ b/src/utils/types.ts @@ -0,0 +1,7 @@ +import type { ChannelType } from "discord.js"; + +export type TextChannels = + | ChannelType.GuildText + | ChannelType.GuildPublicThread + | ChannelType.GuildPrivateThread + | ChannelType.GuildVoice; From f37a82ad7611c10972d63db6fe3c433a4085b68e Mon Sep 17 00:00:00 2001 From: MrSerge01 <86667481+MrSerge01@users.noreply.github.com> Date: Fri, 14 Jun 2024 23:10:23 +0500 Subject: [PATCH 063/127] modEmbed cooking --- src/commands/moderation/ban.ts | 41 +++------------------- src/commands/moderation/mute.ts | 53 +++++++---------------------- src/utils/embeds/modEmbed.ts | 60 +++++++++++++++++++++++++++++---- 3 files changed, 72 insertions(+), 82 deletions(-) diff --git a/src/commands/moderation/ban.ts b/src/commands/moderation/ban.ts index 7ae6ea8..cafa01f 100644 --- a/src/commands/moderation/ban.ts +++ b/src/commands/moderation/ban.ts @@ -1,10 +1,9 @@ import { - SlashCommandSubcommandBuilder, PermissionsBitField, + SlashCommandSubcommandBuilder, type ChatInputCommandInteraction } from "discord.js"; -import { errorEmbed } from "../../utils/embeds/errorEmbed"; -import { modEmbed } from "../../utils/embeds/modEmbed"; +import { modEmbed, errorCheck } from "../../utils/embeds/modEmbed"; export default class Ban { data: SlashCommandSubcommandBuilder; @@ -25,39 +24,9 @@ export default class Ban { async run(interaction: ChatInputCommandInteraction) { const user = interaction.options.getUser("user")!; - const guild = interaction.guild!; - const members = guild.members.cache; - const member = members.get(interaction.member?.user.id!)!; - const target = members.get(user.id)!; - const name = user.displayName; - - if (!member.permissions.has(PermissionsBitField.Flags.BanMembers)) - return errorEmbed( - interaction, - "You can't execute this command.", - "You need the **Ban Members** permission." - ); - - if (target === member) return errorEmbed(interaction, "You can't ban yourself."); - if (target.user.id === interaction.client.user.id) - return errorEmbed(interaction, "You can't ban Sokora."); - - if (!target.manageable) - return errorEmbed( - interaction, - `You can't ban ${name}.`, - "The member has a higher role position than Sokora." - ); - - if (member.roles.highest.position < target.roles.highest.position) - return errorEmbed( - interaction, - `You can't ban ${name}.`, - "The member has a higher role position than you." - ); - + errorCheck(PermissionsBitField.Flags.BanMembers, { interaction, user, action: "Ban" }); const reason = interaction.options.getString("reason"); - await target.ban({ reason: reason ?? undefined }); - await modEmbed({ interaction, user, action: "Banned", reason }); + await interaction.guild?.members.cache.get(user.id)?.ban({ reason: reason ?? undefined }); + await modEmbed({ interaction, user, action: "Banned" }, reason); } } diff --git a/src/commands/moderation/mute.ts b/src/commands/moderation/mute.ts index c636314..e872409 100644 --- a/src/commands/moderation/mute.ts +++ b/src/commands/moderation/mute.ts @@ -1,11 +1,10 @@ import { - SlashCommandSubcommandBuilder, PermissionsBitField, + SlashCommandSubcommandBuilder, type ChatInputCommandInteraction } from "discord.js"; -import { errorEmbed } from "../../utils/embeds/errorEmbed"; -import { modEmbed } from "../../utils/embeds/modEmbed"; import ms from "ms"; +import { errorCheck, modEmbed } from "../../utils/embeds/modEmbed"; export default class Mute { data: SlashCommandSubcommandBuilder; @@ -31,48 +30,22 @@ export default class Mute { const user = interaction.options.getUser("user")!; const duration = interaction.options.getString("duration")!; const reason = interaction.options.getString("reason"); - const members = interaction.guild?.members.cache!; - const member = members.get(interaction.member?.user.id!)!; - const target = members.get(user.id)!; - const name = user.displayName; - - if (!member.permissions.has(PermissionsBitField.Flags.MuteMembers)) - return errorEmbed( - interaction, - "You can't execute this command.", - "You need the **Mute Members** permission." - ); - - if (target === member) return errorEmbed(interaction, "You can't mute yourself."); - if (target.user.id === interaction.client.user.id) - return errorEmbed(interaction, "You can't mute Sokora."); - if (!target.manageable) - return errorEmbed( - interaction, - `You can't mute ${name}.`, - "The member has a higher role position than Sokora." - ); - - if (member.roles.highest.position < target.roles.highest.position) - return errorEmbed( - interaction, - `You can't mute ${name}.`, - "The member has a higher role position than you." - ); - - if (!ms(duration) || ms(duration) > ms("28d")) - return errorEmbed( - interaction, - `You can't mute ${name}`, - "The duration is invalid or is above the 28 day limit." - ); + errorCheck(PermissionsBitField.Flags.MuteMembers, { + interaction, + user, + action: "Mute", + duration + }); const time = new Date( Date.parse(new Date().toISOString()) + Date.parse(new Date(ms(duration)).toISOString()) ).toISOString(); - await target.edit({ communicationDisabledUntil: time, reason: reason ?? undefined }); - await modEmbed({ interaction, user, action: "Muted", duration, reason }); + await interaction.guild?.members.cache + .get(user.id) + ?.edit({ communicationDisabledUntil: time, reason: reason ?? undefined }); + + await modEmbed({ interaction, user, action: "Muted", duration }, reason); } } diff --git a/src/utils/embeds/modEmbed.ts b/src/utils/embeds/modEmbed.ts index a14ed28..7cd4bec 100644 --- a/src/utils/embeds/modEmbed.ts +++ b/src/utils/embeds/modEmbed.ts @@ -1,27 +1,75 @@ import { + ChannelType, EmbedBuilder, TextChannel, - ChannelType, type Channel, type ChatInputCommandInteraction, type User } from "discord.js"; -import { getSetting } from "../database/settings"; -import { genColor } from "../colorGen"; import ms from "ms"; +import { genColor } from "../colorGen"; +import { getSetting } from "../database/settings"; +import { errorEmbed } from "./errorEmbed"; type Options = { interaction: ChatInputCommandInteraction; user: User; action: string; duration?: string | null; - reason?: string | null; }; -export async function modEmbed(options: Options) { - const { interaction, user, action, duration, reason } = options; +export async function errorCheck(permission: bigint, options: Options) { + const { interaction, user, action, duration } = options; + const guild = interaction.guild!; + const members = guild.members.cache!; + const member = members.get(interaction.member?.user.id!)!; + const target = members.get(user.id)!; + const name = user.displayName; + + if (!member.permissions.has(permission)) + return errorEmbed( + interaction, + "You can't execute this command.", + `You need the **${action} Members** permission.` + ); + + if (target === member) + return errorEmbed(interaction, `You can't ${action.toLowerCase()} yourself.`); + + if (target.user.id === interaction.client.user.id) + return errorEmbed(interaction, `You can't ${action.toLowerCase()} Sokora.`); + + if (!target.manageable) + return errorEmbed( + interaction, + `You can't ${action} ${name}.`, + "The member has a higher role position than Sokora." + ); + + if (member.roles.highest.position < target.roles.highest.position) + return errorEmbed( + interaction, + `You can't ${action} ${name}.`, + "The member has a higher role position than you." + ); + + if (duration) + if (!ms(duration) || ms(duration) > ms("28d")) + return errorEmbed( + interaction, + `You can't ${action} ${name}`, + "The duration is invalid or is above the 28 day limit." + ); + + if (member.id === guild.ownerId) + return errorEmbed(interaction, `You can't ban ${name}.`, "The member owns the server."); +} + +export async function modEmbed(options: Options, reason?: string | null) { + const { interaction, user, action, duration } = options; const guild = interaction.guild!; const name = user.displayName; + const generalValues = [`**Moderator**: ${interaction.user.displayName}`]; if (duration) generalValues.push(`**Duration**: ${ms(ms(duration), { long: true })}`); if (reason) generalValues.push(`**Reason**: ${reason}`); From 670df2ca444565f46a0c0ab9096dc12464d475d6 Mon Sep 17 00:00:00 2001 From: MrSerge01 <86667481+MrSerge01@users.noreply.github.com> Date: Fri, 21 Jun 2024 23:23:08 +0500 Subject: [PATCH 064/127] more modEmbed, more!!!! --- src/commands/moderation/ban.ts | 6 ++- src/commands/moderation/kick.ts | 81 +++------------------------------ src/commands/moderation/mute.ts | 3 +- src/commands/moderation/warn.ts | 78 ++++++------------------------- src/utils/embeds/modEmbed.ts | 21 ++++++--- 5 files changed, 42 insertions(+), 147 deletions(-) diff --git a/src/commands/moderation/ban.ts b/src/commands/moderation/ban.ts index cafa01f..1920103 100644 --- a/src/commands/moderation/ban.ts +++ b/src/commands/moderation/ban.ts @@ -26,7 +26,11 @@ export default class Ban { const user = interaction.options.getUser("user")!; errorCheck(PermissionsBitField.Flags.BanMembers, { interaction, user, action: "Ban" }); const reason = interaction.options.getString("reason"); - await interaction.guild?.members.cache.get(user.id)?.ban({ reason: reason ?? undefined }); + await interaction.guild?.members.cache + .get(user.id) + ?.ban({ reason: reason ?? undefined }) + .catch(error => console.error(error)); + await modEmbed({ interaction, user, action: "Banned" }, reason); } } diff --git a/src/commands/moderation/kick.ts b/src/commands/moderation/kick.ts index 14be288..572a23a 100644 --- a/src/commands/moderation/kick.ts +++ b/src/commands/moderation/kick.ts @@ -1,16 +1,9 @@ import { SlashCommandSubcommandBuilder, - EmbedBuilder, PermissionsBitField, - TextChannel, - DMChannel, - ChannelType, - type Channel, type ChatInputCommandInteraction } from "discord.js"; -import { genColor } from "../../utils/colorGen"; -import { errorEmbed } from "../../utils/embeds/errorEmbed"; -import { getSetting } from "../../utils/database/settings"; +import { errorCheck, modEmbed } from "../../utils/embeds/modEmbed"; export default class Kick { data: SlashCommandSubcommandBuilder; @@ -28,73 +21,13 @@ export default class Kick { async run(interaction: ChatInputCommandInteraction) { const user = interaction.options.getUser("user")!; - const guild = interaction.guild!; - const members = guild.members.cache!; - const member = members.get(interaction.member?.user.id!)!; - const target = members.get(user.id)!; - const name = user.displayName; - - if (!member.permissions.has(PermissionsBitField.Flags.KickMembers)) - return errorEmbed( - interaction, - "You can't execute this command.", - "You need the **Kick Members** permission." - ); - - if (target === member) return errorEmbed(interaction, "You can't kick yourself."); - if (target.user.id === interaction.client.user.id) - return errorEmbed(interaction, "You can't kick Sokora."); - - if (!target.manageable) - return errorEmbed( - interaction, - `You can't kick ${name}.`, - "The member has a higher role position than Sokora." - ); - - if (member.roles.highest.position < target.roles.highest.position) - return errorEmbed( - interaction, - `You can't kick ${name}.`, - "The member has a higher role position than you." - ); - + errorCheck(PermissionsBitField.Flags.KickMembers, { interaction, user, action: "Kick" }); const reason = interaction.options.getString("reason"); - const embed = new EmbedBuilder() - .setAuthor({ name: `• ${name}`, iconURL: user.displayAvatarURL() }) - .setTitle(`Kicked ${name}.`) - .setDescription( - [ - `**Moderator**: ${interaction.user.displayName}`, - `**Reason**: ${reason ?? "No reason provided"}` - ].join("\n") - ) - .setThumbnail(user.displayAvatarURL()) - .setFooter({ text: `User ID: ${user.id}` }) - .setColor(genColor(100)); - - const logChannel = getSetting(guild.id, "moderation.channel"); - if (logChannel) { - const channel = await guild.channels.cache - .get(`${logChannel}`) - ?.fetch() - .then((channel: Channel) => { - if (channel.type != ChannelType.GuildText) return null; - return channel as TextChannel; - }) - .catch(() => null); - - if (channel) await channel.send({ embeds: [embed] }); - } - - await target.kick(reason ?? undefined); - await interaction.reply({ embeds: [embed] }); + await interaction.guild?.members.cache + .get(user.id) + ?.kick(reason ?? undefined) + .catch(error => console.error(error)); - const dmChannel = (await user.createDM().catch(() => null)) as DMChannel | null; - if (!dmChannel) return; - if (user.bot) return; - await dmChannel.send({ - embeds: [embed.setTitle("You got kicked.").setColor(genColor(0))] - }); + modEmbed({ interaction, user, action: "Kicked" }, reason); } } diff --git a/src/commands/moderation/mute.ts b/src/commands/moderation/mute.ts index e872409..00f579e 100644 --- a/src/commands/moderation/mute.ts +++ b/src/commands/moderation/mute.ts @@ -44,7 +44,8 @@ export default class Mute { await interaction.guild?.members.cache .get(user.id) - ?.edit({ communicationDisabledUntil: time, reason: reason ?? undefined }); + ?.edit({ communicationDisabledUntil: time, reason: reason ?? undefined }) + .catch(error => console.error(error)); await modEmbed({ interaction, user, action: "Muted", duration }, reason); } diff --git a/src/commands/moderation/warn.ts b/src/commands/moderation/warn.ts index 5a06326..4705342 100644 --- a/src/commands/moderation/warn.ts +++ b/src/commands/moderation/warn.ts @@ -12,6 +12,7 @@ import { genColor } from "../../utils/colorGen"; import { errorEmbed } from "../../utils/embeds/errorEmbed"; import { getSetting } from "../../utils/database/settings"; import { addModeration } from "../../utils/database/moderation"; +import { errorCheck, modEmbed } from "../../utils/embeds/modEmbed"; export default class Warn { data: SlashCommandSubcommandBuilder; @@ -30,72 +31,21 @@ export default class Warn { async run(interaction: ChatInputCommandInteraction) { const user = interaction.options.getUser("user")!; const guild = interaction.guild!; - const members = guild.members.cache; - const member = members.get(interaction.member?.user.id!)!; - const target = members.get(user.id)!; - const name = user.displayName; - if (!member.permissions.has(PermissionsBitField.Flags.ModerateMembers)) - return errorEmbed( - interaction, - "You can't execute this command.", - "You need the **Moderate Members** permission." - ); - - if (target === member) return errorEmbed(interaction, "You can't warn yourself."); - if (target.user.id === interaction.client.user.id) - return errorEmbed(interaction, "You can't warn Sokora."); - - if (!target.manageable) - return errorEmbed( - interaction, - `You can't warn ${name}.`, - "The member has a higher role position than Sokora." - ); - - if (member.roles.highest.position < target.roles.highest.position) - return errorEmbed( - interaction, - `You can't warn ${name}.`, - "The member has a higher role position than you." - ); + errorCheck( + PermissionsBitField.Flags.ModerateMembers, + { interaction, user, action: "Warn" }, + "Moderate" + ); const reason = interaction.options.getString("reason"); - const embed = new EmbedBuilder() - .setAuthor({ name: `• ${name}`, iconURL: user.displayAvatarURL() }) - .setTitle(`Warned ${name}.`) - .setDescription( - [ - `**Moderator**: ${interaction.user.displayName}`, - `**Reason**: ${reason ?? "No reason provided"}` - ].join("\n") - ) - .setThumbnail(user.displayAvatarURL()) - .setFooter({ text: `User ID: ${user.id}` }) - .setColor(genColor(100)); - - const logChannel = getSetting(guild.id, "moderation.channel"); - if (logChannel) { - const channel = await guild.channels.cache - .get(`${logChannel}`) - ?.fetch() - .then((channel: Channel) => { - if (channel.type != ChannelType.GuildText) return null; - return channel as TextChannel; - }) - .catch(() => null); - - if (channel) await channel.send({ embeds: [embed] }); - } - - addModeration(guild.id, user.id, "WARN", member.id, reason ?? undefined); - await interaction.reply({ embeds: [embed] }); - - const dmChannel = (await user.createDM().catch(() => null)) as DMChannel | null; - if (!dmChannel) return; - if (user.bot) return; - await dmChannel.send({ - embeds: [embed.setTitle("You got warned.").setColor(genColor(0))] - }); + addModeration( + guild.id, + user.id, + "WARN", + guild.members.cache.get(interaction.member?.user.id!)?.id!, + reason ?? undefined + ); + modEmbed({ interaction, user, action: "Warned" }, reason); } } diff --git a/src/utils/embeds/modEmbed.ts b/src/utils/embeds/modEmbed.ts index 7cd4bec..33a1669 100644 --- a/src/utils/embeds/modEmbed.ts +++ b/src/utils/embeds/modEmbed.ts @@ -18,7 +18,7 @@ type Options = { duration?: string | null; }; -export async function errorCheck(permission: bigint, options: Options) { +export async function errorCheck(permission: bigint, options: Options, permissionAction?: string) { const { interaction, user, action, duration } = options; const guild = interaction.guild!; const members = guild.members.cache!; @@ -30,7 +30,7 @@ export async function errorCheck(permission: bigint, options: Options) { return errorEmbed( interaction, "You can't execute this command.", - `You need the **${action} Members** permission.` + `You need the **${permissionAction ?? action} Members** permission.` ); if (target === member) @@ -42,14 +42,14 @@ export async function errorCheck(permission: bigint, options: Options) { if (!target.manageable) return errorEmbed( interaction, - `You can't ${action} ${name}.`, + `You can't ${action.toLowerCase()} ${name}.`, "The member has a higher role position than Sokora." ); if (member.roles.highest.position < target.roles.highest.position) return errorEmbed( interaction, - `You can't ${action} ${name}.`, + `You can't ${action.toLowerCase()} ${name}.`, "The member has a higher role position than you." ); @@ -57,12 +57,16 @@ export async function errorCheck(permission: bigint, options: Options) { if (!ms(duration) || ms(duration) > ms("28d")) return errorEmbed( interaction, - `You can't ${action} ${name}`, + `You can't ${action.toLowerCase()} ${name}`, "The duration is invalid or is above the 28 day limit." ); if (member.id === guild.ownerId) - return errorEmbed(interaction, `You can't ban ${name}.`, "The member owns the server."); + return errorEmbed( + interaction, + `You can't ${action.toLowerCase()} ${name}.`, + "The member owns the server." + ); } export async function modEmbed(options: Options, reason?: string | null) { @@ -97,8 +101,11 @@ export async function modEmbed(options: Options, reason?: string | null) { } await interaction.reply({ embeds: [embed] }); + + const dmChannel = await user.createDM().catch(() => null); + if (!dmChannel) return; if (user.bot) return; - (await user.createDM()) + await dmChannel .send({ embeds: [embed.setTitle(`You got ${action.toLowerCase()}.`).setColor(genColor(0))] }) From c4c1a17775e3cb83d2b0a28f83ea2fa88404af02 Mon Sep 17 00:00:00 2001 From: Golem64 <65229557+Golem642@users.noreply.github.com> Date: Wed, 3 Jul 2024 00:20:31 +0200 Subject: [PATCH 065/127] i hate ts --- .gitignore | 2 +- bun.lockb | Bin 57166 -> 57166 bytes src/commands/moderation/delwarn.ts | 2 +- src/commands/moderation/purge.ts | 2 +- src/commands/moderation/slowdown.ts | 2 +- src/commands/moderation/unban.ts | 2 +- src/commands/moderation/unmute.ts | 2 +- src/commands/news/edit.ts | 6 +- src/commands/news/remove.ts | 2 +- src/commands/settings.ts | 122 ++++++++++++++++++++-------- src/commands/user.ts | 33 ++++---- src/events/error.ts | 2 +- src/events/guildMemberAdd.ts | 4 +- src/events/guildMemberRemove.ts | 4 +- src/events/interactionCreate.ts | 4 +- src/events/messageCreate.ts | 20 ++--- src/events/messageDelete.ts | 4 +- src/events/messageUpdate.ts | 4 +- src/utils/database/settings.ts | 76 ++++++++++------- src/utils/embeds/modEmbed.ts | 2 +- src/utils/embeds/serverEmbed.ts | 2 +- src/utils/sendChannelNews.ts | 4 +- 22 files changed, 187 insertions(+), 114 deletions(-) diff --git a/.gitignore b/.gitignore index 023011d..00ad2e5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ node_modules/ .env .DS_Store -data.db +data.db \ No newline at end of file diff --git a/bun.lockb b/bun.lockb index 25701689a124ea992c8a7fb360fad07fffe0cea7..c6fce5f402d7f435b4ead3a6c23c63d06e129574 100644 GIT binary patch delta 24 gcmX@NkNMm_<_)2 { if (!i.isModalSubmit()) return; - const role = getSetting(guild.id, "news.roleID"); + const role = getSetting(guild.id, "news", "role_id"); let roleToSend: Role | undefined; if (role) roleToSend = guild.roles.cache.get(role); const title = i.fields.getTextInputValue("title"); const body = i.fields.getTextInputValue("body"); - const newsEditable = getSetting(guild.id, "news.editOriginalMessage"); + const newsEditable = getSetting(guild.id, "news", "edit_original_message"); if (newsEditable === false) await sendChannelNews(guild, id, interaction, title, body); const embed = new EmbedBuilder() @@ -99,7 +99,7 @@ export default class Edit { ( guild.channels.cache.get( - getSetting(guild.id, "news.channelID")! ?? interaction.channel?.id + getSetting(guild.id, "news", "channel_id")! ?? interaction.channel?.id ) as TextChannel )?.messages.edit(news.messageID, { embeds: [embed], diff --git a/src/commands/news/remove.ts b/src/commands/news/remove.ts index dbf4f1f..cfb34be 100644 --- a/src/commands/news/remove.ts +++ b/src/commands/news/remove.ts @@ -41,7 +41,7 @@ export default class Remove { const messageID = news.messageID; const newsChannel = (await guild.channels - .fetch(getSetting(guild.id, "news.channelID")! ?? interaction.channel?.id) + .fetch(getSetting(guild.id, "news", "channel_id")! ?? interaction.channel?.id) .catch(() => null)) as TextChannel; if (newsChannel) await newsChannel.messages.delete(messageID); diff --git a/src/commands/settings.ts b/src/commands/settings.ts index 0bad8ac..206faab 100644 --- a/src/commands/settings.ts +++ b/src/commands/settings.ts @@ -4,7 +4,9 @@ import { EmbedBuilder, SlashCommandBuilder, PermissionsBitField, - type ChatInputCommandInteraction + type ChatInputCommandInteraction, + AutocompleteInteraction, + SlashCommandSubcommandBuilder } from "discord.js"; import { getSetting, @@ -16,24 +18,64 @@ import { genColor } from "../utils/colorGen"; import { errorEmbed } from "../utils/embeds/errorEmbed"; export default class Settings { - data: Omit; + data: SlashCommandBuilder;//Omit; constructor() { this.data = new SlashCommandBuilder() .setName("settings") - .setDescription("Configure Sokora to your liking.") - .addStringOption(string => - string - .setName("key") - .setDescription("The setting key to set") - .addChoices(...settingsKeys.map(key => ({ name: key, value: key }))) - .setRequired(true) - ) - .addStringOption(string => - string - .setName("value") - .setDescription("The value you want to set this option to, or blank for view") - .setAutocomplete(true) - ); + .setDescription("Configure Sokora to your liking."); + // .addStringOption(string => + // string + // .setName("key") + // .setDescription("The setting key to set") + // .addChoices(...settingsKeys.map(key => ({ name: key, value: key }))) + // .setRequired(true) + // ) + // .addStringOption(string => + // string + // .setName("value") + // .setDescription("The value you want to set this option to, or blank for view") + // .setAutocomplete(true) + // ); + + settingsKeys.forEach(key => { + const subcommand = new SlashCommandSubcommandBuilder() + .setName(key) + .setDescription("This command has no description."); + Object.keys(settingsDefinition[key]).forEach((sub) => { + switch (settingsDefinition[key][sub][0] as string) { + case "BOOL": + subcommand.addBooleanOption(option => + option + .setName(sub) + .setDescription(settingsDefinition[key][sub][1]) + .setRequired(false) + ); break; + case "INTEGER": + subcommand.addIntegerOption(option => + option + .setName(sub) + .setDescription(settingsDefinition[key][sub][1]) + .setRequired(false) + ); break; + case "USER": + subcommand.addUserOption(option => + option + .setName(sub) + .setDescription(settingsDefinition[key][sub][1]) + .setRequired(false) + ); break; + default: // Also includes "TEXT" + subcommand.addStringOption(option => + option + .setName(sub) + .setDescription(settingsDefinition[key][sub][1]) + .setRequired(false) + ); break; + }; + }); + //console.log(subcommand); + this.data.addSubcommand(subcommand); + }); } async run(interaction: ChatInputCommandInteraction) { @@ -48,27 +90,41 @@ export default class Settings { "You need the **Administrator** permission." ); - const key = interaction.options.get("key")!.value as keyof typeof settingsDefinition; - const value = interaction.options.get("value")?.value; - if (value == undefined) - return interaction.reply( - `\`${key}\` is currently \`${getSetting(interaction.guildId!, key)}\`` - ); + const key = interaction.options.getSubcommand() as keyof typeof settingsDefinition; + const values = interaction.options.data[0].options; + console.log(values); + if (values.length == 0) { + const embed = new EmbedBuilder(); + embed.setTitle(`Settings for ${key}`); + embed.setColor(genColor(100)); + Object.keys(settingsDefinition[key]).forEach((name) => { + embed.addFields({ + name: name, + value: getSetting(interaction.guildId!, key, name)?.toString() || "Not set" + }); + }); + return interaction.reply({ embeds: [embed] }); + } const embed = new EmbedBuilder() - .setTitle(`\`${key}\` has been set to \`${value}\`.`) + .setTitle(`Parameters changed`) .setColor(genColor(100)); - - setSetting(interaction.guildId!, key, value as keyof typeof settingsDefinition); + values.forEach((option) => { + setSetting(interaction.guildId!, key, option.name, option.value as string); + embed.addFields({ + name: option.name, + value: option.value?.toString() || "Not set" + }); + }); + interaction.reply({ embeds: [embed] }); } - autocompleteHandler(client: Client) { - client.on("interactionCreate", interaction => { + async autocomplete(interaction: AutocompleteInteraction) { if (interaction.type != InteractionType.ApplicationCommandAutocomplete) return; - if (interaction.options.getSubcommand() != this.data.name) return; + //if (interaction.options.getSubcommand() != this.data.name) return; switch ( - settingsDefinition[interaction.options.get("key")!.value as keyof typeof settingsDefinition][0] + Object.keys(settingsDefinition[interaction.options.getSubcommand()])[0] ) { case "BOOL": interaction.respond( @@ -76,9 +132,9 @@ export default class Settings { name: choice, value: choice })) - ); - break; - } - }); + ); break; + default: + interaction.respond([]); + }; } } diff --git a/src/commands/user.ts b/src/commands/user.ts index fb15175..45ee501 100644 --- a/src/commands/user.ts +++ b/src/commands/user.ts @@ -77,20 +77,6 @@ export default class User { .setThumbnail(target.displayAvatarURL()!) .setColor(genColor(200)); - imageColor(embed, undefined, target); - await interaction.reply({ embeds: [embed], components: [] }); - - if (!getSetting(`${guild.id}`, "levelling.enabled" || selectedUser.bot)) return; - const [guildLevel, guildExp] = getLevel(`${guild.id}`, `${target.id}`)!; - const [globalLevel, globalExp] = getLevel("0", `${target.id}`)!; - if (!guildLevel && !guildExp) setLevel(`${guild.id}`, `${target.id}`, 0, 0); - if (!globalLevel && !globalExp) setLevel("0", `${target.id}`, 0, 0); - - const nextLevelExp = Math.floor(100 * 1.15 * ((guildLevel ?? 0) + 1))?.toLocaleString("en-US"); - const globalNextLevelExp = Math.floor(100 * 1.15 * ((globalLevel ?? 0) + 1))?.toLocaleString( - "en-US" - ); - const row = new ActionRowBuilder().addComponents( new ButtonBuilder() .setCustomId("general") @@ -104,7 +90,20 @@ export default class User { .setStyle(ButtonStyle.Primary) ); - await interaction.editReply({ embeds: [embed], components: [row] }); + imageColor(embed, undefined, target); + await interaction.reply({ embeds: [embed], components: [row] }); + + if (!getSetting(`${guild.id}`, "levelling", "enabled") || selectedUser.bot) return; + const [guildLevel, guildExp] = getLevel(`${guild.id}`, `${target.id}`)!; + const [globalLevel, globalExp] = getLevel("0", `${target.id}`)!; + if (!guildLevel && !guildExp) setLevel(`${guild.id}`, `${target.id}`, 0, 0); + if (!globalLevel && !globalExp) setLevel("0", `${target.id}`, 0, 0); + + const nextLevelExp = Math.floor(100 * 1.15 * ((guildLevel ?? 0) + 1))?.toLocaleString("en-US"); + const globalNextLevelExp = Math.floor(100 * 1.15 * ((globalLevel ?? 0) + 1))?.toLocaleString( + "en-US" + ); + //await interaction.editReply({ embeds: [embed], components: [row] }); interaction.channel ?.createMessageComponentCollector({ filter: i => i.user.id === interaction.user.id, @@ -141,9 +140,9 @@ export default class User { imageColor(levelEmbed, undefined, target); switch (i.customId) { case "general": - await interaction.editReply({ embeds: [embed], components: [row] }); + await interaction.editReply({ embeds: [embed], components: [row] }); break; case "level": - await interaction.editReply({ embeds: [levelEmbed], components: [row] }); + await interaction.editReply({ embeds: [levelEmbed], components: [row] }); break; } i.update({}); diff --git a/src/events/error.ts b/src/events/error.ts index 9805583..631fbac 100644 --- a/src/events/error.ts +++ b/src/events/error.ts @@ -9,7 +9,7 @@ export default { } async run(error: DiscordErrorData) { - console.error(error.message); + console.error(error); } } }; diff --git a/src/events/guildMemberAdd.ts b/src/events/guildMemberAdd.ts index 65cccef..627ff9a 100644 --- a/src/events/guildMemberAdd.ts +++ b/src/events/guildMemberAdd.ts @@ -13,10 +13,10 @@ export default { async run(member: GuildMember) { const guildID = member.guild.id; - const id = getSetting(guildID, "welcome.channel"); + const id = getSetting(guildID, "welcome", "channel"); if (!id) return; - let text = getSetting(guildID, "welcome.text"); + let text = getSetting(guildID, "welcome", "text"); const user = member.user; const guild = member.guild; const channel = (await member.guild.channels.cache diff --git a/src/events/guildMemberRemove.ts b/src/events/guildMemberRemove.ts index fbf9f4f..805f429 100644 --- a/src/events/guildMemberRemove.ts +++ b/src/events/guildMemberRemove.ts @@ -13,10 +13,10 @@ export default { async run(member: GuildMember) { const guildID = member.guild.id; - const id = getSetting(guildID, "welcome.channel"); + const id = getSetting(guildID, "welcome", "channel"); if (!id) return; - let text = getSetting(guildID, "welcome.goodbyeText"); + let text = getSetting(guildID, "welcome", "goodbye_text"); const user = member.user; const guild = member.guild; const channel = (await member.guild.channels.cache diff --git a/src/events/interactionCreate.ts b/src/events/interactionCreate.ts index 26fbed7..c677c1e 100644 --- a/src/events/interactionCreate.ts +++ b/src/events/interactionCreate.ts @@ -1,6 +1,7 @@ import type { CommandInteraction, Client, AutocompleteInteraction } from "discord.js"; import { pathToFileURL } from "url"; import { join } from "path"; +import { file } from "bun"; async function getCommand( interaction: CommandInteraction | AutocompleteInteraction, @@ -9,7 +10,7 @@ async function getCommand( const commandName = interaction.commandName; const subcommandName = options.getSubcommand(false); const commandGroupName = options.getSubcommandGroup(false); - const commandImportPath = join( + var commandImportPath = join( join(process.cwd(), "src", "commands"), `${ subcommandName @@ -19,6 +20,7 @@ async function getCommand( : commandName }.ts` ); + if (!await file(commandImportPath).exists()) commandImportPath = join(join(process.cwd(), "src", "commands", `${commandName}.ts`)); return new (await import(pathToFileURL(commandImportPath).toString())).default(); } diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts index b1efae8..da83633 100644 --- a/src/events/messageCreate.ts +++ b/src/events/messageCreate.ts @@ -23,26 +23,26 @@ export default { for (const easterEggFile of readdirSync(eventsPath)) new ( await import(pathToFileURL(join(eventsPath, easterEggFile)).toString()) - ).default().run(message, ...message.content); + ).default().run(message); } // Levelling - if (!getSetting(guild.id, "levelling.enabled")) return; + if (!getSetting(guild.id, "levelling", "enabled")) return; - const level = getSetting(guild.id, "levelling.setLevel")!; - if (level != "") { + const level = getSetting(guild.id, "levelling", "set_level")!; + if (level != "" && level != null) { const newLevel = kominator(level); setLevel(guild.id, newLevel[0], +newLevel[1], 100 * +newLevel[1]); - setSetting(guild.id, "levelling.setLevel", ""); + setSetting(guild.id, "levelling", "set_level", ""); } - const blockedChannels = getSetting(guild.id, "levelling.blockChannels")!; + const blockedChannels = getSetting(guild.id, "levelling", "block_channels")!; if (blockedChannels != undefined) for (const channelID of blockedChannels.split(", ")) if (message.channelId === channelID) return; - let expGain = getSetting(guild.id, "levelling.setXPGain")! ?? 2; - const multiplier = getSetting(guild.id, "levelling.addMultiplier")!; + let expGain = getSetting(guild.id, "levelling", "set_xp_gain")! ?? 2; + const multiplier = getSetting(guild.id, "levelling", "add_multiplier")!; if (multiplier != null) { const expMultiplier = kominator(multiplier); @@ -54,13 +54,13 @@ export default { } // const cooldown = getSetting(guild.id, "levelling.setCooldown") ?? 4; - const levelChannelId = getSetting(guild.id, "levelling.channel"); + const levelChannelId = getSetting(guild.id, "levelling", "channel"); const [guildLevel, guildExp] = getLevel(guild.id, author.id); const [globalLevel, globalExp] = getLevel("0", author.id); const expUntilLevelup = 100 * 1.15 * (guildLevel + 1); const newLevelData = { level: guildLevel ?? 0, - exp: (guildExp ?? 0) + getSetting(guild.id, "levelling.setXPGain")! ?? 2 + exp: (guildExp ?? 0) + getSetting(guild.id, "levelling", "set_xp_gain")! ?? 2 }; const globalNewLevelData = { level: globalLevel ?? 0, exp: (globalExp ?? 0) + 2 }; diff --git a/src/events/messageDelete.ts b/src/events/messageDelete.ts index 7929e58..cbf9a42 100644 --- a/src/events/messageDelete.ts +++ b/src/events/messageDelete.ts @@ -11,8 +11,8 @@ export default { if (author.bot) return; const guild = message.guild!; - const logUpdate = getSetting(guild.id, "moderation.logMessages"); - const logChannel = getSetting(guild.id, "moderation.channel"); + const logUpdate = getSetting(guild.id, "moderation", "log_messages"); + const logChannel = getSetting(guild.id, "moderation", "channel"); if (!logUpdate) return; if (!logChannel) return; diff --git a/src/events/messageUpdate.ts b/src/events/messageUpdate.ts index 0430cbc..e9354e1 100644 --- a/src/events/messageUpdate.ts +++ b/src/events/messageUpdate.ts @@ -10,8 +10,8 @@ export default { if (author.bot) return; const guild = oldMessage.guild!; - const logUpdate = getSetting(guild.id, "moderation.logMessages"); - const logChannel = getSetting(guild.id, "moderation.channel"); + const logUpdate = getSetting(guild.id, "moderation", "log_messages"); + const logChannel = getSetting(guild.id, "moderation", "channel"); if (!logUpdate) return; if (!logChannel) return; diff --git a/src/utils/database/settings.ts b/src/utils/database/settings.ts index 147da2c..181d2f9 100644 --- a/src/utils/database/settings.ts +++ b/src/utils/database/settings.ts @@ -10,31 +10,43 @@ const tableDefinition = { } } satisfies TableDefinition; -export const settingsDefinition = { - "levelling.enabled": ["BOOL", "Enable/disable the levelling system."], - "levelling.channel": ["TEXT", "ID of the log channel for levelling-related stuff (i.e someone levelling up)."], - "levelling.blockChannels": ["TEXT", "ID(s) of the channels where messages aren't counted, comma separated."], - "levelling.setLevel": ["TEXT", "Set the level of a user."], - "levelling.addMultiplier": ["TEXT", "Add an XP multiplier to the levelling system. Syntax: multiplier, role/channel (choose), id."], - "levelling.setXPGain": ["INTEGER", "Set the amount of XP a user gains per message."], - "levelling.setCooldown": ["INTEGER", "Set the cooldown between messages that add XP."], - "moderation.channel": ["TEXT", "ID of the log channel for moderation-related stuff (i.e a message being edited)."], - "moderation.logMessages": ["BOOL", "Whether or not edited/deleted messages should be logged."], - "news.channelID": ["TEXT", "ID of the channel where news messages are sent."], - "news.roleID": ["TEXT", "ID of the roles that should be pinged when a news message is sent."], - "news.editOriginalMessage": ["BOOL", "Whether or not the original message should be edited when a news message is updated."], - "serverboard.inviteLink": ["TEXT", "The invite link which is shown on the serverboard."], - "serverboard.shown": ["BOOL", "Whether or not the server should be shown on the serverboard."], - "welcome.text": ["TEXT", "The welcome message that is sent when a user joins."], - "welcome.goodbyeText": ["TEXT", "The goodbye message that is sent when a user leaves."], - "welcome.channel": ["TEXT", "ID of the channel where welcome messages are sent."], -} satisfies Record; +// Ok so discord being a dick makes it so that you can't put capital letters for a command option name for some reason -_- + +export const settingsDefinition: Record> = { + "levelling": { + "enabled": ["BOOL", "Enable/disable the levelling system."], + "channel": ["TEXT", "ID of the log channel for levelling-related stuff (i.e someone levelling up)."], + "block_channels": ["TEXT", "ID(s) of the channels where messages aren't counted, comma separated."], + "set_level": ["TEXT", "Set the level of a user."], + "add_multiplier": ["TEXT", "Add an XP multiplier to the levelling system. Syntax: multiplier, role/channel (choose), id."], + "set_xp_gain": ["INTEGER", "Set the amount of XP a user gains per message."], + "set_cooldown": ["INTEGER", "Set the cooldown between messages that add XP."], + }, + "moderation": { + "channel": ["TEXT", "ID of the log channel for moderation-related stuff (i.e a message being edited)."], + "log_messages": ["BOOL", "Whether or not edited/deleted messages should be logged."], + }, + "news": { + "channel_id": ["TEXT", "ID of the channel where news messages are sent."], + "role_id": ["TEXT", "ID of the roles that should be pinged when a news message is sent."], + "edit_original_message": ["BOOL", "Whether or not the original message should be edited when a news message is updated."], + }, + "serverboard": { + "invite_link": ["TEXT", "The invite link which is shown on the serverboard."], + "shown": ["BOOL", "Whether or not the server should be shown on the serverboard."], + }, + "welcome": { + "text": ["TEXT", "The welcome message that is sent when a user joins."], + "goodbye_text": ["TEXT", "The goodbye message that is sent when a user leaves."], + "channel": ["TEXT", "ID of the channel where welcome messages are sent."], + }, +}; export const settingsKeys = Object.keys(settingsDefinition) as (keyof typeof settingsDefinition)[]; const database = getDatabase(tableDefinition); const getQuery = database.query("SELECT * FROM settings WHERE guildID = $1 AND key = $2;"); const listPublicQuery = database.query( - "SELECT * FROM settings WHERE key = 'serverboard.shown' AND value = 'TRUE';" + "SELECT * FROM settings WHERE key = 'serverboard.shown' AND value = '1';" ); const deleteQuery = database.query("DELETE FROM settings WHERE guildID = $1 AND key = $2;"); const insertQuery = database.query( @@ -43,17 +55,20 @@ const insertQuery = database.query( export function getSetting( guildID: string, - key: K + key: K, + setting: string, ): TypeOfKey | null { - let res = getQuery.all(JSON.stringify(guildID), key) as TypeOfDefinition< + let res = getQuery.all(JSON.stringify(guildID), key + "." + setting) as TypeOfDefinition< typeof tableDefinition >[]; if (res.length == 0) return null; - switch (settingsDefinition[key][0]) { - case "TEXT" || "INTEGER": + switch (settingsDefinition[key][setting][0]) { + case "TEXT": return res[0].value as TypeOfKey; case "BOOL": - return (res[0].value == "true") as TypeOfKey; + return (res[0].value != null && res[0].value != "" && res[0].value != "false" ? "true" : "false") as TypeOfKey; + case "INTEGER": + return parseInt(res[0].value) as TypeOfKey; default: return "WIP" as TypeOfKey; } @@ -62,13 +77,14 @@ export function getSetting( export function setSetting( guildID: string, key: K, - value: TypeOfKey + setting: string, + value: string//TypeOfKey ) { - const doInsert = getSetting(guildID, key) == null; + const doInsert = getSetting(guildID, key, setting) == null; if (!doInsert) { - deleteQuery.all(JSON.stringify(guildID), key); + deleteQuery.all(JSON.stringify(guildID), key + "." + setting); } - insertQuery.run(JSON.stringify(guildID), key, JSON.stringify(value)); + insertQuery.run(JSON.stringify(guildID), key + "." + setting, value); } export function listPublicServers() { @@ -78,4 +94,4 @@ export function listPublicServers() { } // Utility type -type TypeOfKey = SqlType<(typeof settingsDefinition)[T][0]>; +type TypeOfKey = SqlType<(typeof settingsDefinition)[T][any][0]>; diff --git a/src/utils/embeds/modEmbed.ts b/src/utils/embeds/modEmbed.ts index 33a1669..5b89058 100644 --- a/src/utils/embeds/modEmbed.ts +++ b/src/utils/embeds/modEmbed.ts @@ -86,7 +86,7 @@ export async function modEmbed(options: Options, reason?: string | null) { .setFooter({ text: `User ID: ${user.id}` }) .setColor(genColor(100)); - const logChannel = getSetting(guild.id, "moderation.channel"); + const logChannel = getSetting(guild.id, "moderation", "channel"); if (logChannel) { const channel = await guild.channels.cache .get(`${logChannel}`) diff --git a/src/utils/embeds/serverEmbed.ts b/src/utils/embeds/serverEmbed.ts index a64facd..c57dacb 100644 --- a/src/utils/embeds/serverEmbed.ts +++ b/src/utils/embeds/serverEmbed.ts @@ -19,7 +19,7 @@ type Options = { export async function serverEmbed(options: Options) { const { page, pages, guild } = options; const { premiumTier: boostTier, premiumSubscriptionCount: boostCount } = guild; - const invite = getSetting(guild.id, "serverboard.inviteLink"); + const invite = getSetting(guild.id, "serverboard", "invite_link"); const members = guild.members.cache; const boosters = members.filter(member => member.premiumSince); const onlineMembers = members.filter(member => diff --git a/src/utils/sendChannelNews.ts b/src/utils/sendChannelNews.ts index 41b7ad2..3124d18 100644 --- a/src/utils/sendChannelNews.ts +++ b/src/utils/sendChannelNews.ts @@ -17,7 +17,7 @@ export async function sendChannelNews( body?: string, ) { const news = get(id)!; - const role = getSetting(guild.id, "news.roleID"); + const role = getSetting(guild.id, "news", "role_id"); let roleToSend: Role | undefined; if (role) roleToSend = guild.roles.cache.get(role); @@ -31,7 +31,7 @@ export async function sendChannelNews( return ( guild.channels.cache.get( - getSetting(guild.id, "news.channelID")! ?? interaction.channel?.id + getSetting(guild.id, "news", "channel_id")! ?? interaction.channel?.id ) as TextChannel ) .send({ From 4a6a12e417ab0616bf8036a92e6a884c32c17a59 Mon Sep 17 00:00:00 2001 From: Golem64 <65229557+Golem642@users.noreply.github.com> Date: Thu, 4 Jul 2024 19:44:54 +0200 Subject: [PATCH 066/127] user profile and settings fixed, + (un)lock channels --- src/commands/moderation/lock.ts | 96 ++++++++++++++++++++++++++++++ src/commands/moderation/unlock.ts | 97 +++++++++++++++++++++++++++++++ src/commands/user.ts | 60 ++++++++++--------- src/utils/database/settings.ts | 2 +- 4 files changed, 227 insertions(+), 28 deletions(-) create mode 100644 src/commands/moderation/lock.ts create mode 100644 src/commands/moderation/unlock.ts diff --git a/src/commands/moderation/lock.ts b/src/commands/moderation/lock.ts new file mode 100644 index 0000000..b808aca --- /dev/null +++ b/src/commands/moderation/lock.ts @@ -0,0 +1,96 @@ +import { + SlashCommandSubcommandBuilder, + EmbedBuilder, + PermissionsBitField, + ChannelType, + TextChannel, + type Channel, + type ChatInputCommandInteraction +} from "discord.js"; +import { genColor } from "../../utils/colorGen"; +import { errorEmbed } from "../../utils/embeds/errorEmbed"; +import { getSetting } from "../../utils/database/settings"; + +export default class Lock { + data: SlashCommandSubcommandBuilder; + constructor() { + this.data = new SlashCommandSubcommandBuilder() + .setName("lock") + .setDescription("Locks a channel") + .addChannelOption(channel => + channel + .setName("channel") + .setDescription("The channel that you want to lock.") + .addChannelTypes( + ChannelType.GuildText, + ChannelType.PublicThread, + ChannelType.PrivateThread, + ChannelType.GuildVoice + ) + ); + } + + async run(interaction: ChatInputCommandInteraction) { + const guild = interaction.guild!; + const member = guild.members.cache.get(interaction.member?.user.id!)!; + + if (!member.permissions.has(PermissionsBitField.Flags.ManageChannels)) + return errorEmbed( + interaction, + "You can't execute this command", + "You need the **Manage Channels** permission." + ); + + const channelOption = interaction.options.getChannel("channel")!; + const channel = guild.channels.cache.get(interaction.channel?.id ?? channelOption.id)!; + + if (!channel.permissionsFor(guild.id)?.has("SendMessages")) + return errorEmbed( + interaction, + "You can't execute this command", + "The channel is already locked" + ); + + const embed = new EmbedBuilder() + .setTitle(`Locked a channel`) + .setDescription( + [ + `**Moderator**: ${interaction.user.username}`, + `**Channel**: ${channelOption ?? `<#${channel.id}>`}` + ].join("\n") + ) + .setColor(genColor(100)); + + if ( + channel.type === ChannelType.GuildText && + ChannelType.PublicThread && + ChannelType.PrivateThread && + ChannelType.GuildVoice + ) + + channel.permissionOverwrites.create(interaction.guild!.id, { + SendMessages: false, + SendMessagesInThreads: false, + CreatePublicThreads: false, + CreatePrivateThreads: false, + UseApplicationCommands: false, + UseEmbeddedActivities: false + }); + + const logChannel = getSetting(guild.id, "moderation", "channel"); + if (logChannel) { + const channel = await guild.channels.cache + .get(`${logChannel}`) + ?.fetch() + .then((channel: Channel) => { + if (channel.type != ChannelType.GuildText) return null; + return channel as TextChannel; + }) + .catch(() => null); + + if (channel) await channel.send({ embeds: [embed] }); + } + + await interaction.reply({ embeds: [embed] }); + } +} diff --git a/src/commands/moderation/unlock.ts b/src/commands/moderation/unlock.ts new file mode 100644 index 0000000..207ce7d --- /dev/null +++ b/src/commands/moderation/unlock.ts @@ -0,0 +1,97 @@ +import { + SlashCommandSubcommandBuilder, + EmbedBuilder, + PermissionsBitField, + ChannelType, + TextChannel, + type Channel, + type ChatInputCommandInteraction +} from "discord.js"; +import { genColor } from "../../utils/colorGen"; +import { errorEmbed } from "../../utils/embeds/errorEmbed"; +import { getSetting } from "../../utils/database/settings"; + +export default class Unlock { + data: SlashCommandSubcommandBuilder; + constructor() { + this.data = new SlashCommandSubcommandBuilder() + .setName("unlock") + .setDescription("Unlocks a channel") + .addChannelOption(channel => + channel + .setName("channel") + .setDescription("The channel that you want to unlock.") + .addChannelTypes( + ChannelType.GuildText, + ChannelType.PublicThread, + ChannelType.PrivateThread, + ChannelType.GuildVoice + ) + ); + } + + async run(interaction: ChatInputCommandInteraction) { + const guild = interaction.guild!; + const member = guild.members.cache.get(interaction.member?.user.id!)!; + + if (!member.permissions.has(PermissionsBitField.Flags.ManageChannels)) + return errorEmbed( + interaction, + "You can't execute this command", + "You need the **Manage Channels** permission." + ); + + const channelOption = interaction.options.getChannel("channel")!; + const channel = guild.channels.cache.get(interaction.channel?.id ?? channelOption.id)!; + + if (channel.permissionsFor(guild.id)?.has("SendMessages")) + return errorEmbed( + interaction, + "You can't execute this command", + "The channel is already unlocked" + ); + + + const embed = new EmbedBuilder() + .setTitle(`Unlocked a channel`) + .setDescription( + [ + `**Moderator**: ${interaction.user.username}`, + `**Channel**: ${channelOption ?? `<#${channel.id}>`}` + ].join("\n") + ) + .setColor(genColor(100)); + + if ( + channel.type === ChannelType.GuildText && + ChannelType.PublicThread && + ChannelType.PrivateThread && + ChannelType.GuildVoice + ) + + channel.permissionOverwrites.create(interaction.guild!.id, { + SendMessages: null, + SendMessagesInThreads: null, + CreatePublicThreads: null, + CreatePrivateThreads: null, + UseApplicationCommands: null, + UseEmbeddedActivities: null + }); + + const logChannel = getSetting(guild.id, "moderation", "channel"); + if (logChannel) { + const channel = await guild.channels.cache + .get(`${logChannel}`) + ?.fetch() + .then((channel: Channel) => { + if (channel.type != ChannelType.GuildText) return null; + return channel as TextChannel; + }) + .catch(() => null); + + if (channel) await channel.send({ embeds: [embed] }); + } + + await interaction.reply({ embeds: [embed] }); + } +} diff --git a/src/commands/user.ts b/src/commands/user.ts index 45ee501..6a6db19 100644 --- a/src/commands/user.ts +++ b/src/commands/user.ts @@ -76,36 +76,35 @@ export default class User { .setFooter({ text: `User ID: ${target.id}` }) .setThumbnail(target.displayAvatarURL()!) .setColor(genColor(200)); + + const components = []; - const row = new ActionRowBuilder().addComponents( - new ButtonBuilder() - .setCustomId("general") - .setLabel("• General") - .setEmoji("📃") - .setStyle(ButtonStyle.Primary), - new ButtonBuilder() - .setCustomId("level") - .setLabel("• Level") - .setEmoji("⚡") - .setStyle(ButtonStyle.Primary) - ); - - imageColor(embed, undefined, target); - await interaction.reply({ embeds: [embed], components: [row] }); + if (getSetting(`${guild.id}`, "levelling", "enabled") && !selectedUser.bot) { + const row = new ActionRowBuilder(); + row.addComponents( + new ButtonBuilder() + .setCustomId("general") + .setLabel("• General") + .setEmoji("📃") + .setStyle(ButtonStyle.Primary), + new ButtonBuilder() + .setCustomId("level") + .setLabel("• Level") + .setEmoji("⚡") + .setStyle(ButtonStyle.Primary) + ); - if (!getSetting(`${guild.id}`, "levelling", "enabled") || selectedUser.bot) return; - const [guildLevel, guildExp] = getLevel(`${guild.id}`, `${target.id}`)!; - const [globalLevel, globalExp] = getLevel("0", `${target.id}`)!; - if (!guildLevel && !guildExp) setLevel(`${guild.id}`, `${target.id}`, 0, 0); - if (!globalLevel && !globalExp) setLevel("0", `${target.id}`, 0, 0); + const [guildLevel, guildExp] = getLevel(`${guild.id}`, `${target.id}`)!; + const [globalLevel, globalExp] = getLevel("0", `${target.id}`)!; + if (!guildLevel && !guildExp) setLevel(`${guild.id}`, `${target.id}`, 0, 0); + if (!globalLevel && !globalExp) setLevel("0", `${target.id}`, 0, 0); - const nextLevelExp = Math.floor(100 * 1.15 * ((guildLevel ?? 0) + 1))?.toLocaleString("en-US"); - const globalNextLevelExp = Math.floor(100 * 1.15 * ((globalLevel ?? 0) + 1))?.toLocaleString( - "en-US" - ); - //await interaction.editReply({ embeds: [embed], components: [row] }); - interaction.channel - ?.createMessageComponentCollector({ + const nextLevelExp = Math.floor(100 * 1.15 * ((guildLevel ?? 0) + 1))?.toLocaleString("en-US"); + const globalNextLevelExp = Math.floor(100 * 1.15 * ((globalLevel ?? 0) + 1))?.toLocaleString( + "en-US" + ); + + interaction.channel?.createMessageComponentCollector({ filter: i => i.user.id === interaction.user.id, time: 60000 }) @@ -147,5 +146,12 @@ export default class User { i.update({}); }); + + components.push(row); + } + + imageColor(embed, undefined, target); + await interaction.reply({ embeds: [embed], components: components }); + } } diff --git a/src/utils/database/settings.ts b/src/utils/database/settings.ts index 181d2f9..d3682d1 100644 --- a/src/utils/database/settings.ts +++ b/src/utils/database/settings.ts @@ -66,7 +66,7 @@ export function getSetting( case "TEXT": return res[0].value as TypeOfKey; case "BOOL": - return (res[0].value != null && res[0].value != "" && res[0].value != "false" ? "true" : "false") as TypeOfKey; + return (res[0].value != null && res[0].value != "" && res[0].value != "false" && res[0].value != "0") as TypeOfKey; case "INTEGER": return parseInt(res[0].value) as TypeOfKey; default: From 59b6d3b0e797536a2bef3a99660ceb86e269fcde Mon Sep 17 00:00:00 2001 From: MrSerge01 <86667481+MrSerge01@users.noreply.github.com> Date: Thu, 4 Jul 2024 22:53:40 +0500 Subject: [PATCH 067/127] small stuff --- bun.lockb | Bin 57166 -> 57167 bytes package.json | 8 +-- src/commands/about.ts | 2 +- src/commands/moderation/delwarn.ts | 4 +- src/events/messageCreate.ts | 15 +++--- src/index.ts | 9 ---- src/utils/database/settings.ts | 76 ++++++++++++++++++----------- src/utils/types.ts | 7 --- 8 files changed, 60 insertions(+), 61 deletions(-) delete mode 100644 src/index.ts delete mode 100644 src/utils/types.ts diff --git a/bun.lockb b/bun.lockb index c6fce5f402d7f435b4ead3a6c23c63d06e129574..0d5bd5c3531c9afb13baaf4851f64f8cbd812351 100644 GIT binary patch delta 2733 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 sp?x?8Bk { - shard.on("error", err => console.error(err)); - console.log(`Launched shard ${shard.id}`); -}); - -manager.spawn(); diff --git a/src/utils/database/settings.ts b/src/utils/database/settings.ts index d3682d1..86935e5 100644 --- a/src/utils/database/settings.ts +++ b/src/utils/database/settings.ts @@ -10,36 +10,49 @@ const tableDefinition = { } } satisfies TableDefinition; -// Ok so discord being a dick makes it so that you can't put capital letters for a command option name for some reason -_- - export const settingsDefinition: Record> = { - "levelling": { - "enabled": ["BOOL", "Enable/disable the levelling system."], - "channel": ["TEXT", "ID of the log channel for levelling-related stuff (i.e someone levelling up)."], - "block_channels": ["TEXT", "ID(s) of the channels where messages aren't counted, comma separated."], - "set_level": ["TEXT", "Set the level of a user."], - "add_multiplier": ["TEXT", "Add an XP multiplier to the levelling system. Syntax: multiplier, role/channel (choose), id."], - "set_xp_gain": ["INTEGER", "Set the amount of XP a user gains per message."], - "set_cooldown": ["INTEGER", "Set the cooldown between messages that add XP."], - }, - "moderation": { - "channel": ["TEXT", "ID of the log channel for moderation-related stuff (i.e a message being edited)."], - "log_messages": ["BOOL", "Whether or not edited/deleted messages should be logged."], + levelling: { + enabled: ["BOOL", "Enable/disable the levelling system."], + channel: [ + "TEXT", + "ID of the log channel for levelling-related stuff (i.e someone levelling up)." + ], + block_channels: [ + "TEXT", + "ID(s) of the channels where messages aren't counted, comma separated." + ], + set_level: ["TEXT", "Set the level of a user."], + add_multiplier: [ + "TEXT", + "Add an XP multiplier to the levelling system. Syntax: multiplier, role/channel (choose), id." + ], + set_xp_gain: ["INTEGER", "Set the amount of XP a user gains per message."], + set_cooldown: ["INTEGER", "Set the cooldown between messages that add XP."] }, - "news": { - "channel_id": ["TEXT", "ID of the channel where news messages are sent."], - "role_id": ["TEXT", "ID of the roles that should be pinged when a news message is sent."], - "edit_original_message": ["BOOL", "Whether or not the original message should be edited when a news message is updated."], + moderation: { + channel: [ + "TEXT", + "ID of the log channel for moderation-related stuff (i.e a message being edited)." + ], + log_messages: ["BOOL", "Whether or not edited/deleted messages should be logged."] }, - "serverboard": { - "invite_link": ["TEXT", "The invite link which is shown on the serverboard."], - "shown": ["BOOL", "Whether or not the server should be shown on the serverboard."], + news: { + channel_id: ["TEXT", "ID of the channel where news messages are sent."], + role_id: ["TEXT", "ID of the roles that should be pinged when a news message is sent."], + edit_original_message: [ + "BOOL", + "Whether or not the original message should be edited when a news message is updated." + ] }, - "welcome": { - "text": ["TEXT", "The welcome message that is sent when a user joins."], - "goodbye_text": ["TEXT", "The goodbye message that is sent when a user leaves."], - "channel": ["TEXT", "ID of the channel where welcome messages are sent."], + serverboard: { + invite_link: ["TEXT", "The invite link which is shown on the serverboard."], + shown: ["BOOL", "Whether or not the server should be shown on the serverboard."] }, + welcome: { + text: ["TEXT", "The welcome message that is sent when a user joins."], + goodbye_text: ["TEXT", "The goodbye message that is sent when a user leaves."], + channel: ["TEXT", "ID of the channel where welcome messages are sent."] + } }; export const settingsKeys = Object.keys(settingsDefinition) as (keyof typeof settingsDefinition)[]; @@ -56,17 +69,20 @@ const insertQuery = database.query( export function getSetting( guildID: string, key: K, - setting: string, + setting: string ): TypeOfKey | null { let res = getQuery.all(JSON.stringify(guildID), key + "." + setting) as TypeOfDefinition< typeof tableDefinition >[]; + console.log(res); if (res.length == 0) return null; switch (settingsDefinition[key][setting][0]) { case "TEXT": return res[0].value as TypeOfKey; case "BOOL": - return (res[0].value != null && res[0].value != "" && res[0].value != "false" && res[0].value != "0") as TypeOfKey; + return ( + res[0].value != null && res[0].value != "" && res[0].value != "false" ? "true" : "false" + ) as TypeOfKey; case "INTEGER": return parseInt(res[0].value) as TypeOfKey; default: @@ -78,7 +94,7 @@ export function setSetting( guildID: string, key: K, setting: string, - value: string//TypeOfKey + value: string //TypeOfKey ) { const doInsert = getSetting(guildID, key, setting) == null; if (!doInsert) { @@ -94,4 +110,6 @@ export function listPublicServers() { } // Utility type -type TypeOfKey = SqlType<(typeof settingsDefinition)[T][any][0]>; +type TypeOfKey = SqlType< + (typeof settingsDefinition)[T][any][0] +>; diff --git a/src/utils/types.ts b/src/utils/types.ts deleted file mode 100644 index f300e26..0000000 --- a/src/utils/types.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { ChannelType } from "discord.js"; - -export type TextChannels = - | ChannelType.GuildText - | ChannelType.GuildPublicThread - | ChannelType.GuildPrivateThread - | ChannelType.GuildVoice; From c3aa05f331816302d177c77fe785ae384ef49563 Mon Sep 17 00:00:00 2001 From: MrSerge01 <86667481+MrSerge01@users.noreply.github.com> Date: Mon, 8 Jul 2024 21:58:49 +0500 Subject: [PATCH 068/127] index readded + some more stuf --- package.json | 7 +- src/bot.ts | 4 +- src/commands/moderation/ban.ts | 3 - src/commands/moderation/delwarn.ts | 7 +- src/commands/moderation/lock.ts | 21 +++--- src/commands/moderation/mute.ts | 14 ++-- src/commands/moderation/unlock.ts | 36 +++++----- src/commands/moderation/unmute.ts | 18 +---- src/commands/settings.ts | 111 +++++++++++++---------------- src/events/error.ts | 15 ---- src/index.ts | 9 +++ src/utils/database/settings.ts | 1 - src/utils/embeds/modEmbed.ts | 36 ++-------- src/utils/logChannel.ts | 24 +++++++ src/utils/sendChannelNews.ts | 8 +-- 15 files changed, 144 insertions(+), 170 deletions(-) delete mode 100644 src/events/error.ts create mode 100644 src/index.ts create mode 100644 src/utils/logChannel.ts diff --git a/package.json b/package.json index 1b30611..a798301 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,15 @@ { "name": "sokora", "description": "Welcome to Sokora, the multipurpose, multiplatform bot.", - "contributors": ["The Sokora team", "The GitHub contributors"], + "contributors": [ + "The Sokora team", + "The GitHub contributors" + ], "version": "0.1.0", "main": "./src/index.ts", "type": "module", "scripts": { - "start": "bun ./src/bot.ts" + "start": "bun ./src/index.ts" }, "dependencies": { "discord.js": "^14.15.3", diff --git a/src/bot.ts b/src/bot.ts index b0850bb..a79ac57 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -1,10 +1,10 @@ -import { Client, ActivityType } from "discord.js"; +import { ActivityType, Client } from "discord.js"; import Commands from "./handlers/commands"; import Events from "./handlers/events"; const client = new Client({ presence: { - activities: [{ name: "with /settings!", type: ActivityType.Playing }] + activities: [{ name: "your feedback!", type: ActivityType.Listening }] }, intents: [ "Guilds", diff --git a/src/commands/moderation/ban.ts b/src/commands/moderation/ban.ts index 1920103..502ad2a 100644 --- a/src/commands/moderation/ban.ts +++ b/src/commands/moderation/ban.ts @@ -14,9 +14,6 @@ export default class Ban { .addUserOption(user => user.setName("user").setDescription("The user that you want to ban.").setRequired(true) ) - .addStringOption(string => - string.setName("duration").setDescription("The duration of the ban (e.g 30m, 1d, 2h).") - ) .addStringOption(string => string.setName("reason").setDescription("The reason for the ban.") ); diff --git a/src/commands/moderation/delwarn.ts b/src/commands/moderation/delwarn.ts index 6754628..b30746c 100644 --- a/src/commands/moderation/delwarn.ts +++ b/src/commands/moderation/delwarn.ts @@ -88,7 +88,12 @@ export default class Delwarn { if (channel) await channel.send({ embeds: [embed] }); } - removeModeration(guild.id, `${id}`); + try { + removeModeration(guild.id, `${id}`); + } catch (error) { + console.error(error); + } + await interaction.reply({ embeds: [embed] }); const dmChannel = (await user.createDM().catch(() => null)) as DMChannel | null; diff --git a/src/commands/moderation/lock.ts b/src/commands/moderation/lock.ts index b808aca..4fd3526 100644 --- a/src/commands/moderation/lock.ts +++ b/src/commands/moderation/lock.ts @@ -48,7 +48,7 @@ export default class Lock { return errorEmbed( interaction, "You can't execute this command", - "The channel is already locked" + "The channel is already locked." ); const embed = new EmbedBuilder() @@ -67,15 +67,16 @@ export default class Lock { ChannelType.PrivateThread && ChannelType.GuildVoice ) - - channel.permissionOverwrites.create(interaction.guild!.id, { - SendMessages: false, - SendMessagesInThreads: false, - CreatePublicThreads: false, - CreatePrivateThreads: false, - UseApplicationCommands: false, - UseEmbeddedActivities: false - }); + channel.permissionOverwrites + .create(interaction.guild!.id, { + SendMessages: false, + SendMessagesInThreads: false, + CreatePublicThreads: false, + CreatePrivateThreads: false, + UseApplicationCommands: false, + UseEmbeddedActivities: false + }) + .catch(error => console.error(error)); const logChannel = getSetting(guild.id, "moderation", "channel"); if (logChannel) { diff --git a/src/commands/moderation/mute.ts b/src/commands/moderation/mute.ts index 00f579e..749fe10 100644 --- a/src/commands/moderation/mute.ts +++ b/src/commands/moderation/mute.ts @@ -5,6 +5,7 @@ import { } from "discord.js"; import ms from "ms"; import { errorCheck, modEmbed } from "../../utils/embeds/modEmbed"; +import { errorEmbed } from "../../utils/embeds/errorEmbed"; export default class Mute { data: SlashCommandSubcommandBuilder; @@ -31,12 +32,13 @@ export default class Mute { const duration = interaction.options.getString("duration")!; const reason = interaction.options.getString("reason"); - errorCheck(PermissionsBitField.Flags.MuteMembers, { - interaction, - user, - action: "Mute", - duration - }); + errorCheck(PermissionsBitField.Flags.ModerateMembers, { interaction, user, action: "Mute" }); + if (!ms(duration) || ms(duration) > ms("28d")) + return errorEmbed( + interaction, + `You can't mute ${user.displayName}.`, + "The duration is invalid or is above the 28 day limit." + ); const time = new Date( Date.parse(new Date().toISOString()) + Date.parse(new Date(ms(duration)).toISOString()) diff --git a/src/commands/moderation/unlock.ts b/src/commands/moderation/unlock.ts index 207ce7d..5ef3535 100644 --- a/src/commands/moderation/unlock.ts +++ b/src/commands/moderation/unlock.ts @@ -1,15 +1,15 @@ import { - SlashCommandSubcommandBuilder, + ChannelType, EmbedBuilder, PermissionsBitField, - ChannelType, + SlashCommandSubcommandBuilder, TextChannel, type Channel, type ChatInputCommandInteraction } from "discord.js"; import { genColor } from "../../utils/colorGen"; -import { errorEmbed } from "../../utils/embeds/errorEmbed"; import { getSetting } from "../../utils/database/settings"; +import { errorEmbed } from "../../utils/embeds/errorEmbed"; export default class Unlock { data: SlashCommandSubcommandBuilder; @@ -45,13 +45,12 @@ export default class Unlock { const channel = guild.channels.cache.get(interaction.channel?.id ?? channelOption.id)!; if (channel.permissionsFor(guild.id)?.has("SendMessages")) - return errorEmbed( - interaction, - "You can't execute this command", - "The channel is already unlocked" - ); + return errorEmbed( + interaction, + "You can't execute this command.", + "The channel is not locked." + ); - const embed = new EmbedBuilder() .setTitle(`Unlocked a channel`) .setDescription( @@ -68,15 +67,16 @@ export default class Unlock { ChannelType.PrivateThread && ChannelType.GuildVoice ) - - channel.permissionOverwrites.create(interaction.guild!.id, { - SendMessages: null, - SendMessagesInThreads: null, - CreatePublicThreads: null, - CreatePrivateThreads: null, - UseApplicationCommands: null, - UseEmbeddedActivities: null - }); + channel.permissionOverwrites + .create(interaction.guild!.id, { + SendMessages: null, + SendMessagesInThreads: null, + CreatePublicThreads: null, + CreatePrivateThreads: null, + UseApplicationCommands: null, + UseEmbeddedActivities: null + }) + .catch(error => console.error(error)); const logChannel = getSetting(guild.id, "moderation", "channel"); if (logChannel) { diff --git a/src/commands/moderation/unmute.ts b/src/commands/moderation/unmute.ts index e83e3ab..4148b9d 100644 --- a/src/commands/moderation/unmute.ts +++ b/src/commands/moderation/unmute.ts @@ -11,6 +11,7 @@ import { import { genColor } from "../../utils/colorGen"; import { errorEmbed } from "../../utils/embeds/errorEmbed"; import { getSetting } from "../../utils/database/settings"; +import { logChannel } from "../../utils/logChannel"; export default class Unmute { data: SlashCommandSubcommandBuilder; @@ -29,7 +30,7 @@ export default class Unmute { if ( !members .get(interaction.member!.user.id)! - .permissions.has(PermissionsBitField.Flags.MuteMembers) + .permissions.has(PermissionsBitField.Flags.ModerateMembers) ) return errorEmbed( interaction, @@ -54,20 +55,7 @@ export default class Unmute { .setFooter({ text: `User ID: ${user.id}` }) .setColor(genColor(100)); - const logChannel = getSetting(guild.id, "moderation", "channel"); - if (logChannel) { - const channel = await guild.channels.cache - .get(`${logChannel}`) - ?.fetch() - .then((channel: Channel) => { - if (channel.type != ChannelType.GuildText) return null; - return channel as TextChannel; - }) - .catch(() => null); - - if (channel) await channel.send({ embeds: [embed] }); - } - + await logChannel(guild, embed); await members.get(user.id)!.edit({ communicationDisabledUntil: null }); await interaction.reply({ embeds: [embed] }); diff --git a/src/commands/settings.ts b/src/commands/settings.ts index 206faab..d4e8d23 100644 --- a/src/commands/settings.ts +++ b/src/commands/settings.ts @@ -1,5 +1,4 @@ import { - Client, InteractionType, EmbedBuilder, SlashCommandBuilder, @@ -18,60 +17,53 @@ import { genColor } from "../utils/colorGen"; import { errorEmbed } from "../utils/embeds/errorEmbed"; export default class Settings { - data: SlashCommandBuilder;//Omit; + data: SlashCommandBuilder; //Omit; constructor() { this.data = new SlashCommandBuilder() .setName("settings") - .setDescription("Configure Sokora to your liking."); - // .addStringOption(string => - // string - // .setName("key") - // .setDescription("The setting key to set") - // .addChoices(...settingsKeys.map(key => ({ name: key, value: key }))) - // .setRequired(true) - // ) - // .addStringOption(string => - // string - // .setName("value") - // .setDescription("The value you want to set this option to, or blank for view") - // .setAutocomplete(true) - // ); - + .setDescription("Configure Sokora to your liking.") + .setDefaultMemberPermissions(PermissionsBitField.Flags.Administrator); + // .addStringOption(string => + // string + // .setName("key") + // .setDescription("The setting key to set") + // .addChoices(...settingsKeys.map(key => ({ name: key, value: key }))) + // .setRequired(true) + // ) + // .addStringOption(string => + // string + // .setName("value") + // .setDescription("The value you want to set this option to, or blank for view") + // .setAutocomplete(true) + // ); + settingsKeys.forEach(key => { const subcommand = new SlashCommandSubcommandBuilder() .setName(key) .setDescription("This command has no description."); - Object.keys(settingsDefinition[key]).forEach((sub) => { + Object.keys(settingsDefinition[key]).forEach(sub => { switch (settingsDefinition[key][sub][0] as string) { case "BOOL": subcommand.addBooleanOption(option => - option - .setName(sub) - .setDescription(settingsDefinition[key][sub][1]) - .setRequired(false) - ); break; + option.setName(sub).setDescription(settingsDefinition[key][sub][1]).setRequired(false) + ); + break; case "INTEGER": subcommand.addIntegerOption(option => - option - .setName(sub) - .setDescription(settingsDefinition[key][sub][1]) - .setRequired(false) - ); break; + option.setName(sub).setDescription(settingsDefinition[key][sub][1]).setRequired(false) + ); + break; case "USER": subcommand.addUserOption(option => - option - .setName(sub) - .setDescription(settingsDefinition[key][sub][1]) - .setRequired(false) - ); break; + option.setName(sub).setDescription(settingsDefinition[key][sub][1]).setRequired(false) + ); + break; default: // Also includes "TEXT" subcommand.addStringOption(option => - option - .setName(sub) - .setDescription(settingsDefinition[key][sub][1]) - .setRequired(false) - ); break; - }; + option.setName(sub).setDescription(settingsDefinition[key][sub][1]).setRequired(false) + ); + break; + } }); //console.log(subcommand); this.data.addSubcommand(subcommand); @@ -91,13 +83,13 @@ export default class Settings { ); const key = interaction.options.getSubcommand() as keyof typeof settingsDefinition; - const values = interaction.options.data[0].options; + const values = interaction.options.data[0].options!; console.log(values); if (values.length == 0) { const embed = new EmbedBuilder(); embed.setTitle(`Settings for ${key}`); embed.setColor(genColor(100)); - Object.keys(settingsDefinition[key]).forEach((name) => { + Object.keys(settingsDefinition[key]).forEach(name => { embed.addFields({ name: name, value: getSetting(interaction.guildId!, key, name)?.toString() || "Not set" @@ -106,35 +98,32 @@ export default class Settings { return interaction.reply({ embeds: [embed] }); } - const embed = new EmbedBuilder() - .setTitle(`Parameters changed`) - .setColor(genColor(100)); - values.forEach((option) => { + const embed = new EmbedBuilder().setTitle(`Parameters changed`).setColor(genColor(100)); + values.forEach(option => { setSetting(interaction.guildId!, key, option.name, option.value as string); embed.addFields({ name: option.name, value: option.value?.toString() || "Not set" }); }); - + interaction.reply({ embeds: [embed] }); } async autocomplete(interaction: AutocompleteInteraction) { - if (interaction.type != InteractionType.ApplicationCommandAutocomplete) return; - //if (interaction.options.getSubcommand() != this.data.name) return; - switch ( - Object.keys(settingsDefinition[interaction.options.getSubcommand()])[0] - ) { - case "BOOL": - interaction.respond( - ["true", "false"].map(choice => ({ - name: choice, - value: choice - })) - ); break; - default: - interaction.respond([]); - }; + if (interaction.type != InteractionType.ApplicationCommandAutocomplete) return; + //if (interaction.options.getSubcommand() != this.data.name) return; + switch (Object.keys(settingsDefinition[interaction.options.getSubcommand()])[0]) { + case "BOOL": + interaction.respond( + ["true", "false"].map(choice => ({ + name: choice, + value: choice + })) + ); + break; + default: + interaction.respond([]); + } } } diff --git a/src/events/error.ts b/src/events/error.ts deleted file mode 100644 index 631fbac..0000000 --- a/src/events/error.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { DiscordErrorData, type Client } from "discord.js"; - -export default { - name: "error", - event: class Error { - client: Client; - constructor(client: Client) { - this.client = client; - } - - async run(error: DiscordErrorData) { - console.error(error); - } - } -}; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..e4be99a --- /dev/null +++ b/src/index.ts @@ -0,0 +1,9 @@ +import { ShardingManager } from "discord.js"; + +const manager = new ShardingManager("./src/bot.ts", { token: process.env.TOKEN }); +manager.on("shardCreate", shard => { + shard.on("error", err => console.error(err)); + console.log(`Launched shard ${shard.id}`); +}); + +manager.spawn(); diff --git a/src/utils/database/settings.ts b/src/utils/database/settings.ts index 86935e5..2653e13 100644 --- a/src/utils/database/settings.ts +++ b/src/utils/database/settings.ts @@ -74,7 +74,6 @@ export function getSetting( let res = getQuery.all(JSON.stringify(guildID), key + "." + setting) as TypeOfDefinition< typeof tableDefinition >[]; - console.log(res); if (res.length == 0) return null; switch (settingsDefinition[key][setting][0]) { case "TEXT": diff --git a/src/utils/embeds/modEmbed.ts b/src/utils/embeds/modEmbed.ts index 5b89058..22b9241 100644 --- a/src/utils/embeds/modEmbed.ts +++ b/src/utils/embeds/modEmbed.ts @@ -1,14 +1,7 @@ -import { - ChannelType, - EmbedBuilder, - TextChannel, - type Channel, - type ChatInputCommandInteraction, - type User -} from "discord.js"; +import { EmbedBuilder, type ChatInputCommandInteraction, type User } from "discord.js"; import ms from "ms"; import { genColor } from "../colorGen"; -import { getSetting } from "../database/settings"; +import { logChannel } from "../logChannel"; import { errorEmbed } from "./errorEmbed"; type Options = { @@ -19,7 +12,7 @@ type Options = { }; export async function errorCheck(permission: bigint, options: Options, permissionAction?: string) { - const { interaction, user, action, duration } = options; + const { interaction, user, action } = options; const guild = interaction.guild!; const members = guild.members.cache!; const member = members.get(interaction.member?.user.id!)!; @@ -53,14 +46,6 @@ export async function errorCheck(permission: bigint, options: Options, permissio "The member has a higher role position than you." ); - if (duration) - if (!ms(duration) || ms(duration) > ms("28d")) - return errorEmbed( - interaction, - `You can't ${action.toLowerCase()} ${name}`, - "The duration is invalid or is above the 28 day limit." - ); - if (member.id === guild.ownerId) return errorEmbed( interaction, @@ -86,20 +71,7 @@ export async function modEmbed(options: Options, reason?: string | null) { .setFooter({ text: `User ID: ${user.id}` }) .setColor(genColor(100)); - const logChannel = getSetting(guild.id, "moderation", "channel"); - if (logChannel) { - const channel = await guild.channels.cache - .get(`${logChannel}`) - ?.fetch() - .then((channel: Channel) => { - if (channel.type != ChannelType.GuildText) return null; - return channel as TextChannel; - }) - .catch(() => null); - - if (channel) await channel.send({ embeds: [embed] }); - } - + await logChannel(guild, embed); await interaction.reply({ embeds: [embed] }); const dmChannel = await user.createDM().catch(() => null); diff --git a/src/utils/logChannel.ts b/src/utils/logChannel.ts new file mode 100644 index 0000000..ad2f8fa --- /dev/null +++ b/src/utils/logChannel.ts @@ -0,0 +1,24 @@ +import { + ChannelType, + type Channel, + type EmbedBuilder, + type Guild, + type TextChannel +} from "discord.js"; +import { getSetting } from "./database/settings"; + +export async function logChannel(guild: Guild, embed: EmbedBuilder) { + const logChannel = getSetting(guild.id, "moderation", "channel"); + if (logChannel) { + const channel = await guild.channels.cache + .get(`${logChannel}`) + ?.fetch() + .then((channel: Channel) => { + if (channel.type != ChannelType.GuildText) return null; + return channel as TextChannel; + }) + .catch(() => null); + + if (channel) await channel.send({ embeds: [embed] }); + } +} diff --git a/src/utils/sendChannelNews.ts b/src/utils/sendChannelNews.ts index 3124d18..005ee94 100644 --- a/src/utils/sendChannelNews.ts +++ b/src/utils/sendChannelNews.ts @@ -1,9 +1,9 @@ import { EmbedBuilder, - type Role, - type TextChannel, + type ChatInputCommandInteraction, type Guild, - type ChatInputCommandInteraction + type Role, + type TextChannel } from "discord.js"; import { genColor } from "./colorGen"; import { get, updateNews } from "./database/news"; @@ -14,7 +14,7 @@ export async function sendChannelNews( id: string, interaction: ChatInputCommandInteraction, title?: string, - body?: string, + body?: string ) { const news = get(id)!; const role = getSetting(guild.id, "news", "role_id"); From e5f08eb2f6f47349a73387f55fea5d39d6165874 Mon Sep 17 00:00:00 2001 From: MrSerge01 <86667481+MrSerge01@users.noreply.github.com> Date: Mon, 8 Jul 2024 23:59:50 +0500 Subject: [PATCH 069/127] one more thing --- bun.lockb | Bin 57167 -> 57167 bytes src/commands/moderation/ban.ts | 12 +++++++++-- src/commands/moderation/kick.ts | 10 ++++++++- src/commands/moderation/lock.ts | 4 ++-- src/commands/moderation/mute.ts | 11 ++++++++-- src/commands/moderation/unban.ts | 33 ++++++++---------------------- src/commands/moderation/unlock.ts | 23 ++++----------------- src/commands/moderation/unmute.ts | 4 ---- src/commands/moderation/warn.ts | 17 ++++++--------- src/utils/embeds/modEmbed.ts | 20 ++++++++++++++++-- 10 files changed, 67 insertions(+), 67 deletions(-) diff --git a/bun.lockb b/bun.lockb index 0d5bd5c3531c9afb13baaf4851f64f8cbd812351..d9bdc1066b0590e381466960c796ecfc936d48b1 100644 GIT binary patch delta 41 xcmX@VkNNyQ<_#LNq%0UX85kJ27#JGnt2XFpx27g>Fvgkcnd%vDcACZO3IGUL3!nf1 delta 41 xcmX@VkNNyQ<_#LNq|6yO85kJ27#JGnt2XFpx27g>Fvgkcnd%vBcACZO3IGT*3!VS~ diff --git a/src/commands/moderation/ban.ts b/src/commands/moderation/ban.ts index 502ad2a..45e8f5f 100644 --- a/src/commands/moderation/ban.ts +++ b/src/commands/moderation/ban.ts @@ -3,7 +3,7 @@ import { SlashCommandSubcommandBuilder, type ChatInputCommandInteraction } from "discord.js"; -import { modEmbed, errorCheck } from "../../utils/embeds/modEmbed"; +import { errorCheck, modEmbed } from "../../utils/embeds/modEmbed"; export default class Ban { data: SlashCommandSubcommandBuilder; @@ -21,7 +21,15 @@ export default class Ban { async run(interaction: ChatInputCommandInteraction) { const user = interaction.options.getUser("user")!; - errorCheck(PermissionsBitField.Flags.BanMembers, { interaction, user, action: "Ban" }); + + await errorCheck( + PermissionsBitField.Flags.BanMembers, + { interaction, user, action: "Ban" }, + true, + true, + "Ban Members" + ); + const reason = interaction.options.getString("reason"); await interaction.guild?.members.cache .get(user.id) diff --git a/src/commands/moderation/kick.ts b/src/commands/moderation/kick.ts index 572a23a..686e0b5 100644 --- a/src/commands/moderation/kick.ts +++ b/src/commands/moderation/kick.ts @@ -21,7 +21,15 @@ export default class Kick { async run(interaction: ChatInputCommandInteraction) { const user = interaction.options.getUser("user")!; - errorCheck(PermissionsBitField.Flags.KickMembers, { interaction, user, action: "Kick" }); + + await errorCheck( + PermissionsBitField.Flags.KickMembers, + { interaction, user, action: "Kick" }, + true, + true, + "Kick Members" + ); + const reason = interaction.options.getString("reason"); await interaction.guild?.members.cache .get(user.id) diff --git a/src/commands/moderation/lock.ts b/src/commands/moderation/lock.ts index 4fd3526..aea7db3 100644 --- a/src/commands/moderation/lock.ts +++ b/src/commands/moderation/lock.ts @@ -34,11 +34,11 @@ export default class Lock { const guild = interaction.guild!; const member = guild.members.cache.get(interaction.member?.user.id!)!; - if (!member.permissions.has(PermissionsBitField.Flags.ManageChannels)) + if (!member.permissions.has(PermissionsBitField.Flags.ManageRoles)) return errorEmbed( interaction, "You can't execute this command", - "You need the **Manage Channels** permission." + "You need the **Manage Roles** permission." ); const channelOption = interaction.options.getChannel("channel")!; diff --git a/src/commands/moderation/mute.ts b/src/commands/moderation/mute.ts index 749fe10..a52544e 100644 --- a/src/commands/moderation/mute.ts +++ b/src/commands/moderation/mute.ts @@ -4,8 +4,8 @@ import { type ChatInputCommandInteraction } from "discord.js"; import ms from "ms"; -import { errorCheck, modEmbed } from "../../utils/embeds/modEmbed"; import { errorEmbed } from "../../utils/embeds/errorEmbed"; +import { errorCheck, modEmbed } from "../../utils/embeds/modEmbed"; export default class Mute { data: SlashCommandSubcommandBuilder; @@ -32,7 +32,14 @@ export default class Mute { const duration = interaction.options.getString("duration")!; const reason = interaction.options.getString("reason"); - errorCheck(PermissionsBitField.Flags.ModerateMembers, { interaction, user, action: "Mute" }); + await errorCheck( + PermissionsBitField.Flags.ModerateMembers, + { interaction, user, action: "Mute" }, + true, + true, + "Moderate Members" + ); + if (!ms(duration) || ms(duration) > ms("28d")) return errorEmbed( interaction, diff --git a/src/commands/moderation/unban.ts b/src/commands/moderation/unban.ts index 1657bc0..c3bb5dd 100644 --- a/src/commands/moderation/unban.ts +++ b/src/commands/moderation/unban.ts @@ -2,15 +2,13 @@ import { PermissionsBitField, EmbedBuilder, SlashCommandSubcommandBuilder, - TextChannel, DMChannel, - ChannelType, - type Channel, type ChatInputCommandInteraction } from "discord.js"; import { genColor } from "../../utils/colorGen"; import { errorEmbed } from "../../utils/embeds/errorEmbed"; -import { getSetting } from "../../utils/database/settings"; +import { logChannel } from "../../utils/logChannel"; +import { errorCheck } from "../../utils/embeds/modEmbed"; export default class Unban { data: SlashCommandSubcommandBuilder; @@ -34,12 +32,12 @@ export default class Unban { .map(user => user.user) .filter(user => user.id === id)[0]!; - if (!member.permissions.has(PermissionsBitField.Flags.BanMembers)) - return errorEmbed( - interaction, - "You can't execute this command.", - "You need the **Ban Members** permission." - ); + await errorCheck( + PermissionsBitField.Flags.BanMembers, + { interaction, user: target, action: "Unban" }, + false, + "Ban Members" + ); if (target == undefined) return errorEmbed(interaction, "You can't unban this user.", "The user was never banned."); @@ -52,20 +50,7 @@ export default class Unban { .setFooter({ text: `User ID: ${id}` }) .setColor(genColor(100)); - const logChannel = getSetting(guild.id, "moderation", "channel"); - if (logChannel) { - const channel = await guild.channels.cache - .get(`${logChannel}`) - ?.fetch() - .then((channel: Channel) => { - if (channel.type != ChannelType.GuildText) return null; - return channel as TextChannel; - }) - .catch(() => null); - - if (channel) await channel.send({ embeds: [embed] }); - } - + await logChannel(guild, embed); await guild.members.unban(id); await interaction.reply({ embeds: [embed] }); diff --git a/src/commands/moderation/unlock.ts b/src/commands/moderation/unlock.ts index 5ef3535..369a096 100644 --- a/src/commands/moderation/unlock.ts +++ b/src/commands/moderation/unlock.ts @@ -3,13 +3,11 @@ import { EmbedBuilder, PermissionsBitField, SlashCommandSubcommandBuilder, - TextChannel, - type Channel, type ChatInputCommandInteraction } from "discord.js"; import { genColor } from "../../utils/colorGen"; -import { getSetting } from "../../utils/database/settings"; import { errorEmbed } from "../../utils/embeds/errorEmbed"; +import { logChannel } from "../../utils/logChannel"; export default class Unlock { data: SlashCommandSubcommandBuilder; @@ -34,11 +32,11 @@ export default class Unlock { const guild = interaction.guild!; const member = guild.members.cache.get(interaction.member?.user.id!)!; - if (!member.permissions.has(PermissionsBitField.Flags.ManageChannels)) + if (!member.permissions.has(PermissionsBitField.Flags.ManageRoles)) return errorEmbed( interaction, "You can't execute this command", - "You need the **Manage Channels** permission." + "You need the **Manage Roles** permission." ); const channelOption = interaction.options.getChannel("channel")!; @@ -78,20 +76,7 @@ export default class Unlock { }) .catch(error => console.error(error)); - const logChannel = getSetting(guild.id, "moderation", "channel"); - if (logChannel) { - const channel = await guild.channels.cache - .get(`${logChannel}`) - ?.fetch() - .then((channel: Channel) => { - if (channel.type != ChannelType.GuildText) return null; - return channel as TextChannel; - }) - .catch(() => null); - - if (channel) await channel.send({ embeds: [embed] }); - } - + await logChannel(guild, embed); await interaction.reply({ embeds: [embed] }); } } diff --git a/src/commands/moderation/unmute.ts b/src/commands/moderation/unmute.ts index 4148b9d..5a8d410 100644 --- a/src/commands/moderation/unmute.ts +++ b/src/commands/moderation/unmute.ts @@ -2,15 +2,11 @@ import { SlashCommandSubcommandBuilder, EmbedBuilder, PermissionsBitField, - TextChannel, DMChannel, - ChannelType, - type Channel, type ChatInputCommandInteraction } from "discord.js"; import { genColor } from "../../utils/colorGen"; import { errorEmbed } from "../../utils/embeds/errorEmbed"; -import { getSetting } from "../../utils/database/settings"; import { logChannel } from "../../utils/logChannel"; export default class Unmute { diff --git a/src/commands/moderation/warn.ts b/src/commands/moderation/warn.ts index 4705342..1ced1c8 100644 --- a/src/commands/moderation/warn.ts +++ b/src/commands/moderation/warn.ts @@ -1,16 +1,8 @@ import { SlashCommandSubcommandBuilder, - EmbedBuilder, PermissionsBitField, - TextChannel, - DMChannel, - ChannelType, - type Channel, type ChatInputCommandInteraction } from "discord.js"; -import { genColor } from "../../utils/colorGen"; -import { errorEmbed } from "../../utils/embeds/errorEmbed"; -import { getSetting } from "../../utils/database/settings"; import { addModeration } from "../../utils/database/moderation"; import { errorCheck, modEmbed } from "../../utils/embeds/modEmbed"; @@ -31,14 +23,16 @@ export default class Warn { async run(interaction: ChatInputCommandInteraction) { const user = interaction.options.getUser("user")!; const guild = interaction.guild!; + const reason = interaction.options.getString("reason"); - errorCheck( + await errorCheck( PermissionsBitField.Flags.ModerateMembers, { interaction, user, action: "Warn" }, - "Moderate" + false, + true, + "Moderate Members" ); - const reason = interaction.options.getString("reason"); addModeration( guild.id, user.id, @@ -46,6 +40,7 @@ export default class Warn { guild.members.cache.get(interaction.member?.user.id!)?.id!, reason ?? undefined ); + modEmbed({ interaction, user, action: "Warned" }, reason); } } diff --git a/src/utils/embeds/modEmbed.ts b/src/utils/embeds/modEmbed.ts index 22b9241..8029b6c 100644 --- a/src/utils/embeds/modEmbed.ts +++ b/src/utils/embeds/modEmbed.ts @@ -11,21 +11,37 @@ type Options = { duration?: string | null; }; -export async function errorCheck(permission: bigint, options: Options, permissionAction?: string) { +export async function errorCheck( + permission: bigint, + options: Options, + botError: boolean, + allErrors: boolean, + permissionAction: string +) { const { interaction, user, action } = options; const guild = interaction.guild!; const members = guild.members.cache!; const member = members.get(interaction.member?.user.id!)!; + const client = members.get(interaction.client.user.id!)!; const target = members.get(user.id)!; const name = user.displayName; + if (botError) + if (!client.permissions.has(permission)) + return errorEmbed( + interaction, + "The bot can't execute this command.", + `The bot is missing the **${permissionAction}** permission.` + ); + if (!member.permissions.has(permission)) return errorEmbed( interaction, "You can't execute this command.", - `You need the **${permissionAction ?? action} Members** permission.` + `You're missing the **${permissionAction} Members** permission.` ); + if (!allErrors) return; if (target === member) return errorEmbed(interaction, `You can't ${action.toLowerCase()} yourself.`); From 2380f5b952fce60358b1b1950d1bd28f4102c4b0 Mon Sep 17 00:00:00 2001 From: MrSerge01 <86667481+MrSerge01@users.noreply.github.com> Date: Thu, 11 Jul 2024 21:38:29 +0500 Subject: [PATCH 070/127] some /user changes + even more modEmbed --- src/commands/moderation/ban.ts | 4 ++ src/commands/moderation/kick.ts | 2 +- src/commands/moderation/unban.ts | 32 +++------ src/commands/user.ts | 110 ++++++++++++++++--------------- src/utils/imageColor.ts | 26 ++++---- 5 files changed, 85 insertions(+), 89 deletions(-) diff --git a/src/commands/moderation/ban.ts b/src/commands/moderation/ban.ts index 45e8f5f..0f779dd 100644 --- a/src/commands/moderation/ban.ts +++ b/src/commands/moderation/ban.ts @@ -4,6 +4,7 @@ import { type ChatInputCommandInteraction } from "discord.js"; import { errorCheck, modEmbed } from "../../utils/embeds/modEmbed"; +import { errorEmbed } from "../../utils/embeds/errorEmbed"; export default class Ban { data: SlashCommandSubcommandBuilder; @@ -14,6 +15,9 @@ export default class Ban { .addUserOption(user => user.setName("user").setDescription("The user that you want to ban.").setRequired(true) ) + .addStringOption(string => + string.setName("duration").setDescription("The duration of the ban (e.g 30m, 1d, 2h).") + ) .addStringOption(string => string.setName("reason").setDescription("The reason for the ban.") ); diff --git a/src/commands/moderation/kick.ts b/src/commands/moderation/kick.ts index 686e0b5..b828b81 100644 --- a/src/commands/moderation/kick.ts +++ b/src/commands/moderation/kick.ts @@ -36,6 +36,6 @@ export default class Kick { ?.kick(reason ?? undefined) .catch(error => console.error(error)); - modEmbed({ interaction, user, action: "Kicked" }, reason); + await modEmbed({ interaction, user, action: "Kicked" }, reason); } } diff --git a/src/commands/moderation/unban.ts b/src/commands/moderation/unban.ts index c3bb5dd..3d243c7 100644 --- a/src/commands/moderation/unban.ts +++ b/src/commands/moderation/unban.ts @@ -1,14 +1,10 @@ import { PermissionsBitField, - EmbedBuilder, SlashCommandSubcommandBuilder, - DMChannel, type ChatInputCommandInteraction } from "discord.js"; -import { genColor } from "../../utils/colorGen"; import { errorEmbed } from "../../utils/embeds/errorEmbed"; -import { logChannel } from "../../utils/logChannel"; -import { errorCheck } from "../../utils/embeds/modEmbed"; +import { errorCheck, modEmbed } from "../../utils/embeds/modEmbed"; export default class Unban { data: SlashCommandSubcommandBuilder; @@ -21,20 +17,24 @@ export default class Unban { .setName("id") .setDescription("The ID of the user that you want to unban.") .setRequired(true) + ) + .addStringOption(string => + string.setName("reason").setDescription("The reason for the unban.") ); } async run(interaction: ChatInputCommandInteraction) { const id = interaction.options.getString("id")!; + const reason = interaction.options.getString("reason")!; const guild = interaction.guild!; - const member = guild.members.cache.get(interaction.member?.user.id!)!; const target = (await guild.bans.fetch()) - .map(user => user.user) + .map(ban => ban.user) .filter(user => user.id === id)[0]!; await errorCheck( PermissionsBitField.Flags.BanMembers, { interaction, user: target, action: "Unban" }, + true, false, "Ban Members" ); @@ -42,21 +42,7 @@ export default class Unban { if (target == undefined) return errorEmbed(interaction, "You can't unban this user.", "The user was never banned."); - const embed = new EmbedBuilder() - .setAuthor({ name: `• ${target.displayName}`, iconURL: target.displayAvatarURL() }) - .setTitle(`Unbanned ${target.displayName}.`) - .setDescription(`**Moderator**: ${interaction.user.displayName}`) - .setThumbnail(target.displayAvatarURL()) - .setFooter({ text: `User ID: ${id}` }) - .setColor(genColor(100)); - - await logChannel(guild, embed); - await guild.members.unban(id); - await interaction.reply({ embeds: [embed] }); - - const dmChannel = (await target.createDM().catch(() => null)) as DMChannel | null; - if (!dmChannel) return; - if (target.bot) return; - await dmChannel.send({ embeds: [embed.setTitle("You got unbanned.")] }); + await guild.members.unban(id, reason ?? undefined); + await modEmbed({ interaction, user: target, action: "Unbanned" }, reason); } } diff --git a/src/commands/user.ts b/src/commands/user.ts index 6a6db19..9b56599 100644 --- a/src/commands/user.ts +++ b/src/commands/user.ts @@ -1,15 +1,15 @@ import { - SlashCommandBuilder, - EmbedBuilder, + ActionRowBuilder, ButtonBuilder, - ButtonStyle, ButtonInteraction, - ActionRowBuilder, + ButtonStyle, + EmbedBuilder, + SlashCommandBuilder, type ChatInputCommandInteraction } from "discord.js"; import { genColor } from "../utils/colorGen"; -import { getSetting } from "../utils/database/settings"; import { getLevel, setLevel } from "../utils/database/levelling"; +import { getSetting } from "../utils/database/settings"; import { imageColor } from "../utils/imageColor"; export default class User { @@ -29,7 +29,7 @@ export default class User { .filter(member => member.user.id === id) .map(user => user)[0]!; - const selectedUser = target.user!; + const selectedUser = await target.user.fetch(); let serverInfo = [`Joined on ****`]; const guildRoles = guild.roles.cache.filter(role => target.roles.cache.has(role.id))!; const memberRoles = [...guildRoles].sort( @@ -75,8 +75,10 @@ export default class User { ) .setFooter({ text: `User ID: ${target.id}` }) .setThumbnail(target.displayAvatarURL()!) - .setColor(genColor(200)); - + .setColor( + selectedUser.hexAccentColor ?? (await imageColor(undefined, target)) ?? genColor(200) + ); + const components = []; if (getSetting(`${guild.id}`, "levelling", "enabled") && !selectedUser.bot) { @@ -99,59 +101,63 @@ export default class User { if (!guildLevel && !guildExp) setLevel(`${guild.id}`, `${target.id}`, 0, 0); if (!globalLevel && !globalExp) setLevel("0", `${target.id}`, 0, 0); - const nextLevelExp = Math.floor(100 * 1.15 * ((guildLevel ?? 0) + 1))?.toLocaleString("en-US"); + const nextLevelExp = Math.floor(100 * 1.15 * ((guildLevel ?? 0) + 1))?.toLocaleString( + "en-US" + ); const globalNextLevelExp = Math.floor(100 * 1.15 * ((globalLevel ?? 0) + 1))?.toLocaleString( "en-US" ); - - interaction.channel?.createMessageComponentCollector({ - filter: i => i.user.id === interaction.user.id, - time: 60000 - }) - .on("collect", async (i: ButtonInteraction) => { - const levelEmbed = new EmbedBuilder() - .setAuthor({ - name: `• ${target.nickname ?? selectedUser.displayName}`, - iconURL: target.displayAvatarURL() - }) - .setFields( - { - name: `⚡ • Guild level ${guildLevel ?? 0}`, - value: [ - `**${guildExp.toLocaleString("en-US") ?? 0}/${nextLevelExp}** EXP`, - `**Next level**: ${(guildLevel ?? 0) + 1}` - ].join("\n"), - inline: true - }, - { - name: `⛈️ • Global level ${globalLevel ?? 0}`, - value: [ - `**${globalExp.toLocaleString("en-US") ?? 0}/${globalNextLevelExp}** EXP`, - `**Next level**: ${(globalLevel ?? 0) + 1}` - ].join("\n"), - inline: true - } - ) - .setFooter({ text: `User ID: ${target.id}` }) - .setThumbnail(target.displayAvatarURL()) - .setColor(genColor(200)); - imageColor(levelEmbed, undefined, target); - switch (i.customId) { - case "general": - await interaction.editReply({ embeds: [embed], components: [row] }); break; - case "level": - await interaction.editReply({ embeds: [levelEmbed], components: [row] }); break; - } + interaction.channel + ?.createMessageComponentCollector({ + filter: i => i.user.id === interaction.user.id, + time: 60000 + }) + .on("collect", async (i: ButtonInteraction) => { + const levelEmbed = new EmbedBuilder() + .setAuthor({ + name: `• ${target.nickname ?? selectedUser.displayName}`, + iconURL: target.displayAvatarURL() + }) + .setFields( + { + name: `⚡ • Guild level ${guildLevel ?? 0}`, + value: [ + `**${guildExp.toLocaleString("en-US") ?? 0}/${nextLevelExp}** EXP`, + `**Next level**: ${(guildLevel ?? 0) + 1}` + ].join("\n"), + inline: true + }, + { + name: `⛈️ • Global level ${globalLevel ?? 0}`, + value: [ + `**${globalExp.toLocaleString("en-US") ?? 0}/${globalNextLevelExp}** EXP`, + `**Next level**: ${(globalLevel ?? 0) + 1}` + ].join("\n"), + inline: true + } + ) + .setFooter({ text: `User ID: ${target.id}` }) + .setThumbnail(target.displayAvatarURL()) + .setColor( + selectedUser.hexAccentColor ?? (await imageColor(undefined, target)) ?? genColor(200) + ); + + switch (i.customId) { + case "general": + await interaction.editReply({ embeds: [embed], components: [row] }); + break; + case "level": + await interaction.editReply({ embeds: [levelEmbed], components: [row] }); + break; + } - i.update({}); - }); + i.update({}); + }); components.push(row); } - imageColor(embed, undefined, target); await interaction.reply({ embeds: [embed], components: components }); - } } diff --git a/src/utils/imageColor.ts b/src/utils/imageColor.ts index 95a96c2..84b9a79 100644 --- a/src/utils/imageColor.ts +++ b/src/utils/imageColor.ts @@ -1,22 +1,22 @@ -import type { Guild, ColorResolvable, EmbedBuilder, GuildMember } from "discord.js"; +import type { ColorResolvable, Guild, GuildMember } from "discord.js"; import Vibrant from "node-vibrant"; import sharp from "sharp"; import { genRGBColor } from "./colorGen"; /** - * Outputs and sets the most vibrant color from the image. - * @param embed Embed to set the color to. + * Outputs the most vibrant color from the image. * @param guild Guild image. * @param member Member image. - * @returns Embned with the set color. + * @returns The color in HEX. */ -export async function imageColor(embed: EmbedBuilder, guild?: Guild, member?: GuildMember) { - try { - const imageBuffer = await ( - await fetch(guild ? guild.iconURL()! : member?.displayAvatarURL()!) - ).arrayBuffer(); - const image = sharp(imageBuffer).toFormat("jpg"); - const { r, g, b } = (await new Vibrant(await image.toBuffer()).getPalette()).Vibrant!; - return embed.setColor(genRGBColor(r, g, b) as ColorResolvable); - } catch {} +export async function imageColor(guild?: Guild, member?: GuildMember) { + const imageBuffer = await ( + await fetch(guild ? guild.iconURL()! : member?.displayAvatarURL()!) + ).arrayBuffer(); + + const { r, g, b } = ( + await new Vibrant(await sharp(imageBuffer).toFormat("jpg").toBuffer()).getPalette() + ).Vibrant!; + + return genRGBColor(Math.round(r), Math.round(g), Math.round(b)) as ColorResolvable; } From 036d5fe2f08bcd57f5c5a506345e0a3eba6f1096 Mon Sep 17 00:00:00 2001 From: MrSerge01 <86667481+MrSerge01@users.noreply.github.com> Date: Mon, 22 Jul 2024 23:53:14 +0500 Subject: [PATCH 071/127] i think modEmbed is finally done --- src/commands/moderation/ban.ts | 4 +- src/commands/moderation/delwarn.ts | 57 ++++++----------------------- src/commands/moderation/kick.ts | 5 +-- src/commands/moderation/lock.ts | 23 ++---------- src/commands/moderation/mute.ts | 3 +- src/commands/moderation/purge.ts | 35 ++++++------------ src/commands/moderation/slowdown.ts | 31 +++++----------- src/commands/moderation/unban.ts | 5 +-- src/commands/moderation/unmute.ts | 52 +++++++------------------- src/commands/moderation/warn.ts | 25 +++++++------ src/utils/embeds/modEmbed.ts | 27 +++++++++----- 11 files changed, 88 insertions(+), 179 deletions(-) diff --git a/src/commands/moderation/ban.ts b/src/commands/moderation/ban.ts index 0f779dd..8484df9 100644 --- a/src/commands/moderation/ban.ts +++ b/src/commands/moderation/ban.ts @@ -4,7 +4,6 @@ import { type ChatInputCommandInteraction } from "discord.js"; import { errorCheck, modEmbed } from "../../utils/embeds/modEmbed"; -import { errorEmbed } from "../../utils/embeds/errorEmbed"; export default class Ban { data: SlashCommandSubcommandBuilder; @@ -29,8 +28,7 @@ export default class Ban { await errorCheck( PermissionsBitField.Flags.BanMembers, { interaction, user, action: "Ban" }, - true, - true, + { allErrors: true, botError: true, ownerError: true }, "Ban Members" ); diff --git a/src/commands/moderation/delwarn.ts b/src/commands/moderation/delwarn.ts index b30746c..8ae471f 100644 --- a/src/commands/moderation/delwarn.ts +++ b/src/commands/moderation/delwarn.ts @@ -1,17 +1,15 @@ import { - SlashCommandSubcommandBuilder, + DMChannel, EmbedBuilder, PermissionsBitField, - TextChannel, - DMChannel, - ChannelType, - type Channel, + SlashCommandSubcommandBuilder, type ChatInputCommandInteraction } from "discord.js"; import { genColor } from "../../utils/colorGen"; -import { errorEmbed } from "../../utils/embeds/errorEmbed"; import { listUserModeration, removeModeration } from "../../utils/database/moderation"; -import { getSetting } from "../../utils/database/settings"; +import { errorEmbed } from "../../utils/embeds/errorEmbed"; +import { errorCheck } from "../../utils/embeds/modEmbed"; +import { logChannel } from "../../utils/logChannel"; export default class Delwarn { data: SlashCommandSubcommandBuilder; @@ -33,39 +31,21 @@ export default class Delwarn { async run(interaction: ChatInputCommandInteraction) { const user = interaction.options.getUser("user")!; const guild = interaction.guild!; - const members = guild.members.cache; - const member = members.get(interaction.member?.user.id!)!; - const target = members.get(user.id)!; const name = user.displayName; const id = interaction.options.getNumber("id", true); const warns = listUserModeration(guild.id, user.id, "WARN"); const newWarns = warns.filter(warn => warn.id !== `${id}`); - if (!member.permissions.has(PermissionsBitField.Flags.ModerateMembers)) - return errorEmbed( - interaction, - "You can't execute this command.", - "You need the **Moderate Members** permission." - ); + await errorCheck( + PermissionsBitField.Flags.ModerateMembers, + { interaction, user, action: "Remove a warn" }, + { allErrors: true, botError: false, ownerError: false }, + "Moderate Members" + ); - if (target === member) return errorEmbed(interaction, "You can't remove a warn from yourself."); if (newWarns.length === warns.length) return errorEmbed(interaction, `There is no warn with the id of ${id}.`); - if (!target.manageable) - return errorEmbed( - interaction, - `You can't remove a warn from ${name}.`, - "The member has a higher role position than Sokora." - ); - - if (member.roles.highest.position < target.roles.highest.position) - return errorEmbed( - interaction, - `You can't remove a warn from ${name}.`, - "The member has a higher role position than you." - ); - const embed = new EmbedBuilder() .setAuthor({ name: `• ${name}`, iconURL: user.displayAvatarURL() }) .setTitle(`Removed a warning from ${name}.`) @@ -74,20 +54,7 @@ export default class Delwarn { .setFooter({ text: `User ID: ${user.id}` }) .setColor(genColor(100)); - const logChannel = getSetting(guild.id, "moderation", "channel"); - if (logChannel) { - const channel = await guild.channels.cache - .get(`${logChannel}`) - ?.fetch() - .then((channel: Channel) => { - if (channel.type != ChannelType.GuildText) return null; - return channel as TextChannel; - }) - .catch(() => null); - - if (channel) await channel.send({ embeds: [embed] }); - } - + await logChannel(guild, embed); try { removeModeration(guild.id, `${id}`); } catch (error) { diff --git a/src/commands/moderation/kick.ts b/src/commands/moderation/kick.ts index b828b81..2af0292 100644 --- a/src/commands/moderation/kick.ts +++ b/src/commands/moderation/kick.ts @@ -1,6 +1,6 @@ import { - SlashCommandSubcommandBuilder, PermissionsBitField, + SlashCommandSubcommandBuilder, type ChatInputCommandInteraction } from "discord.js"; import { errorCheck, modEmbed } from "../../utils/embeds/modEmbed"; @@ -25,8 +25,7 @@ export default class Kick { await errorCheck( PermissionsBitField.Flags.KickMembers, { interaction, user, action: "Kick" }, - true, - true, + { allErrors: true, botError: true, ownerError: true }, "Kick Members" ); diff --git a/src/commands/moderation/lock.ts b/src/commands/moderation/lock.ts index aea7db3..d639f44 100644 --- a/src/commands/moderation/lock.ts +++ b/src/commands/moderation/lock.ts @@ -1,15 +1,13 @@ import { - SlashCommandSubcommandBuilder, + ChannelType, EmbedBuilder, PermissionsBitField, - ChannelType, - TextChannel, - type Channel, + SlashCommandSubcommandBuilder, type ChatInputCommandInteraction } from "discord.js"; import { genColor } from "../../utils/colorGen"; import { errorEmbed } from "../../utils/embeds/errorEmbed"; -import { getSetting } from "../../utils/database/settings"; +import { logChannel } from "../../utils/logChannel"; export default class Lock { data: SlashCommandSubcommandBuilder; @@ -78,20 +76,7 @@ export default class Lock { }) .catch(error => console.error(error)); - const logChannel = getSetting(guild.id, "moderation", "channel"); - if (logChannel) { - const channel = await guild.channels.cache - .get(`${logChannel}`) - ?.fetch() - .then((channel: Channel) => { - if (channel.type != ChannelType.GuildText) return null; - return channel as TextChannel; - }) - .catch(() => null); - - if (channel) await channel.send({ embeds: [embed] }); - } - + await logChannel(guild, embed); await interaction.reply({ embeds: [embed] }); } } diff --git a/src/commands/moderation/mute.ts b/src/commands/moderation/mute.ts index a52544e..2136011 100644 --- a/src/commands/moderation/mute.ts +++ b/src/commands/moderation/mute.ts @@ -35,8 +35,7 @@ export default class Mute { await errorCheck( PermissionsBitField.Flags.ModerateMembers, { interaction, user, action: "Mute" }, - true, - true, + { allErrors: true, botError: true, ownerError: true }, "Moderate Members" ); diff --git a/src/commands/moderation/purge.ts b/src/commands/moderation/purge.ts index e4a1a20..d856c2b 100644 --- a/src/commands/moderation/purge.ts +++ b/src/commands/moderation/purge.ts @@ -1,15 +1,13 @@ import { - SlashCommandSubcommandBuilder, + ChannelType, EmbedBuilder, PermissionsBitField, - ChannelType, - TextChannel, - type Channel, + SlashCommandSubcommandBuilder, type ChatInputCommandInteraction } from "discord.js"; import { genColor } from "../../utils/colorGen"; import { errorEmbed } from "../../utils/embeds/errorEmbed"; -import { getSetting } from "../../utils/database/settings"; +import { logChannel } from "../../utils/logChannel"; export default class Purge { data: SlashCommandSubcommandBuilder; @@ -20,7 +18,7 @@ export default class Purge { .addNumberOption(number => number .setName("amount") - .setDescription("The amount of messages that you want to purge.") + .setDescription("The amount of messages that you want to purge (maximum is 100).") .setRequired(true) ) .addChannelOption(channel => @@ -71,24 +69,15 @@ export default class Purge { ChannelType.PrivateThread && ChannelType.GuildVoice ) - channel == interaction.channel - ? await channel.bulkDelete(amount + 1, true) - : await channel.bulkDelete(amount, true); - - const logChannel = getSetting(guild.id, "moderation", "channel"); - if (logChannel) { - const channel = await guild.channels.cache - .get(`${logChannel}`) - ?.fetch() - .then((channel: Channel) => { - if (channel.type != ChannelType.GuildText) return null; - return channel as TextChannel; - }) - .catch(() => null); - - if (channel) await channel.send({ embeds: [embed] }); - } + try { + channel == interaction.channel + ? await channel.bulkDelete(amount + 1, true) + : await channel.bulkDelete(amount, true); + } catch (error) { + console.error(error); + } + await logChannel(guild, embed); await interaction.reply({ embeds: [embed] }); } } diff --git a/src/commands/moderation/slowdown.ts b/src/commands/moderation/slowdown.ts index 68acb3d..843f2a1 100644 --- a/src/commands/moderation/slowdown.ts +++ b/src/commands/moderation/slowdown.ts @@ -1,16 +1,14 @@ import { - SlashCommandSubcommandBuilder, + ChannelType, EmbedBuilder, PermissionsBitField, - ChannelType, - TextChannel, - type Channel, + SlashCommandSubcommandBuilder, type ChatInputCommandInteraction } from "discord.js"; +import ms from "ms"; import { genColor } from "../../utils/colorGen"; import { errorEmbed } from "../../utils/embeds/errorEmbed"; -import { getSetting } from "../../utils/database/settings"; -import ms from "ms"; +import { logChannel } from "../../utils/logChannel"; export default class Slowdown { data: SlashCommandSubcommandBuilder; @@ -47,7 +45,7 @@ export default class Slowdown { const time = interaction.options.getString("time")!; const member = guild.members.cache.get(interaction.member?.user.id!)!; - if (!member.permissions.has(PermissionsBitField.Flags.ManageMessages)) + if (!member.permissions.has(PermissionsBitField.Flags.ManageChannels)) return errorEmbed( interaction, "You can't execute this command", @@ -80,22 +78,11 @@ export default class Slowdown { ChannelType.PrivateThread && ChannelType.GuildVoice ) - await channel.setRateLimitPerUser(ms(time) / 1000, interaction.options.getString("reason")!); - - const logChannel = getSetting(guild.id, "moderation", "channel"); - if (logChannel) { - const channel = await guild.channels.cache - .get(`${logChannel}`) - ?.fetch() - .then((channel: Channel) => { - if (channel.type != ChannelType.GuildText) return null; - return channel as TextChannel; - }) - .catch(() => null); - - if (channel) await channel.send({ embeds: [embed] }); - } + await channel + .setRateLimitPerUser(ms(time) / 1000, interaction.options.getString("reason")!) + .catch(error => console.error(error)); + await logChannel(guild, embed); await interaction.reply({ embeds: [embed] }); } } diff --git a/src/commands/moderation/unban.ts b/src/commands/moderation/unban.ts index 3d243c7..4a88439 100644 --- a/src/commands/moderation/unban.ts +++ b/src/commands/moderation/unban.ts @@ -34,15 +34,14 @@ export default class Unban { await errorCheck( PermissionsBitField.Flags.BanMembers, { interaction, user: target, action: "Unban" }, - true, - false, + { allErrors: false, botError: true, ownerError: true }, "Ban Members" ); if (target == undefined) return errorEmbed(interaction, "You can't unban this user.", "The user was never banned."); - await guild.members.unban(id, reason ?? undefined); + await guild.members.unban(id, reason ?? undefined).catch(error => console.error(error)); await modEmbed({ interaction, user: target, action: "Unbanned" }, reason); } } diff --git a/src/commands/moderation/unmute.ts b/src/commands/moderation/unmute.ts index 5a8d410..d1eeb3c 100644 --- a/src/commands/moderation/unmute.ts +++ b/src/commands/moderation/unmute.ts @@ -1,13 +1,10 @@ import { - SlashCommandSubcommandBuilder, - EmbedBuilder, PermissionsBitField, - DMChannel, + SlashCommandSubcommandBuilder, type ChatInputCommandInteraction } from "discord.js"; -import { genColor } from "../../utils/colorGen"; import { errorEmbed } from "../../utils/embeds/errorEmbed"; -import { logChannel } from "../../utils/logChannel"; +import { errorCheck, modEmbed } from "../../utils/embeds/modEmbed"; export default class Unmute { data: SlashCommandSubcommandBuilder; @@ -21,43 +18,20 @@ export default class Unmute { } async run(interaction: ChatInputCommandInteraction) { - const guild = interaction.guild!; - const members = guild.members.cache!; - if ( - !members - .get(interaction.member!.user.id)! - .permissions.has(PermissionsBitField.Flags.ModerateMembers) - ) - return errorEmbed( - interaction, - "You can't execute this command.", - "You need the **Mute Members** permission." - ); - const user = interaction.options.getUser("user")!; - if (members.get(user.id)?.communicationDisabledUntil === null) - return errorEmbed(interaction, "You can't unmute this user.", "The user was never muted."); + const target = interaction.guild?.members.cache.get(user.id)!; - const embed = new EmbedBuilder() - .setAuthor({ name: `• ${user.displayName}`, iconURL: user.displayAvatarURL() }) - .setTitle(`Unmuted ${user.displayName}.`) - .setDescription( - [ - `**Moderator**: ${interaction.user.displayName}`, - `**Date**: ` - ].join("\n") - ) - .setThumbnail(user.displayAvatarURL()) - .setFooter({ text: `User ID: ${user.id}` }) - .setColor(genColor(100)); + errorCheck( + PermissionsBitField.Flags.ModerateMembers, + { interaction, user, action: "Unmute" }, + { allErrors: false, botError: true, ownerError: false }, + "Moderate Members" + ); - await logChannel(guild, embed); - await members.get(user.id)!.edit({ communicationDisabledUntil: null }); - await interaction.reply({ embeds: [embed] }); + if (target.communicationDisabledUntil === null) + return errorEmbed(interaction, "You can't unmute this user.", "The user was never muted."); - const dmChannel = (await user.createDM().catch(() => null)) as DMChannel | null; - if (!dmChannel) return; - if (user.bot) return; - await dmChannel.send({ embeds: [embed.setTitle("You got unmuted.")] }); + await target.edit({ communicationDisabledUntil: null }).catch(error => console.error(error)); + await modEmbed({ interaction, user, action: "Unmuted" }, undefined, true); } } diff --git a/src/commands/moderation/warn.ts b/src/commands/moderation/warn.ts index 1ced1c8..40768eb 100644 --- a/src/commands/moderation/warn.ts +++ b/src/commands/moderation/warn.ts @@ -1,6 +1,6 @@ import { - SlashCommandSubcommandBuilder, PermissionsBitField, + SlashCommandSubcommandBuilder, type ChatInputCommandInteraction } from "discord.js"; import { addModeration } from "../../utils/database/moderation"; @@ -28,19 +28,22 @@ export default class Warn { await errorCheck( PermissionsBitField.Flags.ModerateMembers, { interaction, user, action: "Warn" }, - false, - true, + { allErrors: true, botError: false, ownerError: true }, "Moderate Members" ); - addModeration( - guild.id, - user.id, - "WARN", - guild.members.cache.get(interaction.member?.user.id!)?.id!, - reason ?? undefined - ); + try { + addModeration( + guild.id, + user.id, + "WARN", + guild.members.cache.get(interaction.member?.user.id!)?.id!, + reason ?? undefined + ); + } catch (error) { + console.error(error); + } - modEmbed({ interaction, user, action: "Warned" }, reason); + await modEmbed({ interaction, user, action: "Warned" }, reason); } } diff --git a/src/utils/embeds/modEmbed.ts b/src/utils/embeds/modEmbed.ts index 8029b6c..a2eb5bb 100644 --- a/src/utils/embeds/modEmbed.ts +++ b/src/utils/embeds/modEmbed.ts @@ -11,14 +11,20 @@ type Options = { duration?: string | null; }; +type ErrorOptions = { + allErrors: boolean; + botError: boolean; + ownerError: boolean; +}; + export async function errorCheck( permission: bigint, options: Options, - botError: boolean, - allErrors: boolean, + errorOptions: ErrorOptions, permissionAction: string ) { const { interaction, user, action } = options; + const { allErrors, botError, ownerError } = errorOptions; const guild = interaction.guild!; const members = guild.members.cache!; const member = members.get(interaction.member?.user.id!)!; @@ -62,15 +68,17 @@ export async function errorCheck( "The member has a higher role position than you." ); - if (member.id === guild.ownerId) - return errorEmbed( - interaction, - `You can't ${action.toLowerCase()} ${name}.`, - "The member owns the server." - ); + if (ownerError) { + if (member.id === guild.ownerId) + return errorEmbed( + interaction, + `You can't ${action.toLowerCase()} ${name}.`, + "The member owns the server." + ); + } } -export async function modEmbed(options: Options, reason?: string | null) { +export async function modEmbed(options: Options, reason?: string | null, date?: boolean) { const { interaction, user, action, duration } = options; const guild = interaction.guild!; const name = user.displayName; @@ -78,6 +86,7 @@ export async function modEmbed(options: Options, reason?: string | null) { const generalValues = [`**Moderator**: ${interaction.user.displayName}`]; if (duration) generalValues.push(`**Duration**: ${ms(ms(duration), { long: true })}`); if (reason) generalValues.push(`**Reason**: ${reason}`); + if (date) generalValues.push(`**Date**: `); const embed = new EmbedBuilder() .setAuthor({ name: `• ${name}`, iconURL: user.displayAvatarURL() }) From 0af85558655a9447004a4b0bebad88b593367239 Mon Sep 17 00:00:00 2001 From: MrSerge01 <86667481+MrSerge01@users.noreply.github.com> Date: Sat, 27 Jul 2024 23:51:11 +0500 Subject: [PATCH 072/127] button progress (more on this later) --- bun.lockb | Bin 57167 -> 57167 bytes package.json | 4 ++-- src/commands/about.ts | 2 +- src/commands/moderation/warns.ts | 4 ++-- src/commands/serverboard.ts | 8 ++++---- src/commands/user.ts | 25 +++++++++++++++++-------- src/utils/embeds/errorEmbed.ts | 8 ++++---- 7 files changed, 30 insertions(+), 21 deletions(-) diff --git a/bun.lockb b/bun.lockb index d9bdc1066b0590e381466960c796ecfc936d48b1..6cb272719a61a930808f6a53bac83074852599fc 100644 GIT binary patch delta 229 zcmVC@5A z2;CFlv`2>PU-6X65z=|a$1qaO1h)ww`+c5YavPM{*9V9BlPzi(K%+lu*rB4i23(0> zCr delta 230 zcmV9&qptPtUCYvs>Jdv(f!ovu z`DBKLo;gz^GU3f0O#wt4;*&du4x?s02H@r=%o3{yE+Q0hlPzi(K%0@LLb54A)N*}f zXlt5p!j(WUIFLtClYqk+Ht<%=pQPLO!snAw6_A9naYWz2%NCP4?vLGw2+!WQ8gbA# g60?PBxO0;N*&vf$-WmciIFkX^Ad^eo7L%{upgoakY5)KL diff --git a/package.json b/package.json index a798301..36ccf6b 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "sharp": "^0.33.4" }, "devDependencies": { - "bun-types": "^1.1.18", - "typescript": "^5.5.3" + "bun-types": "^1.1.21", + "typescript": "^5.5.4" } } diff --git a/src/commands/about.ts b/src/commands/about.ts index 7a35ea6..0f72d60 100644 --- a/src/commands/about.ts +++ b/src/commands/about.ts @@ -1,4 +1,4 @@ -import { SlashCommandBuilder, EmbedBuilder, type ChatInputCommandInteraction } from "discord.js"; +import { EmbedBuilder, SlashCommandBuilder, type ChatInputCommandInteraction } from "discord.js"; import { genColor } from "../utils/colorGen"; import { randomise } from "../utils/randomise"; diff --git a/src/commands/moderation/warns.ts b/src/commands/moderation/warns.ts index 623049a..6124fa3 100644 --- a/src/commands/moderation/warns.ts +++ b/src/commands/moderation/warns.ts @@ -1,12 +1,12 @@ import { - SlashCommandSubcommandBuilder, EmbedBuilder, PermissionsBitField, + SlashCommandSubcommandBuilder, type ChatInputCommandInteraction } from "discord.js"; import { genColor } from "../../utils/colorGen"; -import { errorEmbed } from "../../utils/embeds/errorEmbed"; import { listUserModeration } from "../../utils/database/moderation"; +import { errorEmbed } from "../../utils/embeds/errorEmbed"; export default class Warns { data: SlashCommandSubcommandBuilder; diff --git a/src/commands/serverboard.ts b/src/commands/serverboard.ts index 0e39486..359d379 100644 --- a/src/commands/serverboard.ts +++ b/src/commands/serverboard.ts @@ -1,15 +1,15 @@ import { - SlashCommandBuilder, - ButtonBuilder, ActionRowBuilder, - ButtonStyle, + ButtonBuilder, ButtonInteraction, + ButtonStyle, ComponentType, + SlashCommandBuilder, type ChatInputCommandInteraction } from "discord.js"; -import { serverEmbed } from "../utils/embeds/serverEmbed"; import { listPublicServers } from "../utils/database/settings"; import { errorEmbed } from "../utils/embeds/errorEmbed"; +import { serverEmbed } from "../utils/embeds/serverEmbed"; export default class Serverboard { data: Omit; diff --git a/src/commands/user.ts b/src/commands/user.ts index 9b56599..0c01870 100644 --- a/src/commands/user.ts +++ b/src/commands/user.ts @@ -10,6 +10,7 @@ import { import { genColor } from "../utils/colorGen"; import { getLevel, setLevel } from "../utils/database/levelling"; import { getSetting } from "../utils/database/settings"; +import { errorEmbed } from "../utils/embeds/errorEmbed"; import { imageColor } from "../utils/imageColor"; export default class User { @@ -57,7 +58,7 @@ export default class User { }) .setFields( { - name: `<:realdiscord:1221878641462345788> • Discord info`, + name: `<:discord:1266797021126459423> • Discord info`, value: [ `Username is **${selectedUser.username}**`, `Display name is ${ @@ -80,7 +81,6 @@ export default class User { ); const components = []; - if (getSetting(`${guild.id}`, "levelling", "enabled") && !selectedUser.bot) { const row = new ActionRowBuilder(); row.addComponents( @@ -95,6 +95,7 @@ export default class User { .setEmoji("⚡") .setStyle(ButtonStyle.Primary) ); + row.components[0].setDisabled(true); const [guildLevel, guildExp] = getLevel(`${guild.id}`, `${target.id}`)!; const [globalLevel, globalExp] = getLevel("0", `${target.id}`)!; @@ -109,11 +110,17 @@ export default class User { ); interaction.channel - ?.createMessageComponentCollector({ - filter: i => i.user.id === interaction.user.id, - time: 60000 - }) + ?.createMessageComponentCollector({ time: 60000 }) .on("collect", async (i: ButtonInteraction) => { + if (i.member?.user.id !== interaction.member?.user.id) + return await errorEmbed(i, "You aren't the person who executed this command"); + + setTimeout(async () => await interaction.editReply({ components: [] }), 60000); + + i.customId === "general" + ? row.components[0].setDisabled(true) + : row.components[1].setDisabled(true); + const levelEmbed = new EmbedBuilder() .setAuthor({ name: `• ${target.nickname ?? selectedUser.displayName}`, @@ -145,14 +152,16 @@ export default class User { switch (i.customId) { case "general": - await interaction.editReply({ embeds: [embed], components: [row] }); + row.components[1].setDisabled(false); + await interaction.editReply({ embeds: [levelEmbed], components: [row] }); break; case "level": + row.components[0].setDisabled(false); await interaction.editReply({ embeds: [levelEmbed], components: [row] }); break; } - i.update({}); + await i.update({}); }); components.push(row); diff --git a/src/utils/embeds/errorEmbed.ts b/src/utils/embeds/errorEmbed.ts index fb85e47..78b9dae 100644 --- a/src/utils/embeds/errorEmbed.ts +++ b/src/utils/embeds/errorEmbed.ts @@ -1,4 +1,4 @@ -import { EmbedBuilder, type ChatInputCommandInteraction } from "discord.js"; +import { EmbedBuilder, type ChatInputCommandInteraction, type ButtonInteraction } from "discord.js"; import { genColor } from "../colorGen"; /** @@ -8,8 +8,8 @@ import { genColor } from "../colorGen"; * @param reason The reason of the error. * @returns Embed with the error description. */ -export function errorEmbed( - interaction: ChatInputCommandInteraction, +export async function errorEmbed( + interaction: ChatInputCommandInteraction | ButtonInteraction, title: string, reason?: string ) { @@ -20,5 +20,5 @@ export function errorEmbed( .setDescription(content.join("\n")) .setColor(genColor(0)); - return interaction.reply({ embeds: [embed], ephemeral: true }); + return await interaction.reply({ embeds: [embed], ephemeral: true }); } From e10aa7157f7240131bbcc04263e6745182ca1e2b Mon Sep 17 00:00:00 2001 From: MrSerge01 <86667481+MrSerge01@users.noreply.github.com> Date: Sun, 28 Jul 2024 00:16:54 +0500 Subject: [PATCH 073/127] guh --- src/commands/user.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/user.ts b/src/commands/user.ts index 0c01870..38a19d9 100644 --- a/src/commands/user.ts +++ b/src/commands/user.ts @@ -153,7 +153,7 @@ export default class User { switch (i.customId) { case "general": row.components[1].setDisabled(false); - await interaction.editReply({ embeds: [levelEmbed], components: [row] }); + await interaction.editReply({ embeds: [embed], components: [row] }); break; case "level": row.components[0].setDisabled(false); From 8ce0847ad938fb135687b30d8e0378dad89e0d4a Mon Sep 17 00:00:00 2001 From: MrSerge01 <86667481+MrSerge01@users.noreply.github.com> Date: Thu, 1 Aug 2024 23:55:45 +0500 Subject: [PATCH 074/127] ok these changes are unfinished as hell lmao --- src/commands/about.ts | 2 +- src/commands/moderation/lock.ts | 2 +- src/commands/moderation/unlock.ts | 2 +- src/events/messageCreate.ts | 2 +- src/utils/database/settings.ts | 103 +++++++++++++++++++----------- src/utils/embeds/modEmbed.ts | 6 +- 6 files changed, 73 insertions(+), 44 deletions(-) diff --git a/src/commands/about.ts b/src/commands/about.ts index 0f72d60..5304506 100644 --- a/src/commands/about.ts +++ b/src/commands/about.ts @@ -37,7 +37,7 @@ export default class About { name: "🌌 • Entities involved", value: [ "**Founder**: Goos", - "**Developers**: Dimkauzh, Froxcey, Golem64, Spectrum, Nikkerudon, ThatBOI", + "**Developers**: Dimkauzh, Froxcey, Golem64, MQuery, Nikkerudon, Spectrum, ThatBOI", "**Designers**: ArtyH, Optix, proJM, Slider_on_the_black", "**Translators**: Dimkauzh, Golem64, Optix, SaFire, ThatBOI, Trynera", "**Testers**: Blaze, fishy, flojo, Tech, Trynera", diff --git a/src/commands/moderation/lock.ts b/src/commands/moderation/lock.ts index d639f44..cf5889e 100644 --- a/src/commands/moderation/lock.ts +++ b/src/commands/moderation/lock.ts @@ -14,7 +14,7 @@ export default class Lock { constructor() { this.data = new SlashCommandSubcommandBuilder() .setName("lock") - .setDescription("Locks a channel") + .setDescription("Locks a channel.") .addChannelOption(channel => channel .setName("channel") diff --git a/src/commands/moderation/unlock.ts b/src/commands/moderation/unlock.ts index 369a096..1d2968f 100644 --- a/src/commands/moderation/unlock.ts +++ b/src/commands/moderation/unlock.ts @@ -14,7 +14,7 @@ export default class Unlock { constructor() { this.data = new SlashCommandSubcommandBuilder() .setName("unlock") - .setDescription("Unlocks a channel") + .setDescription("Unlocks a channel.") .addChannelOption(channel => channel .setName("channel") diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts index f163fc6..23c7279 100644 --- a/src/events/messageCreate.ts +++ b/src/events/messageCreate.ts @@ -41,7 +41,7 @@ export default { for (const channelID of kominator(blockedChannels)) if (message.channelId === channelID) return; - let expGain = (getSetting(guild.id, "levelling", "set_xp_gain")! as number) ?? 2; + let expGain = getSetting(guild.id, "levelling", "set_xp_gain")! as number; const multiplier = getSetting(guild.id, "levelling", "add_multiplier")! as string; if (multiplier != null) { const expMultiplier = kominator(multiplier); diff --git a/src/utils/database/settings.ts b/src/utils/database/settings.ts index 2653e13..348e34a 100644 --- a/src/utils/database/settings.ts +++ b/src/utils/database/settings.ts @@ -10,48 +10,77 @@ const tableDefinition = { } } satisfies TableDefinition; -export const settingsDefinition: Record> = { +export const settingsDefinition: Record< + string, + Record +> = { levelling: { - enabled: ["BOOL", "Enable/disable the levelling system."], - channel: [ - "TEXT", - "ID of the log channel for levelling-related stuff (i.e someone levelling up)." - ], - block_channels: [ - "TEXT", - "ID(s) of the channels where messages aren't counted, comma separated." - ], - set_level: ["TEXT", "Set the level of a user."], - add_multiplier: [ - "TEXT", - "Add an XP multiplier to the levelling system. Syntax: multiplier, role/channel (choose), id." - ], - set_xp_gain: ["INTEGER", "Set the amount of XP a user gains per message."], - set_cooldown: ["INTEGER", "Set the cooldown between messages that add XP."] + enabled: { type: "BOOL", desc: "Enable/disable the levelling system.", val: true }, + channel: { + type: "TEXT", + desc: "ID of the log channel for levelling-related stuff (i.e someone levelling up)." + }, + block_channels: { + type: "TEXT", + desc: "ID(s) of the channels where messages aren't counted, comma separated." + }, + set_level: { type: "TEXT", desc: "Set the level of a user." }, + add_multiplier: { + type: "TEXT", + desc: "Add an XP multiplier to the levelling system. Syntax: multiplier, role/channel (choose), id." + }, + set_xp_gain: { + type: "INTEGER", + desc: "Set the amount of XP a user gains per message.", + val: 2 + }, + set_cooldown: { + type: "INTEGER", + desc: "Set the cooldown between messages that add XP.", + val: 2 + } }, moderation: { - channel: [ - "TEXT", - "ID of the log channel for moderation-related stuff (i.e a message being edited)." - ], - log_messages: ["BOOL", "Whether or not edited/deleted messages should be logged."] + channel: { + type: "TEXT", + desc: "ID of the log channel for moderation-related stuff (i.e a message being edited)." + }, + log_messages: { + type: "BOOL", + desc: "Whether or not edited/deleted messages should be logged.", + val: true + } }, news: { - channel_id: ["TEXT", "ID of the channel where news messages are sent."], - role_id: ["TEXT", "ID of the roles that should be pinged when a news message is sent."], - edit_original_message: [ - "BOOL", - "Whether or not the original message should be edited when a news message is updated." - ] + channel_id: { type: "TEXT", desc: "ID of the channel where news messages are sent." }, + role_id: { + type: "TEXT", + desc: "ID of the roles that should be pinged when a news message is sent." + }, + edit_original_message: { + type: "BOOL", + desc: "Whether or not the original message should be edited when a news message is updated.", + val: true + } }, serverboard: { - invite_link: ["TEXT", "The invite link which is shown on the serverboard."], - shown: ["BOOL", "Whether or not the server should be shown on the serverboard."] + invite_link: { + type: "TEXT", + desc: "The invite link which is shown on the serverboard." + }, + shown: { + type: "BOOL", + desc: "Whether or not the server should be shown on the serverboard.", + val: false + } }, welcome: { - text: ["TEXT", "The welcome message that is sent when a user joins."], - goodbye_text: ["TEXT", "The goodbye message that is sent when a user leaves."], - channel: ["TEXT", "ID of the channel where welcome messages are sent."] + text: { type: "TEXT", desc: "The welcome message that is sent when a user joins." }, + goodbye_text: { + type: "TEXT", + desc: "The goodbye message that is sent when a user leaves." + }, + channel: { type: "TEXT", desc: "ID of the channel where welcome messages are sent." } } }; @@ -75,7 +104,7 @@ export function getSetting( typeof tableDefinition >[]; if (res.length == 0) return null; - switch (settingsDefinition[key][setting][0]) { + switch (settingsDefinition[key][setting].type) { case "TEXT": return res[0].value as TypeOfKey; case "BOOL": @@ -93,7 +122,7 @@ export function setSetting( guildID: string, key: K, setting: string, - value: string //TypeOfKey + value: string // TypeOfKey ) { const doInsert = getSetting(guildID, key, setting) == null; if (!doInsert) { @@ -109,6 +138,4 @@ export function listPublicServers() { } // Utility type -type TypeOfKey = SqlType< - (typeof settingsDefinition)[T][any][0] ->; +type TypeOfKey = SqlType<(typeof settingsDefinition)[T]>; diff --git a/src/utils/embeds/modEmbed.ts b/src/utils/embeds/modEmbed.ts index a2eb5bb..756f34a 100644 --- a/src/utils/embeds/modEmbed.ts +++ b/src/utils/embeds/modEmbed.ts @@ -48,10 +48,11 @@ export async function errorCheck( ); if (!allErrors) return; + if (!target) return; if (target === member) return errorEmbed(interaction, `You can't ${action.toLowerCase()} yourself.`); - if (target.user.id === interaction.client.user.id) + if (target.id === interaction.client.user.id) return errorEmbed(interaction, `You can't ${action.toLowerCase()} Sokora.`); if (!target.manageable) @@ -69,7 +70,7 @@ export async function errorCheck( ); if (ownerError) { - if (member.id === guild.ownerId) + if (target.id === guild.ownerId) return errorEmbed( interaction, `You can't ${action.toLowerCase()} ${name}.`, @@ -101,6 +102,7 @@ export async function modEmbed(options: Options, reason?: string | null, date?: const dmChannel = await user.createDM().catch(() => null); if (!dmChannel) return; + if (!guild.members.cache.get(user.id)) return; if (user.bot) return; await dmChannel .send({ From abe3fe16421f1bbc11b43ca5e77ac31691e7d015 Mon Sep 17 00:00:00 2001 From: MrSerge01 <86667481+MrSerge01@users.noreply.github.com> Date: Sat, 3 Aug 2024 23:50:22 +0500 Subject: [PATCH 075/127] it works --- src/commands/settings.ts | 22 +++++++++++++++++----- src/utils/database/settings.ts | 4 +++- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/commands/settings.ts b/src/commands/settings.ts index d4e8d23..2304804 100644 --- a/src/commands/settings.ts +++ b/src/commands/settings.ts @@ -42,25 +42,37 @@ export default class Settings { .setName(key) .setDescription("This command has no description."); Object.keys(settingsDefinition[key]).forEach(sub => { - switch (settingsDefinition[key][sub][0] as string) { + switch (settingsDefinition[key][sub]["type"] as string) { case "BOOL": subcommand.addBooleanOption(option => - option.setName(sub).setDescription(settingsDefinition[key][sub][1]).setRequired(false) + option + .setName(sub) + .setDescription(settingsDefinition[key][sub]["desc"]) + .setRequired(false) ); break; case "INTEGER": subcommand.addIntegerOption(option => - option.setName(sub).setDescription(settingsDefinition[key][sub][1]).setRequired(false) + option + .setName(sub) + .setDescription(settingsDefinition[key][sub]["desc"]) + .setRequired(false) ); break; case "USER": subcommand.addUserOption(option => - option.setName(sub).setDescription(settingsDefinition[key][sub][1]).setRequired(false) + option + .setName(sub) + .setDescription(settingsDefinition[key][sub]["desc"]) + .setRequired(false) ); break; default: // Also includes "TEXT" subcommand.addStringOption(option => - option.setName(sub).setDescription(settingsDefinition[key][sub][1]).setRequired(false) + option + .setName(sub) + .setDescription(settingsDefinition[key][sub]["desc"]) + .setRequired(false) ); break; } diff --git a/src/utils/database/settings.ts b/src/utils/database/settings.ts index 348e34a..3404969 100644 --- a/src/utils/database/settings.ts +++ b/src/utils/database/settings.ts @@ -138,4 +138,6 @@ export function listPublicServers() { } // Utility type -type TypeOfKey = SqlType<(typeof settingsDefinition)[T]>; +type TypeOfKey = SqlType< + (typeof settingsDefinition)[T][any]["type"] +>; From fe699df94acebe14986569eeadab19d9a945cac2 Mon Sep 17 00:00:00 2001 From: MrSerge01 <86667481+MrSerge01@users.noreply.github.com> Date: Sun, 4 Aug 2024 23:37:26 +0500 Subject: [PATCH 076/127] hell yeah --- src/events/guildCreate.ts | 16 +++++++++++----- src/utils/database/settings.ts | 4 +--- src/utils/database/types.ts | 2 +- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/events/guildCreate.ts b/src/events/guildCreate.ts index 25cc0e2..e6a78dd 100644 --- a/src/events/guildCreate.ts +++ b/src/events/guildCreate.ts @@ -1,7 +1,8 @@ -import { EmbedBuilder, type DMChannel, type Client, type Guild } from "discord.js"; +import { EmbedBuilder, type Client, type DMChannel, type Guild } from "discord.js"; +import Commands from "../handlers/commands"; import { genColor } from "../utils/colorGen"; +import { setSetting, settingsDefinition } from "../utils/database/settings"; import { randomise } from "../utils/randomise"; -import Commands from "../handlers/commands"; export default { name: "guildCreate", @@ -12,9 +13,9 @@ export default { } async run(guild: Guild) { - const dmChannel = (await (await guild.fetchOwner()) - .createDM() - .catch(() => null)) as DMChannel | undefined; + const dmChannel = (await (await guild.fetchOwner()).createDM().catch(() => null)) as + | DMChannel + | undefined; let emojis = ["💖", "💝", "💓", "💗", "💘", "💟", "💕", "💞"]; if (Math.round(Math.random() * 100) <= 5) emojis = ["⌨️", "💻", "🖥️"]; @@ -32,6 +33,11 @@ export default { .setColor(genColor(200)); await new Commands(guild.client).registerCommandsForGuild(guild); + + for (const key in settingsDefinition) + for (const setting in settingsDefinition[key]) + setSetting(guild.id, key, setting, settingsDefinition[key][setting].val); + if (dmChannel) await dmChannel.send({ embeds: [embed] }); } } diff --git a/src/utils/database/settings.ts b/src/utils/database/settings.ts index 3404969..b4adc80 100644 --- a/src/utils/database/settings.ts +++ b/src/utils/database/settings.ts @@ -125,9 +125,7 @@ export function setSetting( value: string // TypeOfKey ) { const doInsert = getSetting(guildID, key, setting) == null; - if (!doInsert) { - deleteQuery.all(JSON.stringify(guildID), key + "." + setting); - } + if (!doInsert) deleteQuery.all(JSON.stringify(guildID), key + "." + setting); insertQuery.run(JSON.stringify(guildID), key + "." + setting, value); } diff --git a/src/utils/database/types.ts b/src/utils/database/types.ts index 8c3daa3..99d1623 100644 --- a/src/utils/database/types.ts +++ b/src/utils/database/types.ts @@ -1,4 +1,4 @@ -export type FieldData = "TEXT" | "INTEGER" | "BOOL" | "TIMESTAMP"; +export type FieldData = "TEXT" | "INTEGER" | "BOOL" | "TIMESTAMP" | "LIST"; export type TableDefinition = { name: string; From a68d955bd463d0df2df12e2a5c2c727db74d068f Mon Sep 17 00:00:00 2001 From: MrSerge01 <86667481+MrSerge01@users.noreply.github.com> Date: Tue, 6 Aug 2024 23:52:49 +0500 Subject: [PATCH 077/127] oh god --- src/commands/settings.ts | 33 ++++++++++----------------------- src/events/guildCreate.ts | 14 ++++++++------ src/utils/database/settings.ts | 28 +++++++++++++++++++++------- src/utils/database/types.ts | 1 + src/utils/embeds/modEmbed.ts | 4 ++++ src/utils/embeds/serverEmbed.ts | 5 +---- 6 files changed, 45 insertions(+), 40 deletions(-) diff --git a/src/commands/settings.ts b/src/commands/settings.ts index 2304804..19414bf 100644 --- a/src/commands/settings.ts +++ b/src/commands/settings.ts @@ -1,19 +1,19 @@ import { - InteractionType, + AutocompleteInteraction, EmbedBuilder, - SlashCommandBuilder, + InteractionType, PermissionsBitField, - type ChatInputCommandInteraction, - AutocompleteInteraction, - SlashCommandSubcommandBuilder + SlashCommandBuilder, + SlashCommandSubcommandBuilder, + type ChatInputCommandInteraction } from "discord.js"; +import { genColor } from "../utils/colorGen"; import { getSetting, setSetting, settingsDefinition, settingsKeys } from "../utils/database/settings"; -import { genColor } from "../utils/colorGen"; import { errorEmbed } from "../utils/embeds/errorEmbed"; export default class Settings { @@ -23,19 +23,6 @@ export default class Settings { .setName("settings") .setDescription("Configure Sokora to your liking.") .setDefaultMemberPermissions(PermissionsBitField.Flags.Administrator); - // .addStringOption(string => - // string - // .setName("key") - // .setDescription("The setting key to set") - // .addChoices(...settingsKeys.map(key => ({ name: key, value: key }))) - // .setRequired(true) - // ) - // .addStringOption(string => - // string - // .setName("value") - // .setDescription("The value you want to set this option to, or blank for view") - // .setAutocomplete(true) - // ); settingsKeys.forEach(key => { const subcommand = new SlashCommandSubcommandBuilder() @@ -107,7 +94,7 @@ export default class Settings { value: getSetting(interaction.guildId!, key, name)?.toString() || "Not set" }); }); - return interaction.reply({ embeds: [embed] }); + return await interaction.reply({ embeds: [embed] }); } const embed = new EmbedBuilder().setTitle(`Parameters changed`).setColor(genColor(100)); @@ -119,7 +106,7 @@ export default class Settings { }); }); - interaction.reply({ embeds: [embed] }); + await interaction.reply({ embeds: [embed] }); } async autocomplete(interaction: AutocompleteInteraction) { @@ -127,7 +114,7 @@ export default class Settings { //if (interaction.options.getSubcommand() != this.data.name) return; switch (Object.keys(settingsDefinition[interaction.options.getSubcommand()])[0]) { case "BOOL": - interaction.respond( + await interaction.respond( ["true", "false"].map(choice => ({ name: choice, value: choice @@ -135,7 +122,7 @@ export default class Settings { ); break; default: - interaction.respond([]); + await interaction.respond([]); } } } diff --git a/src/events/guildCreate.ts b/src/events/guildCreate.ts index e6a78dd..7b1b604 100644 --- a/src/events/guildCreate.ts +++ b/src/events/guildCreate.ts @@ -1,7 +1,7 @@ -import { EmbedBuilder, type Client, type DMChannel, type Guild } from "discord.js"; +import { EmbedBuilder, Guild, type Client, type DMChannel } from "discord.js"; import Commands from "../handlers/commands"; import { genColor } from "../utils/colorGen"; -import { setSetting, settingsDefinition } from "../utils/database/settings"; +import { getSetting, setSetting, settingsDefinition } from "../utils/database/settings"; import { randomise } from "../utils/randomise"; export default { @@ -25,18 +25,20 @@ export default { .setDescription( [ "Sokora is a multipurpose Discord bot that lets you manage your servers easily.", - "To manage the bot, use the **/settings** command.", + "To manage the bot, use the **/settings** command.\n", "Sokora is in an early stage of development. If you find bugs, please go to our [official server](https://discord.gg/c6C25P4BuY) and report them." - ].join("\n\n") + ].join("\n") ) .setFooter({ text: `Made with ${randomise(emojis)} by the Sokora team` }) .setColor(genColor(200)); await new Commands(guild.client).registerCommandsForGuild(guild); - for (const key in settingsDefinition) - for (const setting in settingsDefinition[key]) + for (const setting in settingsDefinition[key]) { + if (settingsDefinition[key][setting].type !== "LIST") continue; + if (!getSetting(guild.id, key, setting)) continue; setSetting(guild.id, key, setting, settingsDefinition[key][setting].val); + } if (dmChannel) await dmChannel.send({ embeds: [embed] }); } diff --git a/src/utils/database/settings.ts b/src/utils/database/settings.ts index b4adc80..fa1a11d 100644 --- a/src/utils/database/settings.ts +++ b/src/utils/database/settings.ts @@ -26,8 +26,13 @@ export const settingsDefinition: Record< }, set_level: { type: "TEXT", desc: "Set the level of a user." }, add_multiplier: { - type: "TEXT", - desc: "Add an XP multiplier to the levelling system. Syntax: multiplier, role/channel (choose), id." + type: "LIST", + desc: "Add an XP multiplier to the levelling system.", + val: { + multiplier: { type: "INTEGER", desc: "Set the XP multiplier for the role/channel." }, + role_channel: { type: "TEXT", desc: "Role or channel. (choose)" }, + id: { type: "TEXT", desc: "ID of the role/channel." } + } }, set_xp_gain: { type: "INTEGER", @@ -49,6 +54,11 @@ export const settingsDefinition: Record< type: "BOOL", desc: "Whether or not edited/deleted messages should be logged.", val: true + }, + dm_user: { + type: "BOOL", + desc: "Whether or not should the bot send a DM after a moderation action.", + val: true } }, news: { @@ -64,10 +74,6 @@ export const settingsDefinition: Record< } }, serverboard: { - invite_link: { - type: "TEXT", - desc: "The invite link which is shown on the serverboard." - }, shown: { type: "BOOL", desc: "Whether or not the server should be shown on the serverboard.", @@ -103,6 +109,7 @@ export function getSetting( let res = getQuery.all(JSON.stringify(guildID), key + "." + setting) as TypeOfDefinition< typeof tableDefinition >[]; + console.log(res); if (res.length == 0) return null; switch (settingsDefinition[key][setting].type) { case "TEXT": @@ -113,6 +120,8 @@ export function getSetting( ) as TypeOfKey; case "INTEGER": return parseInt(res[0].value) as TypeOfKey; + case "LIST": + return res[0].value as TypeOfKey; default: return "WIP" as TypeOfKey; } @@ -126,7 +135,12 @@ export function setSetting( ) { const doInsert = getSetting(guildID, key, setting) == null; if (!doInsert) deleteQuery.all(JSON.stringify(guildID), key + "." + setting); - insertQuery.run(JSON.stringify(guildID), key + "." + setting, value); + if (settingsDefinition[key][setting].type == "LIST") { + const values = Object.values(settingsDefinition[key][setting].val) as string[]; + value = values.toString(); + } + + insertQuery.run(JSON.stringify(guildID), `${key}.${setting}`, value); } export function listPublicServers() { diff --git a/src/utils/database/types.ts b/src/utils/database/types.ts index 99d1623..fb02252 100644 --- a/src/utils/database/types.ts +++ b/src/utils/database/types.ts @@ -10,6 +10,7 @@ export type SqlType = { INTEGER: number; TEXT: string; TIMESTAMP: Date; + LIST: any[]; }[T]; export type TypeOfDefinition = { diff --git a/src/utils/embeds/modEmbed.ts b/src/utils/embeds/modEmbed.ts index 756f34a..81d4c1b 100644 --- a/src/utils/embeds/modEmbed.ts +++ b/src/utils/embeds/modEmbed.ts @@ -3,6 +3,7 @@ import ms from "ms"; import { genColor } from "../colorGen"; import { logChannel } from "../logChannel"; import { errorEmbed } from "./errorEmbed"; +import { getSetting } from "../database/settings"; type Options = { interaction: ChatInputCommandInteraction; @@ -100,6 +101,9 @@ export async function modEmbed(options: Options, reason?: string | null, date?: await logChannel(guild, embed); await interaction.reply({ embeds: [embed] }); + const dmOrNot = getSetting(guild.id, "moderation", "enabled"); + if (!dmOrNot) return; + const dmChannel = await user.createDM().catch(() => null); if (!dmChannel) return; if (!guild.members.cache.get(user.id)) return; diff --git a/src/utils/embeds/serverEmbed.ts b/src/utils/embeds/serverEmbed.ts index c57dacb..224aade 100644 --- a/src/utils/embeds/serverEmbed.ts +++ b/src/utils/embeds/serverEmbed.ts @@ -19,7 +19,6 @@ type Options = { export async function serverEmbed(options: Options) { const { page, pages, guild } = options; const { premiumTier: boostTier, premiumSubscriptionCount: boostCount } = guild; - const invite = getSetting(guild.id, "serverboard", "invite_link"); const members = guild.members.cache; const boosters = members.filter(member => member.premiumSince); const onlineMembers = members.filter(member => @@ -45,7 +44,6 @@ export async function serverEmbed(options: Options) { `Owned by **${(await guild.fetchOwner()).user.displayName}**`, `Created on ****` ]; - if (options.showInvite && invite !== null) generalValues.push(`**Invite link**: ${invite}`); const embed = new EmbedBuilder() .setAuthor({ @@ -56,9 +54,8 @@ export async function serverEmbed(options: Options) { .setFields({ name: "📃 • General", value: generalValues.join("\n") }) .setFooter({ text: `Server ID: ${guild.id}${pages ? ` • Page ${page}/${pages}` : ""}` }) .setThumbnail(guild.iconURL()) - .setColor(genColor(200)); + .setColor((await imageColor(guild)) ?? genColor(200)); - imageColor(embed, guild); if (options.roles) embed.addFields({ name: `🎭 • ${roles.size - 1} ${roles.size === 1 ? "role" : "roles"}`, From cd3fa5206f5495a6543e7e5be3a70531b6ccefc2 Mon Sep 17 00:00:00 2001 From: MrSerge01 <86667481+MrSerge01@users.noreply.github.com> Date: Sun, 18 Aug 2024 13:51:33 +0500 Subject: [PATCH 078/127] small changes --- bun.lockb | Bin 57167 -> 57167 bytes package.json | 6 +++--- src/bot.ts | 4 ++-- src/commands/news/edit.ts | 18 +++++++++--------- src/commands/news/remove.ts | 8 ++++---- src/commands/news/send.ts | 10 +++++----- src/commands/news/view.ts | 10 +++++----- src/commands/serverboard.ts | 4 ++-- src/commands/settings.ts | 24 +++++++++++++++--------- src/events/guildCreate.ts | 2 +- src/events/guildMemberAdd.ts | 9 +++++---- src/events/guildMemberRemove.ts | 9 +++++---- src/events/interactionCreate.ts | 12 +++++++----- src/handlers/commands.ts | 8 ++++---- src/handlers/events.ts | 10 +++++----- src/utils/database/settings.ts | 15 +++++++-------- 16 files changed, 79 insertions(+), 70 deletions(-) diff --git a/bun.lockb b/bun.lockb index 6cb272719a61a930808f6a53bac83074852599fc..5c6eba8433de340b178dbdd2730c28b82e52a817 100644 GIT binary patch delta 2849 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 PU#SeXypjQBuh0JngP=o3 delta 2804 zcmaKtc|25Y8^_NKX&C#GnGqqHo-hU@OP0yro>-W3w&-c2{x$k+z6?()KN++n^ z=4x@A?iaTRRR&)0Z!gb&F{JH{y+$LEt{|_S@#tT;>3ClhP%#@b#ZZ7ECM^gVOCe|# z!w6C+v^bDgAOQ+`ry$jdmIfFM5un6>PNcx$Vv6xA)w!HPN&IrbA>Xx>0<995rG+H< z7C@F(581SIde=_<6&p2YM+-#ce4-zf)Ef2qCpTO1mA*7c&0ab#`TNyn$;mH<7cie z{Fn88mOV+_k0qKo{&0FmE$#|GaZG%-6BdV=OiuD_pic{b>Yo`NiLJyDYa>#R@oIsW z2_{Y?Fuzen+si?|W}Elcs=$c8``ok}L9VAa$xmM<8dEb;+K#1aMrUq6>{mj;Z{Qp% z?Z0I$ebwua3?J84m#XbWe7Xgx1$bN~K!ZnN9%~eA$>=TnJi-}>HJv7L?wTe}+|$81 zK720in^+SdP9=HZW>MG!?DlOw50y2p5QHs5p4ci&h81LG97u}w0urX7WcK==%rS$u zOYinumkq{GVF$7&YVF1bn$xQt)x6x$Iq1&fuhQL6b8o=n=)Q~JJ+SMXx99l;>VFu67Mqz5C}rQRjlOdpx!ez3!W z0<}%Po}a8ZzxBkO9#>_2cjoDW$%Fh7g&}FUEYX!E7URBl>fFxqxG!OHGIT78ZtAA* zjoPk&O&%#-^9A3qGe=QxI(RA(J5jq;H^9=gD!8KHRR6^oyI~W5*|5F#?R0Q-3nuQR zszy;ypr8F)$!erQeraaMow>^l8{5G9Nl9vs<;T-xHM9JYM@CA8(Z~fg*RYrlO~j1R zg3?6s^Lereu|S4T2cZ_}&@1V1XJY5N6!&(w@79&wx!=--rm|k2w^?sEsA?$E6IUU1 z@5Ie4a*988WM9z(>GQ5K7IUXnB*CEbfI(lJkkgxxz? zYh_DK?6?ChCTu83rSJWn*44AtPOnUM{HS}SupgVR_942y?YER%yIqC(^T85D&j(+J zW^_A@fz#%gxDF1I6&~><>27|YMpS2LT1>myZ|@k!F-oGmp-5tY`?SAl+Fh?5S!Q{r zb3*P<(y99MPrc1PgqjHIXUD53l=wo4f3@xfN3q*hwIs|PYO{apADs1Q^~nb#qq*Cy*?2ocpkslFV=&{n zb(f+&RI|jr3SoD8%@lW9GX}JN-oMT`7{Qaro zWeu+uloeL{C~951NNblq*6-Yv+tKzDAqF0%fujqGfCtkt+y4EAdU!IvVp~}TN|nGl z|5iM=yC{B5pKWjdc2DV=KXvbBe0s&bZg6M|PTD35K;&y$C`cuIqB`vS=i|d`mv_Y? zox)1GC*D&w?%{7N1ZoYg!N34ql~0|FJDb%l)2+s27m64&QXlO_P7=5ul;f} z_Y&{ieCfTAno8H1==alWxl-V~B_^)Yfv~w^{8WKCE8?)WTqAdG@H!}^Gpi?RP3WYM zs3QT5f7$f2a8(Dib52C(Rs?u{lgrT4@*43+zGrt#NKyE7ONXZ7N;>&&)GFVb)0Hxb z^q2Iui1@EtD7*uHJMz4TOL82QCbBS&adWJ*nxZgV*1D)T zNjjc-Y!zj3!6X0Wdb;eX%!keltzzZ+vxm!cmGq4H$DJ4i(MmLF+FEB1_@fZsh6F=` z0kFzv8WJHW5{8Ehpj9{dj+VyLjeBd=%a0CydY|5DEIgrvQu42lOH=-=zq5N(FW`+%BuzEc>=X;JR9{r0;@t zby)YMI%7Ujn;ZrDYyK+V3vfy>OC@Yt1Dj$#`A>=`RR3Zj7Ge!yy$9CiVV(X6iFZjr zo))k|&_osG)wQhNi>57~%*vl5=F(AEu6=1W~{-%p@e(C<4 z4S&-(9Hr$oF+ieGov%4HnlFzy(B6n&_M-u&Nx&rr0-Eq}Jr|nDaEEqvt*akzN>K+} O;ldnvYryqam;MK8S3<)8 diff --git a/package.json b/package.json index 36ccf6b..e22e990 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sokora", - "description": "Welcome to Sokora, the multipurpose, multiplatform bot.", + "description": "Welcome to Sokora, a multipurpose Discord bot that lets you manage your servers easily.", "contributors": [ "The Sokora team", "The GitHub contributors" @@ -15,10 +15,10 @@ "discord.js": "^14.15.3", "ms": "^2.1.3", "node-vibrant": "^3.2.1-alpha.1", - "sharp": "^0.33.4" + "sharp": "^0.33.5" }, "devDependencies": { - "bun-types": "^1.1.21", + "bun-types": "^1.1.24", "typescript": "^5.5.4" } } diff --git a/src/bot.ts b/src/bot.ts index a79ac57..cd97220 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -1,6 +1,6 @@ import { ActivityType, Client } from "discord.js"; -import Commands from "./handlers/commands"; -import Events from "./handlers/events"; +import { Commands } from "./handlers/commands"; +import { Events } from "./handlers/events"; const client = new Client({ presence: { diff --git a/src/commands/news/edit.ts b/src/commands/news/edit.ts index c5f2030..cb9bbd2 100644 --- a/src/commands/news/edit.ts +++ b/src/commands/news/edit.ts @@ -1,19 +1,19 @@ import { - SlashCommandSubcommandBuilder, + ActionRowBuilder, EmbedBuilder, - PermissionsBitField, ModalBuilder, + PermissionsBitField, + SlashCommandSubcommandBuilder, TextInputBuilder, - ActionRowBuilder, TextInputStyle, type ChatInputCommandInteraction, - type TextChannel, - type Role + type Role, + type TextChannel } from "discord.js"; import { genColor } from "../../utils/colorGen"; -import { errorEmbed } from "../../utils/embeds/errorEmbed"; import { get, updateNews } from "../../utils/database/news"; import { getSetting } from "../../utils/database/settings"; +import { errorEmbed } from "../../utils/embeds/errorEmbed"; import { sendChannelNews } from "../../utils/sendChannelNews"; export default class Edit { @@ -81,7 +81,7 @@ export default class Edit { interaction.client.once("interactionCreate", async i => { if (!i.isModalSubmit()) return; - const role = getSetting(guild.id, "news", "role_id"); + const role = getSetting(guild.id, "news", "role_id") as string; let roleToSend: Role | undefined; if (role) roleToSend = guild.roles.cache.get(role); const title = i.fields.getTextInputValue("title"); @@ -99,7 +99,7 @@ export default class Edit { ( guild.channels.cache.get( - getSetting(guild.id, "news", "channel_id")! ?? interaction.channel?.id + (getSetting(guild.id, "news", "channel_id") as string) ?? interaction.channel?.id ) as TextChannel )?.messages.edit(news.messageID, { embeds: [embed], @@ -108,7 +108,7 @@ export default class Edit { updateNews(id, title, body); await interaction.reply({ - embeds: [new EmbedBuilder().setTitle("✅ • News edited!").setColor(genColor(100))] + embeds: [new EmbedBuilder().setTitle("News edited!").setColor(genColor(100))] }); }); } diff --git a/src/commands/news/remove.ts b/src/commands/news/remove.ts index cfb34be..71102b1 100644 --- a/src/commands/news/remove.ts +++ b/src/commands/news/remove.ts @@ -1,14 +1,14 @@ import { - SlashCommandSubcommandBuilder, EmbedBuilder, PermissionsBitField, + SlashCommandSubcommandBuilder, TextChannel, type ChatInputCommandInteraction } from "discord.js"; import { genColor } from "../../utils/colorGen"; -import { errorEmbed } from "../../utils/embeds/errorEmbed"; import { deleteNews, get } from "../../utils/database/news"; import { getSetting } from "../../utils/database/settings"; +import { errorEmbed } from "../../utils/embeds/errorEmbed"; export default class Remove { data: SlashCommandSubcommandBuilder; @@ -41,13 +41,13 @@ export default class Remove { const messageID = news.messageID; const newsChannel = (await guild.channels - .fetch(getSetting(guild.id, "news", "channel_id")! ?? interaction.channel?.id) + .fetch((getSetting(guild.id, "news", "channel_id") as string) ?? interaction.channel?.id) .catch(() => null)) as TextChannel; if (newsChannel) await newsChannel.messages.delete(messageID); deleteNews(id); await interaction.reply({ - embeds: [new EmbedBuilder().setTitle("✅ • News deleted!").setColor(genColor(100))] + embeds: [new EmbedBuilder().setTitle("News deleted!").setColor(genColor(100))] }); } } diff --git a/src/commands/news/send.ts b/src/commands/news/send.ts index dd4c1e6..32842a6 100644 --- a/src/commands/news/send.ts +++ b/src/commands/news/send.ts @@ -1,17 +1,17 @@ import { - SlashCommandSubcommandBuilder, + ActionRowBuilder, EmbedBuilder, - PermissionsBitField, ModalBuilder, + PermissionsBitField, + SlashCommandSubcommandBuilder, TextInputBuilder, - ActionRowBuilder, TextInputStyle, type ChatInputCommandInteraction } from "discord.js"; import { genColor } from "../../utils/colorGen"; +import { sendNews } from "../../utils/database/news"; import { errorEmbed } from "../../utils/embeds/errorEmbed"; import { sendChannelNews } from "../../utils/sendChannelNews"; -import { sendNews } from "../../utils/database/news"; export default class Send { data: SlashCommandSubcommandBuilder; @@ -68,7 +68,7 @@ export default class Send { await sendChannelNews(guild, id, interaction).catch(err => console.error(err)); await i.reply({ - embeds: [new EmbedBuilder().setTitle("✅ • News sent!").setColor(genColor(100))] + embeds: [new EmbedBuilder().setTitle("News sent!").setColor(genColor(100))] }); }); } diff --git a/src/commands/news/view.ts b/src/commands/news/view.ts index bf4967c..0ae5770 100644 --- a/src/commands/news/view.ts +++ b/src/commands/news/view.ts @@ -1,14 +1,14 @@ import { - SlashCommandSubcommandBuilder, - EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, + EmbedBuilder, + SlashCommandSubcommandBuilder, type ChatInputCommandInteraction } from "discord.js"; import { genColor } from "../../utils/colorGen"; -import { errorEmbed } from "../../utils/embeds/errorEmbed"; import { listAllNews } from "../../utils/database/news"; +import { errorEmbed } from "../../utils/embeds/errorEmbed"; export default class View { data: SlashCommandSubcommandBuilder; @@ -49,11 +49,11 @@ export default class View { const row = new ActionRowBuilder().addComponents( new ButtonBuilder() .setCustomId("left") - .setEmoji("1137330341472915526") + .setEmoji("1271045078042935398") .setStyle(ButtonStyle.Primary), new ButtonBuilder() .setCustomId("right") - .setEmoji("1137330125004869702") + .setEmoji("1271045041313415370") .setStyle(ButtonStyle.Primary) ); diff --git a/src/commands/serverboard.ts b/src/commands/serverboard.ts index 359d379..6c72731 100644 --- a/src/commands/serverboard.ts +++ b/src/commands/serverboard.ts @@ -45,11 +45,11 @@ export default class Serverboard { const row = new ActionRowBuilder().addComponents( new ButtonBuilder() .setCustomId("left") - .setEmoji("1137330341472915526") + .setEmoji("1271045078042935398") .setStyle(ButtonStyle.Primary), new ButtonBuilder() .setCustomId("right") - .setEmoji("1137330125004869702") + .setEmoji("1271045041313415370") .setStyle(ButtonStyle.Primary) ); diff --git a/src/commands/settings.ts b/src/commands/settings.ts index 19414bf..adf1327 100644 --- a/src/commands/settings.ts +++ b/src/commands/settings.ts @@ -5,6 +5,7 @@ import { PermissionsBitField, SlashCommandBuilder, SlashCommandSubcommandBuilder, + SlashCommandSubcommandGroupBuilder, type ChatInputCommandInteraction } from "discord.js"; import { genColor } from "../utils/colorGen"; @@ -27,7 +28,8 @@ export default class Settings { settingsKeys.forEach(key => { const subcommand = new SlashCommandSubcommandBuilder() .setName(key) - .setDescription("This command has no description."); + .setDescription("This subcommand has no description."); + Object.keys(settingsDefinition[key]).forEach(sub => { switch (settingsDefinition[key][sub]["type"] as string) { case "BOOL": @@ -54,6 +56,10 @@ export default class Settings { .setRequired(false) ); break; + // case "LIST": + // const subcommandGroup = new SlashCommandSubcommandGroupBuilder() + // .setName(key) + // .setDescription("This subcommand group has no description."); default: // Also includes "TEXT" subcommand.addStringOption(option => option @@ -64,7 +70,6 @@ export default class Settings { break; } }); - //console.log(subcommand); this.data.addSubcommand(subcommand); }); } @@ -85,15 +90,16 @@ export default class Settings { const values = interaction.options.data[0].options!; console.log(values); if (values.length == 0) { - const embed = new EmbedBuilder(); - embed.setTitle(`Settings for ${key}`); - embed.setColor(genColor(100)); + const embed = new EmbedBuilder().setTitle(`Settings for ${key}`).setColor(genColor(100)); + const description: string[] = []; + Object.keys(settingsDefinition[key]).forEach(name => { - embed.addFields({ - name: name, - value: getSetting(interaction.guildId!, key, name)?.toString() || "Not set" - }); + description.push( + `${name}: ${getSetting(interaction.guildId!, key, name)?.toString() || "Not set"}` + ); + embed.setDescription(description.join("\n")); }); + return await interaction.reply({ embeds: [embed] }); } diff --git a/src/events/guildCreate.ts b/src/events/guildCreate.ts index 7b1b604..f1e5db5 100644 --- a/src/events/guildCreate.ts +++ b/src/events/guildCreate.ts @@ -1,5 +1,5 @@ import { EmbedBuilder, Guild, type Client, type DMChannel } from "discord.js"; -import Commands from "../handlers/commands"; +import { Commands } from "../handlers/commands"; import { genColor } from "../utils/colorGen"; import { getSetting, setSetting, settingsDefinition } from "../utils/database/settings"; import { randomise } from "../utils/randomise"; diff --git a/src/events/guildMemberAdd.ts b/src/events/guildMemberAdd.ts index 627ff9a..fce88be 100644 --- a/src/events/guildMemberAdd.ts +++ b/src/events/guildMemberAdd.ts @@ -13,10 +13,10 @@ export default { async run(member: GuildMember) { const guildID = member.guild.id; - const id = getSetting(guildID, "welcome", "channel"); + const id = getSetting(guildID, "welcome", "channel") as string; if (!id) return; - let text = getSetting(guildID, "welcome", "text"); + let text = getSetting(guildID, "welcome", "text") as string; const user = member.user; const guild = member.guild; const channel = (await member.guild.channels.cache @@ -37,9 +37,10 @@ export default { ) .setFooter({ text: `User ID: ${member.id}` }) .setThumbnail(avatarURL) - .setColor(genColor(200)); + .setColor( + member.user.hexAccentColor ?? (await imageColor(undefined, member)) ?? genColor(200) + ); - imageColor(embed, undefined, member); await channel.send({ embeds: [embed] }); } } diff --git a/src/events/guildMemberRemove.ts b/src/events/guildMemberRemove.ts index 805f429..1fca228 100644 --- a/src/events/guildMemberRemove.ts +++ b/src/events/guildMemberRemove.ts @@ -13,10 +13,10 @@ export default { async run(member: GuildMember) { const guildID = member.guild.id; - const id = getSetting(guildID, "welcome", "channel"); + const id = getSetting(guildID, "welcome", "channel") as string; if (!id) return; - let text = getSetting(guildID, "welcome", "goodbye_text"); + let text = getSetting(guildID, "welcome", "goodbye_text") as string; const user = member.user; const guild = member.guild; const channel = (await member.guild.channels.cache @@ -34,9 +34,10 @@ export default { .setDescription(text ?? `**@${user.displayName}** has left the server 😥`) .setFooter({ text: `User ID: ${member.id}` }) .setThumbnail(avatarURL) - .setColor(genColor(200)); + .setColor( + member.user.hexAccentColor ?? (await imageColor(undefined, member)) ?? genColor(200) + ); - imageColor(embed, undefined, member); await channel.send({ embeds: [embed] }); } } diff --git a/src/events/interactionCreate.ts b/src/events/interactionCreate.ts index c677c1e..05dc5fa 100644 --- a/src/events/interactionCreate.ts +++ b/src/events/interactionCreate.ts @@ -1,7 +1,7 @@ -import type { CommandInteraction, Client, AutocompleteInteraction } from "discord.js"; -import { pathToFileURL } from "url"; -import { join } from "path"; import { file } from "bun"; +import type { AutocompleteInteraction, Client, CommandInteraction } from "discord.js"; +import { join } from "path"; +import { pathToFileURL } from "url"; async function getCommand( interaction: CommandInteraction | AutocompleteInteraction, @@ -10,7 +10,7 @@ async function getCommand( const commandName = interaction.commandName; const subcommandName = options.getSubcommand(false); const commandGroupName = options.getSubcommandGroup(false); - var commandImportPath = join( + let commandImportPath = join( join(process.cwd(), "src", "commands"), `${ subcommandName @@ -20,7 +20,9 @@ async function getCommand( : commandName }.ts` ); - if (!await file(commandImportPath).exists()) commandImportPath = join(join(process.cwd(), "src", "commands", `${commandName}.ts`)); + + if (!(await file(commandImportPath).exists())) + commandImportPath = join(join(process.cwd(), "src", "commands", `${commandName}.ts`)); return new (await import(pathToFileURL(commandImportPath).toString())).default(); } diff --git a/src/handlers/commands.ts b/src/handlers/commands.ts index c3cbcfe..999f1ff 100644 --- a/src/handlers/commands.ts +++ b/src/handlers/commands.ts @@ -1,15 +1,15 @@ import { + Guild, SlashCommandBuilder, SlashCommandSubcommandGroupBuilder, - Guild, type Client } from "discord.js"; -import { pathToFileURL } from "url"; -import { join } from "path"; import { readdirSync } from "fs"; +import { join } from "path"; +import { pathToFileURL } from "url"; import { getDisabledCommands } from "../utils/database/disabledCommands"; -export default class Commands { +export class Commands { client: Client; commands: any[] = []; constructor(client: Client) { diff --git a/src/handlers/events.ts b/src/handlers/events.ts index e07fd37..2b6ba8d 100644 --- a/src/handlers/events.ts +++ b/src/handlers/events.ts @@ -1,15 +1,15 @@ import type { Client } from "discord.js"; -import { pathToFileURL } from "url"; -import { join } from "path"; import { readdirSync } from "fs"; +import { join } from "path"; +import { pathToFileURL } from "url"; -export default class Events { +export class Events { client: Client; events: any[] = []; constructor(client: Client) { this.client = client; - (async () => { + async () => { const eventsPath = join(process.cwd(), "src", "events"); for (const eventFile of readdirSync(eventsPath)) { @@ -20,6 +20,6 @@ export default class Events { this.events.push({ name: event.default.name, event: clientEvent }); } - })(); + }; } } diff --git a/src/utils/database/settings.ts b/src/utils/database/settings.ts index fa1a11d..729d213 100644 --- a/src/utils/database/settings.ts +++ b/src/utils/database/settings.ts @@ -1,4 +1,5 @@ import { getDatabase } from "."; +import { kominator } from "../kominator"; import { FieldData, SqlType, TableDefinition, TypeOfDefinition } from "./types"; const tableDefinition = { @@ -81,10 +82,13 @@ export const settingsDefinition: Record< } }, welcome: { - text: { type: "TEXT", desc: "The welcome message that is sent when a user joins." }, + text: { + type: "TEXT", + desc: "The welcome message that is sent when a user joins. You can type (user) to display the username, (count) to display the server count and (servername) to display the server name." + }, goodbye_text: { type: "TEXT", - desc: "The goodbye message that is sent when a user leaves." + desc: "The goodbye message that is sent when a user leaves. You can type (user) to display the username, (count) to display the server count and (servername) to display the server name." }, channel: { type: "TEXT", desc: "ID of the channel where welcome messages are sent." } } @@ -121,7 +125,7 @@ export function getSetting( case "INTEGER": return parseInt(res[0].value) as TypeOfKey; case "LIST": - return res[0].value as TypeOfKey; + return kominator(res[0].value) as TypeOfKey; default: return "WIP" as TypeOfKey; } @@ -135,11 +139,6 @@ export function setSetting( ) { const doInsert = getSetting(guildID, key, setting) == null; if (!doInsert) deleteQuery.all(JSON.stringify(guildID), key + "." + setting); - if (settingsDefinition[key][setting].type == "LIST") { - const values = Object.values(settingsDefinition[key][setting].val) as string[]; - value = values.toString(); - } - insertQuery.run(JSON.stringify(guildID), `${key}.${setting}`, value); } From 8e3f52c4cc832ff9437ab8604ff94ea7ff052005 Mon Sep 17 00:00:00 2001 From: Serge Date: Mon, 19 Aug 2024 20:55:43 +0500 Subject: [PATCH 079/127] interaction errors FIXED --- src/bot.ts | 2 +- src/commands/moderation/delwarn.ts | 2 +- src/commands/moderation/lock.ts | 25 ++++++++++++------------- src/commands/moderation/mute.ts | 2 +- src/commands/moderation/purge.ts | 16 +++++++++------- src/commands/moderation/slowdown.ts | 12 +++++++----- src/commands/moderation/unban.ts | 6 +++++- src/commands/moderation/unlock.ts | 23 +++++++++++------------ src/commands/moderation/unmute.ts | 6 +++++- src/commands/moderation/warn.ts | 2 +- src/commands/moderation/warns.ts | 4 ++-- src/commands/news/edit.ts | 6 +++--- src/commands/news/remove.ts | 6 +++--- src/commands/news/send.ts | 4 ++-- src/commands/news/view.ts | 22 +++++++--------------- src/commands/serverboard.ts | 13 ++++++++----- src/commands/settings.ts | 13 ++++++------- src/commands/user.ts | 25 ++++++++++++------------- src/handlers/commands.ts | 4 +--- src/handlers/events.ts | 21 ++++++++++++--------- src/utils/database/settings.ts | 8 ++++---- src/utils/embeds/errorEmbed.ts | 1 + src/utils/embeds/modEmbed.ts | 18 +++++++++--------- src/utils/embeds/serverEmbed.ts | 1 - src/utils/sendChannelNews.ts | 4 ++-- 25 files changed, 125 insertions(+), 121 deletions(-) diff --git a/src/bot.ts b/src/bot.ts index cd97220..487f70b 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -18,7 +18,7 @@ const client = new Client({ }); client.on("ready", async () => { - new Events(client); + await new Events(client).loadEvents(); await new Commands(client).registerCommands(); console.log("ちーっす!"); }); diff --git a/src/commands/moderation/delwarn.ts b/src/commands/moderation/delwarn.ts index 8ae471f..7e99ea5 100644 --- a/src/commands/moderation/delwarn.ts +++ b/src/commands/moderation/delwarn.ts @@ -44,7 +44,7 @@ export default class Delwarn { ); if (newWarns.length === warns.length) - return errorEmbed(interaction, `There is no warn with the id of ${id}.`); + return await errorEmbed(interaction, `There is no warn with the id of ${id}.`); const embed = new EmbedBuilder() .setAuthor({ name: `• ${name}`, iconURL: user.displayAvatarURL() }) diff --git a/src/commands/moderation/lock.ts b/src/commands/moderation/lock.ts index cf5889e..25e6fdf 100644 --- a/src/commands/moderation/lock.ts +++ b/src/commands/moderation/lock.ts @@ -30,12 +30,14 @@ export default class Lock { async run(interaction: ChatInputCommandInteraction) { const guild = interaction.guild!; - const member = guild.members.cache.get(interaction.member?.user.id!)!; - - if (!member.permissions.has(PermissionsBitField.Flags.ManageRoles)) - return errorEmbed( + if ( + !guild.members.cache + .get(interaction.user.id) + ?.permissions.has(PermissionsBitField.Flags.ManageRoles) + ) + return await errorEmbed( interaction, - "You can't execute this command", + "You can't execute this command.", "You need the **Manage Roles** permission." ); @@ -43,14 +45,14 @@ export default class Lock { const channel = guild.channels.cache.get(interaction.channel?.id ?? channelOption.id)!; if (!channel.permissionsFor(guild.id)?.has("SendMessages")) - return errorEmbed( + return await errorEmbed( interaction, - "You can't execute this command", + "You can't execute this command.", "The channel is already locked." ); const embed = new EmbedBuilder() - .setTitle(`Locked a channel`) + .setTitle(`Locked a channel.`) .setDescription( [ `**Moderator**: ${interaction.user.username}`, @@ -66,13 +68,10 @@ export default class Lock { ChannelType.GuildVoice ) channel.permissionOverwrites - .create(interaction.guild!.id, { + .create(guild.id, { SendMessages: false, SendMessagesInThreads: false, - CreatePublicThreads: false, - CreatePrivateThreads: false, - UseApplicationCommands: false, - UseEmbeddedActivities: false + CreatePublicThreads: false }) .catch(error => console.error(error)); diff --git a/src/commands/moderation/mute.ts b/src/commands/moderation/mute.ts index 2136011..e54c920 100644 --- a/src/commands/moderation/mute.ts +++ b/src/commands/moderation/mute.ts @@ -40,7 +40,7 @@ export default class Mute { ); if (!ms(duration) || ms(duration) > ms("28d")) - return errorEmbed( + return await errorEmbed( interaction, `You can't mute ${user.displayName}.`, "The duration is invalid or is above the 28 day limit." diff --git a/src/commands/moderation/purge.ts b/src/commands/moderation/purge.ts index d856c2b..9ccb5be 100644 --- a/src/commands/moderation/purge.ts +++ b/src/commands/moderation/purge.ts @@ -37,19 +37,21 @@ export default class Purge { async run(interaction: ChatInputCommandInteraction) { const guild = interaction.guild!; const amount = interaction.options.getNumber("amount")!; - const member = guild.members.cache.get(interaction.member?.user.id!)!; - - if (!member.permissions.has(PermissionsBitField.Flags.ManageMessages)) - return errorEmbed( + if ( + !guild.members.cache + .get(interaction.user.id) + ?.permissions.has(PermissionsBitField.Flags.ManageMessages) + ) + return await errorEmbed( interaction, - "You can't execute this command", + "You can't execute this command.", "You need the **Manage Messages** permission." ); if (amount > 100) - return errorEmbed(interaction, "You can only purge up to 100 messages at a time."); + return await errorEmbed(interaction, "You can only purge up to 100 messages at a time."); - if (amount < 1) return errorEmbed(interaction, "You must purge at least 1 message."); + if (amount < 1) return await errorEmbed(interaction, "You must purge at least 1 message."); const channelOption = interaction.options.getChannel("channel")!; const channel = guild.channels.cache.get(interaction.channel?.id ?? channelOption.id)!; diff --git a/src/commands/moderation/slowdown.ts b/src/commands/moderation/slowdown.ts index 843f2a1..1a7f198 100644 --- a/src/commands/moderation/slowdown.ts +++ b/src/commands/moderation/slowdown.ts @@ -43,12 +43,14 @@ export default class Slowdown { async run(interaction: ChatInputCommandInteraction) { const guild = interaction.guild!; const time = interaction.options.getString("time")!; - const member = guild.members.cache.get(interaction.member?.user.id!)!; - - if (!member.permissions.has(PermissionsBitField.Flags.ManageChannels)) - return errorEmbed( + if ( + !guild.members.cache + .get(interaction.user.id) + ?.permissions.has(PermissionsBitField.Flags.ManageChannels) + ) + return await errorEmbed( interaction, - "You can't execute this command", + "You can't execute this command.", "You need the **Manage Channels** permission." ); diff --git a/src/commands/moderation/unban.ts b/src/commands/moderation/unban.ts index 4a88439..f278e3b 100644 --- a/src/commands/moderation/unban.ts +++ b/src/commands/moderation/unban.ts @@ -39,7 +39,11 @@ export default class Unban { ); if (target == undefined) - return errorEmbed(interaction, "You can't unban this user.", "The user was never banned."); + return await errorEmbed( + interaction, + "You can't unban this user.", + "The user was never banned." + ); await guild.members.unban(id, reason ?? undefined).catch(error => console.error(error)); await modEmbed({ interaction, user: target, action: "Unbanned" }, reason); diff --git a/src/commands/moderation/unlock.ts b/src/commands/moderation/unlock.ts index 1d2968f..c3eba7f 100644 --- a/src/commands/moderation/unlock.ts +++ b/src/commands/moderation/unlock.ts @@ -30,12 +30,14 @@ export default class Unlock { async run(interaction: ChatInputCommandInteraction) { const guild = interaction.guild!; - const member = guild.members.cache.get(interaction.member?.user.id!)!; - - if (!member.permissions.has(PermissionsBitField.Flags.ManageRoles)) - return errorEmbed( + if ( + !guild.members.cache + .get(interaction.user.id) + ?.permissions.has(PermissionsBitField.Flags.ManageRoles) + ) + return await errorEmbed( interaction, - "You can't execute this command", + "You can't execute this command.", "You need the **Manage Roles** permission." ); @@ -43,14 +45,14 @@ export default class Unlock { const channel = guild.channels.cache.get(interaction.channel?.id ?? channelOption.id)!; if (channel.permissionsFor(guild.id)?.has("SendMessages")) - return errorEmbed( + return await errorEmbed( interaction, "You can't execute this command.", "The channel is not locked." ); const embed = new EmbedBuilder() - .setTitle(`Unlocked a channel`) + .setTitle(`Unlocked a channel.`) .setDescription( [ `**Moderator**: ${interaction.user.username}`, @@ -66,13 +68,10 @@ export default class Unlock { ChannelType.GuildVoice ) channel.permissionOverwrites - .create(interaction.guild!.id, { + .create(guild.id, { SendMessages: null, SendMessagesInThreads: null, - CreatePublicThreads: null, - CreatePrivateThreads: null, - UseApplicationCommands: null, - UseEmbeddedActivities: null + CreatePublicThreads: null }) .catch(error => console.error(error)); diff --git a/src/commands/moderation/unmute.ts b/src/commands/moderation/unmute.ts index d1eeb3c..e8cd588 100644 --- a/src/commands/moderation/unmute.ts +++ b/src/commands/moderation/unmute.ts @@ -29,7 +29,11 @@ export default class Unmute { ); if (target.communicationDisabledUntil === null) - return errorEmbed(interaction, "You can't unmute this user.", "The user was never muted."); + return await errorEmbed( + interaction, + "You can't unmute this user.", + "The user was never muted." + ); await target.edit({ communicationDisabledUntil: null }).catch(error => console.error(error)); await modEmbed({ interaction, user, action: "Unmuted" }, undefined, true); diff --git a/src/commands/moderation/warn.ts b/src/commands/moderation/warn.ts index 40768eb..b5dce39 100644 --- a/src/commands/moderation/warn.ts +++ b/src/commands/moderation/warn.ts @@ -37,7 +37,7 @@ export default class Warn { guild.id, user.id, "WARN", - guild.members.cache.get(interaction.member?.user.id!)?.id!, + guild.members.cache.get(interaction.user.id)?.id!, reason ?? undefined ); } catch (error) { diff --git a/src/commands/moderation/warns.ts b/src/commands/moderation/warns.ts index 6124fa3..db89ca1 100644 --- a/src/commands/moderation/warns.ts +++ b/src/commands/moderation/warns.ts @@ -23,10 +23,10 @@ export default class Warns { const guild = interaction.guild!; if ( !guild.members.cache - .get(interaction.member?.user.id!) + .get(interaction.user.id) ?.permissions.has(PermissionsBitField.Flags.ModerateMembers) ) - return errorEmbed( + return await errorEmbed( interaction, "You can't execute this command.", "You need the **Moderate Members** permission." diff --git a/src/commands/news/edit.ts b/src/commands/news/edit.ts index cb9bbd2..b23a714 100644 --- a/src/commands/news/edit.ts +++ b/src/commands/news/edit.ts @@ -36,7 +36,7 @@ export default class Edit { .guild!.members.cache.get(interaction.user.id)! .permissions.has(PermissionsBitField.Flags.ManageGuild) ) - return errorEmbed( + return await errorEmbed( interaction, "You can't execute this command.", "You need the **Manage Server** permission." @@ -45,7 +45,7 @@ export default class Edit { const guild = interaction.guild!; const id = interaction.options.getString("id", true).trim(); const news = get(id); - if (!news) return errorEmbed(interaction, "The specified news doesn't exist."); + if (!news) return await errorEmbed(interaction, "The specified news don't exist."); const editModal = new ModalBuilder() .setCustomId("editnews") @@ -108,7 +108,7 @@ export default class Edit { updateNews(id, title, body); await interaction.reply({ - embeds: [new EmbedBuilder().setTitle("News edited!").setColor(genColor(100))] + embeds: [new EmbedBuilder().setTitle("News edited.").setColor(genColor(100))] }); }); } diff --git a/src/commands/news/remove.ts b/src/commands/news/remove.ts index 71102b1..f293d17 100644 --- a/src/commands/news/remove.ts +++ b/src/commands/news/remove.ts @@ -30,14 +30,14 @@ export default class Remove { const member = guild.members.cache.get(interaction.user.id)!; if (!member.permissions.has(PermissionsBitField.Flags.ManageGuild)) - return errorEmbed( + return await errorEmbed( interaction, "You can't execute this command.", "You need the **Manage Server** permission." ); const news = get(id); - if (!news) return errorEmbed(interaction, "The specified news don't exist."); + if (!news) return await errorEmbed(interaction, "The specified news don't exist."); const messageID = news.messageID; const newsChannel = (await guild.channels @@ -47,7 +47,7 @@ export default class Remove { if (newsChannel) await newsChannel.messages.delete(messageID); deleteNews(id); await interaction.reply({ - embeds: [new EmbedBuilder().setTitle("News deleted!").setColor(genColor(100))] + embeds: [new EmbedBuilder().setTitle("News removed.").setColor(genColor(100))] }); } } diff --git a/src/commands/news/send.ts b/src/commands/news/send.ts index 32842a6..658d96b 100644 --- a/src/commands/news/send.ts +++ b/src/commands/news/send.ts @@ -25,7 +25,7 @@ export default class Send { const guild = interaction.guild!; const member = guild.members.cache.get(interaction.user.id)!; if (!member.permissions.has(PermissionsBitField.Flags.ManageGuild)) - return errorEmbed( + return await errorEmbed( interaction, "You can't execute this command.", "You need the **Manage Server** permission." @@ -68,7 +68,7 @@ export default class Send { await sendChannelNews(guild, id, interaction).catch(err => console.error(err)); await i.reply({ - embeds: [new EmbedBuilder().setTitle("News sent!").setColor(genColor(100))] + embeds: [new EmbedBuilder().setTitle("News sent.").setColor(genColor(100))] }); }); } diff --git a/src/commands/news/view.ts b/src/commands/news/view.ts index 0ae5770..401e64b 100644 --- a/src/commands/news/view.ts +++ b/src/commands/news/view.ts @@ -1,6 +1,7 @@ import { ActionRowBuilder, ButtonBuilder, + ButtonInteraction, ButtonStyle, EmbedBuilder, SlashCommandSubcommandBuilder, @@ -28,7 +29,7 @@ export default class View { let currentNews = sortedNews[page - 1]; if (!news || !sortedNews || sortedNews.length == 0) - return errorEmbed( + return await errorEmbed( interaction, "No news found.", "Admins can add news with the **/news send** command." @@ -59,21 +60,12 @@ export default class View { await interaction.reply({ embeds: [embed], components: [row] }); interaction.channel - ?.createMessageComponentCollector({ - filter: i => i.user.id === interaction.user.id, - time: 60000 - }) - .on("collect", async i => { - if (!i.isButton()) return; - if (i.user.id !== interaction.user.id) { - errorEmbed( - interaction, - "No.", - "You have not sent this command. Type **/news view** to view news yourself." - ); - return; - } + ?.createMessageComponentCollector({ time: 60000 }) + .on("collect", async (i: ButtonInteraction) => { + if (i.user.id !== interaction.user.id) + return await errorEmbed(i, "You aren't the person who executed this command."); + setTimeout(async () => await interaction.editReply({ components: [] }), 60000); if (i.customId === "left") { page--; if (page < 1) page = sortedNews.length; diff --git a/src/commands/serverboard.ts b/src/commands/serverboard.ts index 6c72731..8b725c3 100644 --- a/src/commands/serverboard.ts +++ b/src/commands/serverboard.ts @@ -3,7 +3,6 @@ import { ButtonBuilder, ButtonInteraction, ButtonStyle, - ComponentType, SlashCommandBuilder, type ChatInputCommandInteraction } from "discord.js"; @@ -29,10 +28,10 @@ export default class Serverboard { const pages = guildList.length; if (pages == 0) - return errorEmbed( + return await errorEmbed( interaction, - "No public server found", - "By some magical miracle, all the servers using Sokora turned off their visibility. Use /settings key:`serverboard.shown` value:`TRUE` to make your server publicly visible." + "No public server found.", + "By some magical miracle, all the servers using Sokora turned off their visibility. Use /settings serverboard `shown: True` to make your server publicly visible." ); const argPage = interaction.options.get("page")?.value as number; @@ -55,8 +54,12 @@ export default class Serverboard { const reply = await interaction.reply({ embeds: [await getEmbed()], components: [row] }); reply - .createMessageComponentCollector({ componentType: ComponentType.Button, time: 60000 }) + .createMessageComponentCollector({ time: 60000 }) .on("collect", async (i: ButtonInteraction) => { + if (i.user.id !== interaction.user.id) + return await errorEmbed(i, "You aren't the person who executed this command."); + + setTimeout(async () => await interaction.editReply({ components: [] }), 60000); switch (i.customId) { case "left": page--; diff --git a/src/commands/settings.ts b/src/commands/settings.ts index adf1327..3d3ae09 100644 --- a/src/commands/settings.ts +++ b/src/commands/settings.ts @@ -75,12 +75,13 @@ export default class Settings { } async run(interaction: ChatInputCommandInteraction) { + const guild = interaction.guild!; if ( - !interaction.guild?.members.cache - ?.get(interaction.member?.user.id!) + !guild.members.cache + ?.get(interaction.user.id) ?.permissions.has(PermissionsBitField.Flags.Administrator) ) - return errorEmbed( + return await errorEmbed( interaction, "You can't execute this command.", "You need the **Administrator** permission." @@ -94,9 +95,7 @@ export default class Settings { const description: string[] = []; Object.keys(settingsDefinition[key]).forEach(name => { - description.push( - `${name}: ${getSetting(interaction.guildId!, key, name)?.toString() || "Not set"}` - ); + description.push(`${name}: ${getSetting(guild.id, key, name)?.toString() || "Not set"}`); embed.setDescription(description.join("\n")); }); @@ -105,7 +104,7 @@ export default class Settings { const embed = new EmbedBuilder().setTitle(`Parameters changed`).setColor(genColor(100)); values.forEach(option => { - setSetting(interaction.guildId!, key, option.name, option.value as string); + setSetting(guild.id, key, option.name, option.value as string); embed.addFields({ name: option.name, value: option.value?.toString() || "Not set" diff --git a/src/commands/user.ts b/src/commands/user.ts index 38a19d9..5962f06 100644 --- a/src/commands/user.ts +++ b/src/commands/user.ts @@ -24,10 +24,11 @@ export default class User { async run(interaction: ChatInputCommandInteraction) { const guild = interaction.guild!; - const user = interaction.options.getUser("user"); - const id = user ? user.id : interaction.member?.user.id; const target = guild.members.cache - .filter(member => member.user.id === id) + .filter( + member => + member.user.id === (interaction.options.getUser("user")?.id ?? interaction.user.id) + ) .map(user => user)[0]!; const selectedUser = await target.user.fetch(); @@ -51,6 +52,9 @@ export default class User { .join(", ")}${memberRoles.length > 3 ? ` **and ${memberRoles.length - 4} more**` : ""}` ); + const embedColor = + selectedUser.hexAccentColor ?? (await imageColor(undefined, target)) ?? genColor(200); + let embed = new EmbedBuilder() .setAuthor({ name: `• ${target.nickname ?? selectedUser.displayName}`, @@ -76,14 +80,11 @@ export default class User { ) .setFooter({ text: `User ID: ${target.id}` }) .setThumbnail(target.displayAvatarURL()!) - .setColor( - selectedUser.hexAccentColor ?? (await imageColor(undefined, target)) ?? genColor(200) - ); + .setColor(embedColor); const components = []; if (getSetting(`${guild.id}`, "levelling", "enabled") && !selectedUser.bot) { - const row = new ActionRowBuilder(); - row.addComponents( + const row = new ActionRowBuilder().addComponents( new ButtonBuilder() .setCustomId("general") .setLabel("• General") @@ -112,8 +113,8 @@ export default class User { interaction.channel ?.createMessageComponentCollector({ time: 60000 }) .on("collect", async (i: ButtonInteraction) => { - if (i.member?.user.id !== interaction.member?.user.id) - return await errorEmbed(i, "You aren't the person who executed this command"); + if (i.user.id !== interaction.user.id) + return await errorEmbed(i, "You aren't the person who executed this command."); setTimeout(async () => await interaction.editReply({ components: [] }), 60000); @@ -146,9 +147,7 @@ export default class User { ) .setFooter({ text: `User ID: ${target.id}` }) .setThumbnail(target.displayAvatarURL()) - .setColor( - selectedUser.hexAccentColor ?? (await imageColor(undefined, target)) ?? genColor(200) - ); + .setColor(embedColor); switch (i.customId) { case "general": diff --git a/src/handlers/commands.ts b/src/handlers/commands.ts index 999f1ff..792dbb3 100644 --- a/src/handlers/commands.ts +++ b/src/handlers/commands.ts @@ -43,9 +43,7 @@ export class Commands { const subCommand = new subCommandModule.default(); command.addSubcommand(subCommand.data); - if ("autocompleteHandler" in subCommand) { - subCommand.autocompleteHandler(this.client); - } + if ("autocompleteHandler" in subCommand) subCommand.autocompleteHandler(this.client); continue; } diff --git a/src/handlers/events.ts b/src/handlers/events.ts index 2b6ba8d..7aa934c 100644 --- a/src/handlers/events.ts +++ b/src/handlers/events.ts @@ -8,18 +8,21 @@ export class Events { events: any[] = []; constructor(client: Client) { this.client = client; + } - async () => { - const eventsPath = join(process.cwd(), "src", "events"); + async loadEvents() { + const eventsPath = join(process.cwd(), "src", "events"); - for (const eventFile of readdirSync(eventsPath)) { - if (!eventFile.endsWith("ts")) continue; + for (const eventFile of readdirSync(eventsPath)) { + if (!eventFile.endsWith("ts")) continue; - const event = await import(pathToFileURL(join(eventsPath, eventFile)).toString()); - const clientEvent = client.on(event.default.name, new event.default.event(client).run); + const event = await import(pathToFileURL(join(eventsPath, eventFile)).toString()); + const clientEvent = this.client.on( + event.default.name, + new event.default.event(this.client).run + ); - this.events.push({ name: event.default.name, event: clientEvent }); - } - }; + this.events.push({ name: event.default.name, event: clientEvent }); + } } } diff --git a/src/utils/database/settings.ts b/src/utils/database/settings.ts index 729d213..4ffda4e 100644 --- a/src/utils/database/settings.ts +++ b/src/utils/database/settings.ts @@ -82,13 +82,13 @@ export const settingsDefinition: Record< } }, welcome: { - text: { + join_text: { type: "TEXT", - desc: "The welcome message that is sent when a user joins. You can type (user) to display the username, (count) to display the server count and (servername) to display the server name." + desc: "Text sent when a user joins. (user) - username, (count) - member count, (servername) - server name." }, - goodbye_text: { + leave_text: { type: "TEXT", - desc: "The goodbye message that is sent when a user leaves. You can type (user) to display the username, (count) to display the server count and (servername) to display the server name." + desc: "Text sent when a user leaves. (user) - username, (count) - member count, (servername) - server name." }, channel: { type: "TEXT", desc: "ID of the channel where welcome messages are sent." } } diff --git a/src/utils/embeds/errorEmbed.ts b/src/utils/embeds/errorEmbed.ts index 78b9dae..cd12122 100644 --- a/src/utils/embeds/errorEmbed.ts +++ b/src/utils/embeds/errorEmbed.ts @@ -20,5 +20,6 @@ export async function errorEmbed( .setDescription(content.join("\n")) .setColor(genColor(0)); + if (interaction.replied) return await interaction.followUp({ embeds: [embed], ephemeral: true }); return await interaction.reply({ embeds: [embed], ephemeral: true }); } diff --git a/src/utils/embeds/modEmbed.ts b/src/utils/embeds/modEmbed.ts index 81d4c1b..fb101a5 100644 --- a/src/utils/embeds/modEmbed.ts +++ b/src/utils/embeds/modEmbed.ts @@ -28,21 +28,21 @@ export async function errorCheck( const { allErrors, botError, ownerError } = errorOptions; const guild = interaction.guild!; const members = guild.members.cache!; - const member = members.get(interaction.member?.user.id!)!; - const client = members.get(interaction.client.user.id!)!; + const member = members.get(interaction.user.id)!; + const client = members.get(interaction.client.user.id)!; const target = members.get(user.id)!; const name = user.displayName; if (botError) if (!client.permissions.has(permission)) - return errorEmbed( + return await errorEmbed( interaction, "The bot can't execute this command.", `The bot is missing the **${permissionAction}** permission.` ); if (!member.permissions.has(permission)) - return errorEmbed( + return await errorEmbed( interaction, "You can't execute this command.", `You're missing the **${permissionAction} Members** permission.` @@ -51,20 +51,20 @@ export async function errorCheck( if (!allErrors) return; if (!target) return; if (target === member) - return errorEmbed(interaction, `You can't ${action.toLowerCase()} yourself.`); + return await errorEmbed(interaction, `You can't ${action.toLowerCase()} yourself.`); if (target.id === interaction.client.user.id) - return errorEmbed(interaction, `You can't ${action.toLowerCase()} Sokora.`); + return await errorEmbed(interaction, `You can't ${action.toLowerCase()} Sokora.`); if (!target.manageable) - return errorEmbed( + return await errorEmbed( interaction, `You can't ${action.toLowerCase()} ${name}.`, "The member has a higher role position than Sokora." ); if (member.roles.highest.position < target.roles.highest.position) - return errorEmbed( + return await errorEmbed( interaction, `You can't ${action.toLowerCase()} ${name}.`, "The member has a higher role position than you." @@ -72,7 +72,7 @@ export async function errorCheck( if (ownerError) { if (target.id === guild.ownerId) - return errorEmbed( + return await errorEmbed( interaction, `You can't ${action.toLowerCase()} ${name}.`, "The member owns the server." diff --git a/src/utils/embeds/serverEmbed.ts b/src/utils/embeds/serverEmbed.ts index 224aade..55f5384 100644 --- a/src/utils/embeds/serverEmbed.ts +++ b/src/utils/embeds/serverEmbed.ts @@ -1,6 +1,5 @@ import { EmbedBuilder, type Guild } from "discord.js"; import { genColor } from "../colorGen"; -import { getSetting } from "../database/settings"; import { imageColor } from "../imageColor"; type Options = { diff --git a/src/utils/sendChannelNews.ts b/src/utils/sendChannelNews.ts index 005ee94..6d0bfcd 100644 --- a/src/utils/sendChannelNews.ts +++ b/src/utils/sendChannelNews.ts @@ -17,7 +17,7 @@ export async function sendChannelNews( body?: string ) { const news = get(id)!; - const role = getSetting(guild.id, "news", "role_id"); + const role = getSetting(guild.id, "news", "role_id") as string; let roleToSend: Role | undefined; if (role) roleToSend = guild.roles.cache.get(role); @@ -31,7 +31,7 @@ export async function sendChannelNews( return ( guild.channels.cache.get( - getSetting(guild.id, "news", "channel_id")! ?? interaction.channel?.id + (getSetting(guild.id, "news", "channel_id") as string) ?? interaction.channel?.id ) as TextChannel ) .send({ From d89d0151ca39ee4074310b913edc58e10a830a31 Mon Sep 17 00:00:00 2001 From: ThatFrogDev Date: Mon, 19 Aug 2024 21:25:10 +0200 Subject: [PATCH 080/127] feat: temporary ban --- bun.lockb | Bin 57167 -> 57167 bytes src/commands/moderation/ban.ts | 22 +++++++++++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) mode change 100644 => 100755 bun.lockb diff --git a/bun.lockb b/bun.lockb old mode 100644 new mode 100755 index 5c6eba8433de340b178dbdd2730c28b82e52a817..80eda75d0774d5a57a9c38b8fc069b1dbf5e966f GIT binary patch delta 34 ncmX@VkNNyQ<_#LN7)>T?&T?aCVgQ55WwT5eO*gNewZsJg(0&VA delta 34 qcmX@VkNNyQ<_#LN7>y=t&T?aCj5E?R)H9x3Hp_(3Wb^7-OI!fst_-FC diff --git a/src/commands/moderation/ban.ts b/src/commands/moderation/ban.ts index 8484df9..7011361 100644 --- a/src/commands/moderation/ban.ts +++ b/src/commands/moderation/ban.ts @@ -4,6 +4,9 @@ import { type ChatInputCommandInteraction } from "discord.js"; import { errorCheck, modEmbed } from "../../utils/embeds/modEmbed"; +import { errorEmbed } from "../../utils/embeds/errorEmbed"; +import Unban from "./unban"; +import ms from "ms"; export default class Ban { data: SlashCommandSubcommandBuilder; @@ -24,6 +27,21 @@ export default class Ban { async run(interaction: ChatInputCommandInteraction) { const user = interaction.options.getUser("user")!; + const guild = interaction.guild!; + const duration = interaction.options.getString("duration", false)!; + + if (duration) { + if (!ms(duration)) + return await errorEmbed( + interaction, + `You can't ban ${user.displayName} temporary.`, + "The duration is invalid" + ); + + setTimeout(() => { + await guild.members.unban(user.id, reason ?? undefined).catch(error => console.error(error)); + }, ms(duration)); + } await errorCheck( PermissionsBitField.Flags.BanMembers, @@ -38,6 +56,8 @@ export default class Ban { ?.ban({ reason: reason ?? undefined }) .catch(error => console.error(error)); - await modEmbed({ interaction, user, action: "Banned" }, reason); + await modEmbed({ interaction, user, action: "Banned", duration? }, reason ); + // TODO: Send the user a message when they're banned including the duration + } } } From c7043cc87e354f9c64c493df88a9c54e84686b5c Mon Sep 17 00:00:00 2001 From: Serge Date: Mon, 26 Aug 2024 23:39:15 +0500 Subject: [PATCH 081/127] smol --- src/commands/moderation/ban.ts | 21 +++-- src/commands/news/view.ts | 18 ++-- src/commands/serverboard.ts | 17 ++-- src/commands/settings.ts | 2 +- src/commands/user.ts | 160 +++++++++++++++----------------- src/events/messageCreate.ts | 10 +- src/utils/database/settings.ts | 7 +- src/utils/embeds/serverEmbed.ts | 1 - src/utils/logChannel.ts | 28 +++--- src/utils/multiReact.ts | 3 +- src/utils/quickSort.ts | 12 +-- src/utils/sendChannelNews.ts | 9 ++ 12 files changed, 146 insertions(+), 142 deletions(-) diff --git a/src/commands/moderation/ban.ts b/src/commands/moderation/ban.ts index 7011361..cf44408 100644 --- a/src/commands/moderation/ban.ts +++ b/src/commands/moderation/ban.ts @@ -5,7 +5,6 @@ import { } from "discord.js"; import { errorCheck, modEmbed } from "../../utils/embeds/modEmbed"; import { errorEmbed } from "../../utils/embeds/errorEmbed"; -import Unban from "./unban"; import ms from "ms"; export default class Ban { @@ -18,7 +17,7 @@ export default class Ban { user.setName("user").setDescription("The user that you want to ban.").setRequired(true) ) .addStringOption(string => - string.setName("duration").setDescription("The duration of the ban (e.g 30m, 1d, 2h).") + string.setName("duration").setDescription("The duration of the ban (e.g 2mo, 1y).") ) .addStringOption(string => string.setName("reason").setDescription("The reason for the ban.") @@ -28,18 +27,22 @@ export default class Ban { async run(interaction: ChatInputCommandInteraction) { const user = interaction.options.getUser("user")!; const guild = interaction.guild!; - const duration = interaction.options.getString("duration", false)!; + const duration = interaction.options.getString("duration", false); if (duration) { if (!ms(duration)) return await errorEmbed( interaction, - `You can't ban ${user.displayName} temporary.`, - "The duration is invalid" + `You can't ban ${user.displayName} temporarily.`, + "The duration is invalid." ); - setTimeout(() => { - await guild.members.unban(user.id, reason ?? undefined).catch(error => console.error(error)); + setTimeout(async () => { + await guild.members + .unban(user.id, reason ?? undefined) + .catch(error => console.error(error)); + + await modEmbed({ interaction, user, action: "Unbanned" }, reason); }, ms(duration)); } @@ -56,8 +59,6 @@ export default class Ban { ?.ban({ reason: reason ?? undefined }) .catch(error => console.error(error)); - await modEmbed({ interaction, user, action: "Banned", duration? }, reason ); - // TODO: Send the user a message when they're banned including the duration - } + await modEmbed({ interaction, user, action: "Banned", duration }, reason); } } diff --git a/src/commands/news/view.ts b/src/commands/news/view.ts index 401e64b..4b1d58a 100644 --- a/src/commands/news/view.ts +++ b/src/commands/news/view.ts @@ -58,7 +58,6 @@ export default class View { .setStyle(ButtonStyle.Primary) ); - await interaction.reply({ embeds: [embed], components: [row] }); interaction.channel ?.createMessageComponentCollector({ time: 60000 }) .on("collect", async (i: ButtonInteraction) => { @@ -66,12 +65,13 @@ export default class View { return await errorEmbed(i, "You aren't the person who executed this command."); setTimeout(async () => await interaction.editReply({ components: [] }), 60000); - if (i.customId === "left") { - page--; - if (page < 1) page = sortedNews.length; - } else if (i.customId === "right") { - page++; - if (page > sortedNews.length) page = 1; + switch (i.customId) { + case "left": + page--; + if (page < 1) page = sortedNews.length; + case "right": + page++; + if (page > sortedNews.length) page = 1; } currentNews = sortedNews[page - 1]; @@ -85,7 +85,9 @@ export default class View { .setColor(genColor(200)); await interaction.editReply({ embeds: [embed], components: [row] }); - await i.deferUpdate(); + await i.update({}); }); + + await interaction.reply({ embeds: [embed], components: [row] }); } } diff --git a/src/commands/serverboard.ts b/src/commands/serverboard.ts index 8b725c3..58b42d7 100644 --- a/src/commands/serverboard.ts +++ b/src/commands/serverboard.ts @@ -36,11 +36,7 @@ export default class Serverboard { const argPage = interaction.options.get("page")?.value as number; let page = (argPage - 1 <= 0 ? 0 : argPage - 1 > pages ? pages - 1 : argPage - 1) || 0; - - async function getEmbed() { - return await serverEmbed({ guild: guildList[page], page: page + 1, pages, showInvite: true }); - } - + const embed = await serverEmbed({ guild: guildList[page], page: page + 1, pages }); const row = new ActionRowBuilder().addComponents( new ButtonBuilder() .setCustomId("left") @@ -52,9 +48,8 @@ export default class Serverboard { .setStyle(ButtonStyle.Primary) ); - const reply = await interaction.reply({ embeds: [await getEmbed()], components: [row] }); - reply - .createMessageComponentCollector({ time: 60000 }) + interaction.channel + ?.createMessageComponentCollector({ time: 60000 }) .on("collect", async (i: ButtonInteraction) => { if (i.user.id !== interaction.user.id) return await errorEmbed(i, "You aren't the person who executed this command."); @@ -69,8 +64,10 @@ export default class Serverboard { if (page >= pages) page = 0; } - await reply.edit({ embeds: [await getEmbed()], components: [row] }); - i.update({}); + await interaction.editReply({ embeds: [embed], components: [row] }); + await i.update({}); }); + + await interaction.reply({ embeds: [embed], components: [row] }); } } diff --git a/src/commands/settings.ts b/src/commands/settings.ts index 3d3ae09..1c4378a 100644 --- a/src/commands/settings.ts +++ b/src/commands/settings.ts @@ -58,7 +58,7 @@ export default class Settings { break; // case "LIST": // const subcommandGroup = new SlashCommandSubcommandGroupBuilder() - // .setName(key) + // .setName(sub) // .setDescription("This subcommand group has no description."); default: // Also includes "TEXT" subcommand.addStringOption(option => diff --git a/src/commands/user.ts b/src/commands/user.ts index 5962f06..140176d 100644 --- a/src/commands/user.ts +++ b/src/commands/user.ts @@ -4,6 +4,7 @@ import { ButtonInteraction, ButtonStyle, EmbedBuilder, + SlashCommandOptionsOnlyBuilder, SlashCommandBuilder, type ChatInputCommandInteraction } from "discord.js"; @@ -14,7 +15,7 @@ import { errorEmbed } from "../utils/embeds/errorEmbed"; import { imageColor } from "../utils/imageColor"; export default class User { - data: Omit; + data: SlashCommandOptionsOnlyBuilder; constructor() { this.data = new SlashCommandBuilder() .setName("user") @@ -82,90 +83,83 @@ export default class User { .setThumbnail(target.displayAvatarURL()!) .setColor(embedColor); - const components = []; - if (getSetting(`${guild.id}`, "levelling", "enabled") && !selectedUser.bot) { - const row = new ActionRowBuilder().addComponents( - new ButtonBuilder() - .setCustomId("general") - .setLabel("• General") - .setEmoji("📃") - .setStyle(ButtonStyle.Primary), - new ButtonBuilder() - .setCustomId("level") - .setLabel("• Level") - .setEmoji("⚡") - .setStyle(ButtonStyle.Primary) - ); - row.components[0].setDisabled(true); + if (!getSetting(`${guild.id}`, "levelling", "enabled") && selectedUser.bot) return; + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId("general") + .setLabel("• General") + .setEmoji("📃") + .setStyle(ButtonStyle.Primary), + new ButtonBuilder() + .setCustomId("level") + .setLabel("• Level") + .setEmoji("⚡") + .setStyle(ButtonStyle.Primary) + ); + row.components[0].setDisabled(true); - const [guildLevel, guildExp] = getLevel(`${guild.id}`, `${target.id}`)!; - const [globalLevel, globalExp] = getLevel("0", `${target.id}`)!; - if (!guildLevel && !guildExp) setLevel(`${guild.id}`, `${target.id}`, 0, 0); - if (!globalLevel && !globalExp) setLevel("0", `${target.id}`, 0, 0); + const [guildLevel, guildExp] = getLevel(`${guild.id}`, `${target.id}`)!; + const [globalLevel, globalExp] = getLevel("0", `${target.id}`)!; + if (!guildLevel && !guildExp) setLevel(`${guild.id}`, `${target.id}`, 0, 0); + if (!globalLevel && !globalExp) setLevel("0", `${target.id}`, 0, 0); - const nextLevelExp = Math.floor(100 * 1.15 * ((guildLevel ?? 0) + 1))?.toLocaleString( - "en-US" - ); - const globalNextLevelExp = Math.floor(100 * 1.15 * ((globalLevel ?? 0) + 1))?.toLocaleString( - "en-US" - ); + const nextLevelExp = Math.floor(100 * 1.15 * ((guildLevel ?? 0) + 1))?.toLocaleString("en-US"); + const globalNextLevelExp = Math.floor(100 * 1.15 * ((globalLevel ?? 0) + 1))?.toLocaleString( + "en-US" + ); + + interaction.channel + ?.createMessageComponentCollector({ time: 60000 }) + .on("collect", async (i: ButtonInteraction) => { + if (i.user.id !== interaction.user.id) + return await errorEmbed(i, "You aren't the person who executed this command."); + + setTimeout(async () => await interaction.editReply({ components: [] }), 60000); + i.customId === "general" + ? row.components[0].setDisabled(true) + : row.components[1].setDisabled(true); + + const levelEmbed = new EmbedBuilder() + .setAuthor({ + name: `• ${target.nickname ?? selectedUser.displayName}`, + iconURL: target.displayAvatarURL() + }) + .setFields( + { + name: `⚡ • Guild level ${guildLevel ?? 0}`, + value: [ + `**${guildExp.toLocaleString("en-US") ?? 0}/${nextLevelExp}** EXP`, + `**Next level**: ${(guildLevel ?? 0) + 1}` + ].join("\n"), + inline: true + }, + { + name: `⛈️ • Global level ${globalLevel ?? 0}`, + value: [ + `**${globalExp.toLocaleString("en-US") ?? 0}/${globalNextLevelExp}** EXP`, + `**Next level**: ${(globalLevel ?? 0) + 1}` + ].join("\n"), + inline: true + } + ) + .setFooter({ text: `User ID: ${target.id}` }) + .setThumbnail(target.displayAvatarURL()) + .setColor(embedColor); + + switch (i.customId) { + case "general": + row.components[1].setDisabled(false); + await interaction.editReply({ embeds: [embed], components: [row] }); + break; + case "level": + row.components[0].setDisabled(false); + await interaction.editReply({ embeds: [levelEmbed], components: [row] }); + break; + } + + await i.update({}); + }); - interaction.channel - ?.createMessageComponentCollector({ time: 60000 }) - .on("collect", async (i: ButtonInteraction) => { - if (i.user.id !== interaction.user.id) - return await errorEmbed(i, "You aren't the person who executed this command."); - - setTimeout(async () => await interaction.editReply({ components: [] }), 60000); - - i.customId === "general" - ? row.components[0].setDisabled(true) - : row.components[1].setDisabled(true); - - const levelEmbed = new EmbedBuilder() - .setAuthor({ - name: `• ${target.nickname ?? selectedUser.displayName}`, - iconURL: target.displayAvatarURL() - }) - .setFields( - { - name: `⚡ • Guild level ${guildLevel ?? 0}`, - value: [ - `**${guildExp.toLocaleString("en-US") ?? 0}/${nextLevelExp}** EXP`, - `**Next level**: ${(guildLevel ?? 0) + 1}` - ].join("\n"), - inline: true - }, - { - name: `⛈️ • Global level ${globalLevel ?? 0}`, - value: [ - `**${globalExp.toLocaleString("en-US") ?? 0}/${globalNextLevelExp}** EXP`, - `**Next level**: ${(globalLevel ?? 0) + 1}` - ].join("\n"), - inline: true - } - ) - .setFooter({ text: `User ID: ${target.id}` }) - .setThumbnail(target.displayAvatarURL()) - .setColor(embedColor); - - switch (i.customId) { - case "general": - row.components[1].setDisabled(false); - await interaction.editReply({ embeds: [embed], components: [row] }); - break; - case "level": - row.components[0].setDisabled(false); - await interaction.editReply({ embeds: [levelEmbed], components: [row] }); - break; - } - - await i.update({}); - }); - - components.push(row); - } - - await interaction.reply({ embeds: [embed], components: components }); + await interaction.reply({ embeds: [embed], components: [row] }); } } diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts index 23c7279..20353ef 100644 --- a/src/events/messageCreate.ts +++ b/src/events/messageCreate.ts @@ -29,20 +29,21 @@ export default { // Levelling if (!getSetting(guild.id, "levelling", "enabled")) return; - const level = getSetting(guild.id, "levelling", "set_level")! as string; + const level = getSetting(guild.id, "levelling", "set_level") as string; if (level != "" && level != null) { const newLevel = kominator(level); setLevel(guild.id, newLevel[0], +newLevel[1], 100 * +newLevel[1]); setSetting(guild.id, "levelling", "set_level", ""); } - const blockedChannels = getSetting(guild.id, "levelling", "block_channels")! as string; + const blockedChannels = getSetting(guild.id, "levelling", "block_channels") as string; if (blockedChannels != undefined) for (const channelID of kominator(blockedChannels)) if (message.channelId === channelID) return; - let expGain = getSetting(guild.id, "levelling", "set_xp_gain")! as number; - const multiplier = getSetting(guild.id, "levelling", "add_multiplier")! as string; + // const cooldown = getSetting(guild.id, "levelling", "set_cooldown") as number; + let expGain = getSetting(guild.id, "levelling", "set_xp_gain") as number; + const multiplier = getSetting(guild.id, "levelling", "add_multiplier") as string; if (multiplier != null) { const expMultiplier = kominator(multiplier); @@ -53,7 +54,6 @@ export default { expGain = expGain * +expMultiplier[0]; } - // const cooldown = getSetting(guild.id, "levelling.setCooldown") ?? 4; const levelChannelId = getSetting(guild.id, "levelling", "channel"); const [guildLevel, guildExp] = getLevel(guild.id, author.id); const [globalLevel, globalExp] = getLevel("0", author.id); diff --git a/src/utils/database/settings.ts b/src/utils/database/settings.ts index 4ffda4e..da89198 100644 --- a/src/utils/database/settings.ts +++ b/src/utils/database/settings.ts @@ -55,11 +55,6 @@ export const settingsDefinition: Record< type: "BOOL", desc: "Whether or not edited/deleted messages should be logged.", val: true - }, - dm_user: { - type: "BOOL", - desc: "Whether or not should the bot send a DM after a moderation action.", - val: true } }, news: { @@ -113,7 +108,7 @@ export function getSetting( let res = getQuery.all(JSON.stringify(guildID), key + "." + setting) as TypeOfDefinition< typeof tableDefinition >[]; - console.log(res); + if (res.length == 0) return null; switch (settingsDefinition[key][setting].type) { case "TEXT": diff --git a/src/utils/embeds/serverEmbed.ts b/src/utils/embeds/serverEmbed.ts index 55f5384..d490d04 100644 --- a/src/utils/embeds/serverEmbed.ts +++ b/src/utils/embeds/serverEmbed.ts @@ -5,7 +5,6 @@ import { imageColor } from "../imageColor"; type Options = { guild: Guild; roles?: boolean; - showInvite?: boolean; page?: number; pages?: number; }; diff --git a/src/utils/logChannel.ts b/src/utils/logChannel.ts index ad2f8fa..5a5ed42 100644 --- a/src/utils/logChannel.ts +++ b/src/utils/logChannel.ts @@ -7,18 +7,24 @@ import { } from "discord.js"; import { getSetting } from "./database/settings"; +/** + * Sends a message in the log channel. (if there is one set) + * @param guild The guild where the log channel is located. + * @param embed Embed of the log. + * @returns Log message. + */ export async function logChannel(guild: Guild, embed: EmbedBuilder) { const logChannel = getSetting(guild.id, "moderation", "channel"); - if (logChannel) { - const channel = await guild.channels.cache - .get(`${logChannel}`) - ?.fetch() - .then((channel: Channel) => { - if (channel.type != ChannelType.GuildText) return null; - return channel as TextChannel; - }) - .catch(() => null); + if (!logChannel) return; - if (channel) await channel.send({ embeds: [embed] }); - } + const channel = await guild.channels.cache + .get(`${logChannel}`) + ?.fetch() + .then((channel: Channel) => { + if (channel.type != ChannelType.GuildText) return null; + return channel as TextChannel; + }) + .catch(() => null); + + if (channel) return await channel.send({ embeds: [embed] }); } diff --git a/src/utils/multiReact.ts b/src/utils/multiReact.ts index 904ead7..c7fa763 100644 --- a/src/utils/multiReact.ts +++ b/src/utils/multiReact.ts @@ -4,6 +4,7 @@ import type { Message } from "discord.js"; * Reacts to a message with multiple emojis. * @param message Message to react to. * @param emojis Emojis that will be used to react. + * @returns Reactions with the provided emojis. */ export async function multiReact(message: Message, ...emojis: string[]) { for (const i of emojis) { @@ -11,6 +12,6 @@ export async function multiReact(message: Message, ...emojis: string[]) { await message.react(i); continue; } - for (const reaction of i) if (reaction !== " ") await message.react(reaction); + for (const reaction of i) if (reaction !== " ") return await message.react(reaction); } } diff --git a/src/utils/quickSort.ts b/src/utils/quickSort.ts index 205660e..38beb94 100644 --- a/src/utils/quickSort.ts +++ b/src/utils/quickSort.ts @@ -23,12 +23,12 @@ function swap( } /** - * Sorts an array of items and returns the sorted items and corresponding items - * @param sortItems The items to sort - * @param corresponding The corresponding items to sort - * @param leftIndex The left index of the sortItems array - * @param rightIndex The right index of the sortItems array - * @returns The sorted items and corresponding items + * Sorts an array of items and returns the sorted items and corresponding items. + * @param sortItems The items to sort. + * @param corresponding The corresponding items to sort. + * @param leftIndex The left index of the sortItems array. + * @param rightIndex The right index of the sortItems array. + * @returns The sorted items and corresponding items. */ export function quickSort( sortItems: number[], diff --git a/src/utils/sendChannelNews.ts b/src/utils/sendChannelNews.ts index 6d0bfcd..c93d5fb 100644 --- a/src/utils/sendChannelNews.ts +++ b/src/utils/sendChannelNews.ts @@ -9,6 +9,15 @@ import { genColor } from "./colorGen"; import { get, updateNews } from "./database/news"; import { getSetting } from "./database/settings"; +/** + * Sends news to a channel. + * @param guild Guild where the channel is in. + * @param id ID of the news. + * @param interaction Command nteraction. + * @param title Title of the news. + * @param body Content of the news. + * @returns News message in a channel. + */ export async function sendChannelNews( guild: Guild, id: string, From bb651e07312d80b5f994b951e9801c77c949e8a6 Mon Sep 17 00:00:00 2001 From: Serge Date: Fri, 30 Aug 2024 22:03:32 +0500 Subject: [PATCH 082/127] inconsistency fixing + one more step towards fixing interaction errors --- src/commands/about.ts | 4 +- src/commands/moderation/ban.ts | 2 +- src/commands/moderation/delwarn.ts | 6 +-- src/commands/moderation/lock.ts | 2 +- src/commands/moderation/purge.ts | 4 +- src/commands/moderation/slowdown.ts | 7 ++- src/commands/moderation/unban.ts | 4 +- src/commands/moderation/unlock.ts | 2 +- src/commands/moderation/unmute.ts | 2 +- src/commands/news/{send.ts => add.ts} | 37 +++++++++------ src/commands/news/edit.ts | 67 +++++++++++++-------------- src/commands/news/remove.ts | 18 +++---- src/commands/news/view.ts | 24 ++++++---- src/commands/serverboard.ts | 29 +++++++----- src/commands/settings.ts | 4 +- src/commands/user.ts | 60 +++++++++++------------- src/events/easterEggs/Bread.ts | 4 +- src/events/easterEggs/Crazy.ts | 4 +- src/events/easterEggs/Fireship.ts | 2 +- src/events/guildCreate.ts | 2 +- src/events/guildMemberAdd.ts | 2 +- src/events/guildMemberRemove.ts | 2 +- src/events/interactionCreate.ts | 2 +- src/events/messageCreate.ts | 12 ++--- src/events/messageDelete.ts | 20 ++------ src/events/messageUpdate.ts | 21 ++------- src/utils/colorGen.ts | 6 +-- src/utils/database/levelling.ts | 4 +- src/utils/database/news.ts | 2 +- src/utils/database/settings.ts | 6 +-- src/utils/embeds/modEmbed.ts | 10 ++-- src/utils/embeds/serverEmbed.ts | 19 ++++---- src/utils/logChannel.ts | 9 +++- src/utils/multiReact.ts | 5 +- 34 files changed, 195 insertions(+), 209 deletions(-) rename src/commands/news/{send.ts => add.ts} (67%) diff --git a/src/commands/about.ts b/src/commands/about.ts index 5304506..e3eee35 100644 --- a/src/commands/about.ts +++ b/src/commands/about.ts @@ -28,8 +28,8 @@ export default class About { name: "📃 • General", value: [ "**Version** 0.1-preview, *Kaishi*", - `**${members}** members • **${guilds.size}** guild${guilds.size === 1 ? "" : "s"} ${ - shards == undefined ? "" : `• **${shards}** shard${shards === 1 ? "" : "s"}` + `**${members}** members • **${guilds.size}** guild${guilds.size == 1 ? "" : "s"} ${ + !shards ? "" : `• **${shards}** shard${shards == 1 ? "" : "s"}` }` ].join("\n") }, diff --git a/src/commands/moderation/ban.ts b/src/commands/moderation/ban.ts index cf44408..3ba4a6f 100644 --- a/src/commands/moderation/ban.ts +++ b/src/commands/moderation/ban.ts @@ -27,7 +27,7 @@ export default class Ban { async run(interaction: ChatInputCommandInteraction) { const user = interaction.options.getUser("user")!; const guild = interaction.guild!; - const duration = interaction.options.getString("duration", false); + const duration = interaction.options.getString("duration"); if (duration) { if (!ms(duration)) diff --git a/src/commands/moderation/delwarn.ts b/src/commands/moderation/delwarn.ts index 7e99ea5..f9dface 100644 --- a/src/commands/moderation/delwarn.ts +++ b/src/commands/moderation/delwarn.ts @@ -32,9 +32,9 @@ export default class Delwarn { const user = interaction.options.getUser("user")!; const guild = interaction.guild!; const name = user.displayName; - const id = interaction.options.getNumber("id", true); + const id = interaction.options.getNumber("id"); const warns = listUserModeration(guild.id, user.id, "WARN"); - const newWarns = warns.filter(warn => warn.id !== `${id}`); + const newWarns = warns.filter(warn => warn.id != `${id}`); await errorCheck( PermissionsBitField.Flags.ModerateMembers, @@ -43,7 +43,7 @@ export default class Delwarn { "Moderate Members" ); - if (newWarns.length === warns.length) + if (newWarns.length == warns.length) return await errorEmbed(interaction, `There is no warn with the id of ${id}.`); const embed = new EmbedBuilder() diff --git a/src/commands/moderation/lock.ts b/src/commands/moderation/lock.ts index 25e6fdf..5a43262 100644 --- a/src/commands/moderation/lock.ts +++ b/src/commands/moderation/lock.ts @@ -62,7 +62,7 @@ export default class Lock { .setColor(genColor(100)); if ( - channel.type === ChannelType.GuildText && + channel.type == ChannelType.GuildText && ChannelType.PublicThread && ChannelType.PrivateThread && ChannelType.GuildVoice diff --git a/src/commands/moderation/purge.ts b/src/commands/moderation/purge.ts index 9ccb5be..4e0d32d 100644 --- a/src/commands/moderation/purge.ts +++ b/src/commands/moderation/purge.ts @@ -36,7 +36,6 @@ export default class Purge { async run(interaction: ChatInputCommandInteraction) { const guild = interaction.guild!; - const amount = interaction.options.getNumber("amount")!; if ( !guild.members.cache .get(interaction.user.id) @@ -48,6 +47,7 @@ export default class Purge { "You need the **Manage Messages** permission." ); + const amount = interaction.options.getNumber("amount")!; if (amount > 100) return await errorEmbed(interaction, "You can only purge up to 100 messages at a time."); @@ -66,7 +66,7 @@ export default class Purge { .setColor(genColor(100)); if ( - channel.type === ChannelType.GuildText && + channel.type == ChannelType.GuildText && ChannelType.PublicThread && ChannelType.PrivateThread && ChannelType.GuildVoice diff --git a/src/commands/moderation/slowdown.ts b/src/commands/moderation/slowdown.ts index 1a7f198..9c81133 100644 --- a/src/commands/moderation/slowdown.ts +++ b/src/commands/moderation/slowdown.ts @@ -42,7 +42,6 @@ export default class Slowdown { async run(interaction: ChatInputCommandInteraction) { const guild = interaction.guild!; - const time = interaction.options.getString("time")!; if ( !guild.members.cache .get(interaction.user.id) @@ -54,14 +53,14 @@ export default class Slowdown { "You need the **Manage Channels** permission." ); + const time = interaction.options.getString("time")!; const channelOption = interaction.options.getChannel("channel")!; const channel = guild.channels.cache.get(interaction.channel?.id ?? channelOption.id)!; let title = `Set a slowdown of \`${channelOption ?? `${channel.name}`}\` to ${ms(ms(time), { long: true })}.`; - if (ms(time) === 0) - title = `Removed the slowdown from \`${channelOption ?? `${channel.name}`}\`.`; + if (!ms(time)) title = `Removed the slowdown from \`${channelOption ?? `${channel.name}`}\`.`; const embed = new EmbedBuilder() .setTitle(title) @@ -75,7 +74,7 @@ export default class Slowdown { .setColor(genColor(100)); if ( - channel.type === ChannelType.GuildText && + channel.type == ChannelType.GuildText && ChannelType.PublicThread && ChannelType.PrivateThread && ChannelType.GuildVoice diff --git a/src/commands/moderation/unban.ts b/src/commands/moderation/unban.ts index f278e3b..49708db 100644 --- a/src/commands/moderation/unban.ts +++ b/src/commands/moderation/unban.ts @@ -29,7 +29,7 @@ export default class Unban { const guild = interaction.guild!; const target = (await guild.bans.fetch()) .map(ban => ban.user) - .filter(user => user.id === id)[0]!; + .filter(user => user.id == id)[0]!; await errorCheck( PermissionsBitField.Flags.BanMembers, @@ -38,7 +38,7 @@ export default class Unban { "Ban Members" ); - if (target == undefined) + if (!target) return await errorEmbed( interaction, "You can't unban this user.", diff --git a/src/commands/moderation/unlock.ts b/src/commands/moderation/unlock.ts index c3eba7f..8a094c8 100644 --- a/src/commands/moderation/unlock.ts +++ b/src/commands/moderation/unlock.ts @@ -62,7 +62,7 @@ export default class Unlock { .setColor(genColor(100)); if ( - channel.type === ChannelType.GuildText && + channel.type == ChannelType.GuildText && ChannelType.PublicThread && ChannelType.PrivateThread && ChannelType.GuildVoice diff --git a/src/commands/moderation/unmute.ts b/src/commands/moderation/unmute.ts index e8cd588..2b354ee 100644 --- a/src/commands/moderation/unmute.ts +++ b/src/commands/moderation/unmute.ts @@ -28,7 +28,7 @@ export default class Unmute { "Moderate Members" ); - if (target.communicationDisabledUntil === null) + if (!target.communicationDisabledUntil) return await errorEmbed( interaction, "You can't unmute this user.", diff --git a/src/commands/news/send.ts b/src/commands/news/add.ts similarity index 67% rename from src/commands/news/send.ts rename to src/commands/news/add.ts index 658d96b..08ee723 100644 --- a/src/commands/news/send.ts +++ b/src/commands/news/add.ts @@ -9,54 +9,60 @@ import { type ChatInputCommandInteraction } from "discord.js"; import { genColor } from "../../utils/colorGen"; -import { sendNews } from "../../utils/database/news"; +import { addNews } from "../../utils/database/news"; import { errorEmbed } from "../../utils/embeds/errorEmbed"; import { sendChannelNews } from "../../utils/sendChannelNews"; export default class Send { data: SlashCommandSubcommandBuilder; constructor() { - this.data = new SlashCommandSubcommandBuilder() - .setName("send") - .setDescription("Send your news."); + this.data = new SlashCommandSubcommandBuilder().setName("add").setDescription("Add your news."); } async run(interaction: ChatInputCommandInteraction) { const guild = interaction.guild!; - const member = guild.members.cache.get(interaction.user.id)!; - if (!member.permissions.has(PermissionsBitField.Flags.ManageGuild)) + if ( + !guild.members.cache + .get(interaction.user.id) + ?.permissions.has(PermissionsBitField.Flags.ManageGuild) + ) return await errorEmbed( interaction, "You can't execute this command.", "You need the **Manage Server** permission." ); - const newsModal = new ModalBuilder().setCustomId("sendnews").setTitle("Write your news."); - const firstActionRow = new ActionRowBuilder().addComponents( + const firstActionRow = new ActionRowBuilder().addComponents( new TextInputBuilder() .setCustomId("title") .setPlaceholder("Write a title") - .setStyle(TextInputStyle.Short) .setMaxLength(100) + .setStyle(TextInputStyle.Short) .setLabel("Title") - ) as ActionRowBuilder; + .setRequired(true) + ); - const secondActionRow = new ActionRowBuilder().addComponents( + const secondActionRow = new ActionRowBuilder().addComponents( new TextInputBuilder() .setCustomId("body") .setPlaceholder("Insert your content here") .setMaxLength(4000) .setStyle(TextInputStyle.Paragraph) .setLabel("Content (supports Markdown)") - ) as ActionRowBuilder; + .setRequired(true) + ); + + const newsModal = new ModalBuilder() + .setCustomId("addnews") + .setTitle("Write your news.") + .addComponents(firstActionRow, secondActionRow); - newsModal.addComponents(firstActionRow, secondActionRow); await interaction.showModal(newsModal).catch(err => console.error(err)); interaction.client.once("interactionCreate", async i => { if (!i.isModalSubmit()) return; const id = crypto.randomUUID(); - sendNews( + addNews( guild.id, i.fields.getTextInputValue("title"), i.fields.getTextInputValue("body"), @@ -68,7 +74,8 @@ export default class Send { await sendChannelNews(guild, id, interaction).catch(err => console.error(err)); await i.reply({ - embeds: [new EmbedBuilder().setTitle("News sent.").setColor(genColor(100))] + embeds: [new EmbedBuilder().setTitle("News added.").setColor(genColor(100))], + ephemeral: true }); }); } diff --git a/src/commands/news/edit.ts b/src/commands/news/edit.ts index b23a714..48f3a0f 100644 --- a/src/commands/news/edit.ts +++ b/src/commands/news/edit.ts @@ -31,10 +31,11 @@ export default class Edit { } async run(interaction: ChatInputCommandInteraction) { + const guild = interaction.guild!; if ( - !interaction - .guild!.members.cache.get(interaction.user.id)! - .permissions.has(PermissionsBitField.Flags.ManageGuild) + !guild.members.cache + .get(interaction.user.id) + ?.permissions.has(PermissionsBitField.Flags.ManageGuild) ) return await errorEmbed( interaction, @@ -42,41 +43,35 @@ export default class Edit { "You need the **Manage Server** permission." ); - const guild = interaction.guild!; - const id = interaction.options.getString("id", true).trim(); + const id = interaction.options.getString("id")!; const news = get(id); if (!news) return await errorEmbed(interaction, "The specified news don't exist."); - const editModal = new ModalBuilder() - .setCustomId("editnews") - .setTitle(`Edit news: ${news.title}`); - - const titleInput = new TextInputBuilder() - .setCustomId("title") - .setPlaceholder("Title") - .setStyle(TextInputStyle.Short) - .setMaxLength(100) - .setLabel("Title") - .setValue(news.title) - .setRequired(true); + const firstActionRow = new ActionRowBuilder().addComponents( + new TextInputBuilder() + .setCustomId("title") + .setMaxLength(100) + .setStyle(TextInputStyle.Short) + .setLabel("Title") + .setValue(news.title) + .setRequired(true) + ); - const bodyInput = new TextInputBuilder() - .setCustomId("body") - .setPlaceholder("Content (markdown)") - .setMaxLength(4000) - .setStyle(TextInputStyle.Paragraph) - .setLabel("Content (markdown)") - .setValue(news.body) - .setRequired(true); + const secondActionRow = new ActionRowBuilder().addComponents( + new TextInputBuilder() + .setCustomId("body") + .setMaxLength(4000) + .setStyle(TextInputStyle.Paragraph) + .setLabel("Content (supports Markdown)") + .setValue(news.body) + .setRequired(true) + ); - const firstActionRow = new ActionRowBuilder().addComponents( - titleInput - ) as ActionRowBuilder; - const secondActionRow = new ActionRowBuilder().addComponents( - bodyInput - ) as ActionRowBuilder; + const editModal = new ModalBuilder() + .setCustomId("editnews") + .setTitle(`Edit news: ${news.title}`) + .addComponents(firstActionRow, secondActionRow); - editModal.addComponents(firstActionRow, secondActionRow); await interaction.showModal(editModal).catch(err => console.error(err)); interaction.client.once("interactionCreate", async i => { if (!i.isModalSubmit()) return; @@ -86,8 +81,9 @@ export default class Edit { if (role) roleToSend = guild.roles.cache.get(role); const title = i.fields.getTextInputValue("title"); const body = i.fields.getTextInputValue("body"); - const newsEditable = getSetting(guild.id, "news", "edit_original_message"); - if (newsEditable === false) await sendChannelNews(guild, id, interaction, title, body); + + if (!getSetting(guild.id, "news", "edit_original_message")) + await sendChannelNews(guild, id, interaction, title, body); const embed = new EmbedBuilder() .setAuthor({ name: `• ${news.author}`, iconURL: news.authorPFP }) @@ -108,7 +104,8 @@ export default class Edit { updateNews(id, title, body); await interaction.reply({ - embeds: [new EmbedBuilder().setTitle("News edited.").setColor(genColor(100))] + embeds: [new EmbedBuilder().setTitle("News edited.").setColor(genColor(100))], + ephemeral: true }); }); } diff --git a/src/commands/news/remove.ts b/src/commands/news/remove.ts index f293d17..0cb5bee 100644 --- a/src/commands/news/remove.ts +++ b/src/commands/news/remove.ts @@ -19,35 +19,37 @@ export default class Remove { .addStringOption(string => string .setName("id") - .setDescription("The ID of the news. Found in the footer of the news.") + .setDescription("The ID of the news. (found in the footer)") .setRequired(true) ); } async run(interaction: ChatInputCommandInteraction) { const guild = interaction.guild!; - const id = interaction.options.getString("id")!; - const member = guild.members.cache.get(interaction.user.id)!; - - if (!member.permissions.has(PermissionsBitField.Flags.ManageGuild)) + if ( + !guild.members.cache + .get(interaction.user.id) + ?.permissions.has(PermissionsBitField.Flags.ManageGuild) + ) return await errorEmbed( interaction, "You can't execute this command.", "You need the **Manage Server** permission." ); + const id = interaction.options.getString("id")!; const news = get(id); if (!news) return await errorEmbed(interaction, "The specified news don't exist."); - const messageID = news.messageID; const newsChannel = (await guild.channels .fetch((getSetting(guild.id, "news", "channel_id") as string) ?? interaction.channel?.id) .catch(() => null)) as TextChannel; - if (newsChannel) await newsChannel.messages.delete(messageID); + if (newsChannel) await newsChannel.messages.delete(news.messageID); deleteNews(id); await interaction.reply({ - embeds: [new EmbedBuilder().setTitle("News removed.").setColor(genColor(100))] + embeds: [new EmbedBuilder().setTitle("News removed.").setColor(genColor(100))], + ephemeral: true }); } } diff --git a/src/commands/news/view.ts b/src/commands/news/view.ts index 4b1d58a..bc3038f 100644 --- a/src/commands/news/view.ts +++ b/src/commands/news/view.ts @@ -28,11 +28,11 @@ export default class View { const sortedNews = (Object.values(news) as any[])?.sort((a, b) => b.createdAt - a.createdAt); let currentNews = sortedNews[page - 1]; - if (!news || !sortedNews || sortedNews.length == 0) + if (!news || !sortedNews || !sortedNews.length) return await errorEmbed( interaction, "No news found.", - "Admins can add news with the **/news send** command." + "Admins can add news with the **/news add** command." ); if (page > sortedNews.length) page = sortedNews.length; @@ -58,13 +58,20 @@ export default class View { .setStyle(ButtonStyle.Primary) ); - interaction.channel - ?.createMessageComponentCollector({ time: 60000 }) + const reply = await interaction.reply({ embeds: [embed], components: [row] }); + reply + .createMessageComponentCollector({ time: 60000 }) .on("collect", async (i: ButtonInteraction) => { - if (i.user.id !== interaction.user.id) + if (i.message.id != (await reply.fetch()).id) + return await errorEmbed( + i, + "For some reason, this click would've caused the bot to error. Thankfully, this message right here prevents that." + ); + + if (i.user.id != interaction.user.id) return await errorEmbed(i, "You aren't the person who executed this command."); - setTimeout(async () => await interaction.editReply({ components: [] }), 60000); + setTimeout(async () => await i.update({ components: [] }), 60000); switch (i.customId) { case "left": page--; @@ -84,10 +91,7 @@ export default class View { .setFooter({ text: `Page ${page} of ${sortedNews.length} • ID: ${currentNews.id}` }) .setColor(genColor(200)); - await interaction.editReply({ embeds: [embed], components: [row] }); - await i.update({}); + await i.update({ embeds: [embed], components: [row] }); }); - - await interaction.reply({ embeds: [embed], components: [row] }); } } diff --git a/src/commands/serverboard.ts b/src/commands/serverboard.ts index 58b42d7..11f0f49 100644 --- a/src/commands/serverboard.ts +++ b/src/commands/serverboard.ts @@ -4,14 +4,15 @@ import { ButtonInteraction, ButtonStyle, SlashCommandBuilder, - type ChatInputCommandInteraction + type ChatInputCommandInteraction, + type SlashCommandOptionsOnlyBuilder } from "discord.js"; import { listPublicServers } from "../utils/database/settings"; import { errorEmbed } from "../utils/embeds/errorEmbed"; import { serverEmbed } from "../utils/embeds/serverEmbed"; export default class Serverboard { - data: Omit; + data: SlashCommandOptionsOnlyBuilder; constructor() { this.data = new SlashCommandBuilder() .setName("serverboard") @@ -27,14 +28,14 @@ export default class Serverboard { ).sort((a, b) => b.memberCount - a.memberCount); const pages = guildList.length; - if (pages == 0) + if (!pages) return await errorEmbed( interaction, "No public server found.", "By some magical miracle, all the servers using Sokora turned off their visibility. Use /settings serverboard `shown: True` to make your server publicly visible." ); - const argPage = interaction.options.get("page")?.value as number; + const argPage = interaction.options.getNumber("page") as number; let page = (argPage - 1 <= 0 ? 0 : argPage - 1 > pages ? pages - 1 : argPage - 1) || 0; const embed = await serverEmbed({ guild: guildList[page], page: page + 1, pages }); const row = new ActionRowBuilder().addComponents( @@ -48,13 +49,20 @@ export default class Serverboard { .setStyle(ButtonStyle.Primary) ); - interaction.channel - ?.createMessageComponentCollector({ time: 60000 }) + const reply = await interaction.reply({ embeds: [embed], components: [row] }); + reply + .createMessageComponentCollector({ time: 60000 }) .on("collect", async (i: ButtonInteraction) => { - if (i.user.id !== interaction.user.id) + if (i.message.id != (await reply.fetch()).id) + return await errorEmbed( + i, + "For some reason, this click would've caused the bot to error. Thankfully, this message right here prevents that." + ); + + if (i.user.id != interaction.user.id) return await errorEmbed(i, "You aren't the person who executed this command."); - setTimeout(async () => await interaction.editReply({ components: [] }), 60000); + setTimeout(async () => await i.update({ components: [] }), 60000); switch (i.customId) { case "left": page--; @@ -64,10 +72,7 @@ export default class Serverboard { if (page >= pages) page = 0; } - await interaction.editReply({ embeds: [embed], components: [row] }); - await i.update({}); + await i.update({ embeds: [embed], components: [row] }); }); - - await interaction.reply({ embeds: [embed], components: [row] }); } } diff --git a/src/commands/settings.ts b/src/commands/settings.ts index 1c4378a..15f94da 100644 --- a/src/commands/settings.ts +++ b/src/commands/settings.ts @@ -18,7 +18,7 @@ import { import { errorEmbed } from "../utils/embeds/errorEmbed"; export default class Settings { - data: SlashCommandBuilder; //Omit; + data: SlashCommandBuilder; constructor() { this.data = new SlashCommandBuilder() .setName("settings") @@ -90,7 +90,7 @@ export default class Settings { const key = interaction.options.getSubcommand() as keyof typeof settingsDefinition; const values = interaction.options.data[0].options!; console.log(values); - if (values.length == 0) { + if (!values.length) { const embed = new EmbedBuilder().setTitle(`Settings for ${key}`).setColor(genColor(100)); const description: string[] = []; diff --git a/src/commands/user.ts b/src/commands/user.ts index 140176d..6d1e628 100644 --- a/src/commands/user.ts +++ b/src/commands/user.ts @@ -4,9 +4,9 @@ import { ButtonInteraction, ButtonStyle, EmbedBuilder, - SlashCommandOptionsOnlyBuilder, SlashCommandBuilder, - type ChatInputCommandInteraction + type ChatInputCommandInteraction, + type SlashCommandOptionsOnlyBuilder } from "discord.js"; import { genColor } from "../utils/colorGen"; import { getLevel, setLevel } from "../utils/database/levelling"; @@ -25,14 +25,9 @@ export default class User { async run(interaction: ChatInputCommandInteraction) { const guild = interaction.guild!; - const target = guild.members.cache - .filter( - member => - member.user.id === (interaction.options.getUser("user")?.id ?? interaction.user.id) - ) - .map(user => user)[0]!; + const user = interaction.options.getUser("user")!; + const target = guild.members.cache.get(user.id)!; - const selectedUser = await target.user.fetch(); let serverInfo = [`Joined on ****`]; const guildRoles = guild.roles.cache.filter(role => target.roles.cache.has(role.id))!; const memberRoles = [...guildRoles].sort( @@ -40,13 +35,13 @@ export default class User { ); memberRoles.pop(); - if (target.premiumSinceTimestamp != null) + if (target.premiumSinceTimestamp) serverInfo.push(`Boosting since **${target.premiumSinceTimestamp}**`); - if (memberRoles.length !== 0) + if (memberRoles.length) serverInfo.push( `**${guildRoles.filter(role => target.roles.cache.has(role.id)).size! - 1}** ${ - memberRoles.length === 1 ? "role" : "roles" + memberRoles.length == 1 ? "role" : "roles" } • ${memberRoles .slice(0, 5) .map(role => `<@&${role[1].id}>`) @@ -54,24 +49,22 @@ export default class User { ); const embedColor = - selectedUser.hexAccentColor ?? (await imageColor(undefined, target)) ?? genColor(200); + user.hexAccentColor ?? (await imageColor(undefined, target)) ?? genColor(200); let embed = new EmbedBuilder() .setAuthor({ - name: `• ${target.nickname ?? selectedUser.displayName}`, + name: `• ${target.nickname ?? user.displayName}`, iconURL: target.displayAvatarURL() }) .setFields( { name: `<:discord:1266797021126459423> • Discord info`, value: [ - `Username is **${selectedUser.username}**`, + `Username is **${user.username}**`, `Display name is ${ - selectedUser.displayName === selectedUser.username - ? "*not there*" - : `**${selectedUser.displayName}**` + user.displayName == user.username ? "*not there*" : `**${user.displayName}**` }`, - `Created on ****` + `Created on ****` ].join("\n") }, { @@ -83,7 +76,7 @@ export default class User { .setThumbnail(target.displayAvatarURL()!) .setColor(embedColor); - if (!getSetting(`${guild.id}`, "levelling", "enabled") && selectedUser.bot) return; + if (!getSetting(`${guild.id}`, "levelling", "enabled") && user.bot) return; const row = new ActionRowBuilder().addComponents( new ButtonBuilder() .setCustomId("general") @@ -108,20 +101,27 @@ export default class User { "en-US" ); - interaction.channel - ?.createMessageComponentCollector({ time: 60000 }) + const reply = await interaction.reply({ embeds: [embed], components: [row] }); + reply + .createMessageComponentCollector({ time: 60000 }) .on("collect", async (i: ButtonInteraction) => { - if (i.user.id !== interaction.user.id) + if (i.message.id != (await reply.fetch()).id) + return await errorEmbed( + i, + "For some reason, this click would've caused the bot to error. Thankfully, this message right here prevents that." + ); + + if (i.user.id != interaction.user.id) return await errorEmbed(i, "You aren't the person who executed this command."); - setTimeout(async () => await interaction.editReply({ components: [] }), 60000); - i.customId === "general" + setTimeout(async () => await i.update({ components: [] }), 60000); + i.customId == "general" ? row.components[0].setDisabled(true) : row.components[1].setDisabled(true); const levelEmbed = new EmbedBuilder() .setAuthor({ - name: `• ${target.nickname ?? selectedUser.displayName}`, + name: `• ${target.nickname ?? user.displayName}`, iconURL: target.displayAvatarURL() }) .setFields( @@ -149,17 +149,13 @@ export default class User { switch (i.customId) { case "general": row.components[1].setDisabled(false); - await interaction.editReply({ embeds: [embed], components: [row] }); + await i.update({ embeds: [embed], components: [row] }); break; case "level": row.components[0].setDisabled(false); - await interaction.editReply({ embeds: [levelEmbed], components: [row] }); + await i.update({ embeds: [levelEmbed], components: [row] }); break; } - - await i.update({}); }); - - await interaction.reply({ embeds: [embed], components: [row] }); } } diff --git a/src/events/easterEggs/Bread.ts b/src/events/easterEggs/Bread.ts index 0c8e9d0..35595a1 100644 --- a/src/events/easterEggs/Bread.ts +++ b/src/events/easterEggs/Bread.ts @@ -5,11 +5,11 @@ export default class Bread { async run(message: Message) { const breadSplit = message.content.toLowerCase().split("bread"); - if (breadSplit[1] == null) return; + if (!breadSplit[1]) return; if ( ((breadSplit[0].endsWith(" ") || breadSplit[0].endsWith("")) && breadSplit[1].startsWith(" ")) || - message.content.toLowerCase() === "bread" + message.content.toLowerCase() == "bread" ) { if (Math.round(Math.random() * 100) <= 0.25) message.channel.send("https://tenor.com/bOMAb.gif"); diff --git a/src/events/easterEggs/Crazy.ts b/src/events/easterEggs/Crazy.ts index 0c9bf3f..975a8e8 100644 --- a/src/events/easterEggs/Crazy.ts +++ b/src/events/easterEggs/Crazy.ts @@ -4,10 +4,10 @@ export default class Crazy { async run(message: Message) { const crazy = message.content.toLowerCase().split("crazy"); - if (crazy[1] == null) return; + if (!crazy[1]) return; if ( ((crazy[0].endsWith(" ") || crazy[0].endsWith("")) && crazy[1].startsWith(" ")) || - message.content.toLowerCase() === "crazy" + message.content.toLowerCase() == "crazy" ) { await message.channel.send( "Crazy? I was crazy once.\nThey locked me in a room.\nA rubber room.\nA rubber room with rats.\nAnd rats make me crazy.\nCrazy? I was crazy once..." diff --git a/src/events/easterEggs/Fireship.ts b/src/events/easterEggs/Fireship.ts index 2d00d84..1805134 100644 --- a/src/events/easterEggs/Fireship.ts +++ b/src/events/easterEggs/Fireship.ts @@ -6,7 +6,7 @@ export default class FireShip { if ( content.startsWith("this has been") && content.endsWith("in 100 seconds") && - message.content !== "this has been in 100 seconds" + message.content != "this has been in 100 seconds" ) await message.channel.send( "hit the like button and subscribe if you want to see more short videos like this thanks for watching and I will see you in the next one" diff --git a/src/events/guildCreate.ts b/src/events/guildCreate.ts index f1e5db5..5710181 100644 --- a/src/events/guildCreate.ts +++ b/src/events/guildCreate.ts @@ -35,7 +35,7 @@ export default { await new Commands(guild.client).registerCommandsForGuild(guild); for (const key in settingsDefinition) for (const setting in settingsDefinition[key]) { - if (settingsDefinition[key][setting].type !== "LIST") continue; + if (settingsDefinition[key][setting].type != "LIST") continue; if (!getSetting(guild.id, key, setting)) continue; setSetting(guild.id, key, setting, settingsDefinition[key][setting].val); } diff --git a/src/events/guildMemberAdd.ts b/src/events/guildMemberAdd.ts index fce88be..d2f481b 100644 --- a/src/events/guildMemberAdd.ts +++ b/src/events/guildMemberAdd.ts @@ -20,7 +20,7 @@ export default { const user = member.user; const guild = member.guild; const channel = (await member.guild.channels.cache - .find(channel => channel.id === id) + .find(channel => channel.id == id) ?.fetch()) as TextChannel; if (text?.includes("(name)")) text = text.replaceAll("(name)", user.displayName); diff --git a/src/events/guildMemberRemove.ts b/src/events/guildMemberRemove.ts index 1fca228..169b0c3 100644 --- a/src/events/guildMemberRemove.ts +++ b/src/events/guildMemberRemove.ts @@ -20,7 +20,7 @@ export default { const user = member.user; const guild = member.guild; const channel = (await member.guild.channels.cache - .find(channel => channel.id === id) + .find(channel => channel.id == id) ?.fetch()) as TextChannel; if (text?.includes("(name)")) text = text.replaceAll("(name)", user.displayName); diff --git a/src/events/interactionCreate.ts b/src/events/interactionCreate.ts index 05dc5fa..8b08931 100644 --- a/src/events/interactionCreate.ts +++ b/src/events/interactionCreate.ts @@ -41,7 +41,7 @@ export default { if (interaction.isChatInputCommand()) { const command = await getCommand(interaction, interaction.options); if (!command) return; - if (command.deferred === true) await interaction.deferReply(); + if (command.deferred) await interaction.deferReply(); command.run(interaction); } else if (interaction.isAutocomplete()) { const command = await getCommand(interaction, interaction.options); diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts index 20353ef..8ff33e5 100644 --- a/src/events/messageCreate.ts +++ b/src/events/messageCreate.ts @@ -17,7 +17,7 @@ export default { const guild = message.guild!; // Easter egg handler - if (guild.id === "1079612082636472420") { + if (guild.id == "1079612082636472420") { const eventsPath = join(process.cwd(), "src", "events", "easterEggs"); for (const easterEggFile of readdirSync(eventsPath)) @@ -30,7 +30,7 @@ export default { if (!getSetting(guild.id, "levelling", "enabled")) return; const level = getSetting(guild.id, "levelling", "set_level") as string; - if (level != "" && level != null) { + if (level) { const newLevel = kominator(level); setLevel(guild.id, newLevel[0], +newLevel[1], 100 * +newLevel[1]); setSetting(guild.id, "levelling", "set_level", ""); @@ -39,16 +39,16 @@ export default { const blockedChannels = getSetting(guild.id, "levelling", "block_channels") as string; if (blockedChannels != undefined) for (const channelID of kominator(blockedChannels)) - if (message.channelId === channelID) return; + if (message.channelId == channelID) return; // const cooldown = getSetting(guild.id, "levelling", "set_cooldown") as number; let expGain = getSetting(guild.id, "levelling", "set_xp_gain") as number; const multiplier = getSetting(guild.id, "levelling", "add_multiplier") as string; - if (multiplier != null) { + if (multiplier) { const expMultiplier = kominator(multiplier); - if (expMultiplier[1] === "channel") if (message.channelId !== expMultiplier[2]) return; - if (expMultiplier[1] === "role") + if (expMultiplier[1] == "channel") if (message.channelId != expMultiplier[2]) return; + if (expMultiplier[1] == "role") if (!message.member?.roles.cache.has(expMultiplier[2])) return; expGain = expGain * +expMultiplier[0]; diff --git a/src/events/messageDelete.ts b/src/events/messageDelete.ts index cbf9a42..5c16d62 100644 --- a/src/events/messageDelete.ts +++ b/src/events/messageDelete.ts @@ -1,7 +1,7 @@ -// TODO: display the person who deleted the message -import { codeBlock, EmbedBuilder, type Message, type TextChannel, type Channel } from "discord.js"; +import { codeBlock, EmbedBuilder, type Message } from "discord.js"; import { genColor } from "../utils/colorGen"; import { getSetting } from "../utils/database/settings"; +import { logChannel } from "../utils/logChannel"; export default { name: "messageDelete", @@ -11,10 +11,7 @@ export default { if (author.bot) return; const guild = message.guild!; - const logUpdate = getSetting(guild.id, "moderation", "log_messages"); - const logChannel = getSetting(guild.id, "moderation", "channel"); - if (!logUpdate) return; - if (!logChannel) return; + if (!getSetting(guild.id, "moderation", "log_messages")) return; const embed = new EmbedBuilder() .setAuthor({ name: `• ${author.displayName}`, iconURL: author.displayAvatarURL() }) @@ -27,16 +24,7 @@ export default { .setThumbnail(author.displayAvatarURL()) .setColor(genColor(60)); - const channel = await guild.channels.cache - .get(`${logChannel}`) - ?.fetch() - .then((channel: Channel) => { - return channel as TextChannel; - }) - .catch(() => null); - - if (!channel) return; - await channel.send({ embeds: [embed] }); + await logChannel(guild, embed); } } }; diff --git a/src/events/messageUpdate.ts b/src/events/messageUpdate.ts index e9354e1..8284812 100644 --- a/src/events/messageUpdate.ts +++ b/src/events/messageUpdate.ts @@ -1,6 +1,7 @@ -import { codeBlock, EmbedBuilder, type Message, type TextChannel, type Channel } from "discord.js"; +import { codeBlock, EmbedBuilder, type Message } from "discord.js"; import { genColor } from "../utils/colorGen"; import { getSetting } from "../utils/database/settings"; +import { logChannel } from "../utils/logChannel"; export default { name: "messageUpdate", @@ -10,14 +11,11 @@ export default { if (author.bot) return; const guild = oldMessage.guild!; - const logUpdate = getSetting(guild.id, "moderation", "log_messages"); - const logChannel = getSetting(guild.id, "moderation", "channel"); - if (!logUpdate) return; - if (!logChannel) return; + if (!getSetting(guild.id, "moderation", "log_messages")) return; const oldContent = oldMessage.content; const newContent = newMessage.content; - if (oldContent === newContent) return; + if (oldContent == newContent) return; const embed = new EmbedBuilder() .setAuthor({ name: `• ${author.displayName}`, iconURL: author.displayAvatarURL() }) @@ -36,16 +34,7 @@ export default { .setThumbnail(author.displayAvatarURL()) .setColor(genColor(60)); - const channel = await guild.channels.cache - .get(`${logChannel}`) - ?.fetch() - .then((channel: Channel) => { - return channel as TextChannel; - }) - .catch(() => null); - - if (!channel) return; - await channel.send({ embeds: [embed] }); + await logChannel(guild, embed); } } }; diff --git a/src/utils/colorGen.ts b/src/utils/colorGen.ts index f29e83e..3532c55 100644 --- a/src/utils/colorGen.ts +++ b/src/utils/colorGen.ts @@ -32,9 +32,9 @@ export function genRGBColor(r: any, g: any, b: any) { g = g.toString(16); b = b.toString(16); - if (r.length === 1) r = `0${r}`; - if (g.length === 1) g = `0${g}`; - if (b.length === 1) b = `0${b}`; + if (r.length == 1) r = `0${r}`; + if (g.length == 1) g = `0${g}`; + if (b.length == 1) b = `0${b}`; return `#${r}${g}${b}`; } diff --git a/src/utils/database/levelling.ts b/src/utils/database/levelling.ts index fc70580..b61040a 100644 --- a/src/utils/database/levelling.ts +++ b/src/utils/database/levelling.ts @@ -20,11 +20,11 @@ const insertQuery = database.query( export function getLevel(guildID: string, userID: string): [number, number] { const res = getQuery.all(guildID, userID) as TypeOfDefinition[]; - if (res.length == 0) return [0, 0]; + if (!res.length) return [0, 0]; return [res[0].level, res[0].exp]; } export function setLevel(guildID: string | number, userID: string, level: number, exp: number) { - if (getQuery.all(guildID, userID).length != 0) deleteQuery.run(guildID, userID); + if (getQuery.all(guildID, userID).length) deleteQuery.run(guildID, userID); insertQuery.run(guildID, userID, level, exp); } diff --git a/src/utils/database/news.ts b/src/utils/database/news.ts index 1937302..de6eccc 100644 --- a/src/utils/database/news.ts +++ b/src/utils/database/news.ts @@ -24,7 +24,7 @@ const listAllQuery = database.query("SELECT * FROM news WHERE guildID = $1;"); const getIdQuery = database.query("SELECT * FROM news WHERE id = $1;"); const deleteQuery = database.query("DELETE FROM news WHERE id = $1"); -export function sendNews( +export function addNews( guildID: string, title: string, body: string, diff --git a/src/utils/database/settings.ts b/src/utils/database/settings.ts index da89198..dc7d5dd 100644 --- a/src/utils/database/settings.ts +++ b/src/utils/database/settings.ts @@ -109,14 +109,12 @@ export function getSetting( typeof tableDefinition >[]; - if (res.length == 0) return null; + if (!res.length) return null; switch (settingsDefinition[key][setting].type) { case "TEXT": return res[0].value as TypeOfKey; case "BOOL": - return ( - res[0].value != null && res[0].value != "" && res[0].value != "false" ? "true" : "false" - ) as TypeOfKey; + return (res[0].value ? "true" : "false") as TypeOfKey; case "INTEGER": return parseInt(res[0].value) as TypeOfKey; case "LIST": diff --git a/src/utils/embeds/modEmbed.ts b/src/utils/embeds/modEmbed.ts index fb101a5..f1a6d4b 100644 --- a/src/utils/embeds/modEmbed.ts +++ b/src/utils/embeds/modEmbed.ts @@ -3,7 +3,6 @@ import ms from "ms"; import { genColor } from "../colorGen"; import { logChannel } from "../logChannel"; import { errorEmbed } from "./errorEmbed"; -import { getSetting } from "../database/settings"; type Options = { interaction: ChatInputCommandInteraction; @@ -50,10 +49,10 @@ export async function errorCheck( if (!allErrors) return; if (!target) return; - if (target === member) + if (target == member) return await errorEmbed(interaction, `You can't ${action.toLowerCase()} yourself.`); - if (target.id === interaction.client.user.id) + if (target.id == interaction.client.user.id) return await errorEmbed(interaction, `You can't ${action.toLowerCase()} Sokora.`); if (!target.manageable) @@ -71,7 +70,7 @@ export async function errorCheck( ); if (ownerError) { - if (target.id === guild.ownerId) + if (target.id == guild.ownerId) return await errorEmbed( interaction, `You can't ${action.toLowerCase()} ${name}.`, @@ -101,9 +100,6 @@ export async function modEmbed(options: Options, reason?: string | null, date?: await logChannel(guild, embed); await interaction.reply({ embeds: [embed] }); - const dmOrNot = getSetting(guild.id, "moderation", "enabled"); - if (!dmOrNot) return; - const dmChannel = await user.createDM().catch(() => null); if (!dmChannel) return; if (!guild.members.cache.get(user.id)) return; diff --git a/src/utils/embeds/serverEmbed.ts b/src/utils/embeds/serverEmbed.ts index d490d04..591aff9 100644 --- a/src/utils/embeds/serverEmbed.ts +++ b/src/utils/embeds/serverEmbed.ts @@ -31,11 +31,10 @@ export async function serverEmbed(options: Options) { const channels = guild.channels.cache; const channelSizes = { - text: channels.filter( - channel => channel.type === 0 || channel.type === 15 || channel.type === 5 - ).size, - voice: channels.filter(channel => channel.type === 2 || channel.type === 13).size, - categories: channels.filter(channel => channel.type === 4).size + text: channels.filter(channel => channel.type == 0 || channel.type == 15 || channel.type == 5) + .size, + voice: channels.filter(channel => channel.type == 2 || channel.type == 13).size, + categories: channels.filter(channel => channel.type == 4).size }; const generalValues = [ @@ -56,9 +55,9 @@ export async function serverEmbed(options: Options) { if (options.roles) embed.addFields({ - name: `🎭 • ${roles.size - 1} ${roles.size === 1 ? "role" : "roles"}`, + name: `🎭 • ${roles.size - 1} ${roles.size == 1 ? "role" : "roles"}`, value: - roles.size === 1 + roles.size == 1 ? "*None*" : `${sortedRoles .slice(0, 5) @@ -84,12 +83,12 @@ export async function serverEmbed(options: Options) { inline: true }, { - name: `🌟 • ${boostTier == 0 ? "No level" : `Level ${boostTier}`}`, + name: `🌟 • ${!boostTier ? "No level" : `Level ${boostTier}`}`, value: [ `**${boostCount}**${ - boostTier === 0 ? "/2" : boostTier === 1 ? "/7" : boostTier === 2 ? "/14" : "" + boostTier == 0 ? "/2" : boostTier == 1 ? "/7" : boostTier == 2 ? "/14" : "" } boosts`, - `**${boosters.size}** ${boosters.size === 1 ? "booster" : "boosters"}` + `**${boosters.size}** ${boosters.size == 1 ? "booster" : "boosters"}` ].join("\n"), inline: true } diff --git a/src/utils/logChannel.ts b/src/utils/logChannel.ts index 5a5ed42..27e37b3 100644 --- a/src/utils/logChannel.ts +++ b/src/utils/logChannel.ts @@ -21,7 +21,14 @@ export async function logChannel(guild: Guild, embed: EmbedBuilder) { .get(`${logChannel}`) ?.fetch() .then((channel: Channel) => { - if (channel.type != ChannelType.GuildText) return null; + if ( + channel.type != ChannelType.GuildText && + ChannelType.PublicThread && + ChannelType.PrivateThread && + ChannelType.GuildVoice + ) + return null; + return channel as TextChannel; }) .catch(() => null); diff --git a/src/utils/multiReact.ts b/src/utils/multiReact.ts index c7fa763..690c284 100644 --- a/src/utils/multiReact.ts +++ b/src/utils/multiReact.ts @@ -4,14 +4,13 @@ import type { Message } from "discord.js"; * Reacts to a message with multiple emojis. * @param message Message to react to. * @param emojis Emojis that will be used to react. - * @returns Reactions with the provided emojis. */ export async function multiReact(message: Message, ...emojis: string[]) { for (const i of emojis) { - if (typeof i === "object") { + if (typeof i == "object") { await message.react(i); continue; } - for (const reaction of i) if (reaction !== " ") return await message.react(reaction); + for (const reaction of i) if (reaction != " ") await message.react(reaction); } } From e0d0f0c682bc7f7f63ff3e7c312ddfa9c46745a6 Mon Sep 17 00:00:00 2001 From: Serge Date: Wed, 4 Sep 2024 22:01:44 +0500 Subject: [PATCH 083/127] fixed a couple more issues --- src/commands/news/view.ts | 40 ++++++++++++++------------------- src/commands/serverboard.ts | 14 ++++++++---- src/commands/user.ts | 10 ++++++--- src/utils/embeds/serverEmbed.ts | 2 +- src/utils/imageColor.ts | 7 +++--- 5 files changed, 39 insertions(+), 34 deletions(-) diff --git a/src/commands/news/view.ts b/src/commands/news/view.ts index bc3038f..3bbf7ce 100644 --- a/src/commands/news/view.ts +++ b/src/commands/news/view.ts @@ -26,7 +26,6 @@ export default class View { let page = interaction.options.getNumber("page") ?? 1; const news = listAllNews(interaction.guild?.id!); const sortedNews = (Object.values(news) as any[])?.sort((a, b) => b.createdAt - a.createdAt); - let currentNews = sortedNews[page - 1]; if (!news || !sortedNews || !sortedNews.length) return await errorEmbed( @@ -38,14 +37,17 @@ export default class View { if (page > sortedNews.length) page = sortedNews.length; if (page < 1) page = 1; - let embed = new EmbedBuilder() - .setAuthor({ name: `• ${currentNews.author}`, iconURL: currentNews.authorPFP }) - .setTitle(currentNews.title) - .setDescription(currentNews.body) - .setImage(currentNews.imageURL || null) - .setTimestamp(parseInt(currentNews.updatedAt)) - .setFooter({ text: `Page ${page} of ${sortedNews.length} • ID: ${currentNews.id}` }) - .setColor(genColor(200)); + function getEmbed() { + const currentNews = sortedNews[page - 1]; + return new EmbedBuilder() + .setAuthor({ name: `• ${currentNews.author}`, iconURL: currentNews.authorPFP }) + .setTitle(currentNews.title) + .setDescription(currentNews.body) + .setImage(currentNews.imageURL || null) + .setTimestamp(parseInt(currentNews.updatedAt)) + .setFooter({ text: `Page ${page} of ${sortedNews.length} • ID: ${currentNews.id}` }) + .setColor(genColor(200)); + } const row = new ActionRowBuilder().addComponents( new ButtonBuilder() @@ -58,7 +60,7 @@ export default class View { .setStyle(ButtonStyle.Primary) ); - const reply = await interaction.reply({ embeds: [embed], components: [row] }); + const reply = await interaction.reply({ embeds: [getEmbed()], components: [row] }); reply .createMessageComponentCollector({ time: 60000 }) .on("collect", async (i: ButtonInteraction) => { @@ -71,27 +73,19 @@ export default class View { if (i.user.id != interaction.user.id) return await errorEmbed(i, "You aren't the person who executed this command."); - setTimeout(async () => await i.update({ components: [] }), 60000); + setTimeout(async () => await i.editReply({ components: [] }), 60000); switch (i.customId) { case "left": page--; if (page < 1) page = sortedNews.length; + await i.update({ embeds: [getEmbed()], components: [row] }); + break; case "right": page++; if (page > sortedNews.length) page = 1; + await i.update({ embeds: [getEmbed()], components: [row] }); + break; } - - currentNews = sortedNews[page - 1]; - embed = new EmbedBuilder() - .setAuthor({ name: `• ${currentNews.author}`, iconURL: currentNews.authorPFP }) - .setTitle(currentNews.title) - .setDescription(currentNews.body) - .setImage(currentNews.imageURL || null) - .setTimestamp(parseInt(currentNews.updatedAt)) - .setFooter({ text: `Page ${page} of ${sortedNews.length} • ID: ${currentNews.id}` }) - .setColor(genColor(200)); - - await i.update({ embeds: [embed], components: [row] }); }); } } diff --git a/src/commands/serverboard.ts b/src/commands/serverboard.ts index 11f0f49..645f593 100644 --- a/src/commands/serverboard.ts +++ b/src/commands/serverboard.ts @@ -37,7 +37,11 @@ export default class Serverboard { const argPage = interaction.options.getNumber("page") as number; let page = (argPage - 1 <= 0 ? 0 : argPage - 1 > pages ? pages - 1 : argPage - 1) || 0; - const embed = await serverEmbed({ guild: guildList[page], page: page + 1, pages }); + + async function getEmbed() { + return await serverEmbed({ guild: guildList[page], page: page + 1, pages }); + } + const row = new ActionRowBuilder().addComponents( new ButtonBuilder() .setCustomId("left") @@ -49,7 +53,7 @@ export default class Serverboard { .setStyle(ButtonStyle.Primary) ); - const reply = await interaction.reply({ embeds: [embed], components: [row] }); + const reply = await interaction.reply({ embeds: [await getEmbed()], components: [row] }); reply .createMessageComponentCollector({ time: 60000 }) .on("collect", async (i: ButtonInteraction) => { @@ -62,17 +66,19 @@ export default class Serverboard { if (i.user.id != interaction.user.id) return await errorEmbed(i, "You aren't the person who executed this command."); - setTimeout(async () => await i.update({ components: [] }), 60000); + setTimeout(async () => await i.editReply({ components: [] }), 60000); switch (i.customId) { case "left": page--; if (page < 0) page = pages - 1; + break; case "right": page++; if (page >= pages) page = 0; + break; } - await i.update({ embeds: [embed], components: [row] }); + await i.update({ embeds: [await getEmbed()], components: [row] }); }); } } diff --git a/src/commands/user.ts b/src/commands/user.ts index 6d1e628..c296953 100644 --- a/src/commands/user.ts +++ b/src/commands/user.ts @@ -25,9 +25,13 @@ export default class User { async run(interaction: ChatInputCommandInteraction) { const guild = interaction.guild!; - const user = interaction.options.getUser("user")!; - const target = guild.members.cache.get(user.id)!; + const target = guild.members.cache + .filter( + member => member.id == (interaction.options.getUser("user")?.id ?? interaction.user.id) + ) + .map(user => user)[0]; + const user = await target.user.fetch(); let serverInfo = [`Joined on ****`]; const guildRoles = guild.roles.cache.filter(role => target.roles.cache.has(role.id))!; const memberRoles = [...guildRoles].sort( @@ -114,7 +118,7 @@ export default class User { if (i.user.id != interaction.user.id) return await errorEmbed(i, "You aren't the person who executed this command."); - setTimeout(async () => await i.update({ components: [] }), 60000); + setTimeout(async () => await i.editReply({ components: [] }), 60000); i.customId == "general" ? row.components[0].setDisabled(true) : row.components[1].setDisabled(true); diff --git a/src/utils/embeds/serverEmbed.ts b/src/utils/embeds/serverEmbed.ts index 591aff9..4ea3083 100644 --- a/src/utils/embeds/serverEmbed.ts +++ b/src/utils/embeds/serverEmbed.ts @@ -86,7 +86,7 @@ export async function serverEmbed(options: Options) { name: `🌟 • ${!boostTier ? "No level" : `Level ${boostTier}`}`, value: [ `**${boostCount}**${ - boostTier == 0 ? "/2" : boostTier == 1 ? "/7" : boostTier == 2 ? "/14" : "" + !boostTier ? "/2" : boostTier == 1 ? "/7" : boostTier == 2 ? "/14" : "" } boosts`, `**${boosters.size}** ${boosters.size == 1 ? "booster" : "boosters"}` ].join("\n"), diff --git a/src/utils/imageColor.ts b/src/utils/imageColor.ts index 84b9a79..6d37d09 100644 --- a/src/utils/imageColor.ts +++ b/src/utils/imageColor.ts @@ -10,10 +10,11 @@ import { genRGBColor } from "./colorGen"; * @returns The color in HEX. */ export async function imageColor(guild?: Guild, member?: GuildMember) { - const imageBuffer = await ( - await fetch(guild ? guild.iconURL()! : member?.displayAvatarURL()!) - ).arrayBuffer(); + const guildURL = guild?.iconURL(); + const memberURL = member?.displayAvatarURL(); + if (!guildURL || !memberURL) return; + const imageBuffer = await (await fetch(guild ? guildURL : memberURL)).arrayBuffer(); const { r, g, b } = ( await new Vibrant(await sharp(imageBuffer).toFormat("jpg").toBuffer()).getPalette() ).Vibrant!; From 8177c30868bf84adac0a97d3a3402f44a4da3266 Mon Sep 17 00:00:00 2001 From: Golem64 <65229557+Golem642@users.noreply.github.com> Date: Sun, 8 Sep 2024 18:40:37 +0200 Subject: [PATCH 084/127] mdoeraition --- bun.lockb | Bin 57167 -> 60065 bytes package.json | 4 +- src/commands/moderation/ban.ts | 20 ++++---- src/commands/moderation/history.ts | 76 +++++++++++++++++++++++++++++ src/commands/moderation/kick.ts | 20 ++++---- src/commands/moderation/mute.ts | 50 ++++++++++++------- src/commands/moderation/unban.ts | 24 ++++----- src/commands/moderation/unmute.ts | 24 ++++----- src/commands/moderation/warn.ts | 34 +++++++------ src/commands/moderation/warns.ts | 60 ----------------------- src/utils/database/moderation.ts | 15 +++++- src/utils/embeds/modEmbed.ts | 9 ++-- 12 files changed, 193 insertions(+), 143 deletions(-) create mode 100644 src/commands/moderation/history.ts delete mode 100644 src/commands/moderation/warns.ts diff --git a/bun.lockb b/bun.lockb index 80eda75d0774d5a57a9c38b8fc069b1dbf5e966f..eafe73c3c9edcd30a7eaee1c2749da487d4ae362 100755 GIT binary patch delta 3416 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 V$5Ce76+&Yv#7)v=3>Do){{;xkY%Tx* delta 2047 zcmb7Fc~BE)6yFU5lVCVS14PgYND|qG1W3dR;)N0iwT^(=iYSLc5XNAP%5VvIAZmDc zfvCuMQmmRGa){NA;|1bHTkBLV4Iqj~N2|3eeIJAw|LGrn$!~xA&hPEp-EYf(vABKF zVizOYDO%K5-{1XP+0}eUO=@-aVt;J|#dYoJlja7+#yq(FQ!6zf#rjBh)|H~OIy=kE zq?Fhg%}R>mdQy}vMP+^P800jKPqJDUw?=?=JG4JD+oMr;LVfM(SoI1uMfG}9lnv^Q z$nMBSWa4kdcs_EG7;7V+v8E{eXK^GHH5_@JW}p>AD0{5svP+%X$z>D6Tb!@!xsUSB zRF*8SxU%=cEWK>qv#8z8Dg70xBd%#T7s$G;KT5~7wKi8f_!V9@T^X6#6d%lyaf0@J zJkZ(HS;aXi*S>3+?PkAXan0TEQMtR$pEX4s_dH{5;P9H~x|LjFkGqFVr?2rq3$$*uf4mZTX+zsQ+KG1tBfFr?tcPY%0 zOlQx+3`@{EDYzSP1d~u>;d%=0d(=V)G`+im6)b|VuY*`=BH;N1v1&zd6Uk8wJSK>} z1V^QSLZ?ZrQZa19sM}(=i8Mk2JZTVXp#;LDLEK|Fz;Cb~8|J$y>^o5-Yth&%fdE2y zHI)syp;?X@@0%NnC^MfdslpZeevy2m=QrQ@WoUG0&% zw5pgtsl;BsDyte-q^ZxgpOH6WFGFIPUKTj#Om%?x#i$J3%dqnS4ND4IL$rV9QPE&B zM8_U<^?$hkQA9?#;6dPQW#U}tx>HY!T-S}QaJVeIRQERS*7r~rCxE^rRZ)DylxNmg zA6ez>xX;{oHf_v3q1>D|Y*Sr&&fBurr$hi zb^OWV-Ev)3PTXtTkFd`&qew1ZlQC`Cr&4idn5Uw$scKw%+ZXMFVSZ|bwV^nDmk?jC z!CO`gU@VEJO)$FDJq%+hOT-29?bIU^8yit+wiTIeR>&1*8>vO&E}4tk?Sa#!fo$@y zc0pfhg*|y(A0XNg?TGtOeqb8iY%m=dPrG%Xf$Vv|B6cB;BBt!Y14{dTtmMTei|P$) z54N$K$+}w*+Y$MQO^6&s9we8$W)g4i7uIw<&q9!Altg7C3J@8HZ3yDuiWsW88P7>Y z(g(2>B8FBWvBXdEN$w8BPDH<1FxHVG6Cg&r5TBHLp3woWR!?LpTv!J39)D;y&44XkRj{Bdz@YAlp|OXM OwlPrFGZN_D!~X(>rk$_= diff --git a/package.json b/package.json index e22e990..7dd35e0 100644 --- a/package.json +++ b/package.json @@ -12,13 +12,13 @@ "start": "bun ./src/index.ts" }, "dependencies": { - "discord.js": "^14.15.3", + "discord.js": "^14.16.1", "ms": "^2.1.3", "node-vibrant": "^3.2.1-alpha.1", "sharp": "^0.33.5" }, "devDependencies": { - "bun-types": "^1.1.24", + "bun-types": "^1.1.27", "typescript": "^5.5.4" } } diff --git a/src/commands/moderation/ban.ts b/src/commands/moderation/ban.ts index 3ba4a6f..88ba947 100644 --- a/src/commands/moderation/ban.ts +++ b/src/commands/moderation/ban.ts @@ -46,19 +46,19 @@ export default class Ban { }, ms(duration)); } - await errorCheck( + if (await errorCheck( PermissionsBitField.Flags.BanMembers, { interaction, user, action: "Ban" }, { allErrors: true, botError: true, ownerError: true }, "Ban Members" - ); - - const reason = interaction.options.getString("reason"); - await interaction.guild?.members.cache - .get(user.id) - ?.ban({ reason: reason ?? undefined }) - .catch(error => console.error(error)); - - await modEmbed({ interaction, user, action: "Banned", duration }, reason); + ) == null) { + const reason = interaction.options.getString("reason"); + await interaction.guild?.members.cache + .get(user.id) + ?.ban({ reason: reason ?? undefined }) + .catch(error => console.error(error)); + + await modEmbed({ interaction, user, action: "Banned", duration }, reason); + } } } diff --git a/src/commands/moderation/history.ts b/src/commands/moderation/history.ts new file mode 100644 index 0000000..7f96c1d --- /dev/null +++ b/src/commands/moderation/history.ts @@ -0,0 +1,76 @@ +import { + EmbedBuilder, + PermissionsBitField, + SlashCommandSubcommandBuilder, + type ChatInputCommandInteraction +} from "discord.js"; +import { genColor } from "../../utils/colorGen"; +import { getModeration, listUserModeration } from "../../utils/database/moderation"; +import { errorEmbed } from "../../utils/embeds/errorEmbed"; + +const actionsEmojis = { + WARN: "⚠️", + MUTE: "🔇", + KICK: "🦶", + BAN: "🔨" +}; + +export default class History { + data: SlashCommandSubcommandBuilder; + constructor() { + this.data = new SlashCommandSubcommandBuilder() + .setName("history") + .setDescription("Moderation actions history of a user.") // Can be misundertood as the user's history, needs changing + .addUserOption(user => + user.setName("user").setDescription("The user that you want to see.").setRequired(true) + ) + .addStringOption(string => + string.setName("id").setDescription("The ID of a specific moderation action you want to see.") + ); + } + + async run(interaction: ChatInputCommandInteraction) { + const guild = interaction.guild!; + if ( + !guild.members.cache + .get(interaction.user.id) + ?.permissions.has(PermissionsBitField.Flags.ModerateMembers) + ) + return await errorEmbed( + interaction, + "You can't execute this command.", + "You need the **Moderate Members** permission." + ); + + const user = interaction.options.getUser("user")!; + // const warns = listUserModeration(guild.id, user.id, "WARN"); + // const mutes = listUserModeration(guild.id, user.id, "MUTE"); + // const kicks = listUserModeration(guild.id, user.id, "KICK"); + // const bans = listUserModeration(guild.id, user.id, "BAN"); + const actionid = interaction.options.getString("id") + const allactions = actionid ? getModeration(guild.id, actionid) : listUserModeration(guild.id, user.id); + const embed = new EmbedBuilder() + .setAuthor({ name: `• ${user.displayName}`, iconURL: user.displayAvatarURL() }) + .setDescription(`Moderation actions history of ${user.displayName}.`) + .setTitle(`Mod Action history of ${user.displayName}.`) + .setFields( + allactions.length > 0 + ? allactions.map(action => { + return { + name: `${actionsEmojis[action.type]} ${action.type} #${action.id}`, + value: [ + `- __Moderator__: <@${action.moderator}>`, + `- __Reason__: ${action.reason}`, + `- __Date__: ` + ].join("\n") + }; + }) + : [{ name: "No actions", value: "No action has been taken on this user" }] + ) + .setThumbnail(user.displayAvatarURL()) + .setFooter({ text: `User ID: ${user.id}` }) + .setColor(genColor(100)); + + await interaction.reply({ embeds: [embed] }); + } +} diff --git a/src/commands/moderation/kick.ts b/src/commands/moderation/kick.ts index 2af0292..64a20da 100644 --- a/src/commands/moderation/kick.ts +++ b/src/commands/moderation/kick.ts @@ -22,19 +22,19 @@ export default class Kick { async run(interaction: ChatInputCommandInteraction) { const user = interaction.options.getUser("user")!; - await errorCheck( + if (await errorCheck( PermissionsBitField.Flags.KickMembers, { interaction, user, action: "Kick" }, { allErrors: true, botError: true, ownerError: true }, "Kick Members" - ); - - const reason = interaction.options.getString("reason"); - await interaction.guild?.members.cache - .get(user.id) - ?.kick(reason ?? undefined) - .catch(error => console.error(error)); - - await modEmbed({ interaction, user, action: "Kicked" }, reason); + ) == null) { + const reason = interaction.options.getString("reason"); + await interaction.guild?.members.cache + .get(user.id) + ?.kick(reason ?? undefined) + .catch(error => console.error(error)); + + await modEmbed({ interaction, user, action: "Kicked" }, reason); + } } } diff --git a/src/commands/moderation/mute.ts b/src/commands/moderation/mute.ts index e54c920..f297de7 100644 --- a/src/commands/moderation/mute.ts +++ b/src/commands/moderation/mute.ts @@ -6,6 +6,7 @@ import { import ms from "ms"; import { errorEmbed } from "../../utils/embeds/errorEmbed"; import { errorCheck, modEmbed } from "../../utils/embeds/modEmbed"; +import { addModeration } from "../../utils/database/moderation"; export default class Mute { data: SlashCommandSubcommandBuilder; @@ -32,29 +33,44 @@ export default class Mute { const duration = interaction.options.getString("duration")!; const reason = interaction.options.getString("reason"); - await errorCheck( + if (await errorCheck( PermissionsBitField.Flags.ModerateMembers, { interaction, user, action: "Mute" }, { allErrors: true, botError: true, ownerError: true }, "Moderate Members" - ); + ) == null) { - if (!ms(duration) || ms(duration) > ms("28d")) - return await errorEmbed( - interaction, - `You can't mute ${user.displayName}.`, - "The duration is invalid or is above the 28 day limit." - ); - - const time = new Date( - Date.parse(new Date().toISOString()) + Date.parse(new Date(ms(duration)).toISOString()) - ).toISOString(); + if (!ms(duration) || ms(duration) > ms("28d")) + return await errorEmbed( + interaction, + `You can't mute ${user.displayName}.`, + "The duration is invalid or is above the 28 day limit." + ); + + const time = new Date( + Date.parse(new Date().toISOString()) + Date.parse(new Date(ms(duration)).toISOString()) + ).toISOString(); + + await interaction.guild?.members.cache + .get(user.id) + ?.edit({ communicationDisabledUntil: time, reason: reason ?? undefined }) + .catch(error => console.error(error)); - await interaction.guild?.members.cache - .get(user.id) - ?.edit({ communicationDisabledUntil: time, reason: reason ?? undefined }) - .catch(error => console.error(error)); + const guild = interaction.guild!; - await modEmbed({ interaction, user, action: "Muted", duration }, reason); + try { + addModeration( + guild.id, + user.id, + "MUTE", + guild.members.cache.get(interaction.user.id)?.id!, + reason ?? undefined + ); + } catch (error) { + console.error(error); + } + + await modEmbed({ interaction, user, action: "Muted", duration }, reason); + } } } diff --git a/src/commands/moderation/unban.ts b/src/commands/moderation/unban.ts index 49708db..d84c90e 100644 --- a/src/commands/moderation/unban.ts +++ b/src/commands/moderation/unban.ts @@ -31,21 +31,21 @@ export default class Unban { .map(ban => ban.user) .filter(user => user.id == id)[0]!; - await errorCheck( + if (await errorCheck( PermissionsBitField.Flags.BanMembers, { interaction, user: target, action: "Unban" }, { allErrors: false, botError: true, ownerError: true }, "Ban Members" - ); - - if (!target) - return await errorEmbed( - interaction, - "You can't unban this user.", - "The user was never banned." - ); - - await guild.members.unban(id, reason ?? undefined).catch(error => console.error(error)); - await modEmbed({ interaction, user: target, action: "Unbanned" }, reason); + ) == null) { + if (!target) + return await errorEmbed( + interaction, + "You can't unban this user.", + "The user was never banned." + ); + + await guild.members.unban(id, reason ?? undefined).catch(error => console.error(error)); + await modEmbed({ interaction, user: target, action: "Unbanned" }, reason); + } } } diff --git a/src/commands/moderation/unmute.ts b/src/commands/moderation/unmute.ts index 2b354ee..a5d184c 100644 --- a/src/commands/moderation/unmute.ts +++ b/src/commands/moderation/unmute.ts @@ -21,21 +21,21 @@ export default class Unmute { const user = interaction.options.getUser("user")!; const target = interaction.guild?.members.cache.get(user.id)!; - errorCheck( + if (await errorCheck( PermissionsBitField.Flags.ModerateMembers, { interaction, user, action: "Unmute" }, { allErrors: false, botError: true, ownerError: false }, "Moderate Members" - ); - - if (!target.communicationDisabledUntil) - return await errorEmbed( - interaction, - "You can't unmute this user.", - "The user was never muted." - ); - - await target.edit({ communicationDisabledUntil: null }).catch(error => console.error(error)); - await modEmbed({ interaction, user, action: "Unmuted" }, undefined, true); + ) == null) { + if (!target.communicationDisabledUntil) + return await errorEmbed( + interaction, + "You can't unmute this user.", + "The user was never muted." + ); + + await target.edit({ communicationDisabledUntil: null }).catch(error => console.error(error)); + await modEmbed({ interaction, user, action: "Unmuted" }, undefined, true); + } } } diff --git a/src/commands/moderation/warn.ts b/src/commands/moderation/warn.ts index b5dce39..80c69ac 100644 --- a/src/commands/moderation/warn.ts +++ b/src/commands/moderation/warn.ts @@ -17,6 +17,9 @@ export default class Warn { ) .addStringOption(string => string.setName("reason").setDescription("The reason for the warn.") + ) + .addBooleanOption(bool => + bool.setName("show_moderator").setDescription("Inform the warned user of the moderator that took the action. Defaults to false") ); } @@ -24,26 +27,27 @@ export default class Warn { const user = interaction.options.getUser("user")!; const guild = interaction.guild!; const reason = interaction.options.getString("reason"); + const showModerator = interaction.options.getBoolean("show_moderator") ?? false; - await errorCheck( + if (await errorCheck( PermissionsBitField.Flags.ModerateMembers, { interaction, user, action: "Warn" }, { allErrors: true, botError: false, ownerError: true }, "Moderate Members" - ); - - try { - addModeration( - guild.id, - user.id, - "WARN", - guild.members.cache.get(interaction.user.id)?.id!, - reason ?? undefined - ); - } catch (error) { - console.error(error); + ) == null) { + try { + addModeration( + guild.id, + user.id, + "WARN", + guild.members.cache.get(interaction.user.id)?.id!, + reason ?? undefined + ); + } catch (error) { + console.error(error); + } + + await modEmbed({ interaction, user, action: "Warned" }, reason, false, showModerator); } - - await modEmbed({ interaction, user, action: "Warned" }, reason); } } diff --git a/src/commands/moderation/warns.ts b/src/commands/moderation/warns.ts deleted file mode 100644 index db89ca1..0000000 --- a/src/commands/moderation/warns.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { - EmbedBuilder, - PermissionsBitField, - SlashCommandSubcommandBuilder, - type ChatInputCommandInteraction -} from "discord.js"; -import { genColor } from "../../utils/colorGen"; -import { listUserModeration } from "../../utils/database/moderation"; -import { errorEmbed } from "../../utils/embeds/errorEmbed"; - -export default class Warns { - data: SlashCommandSubcommandBuilder; - constructor() { - this.data = new SlashCommandSubcommandBuilder() - .setName("warns") - .setDescription("Warns of a user.") - .addUserOption(user => - user.setName("user").setDescription("The user that you want to see.").setRequired(true) - ); - } - - async run(interaction: ChatInputCommandInteraction) { - const guild = interaction.guild!; - if ( - !guild.members.cache - .get(interaction.user.id) - ?.permissions.has(PermissionsBitField.Flags.ModerateMembers) - ) - return await errorEmbed( - interaction, - "You can't execute this command.", - "You need the **Moderate Members** permission." - ); - - const user = interaction.options.getUser("user")!; - const warns = listUserModeration(guild.id, user.id, "WARN"); - const embed = new EmbedBuilder() - .setAuthor({ name: `• ${user.displayName}`, iconURL: user.displayAvatarURL() }) - .setTitle(`Warns of ${user.displayName}.`) - .setFields( - warns.length > 0 - ? warns.map(warn => { - return { - name: `#${warn.id}`, - value: [ - `**Moderator**: <@${warn.moderator}>`, - `**Reason**: ${warn.reason}`, - `**Date**: ` - ].join("\n") - }; - }) - : [{ name: "No warns", value: "This user has no warns." }] - ) - .setThumbnail(user.displayAvatarURL()) - .setFooter({ text: `User ID: ${user.id}` }) - .setColor(genColor(100)); - - await interaction.reply({ embeds: [embed] }); - } -} diff --git a/src/utils/database/moderation.ts b/src/utils/database/moderation.ts index 2be8e5c..7d71230 100644 --- a/src/utils/database/moderation.ts +++ b/src/utils/database/moderation.ts @@ -21,11 +21,17 @@ const addQuery = database.query( "INSERT INTO moderation (guild, user, type, moderator, reason, public, id, timestamp) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8);" ); const listUserQuery = database.query( + "SELECT * FROM moderation WHERE guild = $1 AND user = $2;" +); +const listUserTypeQuery = database.query( "SELECT * FROM moderation WHERE guild = $1 AND user = $2 AND type = $3;" ); const listModQuery = database.query( "SELECT * FROM moderation WHERE guild = $1 AND moderator = $2;" ); +const getIdQuery = database.query( + "SELECT * FROM moderation WHERE guild = $1 AND id = $2;" +); const removeQuery = database.query("DELETE FROM moderation WHERE guild = $1 AND id = $2"); export function addModeration( @@ -44,9 +50,14 @@ export function addModeration( export function listUserModeration( guildID: number | string, userID: number | string, - type: modType + type?: modType ) { - return listUserQuery.all(guildID, userID, type) as TypeOfDefinition[]; + if (type) return listUserTypeQuery.all(guildID, userID, type) as TypeOfDefinition[]; + return listUserQuery.all(guildID, userID) as TypeOfDefinition[]; +} + +export function getModeration(guildID: number | string, id: string) { + return getIdQuery.all(guildID, id) as TypeOfDefinition[]; } export function listModeratorLog(guildID: number | string, moderator: number | string) { diff --git a/src/utils/embeds/modEmbed.ts b/src/utils/embeds/modEmbed.ts index f1a6d4b..f3ae42e 100644 --- a/src/utils/embeds/modEmbed.ts +++ b/src/utils/embeds/modEmbed.ts @@ -79,12 +79,12 @@ export async function errorCheck( } } -export async function modEmbed(options: Options, reason?: string | null, date?: boolean) { +export async function modEmbed(options: Options, reason?: string | null, date?: boolean, showModerator: boolean = false) { const { interaction, user, action, duration } = options; const guild = interaction.guild!; const name = user.displayName; - const generalValues = [`**Moderator**: ${interaction.user.displayName}`]; + const generalValues = [`**Moderator**: <@${interaction.user.id}>`]; if (duration) generalValues.push(`**Duration**: ${ms(ms(duration), { long: true })}`); if (reason) generalValues.push(`**Reason**: ${reason}`); if (date) generalValues.push(`**Date**: `); @@ -106,7 +106,10 @@ export async function modEmbed(options: Options, reason?: string | null, date?: if (user.bot) return; await dmChannel .send({ - embeds: [embed.setTitle(`You got ${action.toLowerCase()}.`).setColor(genColor(0))] + embeds: [ + embed.setTitle(`You got ${action.toLowerCase()}.`) + .setDescription(generalValues.slice(+!showModerator, generalValues.length).join("\n")) + .setColor(genColor(0))] }) .catch(() => null); } From 6a8e848ba7879a76df47061c68b101226c552aea Mon Sep 17 00:00:00 2001 From: Golem64 <65229557+Golem642@users.noreply.github.com> Date: Sun, 8 Sep 2024 20:04:03 +0200 Subject: [PATCH 085/127] =?UTF-8?q?dm=20welcome=20attempt=20n=C2=B01?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/commands/moderation/ban.ts | 21 ++++++++++++++++++++- src/events/guildMemberAdd.ts | 11 +++++++++++ src/utils/database/settings.ts | 10 +++++++++- 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/commands/moderation/ban.ts b/src/commands/moderation/ban.ts index 88ba947..9fc1d88 100644 --- a/src/commands/moderation/ban.ts +++ b/src/commands/moderation/ban.ts @@ -6,6 +6,7 @@ import { import { errorCheck, modEmbed } from "../../utils/embeds/modEmbed"; import { errorEmbed } from "../../utils/embeds/errorEmbed"; import ms from "ms"; +import { addModeration } from "../../utils/database/moderation"; export default class Ban { data: SlashCommandSubcommandBuilder; @@ -53,12 +54,30 @@ export default class Ban { "Ban Members" ) == null) { const reason = interaction.options.getString("reason"); + const dmChannel = await user.createDM().catch(() => null); + if (dmChannel && !user.bot) { + await modEmbed({ interaction, user, action: "Banned", duration }, reason); + } + await interaction.guild?.members.cache .get(user.id) ?.ban({ reason: reason ?? undefined }) .catch(error => console.error(error)); - await modEmbed({ interaction, user, action: "Banned", duration }, reason); + try { + addModeration( + guild.id, + user.id, + "BAN", + guild.members.cache.get(interaction.user.id)?.id!, + reason ?? undefined + ); + } catch (error) { + console.error(error); + } + + if (!dmChannel || user.bot) + await modEmbed({ interaction, user, action: "Banned", duration }, reason); } } } diff --git a/src/events/guildMemberAdd.ts b/src/events/guildMemberAdd.ts index d2f481b..a44f8eb 100644 --- a/src/events/guildMemberAdd.ts +++ b/src/events/guildMemberAdd.ts @@ -42,6 +42,17 @@ export default { ); await channel.send({ embeds: [embed] }); + + const dmwelcome = getSetting(guildID, "welcome", "join_dm") as boolean; + if (!dmwelcome) return; + // use join_text, the embed should be already cooked + console.log("zioeuhzioeufgzhoifeunho") + const dmChannel = await user.createDM().catch(() => null); + if (!dmChannel) return; + if (user.bot) return; + await dmChannel + .send({ embeds: [embed] }) + .catch(() => null); } } }; diff --git a/src/utils/database/settings.ts b/src/utils/database/settings.ts index dc7d5dd..bd19fa6 100644 --- a/src/utils/database/settings.ts +++ b/src/utils/database/settings.ts @@ -85,7 +85,15 @@ export const settingsDefinition: Record< type: "TEXT", desc: "Text sent when a user leaves. (user) - username, (count) - member count, (servername) - server name." }, - channel: { type: "TEXT", desc: "ID of the channel where welcome messages are sent." } + channel: { type: "TEXT", desc: "ID of the channel where welcome messages are sent." }, + join_dm: { + type: "BOOL", + desc: "Whether to send a custom DM message to the user upon joinning" + }, + dm_text: { + type: "TEXT", + desc: "Text sent in the user's dm when they join the server. Same replacements as leave_text." + } } }; From 6e5ffd1f2dc83449f375553ba7af715531a72eb2 Mon Sep 17 00:00:00 2001 From: Golem64 <65229557+Golem642@users.noreply.github.com> Date: Thu, 12 Sep 2024 20:22:48 +0200 Subject: [PATCH 086/127] make it jollier --- src/commands/moderation/history.ts | 24 +++++++++------ src/events/guildMemberAdd.ts | 48 ++++++++++++++++-------------- 2 files changed, 40 insertions(+), 32 deletions(-) diff --git a/src/commands/moderation/history.ts b/src/commands/moderation/history.ts index 7f96c1d..32c8316 100644 --- a/src/commands/moderation/history.ts +++ b/src/commands/moderation/history.ts @@ -15,17 +15,24 @@ const actionsEmojis = { BAN: "🔨" }; +const nothingMsg = [ + "Nothing to see here...", + "Ayay, no cases on this horizon cap'n !", + "Clean like a whistle !", + "What does 0+0= ?" +] + export default class History { data: SlashCommandSubcommandBuilder; constructor() { this.data = new SlashCommandSubcommandBuilder() .setName("history") - .setDescription("Moderation actions history of a user.") // Can be misundertood as the user's history, needs changing + .setDescription("Moderation cases history of a user.") // Can be misundertood as the user's history, needs changing .addUserOption(user => user.setName("user").setDescription("The user that you want to see.").setRequired(true) ) .addStringOption(string => - string.setName("id").setDescription("The ID of a specific moderation action you want to see.") + string.setName("id").setDescription("The ID of a specific moderation case you want to see.") ); } @@ -51,21 +58,20 @@ export default class History { const allactions = actionid ? getModeration(guild.id, actionid) : listUserModeration(guild.id, user.id); const embed = new EmbedBuilder() .setAuthor({ name: `• ${user.displayName}`, iconURL: user.displayAvatarURL() }) - .setDescription(`Moderation actions history of ${user.displayName}.`) - .setTitle(`Mod Action history of ${user.displayName}.`) + .setTitle(`Moderation cases of ${user.displayName}`) + //.setDescription(`Moderation actions history of ${user.displayName}.`) .setFields( allactions.length > 0 ? allactions.map(action => { return { - name: `${actionsEmojis[action.type]} ${action.type} #${action.id}`, + name: `${actionsEmojis[action.type]} ${action.type} #${action.id}`, // Include durations ? needs to add a db column value: [ - `- __Moderator__: <@${action.moderator}>`, - `- __Reason__: ${action.reason}`, - `- __Date__: ` + `**Reason**: ${action.reason}`, + `-# __Moderator:__ <@${action.moderator}> | ` ].join("\n") }; }) - : [{ name: "No actions", value: "No action has been taken on this user" }] + : [{ name: "💨 " + nothingMsg[Math.floor(Math.random() * nothingMsg.length)], value: "No actions has been taken on this user" }] ) .setThumbnail(user.displayAvatarURL()) .setFooter({ text: `User ID: ${user.id}` }) diff --git a/src/events/guildMemberAdd.ts b/src/events/guildMemberAdd.ts index a44f8eb..070859c 100644 --- a/src/events/guildMemberAdd.ts +++ b/src/events/guildMemberAdd.ts @@ -14,42 +14,44 @@ export default { async run(member: GuildMember) { const guildID = member.guild.id; const id = getSetting(guildID, "welcome", "channel") as string; - if (!id) return; - let text = getSetting(guildID, "welcome", "text") as string; const user = member.user; const guild = member.guild; - const channel = (await member.guild.channels.cache - .find(channel => channel.id == id) - ?.fetch()) as TextChannel; - - if (text?.includes("(name)")) text = text.replaceAll("(name)", user.displayName); - if (text?.includes("(count)")) text = text.replaceAll("(count)", `${guild.memberCount}`); - if (text?.includes("(servername)")) text = text.replaceAll("(servername)", `${guild.name}`); - const avatarURL = member.displayAvatarURL(); const embed = new EmbedBuilder() - .setAuthor({ name: `• ${user.displayName}`, iconURL: avatarURL }) - .setTitle("Welcome!") - .setDescription( - text ?? - `Welcome to ${guild.name}, **${user.displayName}**! Interestingly, you just helped us reach **${guild.memberCount}** members. Enjoy, and have a nice day!` - ) - .setFooter({ text: `User ID: ${member.id}` }) - .setThumbnail(avatarURL) - .setColor( - member.user.hexAccentColor ?? (await imageColor(undefined, member)) ?? genColor(200) - ); + .setAuthor({ name: `• ${user.displayName}`, iconURL: avatarURL }) + .setTitle("Welcome!") + .setFooter({ text: `User ID: ${member.id}` }) + .setThumbnail(avatarURL) + .setColor( + member.user.hexAccentColor ?? (await imageColor(undefined, member)) ?? genColor(200) + ); + + if (id) { + const channel = (await member.guild.channels.cache + .find(channel => channel.id == id) + ?.fetch()) as TextChannel; + + if (text?.includes("(name)")) text = text.replaceAll("(name)", user.displayName); + if (text?.includes("(count)")) text = text.replaceAll("(count)", `${guild.memberCount}`); + if (text?.includes("(servername)")) text = text.replaceAll("(servername)", `${guild.name}`); - await channel.send({ embeds: [embed] }); + await channel.send({ embeds: [embed] }); + } const dmwelcome = getSetting(guildID, "welcome", "join_dm") as boolean; if (!dmwelcome) return; // use join_text, the embed should be already cooked - console.log("zioeuhzioeufgzhoifeunho") const dmChannel = await user.createDM().catch(() => null); if (!dmChannel) return; if (user.bot) return; + let dmtext = getSetting(guildID, "welcome", "dm_text") as string; + if (dmtext) { + if (dmtext?.includes("(name)")) dmtext = dmtext.replaceAll("(name)", user.displayName); + if (dmtext?.includes("(count)")) dmtext = dmtext.replaceAll("(count)", `${guild.memberCount}`); + if (dmtext?.includes("(servername)")) dmtext = dmtext.replaceAll("(servername)", `${guild.name}`); + embed.setDescription(dmtext); + } await dmChannel .send({ embeds: [embed] }) .catch(() => null); From 9db5854eb95ebacb9d8cac083ffdf695a750d2ff Mon Sep 17 00:00:00 2001 From: Golem64 <65229557+Golem642@users.noreply.github.com> Date: Thu, 12 Sep 2024 20:57:50 +0200 Subject: [PATCH 087/127] getting closer to finishing the preview --- src/commands/moderation/ban.ts | 11 +++++------ src/commands/moderation/history.ts | 2 +- src/events/guildMemberAdd.ts | 4 +++- src/utils/database/moderation.ts | 6 ++++-- src/utils/database/settings.ts | 11 +++++++---- 5 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/commands/moderation/ban.ts b/src/commands/moderation/ban.ts index 9fc1d88..f6f3f35 100644 --- a/src/commands/moderation/ban.ts +++ b/src/commands/moderation/ban.ts @@ -18,10 +18,10 @@ export default class Ban { user.setName("user").setDescription("The user that you want to ban.").setRequired(true) ) .addStringOption(string => - string.setName("duration").setDescription("The duration of the ban (e.g 2mo, 1y).") + string.setName("reason").setDescription("The reason for the ban.") ) .addStringOption(string => - string.setName("reason").setDescription("The reason for the ban.") + string.setName("duration").setDescription("The duration of the ban (e.g 2mo, 1y).") ); } @@ -54,14 +54,13 @@ export default class Ban { "Ban Members" ) == null) { const reason = interaction.options.getString("reason"); - const dmChannel = await user.createDM().catch(() => null); + const dmChannel = await guild.members.cache.get(user.id)?.createDM().catch(() => null); if (dmChannel && !user.bot) { await modEmbed({ interaction, user, action: "Banned", duration }, reason); } - await interaction.guild?.members.cache - .get(user.id) - ?.ban({ reason: reason ?? undefined }) + await interaction.guild?.bans + .create(user.id, { reason: reason ?? undefined }) .catch(error => console.error(error)); try { diff --git a/src/commands/moderation/history.ts b/src/commands/moderation/history.ts index 32c8316..feb0900 100644 --- a/src/commands/moderation/history.ts +++ b/src/commands/moderation/history.ts @@ -55,7 +55,7 @@ export default class History { // const kicks = listUserModeration(guild.id, user.id, "KICK"); // const bans = listUserModeration(guild.id, user.id, "BAN"); const actionid = interaction.options.getString("id") - const allactions = actionid ? getModeration(guild.id, actionid) : listUserModeration(guild.id, user.id); + const allactions = actionid ? getModeration(guild.id, user.id, actionid) : listUserModeration(guild.id, user.id); const embed = new EmbedBuilder() .setAuthor({ name: `• ${user.displayName}`, iconURL: user.displayAvatarURL() }) .setTitle(`Moderation cases of ${user.displayName}`) diff --git a/src/events/guildMemberAdd.ts b/src/events/guildMemberAdd.ts index 070859c..2d8d670 100644 --- a/src/events/guildMemberAdd.ts +++ b/src/events/guildMemberAdd.ts @@ -14,7 +14,7 @@ export default { async run(member: GuildMember) { const guildID = member.guild.id; const id = getSetting(guildID, "welcome", "channel") as string; - let text = getSetting(guildID, "welcome", "text") as string; + let text = getSetting(guildID, "welcome", "join_text") as string; const user = member.user; const guild = member.guild; const avatarURL = member.displayAvatarURL(); @@ -35,6 +35,8 @@ export default { if (text?.includes("(name)")) text = text.replaceAll("(name)", user.displayName); if (text?.includes("(count)")) text = text.replaceAll("(count)", `${guild.memberCount}`); if (text?.includes("(servername)")) text = text.replaceAll("(servername)", `${guild.name}`); + + embed.setDescription(text) await channel.send({ embeds: [embed] }); } diff --git a/src/utils/database/moderation.ts b/src/utils/database/moderation.ts index 7d71230..1b04977 100644 --- a/src/utils/database/moderation.ts +++ b/src/utils/database/moderation.ts @@ -56,8 +56,10 @@ export function listUserModeration( return listUserQuery.all(guildID, userID) as TypeOfDefinition[]; } -export function getModeration(guildID: number | string, id: string) { - return getIdQuery.all(guildID, id) as TypeOfDefinition[]; +export function getModeration(guildID: number | string, userID: number | string, id: string) { + var thecase = getIdQuery.all(guildID, id) as TypeOfDefinition[]; + if (thecase[0].user == userID) return thecase; + return []; } export function listModeratorLog(guildID: number | string, moderator: number | string) { diff --git a/src/utils/database/settings.ts b/src/utils/database/settings.ts index bd19fa6..62406bc 100644 --- a/src/utils/database/settings.ts +++ b/src/utils/database/settings.ts @@ -79,21 +79,24 @@ export const settingsDefinition: Record< welcome: { join_text: { type: "TEXT", - desc: "Text sent when a user joins. (user) - username, (count) - member count, (servername) - server name." + desc: "Text sent when a user joins. (user) - username, (count) - member count, (servername) - server name.", + val: "Welcome to (servername), (name)! Interestingly, you just helped us reach (count) members. Enjoy, and have a nice day!" }, leave_text: { type: "TEXT", - desc: "Text sent when a user leaves. (user) - username, (count) - member count, (servername) - server name." + desc: "Text sent when a user leaves. (user) - username, (count) - member count, (servername) - server name.", + val: "(name) has left the server 😥" }, channel: { type: "TEXT", desc: "ID of the channel where welcome messages are sent." }, join_dm: { type: "BOOL", - desc: "Whether to send a custom DM message to the user upon joinning" + desc: "Whether to send a custom DM message to the user upon joinning", + val: false }, dm_text: { type: "TEXT", desc: "Text sent in the user's dm when they join the server. Same replacements as leave_text." - } + } // dm_text is already join_text by default if nothing is supplied, see guildMemberAdd.ts } }; From aee8daf0a5ccd3ffb2b83050a49a204b04dbc32a Mon Sep 17 00:00:00 2001 From: Golem64 <65229557+Golem642@users.noreply.github.com> Date: Fri, 13 Sep 2024 01:08:08 +0200 Subject: [PATCH 088/127] no more red fixed every error i came across, also added the outsideError check for errorEmbed to check if a member is in the server or not --- package-lock.json | 2049 ++++++++++++++++++++++++++++ package.json | 1 + src/commands/moderation/ban.ts | 4 +- src/commands/moderation/delwarn.ts | 2 +- src/commands/moderation/history.ts | 11 +- src/commands/moderation/kick.ts | 16 +- src/commands/moderation/mute.ts | 2 +- src/commands/moderation/unban.ts | 2 +- src/commands/moderation/unmute.ts | 2 +- src/commands/moderation/warn.ts | 2 +- src/events/easterEggs/AmericaYa.ts | 4 +- src/events/easterEggs/Bread.ts | 4 +- src/events/easterEggs/Crazy.ts | 4 +- src/events/easterEggs/Fan.ts | 4 +- src/events/easterEggs/Fire.ts | 4 +- src/events/easterEggs/Fireship.ts | 4 +- src/events/easterEggs/Honk.ts | 4 +- src/events/easterEggs/WhoPinged.ts | 4 +- src/utils/embeds/modEmbed.ts | 10 + 19 files changed, 2104 insertions(+), 29 deletions(-) create mode 100644 package-lock.json diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..4721b97 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2049 @@ +{ + "name": "sokora", + "version": "0.1.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "sokora", + "version": "0.1.0", + "dependencies": { + "discord.js": "^14.16.1", + "ms": "^2.1.3", + "node-vibrant": "^3.2.1-alpha.1", + "sharp": "^0.33.5" + }, + "devDependencies": { + "@types/ms": "^0.7.34", + "bun-types": "^1.1.27", + "typescript": "^5.5.4" + } + }, + "node_modules/@babel/runtime": { + "version": "7.23.8", + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@discordjs/builders": { + "version": "1.9.0", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/formatters": "^0.5.0", + "@discordjs/util": "^1.1.1", + "@sapphire/shapeshift": "^4.0.0", + "discord-api-types": "0.37.97", + "fast-deep-equal": "^3.1.3", + "ts-mixer": "^6.0.4", + "tslib": "^2.6.3" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/collection": { + "version": "1.5.3", + "license": "Apache-2.0", + "engines": { + "node": ">=16.11.0" + } + }, + "node_modules/@discordjs/formatters": { + "version": "0.5.0", + "license": "Apache-2.0", + "dependencies": { + "discord-api-types": "0.37.97" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/rest": { + "version": "2.4.0", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/collection": "^2.1.1", + "@discordjs/util": "^1.1.1", + "@sapphire/async-queue": "^1.5.3", + "@sapphire/snowflake": "^3.5.3", + "@vladfrangu/async_event_emitter": "^2.4.6", + "discord-api-types": "0.37.97", + "magic-bytes.js": "^1.10.0", + "tslib": "^2.6.3", + "undici": "6.19.8" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/rest/node_modules/@discordjs/collection": { + "version": "2.1.1", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/rest/node_modules/@sapphire/async-queue": { + "version": "1.5.3", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@discordjs/rest/node_modules/@vladfrangu/async_event_emitter": { + "version": "2.4.6", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@discordjs/util": { + "version": "1.1.1", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/ws": { + "version": "1.1.1", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/collection": "^2.1.0", + "@discordjs/rest": "^2.3.0", + "@discordjs/util": "^1.1.0", + "@sapphire/async-queue": "^1.5.2", + "@types/ws": "^8.5.10", + "@vladfrangu/async_event_emitter": "^2.2.4", + "discord-api-types": "0.37.83", + "tslib": "^2.6.2", + "ws": "^8.16.0" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/ws/node_modules/@discordjs/collection": { + "version": "2.1.0", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/ws/node_modules/@discordjs/rest": { + "version": "2.3.0", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/collection": "^2.1.0", + "@discordjs/util": "^1.1.0", + "@sapphire/async-queue": "^1.5.2", + "@sapphire/snowflake": "^3.5.3", + "@vladfrangu/async_event_emitter": "^2.2.4", + "discord-api-types": "0.37.83", + "magic-bytes.js": "^1.10.0", + "tslib": "^2.6.2", + "undici": "6.13.0" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/ws/node_modules/@discordjs/rest/node_modules/undici": { + "version": "6.13.0", + "license": "MIT", + "engines": { + "node": ">=18.0" + } + }, + "node_modules/@discordjs/ws/node_modules/@discordjs/util": { + "version": "1.1.0", + "license": "Apache-2.0", + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/ws/node_modules/discord-api-types": { + "version": "0.37.83", + "license": "MIT" + }, + "node_modules/@discordjs/ws/node_modules/tslib": { + "version": "2.6.2", + "license": "0BSD" + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.33.5", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jimp/bmp": { + "version": "0.16.13", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/utils": "^0.16.13", + "bmp-js": "^0.1.0" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/core": { + "version": "0.16.13", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/utils": "^0.16.13", + "any-base": "^1.1.0", + "buffer": "^5.2.0", + "exif-parser": "^0.1.12", + "file-type": "^16.5.4", + "load-bmfont": "^1.3.1", + "mkdirp": "^0.5.1", + "phin": "^2.9.1", + "pixelmatch": "^4.0.2", + "tinycolor2": "^1.4.1" + } + }, + "node_modules/@jimp/custom": { + "version": "0.16.13", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/core": "^0.16.13" + } + }, + "node_modules/@jimp/gif": { + "version": "0.16.13", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/utils": "^0.16.13", + "gifwrap": "^0.9.2", + "omggif": "^1.0.9" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/jpeg": { + "version": "0.16.13", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/utils": "^0.16.13", + "jpeg-js": "^0.4.2" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-resize": { + "version": "0.16.13", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/utils": "^0.16.13" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/png": { + "version": "0.16.13", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/utils": "^0.16.13", + "pngjs": "^3.3.3" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/tiff": { + "version": "0.16.13", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "utif": "^2.0.1" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/types": { + "version": "0.16.13", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/bmp": "^0.16.13", + "@jimp/gif": "^0.16.13", + "@jimp/jpeg": "^0.16.13", + "@jimp/png": "^0.16.13", + "@jimp/tiff": "^0.16.13", + "timm": "^1.6.1" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/utils": { + "version": "0.16.13", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "regenerator-runtime": "^0.13.3" + } + }, + "node_modules/@jimp/utils/node_modules/regenerator-runtime": { + "version": "0.13.11", + "license": "MIT" + }, + "node_modules/@sapphire/async-queue": { + "version": "1.5.2", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@sapphire/shapeshift": { + "version": "4.0.0", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=v16" + } + }, + "node_modules/@sapphire/snowflake": { + "version": "3.5.3", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "0.7.34", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", + "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==", + "dev": true + }, + "node_modules/@types/node": { + "version": "20.12.12", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/ws": { + "version": "8.5.10", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ws/node_modules/@types/node": { + "version": "20.11.5", + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@vibrant/color": { + "version": "3.2.1-alpha.1", + "license": "MIT" + }, + "node_modules/@vibrant/core": { + "version": "3.2.1-alpha.1", + "license": "MIT", + "dependencies": { + "@vibrant/color": "^3.2.1-alpha.1", + "@vibrant/generator": "^3.2.1-alpha.1", + "@vibrant/image": "^3.2.1-alpha.1", + "@vibrant/quantizer": "^3.2.1-alpha.1", + "@vibrant/types": "^3.2.1-alpha.1", + "@vibrant/worker": "^3.2.1-alpha.1" + } + }, + "node_modules/@vibrant/generator": { + "version": "3.2.1-alpha.1", + "license": "MIT", + "dependencies": { + "@vibrant/color": "^3.2.1-alpha.1", + "@vibrant/types": "^3.2.1-alpha.1" + } + }, + "node_modules/@vibrant/generator-default": { + "version": "3.2.1-alpha.1", + "license": "MIT", + "dependencies": { + "@vibrant/color": "^3.2.1-alpha.1", + "@vibrant/generator": "^3.2.1-alpha.1" + } + }, + "node_modules/@vibrant/image": { + "version": "3.2.1-alpha.1", + "license": "MIT", + "dependencies": { + "@vibrant/color": "^3.2.1-alpha.1", + "@vibrant/types": "^3.2.1-alpha.1" + } + }, + "node_modules/@vibrant/image-browser": { + "version": "3.2.1-alpha.1", + "license": "MIT", + "dependencies": { + "@vibrant/image": "^3.2.1-alpha.1" + } + }, + "node_modules/@vibrant/image-node": { + "version": "3.2.1-alpha.1", + "license": "MIT", + "dependencies": { + "@jimp/custom": "^0.16.1", + "@jimp/plugin-resize": "^0.16.1", + "@jimp/types": "^0.16.1", + "@vibrant/image": "^3.2.1-alpha.1" + } + }, + "node_modules/@vibrant/quantizer": { + "version": "3.2.1-alpha.1", + "license": "MIT", + "dependencies": { + "@vibrant/color": "^3.2.1-alpha.1", + "@vibrant/image": "^3.2.1-alpha.1", + "@vibrant/types": "^3.2.1-alpha.1" + } + }, + "node_modules/@vibrant/quantizer-mmcq": { + "version": "3.2.1-alpha.1", + "license": "MIT", + "dependencies": { + "@vibrant/color": "^3.2.1-alpha.1", + "@vibrant/image": "^3.2.1-alpha.1", + "@vibrant/quantizer": "^3.2.1-alpha.1" + } + }, + "node_modules/@vibrant/types": { + "version": "3.2.1-alpha.1", + "license": "MIT" + }, + "node_modules/@vibrant/worker": { + "version": "3.2.1-alpha.1", + "license": "MIT", + "dependencies": { + "@vibrant/types": "^3.2.1-alpha.1" + } + }, + "node_modules/@vladfrangu/async_event_emitter": { + "version": "2.2.4", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/any-base": { + "version": "1.1.0", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bmp-js": { + "version": "0.1.0", + "license": "MIT" + }, + "node_modules/buffer": { + "version": "5.7.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-equal": { + "version": "0.0.1", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/bun-types": { + "version": "1.1.27", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "~20.12.8", + "@types/ws": "~8.5.10" + } + }, + "node_modules/call-bind": { + "version": "1.0.5", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/color": { + "version": "4.2.3", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/define-data-property": { + "version": "1.1.1", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/detect-libc": { + "version": "2.0.3", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/discord-api-types": { + "version": "0.37.97", + "license": "MIT" + }, + "node_modules/discord.js": { + "version": "14.16.1", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/builders": "^1.9.0", + "@discordjs/collection": "1.5.3", + "@discordjs/formatters": "^0.5.0", + "@discordjs/rest": "^2.4.0", + "@discordjs/util": "^1.1.1", + "@discordjs/ws": "1.1.1", + "@sapphire/snowflake": "3.5.3", + "discord-api-types": "0.37.97", + "fast-deep-equal": "3.1.3", + "lodash.snakecase": "4.1.1", + "tslib": "^2.6.3", + "undici": "6.19.8" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/dom-walk": { + "version": "0.1.2" + }, + "node_modules/exif-parser": { + "version": "0.1.12" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "license": "MIT" + }, + "node_modules/file-type": { + "version": "16.5.4", + "license": "MIT", + "dependencies": { + "readable-web-to-node-stream": "^3.0.0", + "strtok3": "^6.2.4", + "token-types": "^4.1.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.2", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gifwrap": { + "version": "0.9.4", + "license": "MIT", + "dependencies": { + "image-q": "^4.0.0", + "omggif": "^1.0.10" + } + }, + "node_modules/global": { + "version": "4.4.0", + "license": "MIT", + "dependencies": { + "min-document": "^2.19.0", + "process": "^0.11.10" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/image-q": { + "version": "4.0.0", + "license": "MIT", + "dependencies": { + "@types/node": "16.9.1" + } + }, + "node_modules/image-q/node_modules/@types/node": { + "version": "16.9.1", + "license": "MIT" + }, + "node_modules/inherits": { + "version": "2.0.4", + "license": "ISC" + }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "license": "MIT" + }, + "node_modules/is-function": { + "version": "1.0.2", + "license": "MIT" + }, + "node_modules/jpeg-js": { + "version": "0.4.4", + "license": "BSD-3-Clause" + }, + "node_modules/load-bmfont": { + "version": "1.4.1", + "license": "MIT", + "dependencies": { + "buffer-equal": "0.0.1", + "mime": "^1.3.4", + "parse-bmfont-ascii": "^1.0.3", + "parse-bmfont-binary": "^1.0.5", + "parse-bmfont-xml": "^1.1.4", + "phin": "^2.9.1", + "xhr": "^2.0.1", + "xtend": "^4.0.0" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "license": "MIT" + }, + "node_modules/lodash.snakecase": { + "version": "4.1.1", + "license": "MIT" + }, + "node_modules/magic-bytes.js": { + "version": "1.10.0", + "license": "MIT" + }, + "node_modules/mime": { + "version": "1.6.0", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/min-document": { + "version": "2.19.0", + "dependencies": { + "dom-walk": "^0.1.0" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "license": "MIT" + }, + "node_modules/node-vibrant": { + "version": "3.2.1-alpha.1", + "license": "MIT", + "dependencies": { + "@types/node": "^10.12.18", + "@vibrant/core": "^3.2.1-alpha.1", + "@vibrant/generator-default": "^3.2.1-alpha.1", + "@vibrant/image-browser": "^3.2.1-alpha.1", + "@vibrant/image-node": "^3.2.1-alpha.1", + "@vibrant/quantizer-mmcq": "^3.2.1-alpha.1", + "url": "^0.11.0" + } + }, + "node_modules/node-vibrant/node_modules/@types/node": { + "version": "10.17.60", + "license": "MIT" + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/omggif": { + "version": "1.0.10", + "license": "MIT" + }, + "node_modules/pako": { + "version": "1.0.11", + "license": "(MIT AND Zlib)" + }, + "node_modules/parse-bmfont-ascii": { + "version": "1.0.6", + "license": "MIT" + }, + "node_modules/parse-bmfont-binary": { + "version": "1.0.6", + "license": "MIT" + }, + "node_modules/parse-bmfont-xml": { + "version": "1.1.4", + "license": "MIT", + "dependencies": { + "xml-parse-from-string": "^1.0.0", + "xml2js": "^0.4.5" + } + }, + "node_modules/parse-headers": { + "version": "2.0.5", + "license": "MIT" + }, + "node_modules/peek-readable": { + "version": "4.1.0", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/phin": { + "version": "2.9.3", + "license": "MIT" + }, + "node_modules/pixelmatch": { + "version": "4.0.2", + "license": "ISC", + "dependencies": { + "pngjs": "^3.0.0" + }, + "bin": { + "pixelmatch": "bin/pixelmatch" + } + }, + "node_modules/pngjs": { + "version": "3.4.0", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/process": { + "version": "0.11.10", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/punycode": { + "version": "1.4.1", + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.11.2", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readable-web-to-node-stream": { + "version": "3.0.2", + "license": "MIT", + "dependencies": { + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "license": "MIT" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.3.0", + "license": "ISC" + }, + "node_modules/semver": { + "version": "7.6.3", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-function-length": { + "version": "1.2.0", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.1", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.2", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/sharp": { + "version": "0.33.5", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" + } + }, + "node_modules/side-channel": { + "version": "1.0.4", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strtok3": { + "version": "6.3.0", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "peek-readable": "^4.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/timm": { + "version": "1.7.1", + "license": "MIT" + }, + "node_modules/tinycolor2": { + "version": "1.6.0", + "license": "MIT" + }, + "node_modules/token-types": { + "version": "4.2.1", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/ts-mixer": { + "version": "6.0.4", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.7.0", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.5.4", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici": { + "version": "6.19.8", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "license": "MIT" + }, + "node_modules/url": { + "version": "0.11.3", + "license": "MIT", + "dependencies": { + "punycode": "^1.4.1", + "qs": "^6.11.2" + } + }, + "node_modules/utif": { + "version": "2.0.1", + "license": "MIT", + "dependencies": { + "pako": "^1.0.5" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "license": "MIT" + }, + "node_modules/ws": { + "version": "8.17.0", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xhr": { + "version": "2.6.0", + "license": "MIT", + "dependencies": { + "global": "~4.4.0", + "is-function": "^1.0.1", + "parse-headers": "^2.0.0", + "xtend": "^4.0.0" + } + }, + "node_modules/xml-parse-from-string": { + "version": "1.0.1", + "license": "MIT" + }, + "node_modules/xml2js": { + "version": "0.4.23", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + } + }, + "dependencies": { + "@babel/runtime": { + "version": "7.23.8", + "requires": { + "regenerator-runtime": "^0.14.0" + } + }, + "@discordjs/builders": { + "version": "1.9.0", + "requires": { + "@discordjs/formatters": "^0.5.0", + "@discordjs/util": "^1.1.1", + "@sapphire/shapeshift": "^4.0.0", + "discord-api-types": "0.37.97", + "fast-deep-equal": "^3.1.3", + "ts-mixer": "^6.0.4", + "tslib": "^2.6.3" + } + }, + "@discordjs/collection": { + "version": "1.5.3" + }, + "@discordjs/formatters": { + "version": "0.5.0", + "requires": { + "discord-api-types": "0.37.97" + } + }, + "@discordjs/rest": { + "version": "2.4.0", + "requires": { + "@discordjs/collection": "^2.1.1", + "@discordjs/util": "^1.1.1", + "@sapphire/async-queue": "^1.5.3", + "@sapphire/snowflake": "^3.5.3", + "@vladfrangu/async_event_emitter": "^2.4.6", + "discord-api-types": "0.37.97", + "magic-bytes.js": "^1.10.0", + "tslib": "^2.6.3", + "undici": "6.19.8" + }, + "dependencies": { + "@discordjs/collection": { + "version": "2.1.1" + }, + "@sapphire/async-queue": { + "version": "1.5.3" + }, + "@vladfrangu/async_event_emitter": { + "version": "2.4.6" + } + } + }, + "@discordjs/util": { + "version": "1.1.1" + }, + "@discordjs/ws": { + "version": "1.1.1", + "requires": { + "@discordjs/collection": "^2.1.0", + "@discordjs/rest": "^2.3.0", + "@discordjs/util": "^1.1.0", + "@sapphire/async-queue": "^1.5.2", + "@types/ws": "^8.5.10", + "@vladfrangu/async_event_emitter": "^2.2.4", + "discord-api-types": "0.37.83", + "tslib": "^2.6.2", + "ws": "^8.16.0" + }, + "dependencies": { + "@discordjs/collection": { + "version": "2.1.0" + }, + "@discordjs/rest": { + "version": "2.3.0", + "requires": { + "@discordjs/collection": "^2.1.0", + "@discordjs/util": "^1.1.0", + "@sapphire/async-queue": "^1.5.2", + "@sapphire/snowflake": "^3.5.3", + "@vladfrangu/async_event_emitter": "^2.2.4", + "discord-api-types": "0.37.83", + "magic-bytes.js": "^1.10.0", + "tslib": "^2.6.2", + "undici": "6.13.0" + }, + "dependencies": { + "undici": { + "version": "6.13.0" + } + } + }, + "@discordjs/util": { + "version": "1.1.0" + }, + "discord-api-types": { + "version": "0.37.83" + }, + "tslib": { + "version": "2.6.2" + } + } + }, + "@img/sharp-win32-x64": { + "version": "0.33.5", + "optional": true + }, + "@jimp/bmp": { + "version": "0.16.13", + "requires": { + "@babel/runtime": "^7.7.2", + "@jimp/utils": "^0.16.13", + "bmp-js": "^0.1.0" + } + }, + "@jimp/core": { + "version": "0.16.13", + "requires": { + "@babel/runtime": "^7.7.2", + "@jimp/utils": "^0.16.13", + "any-base": "^1.1.0", + "buffer": "^5.2.0", + "exif-parser": "^0.1.12", + "file-type": "^16.5.4", + "load-bmfont": "^1.3.1", + "mkdirp": "^0.5.1", + "phin": "^2.9.1", + "pixelmatch": "^4.0.2", + "tinycolor2": "^1.4.1" + } + }, + "@jimp/custom": { + "version": "0.16.13", + "requires": { + "@babel/runtime": "^7.7.2", + "@jimp/core": "^0.16.13" + } + }, + "@jimp/gif": { + "version": "0.16.13", + "requires": { + "@babel/runtime": "^7.7.2", + "@jimp/utils": "^0.16.13", + "gifwrap": "^0.9.2", + "omggif": "^1.0.9" + } + }, + "@jimp/jpeg": { + "version": "0.16.13", + "requires": { + "@babel/runtime": "^7.7.2", + "@jimp/utils": "^0.16.13", + "jpeg-js": "^0.4.2" + } + }, + "@jimp/plugin-resize": { + "version": "0.16.13", + "requires": { + "@babel/runtime": "^7.7.2", + "@jimp/utils": "^0.16.13" + } + }, + "@jimp/png": { + "version": "0.16.13", + "requires": { + "@babel/runtime": "^7.7.2", + "@jimp/utils": "^0.16.13", + "pngjs": "^3.3.3" + } + }, + "@jimp/tiff": { + "version": "0.16.13", + "requires": { + "@babel/runtime": "^7.7.2", + "utif": "^2.0.1" + } + }, + "@jimp/types": { + "version": "0.16.13", + "requires": { + "@babel/runtime": "^7.7.2", + "@jimp/bmp": "^0.16.13", + "@jimp/gif": "^0.16.13", + "@jimp/jpeg": "^0.16.13", + "@jimp/png": "^0.16.13", + "@jimp/tiff": "^0.16.13", + "timm": "^1.6.1" + } + }, + "@jimp/utils": { + "version": "0.16.13", + "requires": { + "@babel/runtime": "^7.7.2", + "regenerator-runtime": "^0.13.3" + }, + "dependencies": { + "regenerator-runtime": { + "version": "0.13.11" + } + } + }, + "@sapphire/async-queue": { + "version": "1.5.2" + }, + "@sapphire/shapeshift": { + "version": "4.0.0", + "requires": { + "fast-deep-equal": "^3.1.3", + "lodash": "^4.17.21" + } + }, + "@sapphire/snowflake": { + "version": "3.5.3" + }, + "@tokenizer/token": { + "version": "0.3.0" + }, + "@types/ms": { + "version": "0.7.34", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", + "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==", + "dev": true + }, + "@types/node": { + "version": "20.12.12", + "dev": true, + "requires": { + "undici-types": "~5.26.4" + } + }, + "@types/ws": { + "version": "8.5.10", + "requires": { + "@types/node": "*" + }, + "dependencies": { + "@types/node": { + "version": "20.11.5", + "requires": { + "undici-types": "~5.26.4" + } + } + } + }, + "@vibrant/color": { + "version": "3.2.1-alpha.1" + }, + "@vibrant/core": { + "version": "3.2.1-alpha.1", + "requires": { + "@vibrant/color": "^3.2.1-alpha.1", + "@vibrant/generator": "^3.2.1-alpha.1", + "@vibrant/image": "^3.2.1-alpha.1", + "@vibrant/quantizer": "^3.2.1-alpha.1", + "@vibrant/types": "^3.2.1-alpha.1", + "@vibrant/worker": "^3.2.1-alpha.1" + } + }, + "@vibrant/generator": { + "version": "3.2.1-alpha.1", + "requires": { + "@vibrant/color": "^3.2.1-alpha.1", + "@vibrant/types": "^3.2.1-alpha.1" + } + }, + "@vibrant/generator-default": { + "version": "3.2.1-alpha.1", + "requires": { + "@vibrant/color": "^3.2.1-alpha.1", + "@vibrant/generator": "^3.2.1-alpha.1" + } + }, + "@vibrant/image": { + "version": "3.2.1-alpha.1", + "requires": { + "@vibrant/color": "^3.2.1-alpha.1", + "@vibrant/types": "^3.2.1-alpha.1" + } + }, + "@vibrant/image-browser": { + "version": "3.2.1-alpha.1", + "requires": { + "@vibrant/image": "^3.2.1-alpha.1" + } + }, + "@vibrant/image-node": { + "version": "3.2.1-alpha.1", + "requires": { + "@jimp/custom": "^0.16.1", + "@jimp/plugin-resize": "^0.16.1", + "@jimp/types": "^0.16.1", + "@vibrant/image": "^3.2.1-alpha.1" + } + }, + "@vibrant/quantizer": { + "version": "3.2.1-alpha.1", + "requires": { + "@vibrant/color": "^3.2.1-alpha.1", + "@vibrant/image": "^3.2.1-alpha.1", + "@vibrant/types": "^3.2.1-alpha.1" + } + }, + "@vibrant/quantizer-mmcq": { + "version": "3.2.1-alpha.1", + "requires": { + "@vibrant/color": "^3.2.1-alpha.1", + "@vibrant/image": "^3.2.1-alpha.1", + "@vibrant/quantizer": "^3.2.1-alpha.1" + } + }, + "@vibrant/types": { + "version": "3.2.1-alpha.1" + }, + "@vibrant/worker": { + "version": "3.2.1-alpha.1", + "requires": { + "@vibrant/types": "^3.2.1-alpha.1" + } + }, + "@vladfrangu/async_event_emitter": { + "version": "2.2.4" + }, + "any-base": { + "version": "1.1.0" + }, + "base64-js": { + "version": "1.5.1" + }, + "bmp-js": { + "version": "0.1.0" + }, + "buffer": { + "version": "5.7.1", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "buffer-equal": { + "version": "0.0.1" + }, + "bun-types": { + "version": "1.1.27", + "dev": true, + "requires": { + "@types/node": "~20.12.8", + "@types/ws": "~8.5.10" + } + }, + "call-bind": { + "version": "1.0.5", + "requires": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + } + }, + "color": { + "version": "4.2.3", + "requires": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + } + }, + "color-convert": { + "version": "2.0.1", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4" + }, + "color-string": { + "version": "1.9.1", + "requires": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "define-data-property": { + "version": "1.1.1", + "requires": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + } + }, + "detect-libc": { + "version": "2.0.3" + }, + "discord-api-types": { + "version": "0.37.97" + }, + "discord.js": { + "version": "14.16.1", + "requires": { + "@discordjs/builders": "^1.9.0", + "@discordjs/collection": "1.5.3", + "@discordjs/formatters": "^0.5.0", + "@discordjs/rest": "^2.4.0", + "@discordjs/util": "^1.1.1", + "@discordjs/ws": "1.1.1", + "@sapphire/snowflake": "3.5.3", + "discord-api-types": "0.37.97", + "fast-deep-equal": "3.1.3", + "lodash.snakecase": "4.1.1", + "tslib": "^2.6.3", + "undici": "6.19.8" + } + }, + "dom-walk": { + "version": "0.1.2" + }, + "exif-parser": { + "version": "0.1.12" + }, + "fast-deep-equal": { + "version": "3.1.3" + }, + "file-type": { + "version": "16.5.4", + "requires": { + "readable-web-to-node-stream": "^3.0.0", + "strtok3": "^6.2.4", + "token-types": "^4.1.1" + } + }, + "function-bind": { + "version": "1.1.2" + }, + "get-intrinsic": { + "version": "1.2.2", + "requires": { + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + } + }, + "gifwrap": { + "version": "0.9.4", + "requires": { + "image-q": "^4.0.0", + "omggif": "^1.0.10" + } + }, + "global": { + "version": "4.4.0", + "requires": { + "min-document": "^2.19.0", + "process": "^0.11.10" + } + }, + "gopd": { + "version": "1.0.1", + "requires": { + "get-intrinsic": "^1.1.3" + } + }, + "has-property-descriptors": { + "version": "1.0.1", + "requires": { + "get-intrinsic": "^1.2.2" + } + }, + "has-proto": { + "version": "1.0.1" + }, + "has-symbols": { + "version": "1.0.3" + }, + "hasown": { + "version": "2.0.0", + "requires": { + "function-bind": "^1.1.2" + } + }, + "ieee754": { + "version": "1.2.1" + }, + "image-q": { + "version": "4.0.0", + "requires": { + "@types/node": "16.9.1" + }, + "dependencies": { + "@types/node": { + "version": "16.9.1" + } + } + }, + "inherits": { + "version": "2.0.4" + }, + "is-arrayish": { + "version": "0.3.2" + }, + "is-function": { + "version": "1.0.2" + }, + "jpeg-js": { + "version": "0.4.4" + }, + "load-bmfont": { + "version": "1.4.1", + "requires": { + "buffer-equal": "0.0.1", + "mime": "^1.3.4", + "parse-bmfont-ascii": "^1.0.3", + "parse-bmfont-binary": "^1.0.5", + "parse-bmfont-xml": "^1.1.4", + "phin": "^2.9.1", + "xhr": "^2.0.1", + "xtend": "^4.0.0" + } + }, + "lodash": { + "version": "4.17.21" + }, + "lodash.snakecase": { + "version": "4.1.1" + }, + "magic-bytes.js": { + "version": "1.10.0" + }, + "mime": { + "version": "1.6.0" + }, + "min-document": { + "version": "2.19.0", + "requires": { + "dom-walk": "^0.1.0" + } + }, + "minimist": { + "version": "1.2.8" + }, + "mkdirp": { + "version": "0.5.6", + "requires": { + "minimist": "^1.2.6" + } + }, + "ms": { + "version": "2.1.3" + }, + "node-vibrant": { + "version": "3.2.1-alpha.1", + "requires": { + "@types/node": "^10.12.18", + "@vibrant/core": "^3.2.1-alpha.1", + "@vibrant/generator-default": "^3.2.1-alpha.1", + "@vibrant/image-browser": "^3.2.1-alpha.1", + "@vibrant/image-node": "^3.2.1-alpha.1", + "@vibrant/quantizer-mmcq": "^3.2.1-alpha.1", + "url": "^0.11.0" + }, + "dependencies": { + "@types/node": { + "version": "10.17.60" + } + } + }, + "object-inspect": { + "version": "1.13.1" + }, + "omggif": { + "version": "1.0.10" + }, + "pako": { + "version": "1.0.11" + }, + "parse-bmfont-ascii": { + "version": "1.0.6" + }, + "parse-bmfont-binary": { + "version": "1.0.6" + }, + "parse-bmfont-xml": { + "version": "1.1.4", + "requires": { + "xml-parse-from-string": "^1.0.0", + "xml2js": "^0.4.5" + } + }, + "parse-headers": { + "version": "2.0.5" + }, + "peek-readable": { + "version": "4.1.0" + }, + "phin": { + "version": "2.9.3" + }, + "pixelmatch": { + "version": "4.0.2", + "requires": { + "pngjs": "^3.0.0" + } + }, + "pngjs": { + "version": "3.4.0" + }, + "process": { + "version": "0.11.10" + }, + "punycode": { + "version": "1.4.1" + }, + "qs": { + "version": "6.11.2", + "requires": { + "side-channel": "^1.0.4" + } + }, + "readable-stream": { + "version": "3.6.2", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "readable-web-to-node-stream": { + "version": "3.0.2", + "requires": { + "readable-stream": "^3.6.0" + } + }, + "regenerator-runtime": { + "version": "0.14.1" + }, + "safe-buffer": { + "version": "5.2.1" + }, + "sax": { + "version": "1.3.0" + }, + "semver": { + "version": "7.6.3" + }, + "set-function-length": { + "version": "1.2.0", + "requires": { + "define-data-property": "^1.1.1", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.2", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.1" + } + }, + "sharp": { + "version": "0.33.5", + "requires": { + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5", + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.3" + } + }, + "side-channel": { + "version": "1.0.4", + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "simple-swizzle": { + "version": "0.2.2", + "requires": { + "is-arrayish": "^0.3.1" + } + }, + "string_decoder": { + "version": "1.3.0", + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "strtok3": { + "version": "6.3.0", + "requires": { + "@tokenizer/token": "^0.3.0", + "peek-readable": "^4.1.0" + } + }, + "timm": { + "version": "1.7.1" + }, + "tinycolor2": { + "version": "1.6.0" + }, + "token-types": { + "version": "4.2.1", + "requires": { + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + } + }, + "ts-mixer": { + "version": "6.0.4" + }, + "tslib": { + "version": "2.7.0" + }, + "typescript": { + "version": "5.5.4", + "dev": true + }, + "undici": { + "version": "6.19.8" + }, + "undici-types": { + "version": "5.26.5" + }, + "url": { + "version": "0.11.3", + "requires": { + "punycode": "^1.4.1", + "qs": "^6.11.2" + } + }, + "utif": { + "version": "2.0.1", + "requires": { + "pako": "^1.0.5" + } + }, + "util-deprecate": { + "version": "1.0.2" + }, + "ws": { + "version": "8.17.0", + "requires": {} + }, + "xhr": { + "version": "2.6.0", + "requires": { + "global": "~4.4.0", + "is-function": "^1.0.1", + "parse-headers": "^2.0.0", + "xtend": "^4.0.0" + } + }, + "xml-parse-from-string": { + "version": "1.0.1" + }, + "xml2js": { + "version": "0.4.23", + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + } + }, + "xmlbuilder": { + "version": "11.0.1" + }, + "xtend": { + "version": "4.0.2" + } + } +} diff --git a/package.json b/package.json index 7dd35e0..6b1d389 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "sharp": "^0.33.5" }, "devDependencies": { + "@types/ms": "^0.7.34", "bun-types": "^1.1.27", "typescript": "^5.5.4" } diff --git a/src/commands/moderation/ban.ts b/src/commands/moderation/ban.ts index f6f3f35..b376544 100644 --- a/src/commands/moderation/ban.ts +++ b/src/commands/moderation/ban.ts @@ -29,6 +29,7 @@ export default class Ban { const user = interaction.options.getUser("user")!; const guild = interaction.guild!; const duration = interaction.options.getString("duration"); + const reason = interaction.options.getString("reason"); if (duration) { if (!ms(duration)) @@ -50,10 +51,9 @@ export default class Ban { if (await errorCheck( PermissionsBitField.Flags.BanMembers, { interaction, user, action: "Ban" }, - { allErrors: true, botError: true, ownerError: true }, + { allErrors: true, botError: true, ownerError: true, outsideError: false }, "Ban Members" ) == null) { - const reason = interaction.options.getString("reason"); const dmChannel = await guild.members.cache.get(user.id)?.createDM().catch(() => null); if (dmChannel && !user.bot) { await modEmbed({ interaction, user, action: "Banned", duration }, reason); diff --git a/src/commands/moderation/delwarn.ts b/src/commands/moderation/delwarn.ts index f9dface..eaa1997 100644 --- a/src/commands/moderation/delwarn.ts +++ b/src/commands/moderation/delwarn.ts @@ -39,7 +39,7 @@ export default class Delwarn { await errorCheck( PermissionsBitField.Flags.ModerateMembers, { interaction, user, action: "Remove a warn" }, - { allErrors: true, botError: false, ownerError: false }, + { allErrors: true, botError: false, ownerError: false, outsideError: false }, "Moderate Members" ); diff --git a/src/commands/moderation/history.ts b/src/commands/moderation/history.ts index feb0900..c397b5e 100644 --- a/src/commands/moderation/history.ts +++ b/src/commands/moderation/history.ts @@ -8,11 +8,11 @@ import { genColor } from "../../utils/colorGen"; import { getModeration, listUserModeration } from "../../utils/database/moderation"; import { errorEmbed } from "../../utils/embeds/errorEmbed"; -const actionsEmojis = { +const actionsEmojis: { [key: string]: string } = { WARN: "⚠️", MUTE: "🔇", - KICK: "🦶", - BAN: "🔨" + KICK: "📤", // looks better than 🦶 + BAN: "🔨" // or 🚫 ? }; const nothingMsg = [ @@ -54,7 +54,8 @@ export default class History { // const mutes = listUserModeration(guild.id, user.id, "MUTE"); // const kicks = listUserModeration(guild.id, user.id, "KICK"); // const bans = listUserModeration(guild.id, user.id, "BAN"); - const actionid = interaction.options.getString("id") + let actionid = interaction.options.getString("id") + if (actionid && actionid?.startsWith("#")) actionid = actionid.slice(1); const allactions = actionid ? getModeration(guild.id, user.id, actionid) : listUserModeration(guild.id, user.id); const embed = new EmbedBuilder() .setAuthor({ name: `• ${user.displayName}`, iconURL: user.displayAvatarURL() }) @@ -67,7 +68,7 @@ export default class History { name: `${actionsEmojis[action.type]} ${action.type} #${action.id}`, // Include durations ? needs to add a db column value: [ `**Reason**: ${action.reason}`, - `-# __Moderator:__ <@${action.moderator}> | ` + `-# __Moderator:__ <@${action.moderator}> | ` ].join("\n") }; }) diff --git a/src/commands/moderation/kick.ts b/src/commands/moderation/kick.ts index 64a20da..0f7f056 100644 --- a/src/commands/moderation/kick.ts +++ b/src/commands/moderation/kick.ts @@ -4,6 +4,7 @@ import { type ChatInputCommandInteraction } from "discord.js"; import { errorCheck, modEmbed } from "../../utils/embeds/modEmbed"; +import { addModeration } from "../../utils/database/moderation"; export default class Kick { data: SlashCommandSubcommandBuilder; @@ -21,11 +22,12 @@ export default class Kick { async run(interaction: ChatInputCommandInteraction) { const user = interaction.options.getUser("user")!; + const guild = interaction.guild!; if (await errorCheck( PermissionsBitField.Flags.KickMembers, { interaction, user, action: "Kick" }, - { allErrors: true, botError: true, ownerError: true }, + { allErrors: true, botError: true, ownerError: true, outsideError: true }, "Kick Members" ) == null) { const reason = interaction.options.getString("reason"); @@ -34,6 +36,18 @@ export default class Kick { ?.kick(reason ?? undefined) .catch(error => console.error(error)); + try { + addModeration( + guild.id, + user.id, + "KICK", + guild.members.cache.get(interaction.user.id)?.id!, + reason ?? undefined + ); + } catch (error) { + console.error(error); + } + await modEmbed({ interaction, user, action: "Kicked" }, reason); } } diff --git a/src/commands/moderation/mute.ts b/src/commands/moderation/mute.ts index f297de7..ecba4d3 100644 --- a/src/commands/moderation/mute.ts +++ b/src/commands/moderation/mute.ts @@ -36,7 +36,7 @@ export default class Mute { if (await errorCheck( PermissionsBitField.Flags.ModerateMembers, { interaction, user, action: "Mute" }, - { allErrors: true, botError: true, ownerError: true }, + { allErrors: true, botError: true, ownerError: true, outsideError: false }, // If someone comes back after being kicked but the mods want to make sure they calm down before speaking again "Moderate Members" ) == null) { diff --git a/src/commands/moderation/unban.ts b/src/commands/moderation/unban.ts index d84c90e..49d1a5b 100644 --- a/src/commands/moderation/unban.ts +++ b/src/commands/moderation/unban.ts @@ -34,7 +34,7 @@ export default class Unban { if (await errorCheck( PermissionsBitField.Flags.BanMembers, { interaction, user: target, action: "Unban" }, - { allErrors: false, botError: true, ownerError: true }, + { allErrors: false, botError: true, ownerError: true, outsideError: false }, "Ban Members" ) == null) { if (!target) diff --git a/src/commands/moderation/unmute.ts b/src/commands/moderation/unmute.ts index a5d184c..9432e08 100644 --- a/src/commands/moderation/unmute.ts +++ b/src/commands/moderation/unmute.ts @@ -24,7 +24,7 @@ export default class Unmute { if (await errorCheck( PermissionsBitField.Flags.ModerateMembers, { interaction, user, action: "Unmute" }, - { allErrors: false, botError: true, ownerError: false }, + { allErrors: false, botError: true, ownerError: false, outsideError: false }, "Moderate Members" ) == null) { if (!target.communicationDisabledUntil) diff --git a/src/commands/moderation/warn.ts b/src/commands/moderation/warn.ts index 80c69ac..1ac73dd 100644 --- a/src/commands/moderation/warn.ts +++ b/src/commands/moderation/warn.ts @@ -32,7 +32,7 @@ export default class Warn { if (await errorCheck( PermissionsBitField.Flags.ModerateMembers, { interaction, user, action: "Warn" }, - { allErrors: true, botError: false, ownerError: true }, + { allErrors: true, botError: false, ownerError: true, outsideError: true }, "Moderate Members" ) == null) { try { diff --git a/src/events/easterEggs/AmericaYa.ts b/src/events/easterEggs/AmericaYa.ts index f632037..0c6ad19 100644 --- a/src/events/easterEggs/AmericaYa.ts +++ b/src/events/easterEggs/AmericaYa.ts @@ -1,4 +1,4 @@ -import type { Message } from "discord.js"; +import type { Message, TextChannel } from "discord.js"; import { randomise } from "../../utils/randomise"; export default class AmericaYa { @@ -9,6 +9,6 @@ export default class AmericaYa { "https://tenor.com/view/america-ya-gif-15374592095658975433" ]); - await message.channel.send(response); + await (message.channel as TextChannel).send(response); } } diff --git a/src/events/easterEggs/Bread.ts b/src/events/easterEggs/Bread.ts index 35595a1..c63307e 100644 --- a/src/events/easterEggs/Bread.ts +++ b/src/events/easterEggs/Bread.ts @@ -1,4 +1,4 @@ -import type { Message } from "discord.js"; +import type { Message, TextChannel } from "discord.js"; import { multiReact } from "../../utils/multiReact"; export default class Bread { @@ -12,7 +12,7 @@ export default class Bread { message.content.toLowerCase() == "bread" ) { if (Math.round(Math.random() * 100) <= 0.25) - message.channel.send("https://tenor.com/bOMAb.gif"); + (message.channel as TextChannel).send("https://tenor.com/bOMAb.gif"); else await multiReact(message, "🍞🇧🇷🇪🇦🇩👍"); } } diff --git a/src/events/easterEggs/Crazy.ts b/src/events/easterEggs/Crazy.ts index 975a8e8..1f3804b 100644 --- a/src/events/easterEggs/Crazy.ts +++ b/src/events/easterEggs/Crazy.ts @@ -1,4 +1,4 @@ -import type { Message } from "discord.js"; +import type { Message, TextChannel } from "discord.js"; export default class Crazy { async run(message: Message) { @@ -9,7 +9,7 @@ export default class Crazy { ((crazy[0].endsWith(" ") || crazy[0].endsWith("")) && crazy[1].startsWith(" ")) || message.content.toLowerCase() == "crazy" ) { - await message.channel.send( + await (message.channel as TextChannel).send( "Crazy? I was crazy once.\nThey locked me in a room.\nA rubber room.\nA rubber room with rats.\nAnd rats make me crazy.\nCrazy? I was crazy once..." ); } diff --git a/src/events/easterEggs/Fan.ts b/src/events/easterEggs/Fan.ts index 0af9c3b..868a962 100644 --- a/src/events/easterEggs/Fan.ts +++ b/src/events/easterEggs/Fan.ts @@ -1,4 +1,4 @@ -import type { Message } from "discord.js"; +import type { Message, TextChannel } from "discord.js"; import { randomise } from "../../utils/randomise"; export default class Fan { @@ -10,6 +10,6 @@ export default class Fan { "https://tenor.com/view/below-deck-im-your-biggest-fan-biggest-fan-kate-kate-chastain-gif-15861715" ]); - await message.channel.send(gifs); + await (message.channel as TextChannel).send(gifs); } } diff --git a/src/events/easterEggs/Fire.ts b/src/events/easterEggs/Fire.ts index baf13a4..5288a3f 100644 --- a/src/events/easterEggs/Fire.ts +++ b/src/events/easterEggs/Fire.ts @@ -1,4 +1,4 @@ -import type { Message } from "discord.js"; +import type { Message, TextChannel } from "discord.js"; import { randomise } from "../../utils/randomise"; export default class Fire { @@ -11,6 +11,6 @@ export default class Fire { "https://tenor.com/view/fire-in-the-hole-gif-14799466830322850291" ]); - await message.channel.send(gifs); + await (message.channel as TextChannel).send(gifs); } } diff --git a/src/events/easterEggs/Fireship.ts b/src/events/easterEggs/Fireship.ts index 1805134..034588e 100644 --- a/src/events/easterEggs/Fireship.ts +++ b/src/events/easterEggs/Fireship.ts @@ -1,4 +1,4 @@ -import type { Message } from "discord.js"; +import type { Message, TextChannel } from "discord.js"; export default class FireShip { async run(message: Message) { @@ -8,7 +8,7 @@ export default class FireShip { content.endsWith("in 100 seconds") && message.content != "this has been in 100 seconds" ) - await message.channel.send( + await (message.channel as TextChannel).send( "hit the like button and subscribe if you want to see more short videos like this thanks for watching and I will see you in the next one" ); } diff --git a/src/events/easterEggs/Honk.ts b/src/events/easterEggs/Honk.ts index 744f072..89ee767 100644 --- a/src/events/easterEggs/Honk.ts +++ b/src/events/easterEggs/Honk.ts @@ -1,9 +1,9 @@ -import type { Message } from "discord.js"; +import type { Message, TextChannel } from "discord.js"; export default class Honk { async run(message: Message) { const honks = ["hnok", "hokn", "hkon", "onk", "hon", "honhk", "hhonk", "honkk"]; if (!honks.includes(message.content.toLowerCase())) return; - message.channel.send("https://tenor.com/bW8sm.gif"); + (message.channel as TextChannel).send("https://tenor.com/bW8sm.gif"); } } diff --git a/src/events/easterEggs/WhoPinged.ts b/src/events/easterEggs/WhoPinged.ts index 22fb762..021ffac 100644 --- a/src/events/easterEggs/WhoPinged.ts +++ b/src/events/easterEggs/WhoPinged.ts @@ -1,4 +1,4 @@ -import type { Message } from "discord.js"; +import type { Message, TextChannel } from "discord.js"; import { randomise } from "../../utils/randomise"; export default class WhoPinged { @@ -15,6 +15,6 @@ export default class WhoPinged { "https://tenor.com/view/yakuza-kazuma-kiryu-pissed-off-gif-14586175" ]); - await message.channel.send(gifs); + await (message.channel as TextChannel).send(gifs); } } diff --git a/src/utils/embeds/modEmbed.ts b/src/utils/embeds/modEmbed.ts index f3ae42e..94f0a96 100644 --- a/src/utils/embeds/modEmbed.ts +++ b/src/utils/embeds/modEmbed.ts @@ -15,6 +15,7 @@ type ErrorOptions = { allErrors: boolean; botError: boolean; ownerError: boolean; + outsideError: boolean; }; export async function errorCheck( @@ -77,6 +78,15 @@ export async function errorCheck( "The member owns the server." ); } + + if (ownerError) { + if (!await guild.members.fetch(user.id).then(() => true).catch(() => false)) + return await errorEmbed( + interaction, + `You can't ${action.toLowerCase()} ${name}.`, + "This user isn't in this server." + ); + } } export async function modEmbed(options: Options, reason?: string | null, date?: boolean, showModerator: boolean = false) { From 2a62f4566210fa4649003ee494d79048a5520d4b Mon Sep 17 00:00:00 2001 From: Golem64 <65229557+Golem642@users.noreply.github.com> Date: Fri, 13 Sep 2024 01:12:52 +0200 Subject: [PATCH 089/127] woops just fixing don't mind me --- src/utils/embeds/modEmbed.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/embeds/modEmbed.ts b/src/utils/embeds/modEmbed.ts index 94f0a96..e63a0dd 100644 --- a/src/utils/embeds/modEmbed.ts +++ b/src/utils/embeds/modEmbed.ts @@ -25,7 +25,7 @@ export async function errorCheck( permissionAction: string ) { const { interaction, user, action } = options; - const { allErrors, botError, ownerError } = errorOptions; + const { allErrors, botError, ownerError, outsideError } = errorOptions; const guild = interaction.guild!; const members = guild.members.cache!; const member = members.get(interaction.user.id)!; @@ -79,7 +79,7 @@ export async function errorCheck( ); } - if (ownerError) { + if (outsideError) { if (!await guild.members.fetch(user.id).then(() => true).catch(() => false)) return await errorEmbed( interaction, From 739e48933e21f750a6a4e849808ee47fe021ec0c Mon Sep 17 00:00:00 2001 From: iakzs Date: Fri, 13 Sep 2024 14:24:46 -0300 Subject: [PATCH 090/127] something to test fr --- src/commands/leaderboard.ts | 113 ++++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 src/commands/leaderboard.ts diff --git a/src/commands/leaderboard.ts b/src/commands/leaderboard.ts new file mode 100644 index 0000000..c66f297 --- /dev/null +++ b/src/commands/leaderboard.ts @@ -0,0 +1,113 @@ +import { + ActionRowBuilder, + ButtonBuilder, + ButtonInteraction, + ButtonStyle, + SlashCommandBuilder, + ChatInputCommandInteraction, + EmbedBuilder, +} from 'discord.js'; +import { getGuildLeaderboard } from '../utils/database/levelling'; +import { errorEmbed } from '../utils/embeds/errorEmbed'; + +export default class Leaderboard { + data: SlashCommandBuilder; + + constructor() { + this.data = new SlashCommandBuilder() + .setName('leaderboard') + .setDescription('Displays the guild leaderboard.') + .addNumberOption((option) => + option.setName('page').setDescription('Page number to display.') + ); + } + + async run(interaction: ChatInputCommandInteraction) { + const guildID = interaction.guild?.id; + + if (!guildID) { + return errorEmbed(interaction, 'This command can only be used in a server.'); + } + + const leaderboardData = getGuildLeaderboard(guildID); + + if (!leaderboardData.length) { + return errorEmbed( + interaction, + 'No data found.', + 'There is no levelling data for this server yet.' + ); + } + + leaderboardData.sort((a, b) => b.exp - a.exp); + + const totalPages = Math.ceil(leaderboardData.length / 5); + let page = interaction.options.getNumber('page') || 1; + page = Math.max(1, Math.min(page, totalPages)); + + const generateEmbed = async () => { + const start = (page - 1) * 5; + const end = start + 5; + const pageData = leaderboardData.slice(start, end); + + const embed = new EmbedBuilder() + .setTitle(`Leaderboard - Page ${page}/${totalPages}`) + .setColor('#0099ff'); + + for (let i = 0; i < pageData.length; i++) { + const userData = pageData[i]; + const user = await interaction.client.users.fetch(userData.user); + embed.addFields({ + name: `${start + i + 1}. ${user.tag}`, + value: `Level: ${userData.level} | EXP: ${userData.exp}`, + }); + } + + return embed; + }; + + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId('left') + .setEmoji('⬅️') + .setStyle(ButtonStyle.Primary), + new ButtonBuilder() + .setCustomId('right') + .setEmoji('➡️') + .setStyle(ButtonStyle.Primary) + ); + + const reply = await interaction.reply({ + embeds: [await generateEmbed()], + components: totalPages > 1 ? [row] : [], + fetchReply: true, + }); + + if (totalPages > 1) { + const collector = reply.createMessageComponentCollector({ + time: 60000, + }); + + collector.on('collect', async (i: ButtonInteraction) => { + if (i.user.id !== interaction.user.id) { + return errorEmbed(i, 'You are not the author of this command.'); + } + + if (i.customId === 'left') { + page = page > 1 ? page - 1 : totalPages; + } else if (i.customId === 'right') { + page = page < totalPages ? page + 1 : 1; + } + + await i.update({ + embeds: [await generateEmbed()], + components: [row], + }); + }); + + collector.on('end', async () => { + await interaction.editReply({ components: [] }); + }); + } + } +} From 4085546ddd86f049248d5bdfd4d70752127245b0 Mon Sep 17 00:00:00 2001 From: iakzs Date: Fri, 13 Sep 2024 14:25:59 -0300 Subject: [PATCH 091/127] testing fr real test --- src/utils/database/levelling.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/utils/database/levelling.ts b/src/utils/database/levelling.ts index b61040a..11e315e 100644 --- a/src/utils/database/levelling.ts +++ b/src/utils/database/levelling.ts @@ -17,6 +17,9 @@ const deleteQuery = database.query("DELETE FROM levelling WHERE guild = $1 AND u const insertQuery = database.query( "INSERT INTO levelling (guild, user, level, exp) VALUES (?1, ?2, ?3, ?4);" ); +const getGuildQuery = database.query( + "SELECT * FROM levelling WHERE guild = $1;" +); export function getLevel(guildID: string, userID: string): [number, number] { const res = getQuery.all(guildID, userID) as TypeOfDefinition[]; @@ -28,3 +31,9 @@ export function setLevel(guildID: string | number, userID: string, level: number if (getQuery.all(guildID, userID).length) deleteQuery.run(guildID, userID); insertQuery.run(guildID, userID, level, exp); } + +export function getGuildLeaderboard( + guildID: string +): TypeOfDefinition[] { + return getGuildQuery.all(guildID) as TypeOfDefinition[]; +} From f3b9b130eb5da9b10e05fcd3b9a1afef6613400c Mon Sep 17 00:00:00 2001 From: iakzs Date: Fri, 13 Sep 2024 14:29:17 -0300 Subject: [PATCH 092/127] hmm yes --- src/utils/database/levelling.ts | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/utils/database/levelling.ts b/src/utils/database/levelling.ts index 11e315e..aa58532 100644 --- a/src/utils/database/levelling.ts +++ b/src/utils/database/levelling.ts @@ -1,26 +1,26 @@ -import { getDatabase } from "."; -import { TableDefinition, TypeOfDefinition } from "./types"; +import { getDatabase } from '.'; +import { TableDefinition, TypeOfDefinition } from './types'; const tableDefinition = { - name: "levelling", + name: 'levelling', definition: { - guild: "TEXT", - user: "TEXT", - level: "INTEGER", - exp: "INTEGER" - } + guild: 'TEXT', + user: 'TEXT', + level: 'INTEGER', + exp: 'INTEGER', + }, } satisfies TableDefinition; const database = getDatabase(tableDefinition); -const getQuery = database.query("SELECT * FROM levelling WHERE guild = $1 AND user = $2;"); -const deleteQuery = database.query("DELETE FROM levelling WHERE guild = $1 AND user = $2;"); + +const getQuery = database.query('SELECT * FROM levelling WHERE guild = $1 AND user = $2;'); +const deleteQuery = database.query('DELETE FROM levelling WHERE guild = $1 AND user = $2;'); const insertQuery = database.query( - "INSERT INTO levelling (guild, user, level, exp) VALUES (?1, ?2, ?3, ?4);" -); -const getGuildQuery = database.query( - "SELECT * FROM levelling WHERE guild = $1;" + 'INSERT INTO levelling (guild, user, level, exp) VALUES (?1, ?2, ?3, ?4);' ); +const getGuildQuery = database.query('SELECT * FROM levelling WHERE guild = $1;'); + export function getLevel(guildID: string, userID: string): [number, number] { const res = getQuery.all(guildID, userID) as TypeOfDefinition[]; if (!res.length) return [0, 0]; From 7ff044ed72e1f110241549f2847987582d35c622 Mon Sep 17 00:00:00 2001 From: iakzs Date: Fri, 13 Sep 2024 14:59:11 -0300 Subject: [PATCH 093/127] ok bro --- bun.lockb | Bin 60065 -> 0 bytes src/commands/leaderboard.ts | 40 ++++++++++++++++---------------- src/utils/database/levelling.ts | 22 +++++++++--------- 3 files changed, 31 insertions(+), 31 deletions(-) delete mode 100755 bun.lockb diff --git a/bun.lockb b/bun.lockb deleted file mode 100755 index eafe73c3c9edcd30a7eaee1c2749da487d4ae362..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 60065 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%0DnLLKnt)ApaVDr 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 D(AL8f diff --git a/src/commands/leaderboard.ts b/src/commands/leaderboard.ts index c66f297..e8e4ef1 100644 --- a/src/commands/leaderboard.ts +++ b/src/commands/leaderboard.ts @@ -6,19 +6,19 @@ import { SlashCommandBuilder, ChatInputCommandInteraction, EmbedBuilder, -} from 'discord.js'; -import { getGuildLeaderboard } from '../utils/database/levelling'; -import { errorEmbed } from '../utils/embeds/errorEmbed'; +} from "discord.js"; +import { getGuildLeaderboard } from "../utils/database/levelling"; +import { errorEmbed } from "../utils/embeds/errorEmbed"; export default class Leaderboard { data: SlashCommandBuilder; constructor() { this.data = new SlashCommandBuilder() - .setName('leaderboard') - .setDescription('Displays the guild leaderboard.') + .setName("leaderboard") + .setDescription("Displays the guild leaderboard.") .addNumberOption((option) => - option.setName('page').setDescription('Page number to display.') + option.setName("page").setDescription("Page number to display.") ); } @@ -26,7 +26,7 @@ export default class Leaderboard { const guildID = interaction.guild?.id; if (!guildID) { - return errorEmbed(interaction, 'This command can only be used in a server.'); + return errorEmbed(interaction, "This command can only be used in a server."); } const leaderboardData = getGuildLeaderboard(guildID); @@ -34,15 +34,15 @@ export default class Leaderboard { if (!leaderboardData.length) { return errorEmbed( interaction, - 'No data found.', - 'There is no levelling data for this server yet.' + "No data found.", + "There is no levelling data for this server yet." ); } leaderboardData.sort((a, b) => b.exp - a.exp); const totalPages = Math.ceil(leaderboardData.length / 5); - let page = interaction.options.getNumber('page') || 1; + let page = interaction.options.getNumber("page") || 1; page = Math.max(1, Math.min(page, totalPages)); const generateEmbed = async () => { @@ -52,7 +52,7 @@ export default class Leaderboard { const embed = new EmbedBuilder() .setTitle(`Leaderboard - Page ${page}/${totalPages}`) - .setColor('#0099ff'); + .setColor("#0099ff"); for (let i = 0; i < pageData.length; i++) { const userData = pageData[i]; @@ -68,12 +68,12 @@ export default class Leaderboard { const row = new ActionRowBuilder().addComponents( new ButtonBuilder() - .setCustomId('left') - .setEmoji('⬅️') + .setCustomId("left") + .setEmoji("⬅️") .setStyle(ButtonStyle.Primary), new ButtonBuilder() - .setCustomId('right') - .setEmoji('➡️') + .setCustomId("right") + .setEmoji("➡️") .setStyle(ButtonStyle.Primary) ); @@ -88,14 +88,14 @@ export default class Leaderboard { time: 60000, }); - collector.on('collect', async (i: ButtonInteraction) => { + collector.on("collect", async (i: ButtonInteraction) => { if (i.user.id !== interaction.user.id) { - return errorEmbed(i, 'You are not the author of this command.'); + return errorEmbed(i, "You are not the author of this command."); } - if (i.customId === 'left') { + if (i.customId === "left") { page = page > 1 ? page - 1 : totalPages; - } else if (i.customId === 'right') { + } else if (i.customId === "right") { page = page < totalPages ? page + 1 : 1; } @@ -105,7 +105,7 @@ export default class Leaderboard { }); }); - collector.on('end', async () => { + collector.on("end", async () => { await interaction.editReply({ components: [] }); }); } diff --git a/src/utils/database/levelling.ts b/src/utils/database/levelling.ts index aa58532..f4953c3 100644 --- a/src/utils/database/levelling.ts +++ b/src/utils/database/levelling.ts @@ -1,25 +1,25 @@ -import { getDatabase } from '.'; -import { TableDefinition, TypeOfDefinition } from './types'; +import { getDatabase } from "."; +import { TableDefinition, TypeOfDefinition } from "./types"; const tableDefinition = { - name: 'levelling', + name: "levelling", definition: { - guild: 'TEXT', - user: 'TEXT', - level: 'INTEGER', - exp: 'INTEGER', + guild: "TEXT", + user: "TEXT", + level: "INTEGER", + exp: "INTEGER", }, } satisfies TableDefinition; const database = getDatabase(tableDefinition); -const getQuery = database.query('SELECT * FROM levelling WHERE guild = $1 AND user = $2;'); -const deleteQuery = database.query('DELETE FROM levelling WHERE guild = $1 AND user = $2;'); +const getQuery = database.query("SELECT * FROM levelling WHERE guild = $1 AND user = $2;"); +const deleteQuery = database.query("DELETE FROM levelling WHERE guild = $1 AND user = $2;"); const insertQuery = database.query( - 'INSERT INTO levelling (guild, user, level, exp) VALUES (?1, ?2, ?3, ?4);' + "INSERT INTO levelling (guild, user, level, exp) VALUES (?1, ?2, ?3, ?4);" ); -const getGuildQuery = database.query('SELECT * FROM levelling WHERE guild = $1;'); +const getGuildQuery = database.query("SELECT * FROM levelling WHERE guild = $1;"); export function getLevel(guildID: string, userID: string): [number, number] { const res = getQuery.all(guildID, userID) as TypeOfDefinition[]; From 5d0034e7e97891ca687d2d46b7da7ebccc77a832 Mon Sep 17 00:00:00 2001 From: iakzs Date: Fri, 13 Sep 2024 15:02:58 -0300 Subject: [PATCH 094/127] updat levelling.ts i hate vs code why cant it work correctly with githubhasudvasjdfhabd --- src/utils/database/levelling.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/utils/database/levelling.ts b/src/utils/database/levelling.ts index aa58532..f4953c3 100644 --- a/src/utils/database/levelling.ts +++ b/src/utils/database/levelling.ts @@ -1,25 +1,25 @@ -import { getDatabase } from '.'; -import { TableDefinition, TypeOfDefinition } from './types'; +import { getDatabase } from "."; +import { TableDefinition, TypeOfDefinition } from "./types"; const tableDefinition = { - name: 'levelling', + name: "levelling", definition: { - guild: 'TEXT', - user: 'TEXT', - level: 'INTEGER', - exp: 'INTEGER', + guild: "TEXT", + user: "TEXT", + level: "INTEGER", + exp: "INTEGER", }, } satisfies TableDefinition; const database = getDatabase(tableDefinition); -const getQuery = database.query('SELECT * FROM levelling WHERE guild = $1 AND user = $2;'); -const deleteQuery = database.query('DELETE FROM levelling WHERE guild = $1 AND user = $2;'); +const getQuery = database.query("SELECT * FROM levelling WHERE guild = $1 AND user = $2;"); +const deleteQuery = database.query("DELETE FROM levelling WHERE guild = $1 AND user = $2;"); const insertQuery = database.query( - 'INSERT INTO levelling (guild, user, level, exp) VALUES (?1, ?2, ?3, ?4);' + "INSERT INTO levelling (guild, user, level, exp) VALUES (?1, ?2, ?3, ?4);" ); -const getGuildQuery = database.query('SELECT * FROM levelling WHERE guild = $1;'); +const getGuildQuery = database.query("SELECT * FROM levelling WHERE guild = $1;"); export function getLevel(guildID: string, userID: string): [number, number] { const res = getQuery.all(guildID, userID) as TypeOfDefinition[]; From 2b28aa2929e04423195d57f906eec46b65c1f1cc Mon Sep 17 00:00:00 2001 From: iakzs Date: Fri, 13 Sep 2024 15:03:24 -0300 Subject: [PATCH 095/127] updat leaderboard.ts --- src/commands/leaderboard.ts | 40 ++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src/commands/leaderboard.ts b/src/commands/leaderboard.ts index c66f297..e8e4ef1 100644 --- a/src/commands/leaderboard.ts +++ b/src/commands/leaderboard.ts @@ -6,19 +6,19 @@ import { SlashCommandBuilder, ChatInputCommandInteraction, EmbedBuilder, -} from 'discord.js'; -import { getGuildLeaderboard } from '../utils/database/levelling'; -import { errorEmbed } from '../utils/embeds/errorEmbed'; +} from "discord.js"; +import { getGuildLeaderboard } from "../utils/database/levelling"; +import { errorEmbed } from "../utils/embeds/errorEmbed"; export default class Leaderboard { data: SlashCommandBuilder; constructor() { this.data = new SlashCommandBuilder() - .setName('leaderboard') - .setDescription('Displays the guild leaderboard.') + .setName("leaderboard") + .setDescription("Displays the guild leaderboard.") .addNumberOption((option) => - option.setName('page').setDescription('Page number to display.') + option.setName("page").setDescription("Page number to display.") ); } @@ -26,7 +26,7 @@ export default class Leaderboard { const guildID = interaction.guild?.id; if (!guildID) { - return errorEmbed(interaction, 'This command can only be used in a server.'); + return errorEmbed(interaction, "This command can only be used in a server."); } const leaderboardData = getGuildLeaderboard(guildID); @@ -34,15 +34,15 @@ export default class Leaderboard { if (!leaderboardData.length) { return errorEmbed( interaction, - 'No data found.', - 'There is no levelling data for this server yet.' + "No data found.", + "There is no levelling data for this server yet." ); } leaderboardData.sort((a, b) => b.exp - a.exp); const totalPages = Math.ceil(leaderboardData.length / 5); - let page = interaction.options.getNumber('page') || 1; + let page = interaction.options.getNumber("page") || 1; page = Math.max(1, Math.min(page, totalPages)); const generateEmbed = async () => { @@ -52,7 +52,7 @@ export default class Leaderboard { const embed = new EmbedBuilder() .setTitle(`Leaderboard - Page ${page}/${totalPages}`) - .setColor('#0099ff'); + .setColor("#0099ff"); for (let i = 0; i < pageData.length; i++) { const userData = pageData[i]; @@ -68,12 +68,12 @@ export default class Leaderboard { const row = new ActionRowBuilder().addComponents( new ButtonBuilder() - .setCustomId('left') - .setEmoji('⬅️') + .setCustomId("left") + .setEmoji("⬅️") .setStyle(ButtonStyle.Primary), new ButtonBuilder() - .setCustomId('right') - .setEmoji('➡️') + .setCustomId("right") + .setEmoji("➡️") .setStyle(ButtonStyle.Primary) ); @@ -88,14 +88,14 @@ export default class Leaderboard { time: 60000, }); - collector.on('collect', async (i: ButtonInteraction) => { + collector.on("collect", async (i: ButtonInteraction) => { if (i.user.id !== interaction.user.id) { - return errorEmbed(i, 'You are not the author of this command.'); + return errorEmbed(i, "You are not the author of this command."); } - if (i.customId === 'left') { + if (i.customId === "left") { page = page > 1 ? page - 1 : totalPages; - } else if (i.customId === 'right') { + } else if (i.customId === "right") { page = page < totalPages ? page + 1 : 1; } @@ -105,7 +105,7 @@ export default class Leaderboard { }); }); - collector.on('end', async () => { + collector.on("end", async () => { await interaction.editReply({ components: [] }); }); } From 2f853782e89da1a801d984f12c99e379a33bb089 Mon Sep 17 00:00:00 2001 From: iakzs Date: Sat, 14 Sep 2024 15:00:16 -0300 Subject: [PATCH 096/127] added cooldown --- src/events/messageCreate.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts index 8ff33e5..f275366 100644 --- a/src/events/messageCreate.ts +++ b/src/events/messageCreate.ts @@ -8,6 +8,8 @@ import { getLevel, setLevel } from "../utils/database/levelling"; import { get as getLevelRewards } from "../utils/database/levelRewards"; import { kominator } from "../utils/kominator"; +const cooldowns = new Map(); + export default { name: "messageCreate", event: class MessageCreate { @@ -41,9 +43,22 @@ export default { for (const channelID of kominator(blockedChannels)) if (message.channelId == channelID) return; - // const cooldown = getSetting(guild.id, "levelling", "set_cooldown") as number; + const cooldown = getSetting(guild.id, "levelling", "set_cooldown") as number || 0; let expGain = getSetting(guild.id, "levelling", "set_xp_gain") as number; const multiplier = getSetting(guild.id, "levelling", "add_multiplier") as string; + + if (cooldown > 0) { + const key = `${guild.id}-${author.id}`; + const lastExpTime = cooldowns.get(key) || 0; + const now = Date.now(); + + if (now - lastExpTime < cooldown * 1000) { + return; + } else { + cooldowns.set(key, now); + } + } + if (multiplier) { const expMultiplier = kominator(multiplier); From f618962e3ef97121beec83269ce46fca54042f01 Mon Sep 17 00:00:00 2001 From: iakzs Date: Sat, 14 Sep 2024 15:01:09 -0300 Subject: [PATCH 097/127] better sort + using math.floor --- src/commands/leaderboard.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/commands/leaderboard.ts b/src/commands/leaderboard.ts index e8e4ef1..f6a81cf 100644 --- a/src/commands/leaderboard.ts +++ b/src/commands/leaderboard.ts @@ -39,7 +39,13 @@ export default class Leaderboard { ); } - leaderboardData.sort((a, b) => b.exp - a.exp); + leaderboardData.sort((a, b) => { + if (b.level !== a.level) { + return b.level - a.level; + } else { + return b.exp - a.exp; + } + }); const totalPages = Math.ceil(leaderboardData.length / 5); let page = interaction.options.getNumber("page") || 1; @@ -59,7 +65,7 @@ export default class Leaderboard { const user = await interaction.client.users.fetch(userData.user); embed.addFields({ name: `${start + i + 1}. ${user.tag}`, - value: `Level: ${userData.level} | EXP: ${userData.exp}`, + value: `Level: ${Math.floor(userData.level)} | EXP: ${Math.floor(userData.exp)}`, }); } From 25c24e3b15ad476babcd64afeb8e96a82a5856de Mon Sep 17 00:00:00 2001 From: iakzs Date: Sat, 14 Sep 2024 15:10:31 -0300 Subject: [PATCH 098/127] small --- src/commands/leaderboard.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/commands/leaderboard.ts b/src/commands/leaderboard.ts index f6a81cf..04d3e57 100644 --- a/src/commands/leaderboard.ts +++ b/src/commands/leaderboard.ts @@ -40,12 +40,8 @@ export default class Leaderboard { } leaderboardData.sort((a, b) => { - if (b.level !== a.level) { - return b.level - a.level; - } else { - return b.exp - a.exp; - } - }); + if (b.level != a.level) return b.level - a.level; + else return b.exp - a.exp; const totalPages = Math.ceil(leaderboardData.length / 5); let page = interaction.options.getNumber("page") || 1; From 2ffba65657ef3e5ca967c17e3290ff067914f6a4 Mon Sep 17 00:00:00 2001 From: iakzs Date: Sat, 14 Sep 2024 15:15:24 -0300 Subject: [PATCH 099/127] i forgor --- src/commands/leaderboard.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/commands/leaderboard.ts b/src/commands/leaderboard.ts index 04d3e57..9f5f2be 100644 --- a/src/commands/leaderboard.ts +++ b/src/commands/leaderboard.ts @@ -42,7 +42,8 @@ export default class Leaderboard { leaderboardData.sort((a, b) => { if (b.level != a.level) return b.level - a.level; else return b.exp - a.exp; - + }); + const totalPages = Math.ceil(leaderboardData.length / 5); let page = interaction.options.getNumber("page") || 1; page = Math.max(1, Math.min(page, totalPages)); From cc0878896dd0626ae8b9187e0e4f4b532f1afc3a Mon Sep 17 00:00:00 2001 From: iakzs Date: Sun, 15 Sep 2024 10:33:23 -0300 Subject: [PATCH 100/127] temp ban --- src/commands/moderation/ban.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/commands/moderation/ban.ts b/src/commands/moderation/ban.ts index b376544..6c811d5 100644 --- a/src/commands/moderation/ban.ts +++ b/src/commands/moderation/ban.ts @@ -31,13 +31,17 @@ export default class Ban { const duration = interaction.options.getString("duration"); const reason = interaction.options.getString("reason"); + let expiresAt: number | undefined; + if (duration) { - if (!ms(duration)) + const durationMs = ms(duration); + if (!durationMs) return await errorEmbed( interaction, `You can't ban ${user.displayName} temporarily.`, "The duration is invalid." ); + expiresAt = Date.now() + durationMs; setTimeout(async () => { await guild.members @@ -69,7 +73,9 @@ export default class Ban { user.id, "BAN", guild.members.cache.get(interaction.user.id)?.id!, - reason ?? undefined + reason ?? undefined, + false, + expiresAt ?? null ); } catch (error) { console.error(error); From b19689d3c8a6f18eabc0cd6bbd98acecbf738dd5 Mon Sep 17 00:00:00 2001 From: iakzs Date: Sun, 15 Sep 2024 10:34:01 -0300 Subject: [PATCH 101/127] maybe a fix if an interaction error shows up --- src/utils/embeds/modEmbed.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/utils/embeds/modEmbed.ts b/src/utils/embeds/modEmbed.ts index e63a0dd..29da4b5 100644 --- a/src/utils/embeds/modEmbed.ts +++ b/src/utils/embeds/modEmbed.ts @@ -108,7 +108,11 @@ export async function modEmbed(options: Options, reason?: string | null, date?: .setColor(genColor(100)); await logChannel(guild, embed); - await interaction.reply({ embeds: [embed] }); + if (!interaction.deferred && !interaction.replied) { // fixes any interaction has been sent or deferred error. + await interaction.reply({ embeds: [embed] }); + } else { + await interaction.followUp({ embeds: [embed] }); + } const dmChannel = await user.createDM().catch(() => null); if (!dmChannel) return; From f659155f304329fd28d3746ebba548c8f3d4b6ec Mon Sep 17 00:00:00 2001 From: iakzs Date: Sun, 15 Sep 2024 10:34:22 -0300 Subject: [PATCH 102/127] added expired bans --- src/utils/database/moderation.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/utils/database/moderation.ts b/src/utils/database/moderation.ts index 1b04977..4a416ad 100644 --- a/src/utils/database/moderation.ts +++ b/src/utils/database/moderation.ts @@ -11,14 +11,15 @@ const definition = { reason: "TEXT", public: "BOOL", id: "TEXT", - timestamp: "TIMESTAMP" + timestamp: "TIMESTAMP", + expiresAt: "TIMESTAMP" } } satisfies TableDefinition; type modType = "MUTE" | "WARN" | "KICK" | "BAN"; const database = getDatabase(definition); const addQuery = database.query( - "INSERT INTO moderation (guild, user, type, moderator, reason, public, id, timestamp) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8);" + "INSERT INTO moderation (guild, user, type, moderator, reason, public, id, timestamp, expiresAt) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9);" ); const listUserQuery = database.query( "SELECT * FROM moderation WHERE guild = $1 AND user = $2;" @@ -40,10 +41,11 @@ export function addModeration( type: modType, moderator: string, reason = "", - pub = false + pub = false, + expiresAt?: number | null ) { const id = crypto.randomUUID(); - addQuery.run(guildID, userID, type, moderator, reason, pub, id, Date.now()); + addQuery.run(guildID, userID, type, moderator, reason, pub, id, Date.now(), expiresAt ?? null); return id; } @@ -69,3 +71,11 @@ export function listModeratorLog(guildID: number | string, moderator: number | s export function removeModeration(guildID: string | number, id: string) { removeQuery.run(guildID, id); } + +const getExpiredBansQuery = database.query( + "SELECT * FROM moderation WHERE type = 'BAN' AND expiresAt IS NOT NULL AND expiresAt <= $1;" +); + +export function getExpiredBans(currentTime: number) { + return getExpiredBansQuery.all(currentTime) as TypeOfDefinition[]; +} From d46e1fddfeda992d394efd4569249b2c3916384a Mon Sep 17 00:00:00 2001 From: iakzs Date: Sun, 15 Sep 2024 10:35:17 -0300 Subject: [PATCH 103/127] added the expired ban check at the start --- src/bot.ts | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/bot.ts b/src/bot.ts index 487f70b..bf5cbdc 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -1,6 +1,7 @@ import { ActivityType, Client } from "discord.js"; import { Commands } from "./handlers/commands"; import { Events } from "./handlers/events"; +import { getExpiredBans, removeModeration } from "./utils/database/moderation"; const client = new Client({ presence: { @@ -17,10 +18,41 @@ const client = new Client({ ] }); +const checkInterval = 60 * 500; + client.on("ready", async () => { await new Events(client).loadEvents(); await new Commands(client).registerCommands(); console.log("ちーっす!"); + setInterval(async () => { + const now = Date.now(); + const expiredBans = getExpiredBans(now); + + for (const ban of expiredBans) { + try { + const guild = await client.guilds.fetch(ban.guild).catch(() => null); + if (!guild) { + removeModeration(ban.guild, ban.id); + continue; + } + + const userId = ban.user.toString(); + + const banInfo = await guild.bans.fetch(userId).catch(() => null); + if (!banInfo) { + removeModeration(ban.guild, ban.id); + continue; + } + + await guild.members.unban(userId, "Temporary ban expired"); + + removeModeration(ban.guild, ban.id); + + } catch (error) { + console.error(`Failed to unban user ID ${ban.user} in guild ${ban.guild}:`, error); + } + } + }, checkInterval); }); client.login(process.env.TOKEN); From 868e3cc86aca9af5148a6fa2a35b856a3436e93f Mon Sep 17 00:00:00 2001 From: iakzs Date: Sun, 15 Sep 2024 10:54:37 -0300 Subject: [PATCH 104/127] added myself to the about hehe --- src/commands/about.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/about.ts b/src/commands/about.ts index e3eee35..f1ac95a 100644 --- a/src/commands/about.ts +++ b/src/commands/about.ts @@ -37,7 +37,7 @@ export default class About { name: "🌌 • Entities involved", value: [ "**Founder**: Goos", - "**Developers**: Dimkauzh, Froxcey, Golem64, MQuery, Nikkerudon, Spectrum, ThatBOI", + "**Developers**: Dimkauzh, Froxcey, Golem64, MQuery, Nikkerudon, Spectrum, ThatBOI, Koslz", "**Designers**: ArtyH, Optix, proJM, Slider_on_the_black", "**Translators**: Dimkauzh, Golem64, Optix, SaFire, ThatBOI, Trynera", "**Testers**: Blaze, fishy, flojo, Tech, Trynera", From ba6cfe5405131768939874ab48f7664132466984 Mon Sep 17 00:00:00 2001 From: iakzs Date: Sun, 15 Sep 2024 11:39:09 -0300 Subject: [PATCH 105/127] yes --- src/commands/about.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/about.ts b/src/commands/about.ts index f1ac95a..bd4d1a8 100644 --- a/src/commands/about.ts +++ b/src/commands/about.ts @@ -37,7 +37,7 @@ export default class About { name: "🌌 • Entities involved", value: [ "**Founder**: Goos", - "**Developers**: Dimkauzh, Froxcey, Golem64, MQuery, Nikkerudon, Spectrum, ThatBOI, Koslz", + "**Developers**: Dimkauzh, Froxcey, Golem64, Koslz, MQuery, Nikkerudon, Spectrum, ThatBOI", "**Designers**: ArtyH, Optix, proJM, Slider_on_the_black", "**Translators**: Dimkauzh, Golem64, Optix, SaFire, ThatBOI, Trynera", "**Testers**: Blaze, fishy, flojo, Tech, Trynera", From 6ee38f0063158592a1186f2bab86eb8363af45c3 Mon Sep 17 00:00:00 2001 From: iakzs Date: Sun, 15 Sep 2024 14:14:35 -0300 Subject: [PATCH 106/127] cool --- src/bot.ts | 34 +------------ src/commands/moderation/ban.ts | 82 ++++++++++++++++---------------- src/utils/database/moderation.ts | 8 ++++ src/utils/unbanScheduler.ts | 58 ++++++++++++++++++++++ 4 files changed, 109 insertions(+), 73 deletions(-) create mode 100644 src/utils/unbanScheduler.ts diff --git a/src/bot.ts b/src/bot.ts index bf5cbdc..bd8e19b 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -1,7 +1,7 @@ import { ActivityType, Client } from "discord.js"; import { Commands } from "./handlers/commands"; import { Events } from "./handlers/events"; -import { getExpiredBans, removeModeration } from "./utils/database/moderation"; +import { rescheduleUnbans } from "./utils/unbanScheduler"; const client = new Client({ presence: { @@ -18,41 +18,11 @@ const client = new Client({ ] }); -const checkInterval = 60 * 500; - client.on("ready", async () => { await new Events(client).loadEvents(); await new Commands(client).registerCommands(); console.log("ちーっす!"); - setInterval(async () => { - const now = Date.now(); - const expiredBans = getExpiredBans(now); - - for (const ban of expiredBans) { - try { - const guild = await client.guilds.fetch(ban.guild).catch(() => null); - if (!guild) { - removeModeration(ban.guild, ban.id); - continue; - } - - const userId = ban.user.toString(); - - const banInfo = await guild.bans.fetch(userId).catch(() => null); - if (!banInfo) { - removeModeration(ban.guild, ban.id); - continue; - } - - await guild.members.unban(userId, "Temporary ban expired"); - - removeModeration(ban.guild, ban.id); - - } catch (error) { - console.error(`Failed to unban user ID ${ban.user} in guild ${ban.guild}:`, error); - } - } - }, checkInterval); + rescheduleUnbans(client); }); client.login(process.env.TOKEN); diff --git a/src/commands/moderation/ban.ts b/src/commands/moderation/ban.ts index 6c811d5..87aad0f 100644 --- a/src/commands/moderation/ban.ts +++ b/src/commands/moderation/ban.ts @@ -7,6 +7,7 @@ import { errorCheck, modEmbed } from "../../utils/embeds/modEmbed"; import { errorEmbed } from "../../utils/embeds/errorEmbed"; import ms from "ms"; import { addModeration } from "../../utils/database/moderation"; +import { scheduleUnban } from "../../utils/unbanScheduler"; export default class Ban { data: SlashCommandSubcommandBuilder; @@ -31,58 +32,57 @@ export default class Ban { const duration = interaction.options.getString("duration"); const reason = interaction.options.getString("reason"); - let expiresAt: number | undefined; + let expiresAt: number | null = null; + + const error = await errorCheck( + PermissionsBitField.Flags.BanMembers, + { interaction, user, action: "Ban" }, + { allErrors: true, botError: true, ownerError: true, outsideError: false }, + "Ban Members" + ); + + if (error) return; if (duration) { const durationMs = ms(duration); - if (!durationMs) + if (!durationMs) { return await errorEmbed( interaction, - `You can't ban ${user.displayName} temporarily.`, + `You can't ban ${user.username} temporarily.`, "The duration is invalid." ); - expiresAt = Date.now() + durationMs; + } - setTimeout(async () => { - await guild.members - .unban(user.id, reason ?? undefined) - .catch(error => console.error(error)); + expiresAt = Date.now() + durationMs; + scheduleUnban(interaction.client, guild.id, user.id, durationMs); + } - await modEmbed({ interaction, user, action: "Unbanned" }, reason); - }, ms(duration)); + try { + await guild.members.ban(user.id, { reason: reason ?? undefined }); + } catch (err) { + console.error("Failed to ban user:", err); + return await errorEmbed(interaction, "Ban failed", "An error occurred while banning the user."); } - if (await errorCheck( - PermissionsBitField.Flags.BanMembers, - { interaction, user, action: "Ban" }, - { allErrors: true, botError: true, ownerError: true, outsideError: false }, - "Ban Members" - ) == null) { - const dmChannel = await guild.members.cache.get(user.id)?.createDM().catch(() => null); - if (dmChannel && !user.bot) { - await modEmbed({ interaction, user, action: "Banned", duration }, reason); - } + try { + addModeration( + guild.id, + user.id, + "BAN", + interaction.user.id, + reason ?? "", + false, + expiresAt + ); + } catch (err) { + console.error("Failed to log moderation record:", err); + } - await interaction.guild?.bans - .create(user.id, { reason: reason ?? undefined }) - .catch(error => console.error(error)); - - try { - addModeration( - guild.id, - user.id, - "BAN", - guild.members.cache.get(interaction.user.id)?.id!, - reason ?? undefined, - false, - expiresAt ?? null - ); - } catch (error) { - console.error(error); - } - - if (!dmChannel || user.bot) - await modEmbed({ interaction, user, action: "Banned", duration }, reason); + const dmChannel = await guild.members.cache.get(user.id)?.createDM().catch(() => null); + if (dmChannel && !user.bot) { + await modEmbed({ interaction, user, action: "Banned", duration }, reason); + } else { + await modEmbed({ interaction, user, action: "Banned", duration }, reason); } } -} +} \ No newline at end of file diff --git a/src/utils/database/moderation.ts b/src/utils/database/moderation.ts index 4a416ad..5eb8fdd 100644 --- a/src/utils/database/moderation.ts +++ b/src/utils/database/moderation.ts @@ -79,3 +79,11 @@ const getExpiredBansQuery = database.query( export function getExpiredBans(currentTime: number) { return getExpiredBansQuery.all(currentTime) as TypeOfDefinition[]; } + +const getPendingBansQuery = database.query( + "SELECT * FROM moderation WHERE type = 'BAN' AND expiresAt IS NOT NULL AND expiresAt > $1;" +); + +export function getPendingBans(currentTime: number) { + return getPendingBansQuery.all(currentTime) as TypeOfDefinition[]; +} diff --git a/src/utils/unbanScheduler.ts b/src/utils/unbanScheduler.ts new file mode 100644 index 0000000..c0a2f06 --- /dev/null +++ b/src/utils/unbanScheduler.ts @@ -0,0 +1,58 @@ +import { Client } from "discord.js"; +import { removeModeration, getPendingBans } from "./database/moderation"; + +const scheduledUnbans = new Map(); + +export function scheduleUnban(client: Client, guildId: string, userId: string, delay: number) { + const key = `${guildId}-${userId}`; + + if (scheduledUnbans.has(key)) { + clearTimeout(scheduledUnbans.get(key)!); + } + + // 24.8 days because why not + const maxDelay = 2147483647; + + if (delay > maxDelay) { + const remainingDelay = delay - maxDelay; + const timeout = setTimeout(() => { + scheduleUnban(client, guildId, userId, remainingDelay); + }, maxDelay); + + scheduledUnbans.set(key, timeout); + } else { + const timeout = setTimeout(async () => { + try { + const guild = await client.guilds.fetch(guildId); + await guild.members.unban(userId, "Temporary ban expired"); + removeModeration(guildId, userId); + scheduledUnbans.delete(key); + } catch (error) { + console.error(`Failed to unban user ${userId} in guild ${guildId}:`, error); + } + }, delay); + + scheduledUnbans.set(key, timeout); + } +} + +export function rescheduleUnbans(client: Client) { + const now = Date.now(); + const pendingBans = getPendingBans(now); + + for (const ban of pendingBans) { + if (typeof ban.expiresAt !== 'number' || isNaN(ban.expiresAt)) { + console.error(`Invalid expiresAt value for ban: ${ban.expiresAt}`); + continue; + } + + const delay = ban.expiresAt - now; + + if (delay > 0) { + scheduleUnban(client, ban.guild, ban.user, delay); + } else { + console.log(`Ban for user ${ban.user} has already expired. Removing from records.`); + removeModeration(ban.guild, ban.id); + } + } +} From deda07d134dd0d6a39e21ff09b0afb61cb72b1d5 Mon Sep 17 00:00:00 2001 From: iakzs Date: Sun, 15 Sep 2024 14:28:03 -0300 Subject: [PATCH 107/127] forgot the bun.lockb --- bun.lockb | Bin 0 -> 52571 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 bun.lockb diff --git a/bun.lockb b/bun.lockb new file mode 100644 index 0000000000000000000000000000000000000000..a09c6670e29c989d849920e4d712a54ddf60f52d GIT binary patch literal 52571 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 Date: Sun, 15 Sep 2024 16:19:41 -0300 Subject: [PATCH 108/127] updat unban scheduler setTimeout (i think) only uses some memory at time of making the ban and the time of unbanning. but if i (1st idea i had) made it check every 1 minute or something would use more memory. i might be wrong --- src/utils/unbanScheduler.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/utils/unbanScheduler.ts b/src/utils/unbanScheduler.ts index c0a2f06..cde9f74 100644 --- a/src/utils/unbanScheduler.ts +++ b/src/utils/unbanScheduler.ts @@ -41,6 +41,10 @@ export function rescheduleUnbans(client: Client) { const pendingBans = getPendingBans(now); for (const ban of pendingBans) { + if (!ban.expiresAt || ban.expiresAt === 0) { + continue; + } + if (typeof ban.expiresAt !== 'number' || isNaN(ban.expiresAt)) { console.error(`Invalid expiresAt value for ban: ${ban.expiresAt}`); continue; From 6d5d898ac37626f665a09896c3d01d4e8481026c Mon Sep 17 00:00:00 2001 From: MrSerge01 <86667481+MrSerge01@users.noreply.github.com> Date: Sat, 21 Sep 2024 23:08:59 +0500 Subject: [PATCH 109/127] eternal suffering, pt1 --- bun.lockb | Bin 52571 -> 52571 bytes package-lock.json | 2049 ----------------- package.json | 4 +- src/commands/about.ts | 2 +- src/commands/moderation/ban.ts | 52 +- .../moderation/{history.ts => cases.ts} | 78 +- src/commands/moderation/delwarn.ts | 16 +- src/commands/moderation/mute.ts | 64 +- src/commands/moderation/unban.ts | 35 +- src/commands/moderation/unmute.ts | 35 +- src/commands/serverboard.ts | 1 + src/events/guildCreate.ts | 18 +- src/events/messageCreate.ts | 22 +- src/utils/database/moderation.ts | 24 +- src/utils/database/settings.ts | 16 +- src/utils/embeds/modEmbed.ts | 74 +- src/utils/embeds/serverEmbed.ts | 2 +- src/utils/unbanScheduler.ts | 25 +- 18 files changed, 236 insertions(+), 2281 deletions(-) delete mode 100644 package-lock.json rename src/commands/moderation/{history.ts => cases.ts} (50%) diff --git a/bun.lockb b/bun.lockb index a09c6670e29c989d849920e4d712a54ddf60f52d..0bd2ebffb8a5b3e2223a18fb7509e98d358a6dda 100644 GIT binary patch delta 9571 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 J>D}WE{{??NjO+ja delta 9311 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 diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 4721b97..0000000 --- a/package-lock.json +++ /dev/null @@ -1,2049 +0,0 @@ -{ - "name": "sokora", - "version": "0.1.0", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "name": "sokora", - "version": "0.1.0", - "dependencies": { - "discord.js": "^14.16.1", - "ms": "^2.1.3", - "node-vibrant": "^3.2.1-alpha.1", - "sharp": "^0.33.5" - }, - "devDependencies": { - "@types/ms": "^0.7.34", - "bun-types": "^1.1.27", - "typescript": "^5.5.4" - } - }, - "node_modules/@babel/runtime": { - "version": "7.23.8", - "license": "MIT", - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@discordjs/builders": { - "version": "1.9.0", - "license": "Apache-2.0", - "dependencies": { - "@discordjs/formatters": "^0.5.0", - "@discordjs/util": "^1.1.1", - "@sapphire/shapeshift": "^4.0.0", - "discord-api-types": "0.37.97", - "fast-deep-equal": "^3.1.3", - "ts-mixer": "^6.0.4", - "tslib": "^2.6.3" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/discordjs/discord.js?sponsor" - } - }, - "node_modules/@discordjs/collection": { - "version": "1.5.3", - "license": "Apache-2.0", - "engines": { - "node": ">=16.11.0" - } - }, - "node_modules/@discordjs/formatters": { - "version": "0.5.0", - "license": "Apache-2.0", - "dependencies": { - "discord-api-types": "0.37.97" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/discordjs/discord.js?sponsor" - } - }, - "node_modules/@discordjs/rest": { - "version": "2.4.0", - "license": "Apache-2.0", - "dependencies": { - "@discordjs/collection": "^2.1.1", - "@discordjs/util": "^1.1.1", - "@sapphire/async-queue": "^1.5.3", - "@sapphire/snowflake": "^3.5.3", - "@vladfrangu/async_event_emitter": "^2.4.6", - "discord-api-types": "0.37.97", - "magic-bytes.js": "^1.10.0", - "tslib": "^2.6.3", - "undici": "6.19.8" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/discordjs/discord.js?sponsor" - } - }, - "node_modules/@discordjs/rest/node_modules/@discordjs/collection": { - "version": "2.1.1", - "license": "Apache-2.0", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/discordjs/discord.js?sponsor" - } - }, - "node_modules/@discordjs/rest/node_modules/@sapphire/async-queue": { - "version": "1.5.3", - "license": "MIT", - "engines": { - "node": ">=v14.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/@discordjs/rest/node_modules/@vladfrangu/async_event_emitter": { - "version": "2.4.6", - "license": "MIT", - "engines": { - "node": ">=v14.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/@discordjs/util": { - "version": "1.1.1", - "license": "Apache-2.0", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/discordjs/discord.js?sponsor" - } - }, - "node_modules/@discordjs/ws": { - "version": "1.1.1", - "license": "Apache-2.0", - "dependencies": { - "@discordjs/collection": "^2.1.0", - "@discordjs/rest": "^2.3.0", - "@discordjs/util": "^1.1.0", - "@sapphire/async-queue": "^1.5.2", - "@types/ws": "^8.5.10", - "@vladfrangu/async_event_emitter": "^2.2.4", - "discord-api-types": "0.37.83", - "tslib": "^2.6.2", - "ws": "^8.16.0" - }, - "engines": { - "node": ">=16.11.0" - }, - "funding": { - "url": "https://github.com/discordjs/discord.js?sponsor" - } - }, - "node_modules/@discordjs/ws/node_modules/@discordjs/collection": { - "version": "2.1.0", - "license": "Apache-2.0", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/discordjs/discord.js?sponsor" - } - }, - "node_modules/@discordjs/ws/node_modules/@discordjs/rest": { - "version": "2.3.0", - "license": "Apache-2.0", - "dependencies": { - "@discordjs/collection": "^2.1.0", - "@discordjs/util": "^1.1.0", - "@sapphire/async-queue": "^1.5.2", - "@sapphire/snowflake": "^3.5.3", - "@vladfrangu/async_event_emitter": "^2.2.4", - "discord-api-types": "0.37.83", - "magic-bytes.js": "^1.10.0", - "tslib": "^2.6.2", - "undici": "6.13.0" - }, - "engines": { - "node": ">=16.11.0" - }, - "funding": { - "url": "https://github.com/discordjs/discord.js?sponsor" - } - }, - "node_modules/@discordjs/ws/node_modules/@discordjs/rest/node_modules/undici": { - "version": "6.13.0", - "license": "MIT", - "engines": { - "node": ">=18.0" - } - }, - "node_modules/@discordjs/ws/node_modules/@discordjs/util": { - "version": "1.1.0", - "license": "Apache-2.0", - "engines": { - "node": ">=16.11.0" - }, - "funding": { - "url": "https://github.com/discordjs/discord.js?sponsor" - } - }, - "node_modules/@discordjs/ws/node_modules/discord-api-types": { - "version": "0.37.83", - "license": "MIT" - }, - "node_modules/@discordjs/ws/node_modules/tslib": { - "version": "2.6.2", - "license": "0BSD" - }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.33.5", - "cpu": [ - "x64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@jimp/bmp": { - "version": "0.16.13", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.13", - "bmp-js": "^0.1.0" - }, - "peerDependencies": { - "@jimp/custom": ">=0.3.5" - } - }, - "node_modules/@jimp/core": { - "version": "0.16.13", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.13", - "any-base": "^1.1.0", - "buffer": "^5.2.0", - "exif-parser": "^0.1.12", - "file-type": "^16.5.4", - "load-bmfont": "^1.3.1", - "mkdirp": "^0.5.1", - "phin": "^2.9.1", - "pixelmatch": "^4.0.2", - "tinycolor2": "^1.4.1" - } - }, - "node_modules/@jimp/custom": { - "version": "0.16.13", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/core": "^0.16.13" - } - }, - "node_modules/@jimp/gif": { - "version": "0.16.13", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.13", - "gifwrap": "^0.9.2", - "omggif": "^1.0.9" - }, - "peerDependencies": { - "@jimp/custom": ">=0.3.5" - } - }, - "node_modules/@jimp/jpeg": { - "version": "0.16.13", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.13", - "jpeg-js": "^0.4.2" - }, - "peerDependencies": { - "@jimp/custom": ">=0.3.5" - } - }, - "node_modules/@jimp/plugin-resize": { - "version": "0.16.13", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.13" - }, - "peerDependencies": { - "@jimp/custom": ">=0.3.5" - } - }, - "node_modules/@jimp/png": { - "version": "0.16.13", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.13", - "pngjs": "^3.3.3" - }, - "peerDependencies": { - "@jimp/custom": ">=0.3.5" - } - }, - "node_modules/@jimp/tiff": { - "version": "0.16.13", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.7.2", - "utif": "^2.0.1" - }, - "peerDependencies": { - "@jimp/custom": ">=0.3.5" - } - }, - "node_modules/@jimp/types": { - "version": "0.16.13", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/bmp": "^0.16.13", - "@jimp/gif": "^0.16.13", - "@jimp/jpeg": "^0.16.13", - "@jimp/png": "^0.16.13", - "@jimp/tiff": "^0.16.13", - "timm": "^1.6.1" - }, - "peerDependencies": { - "@jimp/custom": ">=0.3.5" - } - }, - "node_modules/@jimp/utils": { - "version": "0.16.13", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.7.2", - "regenerator-runtime": "^0.13.3" - } - }, - "node_modules/@jimp/utils/node_modules/regenerator-runtime": { - "version": "0.13.11", - "license": "MIT" - }, - "node_modules/@sapphire/async-queue": { - "version": "1.5.2", - "license": "MIT", - "engines": { - "node": ">=v14.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/@sapphire/shapeshift": { - "version": "4.0.0", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "lodash": "^4.17.21" - }, - "engines": { - "node": ">=v16" - } - }, - "node_modules/@sapphire/snowflake": { - "version": "3.5.3", - "license": "MIT", - "engines": { - "node": ">=v14.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/@tokenizer/token": { - "version": "0.3.0", - "license": "MIT" - }, - "node_modules/@types/ms": { - "version": "0.7.34", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", - "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==", - "dev": true - }, - "node_modules/@types/node": { - "version": "20.12.12", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~5.26.4" - } - }, - "node_modules/@types/ws": { - "version": "8.5.10", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/ws/node_modules/@types/node": { - "version": "20.11.5", - "license": "MIT", - "dependencies": { - "undici-types": "~5.26.4" - } - }, - "node_modules/@vibrant/color": { - "version": "3.2.1-alpha.1", - "license": "MIT" - }, - "node_modules/@vibrant/core": { - "version": "3.2.1-alpha.1", - "license": "MIT", - "dependencies": { - "@vibrant/color": "^3.2.1-alpha.1", - "@vibrant/generator": "^3.2.1-alpha.1", - "@vibrant/image": "^3.2.1-alpha.1", - "@vibrant/quantizer": "^3.2.1-alpha.1", - "@vibrant/types": "^3.2.1-alpha.1", - "@vibrant/worker": "^3.2.1-alpha.1" - } - }, - "node_modules/@vibrant/generator": { - "version": "3.2.1-alpha.1", - "license": "MIT", - "dependencies": { - "@vibrant/color": "^3.2.1-alpha.1", - "@vibrant/types": "^3.2.1-alpha.1" - } - }, - "node_modules/@vibrant/generator-default": { - "version": "3.2.1-alpha.1", - "license": "MIT", - "dependencies": { - "@vibrant/color": "^3.2.1-alpha.1", - "@vibrant/generator": "^3.2.1-alpha.1" - } - }, - "node_modules/@vibrant/image": { - "version": "3.2.1-alpha.1", - "license": "MIT", - "dependencies": { - "@vibrant/color": "^3.2.1-alpha.1", - "@vibrant/types": "^3.2.1-alpha.1" - } - }, - "node_modules/@vibrant/image-browser": { - "version": "3.2.1-alpha.1", - "license": "MIT", - "dependencies": { - "@vibrant/image": "^3.2.1-alpha.1" - } - }, - "node_modules/@vibrant/image-node": { - "version": "3.2.1-alpha.1", - "license": "MIT", - "dependencies": { - "@jimp/custom": "^0.16.1", - "@jimp/plugin-resize": "^0.16.1", - "@jimp/types": "^0.16.1", - "@vibrant/image": "^3.2.1-alpha.1" - } - }, - "node_modules/@vibrant/quantizer": { - "version": "3.2.1-alpha.1", - "license": "MIT", - "dependencies": { - "@vibrant/color": "^3.2.1-alpha.1", - "@vibrant/image": "^3.2.1-alpha.1", - "@vibrant/types": "^3.2.1-alpha.1" - } - }, - "node_modules/@vibrant/quantizer-mmcq": { - "version": "3.2.1-alpha.1", - "license": "MIT", - "dependencies": { - "@vibrant/color": "^3.2.1-alpha.1", - "@vibrant/image": "^3.2.1-alpha.1", - "@vibrant/quantizer": "^3.2.1-alpha.1" - } - }, - "node_modules/@vibrant/types": { - "version": "3.2.1-alpha.1", - "license": "MIT" - }, - "node_modules/@vibrant/worker": { - "version": "3.2.1-alpha.1", - "license": "MIT", - "dependencies": { - "@vibrant/types": "^3.2.1-alpha.1" - } - }, - "node_modules/@vladfrangu/async_event_emitter": { - "version": "2.2.4", - "license": "MIT", - "engines": { - "node": ">=v14.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/any-base": { - "version": "1.1.0", - "license": "MIT" - }, - "node_modules/base64-js": { - "version": "1.5.1", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/bmp-js": { - "version": "0.1.0", - "license": "MIT" - }, - "node_modules/buffer": { - "version": "5.7.1", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/buffer-equal": { - "version": "0.0.1", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/bun-types": { - "version": "1.1.27", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "~20.12.8", - "@types/ws": "~8.5.10" - } - }, - "node_modules/call-bind": { - "version": "1.0.5", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.1", - "set-function-length": "^1.1.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/color": { - "version": "4.2.3", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1", - "color-string": "^1.9.0" - }, - "engines": { - "node": ">=12.5.0" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "license": "MIT" - }, - "node_modules/color-string": { - "version": "1.9.1", - "license": "MIT", - "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, - "node_modules/define-data-property": { - "version": "1.1.1", - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/detect-libc": { - "version": "2.0.3", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/discord-api-types": { - "version": "0.37.97", - "license": "MIT" - }, - "node_modules/discord.js": { - "version": "14.16.1", - "license": "Apache-2.0", - "dependencies": { - "@discordjs/builders": "^1.9.0", - "@discordjs/collection": "1.5.3", - "@discordjs/formatters": "^0.5.0", - "@discordjs/rest": "^2.4.0", - "@discordjs/util": "^1.1.1", - "@discordjs/ws": "1.1.1", - "@sapphire/snowflake": "3.5.3", - "discord-api-types": "0.37.97", - "fast-deep-equal": "3.1.3", - "lodash.snakecase": "4.1.1", - "tslib": "^2.6.3", - "undici": "6.19.8" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/discordjs/discord.js?sponsor" - } - }, - "node_modules/dom-walk": { - "version": "0.1.2" - }, - "node_modules/exif-parser": { - "version": "0.1.12" - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "license": "MIT" - }, - "node_modules/file-type": { - "version": "16.5.4", - "license": "MIT", - "dependencies": { - "readable-web-to-node-stream": "^3.0.0", - "strtok3": "^6.2.4", - "token-types": "^4.1.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/file-type?sponsor=1" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.2.2", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gifwrap": { - "version": "0.9.4", - "license": "MIT", - "dependencies": { - "image-q": "^4.0.0", - "omggif": "^1.0.10" - } - }, - "node_modules/global": { - "version": "4.4.0", - "license": "MIT", - "dependencies": { - "min-document": "^2.19.0", - "process": "^0.11.10" - } - }, - "node_modules/gopd": { - "version": "1.0.1", - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.1", - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.0.1", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.0.3", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.0", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/image-q": { - "version": "4.0.0", - "license": "MIT", - "dependencies": { - "@types/node": "16.9.1" - } - }, - "node_modules/image-q/node_modules/@types/node": { - "version": "16.9.1", - "license": "MIT" - }, - "node_modules/inherits": { - "version": "2.0.4", - "license": "ISC" - }, - "node_modules/is-arrayish": { - "version": "0.3.2", - "license": "MIT" - }, - "node_modules/is-function": { - "version": "1.0.2", - "license": "MIT" - }, - "node_modules/jpeg-js": { - "version": "0.4.4", - "license": "BSD-3-Clause" - }, - "node_modules/load-bmfont": { - "version": "1.4.1", - "license": "MIT", - "dependencies": { - "buffer-equal": "0.0.1", - "mime": "^1.3.4", - "parse-bmfont-ascii": "^1.0.3", - "parse-bmfont-binary": "^1.0.5", - "parse-bmfont-xml": "^1.1.4", - "phin": "^2.9.1", - "xhr": "^2.0.1", - "xtend": "^4.0.0" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "license": "MIT" - }, - "node_modules/lodash.snakecase": { - "version": "4.1.1", - "license": "MIT" - }, - "node_modules/magic-bytes.js": { - "version": "1.10.0", - "license": "MIT" - }, - "node_modules/mime": { - "version": "1.6.0", - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/min-document": { - "version": "2.19.0", - "dependencies": { - "dom-walk": "^0.1.0" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/mkdirp": { - "version": "0.5.6", - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "license": "MIT" - }, - "node_modules/node-vibrant": { - "version": "3.2.1-alpha.1", - "license": "MIT", - "dependencies": { - "@types/node": "^10.12.18", - "@vibrant/core": "^3.2.1-alpha.1", - "@vibrant/generator-default": "^3.2.1-alpha.1", - "@vibrant/image-browser": "^3.2.1-alpha.1", - "@vibrant/image-node": "^3.2.1-alpha.1", - "@vibrant/quantizer-mmcq": "^3.2.1-alpha.1", - "url": "^0.11.0" - } - }, - "node_modules/node-vibrant/node_modules/@types/node": { - "version": "10.17.60", - "license": "MIT" - }, - "node_modules/object-inspect": { - "version": "1.13.1", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/omggif": { - "version": "1.0.10", - "license": "MIT" - }, - "node_modules/pako": { - "version": "1.0.11", - "license": "(MIT AND Zlib)" - }, - "node_modules/parse-bmfont-ascii": { - "version": "1.0.6", - "license": "MIT" - }, - "node_modules/parse-bmfont-binary": { - "version": "1.0.6", - "license": "MIT" - }, - "node_modules/parse-bmfont-xml": { - "version": "1.1.4", - "license": "MIT", - "dependencies": { - "xml-parse-from-string": "^1.0.0", - "xml2js": "^0.4.5" - } - }, - "node_modules/parse-headers": { - "version": "2.0.5", - "license": "MIT" - }, - "node_modules/peek-readable": { - "version": "4.1.0", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/phin": { - "version": "2.9.3", - "license": "MIT" - }, - "node_modules/pixelmatch": { - "version": "4.0.2", - "license": "ISC", - "dependencies": { - "pngjs": "^3.0.0" - }, - "bin": { - "pixelmatch": "bin/pixelmatch" - } - }, - "node_modules/pngjs": { - "version": "3.4.0", - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/process": { - "version": "0.11.10", - "license": "MIT", - "engines": { - "node": ">= 0.6.0" - } - }, - "node_modules/punycode": { - "version": "1.4.1", - "license": "MIT" - }, - "node_modules/qs": { - "version": "6.11.2", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/readable-web-to-node-stream": { - "version": "3.0.2", - "license": "MIT", - "dependencies": { - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/regenerator-runtime": { - "version": "0.14.1", - "license": "MIT" - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/sax": { - "version": "1.3.0", - "license": "ISC" - }, - "node_modules/semver": { - "version": "7.6.3", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/set-function-length": { - "version": "1.2.0", - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.1", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.2", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/sharp": { - "version": "0.33.5", - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "color": "^4.2.3", - "detect-libc": "^2.0.3", - "semver": "^7.6.3" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.33.5", - "@img/sharp-darwin-x64": "0.33.5", - "@img/sharp-libvips-darwin-arm64": "1.0.4", - "@img/sharp-libvips-darwin-x64": "1.0.4", - "@img/sharp-libvips-linux-arm": "1.0.5", - "@img/sharp-libvips-linux-arm64": "1.0.4", - "@img/sharp-libvips-linux-s390x": "1.0.4", - "@img/sharp-libvips-linux-x64": "1.0.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", - "@img/sharp-libvips-linuxmusl-x64": "1.0.4", - "@img/sharp-linux-arm": "0.33.5", - "@img/sharp-linux-arm64": "0.33.5", - "@img/sharp-linux-s390x": "0.33.5", - "@img/sharp-linux-x64": "0.33.5", - "@img/sharp-linuxmusl-arm64": "0.33.5", - "@img/sharp-linuxmusl-x64": "0.33.5", - "@img/sharp-wasm32": "0.33.5", - "@img/sharp-win32-ia32": "0.33.5", - "@img/sharp-win32-x64": "0.33.5" - } - }, - "node_modules/side-channel": { - "version": "1.0.4", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/simple-swizzle": { - "version": "0.2.2", - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.3.1" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/strtok3": { - "version": "6.3.0", - "license": "MIT", - "dependencies": { - "@tokenizer/token": "^0.3.0", - "peek-readable": "^4.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/timm": { - "version": "1.7.1", - "license": "MIT" - }, - "node_modules/tinycolor2": { - "version": "1.6.0", - "license": "MIT" - }, - "node_modules/token-types": { - "version": "4.2.1", - "license": "MIT", - "dependencies": { - "@tokenizer/token": "^0.3.0", - "ieee754": "^1.2.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/ts-mixer": { - "version": "6.0.4", - "license": "MIT" - }, - "node_modules/tslib": { - "version": "2.7.0", - "license": "0BSD" - }, - "node_modules/typescript": { - "version": "5.5.4", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici": { - "version": "6.19.8", - "license": "MIT", - "engines": { - "node": ">=18.17" - } - }, - "node_modules/undici-types": { - "version": "5.26.5", - "license": "MIT" - }, - "node_modules/url": { - "version": "0.11.3", - "license": "MIT", - "dependencies": { - "punycode": "^1.4.1", - "qs": "^6.11.2" - } - }, - "node_modules/utif": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "pako": "^1.0.5" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "license": "MIT" - }, - "node_modules/ws": { - "version": "8.17.0", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/xhr": { - "version": "2.6.0", - "license": "MIT", - "dependencies": { - "global": "~4.4.0", - "is-function": "^1.0.1", - "parse-headers": "^2.0.0", - "xtend": "^4.0.0" - } - }, - "node_modules/xml-parse-from-string": { - "version": "1.0.1", - "license": "MIT" - }, - "node_modules/xml2js": { - "version": "0.4.23", - "license": "MIT", - "dependencies": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/xmlbuilder": { - "version": "11.0.1", - "license": "MIT", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/xtend": { - "version": "4.0.2", - "license": "MIT", - "engines": { - "node": ">=0.4" - } - } - }, - "dependencies": { - "@babel/runtime": { - "version": "7.23.8", - "requires": { - "regenerator-runtime": "^0.14.0" - } - }, - "@discordjs/builders": { - "version": "1.9.0", - "requires": { - "@discordjs/formatters": "^0.5.0", - "@discordjs/util": "^1.1.1", - "@sapphire/shapeshift": "^4.0.0", - "discord-api-types": "0.37.97", - "fast-deep-equal": "^3.1.3", - "ts-mixer": "^6.0.4", - "tslib": "^2.6.3" - } - }, - "@discordjs/collection": { - "version": "1.5.3" - }, - "@discordjs/formatters": { - "version": "0.5.0", - "requires": { - "discord-api-types": "0.37.97" - } - }, - "@discordjs/rest": { - "version": "2.4.0", - "requires": { - "@discordjs/collection": "^2.1.1", - "@discordjs/util": "^1.1.1", - "@sapphire/async-queue": "^1.5.3", - "@sapphire/snowflake": "^3.5.3", - "@vladfrangu/async_event_emitter": "^2.4.6", - "discord-api-types": "0.37.97", - "magic-bytes.js": "^1.10.0", - "tslib": "^2.6.3", - "undici": "6.19.8" - }, - "dependencies": { - "@discordjs/collection": { - "version": "2.1.1" - }, - "@sapphire/async-queue": { - "version": "1.5.3" - }, - "@vladfrangu/async_event_emitter": { - "version": "2.4.6" - } - } - }, - "@discordjs/util": { - "version": "1.1.1" - }, - "@discordjs/ws": { - "version": "1.1.1", - "requires": { - "@discordjs/collection": "^2.1.0", - "@discordjs/rest": "^2.3.0", - "@discordjs/util": "^1.1.0", - "@sapphire/async-queue": "^1.5.2", - "@types/ws": "^8.5.10", - "@vladfrangu/async_event_emitter": "^2.2.4", - "discord-api-types": "0.37.83", - "tslib": "^2.6.2", - "ws": "^8.16.0" - }, - "dependencies": { - "@discordjs/collection": { - "version": "2.1.0" - }, - "@discordjs/rest": { - "version": "2.3.0", - "requires": { - "@discordjs/collection": "^2.1.0", - "@discordjs/util": "^1.1.0", - "@sapphire/async-queue": "^1.5.2", - "@sapphire/snowflake": "^3.5.3", - "@vladfrangu/async_event_emitter": "^2.2.4", - "discord-api-types": "0.37.83", - "magic-bytes.js": "^1.10.0", - "tslib": "^2.6.2", - "undici": "6.13.0" - }, - "dependencies": { - "undici": { - "version": "6.13.0" - } - } - }, - "@discordjs/util": { - "version": "1.1.0" - }, - "discord-api-types": { - "version": "0.37.83" - }, - "tslib": { - "version": "2.6.2" - } - } - }, - "@img/sharp-win32-x64": { - "version": "0.33.5", - "optional": true - }, - "@jimp/bmp": { - "version": "0.16.13", - "requires": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.13", - "bmp-js": "^0.1.0" - } - }, - "@jimp/core": { - "version": "0.16.13", - "requires": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.13", - "any-base": "^1.1.0", - "buffer": "^5.2.0", - "exif-parser": "^0.1.12", - "file-type": "^16.5.4", - "load-bmfont": "^1.3.1", - "mkdirp": "^0.5.1", - "phin": "^2.9.1", - "pixelmatch": "^4.0.2", - "tinycolor2": "^1.4.1" - } - }, - "@jimp/custom": { - "version": "0.16.13", - "requires": { - "@babel/runtime": "^7.7.2", - "@jimp/core": "^0.16.13" - } - }, - "@jimp/gif": { - "version": "0.16.13", - "requires": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.13", - "gifwrap": "^0.9.2", - "omggif": "^1.0.9" - } - }, - "@jimp/jpeg": { - "version": "0.16.13", - "requires": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.13", - "jpeg-js": "^0.4.2" - } - }, - "@jimp/plugin-resize": { - "version": "0.16.13", - "requires": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.13" - } - }, - "@jimp/png": { - "version": "0.16.13", - "requires": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.13", - "pngjs": "^3.3.3" - } - }, - "@jimp/tiff": { - "version": "0.16.13", - "requires": { - "@babel/runtime": "^7.7.2", - "utif": "^2.0.1" - } - }, - "@jimp/types": { - "version": "0.16.13", - "requires": { - "@babel/runtime": "^7.7.2", - "@jimp/bmp": "^0.16.13", - "@jimp/gif": "^0.16.13", - "@jimp/jpeg": "^0.16.13", - "@jimp/png": "^0.16.13", - "@jimp/tiff": "^0.16.13", - "timm": "^1.6.1" - } - }, - "@jimp/utils": { - "version": "0.16.13", - "requires": { - "@babel/runtime": "^7.7.2", - "regenerator-runtime": "^0.13.3" - }, - "dependencies": { - "regenerator-runtime": { - "version": "0.13.11" - } - } - }, - "@sapphire/async-queue": { - "version": "1.5.2" - }, - "@sapphire/shapeshift": { - "version": "4.0.0", - "requires": { - "fast-deep-equal": "^3.1.3", - "lodash": "^4.17.21" - } - }, - "@sapphire/snowflake": { - "version": "3.5.3" - }, - "@tokenizer/token": { - "version": "0.3.0" - }, - "@types/ms": { - "version": "0.7.34", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", - "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==", - "dev": true - }, - "@types/node": { - "version": "20.12.12", - "dev": true, - "requires": { - "undici-types": "~5.26.4" - } - }, - "@types/ws": { - "version": "8.5.10", - "requires": { - "@types/node": "*" - }, - "dependencies": { - "@types/node": { - "version": "20.11.5", - "requires": { - "undici-types": "~5.26.4" - } - } - } - }, - "@vibrant/color": { - "version": "3.2.1-alpha.1" - }, - "@vibrant/core": { - "version": "3.2.1-alpha.1", - "requires": { - "@vibrant/color": "^3.2.1-alpha.1", - "@vibrant/generator": "^3.2.1-alpha.1", - "@vibrant/image": "^3.2.1-alpha.1", - "@vibrant/quantizer": "^3.2.1-alpha.1", - "@vibrant/types": "^3.2.1-alpha.1", - "@vibrant/worker": "^3.2.1-alpha.1" - } - }, - "@vibrant/generator": { - "version": "3.2.1-alpha.1", - "requires": { - "@vibrant/color": "^3.2.1-alpha.1", - "@vibrant/types": "^3.2.1-alpha.1" - } - }, - "@vibrant/generator-default": { - "version": "3.2.1-alpha.1", - "requires": { - "@vibrant/color": "^3.2.1-alpha.1", - "@vibrant/generator": "^3.2.1-alpha.1" - } - }, - "@vibrant/image": { - "version": "3.2.1-alpha.1", - "requires": { - "@vibrant/color": "^3.2.1-alpha.1", - "@vibrant/types": "^3.2.1-alpha.1" - } - }, - "@vibrant/image-browser": { - "version": "3.2.1-alpha.1", - "requires": { - "@vibrant/image": "^3.2.1-alpha.1" - } - }, - "@vibrant/image-node": { - "version": "3.2.1-alpha.1", - "requires": { - "@jimp/custom": "^0.16.1", - "@jimp/plugin-resize": "^0.16.1", - "@jimp/types": "^0.16.1", - "@vibrant/image": "^3.2.1-alpha.1" - } - }, - "@vibrant/quantizer": { - "version": "3.2.1-alpha.1", - "requires": { - "@vibrant/color": "^3.2.1-alpha.1", - "@vibrant/image": "^3.2.1-alpha.1", - "@vibrant/types": "^3.2.1-alpha.1" - } - }, - "@vibrant/quantizer-mmcq": { - "version": "3.2.1-alpha.1", - "requires": { - "@vibrant/color": "^3.2.1-alpha.1", - "@vibrant/image": "^3.2.1-alpha.1", - "@vibrant/quantizer": "^3.2.1-alpha.1" - } - }, - "@vibrant/types": { - "version": "3.2.1-alpha.1" - }, - "@vibrant/worker": { - "version": "3.2.1-alpha.1", - "requires": { - "@vibrant/types": "^3.2.1-alpha.1" - } - }, - "@vladfrangu/async_event_emitter": { - "version": "2.2.4" - }, - "any-base": { - "version": "1.1.0" - }, - "base64-js": { - "version": "1.5.1" - }, - "bmp-js": { - "version": "0.1.0" - }, - "buffer": { - "version": "5.7.1", - "requires": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "buffer-equal": { - "version": "0.0.1" - }, - "bun-types": { - "version": "1.1.27", - "dev": true, - "requires": { - "@types/node": "~20.12.8", - "@types/ws": "~8.5.10" - } - }, - "call-bind": { - "version": "1.0.5", - "requires": { - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.1", - "set-function-length": "^1.1.1" - } - }, - "color": { - "version": "4.2.3", - "requires": { - "color-convert": "^2.0.1", - "color-string": "^1.9.0" - } - }, - "color-convert": { - "version": "2.0.1", - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4" - }, - "color-string": { - "version": "1.9.1", - "requires": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, - "define-data-property": { - "version": "1.1.1", - "requires": { - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" - } - }, - "detect-libc": { - "version": "2.0.3" - }, - "discord-api-types": { - "version": "0.37.97" - }, - "discord.js": { - "version": "14.16.1", - "requires": { - "@discordjs/builders": "^1.9.0", - "@discordjs/collection": "1.5.3", - "@discordjs/formatters": "^0.5.0", - "@discordjs/rest": "^2.4.0", - "@discordjs/util": "^1.1.1", - "@discordjs/ws": "1.1.1", - "@sapphire/snowflake": "3.5.3", - "discord-api-types": "0.37.97", - "fast-deep-equal": "3.1.3", - "lodash.snakecase": "4.1.1", - "tslib": "^2.6.3", - "undici": "6.19.8" - } - }, - "dom-walk": { - "version": "0.1.2" - }, - "exif-parser": { - "version": "0.1.12" - }, - "fast-deep-equal": { - "version": "3.1.3" - }, - "file-type": { - "version": "16.5.4", - "requires": { - "readable-web-to-node-stream": "^3.0.0", - "strtok3": "^6.2.4", - "token-types": "^4.1.1" - } - }, - "function-bind": { - "version": "1.1.2" - }, - "get-intrinsic": { - "version": "1.2.2", - "requires": { - "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" - } - }, - "gifwrap": { - "version": "0.9.4", - "requires": { - "image-q": "^4.0.0", - "omggif": "^1.0.10" - } - }, - "global": { - "version": "4.4.0", - "requires": { - "min-document": "^2.19.0", - "process": "^0.11.10" - } - }, - "gopd": { - "version": "1.0.1", - "requires": { - "get-intrinsic": "^1.1.3" - } - }, - "has-property-descriptors": { - "version": "1.0.1", - "requires": { - "get-intrinsic": "^1.2.2" - } - }, - "has-proto": { - "version": "1.0.1" - }, - "has-symbols": { - "version": "1.0.3" - }, - "hasown": { - "version": "2.0.0", - "requires": { - "function-bind": "^1.1.2" - } - }, - "ieee754": { - "version": "1.2.1" - }, - "image-q": { - "version": "4.0.0", - "requires": { - "@types/node": "16.9.1" - }, - "dependencies": { - "@types/node": { - "version": "16.9.1" - } - } - }, - "inherits": { - "version": "2.0.4" - }, - "is-arrayish": { - "version": "0.3.2" - }, - "is-function": { - "version": "1.0.2" - }, - "jpeg-js": { - "version": "0.4.4" - }, - "load-bmfont": { - "version": "1.4.1", - "requires": { - "buffer-equal": "0.0.1", - "mime": "^1.3.4", - "parse-bmfont-ascii": "^1.0.3", - "parse-bmfont-binary": "^1.0.5", - "parse-bmfont-xml": "^1.1.4", - "phin": "^2.9.1", - "xhr": "^2.0.1", - "xtend": "^4.0.0" - } - }, - "lodash": { - "version": "4.17.21" - }, - "lodash.snakecase": { - "version": "4.1.1" - }, - "magic-bytes.js": { - "version": "1.10.0" - }, - "mime": { - "version": "1.6.0" - }, - "min-document": { - "version": "2.19.0", - "requires": { - "dom-walk": "^0.1.0" - } - }, - "minimist": { - "version": "1.2.8" - }, - "mkdirp": { - "version": "0.5.6", - "requires": { - "minimist": "^1.2.6" - } - }, - "ms": { - "version": "2.1.3" - }, - "node-vibrant": { - "version": "3.2.1-alpha.1", - "requires": { - "@types/node": "^10.12.18", - "@vibrant/core": "^3.2.1-alpha.1", - "@vibrant/generator-default": "^3.2.1-alpha.1", - "@vibrant/image-browser": "^3.2.1-alpha.1", - "@vibrant/image-node": "^3.2.1-alpha.1", - "@vibrant/quantizer-mmcq": "^3.2.1-alpha.1", - "url": "^0.11.0" - }, - "dependencies": { - "@types/node": { - "version": "10.17.60" - } - } - }, - "object-inspect": { - "version": "1.13.1" - }, - "omggif": { - "version": "1.0.10" - }, - "pako": { - "version": "1.0.11" - }, - "parse-bmfont-ascii": { - "version": "1.0.6" - }, - "parse-bmfont-binary": { - "version": "1.0.6" - }, - "parse-bmfont-xml": { - "version": "1.1.4", - "requires": { - "xml-parse-from-string": "^1.0.0", - "xml2js": "^0.4.5" - } - }, - "parse-headers": { - "version": "2.0.5" - }, - "peek-readable": { - "version": "4.1.0" - }, - "phin": { - "version": "2.9.3" - }, - "pixelmatch": { - "version": "4.0.2", - "requires": { - "pngjs": "^3.0.0" - } - }, - "pngjs": { - "version": "3.4.0" - }, - "process": { - "version": "0.11.10" - }, - "punycode": { - "version": "1.4.1" - }, - "qs": { - "version": "6.11.2", - "requires": { - "side-channel": "^1.0.4" - } - }, - "readable-stream": { - "version": "3.6.2", - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "readable-web-to-node-stream": { - "version": "3.0.2", - "requires": { - "readable-stream": "^3.6.0" - } - }, - "regenerator-runtime": { - "version": "0.14.1" - }, - "safe-buffer": { - "version": "5.2.1" - }, - "sax": { - "version": "1.3.0" - }, - "semver": { - "version": "7.6.3" - }, - "set-function-length": { - "version": "1.2.0", - "requires": { - "define-data-property": "^1.1.1", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.2", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.1" - } - }, - "sharp": { - "version": "0.33.5", - "requires": { - "@img/sharp-darwin-arm64": "0.33.5", - "@img/sharp-darwin-x64": "0.33.5", - "@img/sharp-libvips-darwin-arm64": "1.0.4", - "@img/sharp-libvips-darwin-x64": "1.0.4", - "@img/sharp-libvips-linux-arm": "1.0.5", - "@img/sharp-libvips-linux-arm64": "1.0.4", - "@img/sharp-libvips-linux-s390x": "1.0.4", - "@img/sharp-libvips-linux-x64": "1.0.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", - "@img/sharp-libvips-linuxmusl-x64": "1.0.4", - "@img/sharp-linux-arm": "0.33.5", - "@img/sharp-linux-arm64": "0.33.5", - "@img/sharp-linux-s390x": "0.33.5", - "@img/sharp-linux-x64": "0.33.5", - "@img/sharp-linuxmusl-arm64": "0.33.5", - "@img/sharp-linuxmusl-x64": "0.33.5", - "@img/sharp-wasm32": "0.33.5", - "@img/sharp-win32-ia32": "0.33.5", - "@img/sharp-win32-x64": "0.33.5", - "color": "^4.2.3", - "detect-libc": "^2.0.3", - "semver": "^7.6.3" - } - }, - "side-channel": { - "version": "1.0.4", - "requires": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" - } - }, - "simple-swizzle": { - "version": "0.2.2", - "requires": { - "is-arrayish": "^0.3.1" - } - }, - "string_decoder": { - "version": "1.3.0", - "requires": { - "safe-buffer": "~5.2.0" - } - }, - "strtok3": { - "version": "6.3.0", - "requires": { - "@tokenizer/token": "^0.3.0", - "peek-readable": "^4.1.0" - } - }, - "timm": { - "version": "1.7.1" - }, - "tinycolor2": { - "version": "1.6.0" - }, - "token-types": { - "version": "4.2.1", - "requires": { - "@tokenizer/token": "^0.3.0", - "ieee754": "^1.2.1" - } - }, - "ts-mixer": { - "version": "6.0.4" - }, - "tslib": { - "version": "2.7.0" - }, - "typescript": { - "version": "5.5.4", - "dev": true - }, - "undici": { - "version": "6.19.8" - }, - "undici-types": { - "version": "5.26.5" - }, - "url": { - "version": "0.11.3", - "requires": { - "punycode": "^1.4.1", - "qs": "^6.11.2" - } - }, - "utif": { - "version": "2.0.1", - "requires": { - "pako": "^1.0.5" - } - }, - "util-deprecate": { - "version": "1.0.2" - }, - "ws": { - "version": "8.17.0", - "requires": {} - }, - "xhr": { - "version": "2.6.0", - "requires": { - "global": "~4.4.0", - "is-function": "^1.0.1", - "parse-headers": "^2.0.0", - "xtend": "^4.0.0" - } - }, - "xml-parse-from-string": { - "version": "1.0.1" - }, - "xml2js": { - "version": "0.4.23", - "requires": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - } - }, - "xmlbuilder": { - "version": "11.0.1" - }, - "xtend": { - "version": "4.0.2" - } - } -} diff --git a/package.json b/package.json index 6b1d389..0b901c6 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "start": "bun ./src/index.ts" }, "dependencies": { - "discord.js": "^14.16.1", + "discord.js": "^14.16.2", "ms": "^2.1.3", "node-vibrant": "^3.2.1-alpha.1", "sharp": "^0.33.5" @@ -20,6 +20,6 @@ "devDependencies": { "@types/ms": "^0.7.34", "bun-types": "^1.1.27", - "typescript": "^5.5.4" + "typescript": "^5.6.2" } } diff --git a/src/commands/about.ts b/src/commands/about.ts index bd4d1a8..e3ef5d7 100644 --- a/src/commands/about.ts +++ b/src/commands/about.ts @@ -40,7 +40,7 @@ export default class About { "**Developers**: Dimkauzh, Froxcey, Golem64, Koslz, MQuery, Nikkerudon, Spectrum, ThatBOI", "**Designers**: ArtyH, Optix, proJM, Slider_on_the_black", "**Translators**: Dimkauzh, Golem64, Optix, SaFire, ThatBOI, Trynera", - "**Testers**: Blaze, fishy, flojo, Tech, Trynera", + "**Testers**: Blaze, fishy, flojo, Trynera", "And **YOU**, for using Sokora." ].join("\n") }, diff --git a/src/commands/moderation/ban.ts b/src/commands/moderation/ban.ts index 87aad0f..e4c4c3c 100644 --- a/src/commands/moderation/ban.ts +++ b/src/commands/moderation/ban.ts @@ -6,7 +6,6 @@ import { import { errorCheck, modEmbed } from "../../utils/embeds/modEmbed"; import { errorEmbed } from "../../utils/embeds/errorEmbed"; import ms from "ms"; -import { addModeration } from "../../utils/database/moderation"; import { scheduleUnban } from "../../utils/unbanScheduler"; export default class Ban { @@ -18,9 +17,7 @@ export default class Ban { .addUserOption(user => user.setName("user").setDescription("The user that you want to ban.").setRequired(true) ) - .addStringOption(string => - string.setName("reason").setDescription("The reason for the ban.") - ) + .addStringOption(string => string.setName("reason").setDescription("The reason for the ban.")) .addStringOption(string => string.setName("duration").setDescription("The duration of the ban (e.g 2mo, 1y).") ); @@ -32,17 +29,17 @@ export default class Ban { const duration = interaction.options.getString("duration"); const reason = interaction.options.getString("reason"); - let expiresAt: number | null = null; - - const error = await errorCheck( - PermissionsBitField.Flags.BanMembers, - { interaction, user, action: "Ban" }, - { allErrors: true, botError: true, ownerError: true, outsideError: false }, - "Ban Members" - ); - - if (error) return; + if ( + !(await errorCheck( + PermissionsBitField.Flags.BanMembers, + { interaction, user, action: "Ban" }, + { allErrors: true, botError: true, ownerError: true }, + "Ban Members" + )) + ) + return; + let expiresAt: number | undefined; if (duration) { const durationMs = ms(duration); if (!durationMs) { @@ -61,28 +58,11 @@ export default class Ban { await guild.members.ban(user.id, { reason: reason ?? undefined }); } catch (err) { console.error("Failed to ban user:", err); - return await errorEmbed(interaction, "Ban failed", "An error occurred while banning the user."); } - try { - addModeration( - guild.id, - user.id, - "BAN", - interaction.user.id, - reason ?? "", - false, - expiresAt - ); - } catch (err) { - console.error("Failed to log moderation record:", err); - } - - const dmChannel = await guild.members.cache.get(user.id)?.createDM().catch(() => null); - if (dmChannel && !user.bot) { - await modEmbed({ interaction, user, action: "Banned", duration }, reason); - } else { - await modEmbed({ interaction, user, action: "Banned", duration }, reason); - } + await modEmbed( + { interaction, user, action: "Banned", duration, dm: true, dbAction: "BAN", expiresAt }, + reason + ); } -} \ No newline at end of file +} diff --git a/src/commands/moderation/history.ts b/src/commands/moderation/cases.ts similarity index 50% rename from src/commands/moderation/history.ts rename to src/commands/moderation/cases.ts index c397b5e..773062b 100644 --- a/src/commands/moderation/history.ts +++ b/src/commands/moderation/cases.ts @@ -2,32 +2,20 @@ import { EmbedBuilder, PermissionsBitField, SlashCommandSubcommandBuilder, + inlineCode, type ChatInputCommandInteraction } from "discord.js"; import { genColor } from "../../utils/colorGen"; import { getModeration, listUserModeration } from "../../utils/database/moderation"; import { errorEmbed } from "../../utils/embeds/errorEmbed"; +import { randomise } from "../../utils/randomise"; -const actionsEmojis: { [key: string]: string } = { - WARN: "⚠️", - MUTE: "🔇", - KICK: "📤", // looks better than 🦶 - BAN: "🔨" // or 🚫 ? -}; - -const nothingMsg = [ - "Nothing to see here...", - "Ayay, no cases on this horizon cap'n !", - "Clean like a whistle !", - "What does 0+0= ?" -] - -export default class History { +export default class Cases { data: SlashCommandSubcommandBuilder; constructor() { this.data = new SlashCommandSubcommandBuilder() - .setName("history") - .setDescription("Moderation cases history of a user.") // Can be misundertood as the user's history, needs changing + .setName("cases") + .setDescription("Moderation cases in a server.") .addUserOption(user => user.setName("user").setDescription("The user that you want to see.").setRequired(true) ) @@ -37,6 +25,20 @@ export default class History { } async run(interaction: ChatInputCommandInteraction) { + const actionsEmojis: { [key: string]: string } = { + WARN: "⚠️", + MUTE: "🔇", + KICK: "📤", + BAN: "🔨" + }; + + const nothingMsg = [ + "Nothing to see here...", + "Ayay, no cases on this horizon cap'n!", + "Clean as a whistle!", + "0+0=?" + ]; + const guild = interaction.guild!; if ( !guild.members.cache @@ -54,29 +56,39 @@ export default class History { // const mutes = listUserModeration(guild.id, user.id, "MUTE"); // const kicks = listUserModeration(guild.id, user.id, "KICK"); // const bans = listUserModeration(guild.id, user.id, "BAN"); - let actionid = interaction.options.getString("id") - if (actionid && actionid?.startsWith("#")) actionid = actionid.slice(1); - const allactions = actionid ? getModeration(guild.id, user.id, actionid) : listUserModeration(guild.id, user.id); + let actionID = interaction.options.getString("id"); + if (actionID && actionID?.startsWith("#")) actionID = actionID.slice(1); + const actions = actionID + ? getModeration(guild.id, user.id, actionID) + : listUserModeration(guild.id, user.id); + const embed = new EmbedBuilder() - .setAuthor({ name: `• ${user.displayName}`, iconURL: user.displayAvatarURL() }) - .setTitle(`Moderation cases of ${user.displayName}`) - //.setDescription(`Moderation actions history of ${user.displayName}.`) + .setAuthor({ name: `• Cases of ${user.displayName}`, iconURL: user.displayAvatarURL() }) .setFields( - allactions.length > 0 - ? allactions.map(action => { + actions.length > 0 + ? actions.map(action => { + const actionValues = [ + `Responsible moderator is <@${action.moderator}>`, + action.reason + ? `The **reason** is ${inlineCode(action.reason)}` + : "*No reason provided*", + `-# Action happened on ****` + ]; + return { - name: `${actionsEmojis[action.type]} ${action.type} #${action.id}`, // Include durations ? needs to add a db column - value: [ - `**Reason**: ${action.reason}`, - `-# __Moderator:__ <@${action.moderator}> | ` - ].join("\n") + name: `${actionsEmojis[action.type]} • ${action.type} #${action.id}`, // Include durations ? needs to add a db column + value: actionValues.join("\n") }; }) - : [{ name: "💨 " + nothingMsg[Math.floor(Math.random() * nothingMsg.length)], value: "No actions has been taken on this user" }] + : [ + { + name: `💨 • ${randomise(nothingMsg)}`, + value: "*No actions have been taken on this user*" + } + ] ) - .setThumbnail(user.displayAvatarURL()) .setFooter({ text: `User ID: ${user.id}` }) - .setColor(genColor(100)); + .setColor(genColor(200)); await interaction.reply({ embeds: [embed] }); } diff --git a/src/commands/moderation/delwarn.ts b/src/commands/moderation/delwarn.ts index eaa1997..0929c28 100644 --- a/src/commands/moderation/delwarn.ts +++ b/src/commands/moderation/delwarn.ts @@ -36,12 +36,15 @@ export default class Delwarn { const warns = listUserModeration(guild.id, user.id, "WARN"); const newWarns = warns.filter(warn => warn.id != `${id}`); - await errorCheck( - PermissionsBitField.Flags.ModerateMembers, - { interaction, user, action: "Remove a warn" }, - { allErrors: true, botError: false, ownerError: false, outsideError: false }, - "Moderate Members" - ); + if ( + (await errorCheck( + PermissionsBitField.Flags.ModerateMembers, + { interaction, user, action: "Remove a warn" }, + { allErrors: true, botError: false }, + "Moderate Members" + )) !== null + ) + return; if (newWarns.length == warns.length) return await errorEmbed(interaction, `There is no warn with the id of ${id}.`); @@ -62,7 +65,6 @@ export default class Delwarn { } await interaction.reply({ embeds: [embed] }); - const dmChannel = (await user.createDM().catch(() => null)) as DMChannel | null; if (!dmChannel) return; if (user.bot) return; diff --git a/src/commands/moderation/mute.ts b/src/commands/moderation/mute.ts index ecba4d3..26a4fa1 100644 --- a/src/commands/moderation/mute.ts +++ b/src/commands/moderation/mute.ts @@ -33,44 +33,36 @@ export default class Mute { const duration = interaction.options.getString("duration")!; const reason = interaction.options.getString("reason"); - if (await errorCheck( - PermissionsBitField.Flags.ModerateMembers, - { interaction, user, action: "Mute" }, - { allErrors: true, botError: true, ownerError: true, outsideError: false }, // If someone comes back after being kicked but the mods want to make sure they calm down before speaking again - "Moderate Members" - ) == null) { + if ( + await errorCheck( + PermissionsBitField.Flags.ModerateMembers, + { interaction, user, action: "Mute" }, + { allErrors: true, botError: true, ownerError: true }, + "Moderate Members" + ) + ) + return; + + if (!ms(duration) || ms(duration) > ms("28d")) + return await errorEmbed( + interaction, + `You can't mute ${user.displayName}.`, + "The duration is invalid or is above the 28 day limit." + ); - if (!ms(duration) || ms(duration) > ms("28d")) - return await errorEmbed( - interaction, - `You can't mute ${user.displayName}.`, - "The duration is invalid or is above the 28 day limit." - ); - - const time = new Date( - Date.parse(new Date().toISOString()) + Date.parse(new Date(ms(duration)).toISOString()) - ).toISOString(); - - await interaction.guild?.members.cache - .get(user.id) - ?.edit({ communicationDisabledUntil: time, reason: reason ?? undefined }) - .catch(error => console.error(error)); + const time = new Date( + Date.parse(new Date().toISOString()) + Date.parse(new Date(ms(duration)).toISOString()) + ).toISOString(); - const guild = interaction.guild!; + await interaction.guild?.members.cache + .get(user.id) + ?.edit({ communicationDisabledUntil: time, reason: reason ?? undefined }) + .catch(error => console.error(error)); - try { - addModeration( - guild.id, - user.id, - "MUTE", - guild.members.cache.get(interaction.user.id)?.id!, - reason ?? undefined - ); - } catch (error) { - console.error(error); - } - - await modEmbed({ interaction, user, action: "Muted", duration }, reason); - } + await modEmbed( + { interaction, user, action: "Muted", duration, dm: true, dbAction: "MUTE" }, + reason, + true + ); } } diff --git a/src/commands/moderation/unban.ts b/src/commands/moderation/unban.ts index 49d1a5b..3f9e1e7 100644 --- a/src/commands/moderation/unban.ts +++ b/src/commands/moderation/unban.ts @@ -31,21 +31,24 @@ export default class Unban { .map(ban => ban.user) .filter(user => user.id == id)[0]!; - if (await errorCheck( - PermissionsBitField.Flags.BanMembers, - { interaction, user: target, action: "Unban" }, - { allErrors: false, botError: true, ownerError: true, outsideError: false }, - "Ban Members" - ) == null) { - if (!target) - return await errorEmbed( - interaction, - "You can't unban this user.", - "The user was never banned." - ); - - await guild.members.unban(id, reason ?? undefined).catch(error => console.error(error)); - await modEmbed({ interaction, user: target, action: "Unbanned" }, reason); - } + if ( + (await errorCheck( + PermissionsBitField.Flags.BanMembers, + { interaction, user: target, action: "Unban" }, + { allErrors: false, botError: true, ownerError: true }, + "Ban Members" + )) !== null + ) + return; + + if (!target) + return await errorEmbed( + interaction, + "You can't unban this user.", + "The user was never banned." + ); + + await guild.members.unban(id, reason ?? undefined).catch(error => console.error(error)); + await modEmbed({ interaction, user: target, action: "Unbanned" }, reason); } } diff --git a/src/commands/moderation/unmute.ts b/src/commands/moderation/unmute.ts index 9432e08..e9f410c 100644 --- a/src/commands/moderation/unmute.ts +++ b/src/commands/moderation/unmute.ts @@ -21,21 +21,24 @@ export default class Unmute { const user = interaction.options.getUser("user")!; const target = interaction.guild?.members.cache.get(user.id)!; - if (await errorCheck( - PermissionsBitField.Flags.ModerateMembers, - { interaction, user, action: "Unmute" }, - { allErrors: false, botError: true, ownerError: false, outsideError: false }, - "Moderate Members" - ) == null) { - if (!target.communicationDisabledUntil) - return await errorEmbed( - interaction, - "You can't unmute this user.", - "The user was never muted." - ); - - await target.edit({ communicationDisabledUntil: null }).catch(error => console.error(error)); - await modEmbed({ interaction, user, action: "Unmuted" }, undefined, true); - } + if ( + await errorCheck( + PermissionsBitField.Flags.ModerateMembers, + { interaction, user, action: "Unmute" }, + { allErrors: false, botError: true }, + "Moderate Members" + ) + ) + return; + + if (!target.communicationDisabledUntil) + return await errorEmbed( + interaction, + "You can't unmute this user.", + "The user was never muted." + ); + + await target.edit({ communicationDisabledUntil: null }).catch(error => console.error(error)); + await modEmbed({ interaction, user, action: "Unmuted" }, undefined); } } diff --git a/src/commands/serverboard.ts b/src/commands/serverboard.ts index 645f593..60c0a29 100644 --- a/src/commands/serverboard.ts +++ b/src/commands/serverboard.ts @@ -54,6 +54,7 @@ export default class Serverboard { ); const reply = await interaction.reply({ embeds: [await getEmbed()], components: [row] }); + if (pages == 1) return; reply .createMessageComponentCollector({ time: 60000 }) .on("collect", async (i: ButtonInteraction) => { diff --git a/src/events/guildCreate.ts b/src/events/guildCreate.ts index 5710181..bec928c 100644 --- a/src/events/guildCreate.ts +++ b/src/events/guildCreate.ts @@ -1,7 +1,6 @@ import { EmbedBuilder, Guild, type Client, type DMChannel } from "discord.js"; import { Commands } from "../handlers/commands"; import { genColor } from "../utils/colorGen"; -import { getSetting, setSetting, settingsDefinition } from "../utils/database/settings"; import { randomise } from "../utils/randomise"; export default { @@ -20,8 +19,13 @@ export default { let emojis = ["💖", "💝", "💓", "💗", "💘", "💟", "💕", "💞"]; if (Math.round(Math.random() * 100) <= 5) emojis = ["⌨️", "💻", "🖥️"]; + const client = guild.client; const embed = new EmbedBuilder() - .setTitle("Welcome to Sokora!") + .setAuthor({ + name: client.user.username, + iconURL: client.user.displayAvatarURL() + }) + .setTitle("Welcome!") .setDescription( [ "Sokora is a multipurpose Discord bot that lets you manage your servers easily.", @@ -30,16 +34,10 @@ export default { ].join("\n") ) .setFooter({ text: `Made with ${randomise(emojis)} by the Sokora team` }) + .setThumbnail(client.user.displayAvatarURL()) .setColor(genColor(200)); - await new Commands(guild.client).registerCommandsForGuild(guild); - for (const key in settingsDefinition) - for (const setting in settingsDefinition[key]) { - if (settingsDefinition[key][setting].type != "LIST") continue; - if (!getSetting(guild.id, key, setting)) continue; - setSetting(guild.id, key, setting, settingsDefinition[key][setting].val); - } - + await new Commands(client).registerCommandsForGuild(guild); if (dmChannel) await dmChannel.send({ embeds: [embed] }); } } diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts index f275366..668bb87 100644 --- a/src/events/messageCreate.ts +++ b/src/events/messageCreate.ts @@ -1,15 +1,13 @@ -import { EmbedBuilder, type TextChannel, type Message } from "discord.js"; -import { pathToFileURL } from "url"; -import { join } from "path"; +import { EmbedBuilder, type Message, type TextChannel } from "discord.js"; import { readdirSync } from "fs"; +import { join } from "path"; +import { pathToFileURL } from "url"; import { genColor } from "../utils/colorGen"; -import { getSetting, setSetting } from "../utils/database/settings"; import { getLevel, setLevel } from "../utils/database/levelling"; import { get as getLevelRewards } from "../utils/database/levelRewards"; +import { getSetting, setSetting } from "../utils/database/settings"; import { kominator } from "../utils/kominator"; -const cooldowns = new Map(); - export default { name: "messageCreate", event: class MessageCreate { @@ -43,20 +41,18 @@ export default { for (const channelID of kominator(blockedChannels)) if (message.channelId == channelID) return; - const cooldown = getSetting(guild.id, "levelling", "set_cooldown") as number || 0; + const cooldowns = new Map(); + const cooldown = getSetting(guild.id, "levelling", "set_cooldown") as number; + const multiplier = getSetting(guild.id, "levelling", "add_multiplier"); let expGain = getSetting(guild.id, "levelling", "set_xp_gain") as number; - const multiplier = getSetting(guild.id, "levelling", "add_multiplier") as string; if (cooldown > 0) { const key = `${guild.id}-${author.id}`; const lastExpTime = cooldowns.get(key) || 0; const now = Date.now(); - if (now - lastExpTime < cooldown * 1000) { - return; - } else { - cooldowns.set(key, now); - } + if (now - lastExpTime < cooldown * 1000) return; + else cooldowns.set(key, now); } if (multiplier) { diff --git a/src/utils/database/moderation.ts b/src/utils/database/moderation.ts index 5eb8fdd..845c0dd 100644 --- a/src/utils/database/moderation.ts +++ b/src/utils/database/moderation.ts @@ -9,30 +9,25 @@ const definition = { type: "TEXT", moderator: "TEXT", reason: "TEXT", - public: "BOOL", id: "TEXT", timestamp: "TIMESTAMP", expiresAt: "TIMESTAMP" } } satisfies TableDefinition; -type modType = "MUTE" | "WARN" | "KICK" | "BAN"; +export type modType = "MUTE" | "WARN" | "KICK" | "BAN"; const database = getDatabase(definition); const addQuery = database.query( - "INSERT INTO moderation (guild, user, type, moderator, reason, public, id, timestamp, expiresAt) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9);" -); -const listUserQuery = database.query( - "SELECT * FROM moderation WHERE guild = $1 AND user = $2;" + "INSERT INTO moderation (guild, user, type, moderator, reason, id, timestamp, expiresAt) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8);" ); +const listUserQuery = database.query("SELECT * FROM moderation WHERE guild = $1 AND user = $2;"); const listUserTypeQuery = database.query( "SELECT * FROM moderation WHERE guild = $1 AND user = $2 AND type = $3;" ); const listModQuery = database.query( "SELECT * FROM moderation WHERE guild = $1 AND moderator = $2;" ); -const getIdQuery = database.query( - "SELECT * FROM moderation WHERE guild = $1 AND id = $2;" -); +const getIdQuery = database.query("SELECT * FROM moderation WHERE guild = $1 AND id = $2;"); const removeQuery = database.query("DELETE FROM moderation WHERE guild = $1 AND id = $2"); export function addModeration( @@ -41,11 +36,10 @@ export function addModeration( type: modType, moderator: string, reason = "", - pub = false, expiresAt?: number | null ) { const id = crypto.randomUUID(); - addQuery.run(guildID, userID, type, moderator, reason, pub, id, Date.now(), expiresAt ?? null); + addQuery.run(guildID, userID, type, moderator, reason, id, Date.now(), expiresAt ?? null); return id; } @@ -54,13 +48,15 @@ export function listUserModeration( userID: number | string, type?: modType ) { - if (type) return listUserTypeQuery.all(guildID, userID, type) as TypeOfDefinition[]; + if (type) + return listUserTypeQuery.all(guildID, userID, type) as TypeOfDefinition[]; + return listUserQuery.all(guildID, userID) as TypeOfDefinition[]; } export function getModeration(guildID: number | string, userID: number | string, id: string) { - var thecase = getIdQuery.all(guildID, id) as TypeOfDefinition[]; - if (thecase[0].user == userID) return thecase; + const modCase = getIdQuery.all(guildID, id) as TypeOfDefinition[]; + if (modCase[0].user == userID) return modCase; return []; } diff --git a/src/utils/database/settings.ts b/src/utils/database/settings.ts index 62406bc..e164869 100644 --- a/src/utils/database/settings.ts +++ b/src/utils/database/settings.ts @@ -90,13 +90,13 @@ export const settingsDefinition: Record< channel: { type: "TEXT", desc: "ID of the channel where welcome messages are sent." }, join_dm: { type: "BOOL", - desc: "Whether to send a custom DM message to the user upon joinning", + desc: "Whether or not the bot should send a custom DM message to the user upon joining.", val: false }, dm_text: { type: "TEXT", - desc: "Text sent in the user's dm when they join the server. Same replacements as leave_text." - } // dm_text is already join_text by default if nothing is supplied, see guildMemberAdd.ts + desc: "Text sent in the user's DM when they join the server. Same syntax as join_text." + } } }; @@ -119,9 +119,15 @@ export function getSetting( let res = getQuery.all(JSON.stringify(guildID), key + "." + setting) as TypeOfDefinition< typeof tableDefinition >[]; + const set = settingsDefinition[key][setting]; if (!res.length) return null; - switch (settingsDefinition[key][setting].type) { + if (res[0].value == "") { + if (set.type == "LIST") return null; + return set.val; + } + + switch (set.type) { case "TEXT": return res[0].value as TypeOfKey; case "BOOL": @@ -129,7 +135,7 @@ export function getSetting( case "INTEGER": return parseInt(res[0].value) as TypeOfKey; case "LIST": - return kominator(res[0].value) as TypeOfKey; + return kominator(res[0].value); default: return "WIP" as TypeOfKey; } diff --git a/src/utils/embeds/modEmbed.ts b/src/utils/embeds/modEmbed.ts index 29da4b5..740f9d1 100644 --- a/src/utils/embeds/modEmbed.ts +++ b/src/utils/embeds/modEmbed.ts @@ -1,6 +1,7 @@ -import { EmbedBuilder, type ChatInputCommandInteraction, type User } from "discord.js"; +import { EmbedBuilder, inlineCode, type ChatInputCommandInteraction, type User } from "discord.js"; import ms from "ms"; import { genColor } from "../colorGen"; +import { addModeration, type modType } from "../database/moderation"; import { logChannel } from "../logChannel"; import { errorEmbed } from "./errorEmbed"; @@ -9,13 +10,16 @@ type Options = { user: User; action: string; duration?: string | null; + dm?: boolean; + dbAction?: modType; + expiresAt?: number; }; type ErrorOptions = { allErrors: boolean; botError: boolean; - ownerError: boolean; - outsideError: boolean; + ownerError?: boolean; + outsideError?: boolean; }; export async function errorCheck( @@ -80,7 +84,12 @@ export async function errorCheck( } if (outsideError) { - if (!await guild.members.fetch(user.id).then(() => true).catch(() => false)) + if ( + !(await guild.members + .fetch(user.id) + .then(() => true) + .catch(() => false)) + ) return await errorEmbed( interaction, `You can't ${action.toLowerCase()} ${name}.`, @@ -89,41 +98,58 @@ export async function errorCheck( } } -export async function modEmbed(options: Options, reason?: string | null, date?: boolean, showModerator: boolean = false) { - const { interaction, user, action, duration } = options; +export async function modEmbed( + options: Options, + reason?: string | null, + showModerator: boolean = false +) { + const { interaction, user, action, duration, dm, dbAction, expiresAt } = options; const guild = interaction.guild!; const name = user.displayName; + const generalValues = [`Responsible moderator is ${interaction.user.displayName}`]; + reason + ? generalValues.push(`The **reason** is ${inlineCode(reason)}`) + : generalValues.push("*No reason provided*"); - const generalValues = [`**Moderator**: <@${interaction.user.id}>`]; if (duration) generalValues.push(`**Duration**: ${ms(ms(duration), { long: true })}`); - if (reason) generalValues.push(`**Reason**: ${reason}`); - if (date) generalValues.push(`**Date**: `); + const footer = [`User ID: ${user.id}`]; + if (dbAction) { + try { + const id = addModeration( + guild.id, + user.id, + dbAction, + guild.members.cache.get(interaction.user.id)?.id!, + reason ?? undefined, + expiresAt ?? undefined + ); + footer.push(`Case ID: ${id}`); + } catch (error) { + console.error(error); + } + } const embed = new EmbedBuilder() - .setAuthor({ name: `• ${name}`, iconURL: user.displayAvatarURL() }) - .setTitle(`${action} ${name}.`) + .setAuthor({ name: `• ${action} ${name}`, iconURL: user.displayAvatarURL() }) .setDescription(generalValues.join("\n")) - .setThumbnail(user.displayAvatarURL()) - .setFooter({ text: `User ID: ${user.id}` }) + .setFooter({ text: footer.join("\n") }) .setColor(genColor(100)); await logChannel(guild, embed); - if (!interaction.deferred && !interaction.replied) { // fixes any interaction has been sent or deferred error. - await interaction.reply({ embeds: [embed] }); - } else { - await interaction.followUp({ embeds: [embed] }); - } + if (interaction.replied) await interaction.followUp({ embeds: [embed] }); + else await interaction.reply({ embeds: [embed] }); + if (!dm) return; const dmChannel = await user.createDM().catch(() => null); - if (!dmChannel) return; - if (!guild.members.cache.get(user.id)) return; - if (user.bot) return; + if (!dmChannel || !guild.members.cache.get(user.id) || user.bot) return; await dmChannel .send({ embeds: [ - embed.setTitle(`You got ${action.toLowerCase()}.`) - .setDescription(generalValues.slice(+!showModerator, generalValues.length).join("\n")) - .setColor(genColor(0))] + embed + .setTitle(`You got ${action.toLowerCase()}.`) + .setDescription(generalValues.slice(+!showModerator, generalValues.length).join("\n")) + .setColor(genColor(0)) + ] }) .catch(() => null); } diff --git a/src/utils/embeds/serverEmbed.ts b/src/utils/embeds/serverEmbed.ts index 4ea3083..54bf3ac 100644 --- a/src/utils/embeds/serverEmbed.ts +++ b/src/utils/embeds/serverEmbed.ts @@ -49,7 +49,7 @@ export async function serverEmbed(options: Options) { }) .setDescription(guild.description ? guild.description : null) .setFields({ name: "📃 • General", value: generalValues.join("\n") }) - .setFooter({ text: `Server ID: ${guild.id}${pages ? ` • Page ${page}/${pages}` : ""}` }) + .setFooter({ text: `${pages ? `Page ${page}/${pages}\n` : ""}Server ID: ${guild.id}` }) .setThumbnail(guild.iconURL()) .setColor((await imageColor(guild)) ?? genColor(200)); diff --git a/src/utils/unbanScheduler.ts b/src/utils/unbanScheduler.ts index cde9f74..18dec97 100644 --- a/src/utils/unbanScheduler.ts +++ b/src/utils/unbanScheduler.ts @@ -1,17 +1,13 @@ import { Client } from "discord.js"; import { removeModeration, getPendingBans } from "./database/moderation"; -const scheduledUnbans = new Map(); - export function scheduleUnban(client: Client, guildId: string, userId: string, delay: number) { + const scheduledUnbans = new Map(); const key = `${guildId}-${userId}`; - - if (scheduledUnbans.has(key)) { - clearTimeout(scheduledUnbans.get(key)!); - } + if (scheduledUnbans.has(key)) clearTimeout(scheduledUnbans.get(key)!); // 24.8 days because why not - const maxDelay = 2147483647; + const maxDelay = 2147483647; if (delay > maxDelay) { const remainingDelay = delay - maxDelay; @@ -41,22 +37,15 @@ export function rescheduleUnbans(client: Client) { const pendingBans = getPendingBans(now); for (const ban of pendingBans) { - if (!ban.expiresAt || ban.expiresAt === 0) { - continue; - } + if (!ban.expiresAt || ban.expiresAt == 0) continue; - if (typeof ban.expiresAt !== 'number' || isNaN(ban.expiresAt)) { + if (typeof ban.expiresAt !== "number" || isNaN(ban.expiresAt)) { console.error(`Invalid expiresAt value for ban: ${ban.expiresAt}`); continue; } const delay = ban.expiresAt - now; - - if (delay > 0) { - scheduleUnban(client, ban.guild, ban.user, delay); - } else { - console.log(`Ban for user ${ban.user} has already expired. Removing from records.`); - removeModeration(ban.guild, ban.id); - } + if (delay > 0) scheduleUnban(client, ban.guild, ban.user, delay); + else removeModeration(ban.guild, ban.id); } } From 71b62896ccf87b3c1e1fd96fe95d374305d0c992 Mon Sep 17 00:00:00 2001 From: MrSerge01 <86667481+MrSerge01@users.noreply.github.com> Date: Sun, 29 Sep 2024 12:12:18 +0500 Subject: [PATCH 110/127] eternal suffering, pt2 --- bun.lockb | Bin 52571 -> 52571 bytes package.json | 2 +- src/commands/leaderboard.ts | 42 ++++++++++------------------ src/commands/moderation/ban.ts | 11 ++++---- src/commands/moderation/cases.ts | 2 +- src/commands/moderation/delwarn.ts | 11 +++----- src/commands/moderation/kick.ts | 42 ++++++++++------------------ src/commands/moderation/lock.ts | 5 ++-- src/commands/moderation/mute.ts | 2 -- src/commands/moderation/purge.ts | 20 ++++++------- src/commands/moderation/slowdown.ts | 9 +++--- src/commands/moderation/unban.ts | 4 +-- src/commands/moderation/unlock.ts | 5 ++-- src/commands/moderation/unmute.ts | 3 +- src/commands/moderation/warn.ts | 42 +++++++++++++--------------- src/events/guildMemberRemove.ts | 1 - src/events/messageDelete.ts | 1 - src/events/messageUpdate.ts | 1 - src/utils/database/levelRewards.ts | 4 ++- src/utils/database/levelling.ts | 8 ++---- src/utils/embeds/modEmbed.ts | 4 +-- 21 files changed, 89 insertions(+), 130 deletions(-) diff --git a/bun.lockb b/bun.lockb index 0bd2ebffb8a5b3e2223a18fb7509e98d358a6dda..1ac37415886c70de587ee2b48705349fb8a62acf 100644 GIT binary patch delta 192 zcmV;x06+iRngiRK1CTBtT#Hgy5U<#`2h)f=cKONIE8#;K7T-~vW9|SBb)gl~u}-Qg z0UeVe2`954E1Wk$1QLD!Pc)A({H5hdR0mTB%z9j{klK+syO}Mt-Qwt9SHYl0w>@u~ zNiPNhrAh3IKbw9T=7 - option.setName("page").setDescription("Page number to display.") - ); + .addNumberOption(option => option.setName("page").setDescription("Page number to display.")); } async run(interaction: ChatInputCommandInteraction) { const guildID = interaction.guild?.id; - if (!guildID) { return errorEmbed(interaction, "This command can only be used in a server."); } const leaderboardData = getGuildLeaderboard(guildID); - if (!leaderboardData.length) { return errorEmbed( interaction, @@ -43,11 +39,10 @@ export default class Leaderboard { if (b.level != a.level) return b.level - a.level; else return b.exp - a.exp; }); - + const totalPages = Math.ceil(leaderboardData.length / 5); let page = interaction.options.getNumber("page") || 1; page = Math.max(1, Math.min(page, totalPages)); - const generateEmbed = async () => { const start = (page - 1) * 5; const end = start + 5; @@ -62,7 +57,7 @@ export default class Leaderboard { const user = await interaction.client.users.fetch(userData.user); embed.addFields({ name: `${start + i + 1}. ${user.tag}`, - value: `Level: ${Math.floor(userData.level)} | EXP: ${Math.floor(userData.exp)}`, + value: `Level: ${Math.floor(userData.level)} | EXP: ${Math.floor(userData.exp)}` }); } @@ -70,25 +65,19 @@ export default class Leaderboard { }; const row = new ActionRowBuilder().addComponents( - new ButtonBuilder() - .setCustomId("left") - .setEmoji("⬅️") - .setStyle(ButtonStyle.Primary), - new ButtonBuilder() - .setCustomId("right") - .setEmoji("➡️") - .setStyle(ButtonStyle.Primary) + new ButtonBuilder().setCustomId("left").setEmoji("⬅️").setStyle(ButtonStyle.Primary), + new ButtonBuilder().setCustomId("right").setEmoji("➡️").setStyle(ButtonStyle.Primary) ); const reply = await interaction.reply({ embeds: [await generateEmbed()], components: totalPages > 1 ? [row] : [], - fetchReply: true, + fetchReply: true }); if (totalPages > 1) { const collector = reply.createMessageComponentCollector({ - time: 60000, + time: 60000 }); collector.on("collect", async (i: ButtonInteraction) => { @@ -96,15 +85,12 @@ export default class Leaderboard { return errorEmbed(i, "You are not the author of this command."); } - if (i.customId === "left") { - page = page > 1 ? page - 1 : totalPages; - } else if (i.customId === "right") { - page = page < totalPages ? page + 1 : 1; - } + if (i.customId == "left") page = page > 1 ? page - 1 : totalPages; + else page = page < totalPages ? page + 1 : 1; await i.update({ embeds: [await generateEmbed()], - components: [row], + components: [row] }); }); diff --git a/src/commands/moderation/ban.ts b/src/commands/moderation/ban.ts index e4c4c3c..277877e 100644 --- a/src/commands/moderation/ban.ts +++ b/src/commands/moderation/ban.ts @@ -3,9 +3,9 @@ import { SlashCommandSubcommandBuilder, type ChatInputCommandInteraction } from "discord.js"; -import { errorCheck, modEmbed } from "../../utils/embeds/modEmbed"; -import { errorEmbed } from "../../utils/embeds/errorEmbed"; import ms from "ms"; +import { errorEmbed } from "../../utils/embeds/errorEmbed"; +import { errorCheck, modEmbed } from "../../utils/embeds/modEmbed"; import { scheduleUnban } from "../../utils/unbanScheduler"; export default class Ban { @@ -28,14 +28,13 @@ export default class Ban { const guild = interaction.guild!; const duration = interaction.options.getString("duration"); const reason = interaction.options.getString("reason"); - if ( - !(await errorCheck( + await errorCheck( PermissionsBitField.Flags.BanMembers, { interaction, user, action: "Ban" }, { allErrors: true, botError: true, ownerError: true }, "Ban Members" - )) + ) ) return; @@ -45,7 +44,7 @@ export default class Ban { if (!durationMs) { return await errorEmbed( interaction, - `You can't ban ${user.username} temporarily.`, + `You can't ban ${user.displayName} temporarily.`, "The duration is invalid." ); } diff --git a/src/commands/moderation/cases.ts b/src/commands/moderation/cases.ts index 773062b..f7ad0da 100644 --- a/src/commands/moderation/cases.ts +++ b/src/commands/moderation/cases.ts @@ -70,7 +70,7 @@ export default class Cases { const actionValues = [ `Responsible moderator is <@${action.moderator}>`, action.reason - ? `The **reason** is ${inlineCode(action.reason)}` + ? `**Reason** provided is ${inlineCode(action.reason)}` : "*No reason provided*", `-# Action happened on ****` ]; diff --git a/src/commands/moderation/delwarn.ts b/src/commands/moderation/delwarn.ts index 0929c28..d01645a 100644 --- a/src/commands/moderation/delwarn.ts +++ b/src/commands/moderation/delwarn.ts @@ -35,14 +35,13 @@ export default class Delwarn { const id = interaction.options.getNumber("id"); const warns = listUserModeration(guild.id, user.id, "WARN"); const newWarns = warns.filter(warn => warn.id != `${id}`); - if ( - (await errorCheck( + await errorCheck( PermissionsBitField.Flags.ModerateMembers, { interaction, user, action: "Remove a warn" }, { allErrors: true, botError: false }, "Moderate Members" - )) !== null + ) ) return; @@ -50,10 +49,8 @@ export default class Delwarn { return await errorEmbed(interaction, `There is no warn with the id of ${id}.`); const embed = new EmbedBuilder() - .setAuthor({ name: `• ${name}`, iconURL: user.displayAvatarURL() }) - .setTitle(`Removed a warning from ${name}.`) - .setDescription(`**Moderator**: ${interaction.user.displayName}`) - .setThumbnail(user.displayAvatarURL()) + .setAuthor({ name: `• Removed a warning from ${name}`, iconURL: user.displayAvatarURL() }) + .setDescription(`Moderator responsible is **${interaction.user.displayName}**`) .setFooter({ text: `User ID: ${user.id}` }) .setColor(genColor(100)); diff --git a/src/commands/moderation/kick.ts b/src/commands/moderation/kick.ts index 0f7f056..e302404 100644 --- a/src/commands/moderation/kick.ts +++ b/src/commands/moderation/kick.ts @@ -4,7 +4,6 @@ import { type ChatInputCommandInteraction } from "discord.js"; import { errorCheck, modEmbed } from "../../utils/embeds/modEmbed"; -import { addModeration } from "../../utils/database/moderation"; export default class Kick { data: SlashCommandSubcommandBuilder; @@ -22,33 +21,22 @@ export default class Kick { async run(interaction: ChatInputCommandInteraction) { const user = interaction.options.getUser("user")!; - const guild = interaction.guild!; + if ( + await errorCheck( + PermissionsBitField.Flags.KickMembers, + { interaction, user, action: "Kick" }, + { allErrors: true, botError: true, ownerError: true, outsideError: true }, + "Kick Members" + ) + ) + return; - if (await errorCheck( - PermissionsBitField.Flags.KickMembers, - { interaction, user, action: "Kick" }, - { allErrors: true, botError: true, ownerError: true, outsideError: true }, - "Kick Members" - ) == null) { - const reason = interaction.options.getString("reason"); - await interaction.guild?.members.cache - .get(user.id) - ?.kick(reason ?? undefined) - .catch(error => console.error(error)); - - try { - addModeration( - guild.id, - user.id, - "KICK", - guild.members.cache.get(interaction.user.id)?.id!, - reason ?? undefined - ); - } catch (error) { - console.error(error); - } + const reason = interaction.options.getString("reason"); + await interaction.guild?.members.cache + .get(user.id) + ?.kick(reason ?? undefined) + .catch(error => console.error(error)); - await modEmbed({ interaction, user, action: "Kicked" }, reason); - } + await modEmbed({ interaction, user, action: "Kicked", dm: true, dbAction: "KICK" }, reason); } } diff --git a/src/commands/moderation/lock.ts b/src/commands/moderation/lock.ts index 5a43262..8099e27 100644 --- a/src/commands/moderation/lock.ts +++ b/src/commands/moderation/lock.ts @@ -43,7 +43,6 @@ export default class Lock { const channelOption = interaction.options.getChannel("channel")!; const channel = guild.channels.cache.get(interaction.channel?.id ?? channelOption.id)!; - if (!channel.permissionsFor(guild.id)?.has("SendMessages")) return await errorEmbed( interaction, @@ -55,8 +54,8 @@ export default class Lock { .setTitle(`Locked a channel.`) .setDescription( [ - `**Moderator**: ${interaction.user.username}`, - `**Channel**: ${channelOption ?? `<#${channel.id}>`}` + `Responsible moderator is **${interaction.user.displayName}**`, + `Locked the **${channelOption ?? `<#${channel.id}>`}** channel` ].join("\n") ) .setColor(genColor(100)); diff --git a/src/commands/moderation/mute.ts b/src/commands/moderation/mute.ts index 26a4fa1..6442573 100644 --- a/src/commands/moderation/mute.ts +++ b/src/commands/moderation/mute.ts @@ -6,7 +6,6 @@ import { import ms from "ms"; import { errorEmbed } from "../../utils/embeds/errorEmbed"; import { errorCheck, modEmbed } from "../../utils/embeds/modEmbed"; -import { addModeration } from "../../utils/database/moderation"; export default class Mute { data: SlashCommandSubcommandBuilder; @@ -32,7 +31,6 @@ export default class Mute { const user = interaction.options.getUser("user")!; const duration = interaction.options.getString("duration")!; const reason = interaction.options.getString("reason"); - if ( await errorCheck( PermissionsBitField.Flags.ModerateMembers, diff --git a/src/commands/moderation/purge.ts b/src/commands/moderation/purge.ts index 4e0d32d..bfb2a19 100644 --- a/src/commands/moderation/purge.ts +++ b/src/commands/moderation/purge.ts @@ -9,22 +9,22 @@ import { genColor } from "../../utils/colorGen"; import { errorEmbed } from "../../utils/embeds/errorEmbed"; import { logChannel } from "../../utils/logChannel"; -export default class Purge { +export default class Clear { data: SlashCommandSubcommandBuilder; constructor() { this.data = new SlashCommandSubcommandBuilder() - .setName("purge") - .setDescription("Purges messages.") + .setName("clear") + .setDescription("Clears messages.") .addNumberOption(number => number .setName("amount") - .setDescription("The amount of messages that you want to purge (maximum is 100).") + .setDescription("The amount of messages that you want to clear (maximum is 100).") .setRequired(true) ) .addChannelOption(channel => channel .setName("channel") - .setDescription("The channel that you want to purge.") + .setDescription("The channel that has the messages that you want to clear.") .addChannelTypes( ChannelType.GuildText, ChannelType.PublicThread, @@ -49,18 +49,18 @@ export default class Purge { const amount = interaction.options.getNumber("amount")!; if (amount > 100) - return await errorEmbed(interaction, "You can only purge up to 100 messages at a time."); + return await errorEmbed(interaction, "You can only clear up to 100 messages at a time."); - if (amount < 1) return await errorEmbed(interaction, "You must purge at least 1 message."); + if (amount < 1) return await errorEmbed(interaction, "You must clear at least 1 message."); const channelOption = interaction.options.getChannel("channel")!; const channel = guild.channels.cache.get(interaction.channel?.id ?? channelOption.id)!; const embed = new EmbedBuilder() - .setTitle(`Purged ${amount} message${amount == 1 ? "" : "s"}.`) + .setTitle(`Cleared ${amount} message${amount == 1 ? "" : "s"}.`) .setDescription( [ - `**Moderator**: ${interaction.user.username}`, - `**Channel**: ${channelOption ?? `<#${channel.id}>`}` + `Moderator responsible is **${interaction.user.displayName}**`, + `Cleared messages of the **${channelOption ?? `<#${channel.id}>** channel`}` ].join("\n") ) .setColor(genColor(100)); diff --git a/src/commands/moderation/slowdown.ts b/src/commands/moderation/slowdown.ts index 9c81133..119ff32 100644 --- a/src/commands/moderation/slowdown.ts +++ b/src/commands/moderation/slowdown.ts @@ -3,6 +3,7 @@ import { EmbedBuilder, PermissionsBitField, SlashCommandSubcommandBuilder, + inlineCode, type ChatInputCommandInteraction } from "discord.js"; import ms from "ms"; @@ -54,9 +55,9 @@ export default class Slowdown { ); const time = interaction.options.getString("time")!; + const reason = interaction.options.getString("reason"); const channelOption = interaction.options.getChannel("channel")!; const channel = guild.channels.cache.get(interaction.channel?.id ?? channelOption.id)!; - let title = `Set a slowdown of \`${channelOption ?? `${channel.name}`}\` to ${ms(ms(time), { long: true })}.`; @@ -66,9 +67,9 @@ export default class Slowdown { .setTitle(title) .setDescription( [ - `**Moderator**: ${interaction.user.username}`, - `**Reason**: ${interaction.options.getString("reason") ?? "No reason provided"}`, - `**Channel**: ${channelOption ?? `<#${channel.id}>`}` + `Moderator responsible is **${interaction.user.displayName}**`, + reason ? `**Reason** provided is ${inlineCode(reason)}` : "*No reason provided*", + `${ms(time) ? "Slowed the" : "Removed the slowdown from the"} **${channelOption ?? `<#${channel.id}>** ${ms(time) ? "channel down" : "channel"}`}` ].join("\n") ) .setColor(genColor(100)); diff --git a/src/commands/moderation/unban.ts b/src/commands/moderation/unban.ts index 3f9e1e7..922d1f3 100644 --- a/src/commands/moderation/unban.ts +++ b/src/commands/moderation/unban.ts @@ -32,12 +32,12 @@ export default class Unban { .filter(user => user.id == id)[0]!; if ( - (await errorCheck( + await errorCheck( PermissionsBitField.Flags.BanMembers, { interaction, user: target, action: "Unban" }, { allErrors: false, botError: true, ownerError: true }, "Ban Members" - )) !== null + ) ) return; diff --git a/src/commands/moderation/unlock.ts b/src/commands/moderation/unlock.ts index 8a094c8..e27c51b 100644 --- a/src/commands/moderation/unlock.ts +++ b/src/commands/moderation/unlock.ts @@ -43,7 +43,6 @@ export default class Unlock { const channelOption = interaction.options.getChannel("channel")!; const channel = guild.channels.cache.get(interaction.channel?.id ?? channelOption.id)!; - if (channel.permissionsFor(guild.id)?.has("SendMessages")) return await errorEmbed( interaction, @@ -55,8 +54,8 @@ export default class Unlock { .setTitle(`Unlocked a channel.`) .setDescription( [ - `**Moderator**: ${interaction.user.username}`, - `**Channel**: ${channelOption ?? `<#${channel.id}>`}` + `Moderator responsible is **${interaction.user.displayName}**`, + `Unlocked the **${channelOption ?? `<#${channel.id}>`}** channel` ].join("\n") ) .setColor(genColor(100)); diff --git a/src/commands/moderation/unmute.ts b/src/commands/moderation/unmute.ts index e9f410c..622e95b 100644 --- a/src/commands/moderation/unmute.ts +++ b/src/commands/moderation/unmute.ts @@ -20,7 +20,6 @@ export default class Unmute { async run(interaction: ChatInputCommandInteraction) { const user = interaction.options.getUser("user")!; const target = interaction.guild?.members.cache.get(user.id)!; - if ( await errorCheck( PermissionsBitField.Flags.ModerateMembers, @@ -39,6 +38,6 @@ export default class Unmute { ); await target.edit({ communicationDisabledUntil: null }).catch(error => console.error(error)); - await modEmbed({ interaction, user, action: "Unmuted" }, undefined); + await modEmbed({ interaction, user, action: "Unmuted" }); } } diff --git a/src/commands/moderation/warn.ts b/src/commands/moderation/warn.ts index 1ac73dd..a458908 100644 --- a/src/commands/moderation/warn.ts +++ b/src/commands/moderation/warn.ts @@ -3,7 +3,6 @@ import { SlashCommandSubcommandBuilder, type ChatInputCommandInteraction } from "discord.js"; -import { addModeration } from "../../utils/database/moderation"; import { errorCheck, modEmbed } from "../../utils/embeds/modEmbed"; export default class Warn { @@ -19,35 +18,32 @@ export default class Warn { string.setName("reason").setDescription("The reason for the warn.") ) .addBooleanOption(bool => - bool.setName("show_moderator").setDescription("Inform the warned user of the moderator that took the action. Defaults to false") + bool + .setName("show_moderator") + .setDescription( + "Inform the warned user of the moderator that took the action. Defaults to false." + ) ); } async run(interaction: ChatInputCommandInteraction) { const user = interaction.options.getUser("user")!; - const guild = interaction.guild!; const reason = interaction.options.getString("reason"); const showModerator = interaction.options.getBoolean("show_moderator") ?? false; + if ( + await errorCheck( + PermissionsBitField.Flags.ModerateMembers, + { interaction, user, action: "Warn" }, + { allErrors: true, botError: false, ownerError: true, outsideError: true }, + "Moderate Members" + ) + ) + return; - if (await errorCheck( - PermissionsBitField.Flags.ModerateMembers, - { interaction, user, action: "Warn" }, - { allErrors: true, botError: false, ownerError: true, outsideError: true }, - "Moderate Members" - ) == null) { - try { - addModeration( - guild.id, - user.id, - "WARN", - guild.members.cache.get(interaction.user.id)?.id!, - reason ?? undefined - ); - } catch (error) { - console.error(error); - } - - await modEmbed({ interaction, user, action: "Warned" }, reason, false, showModerator); - } + await modEmbed( + { interaction, user, action: "Warned", dm: true, dbAction: "WARN" }, + reason, + showModerator + ); } } diff --git a/src/events/guildMemberRemove.ts b/src/events/guildMemberRemove.ts index 169b0c3..047b1df 100644 --- a/src/events/guildMemberRemove.ts +++ b/src/events/guildMemberRemove.ts @@ -33,7 +33,6 @@ export default { .setTitle("Goodbye!") .setDescription(text ?? `**@${user.displayName}** has left the server 😥`) .setFooter({ text: `User ID: ${member.id}` }) - .setThumbnail(avatarURL) .setColor( member.user.hexAccentColor ?? (await imageColor(undefined, member)) ?? genColor(200) ); diff --git a/src/events/messageDelete.ts b/src/events/messageDelete.ts index 5c16d62..ea54808 100644 --- a/src/events/messageDelete.ts +++ b/src/events/messageDelete.ts @@ -21,7 +21,6 @@ export default { value: codeBlock(message.content) }) .setFooter({ text: `Message ID: ${message.id}\nUser ID: ${message.author.id}` }) - .setThumbnail(author.displayAvatarURL()) .setColor(genColor(60)); await logChannel(guild, embed); diff --git a/src/events/messageUpdate.ts b/src/events/messageUpdate.ts index 8284812..c324657 100644 --- a/src/events/messageUpdate.ts +++ b/src/events/messageUpdate.ts @@ -31,7 +31,6 @@ export default { } ) .setFooter({ text: `Message ID: ${oldMessage.id}\nUser ID: ${oldMessage.author.id}` }) - .setThumbnail(author.displayAvatarURL()) .setColor(genColor(60)); await logChannel(guild, embed); diff --git a/src/utils/database/levelRewards.ts b/src/utils/database/levelRewards.ts index a020e83..9255f80 100644 --- a/src/utils/database/levelRewards.ts +++ b/src/utils/database/levelRewards.ts @@ -13,7 +13,9 @@ const tableDefinition = { const database = getDatabase(tableDefinition); const getQuery = database.query("SELECT * FROM levelRewards WHERE guild = $1;"); -const addQuery = database.query("INSERT INTO levelRewards (guild, roleID, level) VALUES (?1, ?2, ?3);"); +const addQuery = database.query( + "INSERT INTO levelRewards (guild, roleID, level) VALUES (?1, ?2, ?3);" +); const removeQuery = database.query("DELETE FROM levelRewards WHERE guild = $1 AND roleID = $2"); export function get(guildID: string) { diff --git a/src/utils/database/levelling.ts b/src/utils/database/levelling.ts index f4953c3..ef0f61f 100644 --- a/src/utils/database/levelling.ts +++ b/src/utils/database/levelling.ts @@ -7,8 +7,8 @@ const tableDefinition = { guild: "TEXT", user: "TEXT", level: "INTEGER", - exp: "INTEGER", - }, + exp: "INTEGER" + } } satisfies TableDefinition; const database = getDatabase(tableDefinition); @@ -32,8 +32,6 @@ export function setLevel(guildID: string | number, userID: string, level: number insertQuery.run(guildID, userID, level, exp); } -export function getGuildLeaderboard( - guildID: string -): TypeOfDefinition[] { +export function getGuildLeaderboard(guildID: string): TypeOfDefinition[] { return getGuildQuery.all(guildID) as TypeOfDefinition[]; } diff --git a/src/utils/embeds/modEmbed.ts b/src/utils/embeds/modEmbed.ts index 740f9d1..c31ccf6 100644 --- a/src/utils/embeds/modEmbed.ts +++ b/src/utils/embeds/modEmbed.ts @@ -106,9 +106,9 @@ export async function modEmbed( const { interaction, user, action, duration, dm, dbAction, expiresAt } = options; const guild = interaction.guild!; const name = user.displayName; - const generalValues = [`Responsible moderator is ${interaction.user.displayName}`]; + const generalValues = [`Responsible moderator is **${interaction.user.displayName}**`]; reason - ? generalValues.push(`The **reason** is ${inlineCode(reason)}`) + ? generalValues.push(`**Reason** provided is ${inlineCode(reason)}`) : generalValues.push("*No reason provided*"); if (duration) generalValues.push(`**Duration**: ${ms(ms(duration), { long: true })}`); From edc85d7c03c82aa067115aa788c309ba67799f3f Mon Sep 17 00:00:00 2001 From: MrSerge01 <86667481+MrSerge01@users.noreply.github.com> Date: Sun, 13 Oct 2024 22:59:04 +0500 Subject: [PATCH 111/127] eternal suffering, pt3 --- bun.lockb | Bin 52571 -> 52960 bytes package.json | 6 +- src/commands/about.ts | 59 ---------- src/commands/leaderboard.ts | 102 ----------------- src/commands/moderation/ban.ts | 67 ----------- src/commands/moderation/cases.ts | 95 ---------------- src/commands/moderation/delwarn.ts | 70 ------------ src/commands/moderation/kick.ts | 42 ------- src/commands/moderation/lock.ts | 80 -------------- src/commands/moderation/mute.ts | 66 ----------- src/commands/moderation/purge.ts | 85 -------------- src/commands/moderation/slowdown.ts | 90 --------------- src/commands/moderation/unban.ts | 54 --------- src/commands/moderation/unlock.ts | 80 -------------- src/commands/moderation/unmute.ts | 43 -------- src/commands/moderation/warn.ts | 49 --------- src/commands/news/add.ts | 82 -------------- src/commands/news/edit.ts | 112 ------------------- src/commands/news/remove.ts | 55 ---------- src/commands/news/view.ts | 91 --------------- src/commands/server.ts | 16 --- src/commands/serverboard.ts | 85 -------------- src/commands/settings.ts | 133 ---------------------- src/commands/user.ts | 165 ---------------------------- src/events/messageCreate.ts | 32 +++--- src/utils/database/settings.ts | 24 ++-- src/utils/embeds/modEmbed.ts | 8 +- src/utils/embeds/serverEmbed.ts | 10 +- src/utils/unbanScheduler.ts | 68 +++++++----- 29 files changed, 79 insertions(+), 1790 deletions(-) delete mode 100644 src/commands/about.ts delete mode 100644 src/commands/leaderboard.ts delete mode 100644 src/commands/moderation/ban.ts delete mode 100644 src/commands/moderation/cases.ts delete mode 100644 src/commands/moderation/delwarn.ts delete mode 100644 src/commands/moderation/kick.ts delete mode 100644 src/commands/moderation/lock.ts delete mode 100644 src/commands/moderation/mute.ts delete mode 100644 src/commands/moderation/purge.ts delete mode 100644 src/commands/moderation/slowdown.ts delete mode 100644 src/commands/moderation/unban.ts delete mode 100644 src/commands/moderation/unlock.ts delete mode 100644 src/commands/moderation/unmute.ts delete mode 100644 src/commands/moderation/warn.ts delete mode 100644 src/commands/news/add.ts delete mode 100644 src/commands/news/edit.ts delete mode 100644 src/commands/news/remove.ts delete mode 100644 src/commands/news/view.ts delete mode 100644 src/commands/server.ts delete mode 100644 src/commands/serverboard.ts delete mode 100644 src/commands/settings.ts delete mode 100644 src/commands/user.ts diff --git a/bun.lockb b/bun.lockb index 1ac37415886c70de587ee2b48705349fb8a62acf..f15464864da4bb236397ddcf6cabd9d4e97ede47 100644 GIT binary patch delta 1407 zcmYk64NOy46vyA|YoQORAYY~oqy<)5Fas>KurfN$rjP&@k-;VzAWH^gFi>G^u<@lx zkgpqIb^Ux?oZ!~JW)Jmkz=1n-U2BbK(^R7B@`8dJA5c~ zQnMC}G#K&VjEkD*Cxt*+G@S}LGA(@)I%HbGBOERU^87Tc76;j3O~pb%4Mu9nbJEb4 zpd&!buJfa)E!N$6v6}ai9|X#yX+7k~we&1>$hEwU{_roIIW<68)#JuCrmA4;`q744|E2* z%zCCLz~|NJ(H`?wQTUwJcw3ab90V5EnO}B(HDhOL9~`Z^^H+cEQ7{7LZ;6i6!Q=cjy z0>{6Wb+r^4s=Zwvs5fa7ek;P zveaLY(Cg%KefjZRWB;~{V#>IA%3Lw==gqGI>d7OZ>$uFU%MK92x^eRS0Z;fQEYv|>w>tG)9sE=KGAQ)v<$P2Gh{bjv zay9Hl*&!V^t=6X5qyHYkm!8twG$DE;LX31LwfJRFq=wiLglH17K+_pky+c%ya!a%X zl=bf1g5Hbg+mCt|atgVQ6d|Nxg{@%R2(o%#5h_-h^gqlc*6-(uN#=*h0i+PgNA@H8 zVCoZ1`3E+&1pPb6`$#rYjJ%7G>Kf!AQjJt0<;cg#M@Sh`iafPh1+FU*QnQv%`mW`Z zzNF?5f_+@APl^->8}JA1q6JPJO{F7&F^17fF2Y#o%!G!7aGlM~G<^#a%^r|t@}?J| z(xl|MOQF|v+$xLdbC7EmTQ}U`OLG$a5bk6A1ngUtv<@__@vC`YYW4g-%V)JkFi6|H zv4OU2HC623+`c-Ft{slG?en~epE3CdI~Wl@OqZ-*sjzLwjBvh|(_T2}$e@LR=?g); c&=5%MmT(6ywFqEuy9C0AJ!vJ8pkVmkf4F+6!T zwy;5W)M42&2)2Z=DdsU13@l6%n4mL|_&3dvIKvV`mc?Z)A+U2xS$2}|oqNtbfA{6x zH}f_BtBr4sj?8I#H_QLh{j1*7rnGX`&-bF9>{JH)w)ZQ;0+E+>ZkU>wByO=-vo>?H zrpuLWeB@{#A^wEae~DUwx~%YUfuVqq@nAyaX!oGrjQR|`A&CiDvx-9rk)bz-C&Ujm zzlD%>EK0OW?(t%=Pqqq19{{bp-Qc% zX)vG`=s9qy1@2}Tq(the0*sNY_CTZ{Yr{ENa|#Vgoow1;;V1z*DhK0hf^28_e?2kc z99n4@F9q!J;uKo$YvGL-(_CeU5eTGAS zA51L!BKE^ygiB7_4-);O{-unBYmFPVTjnF2n%q|}4i5ix(l>Macj~u&f)e^}43%6D zKlEgs@qBs3;6G#7h>~T+GU_}yn3yHzXIJxZG;t3tb(^M~G_N{ILJ_@r`pxt^eILz* zG(Vnmo^>VdeV&q(a=F{!??`HVW_*y+9v+t`|GYL+QGIsX;G?@mP3Gm}`i~zj9GGz_ zC;r#vG@fitB zJN7RgFAissKsIprs~G$N(QF{BVFLGockYieDk$6JB=2vW{=5LWvr!m>X_P9i6e z2Ba3LL8_7CNENc;kvd%0BP?evp4DB8XLVW5DFo}hrg_oJ>FeUi!FvO zcv{53en${3h6;x#%=f;1b=+4RaNAL$vV^bxAWkJEOwvmEa=K%6`^KdBes^ym9>i guild.memberCount).reduce((a, b) => a + b); - const shards = client.shard?.count; - let emojis = ["💖", "💝", "💓", "💗", "💘", "💟", "💕", "💞"]; - if (Math.round(Math.random() * 100) <= 5) emojis = ["⌨️", "💻", "🖥️"]; - - const embed = new EmbedBuilder() - .setAuthor({ name: "• About Sokora", iconURL: client.user.displayAvatarURL() }) - .setDescription( - "Sokora is a multipurpose Discord bot that lets you manage your servers easily." - ) - .setFields( - { - name: "📃 • General", - value: [ - "**Version** 0.1-preview, *Kaishi*", - `**${members}** members • **${guilds.size}** guild${guilds.size == 1 ? "" : "s"} ${ - !shards ? "" : `• **${shards}** shard${shards == 1 ? "" : "s"}` - }` - ].join("\n") - }, - { - name: "🌌 • Entities involved", - value: [ - "**Founder**: Goos", - "**Developers**: Dimkauzh, Froxcey, Golem64, Koslz, MQuery, Nikkerudon, Spectrum, ThatBOI", - "**Designers**: ArtyH, Optix, proJM, Slider_on_the_black", - "**Translators**: Dimkauzh, Golem64, Optix, SaFire, ThatBOI, Trynera", - "**Testers**: Blaze, fishy, flojo, Trynera", - "And **YOU**, for using Sokora." - ].join("\n") - }, - { - name: "🔗 • Links", - value: - "[GitHub](https://www.github.com/NebulaTheBot) • [YouTube](https://www.youtube.com/@NebulaTheBot) • [Instagram](https://instagram.com/NebulaTheBot) • [Mastodon](https://mastodon.online/@NebulaTheBot@mastodon.social) • [Guilded](https://guilded.gg/Nebula) • [Revolt](https://rvlt.gg/28TS9aXy)" - } - ) - .setFooter({ text: `Made with ${randomise(emojis)} by the Sokora team` }) - .setThumbnail(client.user.displayAvatarURL()) - .setColor(genColor(270)); - - await interaction.reply({ embeds: [embed] }); - } -} diff --git a/src/commands/leaderboard.ts b/src/commands/leaderboard.ts deleted file mode 100644 index 8e6a5e7..0000000 --- a/src/commands/leaderboard.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { - ActionRowBuilder, - ButtonBuilder, - ButtonInteraction, - ButtonStyle, - EmbedBuilder, - SlashCommandBuilder, - type ChatInputCommandInteraction, - type SlashCommandOptionsOnlyBuilder -} from "discord.js"; -import { getGuildLeaderboard } from "../utils/database/levelling"; -import { errorEmbed } from "../utils/embeds/errorEmbed"; - -export default class Leaderboard { - data: SlashCommandOptionsOnlyBuilder; - constructor() { - this.data = new SlashCommandBuilder() - .setName("leaderboard") - .setDescription("Displays the guild leaderboard.") - .addNumberOption(option => option.setName("page").setDescription("Page number to display.")); - } - - async run(interaction: ChatInputCommandInteraction) { - const guildID = interaction.guild?.id; - if (!guildID) { - return errorEmbed(interaction, "This command can only be used in a server."); - } - - const leaderboardData = getGuildLeaderboard(guildID); - if (!leaderboardData.length) { - return errorEmbed( - interaction, - "No data found.", - "There is no levelling data for this server yet." - ); - } - - leaderboardData.sort((a, b) => { - if (b.level != a.level) return b.level - a.level; - else return b.exp - a.exp; - }); - - const totalPages = Math.ceil(leaderboardData.length / 5); - let page = interaction.options.getNumber("page") || 1; - page = Math.max(1, Math.min(page, totalPages)); - const generateEmbed = async () => { - const start = (page - 1) * 5; - const end = start + 5; - const pageData = leaderboardData.slice(start, end); - - const embed = new EmbedBuilder() - .setTitle(`Leaderboard - Page ${page}/${totalPages}`) - .setColor("#0099ff"); - - for (let i = 0; i < pageData.length; i++) { - const userData = pageData[i]; - const user = await interaction.client.users.fetch(userData.user); - embed.addFields({ - name: `${start + i + 1}. ${user.tag}`, - value: `Level: ${Math.floor(userData.level)} | EXP: ${Math.floor(userData.exp)}` - }); - } - - return embed; - }; - - const row = new ActionRowBuilder().addComponents( - new ButtonBuilder().setCustomId("left").setEmoji("⬅️").setStyle(ButtonStyle.Primary), - new ButtonBuilder().setCustomId("right").setEmoji("➡️").setStyle(ButtonStyle.Primary) - ); - - const reply = await interaction.reply({ - embeds: [await generateEmbed()], - components: totalPages > 1 ? [row] : [], - fetchReply: true - }); - - if (totalPages > 1) { - const collector = reply.createMessageComponentCollector({ - time: 60000 - }); - - collector.on("collect", async (i: ButtonInteraction) => { - if (i.user.id !== interaction.user.id) { - return errorEmbed(i, "You are not the author of this command."); - } - - if (i.customId == "left") page = page > 1 ? page - 1 : totalPages; - else page = page < totalPages ? page + 1 : 1; - - await i.update({ - embeds: [await generateEmbed()], - components: [row] - }); - }); - - collector.on("end", async () => { - await interaction.editReply({ components: [] }); - }); - } - } -} diff --git a/src/commands/moderation/ban.ts b/src/commands/moderation/ban.ts deleted file mode 100644 index 277877e..0000000 --- a/src/commands/moderation/ban.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { - PermissionsBitField, - SlashCommandSubcommandBuilder, - type ChatInputCommandInteraction -} from "discord.js"; -import ms from "ms"; -import { errorEmbed } from "../../utils/embeds/errorEmbed"; -import { errorCheck, modEmbed } from "../../utils/embeds/modEmbed"; -import { scheduleUnban } from "../../utils/unbanScheduler"; - -export default class Ban { - data: SlashCommandSubcommandBuilder; - constructor() { - this.data = new SlashCommandSubcommandBuilder() - .setName("ban") - .setDescription("Bans a user.") - .addUserOption(user => - user.setName("user").setDescription("The user that you want to ban.").setRequired(true) - ) - .addStringOption(string => string.setName("reason").setDescription("The reason for the ban.")) - .addStringOption(string => - string.setName("duration").setDescription("The duration of the ban (e.g 2mo, 1y).") - ); - } - - async run(interaction: ChatInputCommandInteraction) { - const user = interaction.options.getUser("user")!; - const guild = interaction.guild!; - const duration = interaction.options.getString("duration"); - const reason = interaction.options.getString("reason"); - if ( - await errorCheck( - PermissionsBitField.Flags.BanMembers, - { interaction, user, action: "Ban" }, - { allErrors: true, botError: true, ownerError: true }, - "Ban Members" - ) - ) - return; - - let expiresAt: number | undefined; - if (duration) { - const durationMs = ms(duration); - if (!durationMs) { - return await errorEmbed( - interaction, - `You can't ban ${user.displayName} temporarily.`, - "The duration is invalid." - ); - } - - expiresAt = Date.now() + durationMs; - scheduleUnban(interaction.client, guild.id, user.id, durationMs); - } - - try { - await guild.members.ban(user.id, { reason: reason ?? undefined }); - } catch (err) { - console.error("Failed to ban user:", err); - } - - await modEmbed( - { interaction, user, action: "Banned", duration, dm: true, dbAction: "BAN", expiresAt }, - reason - ); - } -} diff --git a/src/commands/moderation/cases.ts b/src/commands/moderation/cases.ts deleted file mode 100644 index f7ad0da..0000000 --- a/src/commands/moderation/cases.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { - EmbedBuilder, - PermissionsBitField, - SlashCommandSubcommandBuilder, - inlineCode, - type ChatInputCommandInteraction -} from "discord.js"; -import { genColor } from "../../utils/colorGen"; -import { getModeration, listUserModeration } from "../../utils/database/moderation"; -import { errorEmbed } from "../../utils/embeds/errorEmbed"; -import { randomise } from "../../utils/randomise"; - -export default class Cases { - data: SlashCommandSubcommandBuilder; - constructor() { - this.data = new SlashCommandSubcommandBuilder() - .setName("cases") - .setDescription("Moderation cases in a server.") - .addUserOption(user => - user.setName("user").setDescription("The user that you want to see.").setRequired(true) - ) - .addStringOption(string => - string.setName("id").setDescription("The ID of a specific moderation case you want to see.") - ); - } - - async run(interaction: ChatInputCommandInteraction) { - const actionsEmojis: { [key: string]: string } = { - WARN: "⚠️", - MUTE: "🔇", - KICK: "📤", - BAN: "🔨" - }; - - const nothingMsg = [ - "Nothing to see here...", - "Ayay, no cases on this horizon cap'n!", - "Clean as a whistle!", - "0+0=?" - ]; - - const guild = interaction.guild!; - if ( - !guild.members.cache - .get(interaction.user.id) - ?.permissions.has(PermissionsBitField.Flags.ModerateMembers) - ) - return await errorEmbed( - interaction, - "You can't execute this command.", - "You need the **Moderate Members** permission." - ); - - const user = interaction.options.getUser("user")!; - // const warns = listUserModeration(guild.id, user.id, "WARN"); - // const mutes = listUserModeration(guild.id, user.id, "MUTE"); - // const kicks = listUserModeration(guild.id, user.id, "KICK"); - // const bans = listUserModeration(guild.id, user.id, "BAN"); - let actionID = interaction.options.getString("id"); - if (actionID && actionID?.startsWith("#")) actionID = actionID.slice(1); - const actions = actionID - ? getModeration(guild.id, user.id, actionID) - : listUserModeration(guild.id, user.id); - - const embed = new EmbedBuilder() - .setAuthor({ name: `• Cases of ${user.displayName}`, iconURL: user.displayAvatarURL() }) - .setFields( - actions.length > 0 - ? actions.map(action => { - const actionValues = [ - `Responsible moderator is <@${action.moderator}>`, - action.reason - ? `**Reason** provided is ${inlineCode(action.reason)}` - : "*No reason provided*", - `-# Action happened on ****` - ]; - - return { - name: `${actionsEmojis[action.type]} • ${action.type} #${action.id}`, // Include durations ? needs to add a db column - value: actionValues.join("\n") - }; - }) - : [ - { - name: `💨 • ${randomise(nothingMsg)}`, - value: "*No actions have been taken on this user*" - } - ] - ) - .setFooter({ text: `User ID: ${user.id}` }) - .setColor(genColor(200)); - - await interaction.reply({ embeds: [embed] }); - } -} diff --git a/src/commands/moderation/delwarn.ts b/src/commands/moderation/delwarn.ts deleted file mode 100644 index d01645a..0000000 --- a/src/commands/moderation/delwarn.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { - DMChannel, - EmbedBuilder, - PermissionsBitField, - SlashCommandSubcommandBuilder, - type ChatInputCommandInteraction -} from "discord.js"; -import { genColor } from "../../utils/colorGen"; -import { listUserModeration, removeModeration } from "../../utils/database/moderation"; -import { errorEmbed } from "../../utils/embeds/errorEmbed"; -import { errorCheck } from "../../utils/embeds/modEmbed"; -import { logChannel } from "../../utils/logChannel"; - -export default class Delwarn { - data: SlashCommandSubcommandBuilder; - constructor() { - this.data = new SlashCommandSubcommandBuilder() - .setName("delwarn") - .setDescription("Removes a warning from a user.") - .addUserOption(user => - user - .setName("user") - .setDescription("The user that you want to free from the warning.") - .setRequired(true) - ) - .addNumberOption(number => - number.setName("id").setDescription("The id of the warn.").setRequired(true) - ); - } - - async run(interaction: ChatInputCommandInteraction) { - const user = interaction.options.getUser("user")!; - const guild = interaction.guild!; - const name = user.displayName; - const id = interaction.options.getNumber("id"); - const warns = listUserModeration(guild.id, user.id, "WARN"); - const newWarns = warns.filter(warn => warn.id != `${id}`); - if ( - await errorCheck( - PermissionsBitField.Flags.ModerateMembers, - { interaction, user, action: "Remove a warn" }, - { allErrors: true, botError: false }, - "Moderate Members" - ) - ) - return; - - if (newWarns.length == warns.length) - return await errorEmbed(interaction, `There is no warn with the id of ${id}.`); - - const embed = new EmbedBuilder() - .setAuthor({ name: `• Removed a warning from ${name}`, iconURL: user.displayAvatarURL() }) - .setDescription(`Moderator responsible is **${interaction.user.displayName}**`) - .setFooter({ text: `User ID: ${user.id}` }) - .setColor(genColor(100)); - - await logChannel(guild, embed); - try { - removeModeration(guild.id, `${id}`); - } catch (error) { - console.error(error); - } - - await interaction.reply({ embeds: [embed] }); - const dmChannel = (await user.createDM().catch(() => null)) as DMChannel | null; - if (!dmChannel) return; - if (user.bot) return; - await dmChannel.send({ embeds: [embed.setTitle("Your warning has been removed.")] }); - } -} diff --git a/src/commands/moderation/kick.ts b/src/commands/moderation/kick.ts deleted file mode 100644 index e302404..0000000 --- a/src/commands/moderation/kick.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { - PermissionsBitField, - SlashCommandSubcommandBuilder, - type ChatInputCommandInteraction -} from "discord.js"; -import { errorCheck, modEmbed } from "../../utils/embeds/modEmbed"; - -export default class Kick { - data: SlashCommandSubcommandBuilder; - constructor() { - this.data = new SlashCommandSubcommandBuilder() - .setName("kick") - .setDescription("Kicks a user.") - .addUserOption(user => - user.setName("user").setDescription("The user that you want to kick.").setRequired(true) - ) - .addStringOption(string => - string.setName("reason").setDescription("The reason for the kick.") - ); - } - - async run(interaction: ChatInputCommandInteraction) { - const user = interaction.options.getUser("user")!; - if ( - await errorCheck( - PermissionsBitField.Flags.KickMembers, - { interaction, user, action: "Kick" }, - { allErrors: true, botError: true, ownerError: true, outsideError: true }, - "Kick Members" - ) - ) - return; - - const reason = interaction.options.getString("reason"); - await interaction.guild?.members.cache - .get(user.id) - ?.kick(reason ?? undefined) - .catch(error => console.error(error)); - - await modEmbed({ interaction, user, action: "Kicked", dm: true, dbAction: "KICK" }, reason); - } -} diff --git a/src/commands/moderation/lock.ts b/src/commands/moderation/lock.ts deleted file mode 100644 index 8099e27..0000000 --- a/src/commands/moderation/lock.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { - ChannelType, - EmbedBuilder, - PermissionsBitField, - SlashCommandSubcommandBuilder, - type ChatInputCommandInteraction -} from "discord.js"; -import { genColor } from "../../utils/colorGen"; -import { errorEmbed } from "../../utils/embeds/errorEmbed"; -import { logChannel } from "../../utils/logChannel"; - -export default class Lock { - data: SlashCommandSubcommandBuilder; - constructor() { - this.data = new SlashCommandSubcommandBuilder() - .setName("lock") - .setDescription("Locks a channel.") - .addChannelOption(channel => - channel - .setName("channel") - .setDescription("The channel that you want to lock.") - .addChannelTypes( - ChannelType.GuildText, - ChannelType.PublicThread, - ChannelType.PrivateThread, - ChannelType.GuildVoice - ) - ); - } - - async run(interaction: ChatInputCommandInteraction) { - const guild = interaction.guild!; - if ( - !guild.members.cache - .get(interaction.user.id) - ?.permissions.has(PermissionsBitField.Flags.ManageRoles) - ) - return await errorEmbed( - interaction, - "You can't execute this command.", - "You need the **Manage Roles** permission." - ); - - const channelOption = interaction.options.getChannel("channel")!; - const channel = guild.channels.cache.get(interaction.channel?.id ?? channelOption.id)!; - if (!channel.permissionsFor(guild.id)?.has("SendMessages")) - return await errorEmbed( - interaction, - "You can't execute this command.", - "The channel is already locked." - ); - - const embed = new EmbedBuilder() - .setTitle(`Locked a channel.`) - .setDescription( - [ - `Responsible moderator is **${interaction.user.displayName}**`, - `Locked the **${channelOption ?? `<#${channel.id}>`}** channel` - ].join("\n") - ) - .setColor(genColor(100)); - - if ( - channel.type == ChannelType.GuildText && - ChannelType.PublicThread && - ChannelType.PrivateThread && - ChannelType.GuildVoice - ) - channel.permissionOverwrites - .create(guild.id, { - SendMessages: false, - SendMessagesInThreads: false, - CreatePublicThreads: false - }) - .catch(error => console.error(error)); - - await logChannel(guild, embed); - await interaction.reply({ embeds: [embed] }); - } -} diff --git a/src/commands/moderation/mute.ts b/src/commands/moderation/mute.ts deleted file mode 100644 index 6442573..0000000 --- a/src/commands/moderation/mute.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { - PermissionsBitField, - SlashCommandSubcommandBuilder, - type ChatInputCommandInteraction -} from "discord.js"; -import ms from "ms"; -import { errorEmbed } from "../../utils/embeds/errorEmbed"; -import { errorCheck, modEmbed } from "../../utils/embeds/modEmbed"; - -export default class Mute { - data: SlashCommandSubcommandBuilder; - constructor() { - this.data = new SlashCommandSubcommandBuilder() - .setName("mute") - .setDescription("Mutes a user.") - .addUserOption(user => - user.setName("user").setDescription("The user that you want to mute.").setRequired(true) - ) - .addStringOption(string => - string - .setName("duration") - .setDescription("The duration of the mute (e.g 30m, 1d, 2h).") - .setRequired(true) - ) - .addStringOption(string => - string.setName("reason").setDescription("The reason for the mute.") - ); - } - - async run(interaction: ChatInputCommandInteraction) { - const user = interaction.options.getUser("user")!; - const duration = interaction.options.getString("duration")!; - const reason = interaction.options.getString("reason"); - if ( - await errorCheck( - PermissionsBitField.Flags.ModerateMembers, - { interaction, user, action: "Mute" }, - { allErrors: true, botError: true, ownerError: true }, - "Moderate Members" - ) - ) - return; - - if (!ms(duration) || ms(duration) > ms("28d")) - return await errorEmbed( - interaction, - `You can't mute ${user.displayName}.`, - "The duration is invalid or is above the 28 day limit." - ); - - const time = new Date( - Date.parse(new Date().toISOString()) + Date.parse(new Date(ms(duration)).toISOString()) - ).toISOString(); - - await interaction.guild?.members.cache - .get(user.id) - ?.edit({ communicationDisabledUntil: time, reason: reason ?? undefined }) - .catch(error => console.error(error)); - - await modEmbed( - { interaction, user, action: "Muted", duration, dm: true, dbAction: "MUTE" }, - reason, - true - ); - } -} diff --git a/src/commands/moderation/purge.ts b/src/commands/moderation/purge.ts deleted file mode 100644 index bfb2a19..0000000 --- a/src/commands/moderation/purge.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { - ChannelType, - EmbedBuilder, - PermissionsBitField, - SlashCommandSubcommandBuilder, - type ChatInputCommandInteraction -} from "discord.js"; -import { genColor } from "../../utils/colorGen"; -import { errorEmbed } from "../../utils/embeds/errorEmbed"; -import { logChannel } from "../../utils/logChannel"; - -export default class Clear { - data: SlashCommandSubcommandBuilder; - constructor() { - this.data = new SlashCommandSubcommandBuilder() - .setName("clear") - .setDescription("Clears messages.") - .addNumberOption(number => - number - .setName("amount") - .setDescription("The amount of messages that you want to clear (maximum is 100).") - .setRequired(true) - ) - .addChannelOption(channel => - channel - .setName("channel") - .setDescription("The channel that has the messages that you want to clear.") - .addChannelTypes( - ChannelType.GuildText, - ChannelType.PublicThread, - ChannelType.PrivateThread, - ChannelType.GuildVoice - ) - ); - } - - async run(interaction: ChatInputCommandInteraction) { - const guild = interaction.guild!; - if ( - !guild.members.cache - .get(interaction.user.id) - ?.permissions.has(PermissionsBitField.Flags.ManageMessages) - ) - return await errorEmbed( - interaction, - "You can't execute this command.", - "You need the **Manage Messages** permission." - ); - - const amount = interaction.options.getNumber("amount")!; - if (amount > 100) - return await errorEmbed(interaction, "You can only clear up to 100 messages at a time."); - - if (amount < 1) return await errorEmbed(interaction, "You must clear at least 1 message."); - - const channelOption = interaction.options.getChannel("channel")!; - const channel = guild.channels.cache.get(interaction.channel?.id ?? channelOption.id)!; - const embed = new EmbedBuilder() - .setTitle(`Cleared ${amount} message${amount == 1 ? "" : "s"}.`) - .setDescription( - [ - `Moderator responsible is **${interaction.user.displayName}**`, - `Cleared messages of the **${channelOption ?? `<#${channel.id}>** channel`}` - ].join("\n") - ) - .setColor(genColor(100)); - - if ( - channel.type == ChannelType.GuildText && - ChannelType.PublicThread && - ChannelType.PrivateThread && - ChannelType.GuildVoice - ) - try { - channel == interaction.channel - ? await channel.bulkDelete(amount + 1, true) - : await channel.bulkDelete(amount, true); - } catch (error) { - console.error(error); - } - - await logChannel(guild, embed); - await interaction.reply({ embeds: [embed] }); - } -} diff --git a/src/commands/moderation/slowdown.ts b/src/commands/moderation/slowdown.ts deleted file mode 100644 index 119ff32..0000000 --- a/src/commands/moderation/slowdown.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { - ChannelType, - EmbedBuilder, - PermissionsBitField, - SlashCommandSubcommandBuilder, - inlineCode, - type ChatInputCommandInteraction -} from "discord.js"; -import ms from "ms"; -import { genColor } from "../../utils/colorGen"; -import { errorEmbed } from "../../utils/embeds/errorEmbed"; -import { logChannel } from "../../utils/logChannel"; - -export default class Slowdown { - data: SlashCommandSubcommandBuilder; - constructor() { - this.data = new SlashCommandSubcommandBuilder() - .setName("slowdown") - .setDescription("Slows a channel down.") - .addStringOption(string => - string - .setName("time") - .setDescription( - "Time to slow the channel down to (e.g 30m, 1d, 2h). 0 to remove slowdown." - ) - .setRequired(true) - ) - .addStringOption(string => - string.setName("reason").setDescription("The reason for the slowdown.") - ) - .addChannelOption(channel => - channel - .setName("channel") - .setDescription("The channel that you want to slowdown.") - .addChannelTypes( - ChannelType.GuildText, - ChannelType.PublicThread, - ChannelType.PrivateThread, - ChannelType.GuildVoice - ) - ); - } - - async run(interaction: ChatInputCommandInteraction) { - const guild = interaction.guild!; - if ( - !guild.members.cache - .get(interaction.user.id) - ?.permissions.has(PermissionsBitField.Flags.ManageChannels) - ) - return await errorEmbed( - interaction, - "You can't execute this command.", - "You need the **Manage Channels** permission." - ); - - const time = interaction.options.getString("time")!; - const reason = interaction.options.getString("reason"); - const channelOption = interaction.options.getChannel("channel")!; - const channel = guild.channels.cache.get(interaction.channel?.id ?? channelOption.id)!; - let title = `Set a slowdown of \`${channelOption ?? `${channel.name}`}\` to ${ms(ms(time), { - long: true - })}.`; - if (!ms(time)) title = `Removed the slowdown from \`${channelOption ?? `${channel.name}`}\`.`; - - const embed = new EmbedBuilder() - .setTitle(title) - .setDescription( - [ - `Moderator responsible is **${interaction.user.displayName}**`, - reason ? `**Reason** provided is ${inlineCode(reason)}` : "*No reason provided*", - `${ms(time) ? "Slowed the" : "Removed the slowdown from the"} **${channelOption ?? `<#${channel.id}>** ${ms(time) ? "channel down" : "channel"}`}` - ].join("\n") - ) - .setColor(genColor(100)); - - if ( - channel.type == ChannelType.GuildText && - ChannelType.PublicThread && - ChannelType.PrivateThread && - ChannelType.GuildVoice - ) - await channel - .setRateLimitPerUser(ms(time) / 1000, interaction.options.getString("reason")!) - .catch(error => console.error(error)); - - await logChannel(guild, embed); - await interaction.reply({ embeds: [embed] }); - } -} diff --git a/src/commands/moderation/unban.ts b/src/commands/moderation/unban.ts deleted file mode 100644 index 922d1f3..0000000 --- a/src/commands/moderation/unban.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { - PermissionsBitField, - SlashCommandSubcommandBuilder, - type ChatInputCommandInteraction -} from "discord.js"; -import { errorEmbed } from "../../utils/embeds/errorEmbed"; -import { errorCheck, modEmbed } from "../../utils/embeds/modEmbed"; - -export default class Unban { - data: SlashCommandSubcommandBuilder; - constructor() { - this.data = new SlashCommandSubcommandBuilder() - .setName("unban") - .setDescription("Unbans a user.") - .addStringOption(string => - string - .setName("id") - .setDescription("The ID of the user that you want to unban.") - .setRequired(true) - ) - .addStringOption(string => - string.setName("reason").setDescription("The reason for the unban.") - ); - } - - async run(interaction: ChatInputCommandInteraction) { - const id = interaction.options.getString("id")!; - const reason = interaction.options.getString("reason")!; - const guild = interaction.guild!; - const target = (await guild.bans.fetch()) - .map(ban => ban.user) - .filter(user => user.id == id)[0]!; - - if ( - await errorCheck( - PermissionsBitField.Flags.BanMembers, - { interaction, user: target, action: "Unban" }, - { allErrors: false, botError: true, ownerError: true }, - "Ban Members" - ) - ) - return; - - if (!target) - return await errorEmbed( - interaction, - "You can't unban this user.", - "The user was never banned." - ); - - await guild.members.unban(id, reason ?? undefined).catch(error => console.error(error)); - await modEmbed({ interaction, user: target, action: "Unbanned" }, reason); - } -} diff --git a/src/commands/moderation/unlock.ts b/src/commands/moderation/unlock.ts deleted file mode 100644 index e27c51b..0000000 --- a/src/commands/moderation/unlock.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { - ChannelType, - EmbedBuilder, - PermissionsBitField, - SlashCommandSubcommandBuilder, - type ChatInputCommandInteraction -} from "discord.js"; -import { genColor } from "../../utils/colorGen"; -import { errorEmbed } from "../../utils/embeds/errorEmbed"; -import { logChannel } from "../../utils/logChannel"; - -export default class Unlock { - data: SlashCommandSubcommandBuilder; - constructor() { - this.data = new SlashCommandSubcommandBuilder() - .setName("unlock") - .setDescription("Unlocks a channel.") - .addChannelOption(channel => - channel - .setName("channel") - .setDescription("The channel that you want to unlock.") - .addChannelTypes( - ChannelType.GuildText, - ChannelType.PublicThread, - ChannelType.PrivateThread, - ChannelType.GuildVoice - ) - ); - } - - async run(interaction: ChatInputCommandInteraction) { - const guild = interaction.guild!; - if ( - !guild.members.cache - .get(interaction.user.id) - ?.permissions.has(PermissionsBitField.Flags.ManageRoles) - ) - return await errorEmbed( - interaction, - "You can't execute this command.", - "You need the **Manage Roles** permission." - ); - - const channelOption = interaction.options.getChannel("channel")!; - const channel = guild.channels.cache.get(interaction.channel?.id ?? channelOption.id)!; - if (channel.permissionsFor(guild.id)?.has("SendMessages")) - return await errorEmbed( - interaction, - "You can't execute this command.", - "The channel is not locked." - ); - - const embed = new EmbedBuilder() - .setTitle(`Unlocked a channel.`) - .setDescription( - [ - `Moderator responsible is **${interaction.user.displayName}**`, - `Unlocked the **${channelOption ?? `<#${channel.id}>`}** channel` - ].join("\n") - ) - .setColor(genColor(100)); - - if ( - channel.type == ChannelType.GuildText && - ChannelType.PublicThread && - ChannelType.PrivateThread && - ChannelType.GuildVoice - ) - channel.permissionOverwrites - .create(guild.id, { - SendMessages: null, - SendMessagesInThreads: null, - CreatePublicThreads: null - }) - .catch(error => console.error(error)); - - await logChannel(guild, embed); - await interaction.reply({ embeds: [embed] }); - } -} diff --git a/src/commands/moderation/unmute.ts b/src/commands/moderation/unmute.ts deleted file mode 100644 index 622e95b..0000000 --- a/src/commands/moderation/unmute.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { - PermissionsBitField, - SlashCommandSubcommandBuilder, - type ChatInputCommandInteraction -} from "discord.js"; -import { errorEmbed } from "../../utils/embeds/errorEmbed"; -import { errorCheck, modEmbed } from "../../utils/embeds/modEmbed"; - -export default class Unmute { - data: SlashCommandSubcommandBuilder; - constructor() { - this.data = new SlashCommandSubcommandBuilder() - .setName("unmute") - .setDescription("Unmutes a user.") - .addUserOption(user => - user.setName("user").setDescription("The user that you want to unmute.").setRequired(true) - ); - } - - async run(interaction: ChatInputCommandInteraction) { - const user = interaction.options.getUser("user")!; - const target = interaction.guild?.members.cache.get(user.id)!; - if ( - await errorCheck( - PermissionsBitField.Flags.ModerateMembers, - { interaction, user, action: "Unmute" }, - { allErrors: false, botError: true }, - "Moderate Members" - ) - ) - return; - - if (!target.communicationDisabledUntil) - return await errorEmbed( - interaction, - "You can't unmute this user.", - "The user was never muted." - ); - - await target.edit({ communicationDisabledUntil: null }).catch(error => console.error(error)); - await modEmbed({ interaction, user, action: "Unmuted" }); - } -} diff --git a/src/commands/moderation/warn.ts b/src/commands/moderation/warn.ts deleted file mode 100644 index a458908..0000000 --- a/src/commands/moderation/warn.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { - PermissionsBitField, - SlashCommandSubcommandBuilder, - type ChatInputCommandInteraction -} from "discord.js"; -import { errorCheck, modEmbed } from "../../utils/embeds/modEmbed"; - -export default class Warn { - data: SlashCommandSubcommandBuilder; - constructor() { - this.data = new SlashCommandSubcommandBuilder() - .setName("warn") - .setDescription("Warns a user.") - .addUserOption(user => - user.setName("user").setDescription("The user that you want to warn.").setRequired(true) - ) - .addStringOption(string => - string.setName("reason").setDescription("The reason for the warn.") - ) - .addBooleanOption(bool => - bool - .setName("show_moderator") - .setDescription( - "Inform the warned user of the moderator that took the action. Defaults to false." - ) - ); - } - - async run(interaction: ChatInputCommandInteraction) { - const user = interaction.options.getUser("user")!; - const reason = interaction.options.getString("reason"); - const showModerator = interaction.options.getBoolean("show_moderator") ?? false; - if ( - await errorCheck( - PermissionsBitField.Flags.ModerateMembers, - { interaction, user, action: "Warn" }, - { allErrors: true, botError: false, ownerError: true, outsideError: true }, - "Moderate Members" - ) - ) - return; - - await modEmbed( - { interaction, user, action: "Warned", dm: true, dbAction: "WARN" }, - reason, - showModerator - ); - } -} diff --git a/src/commands/news/add.ts b/src/commands/news/add.ts deleted file mode 100644 index 08ee723..0000000 --- a/src/commands/news/add.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { - ActionRowBuilder, - EmbedBuilder, - ModalBuilder, - PermissionsBitField, - SlashCommandSubcommandBuilder, - TextInputBuilder, - TextInputStyle, - type ChatInputCommandInteraction -} from "discord.js"; -import { genColor } from "../../utils/colorGen"; -import { addNews } from "../../utils/database/news"; -import { errorEmbed } from "../../utils/embeds/errorEmbed"; -import { sendChannelNews } from "../../utils/sendChannelNews"; - -export default class Send { - data: SlashCommandSubcommandBuilder; - constructor() { - this.data = new SlashCommandSubcommandBuilder().setName("add").setDescription("Add your news."); - } - - async run(interaction: ChatInputCommandInteraction) { - const guild = interaction.guild!; - if ( - !guild.members.cache - .get(interaction.user.id) - ?.permissions.has(PermissionsBitField.Flags.ManageGuild) - ) - return await errorEmbed( - interaction, - "You can't execute this command.", - "You need the **Manage Server** permission." - ); - - const firstActionRow = new ActionRowBuilder().addComponents( - new TextInputBuilder() - .setCustomId("title") - .setPlaceholder("Write a title") - .setMaxLength(100) - .setStyle(TextInputStyle.Short) - .setLabel("Title") - .setRequired(true) - ); - - const secondActionRow = new ActionRowBuilder().addComponents( - new TextInputBuilder() - .setCustomId("body") - .setPlaceholder("Insert your content here") - .setMaxLength(4000) - .setStyle(TextInputStyle.Paragraph) - .setLabel("Content (supports Markdown)") - .setRequired(true) - ); - - const newsModal = new ModalBuilder() - .setCustomId("addnews") - .setTitle("Write your news.") - .addComponents(firstActionRow, secondActionRow); - - await interaction.showModal(newsModal).catch(err => console.error(err)); - interaction.client.once("interactionCreate", async i => { - if (!i.isModalSubmit()) return; - - const id = crypto.randomUUID(); - addNews( - guild.id, - i.fields.getTextInputValue("title"), - i.fields.getTextInputValue("body"), - i.user.displayName, - i.user.avatarURL()!, - null!, - id - ); - - await sendChannelNews(guild, id, interaction).catch(err => console.error(err)); - await i.reply({ - embeds: [new EmbedBuilder().setTitle("News added.").setColor(genColor(100))], - ephemeral: true - }); - }); - } -} diff --git a/src/commands/news/edit.ts b/src/commands/news/edit.ts deleted file mode 100644 index 48f3a0f..0000000 --- a/src/commands/news/edit.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { - ActionRowBuilder, - EmbedBuilder, - ModalBuilder, - PermissionsBitField, - SlashCommandSubcommandBuilder, - TextInputBuilder, - TextInputStyle, - type ChatInputCommandInteraction, - type Role, - type TextChannel -} from "discord.js"; -import { genColor } from "../../utils/colorGen"; -import { get, updateNews } from "../../utils/database/news"; -import { getSetting } from "../../utils/database/settings"; -import { errorEmbed } from "../../utils/embeds/errorEmbed"; -import { sendChannelNews } from "../../utils/sendChannelNews"; - -export default class Edit { - data: SlashCommandSubcommandBuilder; - constructor() { - this.data = new SlashCommandSubcommandBuilder() - .setName("edit") - .setDescription("Edits the news of your guild.") - .addStringOption(string => - string - .setName("id") - .setDescription("The ID of the news you want to edit.") - .setRequired(true) - ); - } - - async run(interaction: ChatInputCommandInteraction) { - const guild = interaction.guild!; - if ( - !guild.members.cache - .get(interaction.user.id) - ?.permissions.has(PermissionsBitField.Flags.ManageGuild) - ) - return await errorEmbed( - interaction, - "You can't execute this command.", - "You need the **Manage Server** permission." - ); - - const id = interaction.options.getString("id")!; - const news = get(id); - if (!news) return await errorEmbed(interaction, "The specified news don't exist."); - - const firstActionRow = new ActionRowBuilder().addComponents( - new TextInputBuilder() - .setCustomId("title") - .setMaxLength(100) - .setStyle(TextInputStyle.Short) - .setLabel("Title") - .setValue(news.title) - .setRequired(true) - ); - - const secondActionRow = new ActionRowBuilder().addComponents( - new TextInputBuilder() - .setCustomId("body") - .setMaxLength(4000) - .setStyle(TextInputStyle.Paragraph) - .setLabel("Content (supports Markdown)") - .setValue(news.body) - .setRequired(true) - ); - - const editModal = new ModalBuilder() - .setCustomId("editnews") - .setTitle(`Edit news: ${news.title}`) - .addComponents(firstActionRow, secondActionRow); - - await interaction.showModal(editModal).catch(err => console.error(err)); - interaction.client.once("interactionCreate", async i => { - if (!i.isModalSubmit()) return; - - const role = getSetting(guild.id, "news", "role_id") as string; - let roleToSend: Role | undefined; - if (role) roleToSend = guild.roles.cache.get(role); - const title = i.fields.getTextInputValue("title"); - const body = i.fields.getTextInputValue("body"); - - if (!getSetting(guild.id, "news", "edit_original_message")) - await sendChannelNews(guild, id, interaction, title, body); - - const embed = new EmbedBuilder() - .setAuthor({ name: `• ${news.author}`, iconURL: news.authorPFP }) - .setTitle(title) - .setDescription(body) - .setTimestamp(parseInt(news.updatedAt.toString()) ?? null) - .setFooter({ text: `Edited news from ${guild.name}\nID: ${news.id}` }) - .setColor(genColor(200)); - - ( - guild.channels.cache.get( - (getSetting(guild.id, "news", "channel_id") as string) ?? interaction.channel?.id - ) as TextChannel - )?.messages.edit(news.messageID, { - embeds: [embed], - content: roleToSend ? `<@&${roleToSend.id}>` : undefined - }); - - updateNews(id, title, body); - await interaction.reply({ - embeds: [new EmbedBuilder().setTitle("News edited.").setColor(genColor(100))], - ephemeral: true - }); - }); - } -} diff --git a/src/commands/news/remove.ts b/src/commands/news/remove.ts deleted file mode 100644 index 0cb5bee..0000000 --- a/src/commands/news/remove.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { - EmbedBuilder, - PermissionsBitField, - SlashCommandSubcommandBuilder, - TextChannel, - type ChatInputCommandInteraction -} from "discord.js"; -import { genColor } from "../../utils/colorGen"; -import { deleteNews, get } from "../../utils/database/news"; -import { getSetting } from "../../utils/database/settings"; -import { errorEmbed } from "../../utils/embeds/errorEmbed"; - -export default class Remove { - data: SlashCommandSubcommandBuilder; - constructor() { - this.data = new SlashCommandSubcommandBuilder() - .setName("remove") - .setDescription("Removes news from your guild.") - .addStringOption(string => - string - .setName("id") - .setDescription("The ID of the news. (found in the footer)") - .setRequired(true) - ); - } - - async run(interaction: ChatInputCommandInteraction) { - const guild = interaction.guild!; - if ( - !guild.members.cache - .get(interaction.user.id) - ?.permissions.has(PermissionsBitField.Flags.ManageGuild) - ) - return await errorEmbed( - interaction, - "You can't execute this command.", - "You need the **Manage Server** permission." - ); - - const id = interaction.options.getString("id")!; - const news = get(id); - if (!news) return await errorEmbed(interaction, "The specified news don't exist."); - - const newsChannel = (await guild.channels - .fetch((getSetting(guild.id, "news", "channel_id") as string) ?? interaction.channel?.id) - .catch(() => null)) as TextChannel; - - if (newsChannel) await newsChannel.messages.delete(news.messageID); - deleteNews(id); - await interaction.reply({ - embeds: [new EmbedBuilder().setTitle("News removed.").setColor(genColor(100))], - ephemeral: true - }); - } -} diff --git a/src/commands/news/view.ts b/src/commands/news/view.ts deleted file mode 100644 index 3bbf7ce..0000000 --- a/src/commands/news/view.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { - ActionRowBuilder, - ButtonBuilder, - ButtonInteraction, - ButtonStyle, - EmbedBuilder, - SlashCommandSubcommandBuilder, - type ChatInputCommandInteraction -} from "discord.js"; -import { genColor } from "../../utils/colorGen"; -import { listAllNews } from "../../utils/database/news"; -import { errorEmbed } from "../../utils/embeds/errorEmbed"; - -export default class View { - data: SlashCommandSubcommandBuilder; - constructor() { - this.data = new SlashCommandSubcommandBuilder() - .setName("view") - .setDescription("View the news of this server.") - .addNumberOption(number => - number.setName("page").setDescription("The page of the news that you want to see.") - ); - } - - async run(interaction: ChatInputCommandInteraction) { - let page = interaction.options.getNumber("page") ?? 1; - const news = listAllNews(interaction.guild?.id!); - const sortedNews = (Object.values(news) as any[])?.sort((a, b) => b.createdAt - a.createdAt); - - if (!news || !sortedNews || !sortedNews.length) - return await errorEmbed( - interaction, - "No news found.", - "Admins can add news with the **/news add** command." - ); - - if (page > sortedNews.length) page = sortedNews.length; - if (page < 1) page = 1; - - function getEmbed() { - const currentNews = sortedNews[page - 1]; - return new EmbedBuilder() - .setAuthor({ name: `• ${currentNews.author}`, iconURL: currentNews.authorPFP }) - .setTitle(currentNews.title) - .setDescription(currentNews.body) - .setImage(currentNews.imageURL || null) - .setTimestamp(parseInt(currentNews.updatedAt)) - .setFooter({ text: `Page ${page} of ${sortedNews.length} • ID: ${currentNews.id}` }) - .setColor(genColor(200)); - } - - const row = new ActionRowBuilder().addComponents( - new ButtonBuilder() - .setCustomId("left") - .setEmoji("1271045078042935398") - .setStyle(ButtonStyle.Primary), - new ButtonBuilder() - .setCustomId("right") - .setEmoji("1271045041313415370") - .setStyle(ButtonStyle.Primary) - ); - - const reply = await interaction.reply({ embeds: [getEmbed()], components: [row] }); - reply - .createMessageComponentCollector({ time: 60000 }) - .on("collect", async (i: ButtonInteraction) => { - if (i.message.id != (await reply.fetch()).id) - return await errorEmbed( - i, - "For some reason, this click would've caused the bot to error. Thankfully, this message right here prevents that." - ); - - if (i.user.id != interaction.user.id) - return await errorEmbed(i, "You aren't the person who executed this command."); - - setTimeout(async () => await i.editReply({ components: [] }), 60000); - switch (i.customId) { - case "left": - page--; - if (page < 1) page = sortedNews.length; - await i.update({ embeds: [getEmbed()], components: [row] }); - break; - case "right": - page++; - if (page > sortedNews.length) page = 1; - await i.update({ embeds: [getEmbed()], components: [row] }); - break; - } - }); - } -} diff --git a/src/commands/server.ts b/src/commands/server.ts deleted file mode 100644 index 9c5ac8f..0000000 --- a/src/commands/server.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { SlashCommandBuilder, type ChatInputCommandInteraction } from "discord.js"; -import { serverEmbed } from "../utils/embeds/serverEmbed"; - -export default class Server { - data: SlashCommandBuilder; - constructor() { - this.data = new SlashCommandBuilder() - .setName("server") - .setDescription("Shows this server's info."); - } - - async run(interaction: ChatInputCommandInteraction) { - const embed = await serverEmbed({ guild: interaction.guild!, roles: true }); - await interaction.reply({ embeds: [embed] }); - } -} diff --git a/src/commands/serverboard.ts b/src/commands/serverboard.ts deleted file mode 100644 index 60c0a29..0000000 --- a/src/commands/serverboard.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { - ActionRowBuilder, - ButtonBuilder, - ButtonInteraction, - ButtonStyle, - SlashCommandBuilder, - type ChatInputCommandInteraction, - type SlashCommandOptionsOnlyBuilder -} from "discord.js"; -import { listPublicServers } from "../utils/database/settings"; -import { errorEmbed } from "../utils/embeds/errorEmbed"; -import { serverEmbed } from "../utils/embeds/serverEmbed"; - -export default class Serverboard { - data: SlashCommandOptionsOnlyBuilder; - constructor() { - this.data = new SlashCommandBuilder() - .setName("serverboard") - .setDescription("Shows the servers that have Sokora.") - .addNumberOption(number => - number.setName("page").setDescription("The page you want to see.") - ); - } - - async run(interaction: ChatInputCommandInteraction) { - const guildList = ( - await Promise.all(listPublicServers().map(id => interaction.client.guilds.fetch(id))) - ).sort((a, b) => b.memberCount - a.memberCount); - - const pages = guildList.length; - if (!pages) - return await errorEmbed( - interaction, - "No public server found.", - "By some magical miracle, all the servers using Sokora turned off their visibility. Use /settings serverboard `shown: True` to make your server publicly visible." - ); - - const argPage = interaction.options.getNumber("page") as number; - let page = (argPage - 1 <= 0 ? 0 : argPage - 1 > pages ? pages - 1 : argPage - 1) || 0; - - async function getEmbed() { - return await serverEmbed({ guild: guildList[page], page: page + 1, pages }); - } - - const row = new ActionRowBuilder().addComponents( - new ButtonBuilder() - .setCustomId("left") - .setEmoji("1271045078042935398") - .setStyle(ButtonStyle.Primary), - new ButtonBuilder() - .setCustomId("right") - .setEmoji("1271045041313415370") - .setStyle(ButtonStyle.Primary) - ); - - const reply = await interaction.reply({ embeds: [await getEmbed()], components: [row] }); - if (pages == 1) return; - reply - .createMessageComponentCollector({ time: 60000 }) - .on("collect", async (i: ButtonInteraction) => { - if (i.message.id != (await reply.fetch()).id) - return await errorEmbed( - i, - "For some reason, this click would've caused the bot to error. Thankfully, this message right here prevents that." - ); - - if (i.user.id != interaction.user.id) - return await errorEmbed(i, "You aren't the person who executed this command."); - - setTimeout(async () => await i.editReply({ components: [] }), 60000); - switch (i.customId) { - case "left": - page--; - if (page < 0) page = pages - 1; - break; - case "right": - page++; - if (page >= pages) page = 0; - break; - } - - await i.update({ embeds: [await getEmbed()], components: [row] }); - }); - } -} diff --git a/src/commands/settings.ts b/src/commands/settings.ts deleted file mode 100644 index 15f94da..0000000 --- a/src/commands/settings.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { - AutocompleteInteraction, - EmbedBuilder, - InteractionType, - PermissionsBitField, - SlashCommandBuilder, - SlashCommandSubcommandBuilder, - SlashCommandSubcommandGroupBuilder, - type ChatInputCommandInteraction -} from "discord.js"; -import { genColor } from "../utils/colorGen"; -import { - getSetting, - setSetting, - settingsDefinition, - settingsKeys -} from "../utils/database/settings"; -import { errorEmbed } from "../utils/embeds/errorEmbed"; - -export default class Settings { - data: SlashCommandBuilder; - constructor() { - this.data = new SlashCommandBuilder() - .setName("settings") - .setDescription("Configure Sokora to your liking.") - .setDefaultMemberPermissions(PermissionsBitField.Flags.Administrator); - - settingsKeys.forEach(key => { - const subcommand = new SlashCommandSubcommandBuilder() - .setName(key) - .setDescription("This subcommand has no description."); - - Object.keys(settingsDefinition[key]).forEach(sub => { - switch (settingsDefinition[key][sub]["type"] as string) { - case "BOOL": - subcommand.addBooleanOption(option => - option - .setName(sub) - .setDescription(settingsDefinition[key][sub]["desc"]) - .setRequired(false) - ); - break; - case "INTEGER": - subcommand.addIntegerOption(option => - option - .setName(sub) - .setDescription(settingsDefinition[key][sub]["desc"]) - .setRequired(false) - ); - break; - case "USER": - subcommand.addUserOption(option => - option - .setName(sub) - .setDescription(settingsDefinition[key][sub]["desc"]) - .setRequired(false) - ); - break; - // case "LIST": - // const subcommandGroup = new SlashCommandSubcommandGroupBuilder() - // .setName(sub) - // .setDescription("This subcommand group has no description."); - default: // Also includes "TEXT" - subcommand.addStringOption(option => - option - .setName(sub) - .setDescription(settingsDefinition[key][sub]["desc"]) - .setRequired(false) - ); - break; - } - }); - this.data.addSubcommand(subcommand); - }); - } - - async run(interaction: ChatInputCommandInteraction) { - const guild = interaction.guild!; - if ( - !guild.members.cache - ?.get(interaction.user.id) - ?.permissions.has(PermissionsBitField.Flags.Administrator) - ) - return await errorEmbed( - interaction, - "You can't execute this command.", - "You need the **Administrator** permission." - ); - - const key = interaction.options.getSubcommand() as keyof typeof settingsDefinition; - const values = interaction.options.data[0].options!; - console.log(values); - if (!values.length) { - const embed = new EmbedBuilder().setTitle(`Settings for ${key}`).setColor(genColor(100)); - const description: string[] = []; - - Object.keys(settingsDefinition[key]).forEach(name => { - description.push(`${name}: ${getSetting(guild.id, key, name)?.toString() || "Not set"}`); - embed.setDescription(description.join("\n")); - }); - - return await interaction.reply({ embeds: [embed] }); - } - - const embed = new EmbedBuilder().setTitle(`Parameters changed`).setColor(genColor(100)); - values.forEach(option => { - setSetting(guild.id, key, option.name, option.value as string); - embed.addFields({ - name: option.name, - value: option.value?.toString() || "Not set" - }); - }); - - await interaction.reply({ embeds: [embed] }); - } - - async autocomplete(interaction: AutocompleteInteraction) { - if (interaction.type != InteractionType.ApplicationCommandAutocomplete) return; - //if (interaction.options.getSubcommand() != this.data.name) return; - switch (Object.keys(settingsDefinition[interaction.options.getSubcommand()])[0]) { - case "BOOL": - await interaction.respond( - ["true", "false"].map(choice => ({ - name: choice, - value: choice - })) - ); - break; - default: - await interaction.respond([]); - } - } -} diff --git a/src/commands/user.ts b/src/commands/user.ts deleted file mode 100644 index c296953..0000000 --- a/src/commands/user.ts +++ /dev/null @@ -1,165 +0,0 @@ -import { - ActionRowBuilder, - ButtonBuilder, - ButtonInteraction, - ButtonStyle, - EmbedBuilder, - SlashCommandBuilder, - type ChatInputCommandInteraction, - type SlashCommandOptionsOnlyBuilder -} from "discord.js"; -import { genColor } from "../utils/colorGen"; -import { getLevel, setLevel } from "../utils/database/levelling"; -import { getSetting } from "../utils/database/settings"; -import { errorEmbed } from "../utils/embeds/errorEmbed"; -import { imageColor } from "../utils/imageColor"; - -export default class User { - data: SlashCommandOptionsOnlyBuilder; - constructor() { - this.data = new SlashCommandBuilder() - .setName("user") - .setDescription("Shows your (or another user's) info.") - .addUserOption(user => user.setName("user").setDescription("Select the user.")); - } - - async run(interaction: ChatInputCommandInteraction) { - const guild = interaction.guild!; - const target = guild.members.cache - .filter( - member => member.id == (interaction.options.getUser("user")?.id ?? interaction.user.id) - ) - .map(user => user)[0]; - - const user = await target.user.fetch(); - let serverInfo = [`Joined on ****`]; - const guildRoles = guild.roles.cache.filter(role => target.roles.cache.has(role.id))!; - const memberRoles = [...guildRoles].sort( - (role1, role2) => role2[1].position - role1[1].position - ); - memberRoles.pop(); - - if (target.premiumSinceTimestamp) - serverInfo.push(`Boosting since **${target.premiumSinceTimestamp}**`); - - if (memberRoles.length) - serverInfo.push( - `**${guildRoles.filter(role => target.roles.cache.has(role.id)).size! - 1}** ${ - memberRoles.length == 1 ? "role" : "roles" - } • ${memberRoles - .slice(0, 5) - .map(role => `<@&${role[1].id}>`) - .join(", ")}${memberRoles.length > 3 ? ` **and ${memberRoles.length - 4} more**` : ""}` - ); - - const embedColor = - user.hexAccentColor ?? (await imageColor(undefined, target)) ?? genColor(200); - - let embed = new EmbedBuilder() - .setAuthor({ - name: `• ${target.nickname ?? user.displayName}`, - iconURL: target.displayAvatarURL() - }) - .setFields( - { - name: `<:discord:1266797021126459423> • Discord info`, - value: [ - `Username is **${user.username}**`, - `Display name is ${ - user.displayName == user.username ? "*not there*" : `**${user.displayName}**` - }`, - `Created on ****` - ].join("\n") - }, - { - name: "📒 • Server info", - value: serverInfo.join("\n") - } - ) - .setFooter({ text: `User ID: ${target.id}` }) - .setThumbnail(target.displayAvatarURL()!) - .setColor(embedColor); - - if (!getSetting(`${guild.id}`, "levelling", "enabled") && user.bot) return; - const row = new ActionRowBuilder().addComponents( - new ButtonBuilder() - .setCustomId("general") - .setLabel("• General") - .setEmoji("📃") - .setStyle(ButtonStyle.Primary), - new ButtonBuilder() - .setCustomId("level") - .setLabel("• Level") - .setEmoji("⚡") - .setStyle(ButtonStyle.Primary) - ); - row.components[0].setDisabled(true); - - const [guildLevel, guildExp] = getLevel(`${guild.id}`, `${target.id}`)!; - const [globalLevel, globalExp] = getLevel("0", `${target.id}`)!; - if (!guildLevel && !guildExp) setLevel(`${guild.id}`, `${target.id}`, 0, 0); - if (!globalLevel && !globalExp) setLevel("0", `${target.id}`, 0, 0); - - const nextLevelExp = Math.floor(100 * 1.15 * ((guildLevel ?? 0) + 1))?.toLocaleString("en-US"); - const globalNextLevelExp = Math.floor(100 * 1.15 * ((globalLevel ?? 0) + 1))?.toLocaleString( - "en-US" - ); - - const reply = await interaction.reply({ embeds: [embed], components: [row] }); - reply - .createMessageComponentCollector({ time: 60000 }) - .on("collect", async (i: ButtonInteraction) => { - if (i.message.id != (await reply.fetch()).id) - return await errorEmbed( - i, - "For some reason, this click would've caused the bot to error. Thankfully, this message right here prevents that." - ); - - if (i.user.id != interaction.user.id) - return await errorEmbed(i, "You aren't the person who executed this command."); - - setTimeout(async () => await i.editReply({ components: [] }), 60000); - i.customId == "general" - ? row.components[0].setDisabled(true) - : row.components[1].setDisabled(true); - - const levelEmbed = new EmbedBuilder() - .setAuthor({ - name: `• ${target.nickname ?? user.displayName}`, - iconURL: target.displayAvatarURL() - }) - .setFields( - { - name: `⚡ • Guild level ${guildLevel ?? 0}`, - value: [ - `**${guildExp.toLocaleString("en-US") ?? 0}/${nextLevelExp}** EXP`, - `**Next level**: ${(guildLevel ?? 0) + 1}` - ].join("\n"), - inline: true - }, - { - name: `⛈️ • Global level ${globalLevel ?? 0}`, - value: [ - `**${globalExp.toLocaleString("en-US") ?? 0}/${globalNextLevelExp}** EXP`, - `**Next level**: ${(globalLevel ?? 0) + 1}` - ].join("\n"), - inline: true - } - ) - .setFooter({ text: `User ID: ${target.id}` }) - .setThumbnail(target.displayAvatarURL()) - .setColor(embedColor); - - switch (i.customId) { - case "general": - row.components[1].setDisabled(false); - await i.update({ embeds: [embed], components: [row] }); - break; - case "level": - row.components[0].setDisabled(false); - await i.update({ embeds: [levelEmbed], components: [row] }); - break; - } - }); - } -} diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts index 668bb87..a714a59 100644 --- a/src/events/messageCreate.ts +++ b/src/events/messageCreate.ts @@ -29,12 +29,12 @@ export default { // Levelling if (!getSetting(guild.id, "levelling", "enabled")) return; - const level = getSetting(guild.id, "levelling", "set_level") as string; - if (level) { - const newLevel = kominator(level); - setLevel(guild.id, newLevel[0], +newLevel[1], 100 * +newLevel[1]); - setSetting(guild.id, "levelling", "set_level", ""); - } + // const level = getSetting(guild.id, "levelling", "set_level") as string; + // if (level) { + // const newLevel = kominator(level); + // setLevel(guild.id, newLevel[0], +newLevel[1], 100 * +newLevel[1]); + // setSetting(guild.id, "levelling", "set_level", ""); + // } const blockedChannels = getSetting(guild.id, "levelling", "block_channels") as string; if (blockedChannels != undefined) @@ -43,7 +43,7 @@ export default { const cooldowns = new Map(); const cooldown = getSetting(guild.id, "levelling", "set_cooldown") as number; - const multiplier = getSetting(guild.id, "levelling", "add_multiplier"); + // const multiplier = getSetting(guild.id, "levelling", "add_multiplier"); let expGain = getSetting(guild.id, "levelling", "set_xp_gain") as number; if (cooldown > 0) { @@ -55,15 +55,15 @@ export default { else cooldowns.set(key, now); } - if (multiplier) { - const expMultiplier = kominator(multiplier); - - if (expMultiplier[1] == "channel") if (message.channelId != expMultiplier[2]) return; - if (expMultiplier[1] == "role") - if (!message.member?.roles.cache.has(expMultiplier[2])) return; - - expGain = expGain * +expMultiplier[0]; - } + // if (multiplier) { + // const expMultiplier = kominator(multiplier as string); + // + // if (expMultiplier[1] == "channel") if (message.channelId != expMultiplier[2]) return; + // if (expMultiplier[1] == "role") + // if (!message.member?.roles.cache.has(expMultiplier[2])) return; + // + // expGain = expGain * +expMultiplier[0]; + // } const levelChannelId = getSetting(guild.id, "levelling", "channel"); const [guildLevel, guildExp] = getLevel(guild.id, author.id); diff --git a/src/utils/database/settings.ts b/src/utils/database/settings.ts index e164869..8888214 100644 --- a/src/utils/database/settings.ts +++ b/src/utils/database/settings.ts @@ -25,16 +25,16 @@ export const settingsDefinition: Record< type: "TEXT", desc: "ID(s) of the channels where messages aren't counted, comma separated." }, - set_level: { type: "TEXT", desc: "Set the level of a user." }, - add_multiplier: { - type: "LIST", - desc: "Add an XP multiplier to the levelling system.", - val: { - multiplier: { type: "INTEGER", desc: "Set the XP multiplier for the role/channel." }, - role_channel: { type: "TEXT", desc: "Role or channel. (choose)" }, - id: { type: "TEXT", desc: "ID of the role/channel." } - } - }, + // set_level: { type: "TEXT", desc: "Set the level of a user." }, + // add_multiplier: { + // type: "LIST", + // desc: "Add an XP multiplier to the levelling system.", + // val: { + // multiplier: { type: "INTEGER", desc: "Set the XP multiplier for the role/channel." }, + // role_channel: { type: "TEXT", desc: "Role or channel. (choose)" }, + // id: { type: "TEXT", desc: "ID of the role/channel." } + // } + // }, set_xp_gain: { type: "INTEGER", desc: "Set the amount of XP a user gains per message.", @@ -131,11 +131,11 @@ export function getSetting( case "TEXT": return res[0].value as TypeOfKey; case "BOOL": - return (res[0].value ? "true" : "false") as TypeOfKey; + return (res[0].value === "1" ? true : false) as TypeOfKey; case "INTEGER": return parseInt(res[0].value) as TypeOfKey; case "LIST": - return kominator(res[0].value); + return kominator(res[0].value) as TypeOfKey; default: return "WIP" as TypeOfKey; } diff --git a/src/utils/embeds/modEmbed.ts b/src/utils/embeds/modEmbed.ts index c31ccf6..3979529 100644 --- a/src/utils/embeds/modEmbed.ts +++ b/src/utils/embeds/modEmbed.ts @@ -106,11 +106,8 @@ export async function modEmbed( const { interaction, user, action, duration, dm, dbAction, expiresAt } = options; const guild = interaction.guild!; const name = user.displayName; - const generalValues = [`Responsible moderator is **${interaction.user.displayName}**`]; - reason - ? generalValues.push(`**Reason** provided is ${inlineCode(reason)}`) - : generalValues.push("*No reason provided*"); - + const generalValues = [`**Moderator**: ${interaction.user.displayName}`]; + reason ? generalValues.push(`**Reason**: ${reason}`) : generalValues.push("*No reason provided*"); if (duration) generalValues.push(`**Duration**: ${ms(ms(duration), { long: true })}`); const footer = [`User ID: ${user.id}`]; if (dbAction) { @@ -146,6 +143,7 @@ export async function modEmbed( .send({ embeds: [ embed + .setAuthor({ name: `• ${name}`, iconURL: user.displayAvatarURL() }) .setTitle(`You got ${action.toLowerCase()}.`) .setDescription(generalValues.slice(+!showModerator, generalValues.length).join("\n")) .setColor(genColor(0)) diff --git a/src/utils/embeds/serverEmbed.ts b/src/utils/embeds/serverEmbed.ts index 54bf3ac..b2d0103 100644 --- a/src/utils/embeds/serverEmbed.ts +++ b/src/utils/embeds/serverEmbed.ts @@ -24,10 +24,12 @@ export async function serverEmbed(options: Options) { ).size; const bots = members.filter(member => member.user.bot); const formattedUserCount = (guild.memberCount - bots.size)?.toLocaleString("en-US"); + const icon = guild.iconURL(); const roles = guild.roles.cache; const sortedRoles = [...roles].sort((role1, role2) => role2[1].position - role1[1].position); sortedRoles.pop(); + const rolesLength = sortedRoles.length; const channels = guild.channels.cache; const channelSizes = { @@ -44,13 +46,13 @@ export async function serverEmbed(options: Options) { const embed = new EmbedBuilder() .setAuthor({ - name: ` ${pages ? `#${page} • ` : "• "}${guild.name}`, - iconURL: guild.iconURL()! + name: `${pages ? `#${page} • ` : icon ? "• " : ""}${guild.name}`, + iconURL: icon! }) .setDescription(guild.description ? guild.description : null) .setFields({ name: "📃 • General", value: generalValues.join("\n") }) .setFooter({ text: `${pages ? `Page ${page}/${pages}\n` : ""}Server ID: ${guild.id}` }) - .setThumbnail(guild.iconURL()) + .setThumbnail(icon) .setColor((await imageColor(guild)) ?? genColor(200)); if (options.roles) @@ -62,7 +64,7 @@ export async function serverEmbed(options: Options) { : `${sortedRoles .slice(0, 5) .map(role => `<@&${role[0]}>`) - .join(", ")}${roles.size > 5 ? ` and **${roles.size - 6}** more` : ""}` + .join(", ")}${rolesLength > 5 ? ` and **${rolesLength - 5}** more` : ""}` }); embed.addFields( diff --git a/src/utils/unbanScheduler.ts b/src/utils/unbanScheduler.ts index 18dec97..a79ef18 100644 --- a/src/utils/unbanScheduler.ts +++ b/src/utils/unbanScheduler.ts @@ -1,35 +1,45 @@ -import { Client } from "discord.js"; -import { removeModeration, getPendingBans } from "./database/moderation"; +import { Client, EmbedBuilder } from "discord.js"; +import { genColor } from "./colorGen"; +import { getPendingBans, removeModeration } from "./database/moderation"; +import { logChannel } from "./logChannel"; -export function scheduleUnban(client: Client, guildId: string, userId: string, delay: number) { +export async function logUnban(client: Client) { + const pendingBans = getPendingBans(Date.now()); + + const now = Date.now(); + console.log(pendingBans); + console.log(getPendingBans(now)); + for (const ban of pendingBans) { + console.log(ban); + const guild = await client.guilds.fetch(ban.guild); + const user = guild.members.cache.get(ban.user)!; + const embed = new EmbedBuilder() + .setAuthor({ name: `• Unbanned ${user.displayName}`, iconURL: user.displayAvatarURL() }) + .setDescription([`**Moderator**: ${ban.moderator}`, "*Temporary ban has expired*"].join("\n")) + .setFooter({ text: `User ID: ${user.id}\nCase ID: ${ban.id}` }) + .setColor(genColor(100)); + + return await logChannel(guild, embed); + } +} + +export function scheduleUnban(client: Client, guildID: string, userID: string, delay: number) { const scheduledUnbans = new Map(); - const key = `${guildId}-${userId}`; + const key = `${guildID}-${userID}`; if (scheduledUnbans.has(key)) clearTimeout(scheduledUnbans.get(key)!); - // 24.8 days because why not - const maxDelay = 2147483647; - - if (delay > maxDelay) { - const remainingDelay = delay - maxDelay; - const timeout = setTimeout(() => { - scheduleUnban(client, guildId, userId, remainingDelay); - }, maxDelay); - - scheduledUnbans.set(key, timeout); - } else { - const timeout = setTimeout(async () => { - try { - const guild = await client.guilds.fetch(guildId); - await guild.members.unban(userId, "Temporary ban expired"); - removeModeration(guildId, userId); - scheduledUnbans.delete(key); - } catch (error) { - console.error(`Failed to unban user ${userId} in guild ${guildId}:`, error); - } - }, delay); - - scheduledUnbans.set(key, timeout); - } + const timeout = setTimeout(async () => { + try { + await logUnban(client); + await (await client.guilds.fetch(guildID)).members.unban(userID, "Temporary ban has expired"); + removeModeration(guildID, userID); + scheduledUnbans.delete(key); + } catch (error) { + console.error(`Failed to unban user ${userID} in guild ${guildID}:`, error); + } + }, delay); + + return scheduledUnbans.set(key, timeout); } export function rescheduleUnbans(client: Client) { @@ -37,7 +47,7 @@ export function rescheduleUnbans(client: Client) { const pendingBans = getPendingBans(now); for (const ban of pendingBans) { - if (!ban.expiresAt || ban.expiresAt == 0) continue; + if (!ban.expiresAt) continue; if (typeof ban.expiresAt !== "number" || isNaN(ban.expiresAt)) { console.error(`Invalid expiresAt value for ban: ${ban.expiresAt}`); From 1b3e8e26345e3facbdc1e4ba1d60fd3f04a7c770 Mon Sep 17 00:00:00 2001 From: MrSerge01 <86667481+MrSerge01@users.noreply.github.com> Date: Sun, 13 Oct 2024 23:00:00 +0500 Subject: [PATCH 112/127] HUH --- src/commands/About.ts | 59 ++++++++++ src/commands/Leaderboard.ts | 96 ++++++++++++++++ src/commands/Server.ts | 16 +++ src/commands/Serverboard.ts | 89 +++++++++++++++ src/commands/Settings.ts | 133 ++++++++++++++++++++++ src/commands/User.ts | 171 ++++++++++++++++++++++++++++ src/commands/moderation/Ban.ts | 66 +++++++++++ src/commands/moderation/Cases.ts | 90 +++++++++++++++ src/commands/moderation/Delwarn.ts | 70 ++++++++++++ src/commands/moderation/Kick.ts | 42 +++++++ src/commands/moderation/Lock.ts | 80 +++++++++++++ src/commands/moderation/Mute.ts | 66 +++++++++++ src/commands/moderation/Purge.ts | 85 ++++++++++++++ src/commands/moderation/Slowdown.ts | 89 +++++++++++++++ src/commands/moderation/Unban.ts | 54 +++++++++ src/commands/moderation/Unlock.ts | 80 +++++++++++++ src/commands/moderation/Unmute.ts | 43 +++++++ src/commands/moderation/Warn.ts | 49 ++++++++ src/commands/news/Add.ts | 82 +++++++++++++ src/commands/news/Edit.ts | 112 ++++++++++++++++++ src/commands/news/Remove.ts | 55 +++++++++ src/commands/news/View.ts | 91 +++++++++++++++ 22 files changed, 1718 insertions(+) create mode 100644 src/commands/About.ts create mode 100644 src/commands/Leaderboard.ts create mode 100644 src/commands/Server.ts create mode 100644 src/commands/Serverboard.ts create mode 100644 src/commands/Settings.ts create mode 100644 src/commands/User.ts create mode 100644 src/commands/moderation/Ban.ts create mode 100644 src/commands/moderation/Cases.ts create mode 100644 src/commands/moderation/Delwarn.ts create mode 100644 src/commands/moderation/Kick.ts create mode 100644 src/commands/moderation/Lock.ts create mode 100644 src/commands/moderation/Mute.ts create mode 100644 src/commands/moderation/Purge.ts create mode 100644 src/commands/moderation/Slowdown.ts create mode 100644 src/commands/moderation/Unban.ts create mode 100644 src/commands/moderation/Unlock.ts create mode 100644 src/commands/moderation/Unmute.ts create mode 100644 src/commands/moderation/Warn.ts create mode 100644 src/commands/news/Add.ts create mode 100644 src/commands/news/Edit.ts create mode 100644 src/commands/news/Remove.ts create mode 100644 src/commands/news/View.ts diff --git a/src/commands/About.ts b/src/commands/About.ts new file mode 100644 index 0000000..b0c50ba --- /dev/null +++ b/src/commands/About.ts @@ -0,0 +1,59 @@ +import { EmbedBuilder, SlashCommandBuilder, type ChatInputCommandInteraction } from "discord.js"; +import { genColor } from "../utils/colorGen"; +import { randomise } from "../utils/randomise"; + +export default class About { + data: SlashCommandBuilder; + constructor() { + this.data = new SlashCommandBuilder() + .setName("about") + .setDescription("Shows information about Sokora."); + } + + async run(interaction: ChatInputCommandInteraction) { + const client = interaction.client; + const guilds = client.guilds.cache; + const members = guilds.map(guild => guild.memberCount).reduce((a, b) => a + b); + const shards = client.shard?.count; + let emojis = ["💖", "💝", "💓", "💗", "💘", "💟", "💕", "💞"]; + if (Math.round(Math.random() * 100) <= 5) emojis = ["⌨️", "💻", "🖥️"]; + + const embed = new EmbedBuilder() + .setAuthor({ name: "• About Sokora", iconURL: client.user.displayAvatarURL() }) + .setDescription( + "Sokora is a multipurpose Discord bot that lets you manage your servers easily." + ) + .setFields( + { + name: "📃 • General", + value: [ + "**Version** 0.1-preview, *Kaishi*", + `**${members}** members • **${guilds.size}** guild${guilds.size == 1 ? "" : "s"} ${ + !shards ? "" : `• **${shards}** shard${shards == 1 ? "" : "s"}` + }` + ].join("\n") + }, + { + name: "🌌 • Entities involved", + value: [ + "**Founder**: Goos", + "**Developers**: Dimkauzh, Froxcey, Golem64, Koslz, MQuery, Nikkerudon, Spectrum, ThatBOI", + "**Designers**: ArtyH, Optix, proJM", + "**Translators**: Dimkauzh, flojo, Golem64, Optix, SaFire, ThatBOI", + "**Testers**: Blaze, fishy, flojo, Trynera", + "And **YOU**, for using Sokora." + ].join("\n") + }, + { + name: "🔗 • Links", + value: + "[GitHub](https://www.github.com/NebulaTheBot) • [YouTube](https://www.youtube.com/@NebulaTheBot) • [Instagram](https://instagram.com/NebulaTheBot) • [Mastodon](https://mastodon.online/@NebulaTheBot@mastodon.social) • [Guilded](https://guilded.gg/Nebula) • [Revolt](https://rvlt.gg/28TS9aXy)" + } + ) + .setFooter({ text: `Made with ${randomise(emojis)} by the Sokora team` }) + .setThumbnail(client.user.displayAvatarURL()) + .setColor(genColor(270)); + + await interaction.reply({ embeds: [embed] }); + } +} diff --git a/src/commands/Leaderboard.ts b/src/commands/Leaderboard.ts new file mode 100644 index 0000000..274850c --- /dev/null +++ b/src/commands/Leaderboard.ts @@ -0,0 +1,96 @@ +import { + ActionRowBuilder, + ButtonBuilder, + ButtonInteraction, + ButtonStyle, + EmbedBuilder, + SlashCommandBuilder, + type ChatInputCommandInteraction, + type SlashCommandOptionsOnlyBuilder +} from "discord.js"; +import { getGuildLeaderboard } from "../utils/database/levelling"; +import { errorEmbed } from "../utils/embeds/errorEmbed"; + +export default class Leaderboard { + data: SlashCommandOptionsOnlyBuilder; + constructor() { + this.data = new SlashCommandBuilder() + .setName("leaderboard") + .setDescription("Displays the guild leaderboard.") + .addNumberOption(option => option.setName("page").setDescription("Page number to display.")); + } + + async run(interaction: ChatInputCommandInteraction) { + const guildID = interaction.guild?.id; + if (!guildID) return errorEmbed(interaction, "This command can only be used in a server."); + + const leaderboardData = getGuildLeaderboard(guildID); + if (!leaderboardData.length) + return errorEmbed( + interaction, + "No data found.", + "There is no levelling data for this server yet." + ); + + leaderboardData.sort((a, b) => { + if (b.level != a.level) return b.level - a.level; + else return b.exp - a.exp; + }); + + const totalPages = Math.ceil(leaderboardData.length / 5); + let page = interaction.options.getNumber("page") || 1; + page = Math.max(1, Math.min(page, totalPages)); + const generateEmbed = async () => { + const start = (page - 1) * 5; + const end = start + 5; + const pageData = leaderboardData.slice(start, end); + + const embed = new EmbedBuilder() + .setTitle(`Leaderboard - Page ${page}/${totalPages}`) + .setColor("#0099ff"); + + for (let i = 0; i < pageData.length; i++) { + const userData = pageData[i]; + const user = await interaction.client.users.fetch(userData.user); + embed.addFields({ + name: `${start + i + 1}. ${user.tag}`, + value: `Level: ${Math.floor(userData.level)} | EXP: ${Math.floor(userData.exp)}` + }); + } + + return embed; + }; + + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder().setCustomId("left").setEmoji("⬅️").setStyle(ButtonStyle.Primary), + new ButtonBuilder().setCustomId("right").setEmoji("➡️").setStyle(ButtonStyle.Primary) + ); + + const reply = await interaction.reply({ + embeds: [await generateEmbed()], + components: totalPages > 1 ? [row] : [], + fetchReply: true + }); + + if (totalPages > 1) { + const collector = reply.createMessageComponentCollector({ + time: 60000 + }); + + collector.on("collect", async (i: ButtonInteraction) => { + if (i.user.id != interaction.user.id) + return errorEmbed(i, "You are not the author of this command."); + + if (i.customId == "left") page = page > 1 ? page - 1 : totalPages; + else page = page < totalPages ? page + 1 : 1; + + await i.update({ + embeds: [await generateEmbed()], + components: [row] + }); + }); + + collector.on("end", async () => await interaction.editReply({ components: [] })); + } + } +} diff --git a/src/commands/Server.ts b/src/commands/Server.ts new file mode 100644 index 0000000..9c5ac8f --- /dev/null +++ b/src/commands/Server.ts @@ -0,0 +1,16 @@ +import { SlashCommandBuilder, type ChatInputCommandInteraction } from "discord.js"; +import { serverEmbed } from "../utils/embeds/serverEmbed"; + +export default class Server { + data: SlashCommandBuilder; + constructor() { + this.data = new SlashCommandBuilder() + .setName("server") + .setDescription("Shows this server's info."); + } + + async run(interaction: ChatInputCommandInteraction) { + const embed = await serverEmbed({ guild: interaction.guild!, roles: true }); + await interaction.reply({ embeds: [embed] }); + } +} diff --git a/src/commands/Serverboard.ts b/src/commands/Serverboard.ts new file mode 100644 index 0000000..cca033f --- /dev/null +++ b/src/commands/Serverboard.ts @@ -0,0 +1,89 @@ +import { + ActionRowBuilder, + ButtonBuilder, + ButtonInteraction, + ButtonStyle, + SlashCommandBuilder, + type ChatInputCommandInteraction, + type SlashCommandOptionsOnlyBuilder +} from "discord.js"; +import { listPublicServers } from "../utils/database/settings"; +import { errorEmbed } from "../utils/embeds/errorEmbed"; +import { serverEmbed } from "../utils/embeds/serverEmbed"; + +export default class Serverboard { + data: SlashCommandOptionsOnlyBuilder; + constructor() { + this.data = new SlashCommandBuilder() + .setName("serverboard") + .setDescription("Shows the servers that have Sokora.") + .addNumberOption(number => + number.setName("page").setDescription("The page you want to see.") + ); + } + + async run(interaction: ChatInputCommandInteraction) { + const guildList = ( + await Promise.all(listPublicServers().map(id => interaction.client.guilds.fetch(id))) + ).sort((a, b) => b.memberCount - a.memberCount); + + const pages = guildList.length; + if (!pages) + return await errorEmbed( + interaction, + "No public server found.", + "By some magical miracle, all the servers using Sokora turned off their visibility. Use /settings serverboard `shown: True` to make your server publicly visible." + ); + + const argPage = interaction.options.getNumber("page") as number; + let page = (argPage - 1 <= 0 ? 0 : argPage - 1 > pages ? pages - 1 : argPage - 1) || 0; + + async function getEmbed() { + return await serverEmbed({ guild: guildList[page], page: page + 1, pages }); + } + + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId("left") + .setEmoji("1271045078042935398") + .setStyle(ButtonStyle.Primary), + new ButtonBuilder() + .setCustomId("right") + .setEmoji("1271045041313415370") + .setStyle(ButtonStyle.Primary) + ); + + const reply = await interaction.reply({ + embeds: [await getEmbed()], + components: pages != 1 ? [row] : [] + }); + if (pages == 1) return; + + reply + .createMessageComponentCollector({ time: 60000 }) + .on("collect", async (i: ButtonInteraction) => { + if (i.message.id != (await reply.fetch()).id) + return await errorEmbed( + i, + "For some reason, this click would've caused the bot to error. Thankfully, this message right here prevents that." + ); + + if (i.user.id != interaction.user.id) + return await errorEmbed(i, "You aren't the person who executed this command."); + + setTimeout(async () => await i.editReply({ components: [] }), 60000); + switch (i.customId) { + case "left": + page--; + if (page < 0) page = pages - 1; + break; + case "right": + page++; + if (page >= pages) page = 0; + break; + } + + await i.update({ embeds: [await getEmbed()], components: [row] }); + }); + } +} diff --git a/src/commands/Settings.ts b/src/commands/Settings.ts new file mode 100644 index 0000000..15f94da --- /dev/null +++ b/src/commands/Settings.ts @@ -0,0 +1,133 @@ +import { + AutocompleteInteraction, + EmbedBuilder, + InteractionType, + PermissionsBitField, + SlashCommandBuilder, + SlashCommandSubcommandBuilder, + SlashCommandSubcommandGroupBuilder, + type ChatInputCommandInteraction +} from "discord.js"; +import { genColor } from "../utils/colorGen"; +import { + getSetting, + setSetting, + settingsDefinition, + settingsKeys +} from "../utils/database/settings"; +import { errorEmbed } from "../utils/embeds/errorEmbed"; + +export default class Settings { + data: SlashCommandBuilder; + constructor() { + this.data = new SlashCommandBuilder() + .setName("settings") + .setDescription("Configure Sokora to your liking.") + .setDefaultMemberPermissions(PermissionsBitField.Flags.Administrator); + + settingsKeys.forEach(key => { + const subcommand = new SlashCommandSubcommandBuilder() + .setName(key) + .setDescription("This subcommand has no description."); + + Object.keys(settingsDefinition[key]).forEach(sub => { + switch (settingsDefinition[key][sub]["type"] as string) { + case "BOOL": + subcommand.addBooleanOption(option => + option + .setName(sub) + .setDescription(settingsDefinition[key][sub]["desc"]) + .setRequired(false) + ); + break; + case "INTEGER": + subcommand.addIntegerOption(option => + option + .setName(sub) + .setDescription(settingsDefinition[key][sub]["desc"]) + .setRequired(false) + ); + break; + case "USER": + subcommand.addUserOption(option => + option + .setName(sub) + .setDescription(settingsDefinition[key][sub]["desc"]) + .setRequired(false) + ); + break; + // case "LIST": + // const subcommandGroup = new SlashCommandSubcommandGroupBuilder() + // .setName(sub) + // .setDescription("This subcommand group has no description."); + default: // Also includes "TEXT" + subcommand.addStringOption(option => + option + .setName(sub) + .setDescription(settingsDefinition[key][sub]["desc"]) + .setRequired(false) + ); + break; + } + }); + this.data.addSubcommand(subcommand); + }); + } + + async run(interaction: ChatInputCommandInteraction) { + const guild = interaction.guild!; + if ( + !guild.members.cache + ?.get(interaction.user.id) + ?.permissions.has(PermissionsBitField.Flags.Administrator) + ) + return await errorEmbed( + interaction, + "You can't execute this command.", + "You need the **Administrator** permission." + ); + + const key = interaction.options.getSubcommand() as keyof typeof settingsDefinition; + const values = interaction.options.data[0].options!; + console.log(values); + if (!values.length) { + const embed = new EmbedBuilder().setTitle(`Settings for ${key}`).setColor(genColor(100)); + const description: string[] = []; + + Object.keys(settingsDefinition[key]).forEach(name => { + description.push(`${name}: ${getSetting(guild.id, key, name)?.toString() || "Not set"}`); + embed.setDescription(description.join("\n")); + }); + + return await interaction.reply({ embeds: [embed] }); + } + + const embed = new EmbedBuilder().setTitle(`Parameters changed`).setColor(genColor(100)); + values.forEach(option => { + setSetting(guild.id, key, option.name, option.value as string); + embed.addFields({ + name: option.name, + value: option.value?.toString() || "Not set" + }); + }); + + await interaction.reply({ embeds: [embed] }); + } + + async autocomplete(interaction: AutocompleteInteraction) { + if (interaction.type != InteractionType.ApplicationCommandAutocomplete) return; + //if (interaction.options.getSubcommand() != this.data.name) return; + switch (Object.keys(settingsDefinition[interaction.options.getSubcommand()])[0]) { + case "BOOL": + await interaction.respond( + ["true", "false"].map(choice => ({ + name: choice, + value: choice + })) + ); + break; + default: + await interaction.respond([]); + } + } +} diff --git a/src/commands/User.ts b/src/commands/User.ts new file mode 100644 index 0000000..cc2bdb8 --- /dev/null +++ b/src/commands/User.ts @@ -0,0 +1,171 @@ +import { + ActionRowBuilder, + ButtonBuilder, + ButtonInteraction, + ButtonStyle, + EmbedBuilder, + SlashCommandBuilder, + type ChatInputCommandInteraction, + type SlashCommandOptionsOnlyBuilder +} from "discord.js"; +import { genColor } from "../utils/colorGen"; +import { getLevel, setLevel } from "../utils/database/levelling"; +import { getSetting } from "../utils/database/settings"; +import { errorEmbed } from "../utils/embeds/errorEmbed"; +import { imageColor } from "../utils/imageColor"; + +export default class User { + data: SlashCommandOptionsOnlyBuilder; + constructor() { + this.data = new SlashCommandBuilder() + .setName("user") + .setDescription("Shows your (or another user's) info.") + .addUserOption(user => user.setName("user").setDescription("Select the user.")); + } + + async run(interaction: ChatInputCommandInteraction) { + const guild = interaction.guild!; + const target = guild.members.cache.get( + interaction.options.getUser("user")?.id ?? interaction.user.id + )!; + + const user = await target.user.fetch(); + let serverInfo = [`Joined on ****`]; + const guildRoles = guild.roles.cache.filter(role => target.roles.cache.has(role.id))!; + const avatar = target.displayAvatarURL(); + const memberRoles = [...guildRoles].sort( + (role1, role2) => role2[1].position - role1[1].position + ); + memberRoles.pop(); + const rolesLength = memberRoles.length; + + if (target.premiumSinceTimestamp) + serverInfo.push(`Boosting since **${target.premiumSinceTimestamp}**`); + + if (memberRoles.length) + serverInfo.push( + `**${guildRoles.filter(role => target.roles.cache.has(role.id)).size! - 1}** ${ + memberRoles.length == 1 ? "role" : "roles" + } • ${memberRoles + .slice(0, 5) + .map(role => `<@&${role[1].id}>`) + .join(", ")}${rolesLength > 3 ? ` and **${rolesLength - 3}** more` : ""}` + ); + + const embedColor = + user.hexAccentColor ?? (await imageColor(undefined, target)) ?? genColor(200); + + let embed = new EmbedBuilder() + .setAuthor({ + name: `${avatar ? "• " : ""}${target.nickname ?? user.displayName}`, + iconURL: avatar + }) + .setFields( + { + name: `<:discord:1266797021126459423> • Discord info`, + value: [ + `Username is **${user.username}**`, + `Display name is ${ + user.displayName == user.username ? "*not there*" : `**${user.displayName}**` + }`, + `Created on ****` + ].join("\n") + }, + { + name: "📒 • Server info", + value: serverInfo.join("\n") + } + ) + .setFooter({ text: `User ID: ${target.id}` }) + .setThumbnail(avatar) + .setColor(embedColor); + + const enabled = getSetting(`${guild.id}`, "levelling", "enabled"); + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId("general") + .setLabel("• General") + .setEmoji("📃") + .setStyle(ButtonStyle.Primary), + new ButtonBuilder() + .setCustomId("level") + .setLabel("• Level") + .setEmoji("⚡") + .setStyle(ButtonStyle.Primary) + ); + row.components[0].setDisabled(true); + const reply = await interaction.reply({ + embeds: [embed], + components: enabled ? [row] : [] + }); + + console.log(enabled); + console.log(!enabled); + if (!enabled && user.bot) return; + const [guildLevel, guildExp] = getLevel(`${guild.id}`, `${target.id}`)!; + const [globalLevel, globalExp] = getLevel("0", `${target.id}`)!; + if (!guildLevel && !guildExp) setLevel(`${guild.id}`, `${target.id}`, 0, 0); + if (!globalLevel && !globalExp) setLevel("0", `${target.id}`, 0, 0); + + const nextLevelExp = Math.floor(100 * 1.15 * ((guildLevel ?? 0) + 1))?.toLocaleString("en-US"); + const globalNextLevelExp = Math.floor(100 * 1.15 * ((globalLevel ?? 0) + 1))?.toLocaleString( + "en-US" + ); + + const collector = reply.createMessageComponentCollector({ time: 60000 }); + collector.on("collect", async (i: ButtonInteraction) => { + if (i.message.id != (await reply.fetch()).id) + return await errorEmbed( + i, + "For some reason, this click would've caused the bot to error. Thankfully, this message right here prevents that." + ); + + if (i.user.id != interaction.user.id) + return await errorEmbed(i, "You aren't the person who executed this command."); + + i.customId == "general" + ? row.components[0].setDisabled(true) + : row.components[1].setDisabled(true); + + const levelEmbed = new EmbedBuilder() + .setAuthor({ + name: `• ${target.nickname ?? user.displayName}`, + iconURL: target.displayAvatarURL() + }) + .setFields( + { + name: `⚡ • Guild level ${guildLevel ?? 0}`, + value: [ + `**${guildExp.toLocaleString("en-US") ?? 0}/${nextLevelExp}** EXP`, + `**Next level**: ${(guildLevel ?? 0) + 1}` + ].join("\n"), + inline: true + }, + { + name: `⛈️ • Global level ${globalLevel ?? 0}`, + value: [ + `**${globalExp.toLocaleString("en-US") ?? 0}/${globalNextLevelExp}** EXP`, + `**Next level**: ${(globalLevel ?? 0) + 1}` + ].join("\n"), + inline: true + } + ) + .setFooter({ text: `User ID: ${target.id}` }) + .setThumbnail(target.displayAvatarURL()) + .setColor(embedColor); + + switch (i.customId) { + case "general": + row.components[1].setDisabled(false); + await i.update({ embeds: [embed], components: [row] }); + break; + case "level": + row.components[0].setDisabled(false); + await i.update({ embeds: [levelEmbed], components: [row] }); + break; + } + }); + + collector.on("end", async () => await interaction.editReply({ components: [] })); + } +} diff --git a/src/commands/moderation/Ban.ts b/src/commands/moderation/Ban.ts new file mode 100644 index 0000000..f328a3e --- /dev/null +++ b/src/commands/moderation/Ban.ts @@ -0,0 +1,66 @@ +import { + PermissionsBitField, + SlashCommandSubcommandBuilder, + type ChatInputCommandInteraction +} from "discord.js"; +import ms from "ms"; +import { errorEmbed } from "../../utils/embeds/errorEmbed"; +import { errorCheck, modEmbed } from "../../utils/embeds/modEmbed"; +import { scheduleUnban } from "../../utils/unbanScheduler"; + +export default class Ban { + data: SlashCommandSubcommandBuilder; + constructor() { + this.data = new SlashCommandSubcommandBuilder() + .setName("ban") + .setDescription("Bans a user.") + .addUserOption(user => + user.setName("user").setDescription("The user that you want to ban.").setRequired(true) + ) + .addStringOption(string => string.setName("reason").setDescription("The reason for the ban.")) + .addStringOption(string => + string.setName("duration").setDescription("The duration of the ban (e.g 2mo, 1y).") + ); + } + + async run(interaction: ChatInputCommandInteraction) { + const user = interaction.options.getUser("user")!; + const guild = interaction.guild!; + const duration = interaction.options.getString("duration"); + const reason = interaction.options.getString("reason"); + if ( + await errorCheck( + PermissionsBitField.Flags.BanMembers, + { interaction, user, action: "Ban" }, + { allErrors: true, botError: true, ownerError: true }, + "Ban Members" + ) + ) + return; + + let expiresAt: number | undefined; + if (duration) { + const durationMs = ms(duration); + if (!durationMs) + return await errorEmbed( + interaction, + `You can't ban ${user.displayName} temporarily.`, + "The duration is invalid." + ); + + expiresAt = Date.now() + durationMs; + scheduleUnban(interaction.client, guild.id, user.id, durationMs); + } + + try { + await guild.members.ban(user.id, { reason: reason ?? undefined }); + } catch (err) { + console.error("Failed to ban user:", err); + } + + await modEmbed( + { interaction, user, action: "Banned", duration, dm: true, dbAction: "BAN", expiresAt }, + reason + ); + } +} diff --git a/src/commands/moderation/Cases.ts b/src/commands/moderation/Cases.ts new file mode 100644 index 0000000..7013bd5 --- /dev/null +++ b/src/commands/moderation/Cases.ts @@ -0,0 +1,90 @@ +import { + EmbedBuilder, + PermissionsBitField, + SlashCommandSubcommandBuilder, + type ChatInputCommandInteraction +} from "discord.js"; +import { genColor } from "../../utils/colorGen"; +import { getModeration, listUserModeration } from "../../utils/database/moderation"; +import { errorEmbed } from "../../utils/embeds/errorEmbed"; +import { randomise } from "../../utils/randomise"; + +export default class Cases { + data: SlashCommandSubcommandBuilder; + constructor() { + this.data = new SlashCommandSubcommandBuilder() + .setName("cases") + .setDescription("Moderation cases in a server.") + .addUserOption(user => user.setName("user").setDescription("The user that you want to see.")) + .addStringOption(string => + string.setName("id").setDescription("The ID of a specific moderation case you want to see.") + ); + } + + async run(interaction: ChatInputCommandInteraction) { + const actionsEmojis: { [key: string]: string } = { + WARN: "⚠️", + MUTE: "🔇", + KICK: "📤", + BAN: "🔨" + }; + + const nothingMsg = [ + "Nothing to see here...", + "Ayay, no cases on this horizon cap'n!", + "Clean as a whistle!", + "0+0=?" + ]; + + const guild = interaction.guild!; + if ( + !guild.members.cache + .get(interaction.user.id) + ?.permissions.has(PermissionsBitField.Flags.ModerateMembers) + ) + return await errorEmbed( + interaction, + "You can't execute this command.", + "You need the **Moderate Members** permission." + ); + + const user = interaction.options.getUser("user")!; + // const warns = listUserModeration(guild.id, user.id, "WARN"); + // const mutes = listUserModeration(guild.id, user.id, "MUTE"); + // const kicks = listUserModeration(guild.id, user.id, "KICK"); + // const bans = listUserModeration(guild.id, user.id, "BAN"); + let actionID = interaction.options.getString("id"); + if (actionID && actionID?.startsWith("#")) actionID = actionID.slice(1); + const actions = actionID + ? getModeration(guild.id, user.id, actionID) + : listUserModeration(guild.id, user.id); + + const embed = new EmbedBuilder() + .setAuthor({ name: `• Cases of ${user.displayName}`, iconURL: user.displayAvatarURL() }) + .setFields( + actions.length > 0 + ? actions.map(action => { + const actionValues = [ + `**Moderator**: <@${action.moderator}>`, + action.reason ? `**Reason**: ${action.reason}` : "*No reason provided*", + `-# **Time of action**: ` + ]; + + return { + name: `${actionsEmojis[action.type]} • ${action.type} #${action.id}`, // Include durations ? needs to add a db column + value: actionValues.join("\n") + }; + }) + : [ + { + name: `💨 • ${randomise(nothingMsg)}`, + value: "*No actions have been taken on this user*" + } + ] + ) + .setFooter({ text: `User ID: ${user.id}` }) + .setColor(genColor(200)); + + await interaction.reply({ embeds: [embed] }); + } +} diff --git a/src/commands/moderation/Delwarn.ts b/src/commands/moderation/Delwarn.ts new file mode 100644 index 0000000..88af050 --- /dev/null +++ b/src/commands/moderation/Delwarn.ts @@ -0,0 +1,70 @@ +import { + DMChannel, + EmbedBuilder, + PermissionsBitField, + SlashCommandSubcommandBuilder, + type ChatInputCommandInteraction +} from "discord.js"; +import { genColor } from "../../utils/colorGen"; +import { listUserModeration, removeModeration } from "../../utils/database/moderation"; +import { errorEmbed } from "../../utils/embeds/errorEmbed"; +import { errorCheck } from "../../utils/embeds/modEmbed"; +import { logChannel } from "../../utils/logChannel"; + +export default class Delwarn { + data: SlashCommandSubcommandBuilder; + constructor() { + this.data = new SlashCommandSubcommandBuilder() + .setName("delwarn") + .setDescription("Removes a warning from a user.") + .addUserOption(user => + user + .setName("user") + .setDescription("The user that you want to free from the warning.") + .setRequired(true) + ) + .addNumberOption(number => + number.setName("id").setDescription("The id of the warn.").setRequired(true) + ); + } + + async run(interaction: ChatInputCommandInteraction) { + const user = interaction.options.getUser("user")!; + const guild = interaction.guild!; + const name = user.displayName; + const id = interaction.options.getNumber("id"); + const warns = listUserModeration(guild.id, user.id, "WARN"); + const newWarns = warns.filter(warn => warn.id != `${id}`); + if ( + await errorCheck( + PermissionsBitField.Flags.ModerateMembers, + { interaction, user, action: "Remove a warn" }, + { allErrors: true, botError: false }, + "Moderate Members" + ) + ) + return; + + if (newWarns.length == warns.length) + return await errorEmbed(interaction, `There is no warn with the id of ${id}.`); + + const embed = new EmbedBuilder() + .setAuthor({ name: `• Removed a warning from ${name}`, iconURL: user.displayAvatarURL() }) + .setDescription(`**Moderator**: ${interaction.user.displayName}`) + .setFooter({ text: `User ID: ${user.id}` }) + .setColor(genColor(100)); + + await logChannel(guild, embed); + try { + removeModeration(guild.id, `${id}`); + } catch (error) { + console.error(error); + } + + await interaction.reply({ embeds: [embed] }); + const dmChannel = (await user.createDM().catch(() => null)) as DMChannel | null; + if (!dmChannel) return; + if (user.bot) return; + await dmChannel.send({ embeds: [embed.setTitle("Your warning has been removed.")] }); + } +} diff --git a/src/commands/moderation/Kick.ts b/src/commands/moderation/Kick.ts new file mode 100644 index 0000000..e302404 --- /dev/null +++ b/src/commands/moderation/Kick.ts @@ -0,0 +1,42 @@ +import { + PermissionsBitField, + SlashCommandSubcommandBuilder, + type ChatInputCommandInteraction +} from "discord.js"; +import { errorCheck, modEmbed } from "../../utils/embeds/modEmbed"; + +export default class Kick { + data: SlashCommandSubcommandBuilder; + constructor() { + this.data = new SlashCommandSubcommandBuilder() + .setName("kick") + .setDescription("Kicks a user.") + .addUserOption(user => + user.setName("user").setDescription("The user that you want to kick.").setRequired(true) + ) + .addStringOption(string => + string.setName("reason").setDescription("The reason for the kick.") + ); + } + + async run(interaction: ChatInputCommandInteraction) { + const user = interaction.options.getUser("user")!; + if ( + await errorCheck( + PermissionsBitField.Flags.KickMembers, + { interaction, user, action: "Kick" }, + { allErrors: true, botError: true, ownerError: true, outsideError: true }, + "Kick Members" + ) + ) + return; + + const reason = interaction.options.getString("reason"); + await interaction.guild?.members.cache + .get(user.id) + ?.kick(reason ?? undefined) + .catch(error => console.error(error)); + + await modEmbed({ interaction, user, action: "Kicked", dm: true, dbAction: "KICK" }, reason); + } +} diff --git a/src/commands/moderation/Lock.ts b/src/commands/moderation/Lock.ts new file mode 100644 index 0000000..305ee09 --- /dev/null +++ b/src/commands/moderation/Lock.ts @@ -0,0 +1,80 @@ +import { + ChannelType, + EmbedBuilder, + PermissionsBitField, + SlashCommandSubcommandBuilder, + type ChatInputCommandInteraction +} from "discord.js"; +import { genColor } from "../../utils/colorGen"; +import { errorEmbed } from "../../utils/embeds/errorEmbed"; +import { logChannel } from "../../utils/logChannel"; + +export default class Lock { + data: SlashCommandSubcommandBuilder; + constructor() { + this.data = new SlashCommandSubcommandBuilder() + .setName("lock") + .setDescription("Locks a channel.") + .addChannelOption(channel => + channel + .setName("channel") + .setDescription("The channel that you want to lock.") + .addChannelTypes( + ChannelType.GuildText, + ChannelType.PublicThread, + ChannelType.PrivateThread, + ChannelType.GuildVoice + ) + ); + } + + async run(interaction: ChatInputCommandInteraction) { + const guild = interaction.guild!; + if ( + !guild.members.cache + .get(interaction.user.id) + ?.permissions.has(PermissionsBitField.Flags.ManageRoles) + ) + return await errorEmbed( + interaction, + "You can't execute this command.", + "You need the **Manage Roles** permission." + ); + + const channelOption = interaction.options.getChannel("channel")!; + const channel = guild.channels.cache.get(interaction.channel?.id ?? channelOption.id)!; + if (!channel.permissionsFor(guild.id)?.has("SendMessages")) + return await errorEmbed( + interaction, + "You can't execute this command.", + "The channel is already locked." + ); + + const embed = new EmbedBuilder() + .setAuthor({ name: `Locked a channel.` }) + .setDescription( + [ + `**Moderator**: ${interaction.user.displayName}`, + `**Channel**: ${channelOption ?? `<#${channel.id}>`}` + ].join("\n") + ) + .setColor(genColor(100)); + + if ( + channel.type == ChannelType.GuildText && + ChannelType.PublicThread && + ChannelType.PrivateThread && + ChannelType.GuildVoice + ) + channel.permissionOverwrites + .create(guild.id, { + SendMessages: false, + SendMessagesInThreads: false, + CreatePublicThreads: false + }) + .catch(error => console.error(error)); + + await logChannel(guild, embed); + await interaction.reply({ embeds: [embed] }); + } +} diff --git a/src/commands/moderation/Mute.ts b/src/commands/moderation/Mute.ts new file mode 100644 index 0000000..6442573 --- /dev/null +++ b/src/commands/moderation/Mute.ts @@ -0,0 +1,66 @@ +import { + PermissionsBitField, + SlashCommandSubcommandBuilder, + type ChatInputCommandInteraction +} from "discord.js"; +import ms from "ms"; +import { errorEmbed } from "../../utils/embeds/errorEmbed"; +import { errorCheck, modEmbed } from "../../utils/embeds/modEmbed"; + +export default class Mute { + data: SlashCommandSubcommandBuilder; + constructor() { + this.data = new SlashCommandSubcommandBuilder() + .setName("mute") + .setDescription("Mutes a user.") + .addUserOption(user => + user.setName("user").setDescription("The user that you want to mute.").setRequired(true) + ) + .addStringOption(string => + string + .setName("duration") + .setDescription("The duration of the mute (e.g 30m, 1d, 2h).") + .setRequired(true) + ) + .addStringOption(string => + string.setName("reason").setDescription("The reason for the mute.") + ); + } + + async run(interaction: ChatInputCommandInteraction) { + const user = interaction.options.getUser("user")!; + const duration = interaction.options.getString("duration")!; + const reason = interaction.options.getString("reason"); + if ( + await errorCheck( + PermissionsBitField.Flags.ModerateMembers, + { interaction, user, action: "Mute" }, + { allErrors: true, botError: true, ownerError: true }, + "Moderate Members" + ) + ) + return; + + if (!ms(duration) || ms(duration) > ms("28d")) + return await errorEmbed( + interaction, + `You can't mute ${user.displayName}.`, + "The duration is invalid or is above the 28 day limit." + ); + + const time = new Date( + Date.parse(new Date().toISOString()) + Date.parse(new Date(ms(duration)).toISOString()) + ).toISOString(); + + await interaction.guild?.members.cache + .get(user.id) + ?.edit({ communicationDisabledUntil: time, reason: reason ?? undefined }) + .catch(error => console.error(error)); + + await modEmbed( + { interaction, user, action: "Muted", duration, dm: true, dbAction: "MUTE" }, + reason, + true + ); + } +} diff --git a/src/commands/moderation/Purge.ts b/src/commands/moderation/Purge.ts new file mode 100644 index 0000000..2332999 --- /dev/null +++ b/src/commands/moderation/Purge.ts @@ -0,0 +1,85 @@ +import { + ChannelType, + EmbedBuilder, + PermissionsBitField, + SlashCommandSubcommandBuilder, + type ChatInputCommandInteraction +} from "discord.js"; +import { genColor } from "../../utils/colorGen"; +import { errorEmbed } from "../../utils/embeds/errorEmbed"; +import { logChannel } from "../../utils/logChannel"; + +export default class Clear { + data: SlashCommandSubcommandBuilder; + constructor() { + this.data = new SlashCommandSubcommandBuilder() + .setName("clear") + .setDescription("Clears messages.") + .addNumberOption(number => + number + .setName("amount") + .setDescription("The amount of messages that you want to clear (maximum is 100).") + .setRequired(true) + ) + .addChannelOption(channel => + channel + .setName("channel") + .setDescription("The channel that has the messages that you want to clear.") + .addChannelTypes( + ChannelType.GuildText, + ChannelType.PublicThread, + ChannelType.PrivateThread, + ChannelType.GuildVoice + ) + ); + } + + async run(interaction: ChatInputCommandInteraction) { + const guild = interaction.guild!; + if ( + !guild.members.cache + .get(interaction.user.id) + ?.permissions.has(PermissionsBitField.Flags.ManageMessages) + ) + return await errorEmbed( + interaction, + "You can't execute this command.", + "You need the **Manage Messages** permission." + ); + + const amount = interaction.options.getNumber("amount")!; + if (amount > 100) + return await errorEmbed(interaction, "You can only clear up to 100 messages at a time."); + + if (amount < 1) return await errorEmbed(interaction, "You must clear at least 1 message."); + + const channelOption = interaction.options.getChannel("channel")!; + const channel = guild.channels.cache.get(interaction.channel?.id ?? channelOption.id)!; + const embed = new EmbedBuilder() + .setAuthor({ name: `Cleared ${amount} message${amount == 1 ? "" : "s"}.` }) + .setDescription( + [ + `**Moderator**: ${interaction.user.displayName}`, + `**Channel**: ${channelOption ?? `<#${channel.id}>`}` + ].join("\n") + ) + .setColor(genColor(100)); + + if ( + channel.type == ChannelType.GuildText && + ChannelType.PublicThread && + ChannelType.PrivateThread && + ChannelType.GuildVoice + ) + try { + channel == interaction.channel + ? await channel.bulkDelete(amount + 1, true) + : await channel.bulkDelete(amount, true); + } catch (error) { + console.error(error); + } + + await logChannel(guild, embed); + await interaction.reply({ embeds: [embed] }); + } +} diff --git a/src/commands/moderation/Slowdown.ts b/src/commands/moderation/Slowdown.ts new file mode 100644 index 0000000..3451595 --- /dev/null +++ b/src/commands/moderation/Slowdown.ts @@ -0,0 +1,89 @@ +import { + ChannelType, + EmbedBuilder, + PermissionsBitField, + SlashCommandSubcommandBuilder, + type ChatInputCommandInteraction +} from "discord.js"; +import ms from "ms"; +import { genColor } from "../../utils/colorGen"; +import { errorEmbed } from "../../utils/embeds/errorEmbed"; +import { logChannel } from "../../utils/logChannel"; + +export default class Slowdown { + data: SlashCommandSubcommandBuilder; + constructor() { + this.data = new SlashCommandSubcommandBuilder() + .setName("slowdown") + .setDescription("Slows a channel down.") + .addStringOption(string => + string + .setName("time") + .setDescription( + "Time to slow the channel down to (e.g 30m, 1d, 2h). 0 to remove slowdown." + ) + .setRequired(true) + ) + .addStringOption(string => + string.setName("reason").setDescription("The reason for the slowdown.") + ) + .addChannelOption(channel => + channel + .setName("channel") + .setDescription("The channel that you want to slowdown.") + .addChannelTypes( + ChannelType.GuildText, + ChannelType.PublicThread, + ChannelType.PrivateThread, + ChannelType.GuildVoice + ) + ); + } + + async run(interaction: ChatInputCommandInteraction) { + const guild = interaction.guild!; + if ( + !guild.members.cache + .get(interaction.user.id) + ?.permissions.has(PermissionsBitField.Flags.ManageChannels) + ) + return await errorEmbed( + interaction, + "You can't execute this command.", + "You need the **Manage Channels** permission." + ); + + const time = interaction.options.getString("time")!; + const reason = interaction.options.getString("reason"); + const channelOption = interaction.options.getChannel("channel")!; + const channel = guild.channels.cache.get(interaction.channel?.id ?? channelOption.id)!; + let title = `Set a slowdown of \`${channelOption ?? `${channel.name}`}\` to ${ms(ms(time), { + long: true + })}.`; + if (!ms(time)) title = `Removed the slowdown from \`${channelOption ?? `${channel.name}`}\`.`; + + const embed = new EmbedBuilder() + .setAuthor({ name: title }) + .setDescription( + [ + `**Moderator**: ${interaction.user.displayName}`, + reason ? `**Reason**: ${reason}` : "*No reason provided*", + `**Channel**: ${channelOption ?? `<#${channel.id}>**`}` + ].join("\n") + ) + .setColor(genColor(100)); + + if ( + channel.type == ChannelType.GuildText && + ChannelType.PublicThread && + ChannelType.PrivateThread && + ChannelType.GuildVoice + ) + await channel + .setRateLimitPerUser(ms(time) / 1000, interaction.options.getString("reason")!) + .catch(error => console.error(error)); + + await logChannel(guild, embed); + await interaction.reply({ embeds: [embed] }); + } +} diff --git a/src/commands/moderation/Unban.ts b/src/commands/moderation/Unban.ts new file mode 100644 index 0000000..922d1f3 --- /dev/null +++ b/src/commands/moderation/Unban.ts @@ -0,0 +1,54 @@ +import { + PermissionsBitField, + SlashCommandSubcommandBuilder, + type ChatInputCommandInteraction +} from "discord.js"; +import { errorEmbed } from "../../utils/embeds/errorEmbed"; +import { errorCheck, modEmbed } from "../../utils/embeds/modEmbed"; + +export default class Unban { + data: SlashCommandSubcommandBuilder; + constructor() { + this.data = new SlashCommandSubcommandBuilder() + .setName("unban") + .setDescription("Unbans a user.") + .addStringOption(string => + string + .setName("id") + .setDescription("The ID of the user that you want to unban.") + .setRequired(true) + ) + .addStringOption(string => + string.setName("reason").setDescription("The reason for the unban.") + ); + } + + async run(interaction: ChatInputCommandInteraction) { + const id = interaction.options.getString("id")!; + const reason = interaction.options.getString("reason")!; + const guild = interaction.guild!; + const target = (await guild.bans.fetch()) + .map(ban => ban.user) + .filter(user => user.id == id)[0]!; + + if ( + await errorCheck( + PermissionsBitField.Flags.BanMembers, + { interaction, user: target, action: "Unban" }, + { allErrors: false, botError: true, ownerError: true }, + "Ban Members" + ) + ) + return; + + if (!target) + return await errorEmbed( + interaction, + "You can't unban this user.", + "The user was never banned." + ); + + await guild.members.unban(id, reason ?? undefined).catch(error => console.error(error)); + await modEmbed({ interaction, user: target, action: "Unbanned" }, reason); + } +} diff --git a/src/commands/moderation/Unlock.ts b/src/commands/moderation/Unlock.ts new file mode 100644 index 0000000..403d165 --- /dev/null +++ b/src/commands/moderation/Unlock.ts @@ -0,0 +1,80 @@ +import { + ChannelType, + EmbedBuilder, + PermissionsBitField, + SlashCommandSubcommandBuilder, + type ChatInputCommandInteraction +} from "discord.js"; +import { genColor } from "../../utils/colorGen"; +import { errorEmbed } from "../../utils/embeds/errorEmbed"; +import { logChannel } from "../../utils/logChannel"; + +export default class Unlock { + data: SlashCommandSubcommandBuilder; + constructor() { + this.data = new SlashCommandSubcommandBuilder() + .setName("unlock") + .setDescription("Unlocks a channel.") + .addChannelOption(channel => + channel + .setName("channel") + .setDescription("The channel that you want to unlock.") + .addChannelTypes( + ChannelType.GuildText, + ChannelType.PublicThread, + ChannelType.PrivateThread, + ChannelType.GuildVoice + ) + ); + } + + async run(interaction: ChatInputCommandInteraction) { + const guild = interaction.guild!; + if ( + !guild.members.cache + .get(interaction.user.id) + ?.permissions.has(PermissionsBitField.Flags.ManageRoles) + ) + return await errorEmbed( + interaction, + "You can't execute this command.", + "You need the **Manage Roles** permission." + ); + + const channelOption = interaction.options.getChannel("channel")!; + const channel = guild.channels.cache.get(interaction.channel?.id ?? channelOption.id)!; + if (channel.permissionsFor(guild.id)?.has("SendMessages")) + return await errorEmbed( + interaction, + "You can't execute this command.", + "The channel is not locked." + ); + + const embed = new EmbedBuilder() + .setAuthor({ name: `Unlocked a channel.` }) + .setDescription( + [ + `**Moderator**: ${interaction.user.displayName}`, + `**Channel**: ${channelOption ?? `<#${channel.id}>`}` + ].join("\n") + ) + .setColor(genColor(100)); + + if ( + channel.type == ChannelType.GuildText && + ChannelType.PublicThread && + ChannelType.PrivateThread && + ChannelType.GuildVoice + ) + channel.permissionOverwrites + .create(guild.id, { + SendMessages: null, + SendMessagesInThreads: null, + CreatePublicThreads: null + }) + .catch(error => console.error(error)); + + await logChannel(guild, embed); + await interaction.reply({ embeds: [embed] }); + } +} diff --git a/src/commands/moderation/Unmute.ts b/src/commands/moderation/Unmute.ts new file mode 100644 index 0000000..622e95b --- /dev/null +++ b/src/commands/moderation/Unmute.ts @@ -0,0 +1,43 @@ +import { + PermissionsBitField, + SlashCommandSubcommandBuilder, + type ChatInputCommandInteraction +} from "discord.js"; +import { errorEmbed } from "../../utils/embeds/errorEmbed"; +import { errorCheck, modEmbed } from "../../utils/embeds/modEmbed"; + +export default class Unmute { + data: SlashCommandSubcommandBuilder; + constructor() { + this.data = new SlashCommandSubcommandBuilder() + .setName("unmute") + .setDescription("Unmutes a user.") + .addUserOption(user => + user.setName("user").setDescription("The user that you want to unmute.").setRequired(true) + ); + } + + async run(interaction: ChatInputCommandInteraction) { + const user = interaction.options.getUser("user")!; + const target = interaction.guild?.members.cache.get(user.id)!; + if ( + await errorCheck( + PermissionsBitField.Flags.ModerateMembers, + { interaction, user, action: "Unmute" }, + { allErrors: false, botError: true }, + "Moderate Members" + ) + ) + return; + + if (!target.communicationDisabledUntil) + return await errorEmbed( + interaction, + "You can't unmute this user.", + "The user was never muted." + ); + + await target.edit({ communicationDisabledUntil: null }).catch(error => console.error(error)); + await modEmbed({ interaction, user, action: "Unmuted" }); + } +} diff --git a/src/commands/moderation/Warn.ts b/src/commands/moderation/Warn.ts new file mode 100644 index 0000000..a458908 --- /dev/null +++ b/src/commands/moderation/Warn.ts @@ -0,0 +1,49 @@ +import { + PermissionsBitField, + SlashCommandSubcommandBuilder, + type ChatInputCommandInteraction +} from "discord.js"; +import { errorCheck, modEmbed } from "../../utils/embeds/modEmbed"; + +export default class Warn { + data: SlashCommandSubcommandBuilder; + constructor() { + this.data = new SlashCommandSubcommandBuilder() + .setName("warn") + .setDescription("Warns a user.") + .addUserOption(user => + user.setName("user").setDescription("The user that you want to warn.").setRequired(true) + ) + .addStringOption(string => + string.setName("reason").setDescription("The reason for the warn.") + ) + .addBooleanOption(bool => + bool + .setName("show_moderator") + .setDescription( + "Inform the warned user of the moderator that took the action. Defaults to false." + ) + ); + } + + async run(interaction: ChatInputCommandInteraction) { + const user = interaction.options.getUser("user")!; + const reason = interaction.options.getString("reason"); + const showModerator = interaction.options.getBoolean("show_moderator") ?? false; + if ( + await errorCheck( + PermissionsBitField.Flags.ModerateMembers, + { interaction, user, action: "Warn" }, + { allErrors: true, botError: false, ownerError: true, outsideError: true }, + "Moderate Members" + ) + ) + return; + + await modEmbed( + { interaction, user, action: "Warned", dm: true, dbAction: "WARN" }, + reason, + showModerator + ); + } +} diff --git a/src/commands/news/Add.ts b/src/commands/news/Add.ts new file mode 100644 index 0000000..08ee723 --- /dev/null +++ b/src/commands/news/Add.ts @@ -0,0 +1,82 @@ +import { + ActionRowBuilder, + EmbedBuilder, + ModalBuilder, + PermissionsBitField, + SlashCommandSubcommandBuilder, + TextInputBuilder, + TextInputStyle, + type ChatInputCommandInteraction +} from "discord.js"; +import { genColor } from "../../utils/colorGen"; +import { addNews } from "../../utils/database/news"; +import { errorEmbed } from "../../utils/embeds/errorEmbed"; +import { sendChannelNews } from "../../utils/sendChannelNews"; + +export default class Send { + data: SlashCommandSubcommandBuilder; + constructor() { + this.data = new SlashCommandSubcommandBuilder().setName("add").setDescription("Add your news."); + } + + async run(interaction: ChatInputCommandInteraction) { + const guild = interaction.guild!; + if ( + !guild.members.cache + .get(interaction.user.id) + ?.permissions.has(PermissionsBitField.Flags.ManageGuild) + ) + return await errorEmbed( + interaction, + "You can't execute this command.", + "You need the **Manage Server** permission." + ); + + const firstActionRow = new ActionRowBuilder().addComponents( + new TextInputBuilder() + .setCustomId("title") + .setPlaceholder("Write a title") + .setMaxLength(100) + .setStyle(TextInputStyle.Short) + .setLabel("Title") + .setRequired(true) + ); + + const secondActionRow = new ActionRowBuilder().addComponents( + new TextInputBuilder() + .setCustomId("body") + .setPlaceholder("Insert your content here") + .setMaxLength(4000) + .setStyle(TextInputStyle.Paragraph) + .setLabel("Content (supports Markdown)") + .setRequired(true) + ); + + const newsModal = new ModalBuilder() + .setCustomId("addnews") + .setTitle("Write your news.") + .addComponents(firstActionRow, secondActionRow); + + await interaction.showModal(newsModal).catch(err => console.error(err)); + interaction.client.once("interactionCreate", async i => { + if (!i.isModalSubmit()) return; + + const id = crypto.randomUUID(); + addNews( + guild.id, + i.fields.getTextInputValue("title"), + i.fields.getTextInputValue("body"), + i.user.displayName, + i.user.avatarURL()!, + null!, + id + ); + + await sendChannelNews(guild, id, interaction).catch(err => console.error(err)); + await i.reply({ + embeds: [new EmbedBuilder().setTitle("News added.").setColor(genColor(100))], + ephemeral: true + }); + }); + } +} diff --git a/src/commands/news/Edit.ts b/src/commands/news/Edit.ts new file mode 100644 index 0000000..48f3a0f --- /dev/null +++ b/src/commands/news/Edit.ts @@ -0,0 +1,112 @@ +import { + ActionRowBuilder, + EmbedBuilder, + ModalBuilder, + PermissionsBitField, + SlashCommandSubcommandBuilder, + TextInputBuilder, + TextInputStyle, + type ChatInputCommandInteraction, + type Role, + type TextChannel +} from "discord.js"; +import { genColor } from "../../utils/colorGen"; +import { get, updateNews } from "../../utils/database/news"; +import { getSetting } from "../../utils/database/settings"; +import { errorEmbed } from "../../utils/embeds/errorEmbed"; +import { sendChannelNews } from "../../utils/sendChannelNews"; + +export default class Edit { + data: SlashCommandSubcommandBuilder; + constructor() { + this.data = new SlashCommandSubcommandBuilder() + .setName("edit") + .setDescription("Edits the news of your guild.") + .addStringOption(string => + string + .setName("id") + .setDescription("The ID of the news you want to edit.") + .setRequired(true) + ); + } + + async run(interaction: ChatInputCommandInteraction) { + const guild = interaction.guild!; + if ( + !guild.members.cache + .get(interaction.user.id) + ?.permissions.has(PermissionsBitField.Flags.ManageGuild) + ) + return await errorEmbed( + interaction, + "You can't execute this command.", + "You need the **Manage Server** permission." + ); + + const id = interaction.options.getString("id")!; + const news = get(id); + if (!news) return await errorEmbed(interaction, "The specified news don't exist."); + + const firstActionRow = new ActionRowBuilder().addComponents( + new TextInputBuilder() + .setCustomId("title") + .setMaxLength(100) + .setStyle(TextInputStyle.Short) + .setLabel("Title") + .setValue(news.title) + .setRequired(true) + ); + + const secondActionRow = new ActionRowBuilder().addComponents( + new TextInputBuilder() + .setCustomId("body") + .setMaxLength(4000) + .setStyle(TextInputStyle.Paragraph) + .setLabel("Content (supports Markdown)") + .setValue(news.body) + .setRequired(true) + ); + + const editModal = new ModalBuilder() + .setCustomId("editnews") + .setTitle(`Edit news: ${news.title}`) + .addComponents(firstActionRow, secondActionRow); + + await interaction.showModal(editModal).catch(err => console.error(err)); + interaction.client.once("interactionCreate", async i => { + if (!i.isModalSubmit()) return; + + const role = getSetting(guild.id, "news", "role_id") as string; + let roleToSend: Role | undefined; + if (role) roleToSend = guild.roles.cache.get(role); + const title = i.fields.getTextInputValue("title"); + const body = i.fields.getTextInputValue("body"); + + if (!getSetting(guild.id, "news", "edit_original_message")) + await sendChannelNews(guild, id, interaction, title, body); + + const embed = new EmbedBuilder() + .setAuthor({ name: `• ${news.author}`, iconURL: news.authorPFP }) + .setTitle(title) + .setDescription(body) + .setTimestamp(parseInt(news.updatedAt.toString()) ?? null) + .setFooter({ text: `Edited news from ${guild.name}\nID: ${news.id}` }) + .setColor(genColor(200)); + + ( + guild.channels.cache.get( + (getSetting(guild.id, "news", "channel_id") as string) ?? interaction.channel?.id + ) as TextChannel + )?.messages.edit(news.messageID, { + embeds: [embed], + content: roleToSend ? `<@&${roleToSend.id}>` : undefined + }); + + updateNews(id, title, body); + await interaction.reply({ + embeds: [new EmbedBuilder().setTitle("News edited.").setColor(genColor(100))], + ephemeral: true + }); + }); + } +} diff --git a/src/commands/news/Remove.ts b/src/commands/news/Remove.ts new file mode 100644 index 0000000..0cb5bee --- /dev/null +++ b/src/commands/news/Remove.ts @@ -0,0 +1,55 @@ +import { + EmbedBuilder, + PermissionsBitField, + SlashCommandSubcommandBuilder, + TextChannel, + type ChatInputCommandInteraction +} from "discord.js"; +import { genColor } from "../../utils/colorGen"; +import { deleteNews, get } from "../../utils/database/news"; +import { getSetting } from "../../utils/database/settings"; +import { errorEmbed } from "../../utils/embeds/errorEmbed"; + +export default class Remove { + data: SlashCommandSubcommandBuilder; + constructor() { + this.data = new SlashCommandSubcommandBuilder() + .setName("remove") + .setDescription("Removes news from your guild.") + .addStringOption(string => + string + .setName("id") + .setDescription("The ID of the news. (found in the footer)") + .setRequired(true) + ); + } + + async run(interaction: ChatInputCommandInteraction) { + const guild = interaction.guild!; + if ( + !guild.members.cache + .get(interaction.user.id) + ?.permissions.has(PermissionsBitField.Flags.ManageGuild) + ) + return await errorEmbed( + interaction, + "You can't execute this command.", + "You need the **Manage Server** permission." + ); + + const id = interaction.options.getString("id")!; + const news = get(id); + if (!news) return await errorEmbed(interaction, "The specified news don't exist."); + + const newsChannel = (await guild.channels + .fetch((getSetting(guild.id, "news", "channel_id") as string) ?? interaction.channel?.id) + .catch(() => null)) as TextChannel; + + if (newsChannel) await newsChannel.messages.delete(news.messageID); + deleteNews(id); + await interaction.reply({ + embeds: [new EmbedBuilder().setTitle("News removed.").setColor(genColor(100))], + ephemeral: true + }); + } +} diff --git a/src/commands/news/View.ts b/src/commands/news/View.ts new file mode 100644 index 0000000..3bbf7ce --- /dev/null +++ b/src/commands/news/View.ts @@ -0,0 +1,91 @@ +import { + ActionRowBuilder, + ButtonBuilder, + ButtonInteraction, + ButtonStyle, + EmbedBuilder, + SlashCommandSubcommandBuilder, + type ChatInputCommandInteraction +} from "discord.js"; +import { genColor } from "../../utils/colorGen"; +import { listAllNews } from "../../utils/database/news"; +import { errorEmbed } from "../../utils/embeds/errorEmbed"; + +export default class View { + data: SlashCommandSubcommandBuilder; + constructor() { + this.data = new SlashCommandSubcommandBuilder() + .setName("view") + .setDescription("View the news of this server.") + .addNumberOption(number => + number.setName("page").setDescription("The page of the news that you want to see.") + ); + } + + async run(interaction: ChatInputCommandInteraction) { + let page = interaction.options.getNumber("page") ?? 1; + const news = listAllNews(interaction.guild?.id!); + const sortedNews = (Object.values(news) as any[])?.sort((a, b) => b.createdAt - a.createdAt); + + if (!news || !sortedNews || !sortedNews.length) + return await errorEmbed( + interaction, + "No news found.", + "Admins can add news with the **/news add** command." + ); + + if (page > sortedNews.length) page = sortedNews.length; + if (page < 1) page = 1; + + function getEmbed() { + const currentNews = sortedNews[page - 1]; + return new EmbedBuilder() + .setAuthor({ name: `• ${currentNews.author}`, iconURL: currentNews.authorPFP }) + .setTitle(currentNews.title) + .setDescription(currentNews.body) + .setImage(currentNews.imageURL || null) + .setTimestamp(parseInt(currentNews.updatedAt)) + .setFooter({ text: `Page ${page} of ${sortedNews.length} • ID: ${currentNews.id}` }) + .setColor(genColor(200)); + } + + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId("left") + .setEmoji("1271045078042935398") + .setStyle(ButtonStyle.Primary), + new ButtonBuilder() + .setCustomId("right") + .setEmoji("1271045041313415370") + .setStyle(ButtonStyle.Primary) + ); + + const reply = await interaction.reply({ embeds: [getEmbed()], components: [row] }); + reply + .createMessageComponentCollector({ time: 60000 }) + .on("collect", async (i: ButtonInteraction) => { + if (i.message.id != (await reply.fetch()).id) + return await errorEmbed( + i, + "For some reason, this click would've caused the bot to error. Thankfully, this message right here prevents that." + ); + + if (i.user.id != interaction.user.id) + return await errorEmbed(i, "You aren't the person who executed this command."); + + setTimeout(async () => await i.editReply({ components: [] }), 60000); + switch (i.customId) { + case "left": + page--; + if (page < 1) page = sortedNews.length; + await i.update({ embeds: [getEmbed()], components: [row] }); + break; + case "right": + page++; + if (page > sortedNews.length) page = 1; + await i.update({ embeds: [getEmbed()], components: [row] }); + break; + } + }); + } +} From 9d82446b7e8a601cc140923b5b0c617add4d0332 Mon Sep 17 00:00:00 2001 From: MrSerge01 <86667481+MrSerge01@users.noreply.github.com> Date: Wed, 23 Oct 2024 22:52:46 +0500 Subject: [PATCH 113/127] eternal suffering, pt4 --- src/commands/About.ts | 8 ++++-- src/commands/Leaderboard.ts | 8 ++++-- src/commands/Serverboard.ts | 2 +- src/commands/Settings.ts | 1 - src/commands/User.ts | 6 ++-- src/commands/moderation/Cases.ts | 4 ++- src/commands/news/Add.ts | 6 ++-- src/commands/news/View.ts | 48 ++++++++++++++++---------------- src/events/interactionCreate.ts | 9 +++--- src/events/messageCreate.ts | 4 ++- src/utils/capitalize.ts | 9 ++++++ src/utils/database/moderation.ts | 3 +- src/utils/database/news.ts | 2 +- src/utils/database/settings.ts | 3 +- src/utils/embeds/modEmbed.ts | 8 +++--- src/utils/imageColor.ts | 5 ++-- 16 files changed, 71 insertions(+), 55 deletions(-) create mode 100644 src/utils/capitalize.ts diff --git a/src/commands/About.ts b/src/commands/About.ts index b0c50ba..d92818e 100644 --- a/src/commands/About.ts +++ b/src/commands/About.ts @@ -1,5 +1,6 @@ import { EmbedBuilder, SlashCommandBuilder, type ChatInputCommandInteraction } from "discord.js"; import { genColor } from "../utils/colorGen"; +import { imageColor } from "../utils/imageColor"; import { randomise } from "../utils/randomise"; export default class About { @@ -12,6 +13,7 @@ export default class About { async run(interaction: ChatInputCommandInteraction) { const client = interaction.client; + const user = client.user; const guilds = client.guilds.cache; const members = guilds.map(guild => guild.memberCount).reduce((a, b) => a + b); const shards = client.shard?.count; @@ -19,7 +21,7 @@ export default class About { if (Math.round(Math.random() * 100) <= 5) emojis = ["⌨️", "💻", "🖥️"]; const embed = new EmbedBuilder() - .setAuthor({ name: "• About Sokora", iconURL: client.user.displayAvatarURL() }) + .setAuthor({ name: "• About Sokora", iconURL: user.displayAvatarURL() }) .setDescription( "Sokora is a multipurpose Discord bot that lets you manage your servers easily." ) @@ -51,8 +53,8 @@ export default class About { } ) .setFooter({ text: `Made with ${randomise(emojis)} by the Sokora team` }) - .setThumbnail(client.user.displayAvatarURL()) - .setColor(genColor(270)); + .setThumbnail(user.displayAvatarURL()) + .setColor(user.hexAccentColor ?? (await imageColor(undefined, user)) ?? genColor(270)); await interaction.reply({ embeds: [embed] }); } diff --git a/src/commands/Leaderboard.ts b/src/commands/Leaderboard.ts index 274850c..640eceb 100644 --- a/src/commands/Leaderboard.ts +++ b/src/commands/Leaderboard.ts @@ -8,6 +8,7 @@ import { type ChatInputCommandInteraction, type SlashCommandOptionsOnlyBuilder } from "discord.js"; +import { genColor } from "../utils/colorGen"; import { getGuildLeaderboard } from "../utils/database/levelling"; import { errorEmbed } from "../utils/embeds/errorEmbed"; @@ -46,15 +47,16 @@ export default class Leaderboard { const pageData = leaderboardData.slice(start, end); const embed = new EmbedBuilder() - .setTitle(`Leaderboard - Page ${page}/${totalPages}`) - .setColor("#0099ff"); + .setAuthor({ name: "• Leaderboard" }) + .setColor(genColor(200)) + .setFooter({ text: `Page ${page}/${totalPages}` }); for (let i = 0; i < pageData.length; i++) { const userData = pageData[i]; const user = await interaction.client.users.fetch(userData.user); embed.addFields({ name: `${start + i + 1}. ${user.tag}`, - value: `Level: ${Math.floor(userData.level)} | EXP: ${Math.floor(userData.exp)}` + value: `**Level**: ${Math.floor(userData.level)} • **EXP**: ${Math.floor(userData.exp)}` }); } diff --git a/src/commands/Serverboard.ts b/src/commands/Serverboard.ts index cca033f..bed6b78 100644 --- a/src/commands/Serverboard.ts +++ b/src/commands/Serverboard.ts @@ -45,7 +45,7 @@ export default class Serverboard { const row = new ActionRowBuilder().addComponents( new ButtonBuilder() .setCustomId("left") - .setEmoji("1271045078042935398") + .setEmoji("1137330341472915526") .setStyle(ButtonStyle.Primary), new ButtonBuilder() .setCustomId("right") diff --git a/src/commands/Settings.ts b/src/commands/Settings.ts index 15f94da..4430711 100644 --- a/src/commands/Settings.ts +++ b/src/commands/Settings.ts @@ -5,7 +5,6 @@ import { PermissionsBitField, SlashCommandBuilder, SlashCommandSubcommandBuilder, - SlashCommandSubcommandGroupBuilder, type ChatInputCommandInteraction } from "discord.js"; import { genColor } from "../utils/colorGen"; diff --git a/src/commands/User.ts b/src/commands/User.ts index cc2bdb8..e008824 100644 --- a/src/commands/User.ts +++ b/src/commands/User.ts @@ -99,8 +99,6 @@ export default class User { components: enabled ? [row] : [] }); - console.log(enabled); - console.log(!enabled); if (!enabled && user.bot) return; const [guildLevel, guildExp] = getLevel(`${guild.id}`, `${target.id}`)!; const [globalLevel, globalExp] = getLevel("0", `${target.id}`)!; @@ -137,7 +135,7 @@ export default class User { name: `⚡ • Guild level ${guildLevel ?? 0}`, value: [ `**${guildExp.toLocaleString("en-US") ?? 0}/${nextLevelExp}** EXP`, - `**Next level**: ${(guildLevel ?? 0) + 1}` + `The next level is **${(guildLevel ?? 0) + 1}**` ].join("\n"), inline: true }, @@ -145,7 +143,7 @@ export default class User { name: `⛈️ • Global level ${globalLevel ?? 0}`, value: [ `**${globalExp.toLocaleString("en-US") ?? 0}/${globalNextLevelExp}** EXP`, - `**Next level**: ${(globalLevel ?? 0) + 1}` + `The next level is **${(globalLevel ?? 0) + 1}**` ].join("\n"), inline: true } diff --git a/src/commands/moderation/Cases.ts b/src/commands/moderation/Cases.ts index 7013bd5..021d805 100644 --- a/src/commands/moderation/Cases.ts +++ b/src/commands/moderation/Cases.ts @@ -15,7 +15,9 @@ export default class Cases { this.data = new SlashCommandSubcommandBuilder() .setName("cases") .setDescription("Moderation cases in a server.") - .addUserOption(user => user.setName("user").setDescription("The user that you want to see.")) + .addUserOption(user => + user.setName("user").setDescription("The user that you want to see.").setRequired(true) + ) .addStringOption(string => string.setName("id").setDescription("The ID of a specific moderation case you want to see.") ); diff --git a/src/commands/news/Add.ts b/src/commands/news/Add.ts index 08ee723..4420702 100644 --- a/src/commands/news/Add.ts +++ b/src/commands/news/Add.ts @@ -9,11 +9,11 @@ import { type ChatInputCommandInteraction } from "discord.js"; import { genColor } from "../../utils/colorGen"; -import { addNews } from "../../utils/database/news"; +import { addNews, listAllQuery } from "../../utils/database/news"; import { errorEmbed } from "../../utils/embeds/errorEmbed"; import { sendChannelNews } from "../../utils/sendChannelNews"; -export default class Send { +export default class Add { data: SlashCommandSubcommandBuilder; constructor() { this.data = new SlashCommandSubcommandBuilder().setName("add").setDescription("Add your news."); @@ -61,7 +61,7 @@ export default class Send { interaction.client.once("interactionCreate", async i => { if (!i.isModalSubmit()) return; - const id = crypto.randomUUID(); + const id = (listAllQuery.all(guild.id).length + 1).toString(); addNews( guild.id, i.fields.getTextInputValue("title"), diff --git a/src/commands/news/View.ts b/src/commands/news/View.ts index 3bbf7ce..be157ef 100644 --- a/src/commands/news/View.ts +++ b/src/commands/news/View.ts @@ -61,31 +61,31 @@ export default class View { ); const reply = await interaction.reply({ embeds: [getEmbed()], components: [row] }); - reply - .createMessageComponentCollector({ time: 60000 }) - .on("collect", async (i: ButtonInteraction) => { - if (i.message.id != (await reply.fetch()).id) - return await errorEmbed( - i, - "For some reason, this click would've caused the bot to error. Thankfully, this message right here prevents that." - ); + const collector = reply.createMessageComponentCollector({ time: 60000 }); + collector.on("collect", async (i: ButtonInteraction) => { + if (i.message.id != (await reply.fetch()).id) + return await errorEmbed( + i, + "For some reason, this click would've caused the bot to error. Thankfully, this message right here prevents that." + ); - if (i.user.id != interaction.user.id) - return await errorEmbed(i, "You aren't the person who executed this command."); + if (i.user.id != interaction.user.id) + return await errorEmbed(i, "You aren't the person who executed this command."); - setTimeout(async () => await i.editReply({ components: [] }), 60000); - switch (i.customId) { - case "left": - page--; - if (page < 1) page = sortedNews.length; - await i.update({ embeds: [getEmbed()], components: [row] }); - break; - case "right": - page++; - if (page > sortedNews.length) page = 1; - await i.update({ embeds: [getEmbed()], components: [row] }); - break; - } - }); + switch (i.customId) { + case "left": + page--; + if (page < 1) page = sortedNews.length; + await i.update({ embeds: [getEmbed()], components: [row] }); + break; + case "right": + page++; + if (page > sortedNews.length) page = 1; + await i.update({ embeds: [getEmbed()], components: [row] }); + break; + } + }); + + collector.on("end", async () => await interaction.editReply({ components: [] })); } } diff --git a/src/events/interactionCreate.ts b/src/events/interactionCreate.ts index 8b08931..b14ecc8 100644 --- a/src/events/interactionCreate.ts +++ b/src/events/interactionCreate.ts @@ -2,19 +2,20 @@ import { file } from "bun"; import type { AutocompleteInteraction, Client, CommandInteraction } from "discord.js"; import { join } from "path"; import { pathToFileURL } from "url"; +import { capitalize } from "../utils/capitalize"; async function getCommand( interaction: CommandInteraction | AutocompleteInteraction, options: any ): Promise { - const commandName = interaction.commandName; - const subcommandName = options.getSubcommand(false); - const commandGroupName = options.getSubcommandGroup(false); + const commandName = capitalize(interaction.commandName)!; + const subcommandName = capitalize(options.getSubcommand(false)); + const commandGroupName = capitalize(options.getSubcommandGroup(false)); let commandImportPath = join( join(process.cwd(), "src", "commands"), `${ subcommandName - ? `${commandName}/${ + ? `${commandName.toLowerCase()}/${ commandGroupName ? `${commandGroupName}/${subcommandName}` : subcommandName }` : commandName diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts index a714a59..ad223d8 100644 --- a/src/events/messageCreate.ts +++ b/src/events/messageCreate.ts @@ -5,7 +5,7 @@ import { pathToFileURL } from "url"; import { genColor } from "../utils/colorGen"; import { getLevel, setLevel } from "../utils/database/levelling"; import { get as getLevelRewards } from "../utils/database/levelRewards"; -import { getSetting, setSetting } from "../utils/database/settings"; +import { getSetting } from "../utils/database/settings"; import { kominator } from "../utils/kominator"; export default { @@ -43,6 +43,7 @@ export default { const cooldowns = new Map(); const cooldown = getSetting(guild.id, "levelling", "set_cooldown") as number; + console.log(cooldown); // const multiplier = getSetting(guild.id, "levelling", "add_multiplier"); let expGain = getSetting(guild.id, "levelling", "set_xp_gain") as number; @@ -54,6 +55,7 @@ export default { if (now - lastExpTime < cooldown * 1000) return; else cooldowns.set(key, now); } + console.log(cooldowns); // if (multiplier) { // const expMultiplier = kominator(multiplier as string); diff --git a/src/utils/capitalize.ts b/src/utils/capitalize.ts new file mode 100644 index 0000000..9c2d7cf --- /dev/null +++ b/src/utils/capitalize.ts @@ -0,0 +1,9 @@ +/** + * Outputs the string with the first letter capitalized. + * @param string String, the first letter of which should be capitalized. + */ + +export function capitalize(string: string) { + if (!string) return; + return `${string.charAt(0).toUpperCase()}${string.slice(1)}`; +} diff --git a/src/utils/database/moderation.ts b/src/utils/database/moderation.ts index 845c0dd..ce3fc90 100644 --- a/src/utils/database/moderation.ts +++ b/src/utils/database/moderation.ts @@ -20,6 +20,7 @@ const database = getDatabase(definition); const addQuery = database.query( "INSERT INTO moderation (guild, user, type, moderator, reason, id, timestamp, expiresAt) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8);" ); +const listGuildQuery = database.query("SELECT * FROM moderation WHERE guild = $1"); const listUserQuery = database.query("SELECT * FROM moderation WHERE guild = $1 AND user = $2;"); const listUserTypeQuery = database.query( "SELECT * FROM moderation WHERE guild = $1 AND user = $2 AND type = $3;" @@ -38,7 +39,7 @@ export function addModeration( reason = "", expiresAt?: number | null ) { - const id = crypto.randomUUID(); + const id = listGuildQuery.all(guildID).length + 1; addQuery.run(guildID, userID, type, moderator, reason, id, Date.now(), expiresAt ?? null); return id; } diff --git a/src/utils/database/news.ts b/src/utils/database/news.ts index de6eccc..bfff50d 100644 --- a/src/utils/database/news.ts +++ b/src/utils/database/news.ts @@ -20,7 +20,7 @@ const database = getDatabase(definition); const sendQuery = database.query( "INSERT INTO news (guildID, title, body, author, authorPFP, createdAt, updatedAt, messageID, id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9);" ); -const listAllQuery = database.query("SELECT * FROM news WHERE guildID = $1;"); +export const listAllQuery = database.query("SELECT * FROM news WHERE guildID = $1;"); const getIdQuery = database.query("SELECT * FROM news WHERE id = $1;"); const deleteQuery = database.query("DELETE FROM news WHERE id = $1"); diff --git a/src/utils/database/settings.ts b/src/utils/database/settings.ts index 8888214..95ff5cc 100644 --- a/src/utils/database/settings.ts +++ b/src/utils/database/settings.ts @@ -121,8 +121,7 @@ export function getSetting( >[]; const set = settingsDefinition[key][setting]; - if (!res.length) return null; - if (res[0].value == "") { + if (!res.length) { if (set.type == "LIST") return null; return set.val; } diff --git a/src/utils/embeds/modEmbed.ts b/src/utils/embeds/modEmbed.ts index 3979529..d673c9a 100644 --- a/src/utils/embeds/modEmbed.ts +++ b/src/utils/embeds/modEmbed.ts @@ -107,9 +107,9 @@ export async function modEmbed( const guild = interaction.guild!; const name = user.displayName; const generalValues = [`**Moderator**: ${interaction.user.displayName}`]; + let author = `• ${action} ${name}`; reason ? generalValues.push(`**Reason**: ${reason}`) : generalValues.push("*No reason provided*"); if (duration) generalValues.push(`**Duration**: ${ms(ms(duration), { long: true })}`); - const footer = [`User ID: ${user.id}`]; if (dbAction) { try { const id = addModeration( @@ -120,16 +120,16 @@ export async function modEmbed( reason ?? undefined, expiresAt ?? undefined ); - footer.push(`Case ID: ${id}`); + author = author.concat(` • #${id}`); } catch (error) { console.error(error); } } const embed = new EmbedBuilder() - .setAuthor({ name: `• ${action} ${name}`, iconURL: user.displayAvatarURL() }) + .setAuthor({ name: author, iconURL: user.displayAvatarURL() }) .setDescription(generalValues.join("\n")) - .setFooter({ text: footer.join("\n") }) + .setFooter({ text: `User ID: ${user.id}` }) .setColor(genColor(100)); await logChannel(guild, embed); diff --git a/src/utils/imageColor.ts b/src/utils/imageColor.ts index 6d37d09..d5ea6cb 100644 --- a/src/utils/imageColor.ts +++ b/src/utils/imageColor.ts @@ -1,4 +1,4 @@ -import type { ColorResolvable, Guild, GuildMember } from "discord.js"; +import type { ColorResolvable, Guild, GuildMember, User } from "discord.js"; import Vibrant from "node-vibrant"; import sharp from "sharp"; import { genRGBColor } from "./colorGen"; @@ -9,7 +9,8 @@ import { genRGBColor } from "./colorGen"; * @param member Member image. * @returns The color in HEX. */ -export async function imageColor(guild?: Guild, member?: GuildMember) { + +export async function imageColor(guild?: Guild, member?: GuildMember | User) { const guildURL = guild?.iconURL(); const memberURL = member?.displayAvatarURL(); if (!guildURL || !memberURL) return; From 83b3429641a2c0801b882e211c7f59d48872c5e1 Mon Sep 17 00:00:00 2001 From: MrSerge01 <86667481+MrSerge01@users.noreply.github.com> Date: Wed, 23 Oct 2024 23:09:55 +0500 Subject: [PATCH 114/127] small emoji update --- src/commands/Leaderboard.ts | 16 +++++++++++----- src/commands/Serverboard.ts | 4 ++-- src/commands/news/View.ts | 4 ++-- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/commands/Leaderboard.ts b/src/commands/Leaderboard.ts index 640eceb..fd7b3f8 100644 --- a/src/commands/Leaderboard.ts +++ b/src/commands/Leaderboard.ts @@ -47,7 +47,7 @@ export default class Leaderboard { const pageData = leaderboardData.slice(start, end); const embed = new EmbedBuilder() - .setAuthor({ name: "• Leaderboard" }) + .setAuthor({ name: "Leaderboard" }) .setColor(genColor(200)) .setFooter({ text: `Page ${page}/${totalPages}` }); @@ -55,8 +55,8 @@ export default class Leaderboard { const userData = pageData[i]; const user = await interaction.client.users.fetch(userData.user); embed.addFields({ - name: `${start + i + 1}. ${user.tag}`, - value: `**Level**: ${Math.floor(userData.level)} • **EXP**: ${Math.floor(userData.exp)}` + name: `#${start + i + 1} • ${user.tag}`, + value: `Level **${Math.floor(userData.level)}** • **${Math.floor(userData.exp)}** EXP` }); } @@ -64,8 +64,14 @@ export default class Leaderboard { }; const row = new ActionRowBuilder().addComponents( - new ButtonBuilder().setCustomId("left").setEmoji("⬅️").setStyle(ButtonStyle.Primary), - new ButtonBuilder().setCustomId("right").setEmoji("➡️").setStyle(ButtonStyle.Primary) + new ButtonBuilder() + .setCustomId("left") + .setEmoji("1298708251256291379") + .setStyle(ButtonStyle.Primary), + new ButtonBuilder() + .setCustomId("right") + .setEmoji("1298708281493160029") + .setStyle(ButtonStyle.Primary) ); const reply = await interaction.reply({ diff --git a/src/commands/Serverboard.ts b/src/commands/Serverboard.ts index bed6b78..cd97cbe 100644 --- a/src/commands/Serverboard.ts +++ b/src/commands/Serverboard.ts @@ -45,11 +45,11 @@ export default class Serverboard { const row = new ActionRowBuilder().addComponents( new ButtonBuilder() .setCustomId("left") - .setEmoji("1137330341472915526") + .setEmoji("1298708251256291379") .setStyle(ButtonStyle.Primary), new ButtonBuilder() .setCustomId("right") - .setEmoji("1271045041313415370") + .setEmoji("1298708281493160029") .setStyle(ButtonStyle.Primary) ); diff --git a/src/commands/news/View.ts b/src/commands/news/View.ts index be157ef..7c42609 100644 --- a/src/commands/news/View.ts +++ b/src/commands/news/View.ts @@ -52,11 +52,11 @@ export default class View { const row = new ActionRowBuilder().addComponents( new ButtonBuilder() .setCustomId("left") - .setEmoji("1271045078042935398") + .setEmoji("1298708251256291379") .setStyle(ButtonStyle.Primary), new ButtonBuilder() .setCustomId("right") - .setEmoji("1271045041313415370") + .setEmoji("1298708281493160029") .setStyle(ButtonStyle.Primary) ); From 77e2ad98fbaedb05227c62f1d578307281baa67c Mon Sep 17 00:00:00 2001 From: MrSerge01 <86667481+MrSerge01@users.noreply.github.com> Date: Thu, 31 Oct 2024 22:25:05 +0500 Subject: [PATCH 115/127] happy halloween (eternal suffering pt5) [fox happy now?] --- bun.lockb | Bin 52960 -> 52960 bytes package.json | 2 +- src/commands/About.ts | 28 ++- src/commands/Leaderboard.ts | 4 +- src/commands/User.ts | 44 ++-- src/commands/moderation/Ban.ts | 2 +- .../moderation/{Purge.ts => Clear.ts} | 0 src/events/easterEggs/AmericaYa.ts | 14 -- src/events/easterEggs/Bread.ts | 19 -- src/events/easterEggs/Crazy.ts | 17 -- src/events/easterEggs/Fan.ts | 15 -- src/events/easterEggs/Fire.ts | 16 -- src/events/easterEggs/Fireship.ts | 15 -- src/events/easterEggs/Honk.ts | 9 - src/events/easterEggs/WhoPinged.ts | 20 -- src/events/easterEggs/amerikaYa.ts | 12 + src/events/easterEggs/bread.ts | 10 + src/events/easterEggs/crazy.ts | 8 + src/events/easterEggs/fan.ts | 13 + src/events/easterEggs/fire.ts | 14 ++ src/events/easterEggs/fireship.ts | 13 + src/events/easterEggs/honk.ts | 7 + src/events/easterEggs/whoPinged.ts | 18 ++ src/events/guildCreate.ts | 64 +++-- src/events/guildMemberAdd.ts | 83 +++---- src/events/guildMemberRemove.ts | 55 ++--- src/events/interactionCreate.ts | 39 ++- src/events/messageCreate.ts | 223 ++++++++---------- src/events/messageDelete.ts | 42 ++-- src/events/messageUpdate.ts | 60 +++-- src/handlers/events.ts | 10 +- src/utils/database/levelling.ts | 10 +- src/utils/database/settings.ts | 20 +- src/utils/embeds/errorEmbed.ts | 2 +- src/utils/embeds/modEmbed.ts | 6 +- src/utils/replace.ts | 10 + src/utils/types.ts | 3 + src/utils/unbanScheduler.ts | 46 ++-- 38 files changed, 437 insertions(+), 536 deletions(-) rename src/commands/moderation/{Purge.ts => Clear.ts} (100%) delete mode 100644 src/events/easterEggs/AmericaYa.ts delete mode 100644 src/events/easterEggs/Bread.ts delete mode 100644 src/events/easterEggs/Crazy.ts delete mode 100644 src/events/easterEggs/Fan.ts delete mode 100644 src/events/easterEggs/Fire.ts delete mode 100644 src/events/easterEggs/Fireship.ts delete mode 100644 src/events/easterEggs/Honk.ts delete mode 100644 src/events/easterEggs/WhoPinged.ts create mode 100644 src/events/easterEggs/amerikaYa.ts create mode 100644 src/events/easterEggs/bread.ts create mode 100644 src/events/easterEggs/crazy.ts create mode 100644 src/events/easterEggs/fan.ts create mode 100644 src/events/easterEggs/fire.ts create mode 100644 src/events/easterEggs/fireship.ts create mode 100644 src/events/easterEggs/honk.ts create mode 100644 src/events/easterEggs/whoPinged.ts create mode 100644 src/utils/replace.ts create mode 100644 src/utils/types.ts diff --git a/bun.lockb b/bun.lockb index f15464864da4bb236397ddcf6cabd9d4e97ede47..e1a9baa951adfffd999bf57987cf34893a78b141 100644 GIT binary patch delta 187 zcmV;s07U=bode*V1CTBtVB`Ce!3gwhZL{x>Lqz$!#9zLu=5Z2r{rjTQ=u}=Cb z0U?ty2`95~D+M@ANaHQscZlDCGG2fzGaEUnkDl?!&^mu_)D$KCl`_;D4q=OuJQR^s z76jxt+PbDSY~U6cP$O3FM)s7Z3GG`~0RR920R9jD0RRArlVLVjvlwI{EgLg401^NI p015yAfS)CREGva&Vh90VH7+(TGqY5YfjpDJ!6B0{&IXhG&E$V`OAi15 delta 187 zcmV;s07U=bode*V1CTBtx?`2Mo8;RTq`4B8RCX2!U%!G?_@jgSFp~`9kjk@3u}=Cb z0Unbv2`95~D+M@Am}q-6xU4yQBn3{k4-(S*`S6&zLSS|PkSyJ!fZ6xw^RAQl)({;! zrj6h;y0r)1oObx7hV=f(aNF59|Dra@0RR920R9jD0RRArlVLVjvlwI{EgLd901^NI p015yAfS)CREGva&Vh90VH7+(TGP6{XfjpD(tRRyw&IgnH&Ey=vQrG|h diff --git a/package.json b/package.json index a71330d..688566f 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ }, "devDependencies": { "@types/ms": "^0.7.34", - "bun-types": "^1.1.30", + "bun-types": "^1.1.33", "typescript": "^5.6.3" } } diff --git a/src/commands/About.ts b/src/commands/About.ts index d92818e..207cbfa 100644 --- a/src/commands/About.ts +++ b/src/commands/About.ts @@ -1,4 +1,11 @@ -import { EmbedBuilder, SlashCommandBuilder, type ChatInputCommandInteraction } from "discord.js"; +import { + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + EmbedBuilder, + SlashCommandBuilder, + type ChatInputCommandInteraction +} from "discord.js"; import { genColor } from "../utils/colorGen"; import { imageColor } from "../utils/imageColor"; import { randomise } from "../utils/randomise"; @@ -29,7 +36,7 @@ export default class About { { name: "📃 • General", value: [ - "**Version** 0.1-preview, *Kaishi*", + "Version **0.1**, *Kaishi*", `**${members}** members • **${guilds.size}** guild${guilds.size == 1 ? "" : "s"} ${ !shards ? "" : `• **${shards}** shard${shards == 1 ? "" : "s"}` }` @@ -39,10 +46,11 @@ export default class About { name: "🌌 • Entities involved", value: [ "**Founder**: Goos", + "**Translator Lead**: ThatBOI", "**Developers**: Dimkauzh, Froxcey, Golem64, Koslz, MQuery, Nikkerudon, Spectrum, ThatBOI", - "**Designers**: ArtyH, Optix, proJM", - "**Translators**: Dimkauzh, flojo, Golem64, Optix, SaFire, ThatBOI", - "**Testers**: Blaze, fishy, flojo, Trynera", + "**Designers**: ArtyH, Optix, Pjanda", + "**Translators**: Dimkauzh, flojo, Golem64, GraczNet, Nikkerudon, Optix, SaFire, TrulyBlue", + "**Testers**: Blaze, fishy, Trynera", "And **YOU**, for using Sokora." ].join("\n") }, @@ -56,6 +64,14 @@ export default class About { .setThumbnail(user.displayAvatarURL()) .setColor(user.hexAccentColor ?? (await imageColor(undefined, user)) ?? genColor(270)); - await interaction.reply({ embeds: [embed] }); + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setLabel("• Donate") + .setURL("https://paypal.me/SokoraTheBot") + .setEmoji("⭐") + .setStyle(ButtonStyle.Link) + ); + + await interaction.reply({ embeds: [embed], components: [row] }); } } diff --git a/src/commands/Leaderboard.ts b/src/commands/Leaderboard.ts index fd7b3f8..4073c08 100644 --- a/src/commands/Leaderboard.ts +++ b/src/commands/Leaderboard.ts @@ -35,7 +35,7 @@ export default class Leaderboard { leaderboardData.sort((a, b) => { if (b.level != a.level) return b.level - a.level; - else return b.exp - a.exp; + else return b.xp - a.xp; }); const totalPages = Math.ceil(leaderboardData.length / 5); @@ -56,7 +56,7 @@ export default class Leaderboard { const user = await interaction.client.users.fetch(userData.user); embed.addFields({ name: `#${start + i + 1} • ${user.tag}`, - value: `Level **${Math.floor(userData.level)}** • **${Math.floor(userData.exp)}** EXP` + value: `Level **${Math.floor(userData.level)}** • **${Math.floor(userData.xp)}** XP` }); } diff --git a/src/commands/User.ts b/src/commands/User.ts index e008824..673f83d 100644 --- a/src/commands/User.ts +++ b/src/commands/User.ts @@ -9,7 +9,7 @@ import { type SlashCommandOptionsOnlyBuilder } from "discord.js"; import { genColor } from "../utils/colorGen"; -import { getLevel, setLevel } from "../utils/database/levelling"; +import { getLevel } from "../utils/database/levelling"; import { getSetting } from "../utils/database/settings"; import { errorEmbed } from "../utils/embeds/errorEmbed"; import { imageColor } from "../utils/imageColor"; @@ -96,20 +96,17 @@ export default class User { row.components[0].setDisabled(true); const reply = await interaction.reply({ embeds: [embed], - components: enabled ? [row] : [] + components: !user.bot ? (enabled ? [row] : []) : [] }); if (!enabled && user.bot) return; - const [guildLevel, guildExp] = getLevel(`${guild.id}`, `${target.id}`)!; - const [globalLevel, globalExp] = getLevel("0", `${target.id}`)!; - if (!guildLevel && !guildExp) setLevel(`${guild.id}`, `${target.id}`, 0, 0); - if (!globalLevel && !globalExp) setLevel("0", `${target.id}`, 0, 0); - - const nextLevelExp = Math.floor(100 * 1.15 * ((guildLevel ?? 0) + 1))?.toLocaleString("en-US"); - const globalNextLevelExp = Math.floor(100 * 1.15 * ((globalLevel ?? 0) + 1))?.toLocaleString( - "en-US" - ); + const difficulty = getSetting(guild.id, "levelling", "difficulty") as number; + const [level, xp] = getLevel(guild.id, target.id)!; + const nextLevelXp = Math.floor( + 100 * difficulty * (level + 1) + 100 * difficulty * level + )?.toLocaleString("en-US"); + console.log(xp); const collector = reply.createMessageComponentCollector({ time: 60000 }); collector.on("collect", async (i: ButtonInteraction) => { if (i.message.id != (await reply.fetch()).id) @@ -130,24 +127,13 @@ export default class User { name: `• ${target.nickname ?? user.displayName}`, iconURL: target.displayAvatarURL() }) - .setFields( - { - name: `⚡ • Guild level ${guildLevel ?? 0}`, - value: [ - `**${guildExp.toLocaleString("en-US") ?? 0}/${nextLevelExp}** EXP`, - `The next level is **${(guildLevel ?? 0) + 1}**` - ].join("\n"), - inline: true - }, - { - name: `⛈️ • Global level ${globalLevel ?? 0}`, - value: [ - `**${globalExp.toLocaleString("en-US") ?? 0}/${globalNextLevelExp}** EXP`, - `The next level is **${(globalLevel ?? 0) + 1}**` - ].join("\n"), - inline: true - } - ) + .setFields({ + name: `⚡ • Level ${level}`, + value: [ + `**${xp.toLocaleString("en-US")}/${nextLevelXp}** XP`, + `The next level is **${level + 1}**` + ].join("\n") + }) .setFooter({ text: `User ID: ${target.id}` }) .setThumbnail(target.displayAvatarURL()) .setColor(embedColor); diff --git a/src/commands/moderation/Ban.ts b/src/commands/moderation/Ban.ts index f328a3e..7ff47a7 100644 --- a/src/commands/moderation/Ban.ts +++ b/src/commands/moderation/Ban.ts @@ -49,7 +49,7 @@ export default class Ban { ); expiresAt = Date.now() + durationMs; - scheduleUnban(interaction.client, guild.id, user.id, durationMs); + scheduleUnban(interaction.client, guild.id, user.id, interaction.member!.user.id, durationMs); } try { diff --git a/src/commands/moderation/Purge.ts b/src/commands/moderation/Clear.ts similarity index 100% rename from src/commands/moderation/Purge.ts rename to src/commands/moderation/Clear.ts diff --git a/src/events/easterEggs/AmericaYa.ts b/src/events/easterEggs/AmericaYa.ts deleted file mode 100644 index 0c6ad19..0000000 --- a/src/events/easterEggs/AmericaYa.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { Message, TextChannel } from "discord.js"; -import { randomise } from "../../utils/randomise"; - -export default class AmericaYa { - async run(message: Message) { - if (!message.content.toLowerCase().includes("america ya")) return; - const response = randomise([ - "HALLO :D HALLO :D HALLO :D HALLO :D", - "https://tenor.com/view/america-ya-gif-15374592095658975433" - ]); - - await (message.channel as TextChannel).send(response); - } -} diff --git a/src/events/easterEggs/Bread.ts b/src/events/easterEggs/Bread.ts deleted file mode 100644 index c63307e..0000000 --- a/src/events/easterEggs/Bread.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { Message, TextChannel } from "discord.js"; -import { multiReact } from "../../utils/multiReact"; - -export default class Bread { - async run(message: Message) { - const breadSplit = message.content.toLowerCase().split("bread"); - - if (!breadSplit[1]) return; - if ( - ((breadSplit[0].endsWith(" ") || breadSplit[0].endsWith("")) && - breadSplit[1].startsWith(" ")) || - message.content.toLowerCase() == "bread" - ) { - if (Math.round(Math.random() * 100) <= 0.25) - (message.channel as TextChannel).send("https://tenor.com/bOMAb.gif"); - else await multiReact(message, "🍞🇧🇷🇪🇦🇩👍"); - } - } -} diff --git a/src/events/easterEggs/Crazy.ts b/src/events/easterEggs/Crazy.ts deleted file mode 100644 index 1f3804b..0000000 --- a/src/events/easterEggs/Crazy.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { Message, TextChannel } from "discord.js"; - -export default class Crazy { - async run(message: Message) { - const crazy = message.content.toLowerCase().split("crazy"); - - if (!crazy[1]) return; - if ( - ((crazy[0].endsWith(" ") || crazy[0].endsWith("")) && crazy[1].startsWith(" ")) || - message.content.toLowerCase() == "crazy" - ) { - await (message.channel as TextChannel).send( - "Crazy? I was crazy once.\nThey locked me in a room.\nA rubber room.\nA rubber room with rats.\nAnd rats make me crazy.\nCrazy? I was crazy once..." - ); - } - } -} diff --git a/src/events/easterEggs/Fan.ts b/src/events/easterEggs/Fan.ts deleted file mode 100644 index 868a962..0000000 --- a/src/events/easterEggs/Fan.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { Message, TextChannel } from "discord.js"; -import { randomise } from "../../utils/randomise"; - -export default class Fan { - async run(message: Message) { - if (!message.content.toLowerCase().includes("i'm a big fan")) return; - const gifs = randomise([ - "https://tenor.com/bC37i.gif", - "https://tenor.com/view/fan-gif-20757784", - "https://tenor.com/view/below-deck-im-your-biggest-fan-biggest-fan-kate-kate-chastain-gif-15861715" - ]); - - await (message.channel as TextChannel).send(gifs); - } -} diff --git a/src/events/easterEggs/Fire.ts b/src/events/easterEggs/Fire.ts deleted file mode 100644 index 5288a3f..0000000 --- a/src/events/easterEggs/Fire.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { Message, TextChannel } from "discord.js"; -import { randomise } from "../../utils/randomise"; - -export default class Fire { - async run(message: Message) { - if (!message.content.toLowerCase().includes("fire in the hole")) return; - const gifs = randomise([ - "https://cdn.discordapp.com/attachments/799130520846991370/1080439680199315487/cat-chomp-fireball.gif?ex=65e8485d&is=65d5d35d&hm=cef8b83df8120f1419082f184d835c6af679c9d02d69f97e335eafa82b33489e&", - "https://tenor.com/view/dancing-gif-25178472", - "https://tenor.com/view/fire-in-the-hole-gif-11283103876805231056", - "https://tenor.com/view/fire-in-the-hole-gif-14799466830322850291" - ]); - - await (message.channel as TextChannel).send(gifs); - } -} diff --git a/src/events/easterEggs/Fireship.ts b/src/events/easterEggs/Fireship.ts deleted file mode 100644 index 034588e..0000000 --- a/src/events/easterEggs/Fireship.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { Message, TextChannel } from "discord.js"; - -export default class FireShip { - async run(message: Message) { - const content = message.content.toLowerCase(); - if ( - content.startsWith("this has been") && - content.endsWith("in 100 seconds") && - message.content != "this has been in 100 seconds" - ) - await (message.channel as TextChannel).send( - "hit the like button and subscribe if you want to see more short videos like this thanks for watching and I will see you in the next one" - ); - } -} diff --git a/src/events/easterEggs/Honk.ts b/src/events/easterEggs/Honk.ts deleted file mode 100644 index 89ee767..0000000 --- a/src/events/easterEggs/Honk.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { Message, TextChannel } from "discord.js"; - -export default class Honk { - async run(message: Message) { - const honks = ["hnok", "hokn", "hkon", "onk", "hon", "honhk", "hhonk", "honkk"]; - if (!honks.includes(message.content.toLowerCase())) return; - (message.channel as TextChannel).send("https://tenor.com/bW8sm.gif"); - } -} diff --git a/src/events/easterEggs/WhoPinged.ts b/src/events/easterEggs/WhoPinged.ts deleted file mode 100644 index 021ffac..0000000 --- a/src/events/easterEggs/WhoPinged.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { Message, TextChannel } from "discord.js"; -import { randomise } from "../../utils/randomise"; - -export default class WhoPinged { - async run(message: Message) { - if (!message.content.toLowerCase().includes(`<@${message.client.user.id}>`)) return; - const gifs = randomise([ - "https://tenor.com/view/who-pinged-me-ping-discord-up-opening-door-gif-20065356", - "https://tenor.com/view/discord-who-pinged-me-who-pinged-me-gif-25140226", - "https://tenor.com/view/who-pinged-me-ping-discord-discord-ping-undertaker-gif-20399650", - "https://tenor.com/view/who-pinged-me-ping-tudou-mr-potato-cat-gif-22762448", - "https://tenor.com/view/me-when-someone-pings-me-sad-cursed-emoji-crying-gif-22784322", - "https://tenor.com/view/discord-triggered-notification-angry-dog-noises-dog-girl-gif-11710406", - "https://tenor.com/view/tense-table-smash-mad-gif-13656077", - "https://tenor.com/view/yakuza-kazuma-kiryu-pissed-off-gif-14586175" - ]); - - await (message.channel as TextChannel).send(gifs); - } -} diff --git a/src/events/easterEggs/amerikaYa.ts b/src/events/easterEggs/amerikaYa.ts new file mode 100644 index 0000000..ceffcd2 --- /dev/null +++ b/src/events/easterEggs/amerikaYa.ts @@ -0,0 +1,12 @@ +import type { Message, TextChannel } from "discord.js"; +import { randomise } from "../../utils/randomise"; + +export default async function run(message: Message) { + if (!message.content.toLowerCase().includes("amerika ya")) return; + const response = randomise([ + "HALLO :D HALLO :D HALLO :D HALLO :D", + "https://tenor.com/view/america-ya-gif-15374592095658975433" + ]); + + await (message.channel as TextChannel).send(response); +} diff --git a/src/events/easterEggs/bread.ts b/src/events/easterEggs/bread.ts new file mode 100644 index 0000000..d7fd9a6 --- /dev/null +++ b/src/events/easterEggs/bread.ts @@ -0,0 +1,10 @@ +import type { Message, TextChannel } from "discord.js"; +import { multiReact } from "../../utils/multiReact"; + +export default async function run(message: Message) { + if (!message.content.toLowerCase().includes("bread")) return; + + if (Math.round(Math.random() * 100) <= 0.25) + (message.channel as TextChannel).send("https://tenor.com/bOMAb.gif"); + else await multiReact(message, "🍞🇧🇷🇪🇦🇩👍"); +} diff --git a/src/events/easterEggs/crazy.ts b/src/events/easterEggs/crazy.ts new file mode 100644 index 0000000..278a652 --- /dev/null +++ b/src/events/easterEggs/crazy.ts @@ -0,0 +1,8 @@ +import type { Message, TextChannel } from "discord.js"; + +export default async function run(message: Message) { + if (!message.content.toLowerCase().includes("crazy")) return; + await (message.channel as TextChannel).send( + "Crazy? I was crazy once.\nThey locked me in a room.\nA rubber room.\nA rubber room with rats.\nAnd rats make me crazy.\nCrazy? I was crazy once..." + ); +} diff --git a/src/events/easterEggs/fan.ts b/src/events/easterEggs/fan.ts new file mode 100644 index 0000000..24a5d95 --- /dev/null +++ b/src/events/easterEggs/fan.ts @@ -0,0 +1,13 @@ +import type { Message, TextChannel } from "discord.js"; +import { randomise } from "../../utils/randomise"; + +export default async function run(message: Message) { + if (!message.content.toLowerCase().includes("i'm a big fan")) return; + const gifs = randomise([ + "https://tenor.com/bC37i.gif", + "https://tenor.com/view/fan-gif-20757784", + "https://tenor.com/view/below-deck-im-your-biggest-fan-biggest-fan-kate-kate-chastain-gif-15861715" + ]); + + await (message.channel as TextChannel).send(gifs); +} diff --git a/src/events/easterEggs/fire.ts b/src/events/easterEggs/fire.ts new file mode 100644 index 0000000..0918c0b --- /dev/null +++ b/src/events/easterEggs/fire.ts @@ -0,0 +1,14 @@ +import type { Message, TextChannel } from "discord.js"; +import { randomise } from "../../utils/randomise"; + +export default async function run(message: Message) { + if (!message.content.toLowerCase().includes("fire in the hole")) return; + const gifs = randomise([ + "https://cdn.discordapp.com/attachments/799130520846991370/1080439680199315487/cat-chomp-fireball.gif?ex=65e8485d&is=65d5d35d&hm=cef8b83df8120f1419082f184d835c6af679c9d02d69f97e335eafa82b33489e&", + "https://tenor.com/view/dancing-gif-25178472", + "https://tenor.com/view/fire-in-the-hole-gif-11283103876805231056", + "https://tenor.com/view/fire-in-the-hole-gif-14799466830322850291" + ]); + + await (message.channel as TextChannel).send(gifs); +} diff --git a/src/events/easterEggs/fireship.ts b/src/events/easterEggs/fireship.ts new file mode 100644 index 0000000..9124f99 --- /dev/null +++ b/src/events/easterEggs/fireship.ts @@ -0,0 +1,13 @@ +import type { Message, TextChannel } from "discord.js"; + +export default async function run(message: Message) { + const content = message.content.toLowerCase(); + if ( + content.startsWith("this has been") && + content.endsWith("in 100 seconds") && + message.content != "this has been in 100 seconds" + ) + await (message.channel as TextChannel).send( + "hit the like button and subscribe if you want to see more short videos like this thanks for watching and I will see you in the next one" + ); +} diff --git a/src/events/easterEggs/honk.ts b/src/events/easterEggs/honk.ts new file mode 100644 index 0000000..9143205 --- /dev/null +++ b/src/events/easterEggs/honk.ts @@ -0,0 +1,7 @@ +import type { Message, TextChannel } from "discord.js"; + +export default async function run(message: Message) { + const honks = ["hnok", "hokn", "hkon", "onk", "hon", "honhk", "hhonk", "honkk"]; + if (!honks.includes(message.content.toLowerCase())) return; + (message.channel as TextChannel).send("https://tenor.com/bW8sm.gif"); +} diff --git a/src/events/easterEggs/whoPinged.ts b/src/events/easterEggs/whoPinged.ts new file mode 100644 index 0000000..779e5bd --- /dev/null +++ b/src/events/easterEggs/whoPinged.ts @@ -0,0 +1,18 @@ +import type { Message, TextChannel } from "discord.js"; +import { randomise } from "../../utils/randomise"; + +export default async function run(message: Message) { + if (!message.content.toLowerCase().includes(`<@${message.client.user.id}>`)) return; + const gifs = randomise([ + "https://tenor.com/view/who-pinged-me-ping-discord-up-opening-door-gif-20065356", + "https://tenor.com/view/discord-who-pinged-me-who-pinged-me-gif-25140226", + "https://tenor.com/view/who-pinged-me-ping-discord-discord-ping-undertaker-gif-20399650", + "https://tenor.com/view/who-pinged-me-ping-tudou-mr-potato-cat-gif-22762448", + "https://tenor.com/view/me-when-someone-pings-me-sad-cursed-emoji-crying-gif-22784322", + "https://tenor.com/view/discord-triggered-notification-angry-dog-noises-dog-girl-gif-11710406", + "https://tenor.com/view/tense-table-smash-mad-gif-13656077", + "https://tenor.com/view/yakuza-kazuma-kiryu-pissed-off-gif-14586175" + ]); + + await (message.channel as TextChannel).send(gifs); +} diff --git a/src/events/guildCreate.ts b/src/events/guildCreate.ts index bec928c..dd2242d 100644 --- a/src/events/guildCreate.ts +++ b/src/events/guildCreate.ts @@ -1,44 +1,34 @@ -import { EmbedBuilder, Guild, type Client, type DMChannel } from "discord.js"; +import { EmbedBuilder, type DMChannel } from "discord.js"; import { Commands } from "../handlers/commands"; import { genColor } from "../utils/colorGen"; import { randomise } from "../utils/randomise"; +import { Event } from "../utils/types"; -export default { - name: "guildCreate", - event: class GuildCreate { - client: Client; - constructor(client: Client) { - this.client = client; - } +export default (async function run(guild) { + const dmChannel = (await (await guild.fetchOwner()).createDM().catch(() => null)) as + | DMChannel + | undefined; - async run(guild: Guild) { - const dmChannel = (await (await guild.fetchOwner()).createDM().catch(() => null)) as - | DMChannel - | undefined; + let emojis = ["💖", "💝", "💓", "💗", "💘", "💟", "💕", "💞"]; + if (Math.round(Math.random() * 100) <= 5) emojis = ["⌨️", "💻", "🖥️"]; - let emojis = ["💖", "💝", "💓", "💗", "💘", "💟", "💕", "💞"]; - if (Math.round(Math.random() * 100) <= 5) emojis = ["⌨️", "💻", "🖥️"]; + const client = guild.client; + const embed = new EmbedBuilder() + .setAuthor({ + name: `Welcome to ${client.user.username}!`, + iconURL: client.user.displayAvatarURL() + }) + .setDescription( + [ + "Sokora is a multipurpose Discord bot that lets you manage your servers easily.", + "To manage the bot, use the **/settings** command.\n", + "Sokora is in an early stage of development. If you find bugs, please go to our [official server](https://discord.gg/c6C25P4BuY) and report them." + ].join("\n") + ) + .setFooter({ text: `Made with ${randomise(emojis)} by the Sokora team` }) + .setThumbnail(client.user.displayAvatarURL()) + .setColor(genColor(200)); - const client = guild.client; - const embed = new EmbedBuilder() - .setAuthor({ - name: client.user.username, - iconURL: client.user.displayAvatarURL() - }) - .setTitle("Welcome!") - .setDescription( - [ - "Sokora is a multipurpose Discord bot that lets you manage your servers easily.", - "To manage the bot, use the **/settings** command.\n", - "Sokora is in an early stage of development. If you find bugs, please go to our [official server](https://discord.gg/c6C25P4BuY) and report them." - ].join("\n") - ) - .setFooter({ text: `Made with ${randomise(emojis)} by the Sokora team` }) - .setThumbnail(client.user.displayAvatarURL()) - .setColor(genColor(200)); - - await new Commands(client).registerCommandsForGuild(guild); - if (dmChannel) await dmChannel.send({ embeds: [embed] }); - } - } -}; + await new Commands(client).registerCommandsForGuild(guild); + if (dmChannel) await dmChannel.send({ embeds: [embed] }); +} as Event<"guildCreate">); diff --git a/src/events/guildMemberAdd.ts b/src/events/guildMemberAdd.ts index 2d8d670..98ce111 100644 --- a/src/events/guildMemberAdd.ts +++ b/src/events/guildMemberAdd.ts @@ -1,62 +1,35 @@ -import { EmbedBuilder, type Client, type GuildMember, type TextChannel } from "discord.js"; +import { EmbedBuilder, type TextChannel } from "discord.js"; import { genColor } from "../utils/colorGen"; import { getSetting } from "../utils/database/settings"; import { imageColor } from "../utils/imageColor"; +import { replace } from "../utils/replace"; +import { Event } from "../utils/types"; -export default { - name: "guildMemberAdd", - event: class GuildMemberAdd { - client: Client; - constructor(client: Client) { - this.client = client; - } +export default (async function run(member) { + const guildID = member.guild.id; + const id = getSetting(guildID, "welcome", "channel") as string; + const user = member.user; + const avatarURL = member.displayAvatarURL(); + const embed = new EmbedBuilder() + .setAuthor({ name: `• ${user.displayName} has joined`, iconURL: avatarURL }) + .setFooter({ text: `User ID: ${member.id}` }) + .setThumbnail(avatarURL) + .setColor(member.user.hexAccentColor ?? (await imageColor(undefined, member)) ?? genColor(200)); - async run(member: GuildMember) { - const guildID = member.guild.id; - const id = getSetting(guildID, "welcome", "channel") as string; - let text = getSetting(guildID, "welcome", "join_text") as string; - const user = member.user; - const guild = member.guild; - const avatarURL = member.displayAvatarURL(); - const embed = new EmbedBuilder() - .setAuthor({ name: `• ${user.displayName}`, iconURL: avatarURL }) - .setTitle("Welcome!") - .setFooter({ text: `User ID: ${member.id}` }) - .setThumbnail(avatarURL) - .setColor( - member.user.hexAccentColor ?? (await imageColor(undefined, member)) ?? genColor(200) - ); - - if (id) { - const channel = (await member.guild.channels.cache - .find(channel => channel.id == id) - ?.fetch()) as TextChannel; - - if (text?.includes("(name)")) text = text.replaceAll("(name)", user.displayName); - if (text?.includes("(count)")) text = text.replaceAll("(count)", `${guild.memberCount}`); - if (text?.includes("(servername)")) text = text.replaceAll("(servername)", `${guild.name}`); - - embed.setDescription(text) + if (id) { + const channel = (await member.guild.channels.cache + .find(channel => channel.id == id) + ?.fetch()) as TextChannel; - await channel.send({ embeds: [embed] }); - } - - const dmwelcome = getSetting(guildID, "welcome", "join_dm") as boolean; - if (!dmwelcome) return; - // use join_text, the embed should be already cooked - const dmChannel = await user.createDM().catch(() => null); - if (!dmChannel) return; - if (user.bot) return; - let dmtext = getSetting(guildID, "welcome", "dm_text") as string; - if (dmtext) { - if (dmtext?.includes("(name)")) dmtext = dmtext.replaceAll("(name)", user.displayName); - if (dmtext?.includes("(count)")) dmtext = dmtext.replaceAll("(count)", `${guild.memberCount}`); - if (dmtext?.includes("(servername)")) dmtext = dmtext.replaceAll("(servername)", `${guild.name}`); - embed.setDescription(dmtext); - } - await dmChannel - .send({ embeds: [embed] }) - .catch(() => null); - } + replace(member, getSetting(guildID, "welcome", "join_text") as string, embed); + await channel.send({ embeds: [embed] }); } -}; + + if (!getSetting(guildID, "welcome", "join_dm") as boolean) return; + const dmChannel = await user.createDM().catch(() => null); + if (!dmChannel) return; + if (user.bot) return; + + replace(member, getSetting(guildID, "welcome", "dm_text") as string, embed); + await dmChannel.send({ embeds: [embed] }).catch(() => null); +} as Event<"guildMemberAdd">); diff --git a/src/events/guildMemberRemove.ts b/src/events/guildMemberRemove.ts index 047b1df..4ca59f3 100644 --- a/src/events/guildMemberRemove.ts +++ b/src/events/guildMemberRemove.ts @@ -1,43 +1,26 @@ -import { EmbedBuilder, type Client, type GuildMember, type TextChannel } from "discord.js"; +import { EmbedBuilder, type GuildMember, type TextChannel } from "discord.js"; import { genColor } from "../utils/colorGen"; import { getSetting } from "../utils/database/settings"; import { imageColor } from "../utils/imageColor"; +import { replace } from "../utils/replace"; +import { Event } from "../utils/types"; -export default { - name: "guildMemberRemove", - event: class GuildMemberRemove { - client: Client; - constructor(client: Client) { - this.client = client; - } +export default (async function run(member: GuildMember) { + const guildID = member.guild.id; + const id = getSetting(guildID, "welcome", "channel") as string; + if (!id) return; - async run(member: GuildMember) { - const guildID = member.guild.id; - const id = getSetting(guildID, "welcome", "channel") as string; - if (!id) return; + const channel = (await member.guild.channels.cache + .find(channel => channel.id == id) + ?.fetch()) as TextChannel; - let text = getSetting(guildID, "welcome", "goodbye_text") as string; - const user = member.user; - const guild = member.guild; - const channel = (await member.guild.channels.cache - .find(channel => channel.id == id) - ?.fetch()) as TextChannel; + const avatarURL = member.displayAvatarURL(); + const embed = new EmbedBuilder() + .setAuthor({ name: `• ${member.user.displayName} has left`, iconURL: avatarURL }) + .setFooter({ text: `User ID: ${member.id}` }) + .setThumbnail(avatarURL) + .setColor(member.user.hexAccentColor ?? (await imageColor(undefined, member)) ?? genColor(200)); - if (text?.includes("(name)")) text = text.replaceAll("(name)", user.displayName); - if (text?.includes("(count)")) text = text.replaceAll("(count)", `${guild.memberCount}`); - if (text?.includes("(servername)")) text = text.replaceAll("(servername)", `${guild.name}`); - - const avatarURL = member.displayAvatarURL(); - const embed = new EmbedBuilder() - .setAuthor({ name: `• ${user.displayName}`, iconURL: avatarURL }) - .setTitle("Goodbye!") - .setDescription(text ?? `**@${user.displayName}** has left the server 😥`) - .setFooter({ text: `User ID: ${member.id}` }) - .setColor( - member.user.hexAccentColor ?? (await imageColor(undefined, member)) ?? genColor(200) - ); - - await channel.send({ embeds: [embed] }); - } - } -}; + replace(member, getSetting(guildID, "welcome", "leave_text") as string, embed); + await channel.send({ embeds: [embed] }); +} as Event<"guildMemberRemove">); diff --git a/src/events/interactionCreate.ts b/src/events/interactionCreate.ts index b14ecc8..ce203ca 100644 --- a/src/events/interactionCreate.ts +++ b/src/events/interactionCreate.ts @@ -1,8 +1,9 @@ import { file } from "bun"; -import type { AutocompleteInteraction, Client, CommandInteraction } from "discord.js"; +import type { AutocompleteInteraction, CommandInteraction } from "discord.js"; import { join } from "path"; import { pathToFileURL } from "url"; import { capitalize } from "../utils/capitalize"; +import { Event } from "../utils/types"; async function getCommand( interaction: CommandInteraction | AutocompleteInteraction, @@ -28,28 +29,16 @@ async function getCommand( return new (await import(pathToFileURL(commandImportPath).toString())).default(); } -export default { - name: "interactionCreate", - event: class InteractionCreate { - commands: CommandInteraction; - client: Client; - constructor(cmds: CommandInteraction, client: Client) { - this.commands = cmds; - this.client = client; - } - - async run(interaction: CommandInteraction | AutocompleteInteraction) { - if (interaction.isChatInputCommand()) { - const command = await getCommand(interaction, interaction.options); - if (!command) return; - if (command.deferred) await interaction.deferReply(); - command.run(interaction); - } else if (interaction.isAutocomplete()) { - const command = await getCommand(interaction, interaction.options); - if (!command) return; - if (!command.autocomplete) return; - command.autocomplete(interaction); - } - } +export default (async function run(interaction) { + if (interaction.isChatInputCommand()) { + const command = await getCommand(interaction, interaction.options); + if (!command) return; + if (command.deferred) await interaction.deferReply(); + command.run(interaction); + } else if (interaction.isAutocomplete()) { + const command = await getCommand(interaction, interaction.options); + if (!command) return; + if (!command.autocomplete) return; + command.autocomplete(interaction); } -}; +} as Event<"interactionCreate">); diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts index ad223d8..e1129c1 100644 --- a/src/events/messageCreate.ts +++ b/src/events/messageCreate.ts @@ -1,129 +1,112 @@ -import { EmbedBuilder, type Message, type TextChannel } from "discord.js"; +import { EmbedBuilder, type TextChannel } from "discord.js"; import { readdirSync } from "fs"; import { join } from "path"; import { pathToFileURL } from "url"; import { genColor } from "../utils/colorGen"; import { getLevel, setLevel } from "../utils/database/levelling"; -import { get as getLevelRewards } from "../utils/database/levelRewards"; import { getSetting } from "../utils/database/settings"; import { kominator } from "../utils/kominator"; +import { Event } from "../utils/types"; -export default { - name: "messageCreate", - event: class MessageCreate { - async run(message: Message) { - const author = message.author; - if (author.bot) return; - const guild = message.guild!; - - // Easter egg handler - if (guild.id == "1079612082636472420") { - const eventsPath = join(process.cwd(), "src", "events", "easterEggs"); - - for (const easterEggFile of readdirSync(eventsPath)) - new ( - await import(pathToFileURL(join(eventsPath, easterEggFile)).toString()) - ).default().run(message); - } - - // Levelling - if (!getSetting(guild.id, "levelling", "enabled")) return; - - // const level = getSetting(guild.id, "levelling", "set_level") as string; - // if (level) { - // const newLevel = kominator(level); - // setLevel(guild.id, newLevel[0], +newLevel[1], 100 * +newLevel[1]); - // setSetting(guild.id, "levelling", "set_level", ""); - // } - - const blockedChannels = getSetting(guild.id, "levelling", "block_channels") as string; - if (blockedChannels != undefined) - for (const channelID of kominator(blockedChannels)) - if (message.channelId == channelID) return; - - const cooldowns = new Map(); - const cooldown = getSetting(guild.id, "levelling", "set_cooldown") as number; - console.log(cooldown); - // const multiplier = getSetting(guild.id, "levelling", "add_multiplier"); - let expGain = getSetting(guild.id, "levelling", "set_xp_gain") as number; - - if (cooldown > 0) { - const key = `${guild.id}-${author.id}`; - const lastExpTime = cooldowns.get(key) || 0; - const now = Date.now(); - - if (now - lastExpTime < cooldown * 1000) return; - else cooldowns.set(key, now); - } - console.log(cooldowns); - - // if (multiplier) { - // const expMultiplier = kominator(multiplier as string); - // - // if (expMultiplier[1] == "channel") if (message.channelId != expMultiplier[2]) return; - // if (expMultiplier[1] == "role") - // if (!message.member?.roles.cache.has(expMultiplier[2])) return; - // - // expGain = expGain * +expMultiplier[0]; - // } - - const levelChannelId = getSetting(guild.id, "levelling", "channel"); - const [guildLevel, guildExp] = getLevel(guild.id, author.id); - const [globalLevel, globalExp] = getLevel("0", author.id); - const expUntilLevelup = 100 * 1.15 * (guildLevel + 1); - const newLevelData = { level: guildLevel ?? 0, exp: (guildExp ?? 0) + expGain }; - const globalNewLevelData = { level: globalLevel ?? 0, exp: (globalExp ?? 0) + 2 }; - - if (guildExp < expUntilLevelup - 1) { - setLevel(0, author.id, globalNewLevelData.level, globalNewLevelData.exp); - return setLevel(guild.id, author.id, globalNewLevelData.level, globalNewLevelData.exp); - } else if (guildExp >= expUntilLevelup - 1) { - let leftOverExp = guildExp - expUntilLevelup; - if (leftOverExp < 0) leftOverExp = 0; - - newLevelData.exp = leftOverExp ?? 0; - newLevelData.level = guildLevel + 1; - setLevel(guild.id, author.id, newLevelData.level, newLevelData.exp); - } - - if (guildExp >= Math.floor(100 * 1.25 * (globalLevel + 1)) - 1) { - let globalLeftOverExp = guildExp - expUntilLevelup; - if (globalLeftOverExp < 0) globalLeftOverExp = 0; - setLevel(0, author.id, guildLevel + 1, globalLeftOverExp + 1); - } - - const embed = new EmbedBuilder() - .setAuthor({ name: `• ${author.displayName}`, iconURL: author.avatarURL() || undefined }) - .setTitle("Level up!") - .setDescription( - [ - `**Congratulations, ${author.displayName}**!`, - `You made it to **level ${guildLevel + 1}**`, - `You need ${Math.floor(100 * 1.25 * (guildLevel + 2))} EXP to level up again.` - ].join("\n") - ) - .setThumbnail(author.avatarURL()) - .setTimestamp() - .setColor(genColor(200)); - - if (levelChannelId) - (guild.channels.cache.get(`${levelChannelId}`) as TextChannel).send({ - embeds: [embed], - content: `<@${author.id}>` - }); - - for (const { level, roleID } of getLevelRewards(guild.id)) { - const role = guild.roles.cache.get(`${roleID}`); - if (!role) continue; - - const authorRoles = (await guild.members.fetch()).get(author.id)?.roles; - if (guildLevel >= level) { - await authorRoles?.add(role); - continue; - } - - await authorRoles?.remove(role); - } - } +export default (async function run(message) { + const author = message.author; + if (author.bot) return; + const guild = message.guild!; + + // Easter egg handler + if (guild.id == "1079612082636472420") { + const eventsPath = join(process.cwd(), "src", "events", "easterEggs"); + + for (const easterEggFile of readdirSync(eventsPath)) + (await import(pathToFileURL(join(eventsPath, easterEggFile)).toString())).default(message); } -}; + + // Levelling + if (!getSetting(guild.id, "levelling", "enabled")) return; + + // const level = getSetting(guild.id, "levelling", "set_level") as string; + // if (level) { + // const newLevel = kominator(level); + // setLevel(guild.id, newLevel[0], +newLevel[1], 100 * +newLevel[1]); + // setSetting(guild.id, "levelling", "set_level", ""); + // } + + const blockedChannels = getSetting(guild.id, "levelling", "block_channels") as string; + if (blockedChannels != undefined) + for (const channelID of kominator(blockedChannels)) if (message.channelId == channelID) return; + + const cooldowns = new Map(); + const cooldown = getSetting(guild.id, "levelling", "cooldown") as number; + // const multiplier = getSetting(guild.id, "levelling", "add_multiplier"); + + if (cooldown > 0) { + const key = `${guild.id}-${author.id}`; + const lastExpTime = cooldowns.get(key) || 0; + const now = Date.now(); + + if (now - lastExpTime < cooldown * 1000) return; + else cooldowns.set(key, now); + } + console.log(cooldowns); + + // if (multiplier) { + // const expMultiplier = kominator(multiplier as string); + // + // if (expMultiplier[1] == "channel") if (message.channelId != expMultiplier[2]) return; + // if (expMultiplier[1] == "role") + // if (!message.member?.roles.cache.has(expMultiplier[2])) return; + // + // expGain = expGain * +expMultiplier[0]; + // } + + const xpGain = getSetting(guild.id, "levelling", "xp_gain") as number; + const levelChannelId = getSetting(guild.id, "levelling", "channel"); + const difficulty = getSetting(guild.id, "levelling", "difficulty") as number; + const [level, xp] = getLevel(guild.id, author.id); + const xpUntilLevelup = 100 * difficulty * (level + 1) + 100 * difficulty * level; + const newLevelData = { level: level ?? 0, xp: xp + xpGain }; + + console.log(xp, newLevelData.xp); + if (newLevelData.xp < xpUntilLevelup) + return setLevel(guild.id, author.id, newLevelData.level, newLevelData.xp); + + if (newLevelData.xp >= xpUntilLevelup) { + newLevelData.level = level + 1; + setLevel(guild.id, author.id, newLevelData.level, newLevelData.xp); + } + + const embed = new EmbedBuilder() + .setAuthor({ + name: `• ${author.displayName} has levelled up!`, + iconURL: author.displayAvatarURL() + }) + .setDescription( + [ + `**Congratulations, ${author.displayName}**!`, + `You made it to **level ${level + 1}**`, + `You need ${Math.floor(100 * difficulty * (level + 2))} XP to level up again.` + ].join("\n") + ) + .setThumbnail(author.displayAvatarURL()) + .setTimestamp() + .setColor(genColor(200)); + + if (levelChannelId) + (guild.channels.cache.get(`${levelChannelId}`) as TextChannel).send({ + embeds: [embed], + content: `<@${author.id}>` + }); + + // for (const { level, roleID } of getLevelRewards(guild.id)) { + // const role = guild.roles.cache.get(`${roleID}`); + // if (!role) continue; + // + // const authorRoles = (await guild.members.fetch()).get(author.id)?.roles; + // if (guildLevel >= level) { + // await authorRoles?.add(role); + // continue; + // } + // + // await authorRoles?.remove(role); + // } +} as Event<"messageCreate">); diff --git a/src/events/messageDelete.ts b/src/events/messageDelete.ts index ea54808..3f2d05f 100644 --- a/src/events/messageDelete.ts +++ b/src/events/messageDelete.ts @@ -1,29 +1,27 @@ -import { codeBlock, EmbedBuilder, type Message } from "discord.js"; +import { codeBlock, EmbedBuilder } from "discord.js"; import { genColor } from "../utils/colorGen"; import { getSetting } from "../utils/database/settings"; import { logChannel } from "../utils/logChannel"; +import { Event } from "../utils/types"; -export default { - name: "messageDelete", - event: class MessageDelete { - async run(message: Message) { - const author = message.author; - if (author.bot) return; +export default (async function run(message) { + const author = message.author!; + if (author.bot) return; - const guild = message.guild!; - if (!getSetting(guild.id, "moderation", "log_messages")) return; + const guild = message.guild!; + if (!getSetting(guild.id, "moderation", "log_messages")) return; - const embed = new EmbedBuilder() - .setAuthor({ name: `• ${author.displayName}`, iconURL: author.displayAvatarURL() }) - .setTitle("Message has been deleted.") - .addFields({ - name: "🗞️ • Deleted message", - value: codeBlock(message.content) - }) - .setFooter({ text: `Message ID: ${message.id}\nUser ID: ${message.author.id}` }) - .setColor(genColor(60)); + const embed = new EmbedBuilder() + .setAuthor({ + name: `• ${author.displayName}'s message has been deleted.`, + iconURL: author.displayAvatarURL() + }) + .addFields({ + name: "🗞️ • Deleted message", + value: codeBlock(message.content!) + }) + .setFooter({ text: `Message ID: ${message.id}\nUser ID: ${author.id}` }) + .setColor(genColor(60)); - await logChannel(guild, embed); - } - } -}; + await logChannel(guild, embed); +} as Event<"messageDelete">); diff --git a/src/events/messageUpdate.ts b/src/events/messageUpdate.ts index c324657..f76ff05 100644 --- a/src/events/messageUpdate.ts +++ b/src/events/messageUpdate.ts @@ -1,39 +1,37 @@ -import { codeBlock, EmbedBuilder, type Message } from "discord.js"; +import { codeBlock, EmbedBuilder } from "discord.js"; import { genColor } from "../utils/colorGen"; import { getSetting } from "../utils/database/settings"; import { logChannel } from "../utils/logChannel"; +import { Event } from "../utils/types"; -export default { - name: "messageUpdate", - event: class MessageUpdate { - async run(oldMessage: Message, newMessage: Message) { - const author = oldMessage.author; - if (author.bot) return; +export default (async function run(oldMessage, newMessage) { + const author = oldMessage.author!; + if (author.bot) return; - const guild = oldMessage.guild!; - if (!getSetting(guild.id, "moderation", "log_messages")) return; + const guild = oldMessage.guild!; + if (!getSetting(guild.id, "moderation", "log_messages")) return; - const oldContent = oldMessage.content; - const newContent = newMessage.content; - if (oldContent == newContent) return; + const oldContent = oldMessage.content!; + const newContent = newMessage.content!; + if (oldContent == newContent) return; - const embed = new EmbedBuilder() - .setAuthor({ name: `• ${author.displayName}`, iconURL: author.displayAvatarURL() }) - .setTitle("Message has been edited.") - .addFields( - { - name: "🕰️ • Old message", - value: codeBlock(oldContent) - }, - { - name: "🔄️ • New message", - value: codeBlock(newContent) - } - ) - .setFooter({ text: `Message ID: ${oldMessage.id}\nUser ID: ${oldMessage.author.id}` }) - .setColor(genColor(60)); + const embed = new EmbedBuilder() + .setAuthor({ + name: `• ${author.displayName}'s message has been edited`, + iconURL: author.displayAvatarURL() + }) + .addFields( + { + name: "🕰️ • Old message", + value: codeBlock(oldContent) + }, + { + name: "🔄️ • New message", + value: codeBlock(newContent) + } + ) + .setFooter({ text: `Message ID: ${oldMessage.id}\nUser ID: ${author.id}` }) + .setColor(genColor(60)); - await logChannel(guild, embed); - } - } -}; + await logChannel(guild, embed); +} as Event<"messageUpdate">); diff --git a/src/handlers/events.ts b/src/handlers/events.ts index 7aa934c..3ac1b39 100644 --- a/src/handlers/events.ts +++ b/src/handlers/events.ts @@ -16,13 +16,11 @@ export class Events { for (const eventFile of readdirSync(eventsPath)) { if (!eventFile.endsWith("ts")) continue; - const event = await import(pathToFileURL(join(eventsPath, eventFile)).toString()); - const clientEvent = this.client.on( - event.default.name, - new event.default.event(this.client).run - ); + const event = (await import(pathToFileURL(join(eventsPath, eventFile)).toString())).default; + const eventName = eventFile.split(".ts")[0]; + const clientEvent = this.client.on(eventName, event); - this.events.push({ name: event.default.name, event: clientEvent }); + this.events.push({ name: eventName, event: clientEvent }); } } } diff --git a/src/utils/database/levelling.ts b/src/utils/database/levelling.ts index ef0f61f..b39c88d 100644 --- a/src/utils/database/levelling.ts +++ b/src/utils/database/levelling.ts @@ -7,7 +7,7 @@ const tableDefinition = { guild: "TEXT", user: "TEXT", level: "INTEGER", - exp: "INTEGER" + xp: "INTEGER" } } satisfies TableDefinition; @@ -16,7 +16,7 @@ const database = getDatabase(tableDefinition); const getQuery = database.query("SELECT * FROM levelling WHERE guild = $1 AND user = $2;"); const deleteQuery = database.query("DELETE FROM levelling WHERE guild = $1 AND user = $2;"); const insertQuery = database.query( - "INSERT INTO levelling (guild, user, level, exp) VALUES (?1, ?2, ?3, ?4);" + "INSERT INTO levelling (guild, user, level, xp) VALUES (?1, ?2, ?3, ?4);" ); const getGuildQuery = database.query("SELECT * FROM levelling WHERE guild = $1;"); @@ -24,12 +24,12 @@ const getGuildQuery = database.query("SELECT * FROM levelling WHERE guild = $1;" export function getLevel(guildID: string, userID: string): [number, number] { const res = getQuery.all(guildID, userID) as TypeOfDefinition[]; if (!res.length) return [0, 0]; - return [res[0].level, res[0].exp]; + return [res[0].level, res[0].xp]; } -export function setLevel(guildID: string | number, userID: string, level: number, exp: number) { +export function setLevel(guildID: string | number, userID: string, level: number, xp: number) { if (getQuery.all(guildID, userID).length) deleteQuery.run(guildID, userID); - insertQuery.run(guildID, userID, level, exp); + insertQuery.run(guildID, userID, level, xp); } export function getGuildLeaderboard(guildID: string): TypeOfDefinition[] { diff --git a/src/utils/database/settings.ts b/src/utils/database/settings.ts index 95ff5cc..8e6aae3 100644 --- a/src/utils/database/settings.ts +++ b/src/utils/database/settings.ts @@ -35,15 +35,20 @@ export const settingsDefinition: Record< // id: { type: "TEXT", desc: "ID of the role/channel." } // } // }, - set_xp_gain: { + xp_gain: { type: "INTEGER", desc: "Set the amount of XP a user gains per message.", val: 2 }, - set_cooldown: { + cooldown: { type: "INTEGER", desc: "Set the cooldown between messages that add XP.", val: 2 + }, + difficulty: { + type: "INTEGER", + desc: "Set the difficulty (ex: 2 will make it 2x harder to level up).", + val: 1.25 } }, moderation: { @@ -79,13 +84,13 @@ export const settingsDefinition: Record< welcome: { join_text: { type: "TEXT", - desc: "Text sent when a user joins. (user) - username, (count) - member count, (servername) - server name.", - val: "Welcome to (servername), (name)! Interestingly, you just helped us reach (count) members. Enjoy, and have a nice day!" + desc: "Text sent when a user joins. (name) - username, (count) - member count, (servername) - server name.", + val: "Welcome to (servername), (name)! Interestingly, you just helped us reach (count) members. Have a nice day!" }, leave_text: { type: "TEXT", - desc: "Text sent when a user leaves. (user) - username, (count) - member count, (servername) - server name.", - val: "(name) has left the server 😥" + desc: "Text sent when a user leaves. (name) - username, (count) - member count, (servername) - server name.", + val: "(name) has left the server! 😥" }, channel: { type: "TEXT", desc: "ID of the channel where welcome messages are sent." }, join_dm: { @@ -95,7 +100,8 @@ export const settingsDefinition: Record< }, dm_text: { type: "TEXT", - desc: "Text sent in the user's DM when they join the server. Same syntax as join_text." + desc: "Text sent in the user's DM when they join the server. Same syntax as join_text.", + val: "Welcome to (servername), (name)! Interestingly, you just helped us reach (count) members. Have a nice day!" } } }; diff --git a/src/utils/embeds/errorEmbed.ts b/src/utils/embeds/errorEmbed.ts index 8f363d7..ee3e2e6 100644 --- a/src/utils/embeds/errorEmbed.ts +++ b/src/utils/embeds/errorEmbed.ts @@ -17,7 +17,7 @@ export async function errorEmbed( const content = [`**${title}**`]; if (reason != undefined) content.push(reason); const embed = new EmbedBuilder() - .setTitle("Something went wrong!") + .setAuthor({ name: "Something went wrong!" }) .setDescription(content.join("\n")) .setColor(genColor(0)); diff --git a/src/utils/embeds/modEmbed.ts b/src/utils/embeds/modEmbed.ts index d673c9a..e703e8f 100644 --- a/src/utils/embeds/modEmbed.ts +++ b/src/utils/embeds/modEmbed.ts @@ -143,8 +143,10 @@ export async function modEmbed( .send({ embeds: [ embed - .setAuthor({ name: `• ${name}`, iconURL: user.displayAvatarURL() }) - .setTitle(`You got ${action.toLowerCase()}.`) + .setAuthor({ + name: `• You got ${action.toLowerCase()}.`, + iconURL: user.displayAvatarURL() + }) .setDescription(generalValues.slice(+!showModerator, generalValues.length).join("\n")) .setColor(genColor(0)) ] diff --git a/src/utils/replace.ts b/src/utils/replace.ts new file mode 100644 index 0000000..2d0f6a6 --- /dev/null +++ b/src/utils/replace.ts @@ -0,0 +1,10 @@ +import type { GuildMember, EmbedBuilder } from "discord.js"; + +export function replace(member: GuildMember, text: string, embed: EmbedBuilder) { + const user = member.user; + const guild = member.guild; + if (text?.includes("(name)")) text = text.replaceAll("(name)", user.displayName); + if (text?.includes("(count)")) text = text.replaceAll("(count)", `${guild.memberCount}`); + if (text?.includes("(servername)")) text = text.replaceAll("(servername)", `${guild.name}`); + embed.setDescription(text); +} diff --git a/src/utils/types.ts b/src/utils/types.ts new file mode 100644 index 0000000..9e8daf0 --- /dev/null +++ b/src/utils/types.ts @@ -0,0 +1,3 @@ +import { ClientEvents } from "discord.js"; + +export type Event = (...args: ClientEvents[T][0][]) => any; diff --git a/src/utils/unbanScheduler.ts b/src/utils/unbanScheduler.ts index a79ef18..599fccf 100644 --- a/src/utils/unbanScheduler.ts +++ b/src/utils/unbanScheduler.ts @@ -3,35 +3,32 @@ import { genColor } from "./colorGen"; import { getPendingBans, removeModeration } from "./database/moderation"; import { logChannel } from "./logChannel"; -export async function logUnban(client: Client) { - const pendingBans = getPendingBans(Date.now()); - - const now = Date.now(); - console.log(pendingBans); - console.log(getPendingBans(now)); - for (const ban of pendingBans) { - console.log(ban); - const guild = await client.guilds.fetch(ban.guild); - const user = guild.members.cache.get(ban.user)!; - const embed = new EmbedBuilder() - .setAuthor({ name: `• Unbanned ${user.displayName}`, iconURL: user.displayAvatarURL() }) - .setDescription([`**Moderator**: ${ban.moderator}`, "*Temporary ban has expired*"].join("\n")) - .setFooter({ text: `User ID: ${user.id}\nCase ID: ${ban.id}` }) - .setColor(genColor(100)); - - return await logChannel(guild, embed); - } -} - -export function scheduleUnban(client: Client, guildID: string, userID: string, delay: number) { +export function scheduleUnban( + client: Client, + guildID: string, + userID: string, + modID: string, + delay: number +) { const scheduledUnbans = new Map(); const key = `${guildID}-${userID}`; if (scheduledUnbans.has(key)) clearTimeout(scheduledUnbans.get(key)!); const timeout = setTimeout(async () => { try { - await logUnban(client); - await (await client.guilds.fetch(guildID)).members.unban(userID, "Temporary ban has expired"); + const guild = await client.guilds.fetch(guildID); + const user = guild.bans.cache.get(userID)?.user!; + const moderator = guild.members.cache.get(modID)!; + const embed = new EmbedBuilder() + .setAuthor({ name: `• Unbanned ${user.displayName}`, iconURL: user.displayAvatarURL() }) + .setDescription( + [`**Moderator**: ${moderator.displayName}`, "*Temporary ban has expired*"].join("\n") + ) + .setFooter({ text: `User ID: ${user.id}` }) + .setColor(genColor(100)); + + await logChannel(guild, embed); + await guild.members.unban(userID, "Temporary ban has expired"); removeModeration(guildID, userID); scheduledUnbans.delete(key); } catch (error) { @@ -48,14 +45,13 @@ export function rescheduleUnbans(client: Client) { for (const ban of pendingBans) { if (!ban.expiresAt) continue; - if (typeof ban.expiresAt !== "number" || isNaN(ban.expiresAt)) { console.error(`Invalid expiresAt value for ban: ${ban.expiresAt}`); continue; } const delay = ban.expiresAt - now; - if (delay > 0) scheduleUnban(client, ban.guild, ban.user, delay); + if (delay > 0) scheduleUnban(client, ban.guild, ban.user, ban.moderator, delay); else removeModeration(ban.guild, ban.id); } } From b60a94b8297f7fdefb5ea75756d939b6eec39dfc Mon Sep 17 00:00:00 2001 From: MrSerge01 <86667481+MrSerge01@users.noreply.github.com> Date: Sun, 3 Nov 2024 13:11:55 +0500 Subject: [PATCH 116/127] eternal suffering pt6: THE END --- src/commands/Settings.ts | 1 - src/commands/User.ts | 3 +- src/events/messageCreate.ts | 52 +++++++--------------------------- src/utils/database/settings.ts | 4 +-- 4 files changed, 14 insertions(+), 46 deletions(-) diff --git a/src/commands/Settings.ts b/src/commands/Settings.ts index 4430711..cc87523 100644 --- a/src/commands/Settings.ts +++ b/src/commands/Settings.ts @@ -88,7 +88,6 @@ export default class Settings { const key = interaction.options.getSubcommand() as keyof typeof settingsDefinition; const values = interaction.options.data[0].options!; - console.log(values); if (!values.length) { const embed = new EmbedBuilder().setTitle(`Settings for ${key}`).setColor(genColor(100)); const description: string[] = []; diff --git a/src/commands/User.ts b/src/commands/User.ts index 673f83d..fcd5968 100644 --- a/src/commands/User.ts +++ b/src/commands/User.ts @@ -103,10 +103,9 @@ export default class User { const difficulty = getSetting(guild.id, "levelling", "difficulty") as number; const [level, xp] = getLevel(guild.id, target.id)!; const nextLevelXp = Math.floor( - 100 * difficulty * (level + 1) + 100 * difficulty * level + 100 * difficulty * (level + 1) ** 2 - 85 * difficulty * level ** 2 )?.toLocaleString("en-US"); - console.log(xp); const collector = reply.createMessageComponentCollector({ time: 60000 }); collector.on("collect", async (i: ButtonInteraction) => { if (i.message.id != (await reply.fetch()).id) diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts index e1129c1..be7a063 100644 --- a/src/events/messageCreate.ts +++ b/src/events/messageCreate.ts @@ -8,6 +8,7 @@ import { getSetting } from "../utils/database/settings"; import { kominator } from "../utils/kominator"; import { Event } from "../utils/types"; +const cooldowns = new Map(); export default (async function run(message) { const author = message.author; if (author.bot) return; @@ -24,21 +25,11 @@ export default (async function run(message) { // Levelling if (!getSetting(guild.id, "levelling", "enabled")) return; - // const level = getSetting(guild.id, "levelling", "set_level") as string; - // if (level) { - // const newLevel = kominator(level); - // setLevel(guild.id, newLevel[0], +newLevel[1], 100 * +newLevel[1]); - // setSetting(guild.id, "levelling", "set_level", ""); - // } - const blockedChannels = getSetting(guild.id, "levelling", "block_channels") as string; if (blockedChannels != undefined) for (const channelID of kominator(blockedChannels)) if (message.channelId == channelID) return; - const cooldowns = new Map(); const cooldown = getSetting(guild.id, "levelling", "cooldown") as number; - // const multiplier = getSetting(guild.id, "levelling", "add_multiplier"); - if (cooldown > 0) { const key = `${guild.id}-${author.id}`; const lastExpTime = cooldowns.get(key) || 0; @@ -47,34 +38,26 @@ export default (async function run(message) { if (now - lastExpTime < cooldown * 1000) return; else cooldowns.set(key, now); } - console.log(cooldowns); - - // if (multiplier) { - // const expMultiplier = kominator(multiplier as string); - // - // if (expMultiplier[1] == "channel") if (message.channelId != expMultiplier[2]) return; - // if (expMultiplier[1] == "role") - // if (!message.member?.roles.cache.has(expMultiplier[2])) return; - // - // expGain = expGain * +expMultiplier[0]; - // } const xpGain = getSetting(guild.id, "levelling", "xp_gain") as number; const levelChannelId = getSetting(guild.id, "levelling", "channel"); const difficulty = getSetting(guild.id, "levelling", "difficulty") as number; const [level, xp] = getLevel(guild.id, author.id); - const xpUntilLevelup = 100 * difficulty * (level + 1) + 100 * difficulty * level; + const xpUntilLevelUp = Math.floor( + 100 * difficulty * (level + 1) ** 2 - 85 * difficulty * level ** 2 + ); const newLevelData = { level: level ?? 0, xp: xp + xpGain }; - console.log(xp, newLevelData.xp); - if (newLevelData.xp < xpUntilLevelup) + if (newLevelData.xp < xpUntilLevelUp) return setLevel(guild.id, author.id, newLevelData.level, newLevelData.xp); - if (newLevelData.xp >= xpUntilLevelup) { - newLevelData.level = level + 1; - setLevel(guild.id, author.id, newLevelData.level, newLevelData.xp); - } + while ( + newLevelData.xp >= + 100 * difficulty * (newLevelData.level + 1) ** 2 - 85 * difficulty * newLevelData.level ** 2 + ) + newLevelData.level++; + setLevel(guild.id, author.id, newLevelData.level, newLevelData.xp); const embed = new EmbedBuilder() .setAuthor({ name: `• ${author.displayName} has levelled up!`, @@ -96,17 +79,4 @@ export default (async function run(message) { embeds: [embed], content: `<@${author.id}>` }); - - // for (const { level, roleID } of getLevelRewards(guild.id)) { - // const role = guild.roles.cache.get(`${roleID}`); - // if (!role) continue; - // - // const authorRoles = (await guild.members.fetch()).get(author.id)?.roles; - // if (guildLevel >= level) { - // await authorRoles?.add(role); - // continue; - // } - // - // await authorRoles?.remove(role); - // } } as Event<"messageCreate">); diff --git a/src/utils/database/settings.ts b/src/utils/database/settings.ts index 8e6aae3..cd92aa5 100644 --- a/src/utils/database/settings.ts +++ b/src/utils/database/settings.ts @@ -38,7 +38,7 @@ export const settingsDefinition: Record< xp_gain: { type: "INTEGER", desc: "Set the amount of XP a user gains per message.", - val: 2 + val: 5 }, cooldown: { type: "INTEGER", @@ -48,7 +48,7 @@ export const settingsDefinition: Record< difficulty: { type: "INTEGER", desc: "Set the difficulty (ex: 2 will make it 2x harder to level up).", - val: 1.25 + val: 1 } }, moderation: { From e846c99f2c24191c1931430cbb12fbd136a52b4e Mon Sep 17 00:00:00 2001 From: MrSerge01 <86667481+MrSerge01@users.noreply.github.com> Date: Sun, 3 Nov 2024 13:51:53 +0500 Subject: [PATCH 117/127] this is not suffering anymore --- static/banner.svg | 73 ++++++++++++++++------------------------------- 1 file changed, 25 insertions(+), 48 deletions(-) diff --git a/static/banner.svg b/static/banner.svg index faa565e..e3734a9 100644 --- a/static/banner.svg +++ b/static/banner.svg @@ -1,59 +1,36 @@ - + - - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + - + + + + + + - + - + From 2dcaa01af275fe5e9082a390b152caf3b2e33789 Mon Sep 17 00:00:00 2001 From: MrSerge01 <86667481+MrSerge01@users.noreply.github.com> Date: Sun, 3 Nov 2024 13:51:53 +0500 Subject: [PATCH 118/127] this is not suffering anymore --- README.md | 2 +- static/banner.png | Bin 0 -> 168520 bytes static/banner.svg | 37 ------------------------------------- 3 files changed, 1 insertion(+), 38 deletions(-) create mode 100644 static/banner.png delete mode 100644 static/banner.svg diff --git a/README.md b/README.md index fed9142..6685757 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@

- +

diff --git a/static/banner.png b/static/banner.png new file mode 100644 index 0000000000000000000000000000000000000000..a4dfb93781062d921165d2438b7c73db4fbdee21 GIT binary patch literal 168520 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

U5 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

@&qF9^MJ1EtuUbi|1T$Q?6 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 literal 0 HcmV?d00001 diff --git a/static/banner.svg b/static/banner.svg deleted file mode 100644 index e3734a9..0000000 --- a/static/banner.svg +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From 955145fd2a0d5c900a12c1dd13920f8360516548 Mon Sep 17 00:00:00 2001 From: Dima Marhitych Date: Sun, 3 Nov 2024 10:39:12 +0100 Subject: [PATCH 119/127] Switch to github actions --- .github/workflows/pipeline.yml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 .github/workflows/pipeline.yml diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml new file mode 100644 index 0000000..4128e2f --- /dev/null +++ b/.github/workflows/pipeline.yml @@ -0,0 +1,26 @@ +name: Build Sokora + +on: + push: + branches: + - dev + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: '20.x' + + - name: Install packages and build + run: npm install && tsc -b ./ + From 56d78f7beb9ea815e3c08e83db3248ee08799dfe Mon Sep 17 00:00:00 2001 From: Dima Marhitych Date: Sun, 3 Nov 2024 10:43:35 +0100 Subject: [PATCH 120/127] possible macos fix --- .github/workflows/pipeline.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml index 4128e2f..de695ae 100644 --- a/.github/workflows/pipeline.yml +++ b/.github/workflows/pipeline.yml @@ -22,5 +22,5 @@ jobs: node-version: '20.x' - name: Install packages and build - run: npm install && tsc -b ./ + run: npm install && npx tsc -b ./ From bed974598a2151dc0dd0d967eaa8e6f267cdcce2 Mon Sep 17 00:00:00 2001 From: ZakaHaceCosas Date: Sun, 3 Nov 2024 14:09:05 +0100 Subject: [PATCH 121/127] (fix) Replace deprecated component --- src/commands/About.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/About.ts b/src/commands/About.ts index 207cbfa..d2d8b65 100644 --- a/src/commands/About.ts +++ b/src/commands/About.ts @@ -48,8 +48,8 @@ export default class About { "**Founder**: Goos", "**Translator Lead**: ThatBOI", "**Developers**: Dimkauzh, Froxcey, Golem64, Koslz, MQuery, Nikkerudon, Spectrum, ThatBOI", - "**Designers**: ArtyH, Optix, Pjanda", - "**Translators**: Dimkauzh, flojo, Golem64, GraczNet, Nikkerudon, Optix, SaFire, TrulyBlue", + "**Designers**: ArtyH, ZakaHaceCosas, Pjanda", + "**Translators**: Dimkauzh, flojo, Golem64, GraczNet, Nikkerudon, ZakaHaceCosas, SaFire, TrulyBlue", "**Testers**: Blaze, fishy, Trynera", "And **YOU**, for using Sokora." ].join("\n") From 0f15dc05256afc89dc355ee31517227cf6829fe6 Mon Sep 17 00:00:00 2001 From: ZakaHaceCosas Date: Sun, 3 Nov 2024 14:09:22 +0100 Subject: [PATCH 122/127] (feat) Allow to enable/disable easter eggs (making them global) --- src/events/messageCreate.ts | 3 ++- src/utils/database/settings.ts | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts index be7a063..7a05da6 100644 --- a/src/events/messageCreate.ts +++ b/src/events/messageCreate.ts @@ -15,7 +15,8 @@ export default (async function run(message) { const guild = message.guild!; // Easter egg handler - if (guild.id == "1079612082636472420") { + // i kept the old ID used here (if guild.id == "ID") in my .env file, just in case + if (getSetting(guild.id, "easter", "enabled") == true) { const eventsPath = join(process.cwd(), "src", "events", "easterEggs"); for (const easterEggFile of readdirSync(eventsPath)) diff --git a/src/utils/database/settings.ts b/src/utils/database/settings.ts index cd92aa5..9a5691c 100644 --- a/src/utils/database/settings.ts +++ b/src/utils/database/settings.ts @@ -103,6 +103,13 @@ export const settingsDefinition: Record< desc: "Text sent in the user's DM when they join the server. Same syntax as join_text.", val: "Welcome to (servername), (name)! Interestingly, you just helped us reach (count) members. Have a nice day!" } + }, + easter: { + enabled: { + type: "BOOL", + desc: "Whether or not the bot should reply to certain messages with 'easter egg' messages.", + val: true + } } }; From c3dfdb08b490204d0ae416b33f541df55d2734fd Mon Sep 17 00:00:00 2001 From: ZakaHaceCosas Date: Sun, 3 Nov 2024 14:37:49 +0100 Subject: [PATCH 123/127] (feat) Add descriptions to /settings commands --- src/commands/Settings.ts | 14 +-- src/utils/database/settings.ts | 205 +++++++++++++++++++-------------- 2 files changed, 125 insertions(+), 94 deletions(-) diff --git a/src/commands/Settings.ts b/src/commands/Settings.ts index cc87523..8d1692d 100644 --- a/src/commands/Settings.ts +++ b/src/commands/Settings.ts @@ -27,15 +27,15 @@ export default class Settings { settingsKeys.forEach(key => { const subcommand = new SlashCommandSubcommandBuilder() .setName(key) - .setDescription("This subcommand has no description."); + .setDescription(settingsDefinition[key].description); - Object.keys(settingsDefinition[key]).forEach(sub => { - switch (settingsDefinition[key][sub]["type"] as string) { + Object.keys(settingsDefinition[key].settings).forEach(sub => { + switch (settingsDefinition[key].settings[sub]["type"] as string) { case "BOOL": subcommand.addBooleanOption(option => option .setName(sub) - .setDescription(settingsDefinition[key][sub]["desc"]) + .setDescription(settingsDefinition[key].settings[sub]["desc"]) .setRequired(false) ); break; @@ -43,7 +43,7 @@ export default class Settings { subcommand.addIntegerOption(option => option .setName(sub) - .setDescription(settingsDefinition[key][sub]["desc"]) + .setDescription(settingsDefinition[key].settings[sub]["desc"]) .setRequired(false) ); break; @@ -51,7 +51,7 @@ export default class Settings { subcommand.addUserOption(option => option .setName(sub) - .setDescription(settingsDefinition[key][sub]["desc"]) + .setDescription(settingsDefinition[key].settings[sub]["desc"]) .setRequired(false) ); break; @@ -63,7 +63,7 @@ export default class Settings { subcommand.addStringOption(option => option .setName(sub) - .setDescription(settingsDefinition[key][sub]["desc"]) + .setDescription(settingsDefinition[key].settings[sub]["desc"]) .setRequired(false) ); break; diff --git a/src/utils/database/settings.ts b/src/utils/database/settings.ts index 9a5691c..270f5e0 100644 --- a/src/utils/database/settings.ts +++ b/src/utils/database/settings.ts @@ -13,102 +13,130 @@ const tableDefinition = { export const settingsDefinition: Record< string, - Record + { + description: string; + settings: Record; + } > = { levelling: { - enabled: { type: "BOOL", desc: "Enable/disable the levelling system.", val: true }, - channel: { - type: "TEXT", - desc: "ID of the log channel for levelling-related stuff (i.e someone levelling up)." - }, - block_channels: { - type: "TEXT", - desc: "ID(s) of the channels where messages aren't counted, comma separated." - }, - // set_level: { type: "TEXT", desc: "Set the level of a user." }, - // add_multiplier: { - // type: "LIST", - // desc: "Add an XP multiplier to the levelling system.", - // val: { - // multiplier: { type: "INTEGER", desc: "Set the XP multiplier for the role/channel." }, - // role_channel: { type: "TEXT", desc: "Role or channel. (choose)" }, - // id: { type: "TEXT", desc: "ID of the role/channel." } - // } - // }, - xp_gain: { - type: "INTEGER", - desc: "Set the amount of XP a user gains per message.", - val: 5 - }, - cooldown: { - type: "INTEGER", - desc: "Set the cooldown between messages that add XP.", - val: 2 - }, - difficulty: { - type: "INTEGER", - desc: "Set the difficulty (ex: 2 will make it 2x harder to level up).", - val: 1 + description: "Customise the behaviour of the leveling system.", + settings: { + enabled: { + type: "BOOL", + desc: "Enable/disable the levelling system.", + val: true + }, + channel: { + type: "TEXT", + desc: "ID of the log channel for levelling-related stuff (i.e someone levelling up)." + }, + block_channels: { + type: "TEXT", + desc: "ID(s) of the channels where messages aren't counted, comma separated." + }, + // set_level: { type: "TEXT", desc: "Set the level of a user." }, + // add_multiplier: { + // type: "LIST", + // desc: "Add an XP multiplier to the levelling system.", + // val: { + // multiplier: { type: "INTEGER", desc: "Set the XP multiplier for the role/channel." }, + // role_channel: { type: "TEXT", desc: "Role or channel. (choose)" }, + // id: { type: "TEXT", desc: "ID of the role/channel." } + // } + // }, + xp_gain: { + type: "INTEGER", + desc: "Set the amount of XP a user gains per message.", + val: 5 + }, + cooldown: { + type: "INTEGER", + desc: "Set the cooldown between messages that add XP.", + val: 2 + }, + difficulty: { + type: "INTEGER", + desc: "Set the difficulty (ex: 2 will make it 2x harder to level up).", + val: 1 + } } }, moderation: { - channel: { - type: "TEXT", - desc: "ID of the log channel for moderation-related stuff (i.e a message being edited)." - }, - log_messages: { - type: "BOOL", - desc: "Whether or not edited/deleted messages should be logged.", - val: true + description: "Tweak Sokora's moderation-related logs.", + settings: { + channel: { + type: "TEXT", + desc: "ID of the log channel for moderation-related stuff (i.e a message being edited)." + }, + log_messages: { + type: "BOOL", + desc: "Whether or not edited/deleted messages should be logged.", + val: true + } } }, news: { - channel_id: { type: "TEXT", desc: "ID of the channel where news messages are sent." }, - role_id: { - type: "TEXT", - desc: "ID of the roles that should be pinged when a news message is sent." - }, - edit_original_message: { - type: "BOOL", - desc: "Whether or not the original message should be edited when a news message is updated.", - val: true + description: "Configure news for this server.", + settings: { + channel_id: { + type: "TEXT", + desc: "ID of the channel where news messages are sent." + }, + role_id: { + type: "TEXT", + desc: "ID of the roles that should be pinged when a news message is sent." + }, + edit_original_message: { + type: "BOOL", + desc: "Whether or not the original message should be edited when a news message is updated.", + val: true + } } }, serverboard: { - shown: { - type: "BOOL", - desc: "Whether or not the server should be shown on the serverboard.", - val: false + description: "Configure your server's appearance on the serverboard.", + settings: { + shown: { + type: "BOOL", + desc: "Whether or not the server should be shown on the serverboard.", + val: false + } } }, welcome: { - join_text: { - type: "TEXT", - desc: "Text sent when a user joins. (name) - username, (count) - member count, (servername) - server name.", - val: "Welcome to (servername), (name)! Interestingly, you just helped us reach (count) members. Have a nice day!" - }, - leave_text: { - type: "TEXT", - desc: "Text sent when a user leaves. (name) - username, (count) - member count, (servername) - server name.", - val: "(name) has left the server! 😥" - }, - channel: { type: "TEXT", desc: "ID of the channel where welcome messages are sent." }, - join_dm: { - type: "BOOL", - desc: "Whether or not the bot should send a custom DM message to the user upon joining.", - val: false - }, - dm_text: { - type: "TEXT", - desc: "Text sent in the user's DM when they join the server. Same syntax as join_text.", - val: "Welcome to (servername), (name)! Interestingly, you just helped us reach (count) members. Have a nice day!" + description: "Tweak how Sokora welcomes your new users", + settings: { + join_text: { + type: "TEXT", + desc: "Text sent when a user joins. (name) - username, (count) - member count, (servername) - server name.", + val: "Welcome to (servername), (name)! Interestingly, you just helped us reach (count) members. Have a nice day!" + }, + leave_text: { + type: "TEXT", + desc: "Text sent when a user leaves. (name) - username, (count) - member count, (servername) - server name.", + val: "(name) has left the server! 😥" + }, + channel: { type: "TEXT", desc: "ID of the channel where welcome messages are sent." }, + join_dm: { + type: "BOOL", + desc: "Whether or not the bot should send a custom DM message to the user upon joining.", + val: false + }, + dm_text: { + type: "TEXT", + desc: "Text sent in the user's DM when they join the server. Same syntax as join_text.", + val: "Welcome to (servername), (name)! Interestingly, you just helped us reach (count) members. Have a nice day!" + } } }, easter: { - enabled: { + description: "Enable or disable easter eggs", + settings: { + enabled: { type: "BOOL", desc: "Whether or not the bot should reply to certain messages with 'easter egg' messages.", val: true + } } } }; @@ -124,15 +152,18 @@ const insertQuery = database.query( "INSERT INTO settings (guildID, key, value) VALUES (?1, ?2, ?3);" ); -export function getSetting( +export function getSetting< + K extends keyof typeof settingsDefinition, + S extends keyof (typeof settingsDefinition)[K]["settings"] +>( guildID: string, key: K, - setting: string -): TypeOfKey | null { + setting: S +): SqlType<(typeof settingsDefinition)[K]["settings"][S]["type"]> | null { let res = getQuery.all(JSON.stringify(guildID), key + "." + setting) as TypeOfDefinition< typeof tableDefinition >[]; - const set = settingsDefinition[key][setting]; + const set = settingsDefinition[key].settings[setting]; if (!res.length) { if (set.type == "LIST") return null; @@ -141,15 +172,15 @@ export function getSetting( switch (set.type) { case "TEXT": - return res[0].value as TypeOfKey; + return res[0].value as SqlType; case "BOOL": - return (res[0].value === "1" ? true : false) as TypeOfKey; + return (res[0].value === "1" ? true : false) as SqlType; case "INTEGER": - return parseInt(res[0].value) as TypeOfKey; + return parseInt(res[0].value) as SqlType; case "LIST": - return kominator(res[0].value) as TypeOfKey; + return kominator(res[0].value) as SqlType; default: - return "WIP" as TypeOfKey; + return "WIP"; // as TypeOfKey; } } @@ -171,6 +202,6 @@ export function listPublicServers() { } // Utility type -type TypeOfKey = SqlType< - (typeof settingsDefinition)[T][any]["type"] +type TypeOfKey = SqlType< + (typeof settingsDefinition)[K]["settings"][S]["type"] >; From ce94122e2667f6d83b787afcca519db299f5c314 Mon Sep 17 00:00:00 2001 From: froxcey Date: Sun, 3 Nov 2024 13:58:28 +0000 Subject: [PATCH 124/127] Add setup command --- CONTRIBUTING.md | 6 ++---- cli/setup.ts | 50 +++++++++++++++++++++++++++++++++++++++++++++++++ example.env | 2 +- package.json | 1 + 4 files changed, 54 insertions(+), 5 deletions(-) create mode 100644 cli/setup.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 555ab7e..a6f3644 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,7 +6,7 @@ ## Get started with contributing ### Getting the code -- Make a fork of this repository. +- Make a fork of this repository. - Clone your fork. ### Creating your bot @@ -15,11 +15,9 @@ - Reset and then copy your bot's token. ### Setting up .env -- Copy the `example.env` file. -- Paste it into a `.env` file and replace the token with your own. +- Run `bun run setup` and our cli tool will install dependencies and write .env for you ### Running -- Run `bun i` to install all the dependencies needed by the bot. - Run `bun start`. Be sure to open a pull request when you're ready to push your changes. Be descriptive of the changes you've made. diff --git a/cli/setup.ts b/cli/setup.ts new file mode 100644 index 0000000..52c22d3 --- /dev/null +++ b/cli/setup.ts @@ -0,0 +1,50 @@ +import * as fs from "fs"; +import { createInterface } from "readline"; + +const readHiddenInput = async (prompt: string): Promise => { + console.log(prompt); + + return new Promise(resolve => { + const rl = createInterface({ + input: process.stdin, + output: process.stdout + }); + + const stdin = process.stdin; + if (stdin.isTTY) stdin.setRawMode(true); + + stdin.on("data", data => { + if (data.toString() === "\n" || data.toString() === "\r") { + if (stdin.isTTY) stdin.setRawMode(false); + process.stdout.moveCursor(0, -1); + process.stdout.clearLine(1); + rl.close(); + } + }); + + rl.question("", input => { + resolve(input); + }); + }); +}; + +const replaceTokenInEnv = (token: string) => { + try { + const envContent = fs.readFileSync(".env", "utf-8"); + const updatedContent = envContent.replace("YOURTOKEN", token); + fs.writeFileSync(".env", updatedContent, "utf-8"); + console.log("You're good to go, happy coding!"); + } catch (error) { + console.error("Error updating .env file:", error); + } +}; + +const main = async () => { + fs.copyFileSync("example.env", ".env"); + const token = await readHiddenInput( + "Paste your token below: (you can get one from https://discord.com/developers/applications)" + ); + replaceTokenInEnv(token); +}; + +main().catch(console.error); diff --git a/example.env b/example.env index 403cbcc..8973729 100644 --- a/example.env +++ b/example.env @@ -1 +1 @@ -TOKEN="YOURTOKEN" # The bot token found in the Discord Developer portal +TOKEN="YOURTOKEN" diff --git a/package.json b/package.json index 688566f..91dd4ed 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "main": "./src/index.ts", "type": "module", "scripts": { + "setup": "bun i && bun run cli/setup.ts", "start": "bun ./src/index.ts" }, "dependencies": { From b7ca1b269116bcc4c1248af56403d41cd8766bab Mon Sep 17 00:00:00 2001 From: MrSerge01 <86667481+MrSerge01@users.noreply.github.com> Date: Sun, 3 Nov 2024 21:26:40 +0500 Subject: [PATCH 125/127] the suffering is truly never over --- src/commands/About.ts | 7 +- src/commands/Leaderboard.ts | 13 +++- src/commands/Serverboard.ts | 47 ++++++------ src/commands/Settings.ts | 5 -- src/commands/User.ts | 74 ++++++++++--------- src/commands/moderation/Ban.ts | 2 +- src/commands/moderation/Lock.ts | 3 +- src/commands/moderation/Mute.ts | 2 +- src/commands/moderation/Unlock.ts | 3 +- src/commands/news/View.ts | 1 + src/events/easterEggs/amerikaYa.ts | 2 +- src/events/easterEggs/crazy.ts | 7 +- src/events/easterEggs/fan.ts | 2 +- src/events/easterEggs/fire.ts | 2 +- src/events/easterEggs/whoPinged.ts | 2 +- src/events/guildMemberAdd.ts | 8 +- src/events/guildMemberRemove.ts | 8 +- src/events/messageCreate.ts | 19 +++-- src/utils/colorGen.ts | 2 + .../database/{levelling.ts => leveling.ts} | 10 +-- src/utils/database/settings.ts | 26 ++----- src/utils/embeds/modEmbed.ts | 2 +- src/utils/embeds/serverEmbed.ts | 6 +- src/utils/imageColor.ts | 16 ++-- src/utils/kominator.ts | 3 +- src/utils/logChannel.ts | 13 ++-- src/utils/multiReact.ts | 5 +- src/utils/sendChannelNews.ts | 19 ++--- 28 files changed, 155 insertions(+), 154 deletions(-) rename src/utils/database/{levelling.ts => leveling.ts} (75%) diff --git a/src/commands/About.ts b/src/commands/About.ts index d2d8b65..81adb7a 100644 --- a/src/commands/About.ts +++ b/src/commands/About.ts @@ -24,11 +24,12 @@ export default class About { const guilds = client.guilds.cache; const members = guilds.map(guild => guild.memberCount).reduce((a, b) => a + b); const shards = client.shard?.count; + const avatar = user.displayAvatarURL(); let emojis = ["💖", "💝", "💓", "💗", "💘", "💟", "💕", "💞"]; if (Math.round(Math.random() * 100) <= 5) emojis = ["⌨️", "💻", "🖥️"]; const embed = new EmbedBuilder() - .setAuthor({ name: "• About Sokora", iconURL: user.displayAvatarURL() }) + .setAuthor({ name: "• About Sokora", iconURL: avatar }) .setDescription( "Sokora is a multipurpose Discord bot that lets you manage your servers easily." ) @@ -61,8 +62,8 @@ export default class About { } ) .setFooter({ text: `Made with ${randomise(emojis)} by the Sokora team` }) - .setThumbnail(user.displayAvatarURL()) - .setColor(user.hexAccentColor ?? (await imageColor(undefined, user)) ?? genColor(270)); + .setThumbnail(avatar) + .setColor(user.hexAccentColor ?? (await imageColor(undefined, avatar)) ?? genColor(270)); const row = new ActionRowBuilder().addComponents( new ButtonBuilder() diff --git a/src/commands/Leaderboard.ts b/src/commands/Leaderboard.ts index 4073c08..bbedffc 100644 --- a/src/commands/Leaderboard.ts +++ b/src/commands/Leaderboard.ts @@ -9,7 +9,7 @@ import { type SlashCommandOptionsOnlyBuilder } from "discord.js"; import { genColor } from "../utils/colorGen"; -import { getGuildLeaderboard } from "../utils/database/levelling"; +import { getGuildLeaderboard } from "../utils/database/leveling"; import { errorEmbed } from "../utils/embeds/errorEmbed"; export default class Leaderboard { @@ -30,7 +30,7 @@ export default class Leaderboard { return errorEmbed( interaction, "No data found.", - "There is no levelling data for this server yet." + "There is no leveling data for this server yet." ); leaderboardData.sort((a, b) => { @@ -86,9 +86,16 @@ export default class Leaderboard { }); collector.on("collect", async (i: ButtonInteraction) => { + if (i.message.id != (await reply.fetch()).id) + return await errorEmbed( + i, + "For some reason, this click would've caused the bot to error. Thankfully, this message right here prevents that." + ); + if (i.user.id != interaction.user.id) - return errorEmbed(i, "You are not the author of this command."); + return errorEmbed(i, "You are not the person who executed this command."); + collector.resetTimer({ time: 60000 }); if (i.customId == "left") page = page > 1 ? page - 1 : totalPages; else page = page < totalPages ? page + 1 : 1; diff --git a/src/commands/Serverboard.ts b/src/commands/Serverboard.ts index cd97cbe..30323ac 100644 --- a/src/commands/Serverboard.ts +++ b/src/commands/Serverboard.ts @@ -59,31 +59,32 @@ export default class Serverboard { }); if (pages == 1) return; - reply - .createMessageComponentCollector({ time: 60000 }) - .on("collect", async (i: ButtonInteraction) => { - if (i.message.id != (await reply.fetch()).id) - return await errorEmbed( - i, - "For some reason, this click would've caused the bot to error. Thankfully, this message right here prevents that." - ); + const collector = reply.createMessageComponentCollector({ time: 60000 }); + collector.on("collect", async (i: ButtonInteraction) => { + if (i.message.id != (await reply.fetch()).id) + return await errorEmbed( + i, + "For some reason, this click would've caused the bot to error. Thankfully, this message right here prevents that." + ); - if (i.user.id != interaction.user.id) - return await errorEmbed(i, "You aren't the person who executed this command."); + if (i.user.id != interaction.user.id) + return await errorEmbed(i, "You aren't the person who executed this command."); - setTimeout(async () => await i.editReply({ components: [] }), 60000); - switch (i.customId) { - case "left": - page--; - if (page < 0) page = pages - 1; - break; - case "right": - page++; - if (page >= pages) page = 0; - break; - } + collector.resetTimer({ time: 60000 }); + switch (i.customId) { + case "left": + page--; + if (page < 0) page = pages - 1; + break; + case "right": + page++; + if (page >= pages) page = 0; + break; + } - await i.update({ embeds: [await getEmbed()], components: [row] }); - }); + await i.update({ embeds: [await getEmbed()], components: [row] }); + }); + + collector.on("end", async () => await interaction.editReply({ components: [] })); } } diff --git a/src/commands/Settings.ts b/src/commands/Settings.ts index 8d1692d..472faf7 100644 --- a/src/commands/Settings.ts +++ b/src/commands/Settings.ts @@ -55,10 +55,6 @@ export default class Settings { .setRequired(false) ); break; - // case "LIST": - // const subcommandGroup = new SlashCommandSubcommandGroupBuilder() - // .setName(sub) - // .setDescription("This subcommand group has no description."); default: // Also includes "TEXT" subcommand.addStringOption(option => option @@ -114,7 +110,6 @@ export default class Settings { async autocomplete(interaction: AutocompleteInteraction) { if (interaction.type != InteractionType.ApplicationCommandAutocomplete) return; - //if (interaction.options.getSubcommand() != this.data.name) return; switch (Object.keys(settingsDefinition[interaction.options.getSubcommand()])[0]) { case "BOOL": await interaction.respond( diff --git a/src/commands/User.ts b/src/commands/User.ts index fcd5968..246fe6a 100644 --- a/src/commands/User.ts +++ b/src/commands/User.ts @@ -9,7 +9,7 @@ import { type SlashCommandOptionsOnlyBuilder } from "discord.js"; import { genColor } from "../utils/colorGen"; -import { getLevel } from "../utils/database/levelling"; +import { getLevel } from "../utils/database/leveling"; import { getSetting } from "../utils/database/settings"; import { errorEmbed } from "../utils/embeds/errorEmbed"; import { imageColor } from "../utils/imageColor"; @@ -25,14 +25,38 @@ export default class User { async run(interaction: ChatInputCommandInteraction) { const guild = interaction.guild!; - const target = guild.members.cache.get( - interaction.options.getUser("user")?.id ?? interaction.user.id - )!; + const user = interaction.options.getUser("user") ?? interaction.user; + const target = guild.members.cache.get(user.id); + const avatar = target?.displayAvatarURL() ?? user.displayAvatarURL(); + const embedColor = + (await target?.user.fetch())?.hexAccentColor ?? + (await imageColor(undefined, avatar)) ?? + genColor(200); - const user = await target.user.fetch(); + let embed = new EmbedBuilder() + .setAuthor({ + name: `${avatar ? "• " : ""}${user.displayName}`, + iconURL: avatar + }) + .setFields({ + name: `<:discord:1266797021126459423> • Discord info`, + value: [ + `Username is **${user.username}**`, + `Display name is ${ + user.displayName == user.username ? "*not there*" : `**${user.displayName}**` + }`, + `Created on ****` + ].join("\n") + }) + .setFooter({ text: `User ID: ${user.id}` }) + .setThumbnail(avatar) + .setColor(embedColor); + + await interaction.reply({ embeds: [embed] }); + + if (!target) return; let serverInfo = [`Joined on ****`]; const guildRoles = guild.roles.cache.filter(role => target.roles.cache.has(role.id))!; - const avatar = target.displayAvatarURL(); const memberRoles = [...guildRoles].sort( (role1, role2) => role2[1].position - role1[1].position ); @@ -52,35 +76,12 @@ export default class User { .join(", ")}${rolesLength > 3 ? ` and **${rolesLength - 3}** more` : ""}` ); - const embedColor = - user.hexAccentColor ?? (await imageColor(undefined, target)) ?? genColor(200); - - let embed = new EmbedBuilder() - .setAuthor({ - name: `${avatar ? "• " : ""}${target.nickname ?? user.displayName}`, - iconURL: avatar - }) - .setFields( - { - name: `<:discord:1266797021126459423> • Discord info`, - value: [ - `Username is **${user.username}**`, - `Display name is ${ - user.displayName == user.username ? "*not there*" : `**${user.displayName}**` - }`, - `Created on ****` - ].join("\n") - }, - { - name: "📒 • Server info", - value: serverInfo.join("\n") - } - ) - .setFooter({ text: `User ID: ${target.id}` }) - .setThumbnail(avatar) - .setColor(embedColor); + embed.addFields({ + name: "📒 • Server info", + value: serverInfo.join("\n") + }); - const enabled = getSetting(`${guild.id}`, "levelling", "enabled"); + const enabled = getSetting(`${guild.id}`, "leveling", "enabled"); const row = new ActionRowBuilder().addComponents( new ButtonBuilder() .setCustomId("general") @@ -94,13 +95,13 @@ export default class User { .setStyle(ButtonStyle.Primary) ); row.components[0].setDisabled(true); - const reply = await interaction.reply({ + const reply = await interaction.editReply({ embeds: [embed], components: !user.bot ? (enabled ? [row] : []) : [] }); if (!enabled && user.bot) return; - const difficulty = getSetting(guild.id, "levelling", "difficulty") as number; + const difficulty = getSetting(guild.id, "leveling", "difficulty") as number; const [level, xp] = getLevel(guild.id, target.id)!; const nextLevelXp = Math.floor( 100 * difficulty * (level + 1) ** 2 - 85 * difficulty * level ** 2 @@ -117,6 +118,7 @@ export default class User { if (i.user.id != interaction.user.id) return await errorEmbed(i, "You aren't the person who executed this command."); + collector.resetTimer({ time: 60000 }); i.customId == "general" ? row.components[0].setDisabled(true) : row.components[1].setDisabled(true); diff --git a/src/commands/moderation/Ban.ts b/src/commands/moderation/Ban.ts index 7ff47a7..5d2f9b3 100644 --- a/src/commands/moderation/Ban.ts +++ b/src/commands/moderation/Ban.ts @@ -41,7 +41,7 @@ export default class Ban { let expiresAt: number | undefined; if (duration) { const durationMs = ms(duration); - if (!durationMs) + if (!durationMs || durationMs <= 0) return await errorEmbed( interaction, `You can't ban ${user.displayName} temporarily.`, diff --git a/src/commands/moderation/Lock.ts b/src/commands/moderation/Lock.ts index 305ee09..7197865 100644 --- a/src/commands/moderation/Lock.ts +++ b/src/commands/moderation/Lock.ts @@ -70,7 +70,8 @@ export default class Lock { .create(guild.id, { SendMessages: false, SendMessagesInThreads: false, - CreatePublicThreads: false + CreatePublicThreads: false, + CreatePrivateThreads: false }) .catch(error => console.error(error)); diff --git a/src/commands/moderation/Mute.ts b/src/commands/moderation/Mute.ts index 6442573..b32ad71 100644 --- a/src/commands/moderation/Mute.ts +++ b/src/commands/moderation/Mute.ts @@ -41,7 +41,7 @@ export default class Mute { ) return; - if (!ms(duration) || ms(duration) > ms("28d")) + if (!ms(duration) || ms(duration) > ms("28d") || ms(duration) <= 0) return await errorEmbed( interaction, `You can't mute ${user.displayName}.`, diff --git a/src/commands/moderation/Unlock.ts b/src/commands/moderation/Unlock.ts index 403d165..edba7ee 100644 --- a/src/commands/moderation/Unlock.ts +++ b/src/commands/moderation/Unlock.ts @@ -70,7 +70,8 @@ export default class Unlock { .create(guild.id, { SendMessages: null, SendMessagesInThreads: null, - CreatePublicThreads: null + CreatePublicThreads: null, + CreatePrivateThreads: null }) .catch(error => console.error(error)); diff --git a/src/commands/news/View.ts b/src/commands/news/View.ts index 7c42609..1e723f8 100644 --- a/src/commands/news/View.ts +++ b/src/commands/news/View.ts @@ -72,6 +72,7 @@ export default class View { if (i.user.id != interaction.user.id) return await errorEmbed(i, "You aren't the person who executed this command."); + collector.resetTimer({ time: 60000 }); switch (i.customId) { case "left": page--; diff --git a/src/events/easterEggs/amerikaYa.ts b/src/events/easterEggs/amerikaYa.ts index ceffcd2..99ae9a2 100644 --- a/src/events/easterEggs/amerikaYa.ts +++ b/src/events/easterEggs/amerikaYa.ts @@ -2,7 +2,7 @@ import type { Message, TextChannel } from "discord.js"; import { randomise } from "../../utils/randomise"; export default async function run(message: Message) { - if (!message.content.toLowerCase().includes("amerika ya")) return; + if (message.content.toLowerCase() != "amerika ya") return; const response = randomise([ "HALLO :D HALLO :D HALLO :D HALLO :D", "https://tenor.com/view/america-ya-gif-15374592095658975433" diff --git a/src/events/easterEggs/crazy.ts b/src/events/easterEggs/crazy.ts index 278a652..8ded6ec 100644 --- a/src/events/easterEggs/crazy.ts +++ b/src/events/easterEggs/crazy.ts @@ -2,7 +2,8 @@ import type { Message, TextChannel } from "discord.js"; export default async function run(message: Message) { if (!message.content.toLowerCase().includes("crazy")) return; - await (message.channel as TextChannel).send( - "Crazy? I was crazy once.\nThey locked me in a room.\nA rubber room.\nA rubber room with rats.\nAnd rats make me crazy.\nCrazy? I was crazy once..." - ); + if (Math.round(Math.random()) <= 0.5) + await (message.channel as TextChannel).send( + "Crazy? I was crazy once.\nThey locked me in a room.\nA rubber room.\nA rubber room with rats.\nAnd rats make me crazy.\nCrazy? I was crazy once..." + ); } diff --git a/src/events/easterEggs/fan.ts b/src/events/easterEggs/fan.ts index 24a5d95..4d44dc6 100644 --- a/src/events/easterEggs/fan.ts +++ b/src/events/easterEggs/fan.ts @@ -2,7 +2,7 @@ import type { Message, TextChannel } from "discord.js"; import { randomise } from "../../utils/randomise"; export default async function run(message: Message) { - if (!message.content.toLowerCase().includes("i'm a big fan")) return; + if (message.content.toLowerCase() != "i'm a big fan") return; const gifs = randomise([ "https://tenor.com/bC37i.gif", "https://tenor.com/view/fan-gif-20757784", diff --git a/src/events/easterEggs/fire.ts b/src/events/easterEggs/fire.ts index 0918c0b..2a728fc 100644 --- a/src/events/easterEggs/fire.ts +++ b/src/events/easterEggs/fire.ts @@ -2,7 +2,7 @@ import type { Message, TextChannel } from "discord.js"; import { randomise } from "../../utils/randomise"; export default async function run(message: Message) { - if (!message.content.toLowerCase().includes("fire in the hole")) return; + if (message.content.toLowerCase() != "fire in the hole") return; const gifs = randomise([ "https://cdn.discordapp.com/attachments/799130520846991370/1080439680199315487/cat-chomp-fireball.gif?ex=65e8485d&is=65d5d35d&hm=cef8b83df8120f1419082f184d835c6af679c9d02d69f97e335eafa82b33489e&", "https://tenor.com/view/dancing-gif-25178472", diff --git a/src/events/easterEggs/whoPinged.ts b/src/events/easterEggs/whoPinged.ts index 779e5bd..cdd1eb7 100644 --- a/src/events/easterEggs/whoPinged.ts +++ b/src/events/easterEggs/whoPinged.ts @@ -2,7 +2,7 @@ import type { Message, TextChannel } from "discord.js"; import { randomise } from "../../utils/randomise"; export default async function run(message: Message) { - if (!message.content.toLowerCase().includes(`<@${message.client.user.id}>`)) return; + if (message.content.toLowerCase() != `<@${message.client.user.id}>`) return; const gifs = randomise([ "https://tenor.com/view/who-pinged-me-ping-discord-up-opening-door-gif-20065356", "https://tenor.com/view/discord-who-pinged-me-who-pinged-me-gif-25140226", diff --git a/src/events/guildMemberAdd.ts b/src/events/guildMemberAdd.ts index 98ce111..0d2ef0c 100644 --- a/src/events/guildMemberAdd.ts +++ b/src/events/guildMemberAdd.ts @@ -9,12 +9,12 @@ export default (async function run(member) { const guildID = member.guild.id; const id = getSetting(guildID, "welcome", "channel") as string; const user = member.user; - const avatarURL = member.displayAvatarURL(); + const avatar = member.displayAvatarURL(); const embed = new EmbedBuilder() - .setAuthor({ name: `• ${user.displayName} has joined`, iconURL: avatarURL }) + .setAuthor({ name: `• ${user.displayName} has joined`, iconURL: avatar }) .setFooter({ text: `User ID: ${member.id}` }) - .setThumbnail(avatarURL) - .setColor(member.user.hexAccentColor ?? (await imageColor(undefined, member)) ?? genColor(200)); + .setThumbnail(avatar) + .setColor(member.user.hexAccentColor ?? (await imageColor(undefined, avatar)) ?? genColor(200)); if (id) { const channel = (await member.guild.channels.cache diff --git a/src/events/guildMemberRemove.ts b/src/events/guildMemberRemove.ts index 4ca59f3..784f41d 100644 --- a/src/events/guildMemberRemove.ts +++ b/src/events/guildMemberRemove.ts @@ -14,12 +14,12 @@ export default (async function run(member: GuildMember) { .find(channel => channel.id == id) ?.fetch()) as TextChannel; - const avatarURL = member.displayAvatarURL(); + const avatar = member.displayAvatarURL(); const embed = new EmbedBuilder() - .setAuthor({ name: `• ${member.user.displayName} has left`, iconURL: avatarURL }) + .setAuthor({ name: `• ${member.user.displayName} has left`, iconURL: avatar }) .setFooter({ text: `User ID: ${member.id}` }) - .setThumbnail(avatarURL) - .setColor(member.user.hexAccentColor ?? (await imageColor(undefined, member)) ?? genColor(200)); + .setThumbnail(avatar) + .setColor(member.user.hexAccentColor ?? (await imageColor(undefined, avatar)) ?? genColor(200)); replace(member, getSetting(guildID, "welcome", "leave_text") as string, embed); await channel.send({ embeds: [embed] }); diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts index 7a05da6..333e5dc 100644 --- a/src/events/messageCreate.ts +++ b/src/events/messageCreate.ts @@ -3,7 +3,7 @@ import { readdirSync } from "fs"; import { join } from "path"; import { pathToFileURL } from "url"; import { genColor } from "../utils/colorGen"; -import { getLevel, setLevel } from "../utils/database/levelling"; +import { getLevel, setLevel } from "../utils/database/leveling"; import { getSetting } from "../utils/database/settings"; import { kominator } from "../utils/kominator"; import { Event } from "../utils/types"; @@ -15,22 +15,21 @@ export default (async function run(message) { const guild = message.guild!; // Easter egg handler - // i kept the old ID used here (if guild.id == "ID") in my .env file, just in case - if (getSetting(guild.id, "easter", "enabled") == true) { + if (getSetting(guild.id, "easter", "enabled")) { const eventsPath = join(process.cwd(), "src", "events", "easterEggs"); for (const easterEggFile of readdirSync(eventsPath)) (await import(pathToFileURL(join(eventsPath, easterEggFile)).toString())).default(message); } - // Levelling - if (!getSetting(guild.id, "levelling", "enabled")) return; + // Leveling + if (!getSetting(guild.id, "leveling", "enabled")) return; - const blockedChannels = getSetting(guild.id, "levelling", "block_channels") as string; + const blockedChannels = getSetting(guild.id, "leveling", "block_channels") as string; if (blockedChannels != undefined) for (const channelID of kominator(blockedChannels)) if (message.channelId == channelID) return; - const cooldown = getSetting(guild.id, "levelling", "cooldown") as number; + const cooldown = getSetting(guild.id, "leveling", "cooldown") as number; if (cooldown > 0) { const key = `${guild.id}-${author.id}`; const lastExpTime = cooldowns.get(key) || 0; @@ -40,9 +39,9 @@ export default (async function run(message) { else cooldowns.set(key, now); } - const xpGain = getSetting(guild.id, "levelling", "xp_gain") as number; - const levelChannelId = getSetting(guild.id, "levelling", "channel"); - const difficulty = getSetting(guild.id, "levelling", "difficulty") as number; + const xpGain = getSetting(guild.id, "leveling", "xp_gain") as number; + const levelChannelId = getSetting(guild.id, "leveling", "channel"); + const difficulty = getSetting(guild.id, "leveling", "difficulty") as number; const [level, xp] = getLevel(guild.id, author.id); const xpUntilLevelUp = Math.floor( 100 * difficulty * (level + 1) ** 2 - 85 * difficulty * level ** 2 diff --git a/src/utils/colorGen.ts b/src/utils/colorGen.ts index 3532c55..c00e6b0 100644 --- a/src/utils/colorGen.ts +++ b/src/utils/colorGen.ts @@ -3,6 +3,7 @@ * @param hue Color to randomise. * @returns Color in HEX. */ + export function genColor(hue: number) { const h = hue + 15 * Math.random(); let s = 100; @@ -27,6 +28,7 @@ export function genColor(hue: number) { * @param b Blue. * @returns Color in HEX. */ + export function genRGBColor(r: any, g: any, b: any) { r = r.toString(16); g = g.toString(16); diff --git a/src/utils/database/levelling.ts b/src/utils/database/leveling.ts similarity index 75% rename from src/utils/database/levelling.ts rename to src/utils/database/leveling.ts index b39c88d..1a23d53 100644 --- a/src/utils/database/levelling.ts +++ b/src/utils/database/leveling.ts @@ -2,7 +2,7 @@ import { getDatabase } from "."; import { TableDefinition, TypeOfDefinition } from "./types"; const tableDefinition = { - name: "levelling", + name: "leveling", definition: { guild: "TEXT", user: "TEXT", @@ -13,13 +13,13 @@ const tableDefinition = { const database = getDatabase(tableDefinition); -const getQuery = database.query("SELECT * FROM levelling WHERE guild = $1 AND user = $2;"); -const deleteQuery = database.query("DELETE FROM levelling WHERE guild = $1 AND user = $2;"); +const getQuery = database.query("SELECT * FROM leveling WHERE guild = $1 AND user = $2;"); +const deleteQuery = database.query("DELETE FROM leveling WHERE guild = $1 AND user = $2;"); const insertQuery = database.query( - "INSERT INTO levelling (guild, user, level, xp) VALUES (?1, ?2, ?3, ?4);" + "INSERT INTO leveling (guild, user, level, xp) VALUES (?1, ?2, ?3, ?4);" ); -const getGuildQuery = database.query("SELECT * FROM levelling WHERE guild = $1;"); +const getGuildQuery = database.query("SELECT * FROM leveling WHERE guild = $1;"); export function getLevel(guildID: string, userID: string): [number, number] { const res = getQuery.all(guildID, userID) as TypeOfDefinition[]; diff --git a/src/utils/database/settings.ts b/src/utils/database/settings.ts index 270f5e0..d5679f6 100644 --- a/src/utils/database/settings.ts +++ b/src/utils/database/settings.ts @@ -18,32 +18,22 @@ export const settingsDefinition: Record< settings: Record; } > = { - levelling: { + leveling: { description: "Customise the behaviour of the leveling system.", settings: { enabled: { type: "BOOL", - desc: "Enable/disable the levelling system.", + desc: "Enable/disable the leveling system.", val: true }, channel: { type: "TEXT", - desc: "ID of the log channel for levelling-related stuff (i.e someone levelling up)." + desc: "ID of the log channel for leveling-related stuff (i.e someone leveling up)." }, block_channels: { type: "TEXT", desc: "ID(s) of the channels where messages aren't counted, comma separated." }, - // set_level: { type: "TEXT", desc: "Set the level of a user." }, - // add_multiplier: { - // type: "LIST", - // desc: "Add an XP multiplier to the levelling system.", - // val: { - // multiplier: { type: "INTEGER", desc: "Set the XP multiplier for the role/channel." }, - // role_channel: { type: "TEXT", desc: "Role or channel. (choose)" }, - // id: { type: "TEXT", desc: "ID of the role/channel." } - // } - // }, xp_gain: { type: "INTEGER", desc: "Set the amount of XP a user gains per message.", @@ -62,7 +52,7 @@ export const settingsDefinition: Record< } }, moderation: { - description: "Tweak Sokora's moderation-related logs.", + description: "Change where Sokora sends moderation logs.", settings: { channel: { type: "TEXT", @@ -76,7 +66,7 @@ export const settingsDefinition: Record< } }, news: { - description: "Configure news for this server.", + description: "Configure news for your server.", settings: { channel_id: { type: "TEXT", @@ -104,7 +94,7 @@ export const settingsDefinition: Record< } }, welcome: { - description: "Tweak how Sokora welcomes your new users", + description: "Change how Sokora welcomes your new users.", settings: { join_text: { type: "TEXT", @@ -130,12 +120,12 @@ export const settingsDefinition: Record< } }, easter: { - description: "Enable or disable easter eggs", + description: "Enable/disable easter eggs.", settings: { enabled: { type: "BOOL", desc: "Whether or not the bot should reply to certain messages with 'easter egg' messages.", - val: true + val: false } } } diff --git a/src/utils/embeds/modEmbed.ts b/src/utils/embeds/modEmbed.ts index e703e8f..e7ada38 100644 --- a/src/utils/embeds/modEmbed.ts +++ b/src/utils/embeds/modEmbed.ts @@ -1,4 +1,4 @@ -import { EmbedBuilder, inlineCode, type ChatInputCommandInteraction, type User } from "discord.js"; +import { EmbedBuilder, type ChatInputCommandInteraction, type User } from "discord.js"; import ms from "ms"; import { genColor } from "../colorGen"; import { addModeration, type modType } from "../database/moderation"; diff --git a/src/utils/embeds/serverEmbed.ts b/src/utils/embeds/serverEmbed.ts index 0e48fea..fd0b716 100644 --- a/src/utils/embeds/serverEmbed.ts +++ b/src/utils/embeds/serverEmbed.ts @@ -25,7 +25,7 @@ export async function serverEmbed(options: Options) { ).size; const bots = members.filter(member => member.user.bot); const formattedUserCount = (guild.memberCount - bots.size)?.toLocaleString("en-US"); - const icon = guild.iconURL(); + const icon = guild.iconURL()!; const roles = guild.roles.cache; const sortedRoles = [...roles].sort((role1, role2) => role2[1].position - role1[1].position); @@ -48,13 +48,13 @@ export async function serverEmbed(options: Options) { const embed = new EmbedBuilder() .setAuthor({ name: `${pages ? `#${page} • ` : icon ? "• " : ""}${guild.name}`, - iconURL: icon! + iconURL: icon }) .setDescription(guild.description ? guild.description : null) .setFields({ name: "📃 • General", value: generalValues.join("\n") }) .setFooter({ text: `${pages ? `Page ${page}/${pages}\n` : ""}Server ID: ${guild.id}` }) .setThumbnail(icon) - .setColor((await imageColor(guild)) ?? genColor(200)); + .setColor((await imageColor(icon)) ?? genColor(200)); if (options.roles) embed.addFields({ diff --git a/src/utils/imageColor.ts b/src/utils/imageColor.ts index d5ea6cb..4a14ff0 100644 --- a/src/utils/imageColor.ts +++ b/src/utils/imageColor.ts @@ -1,8 +1,3 @@ -import type { ColorResolvable, Guild, GuildMember, User } from "discord.js"; -import Vibrant from "node-vibrant"; -import sharp from "sharp"; -import { genRGBColor } from "./colorGen"; - /** * Outputs the most vibrant color from the image. * @param guild Guild image. @@ -10,12 +5,13 @@ import { genRGBColor } from "./colorGen"; * @returns The color in HEX. */ -export async function imageColor(guild?: Guild, member?: GuildMember | User) { - const guildURL = guild?.iconURL(); - const memberURL = member?.displayAvatarURL(); - if (!guildURL || !memberURL) return; +import type { ColorResolvable } from "discord.js"; +import Vibrant from "node-vibrant"; +import sharp from "sharp"; +import { genRGBColor } from "./colorGen"; - const imageBuffer = await (await fetch(guild ? guildURL : memberURL)).arrayBuffer(); +export async function imageColor(guildURL?: string, memberURL?: string) { + const imageBuffer = await (await fetch(guildURL! ?? memberURL)).arrayBuffer(); const { r, g, b } = ( await new Vibrant(await sharp(imageBuffer).toFormat("jpg").toBuffer()).getPalette() ).Vibrant!; diff --git a/src/utils/kominator.ts b/src/utils/kominator.ts index 55a7e29..b1c8298 100644 --- a/src/utils/kominator.ts +++ b/src/utils/kominator.ts @@ -3,6 +3,7 @@ * @param string String to split. * @returns An array of strings from the original string. */ + export function kominator(string: string): string[] { - return string.split(",").map(str => str.replace("\"", "").trim()); + return string.split(",").map(str => str.replace('"', "").trim()); } diff --git a/src/utils/logChannel.ts b/src/utils/logChannel.ts index 27e37b3..a733617 100644 --- a/src/utils/logChannel.ts +++ b/src/utils/logChannel.ts @@ -1,3 +1,10 @@ +/** + * Sends a message in the log channel. (if there is one set) + * @param guild The guild where the log channel is located. + * @param embed Embed of the log. + * @returns Log message. + */ + import { ChannelType, type Channel, @@ -7,12 +14,6 @@ import { } from "discord.js"; import { getSetting } from "./database/settings"; -/** - * Sends a message in the log channel. (if there is one set) - * @param guild The guild where the log channel is located. - * @param embed Embed of the log. - * @returns Log message. - */ export async function logChannel(guild: Guild, embed: EmbedBuilder) { const logChannel = getSetting(guild.id, "moderation", "channel"); if (!logChannel) return; diff --git a/src/utils/multiReact.ts b/src/utils/multiReact.ts index 690c284..dfbf46e 100644 --- a/src/utils/multiReact.ts +++ b/src/utils/multiReact.ts @@ -1,10 +1,11 @@ -import type { Message } from "discord.js"; - /** * Reacts to a message with multiple emojis. * @param message Message to react to. * @param emojis Emojis that will be used to react. */ + +import type { Message } from "discord.js"; + export async function multiReact(message: Message, ...emojis: string[]) { for (const i of emojis) { if (typeof i == "object") { diff --git a/src/utils/sendChannelNews.ts b/src/utils/sendChannelNews.ts index c93d5fb..b45eb2c 100644 --- a/src/utils/sendChannelNews.ts +++ b/src/utils/sendChannelNews.ts @@ -1,3 +1,13 @@ +/** + * Sends news to a channel. + * @param guild Guild where the channel is in. + * @param id ID of the news. + * @param interaction Command nteraction. + * @param title Title of the news. + * @param body Content of the news. + * @returns News message in a channel. + */ + import { EmbedBuilder, type ChatInputCommandInteraction, @@ -9,15 +19,6 @@ import { genColor } from "./colorGen"; import { get, updateNews } from "./database/news"; import { getSetting } from "./database/settings"; -/** - * Sends news to a channel. - * @param guild Guild where the channel is in. - * @param id ID of the news. - * @param interaction Command nteraction. - * @param title Title of the news. - * @param body Content of the news. - * @returns News message in a channel. - */ export async function sendChannelNews( guild: Guild, id: string, From 0ccfcbe3bec20a406434b917a0d6fbef84eb35e7 Mon Sep 17 00:00:00 2001 From: MrSerge01 <86667481+MrSerge01@users.noreply.github.com> Date: Sun, 3 Nov 2024 21:57:14 +0500 Subject: [PATCH 126/127] quick fix --- src/commands/moderation/Ban.ts | 7 +++++++ src/commands/moderation/Kick.ts | 8 ++++++++ src/commands/moderation/Unban.ts | 4 +--- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/commands/moderation/Ban.ts b/src/commands/moderation/Ban.ts index 5d2f9b3..0dbc4de 100644 --- a/src/commands/moderation/Ban.ts +++ b/src/commands/moderation/Ban.ts @@ -38,6 +38,13 @@ export default class Ban { ) return; + if ((await guild.bans.fetch()).get(user.id)) + return await errorEmbed( + interaction, + `You can't ban ${user.displayName}.`, + "This user is already banned." + ); + let expiresAt: number | undefined; if (duration) { const durationMs = ms(duration); diff --git a/src/commands/moderation/Kick.ts b/src/commands/moderation/Kick.ts index e302404..c67bae8 100644 --- a/src/commands/moderation/Kick.ts +++ b/src/commands/moderation/Kick.ts @@ -3,6 +3,7 @@ import { SlashCommandSubcommandBuilder, type ChatInputCommandInteraction } from "discord.js"; +import { errorEmbed } from "../../utils/embeds/errorEmbed"; import { errorCheck, modEmbed } from "../../utils/embeds/modEmbed"; export default class Kick { @@ -31,6 +32,13 @@ export default class Kick { ) return; + if (!interaction.guild?.members.cache.get(user.id)) + return await errorEmbed( + interaction, + `You can't kick ${user.displayName}.`, + "This user is not in the server." + ); + const reason = interaction.options.getString("reason"); await interaction.guild?.members.cache .get(user.id) diff --git a/src/commands/moderation/Unban.ts b/src/commands/moderation/Unban.ts index 922d1f3..a8d5d8e 100644 --- a/src/commands/moderation/Unban.ts +++ b/src/commands/moderation/Unban.ts @@ -27,9 +27,7 @@ export default class Unban { const id = interaction.options.getString("id")!; const reason = interaction.options.getString("reason")!; const guild = interaction.guild!; - const target = (await guild.bans.fetch()) - .map(ban => ban.user) - .filter(user => user.id == id)[0]!; + const target = (await guild.bans.fetch()).get(id)?.user!; if ( await errorCheck( From 1fef7e724e8fb9eddd9dd226d83d7063dc129111 Mon Sep 17 00:00:00 2001 From: MrSerge01 <86667481+MrSerge01@users.noreply.github.com> Date: Sun, 3 Nov 2024 22:24:45 +0500 Subject: [PATCH 127/127] (fix) 60 -> 30s, imageColor fix, user nickname --- src/commands/Leaderboard.ts | 4 ++-- src/commands/Serverboard.ts | 4 ++-- src/commands/User.ts | 6 +++--- src/commands/news/View.ts | 4 ++-- src/utils/imageColor.ts | 4 +++- 5 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/commands/Leaderboard.ts b/src/commands/Leaderboard.ts index bbedffc..fa778a5 100644 --- a/src/commands/Leaderboard.ts +++ b/src/commands/Leaderboard.ts @@ -82,7 +82,7 @@ export default class Leaderboard { if (totalPages > 1) { const collector = reply.createMessageComponentCollector({ - time: 60000 + time: 30000 }); collector.on("collect", async (i: ButtonInteraction) => { @@ -95,7 +95,7 @@ export default class Leaderboard { if (i.user.id != interaction.user.id) return errorEmbed(i, "You are not the person who executed this command."); - collector.resetTimer({ time: 60000 }); + collector.resetTimer({ time: 30000 }); if (i.customId == "left") page = page > 1 ? page - 1 : totalPages; else page = page < totalPages ? page + 1 : 1; diff --git a/src/commands/Serverboard.ts b/src/commands/Serverboard.ts index 30323ac..cef27aa 100644 --- a/src/commands/Serverboard.ts +++ b/src/commands/Serverboard.ts @@ -59,7 +59,7 @@ export default class Serverboard { }); if (pages == 1) return; - const collector = reply.createMessageComponentCollector({ time: 60000 }); + const collector = reply.createMessageComponentCollector({ time: 30000 }); collector.on("collect", async (i: ButtonInteraction) => { if (i.message.id != (await reply.fetch()).id) return await errorEmbed( @@ -70,7 +70,7 @@ export default class Serverboard { if (i.user.id != interaction.user.id) return await errorEmbed(i, "You aren't the person who executed this command."); - collector.resetTimer({ time: 60000 }); + collector.resetTimer({ time: 30000 }); switch (i.customId) { case "left": page--; diff --git a/src/commands/User.ts b/src/commands/User.ts index 246fe6a..05c0c47 100644 --- a/src/commands/User.ts +++ b/src/commands/User.ts @@ -35,7 +35,7 @@ export default class User { let embed = new EmbedBuilder() .setAuthor({ - name: `${avatar ? "• " : ""}${user.displayName}`, + name: `${avatar ? "• " : ""}${target?.nickname ?? user.displayName}`, iconURL: avatar }) .setFields({ @@ -107,7 +107,7 @@ export default class User { 100 * difficulty * (level + 1) ** 2 - 85 * difficulty * level ** 2 )?.toLocaleString("en-US"); - const collector = reply.createMessageComponentCollector({ time: 60000 }); + const collector = reply.createMessageComponentCollector({ time: 30000 }); collector.on("collect", async (i: ButtonInteraction) => { if (i.message.id != (await reply.fetch()).id) return await errorEmbed( @@ -118,7 +118,7 @@ export default class User { if (i.user.id != interaction.user.id) return await errorEmbed(i, "You aren't the person who executed this command."); - collector.resetTimer({ time: 60000 }); + collector.resetTimer({ time: 30000 }); i.customId == "general" ? row.components[0].setDisabled(true) : row.components[1].setDisabled(true); diff --git a/src/commands/news/View.ts b/src/commands/news/View.ts index 1e723f8..bfc2da4 100644 --- a/src/commands/news/View.ts +++ b/src/commands/news/View.ts @@ -61,7 +61,7 @@ export default class View { ); const reply = await interaction.reply({ embeds: [getEmbed()], components: [row] }); - const collector = reply.createMessageComponentCollector({ time: 60000 }); + const collector = reply.createMessageComponentCollector({ time: 30000 }); collector.on("collect", async (i: ButtonInteraction) => { if (i.message.id != (await reply.fetch()).id) return await errorEmbed( @@ -72,7 +72,7 @@ export default class View { if (i.user.id != interaction.user.id) return await errorEmbed(i, "You aren't the person who executed this command."); - collector.resetTimer({ time: 60000 }); + collector.resetTimer({ time: 30000 }); switch (i.customId) { case "left": page--; diff --git a/src/utils/imageColor.ts b/src/utils/imageColor.ts index 4a14ff0..22a162c 100644 --- a/src/utils/imageColor.ts +++ b/src/utils/imageColor.ts @@ -11,7 +11,9 @@ import sharp from "sharp"; import { genRGBColor } from "./colorGen"; export async function imageColor(guildURL?: string, memberURL?: string) { - const imageBuffer = await (await fetch(guildURL! ?? memberURL)).arrayBuffer(); + if (!guildURL || !memberURL) return; + + const imageBuffer = await (await fetch(guildURL ?? memberURL)).arrayBuffer(); const { r, g, b } = ( await new Vibrant(await sharp(imageBuffer).toFormat("jpg").toBuffer()).getPalette() ).Vibrant!;