diff --git a/.gitignore b/.gitignore index 200aebb..0898602 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ node_modules/ .DS_Store data.db .idea +.vscode/ diff --git a/.prettierrc.json b/.prettierrc.json index 9d5a120..0f4a047 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -2,6 +2,6 @@ "tabWidth": 2, "useTabs": false, "arrowParens": "avoid", - "trailingComma": "none", + "trailingComma": "es5", "printWidth": 100 } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fa5e502..2ccc236 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,25 +1,44 @@ # Sokora Contributing Guide ## Prerequisites + - Basic knowledge of [TypeScript](https://typescriptlang.org/) and [discord.js](https://discord.js.org/). - [Bun](https://bun.sh) installed. ## Get started with contributing + ### Getting the code + - Make a fork of this repository. - Clone your fork. ### Creating your bot + - Head over to the [Discord Developer Portal](https://discord.com/developers/applications) and make a new application. - Invite your bot to your server. - Reset and then copy your bot's token. ### Setting up .env -- Run `bun run setup` and our cli tool will install dependencies and write .env for you + +- Run `bun run setup` and our CLI tool will install dependencies and write .env for you. It'll ask you to paste in your bot's token. ### Running + - Run `bun dev`. Be sure to open a pull request when you're ready to push your changes. Be descriptive of the changes you've made. -![](https://user-images.githubusercontent.com/51555391/176925763-cdfd57ba-ae1e-4bf3-85e9-b3ebd30b1d59.png) +## Code style guidelines + +A few, simple guidelines onto how code contributed to Sokora should look like. + +- Keep a consistent indentation of two spaces. Don't use tabs. +- Use `K&R` style for bracket placement (`function() {}` instead of `function() \n {}`). +- Use `camelCase` for both variables and function names. +- Keep lines reasonably short, don't fear linebreaks. Of course, longer lines are valid where needed. +- Use early returns to avoid nesting. +- Avoid curly braces in one-line `if` statements. +- Non-nullish assertions are valid when needed. +- Use the cache instead of a new `fetch()` call where possible, to avoid unnecessary usage (e.g., if possible, prefer `guild.members.cache` over `await guild.members.fetch()`). + +![PLEASE SUBMIT A PR, NO DIRECT COMMITS](https://user-images.githubusercontent.com/51555391/176925763-cdfd57ba-ae1e-4bf3-85e9-b3ebd30b1d59.png) diff --git a/README.md b/README.md index d0ce211..94d1b05 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,10 @@ # About + Sokora is a multipurpose Discord bot that lets you manage your servers easily. **Please note that Sokora is currently unstable so it might have issues.** # Contributing + While we're developing the bot, you can [help us](CONTRIBUTING.MD) if you find any bugs. diff --git a/bun.lock b/bun.lock index f3941dd..3280928 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,5 @@ { - // Note: this file must be committed for package version consisency for everyone who uses bun install + // Note: this file must be committed for package version consistency for everyone who uses bun install "lockfileVersion": 1, "workspaces": { "": { diff --git a/src/commands/about.ts b/src/commands/about.ts index 43be4ba..32f2f2e 100644 --- a/src/commands/about.ts +++ b/src/commands/about.ts @@ -42,7 +42,7 @@ export async function run(interaction: ChatInputCommandInteraction) { { name: "🔗 • Links", value: [ - "[Discord](https://discord.gg/c6C25P4BuY) • [GitHub](https://www.github.com/SokoraDesu) • [YouTube](https://www.youtube.com/@SokoraDesu) • [Instagram](https://instagram.com/NebulaTheBot) • [Mastodon](https://mastodon.online/@NebulaTheBot@mastodon.social) • [Matrix](https://matrix.to/#/#sokora:matrix.org) • [Revolt](https://rvlt.gg/28TS9aXy)", + "[Discord](https://discord.gg/c6C25P4BuY) • [GitHub](https://www.github.com/SokoraDesu) • [YouTube](https://www.youtube.com/@SokoraDesu) • [Mastodon](https://mastodon.online/@NebulaTheBot@mastodon.social) • [Matrix](https://matrix.to/#/#sokora:matrix.org) • [Revolt](https://rvlt.gg/28TS9aXy)", "Also, please read the [ToS](https://sokora.org/terms) and the [privacy policy](https://sokora.org/privacy)." ].join("\n") } diff --git a/src/commands/games/rps.ts b/src/commands/games/rps.ts index 96aecef..e5c14bb 100644 --- a/src/commands/games/rps.ts +++ b/src/commands/games/rps.ts @@ -4,6 +4,7 @@ import { ButtonInteraction, ButtonStyle, EmbedBuilder, + MessageFlags, SlashCommandSubcommandBuilder, type ChatInputCommandInteraction } from "discord.js"; @@ -74,7 +75,7 @@ export async function run(interaction: ChatInputCommandInteraction) { collector.on("collect", async (i: ButtonInteraction) => { playerChoices.set(i.user.id, i.customId.split("_")[1] as RPSChoice); - await i.reply({ content: "Choice recorded!", ephemeral: true }); + await i.reply({ content: "Choice recorded!", flags: MessageFlags.Ephemeral }); if (playerChoices.size == 2) collector.stop("game-complete"); }); diff --git a/src/commands/math/graph.ts b/src/commands/math/graph.ts index 3414713..89e70ca 100644 --- a/src/commands/math/graph.ts +++ b/src/commands/math/graph.ts @@ -7,6 +7,7 @@ import { } from "discord.js"; import * as math from "mathjs"; import { errorEmbed } from "../../utils/embeds/errorEmbed"; +import { genColor, genRGBColor } from "../../utils/colorGen"; export const data = new SlashCommandSubcommandBuilder() .setName("graph") @@ -58,7 +59,7 @@ export async function run(interaction: ChatInputCommandInteraction) { ctx.stroke(); ctx.strokeStyle = "#ff0000"; - ctx.lineWidth = 2; + ctx.lineWidth = 3; ctx.beginPath(); const points = 1000; @@ -89,16 +90,16 @@ export async function run(interaction: ChatInputCommandInteraction) { const attachment = new AttachmentBuilder(canvas.toBuffer(), { name: "graph.png" }); const embed = new EmbedBuilder() .setTitle("Function Graph") - .setDescription(`f(x) = ${func}`) + .setDescription(`\`f(x) = ${func}\``) .setImage("attachment://graph.png") - .setColor("#00ff00"); + .setColor(genColor(100)); await interaction.reply({ embeds: [embed], files: [attachment] }); } catch (error) { return await errorEmbed( interaction, "Invalid function", - "Please provide a valid mathematical function. Examples: 'x^2', 'sin(x)', '2*x + 1'" + "Please provide a valid mathematical function. Examples: 'x^2', 'sin(x)', '2*x + 1'." ); } } diff --git a/src/commands/news/add.ts b/src/commands/news/add.ts index 33a3cbd..bb72e05 100644 --- a/src/commands/news/add.ts +++ b/src/commands/news/add.ts @@ -1,6 +1,7 @@ import { ActionRowBuilder, EmbedBuilder, + MessageFlags, ModalBuilder, SlashCommandSubcommandBuilder, TextInputBuilder, @@ -68,7 +69,7 @@ export async function run(interaction: ChatInputCommandInteraction) { await sendChannelNews(guild, id, interaction).catch(err => console.error(err)); await i.reply({ embeds: [new EmbedBuilder().setTitle("News added.").setColor(genColor(100))], - ephemeral: true + flags: MessageFlags.Ephemeral }); }); } diff --git a/src/commands/news/edit.ts b/src/commands/news/edit.ts index 32f75b8..298fbbb 100644 --- a/src/commands/news/edit.ts +++ b/src/commands/news/edit.ts @@ -1,6 +1,7 @@ import { ActionRowBuilder, EmbedBuilder, + MessageFlags, ModalBuilder, SlashCommandSubcommandBuilder, TextInputBuilder, @@ -93,7 +94,7 @@ export async function run(interaction: ChatInputCommandInteraction) { updateNews(guild.id, id, title, body); await i.reply({ embeds: [new EmbedBuilder().setTitle("News edited.").setColor(genColor(100))], - ephemeral: true + flags: MessageFlags.Ephemeral }); }); } diff --git a/src/commands/news/remove.ts b/src/commands/news/remove.ts index cdf8b84..eb46b7c 100644 --- a/src/commands/news/remove.ts +++ b/src/commands/news/remove.ts @@ -1,5 +1,6 @@ import { EmbedBuilder, + MessageFlags, SlashCommandSubcommandBuilder, TextChannel, type ChatInputCommandInteraction @@ -40,6 +41,6 @@ export async function run(interaction: ChatInputCommandInteraction) { deleteNews(guild.id, id); await interaction.reply({ embeds: [new EmbedBuilder().setTitle("News removed.").setColor(genColor(100))], - ephemeral: true + flags: MessageFlags.Ephemeral }); } diff --git a/src/commands/serverboard.ts b/src/commands/serverboard.ts index e84222a..551d143 100644 --- a/src/commands/serverboard.ts +++ b/src/commands/serverboard.ts @@ -3,8 +3,9 @@ import { ButtonBuilder, ButtonInteraction, ButtonStyle, + Guild, SlashCommandBuilder, - type ChatInputCommandInteraction + type ChatInputCommandInteraction, } from "discord.js"; import { listPublicServers } from "../utils/database/settings"; import { errorEmbed } from "../utils/embeds/errorEmbed"; @@ -16,9 +17,17 @@ export const data = new SlashCommandBuilder() .addNumberOption(number => number.setName("page").setDescription("The page you want to see.")); export async function 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: { guild: Guild; showInvite: boolean; inviteChannelId: string | null }[] = ( + await Promise.all( + listPublicServers().map(async entry => { + return { + guild: await interaction.client.guilds.fetch(entry.guildID), + showInvite: entry.showInvite, + inviteChannelId: entry.inviteChannelId, + }; + }) + ) + ).sort((a, b) => b.guild.memberCount - a.guild.memberCount); const pages = guildList.length; if (!pages) @@ -32,7 +41,16 @@ export async function run(interaction: ChatInputCommandInteraction) { 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 }); + return await serverEmbed({ + guild: guildList[page].guild, + invite: { + show: guildList[page].showInvite, + channel: guildList[page].inviteChannelId, + }, + page: page + 1, + pages, + roles: false, + }); } const row = new ActionRowBuilder().addComponents( @@ -48,7 +66,7 @@ export async function run(interaction: ChatInputCommandInteraction) { const reply = await interaction.reply({ embeds: [await getEmbed()], - components: pages != 1 ? [row] : [] + components: pages != 1 ? [row] : [], }); if (pages == 1) return; diff --git a/src/commands/settings.ts b/src/commands/settings.ts index 493b87c..823a442 100644 --- a/src/commands/settings.ts +++ b/src/commands/settings.ts @@ -5,17 +5,18 @@ import { PermissionsBitField, SlashCommandBuilder, SlashCommandSubcommandBuilder, - type ChatInputCommandInteraction + type ChatInputCommandInteraction, } from "discord.js"; import { genColor } from "../utils/colorGen"; import { getSetting, setSetting, settingsDefinition, - settingsKeys + settingsKeys, } from "../utils/database/settings"; import { errorEmbed } from "../utils/embeds/errorEmbed"; import { capitalize } from "../utils/capitalize"; +import { humanizeSettings } from "../utils/humanizeSettings"; export let data = new SlashCommandBuilder() .setName("settings") @@ -94,7 +95,7 @@ export async function run(interaction: ChatInputCommandInteraction) { const key = interaction.options.getSubcommand() as keyof typeof settingsDefinition; const values = interaction.options.data[0].options!; const settingsDef = settingsDefinition[key]; - const settingText = (name: string) => { + const settingText = (name: string): string => { const setting = getSetting(guild.id, key, name)?.toString(); let text; switch (settingsDef.settings[name].type) { @@ -108,7 +109,7 @@ export async function run(interaction: ChatInputCommandInteraction) { text = setting ? `<@&${setting}>` : "Not set"; break; default: - text = setting || "Not set"; + text = setting || "*Not set*"; break; } return text; @@ -118,7 +119,14 @@ export async function run(interaction: ChatInputCommandInteraction) { const embed = new EmbedBuilder() .setAuthor({ name: `${capitalize(key)} settings` }) .setDescription( - Object.keys(settingsDef.settings).map(setting => `${settingsDef.settings[setting].emoji} **• ${capitalize(setting.replaceAll("_", " "))}**: ${settingText(setting)}`).join("\n") + Object.keys(settingsDef.settings) + .map( + setting => + `${settingsDef.settings[setting].emoji} **• ${humanizeSettings( + capitalize(setting) + )}**: ${humanizeSettings(settingText(setting))}` + ) + .join("\n") ) .setColor(genColor(100)); @@ -127,9 +135,9 @@ export async function run(interaction: ChatInputCommandInteraction) { const embed = new EmbedBuilder() .setColor(genColor(100)) - .setAuthor({ name: `✅ • ${capitalize(key)} settings changed` }) + .setAuthor({ name: `✅ • ${capitalize(key)} settings changed` }); - let description = "" + let description = ""; for (let i = 0; i < values.length; i++) { const option = values[i]; @@ -147,9 +155,11 @@ export async function run(interaction: ChatInputCommandInteraction) { ); setSetting(guild.id, key, option.name, option.value as string); - description += `**${capitalize(option.name)}:** ${settingText(option.name.toString()!)}\n` + description += `**${humanizeSettings(capitalize(option.name))}:** ${humanizeSettings( + settingText(option.name.toString()) + )}\n`; } - embed.setDescription(description) + embed.setDescription(description); await interaction.reply({ embeds: [embed] }); } @@ -161,7 +171,7 @@ export async function autocomplete(interaction: AutocompleteInteraction) { await interaction.respond( ["true", "false"].map(choice => ({ name: choice, - value: choice + value: choice, })) ); break; diff --git a/src/events/messageDelete.ts b/src/events/messageDelete.ts index 9051681..b00391d 100644 --- a/src/events/messageDelete.ts +++ b/src/events/messageDelete.ts @@ -16,7 +16,10 @@ export default (async function run(message) { name: `• ${author.displayName}'s message has been deleted.`, iconURL: author.displayAvatarURL() }) - .setDescription(`[Jump to message](${message.url})`) + .setDescription( + `[Jump to message](${message.url}) • [See ${author.displayName}'s profile](https://discord.com/users/${author.id})` + ) + .setTimestamp(new Date()) .addFields({ name: "🗑️ • Deleted message", value: message.content! diff --git a/src/events/messageUpdate.ts b/src/events/messageUpdate.ts index e900a52..6d75dfc 100644 --- a/src/events/messageUpdate.ts +++ b/src/events/messageUpdate.ts @@ -20,7 +20,10 @@ export default (async function run(oldMessage, newMessage) { name: `• ${author.displayName} edited a message.`, iconURL: author.displayAvatarURL() }) - .setDescription(`[Jump to message](${oldMessage.url})`) + .setDescription( + `[Jump to message](${oldMessage.url}) • [See ${author.displayName}'s profile](https://discord.com/users/${author.id})` + ) + .setTimestamp(new Date()) .addFields( { name: "🖋️ • Old message", diff --git a/src/utils/capitalize.ts b/src/utils/capitalize.ts index 9c2d7cf..8ae43a8 100644 --- a/src/utils/capitalize.ts +++ b/src/utils/capitalize.ts @@ -3,7 +3,6 @@ * @param string String, the first letter of which should be capitalized. */ -export function capitalize(string: string) { - if (!string) return; +export function capitalize(string: string): string { return `${string.charAt(0).toUpperCase()}${string.slice(1)}`; } diff --git a/src/utils/database/settings.ts b/src/utils/database/settings.ts index 61da872..74076d9 100644 --- a/src/utils/database/settings.ts +++ b/src/utils/database/settings.ts @@ -7,19 +7,19 @@ const tableDefinition = { definition: { guildID: "TEXT", key: "TEXT", - value: "TEXT" - } + value: "TEXT", + }, } satisfies TableDefinition; export const settingsDefinition: Record< string, { description: string; - settings: Record; + settings: Record; } > = { leveling: { - description: "Customise the behaviour of the leveling system.", + description: "Customize the behavior of the leveling system.", settings: { enabled: { type: "BOOL", @@ -131,8 +131,8 @@ export const settingsDefinition: Record< desc: "Delay before autokicking is triggered", val: false, emoji: "🍍", - } - } + }, + }, }, news: { description: "Configure news for your server.", @@ -164,8 +164,8 @@ export const settingsDefinition: Record< desc: "Allow users to receive news in DMs.", val: false, emoji: "🍍", - } - } + }, + }, }, starboard: { description: "Configure the starboard system.", @@ -192,8 +192,8 @@ export const settingsDefinition: Record< desc: "Reactions needed for a message to be starred.", val: 3, emoji: "🍍", - } - } + }, + }, }, serverboard: { description: "Configure your server's appearance on the serverboard.", @@ -209,8 +209,13 @@ export const settingsDefinition: Record< desc: "Whether to show server invite on the serverboard.", val: false, emoji: "🍍", - } - } + }, + invite_channel: { + type: "CHANNEL", + desc: "Channel for the invite. If unset, if a rules channel exists uses it, hides the invite otherwise.", + emoji: "🍍", + }, + }, }, welcome: { description: "Change how Sokora welcomes your new users.", @@ -254,8 +259,8 @@ export const settingsDefinition: Record< type: "TEXT", desc: "Roles to exclude from retention (comma-separated IDs)", emoji: "🍍", - } - } + }, + }, }, easter: { description: "Enable/disable easter eggs.", @@ -270,8 +275,8 @@ export const settingsDefinition: Record< type: "TEXT", desc: "Channel IDs where easter eggs are allowed (comma-separated).", emoji: "🍍", - } - } + }, + }, }, commands: { description: "Configure command availability.", @@ -280,8 +285,8 @@ export const settingsDefinition: Record< type: "TEXT", desc: "Disabled commands (comma-separated names).", emoji: "🍍", - } - } + }, + }, }, currency: { description: "Configure the multi-currency system.", @@ -303,9 +308,9 @@ export const settingsDefinition: Record< desc: "Name of the secondary currency.", val: "gems", emoji: "🍍", - } - } - } + }, + }, + }, }; export const settingsKeys = Object.keys(settingsDefinition) as (keyof typeof settingsDefinition)[]; @@ -314,6 +319,9 @@ const getQuery = database.query("SELECT * FROM settings WHERE guildID = $1 AND k const listPublicQuery = database.query( "SELECT * FROM settings WHERE key = 'serverboard.shown' AND value = '1';" ); +const listPublicWithInvitesEnabledQuery = database.query( + "SELECT * FROM settings WHERE EXISTS (SELECT 1 FROM settings WHERE key = 'serverboard.server_invite' AND value = '1') AND EXISTS (SELECT 1 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( "INSERT INTO settings (guildID, key, value) VALUES (?1, ?2, ?3);" @@ -369,8 +377,30 @@ export function setSetting( insertQuery.run(JSON.stringify(guildID), `${key}.${setting}`, value); } -export function listPublicServers() { - return (listPublicQuery.all() as TypeOfDefinition[]).map(entry => - JSON.parse(entry.guildID) +export function listPublicServers(): { + guildID: string; + showInvite: boolean; + inviteChannelId: string | null; +}[] { + const publicGuildSet = new Set( + (listPublicQuery.all() as TypeOfDefinition[]).map(entry => + JSON.parse(entry.guildID) + ) ); + // you know that time-complexity thingy? idk much but uh an array has O(n) and a JS Set() has O(1) which should mean using a Set is more performant + const inviteGuildsSet = new Set( + (listPublicWithInvitesEnabledQuery.all() as TypeOfDefinition[]).map( + entry => JSON.parse(entry.guildID) + ) + ); + + return Array.from(publicGuildSet).map(entry => { + const inviteChannel = getSetting(entry, "serverboard", "invite_channel"); + + return { + guildID: entry, + showInvite: inviteGuildsSet.has(entry), + inviteChannelId: inviteChannel ? inviteChannel.toString() : null, + }; + }); } diff --git a/src/utils/embeds/errorEmbed.ts b/src/utils/embeds/errorEmbed.ts index b4d60ef..513eb48 100644 --- a/src/utils/embeds/errorEmbed.ts +++ b/src/utils/embeds/errorEmbed.ts @@ -1,4 +1,4 @@ -import { EmbedBuilder, type ButtonInteraction, type ChatInputCommandInteraction } from "discord.js"; +import { EmbedBuilder, MessageFlags, type ButtonInteraction, type ChatInputCommandInteraction } from "discord.js"; import { genColor } from "../colorGen"; /** @@ -21,6 +21,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 }); + if (interaction.replied) return await interaction.followUp({ embeds: [embed], flags: MessageFlags.Ephemeral }); + return await interaction.reply({ embeds: [embed], flags: MessageFlags.Ephemeral }); } diff --git a/src/utils/embeds/modEmbed.ts b/src/utils/embeds/modEmbed.ts index a8cb820..dd39846 100644 --- a/src/utils/embeds/modEmbed.ts +++ b/src/utils/embeds/modEmbed.ts @@ -2,7 +2,7 @@ import { EmbedBuilder, type PermissionResolvable, type ChatInputCommandInteraction, - type User + type User, } from "discord.js"; import ms from "ms"; import { genColor } from "../colorGen"; @@ -89,7 +89,9 @@ export async function errorCheck( return await errorEmbed( interaction, `You can't ${action.toLowerCase()} ${name}.`, - `The member has ${highestModPos == highestTargetPos ? "the same" : "a higher"} role position ${highestModPos == highestTargetPos ? "as" : "than"} you.` + `The member has ${ + highestModPos == highestTargetPos ? "the same" : "a higher" + } role position ${highestModPos == highestTargetPos ? "as" : "than"} you.` ); if (ownerError) { @@ -124,7 +126,9 @@ export async function modEmbed( const guild = interaction.guild!; const name = user.displayName; const generalValues = [`**Moderator**: ${interaction.user.displayName}`]; - let author = `• ${previousID ? "Edited a " : ""}${previousID ? dbAction?.toLowerCase() : action}${previousID ? " on" : ""} ${name}`; + let author = `• ${previousID ? "Edited a " : ""}${ + previousID ? dbAction?.toLowerCase() : action + }${previousID ? " on" : ""} ${name}`; reason ? generalValues.push(`**Reason**: ${reason}`) : generalValues.push("*No reason provided*"); if (duration) generalValues.push(`**Duration**: ${ms(ms(duration), { long: true })}`); if (previousID) { @@ -181,11 +185,11 @@ export async function modEmbed( embed .setAuthor({ name: `• You got ${action.toLowerCase()}.`, - iconURL: user.displayAvatarURL() + iconURL: user.displayAvatarURL(), }) .setDescription(generalValues.slice(+!showModerator, generalValues.length).join("\n")) - .setColor(genColor(0)) - ] + .setColor(genColor(0)), + ], }) .catch(() => null); } catch (e) { diff --git a/src/utils/embeds/serverEmbed.ts b/src/utils/embeds/serverEmbed.ts index 2af9256..366a2ce 100644 --- a/src/utils/embeds/serverEmbed.ts +++ b/src/utils/embeds/serverEmbed.ts @@ -4,13 +4,17 @@ * @returns Embed that contains the guild info. */ -import { EmbedBuilder, type Guild } from "discord.js"; +import { ChannelType, EmbedBuilder, Invite, type Guild } from "discord.js"; import { genColor } from "../colorGen"; import { imageColor } from "../imageColor"; import { pluralOrNot } from "../pluralOrNot"; type Options = { guild: Guild; + invite?: { + show: boolean; + channel: string | null; + }; roles?: boolean; page?: number; pages?: number; @@ -35,18 +39,18 @@ export async function serverEmbed(options: Options) { 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 + categories: channels.filter(channel => channel.type == 4).size, }; const generalValues = [ `Owned by **${(await guild.fetchOwner()).user.displayName}**`, - `Created on ****` + `Created on ****`, ]; 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") }) @@ -54,7 +58,9 @@ export async function serverEmbed(options: Options) { .setThumbnail(icon) .setColor((await imageColor(icon)) ?? genColor(200)); - if (options.roles) + const channelCount = channelSizes.text + channelSizes.voice; + + if (options.roles) { embed.addFields({ name: `🎭 • ${roles.size - 1} ${pluralOrNot("role", roles.size - 1)}`, value: @@ -63,25 +69,26 @@ export async function serverEmbed(options: Options) { : `${sortedRoles .slice(0, 5) .map(role => `<@&${role[0]}>`) - .join(", ")}${rolesLength > 5 ? ` and **${rolesLength - 5}** more` : ""}` + .join(", ")}${rolesLength > 5 ? ` and **${rolesLength - 5}** more` : ""}`, }); + } embed.addFields( { name: `👥 • ${guild.memberCount?.toLocaleString("en-US")} members`, value: [ `**${formattedUserCount}** ${pluralOrNot("user", guild.memberCount - bots.size)}`, - `**${bots.size?.toLocaleString("en-US")}** ${pluralOrNot("bot", bots.size)}` + `**${bots.size?.toLocaleString("en-US")}** ${pluralOrNot("bot", bots.size)}`, ].join("\n"), - inline: true + inline: true, }, { - name: `🗨️ • ${channelSizes.text + channelSizes.voice} ${pluralOrNot("channel", channelSizes.text + channelSizes.voice)}`, + name: `🗨️ • ${channelCount} ${pluralOrNot("channel", channelCount)}`, value: [ `**${channelSizes.text}** text • **${channelSizes.voice}** voice`, - `**${channelSizes.categories}** ${pluralOrNot("category", channelSizes.categories)}` + `**${channelSizes.categories}** ${pluralOrNot("category", channelSizes.categories)}`, ].join("\n"), - inline: true + inline: true, }, { name: `🌟 • ${!boostTier ? "No level" : `Level ${boostTier}`}`, @@ -89,11 +96,51 @@ export async function serverEmbed(options: Options) { `**${boostCount}**${ !boostTier ? "/2" : boostTier == 1 ? "/7" : boostTier == 2 ? "/14" : "" } ${pluralOrNot("boost", boostCount!)}`, - `**${boosters.size}** ${pluralOrNot("booster", boosters.size)}` + `**${boosters.size}** ${pluralOrNot("booster", boosters.size)}`, ].join("\n"), - inline: true + inline: true, } ); + if (options.invite?.show) { + const previousInvite: Invite | undefined = (await options.guild.invites.fetch()).find( + invite => + invite.inviter?.id === "873918300726394960" && + invite.maxUses === null && + invite.expiresAt === null + ); + + if (!options.guild.rulesChannel) return embed; + + const possiblyFetchedInviteChannel = await options.guild.channels.fetch( + options.invite.channel ?? "hi" + ); + + const inviteChannel = + possiblyFetchedInviteChannel && + possiblyFetchedInviteChannel.isTextBased() && + !possiblyFetchedInviteChannel.isThread() + ? possiblyFetchedInviteChannel + : options.guild.rulesChannel; + + if (!inviteChannel) return embed; + + const inviteUrl = previousInvite + ? previousInvite.url + : await inviteChannel.createInvite({ + maxAge: undefined, + maxUses: undefined, + reason: "Serverboard", + temporary: false, + unique: true, + }); + + embed.addFields({ + name: `🚪 • Join in!`, + value: `This server allows you to join from here. ${inviteUrl}`, + inline: true, + }); + } + return embed; } diff --git a/src/utils/humanizeSettings.ts b/src/utils/humanizeSettings.ts new file mode 100644 index 0000000..4235f3a --- /dev/null +++ b/src/utils/humanizeSettings.ts @@ -0,0 +1,17 @@ +/** + * Outputs the given settings_string + * @param {string} string Settings string, either a key or a value. + */ +export function humanizeSettings(string: string) { + if (!string) return; + const humanized = string + .trim() + .replaceAll("_", " ") + .replaceAll("true", "Enabled") + .replaceAll("false", "Disabled") + .replaceAll("(name)", "`(name)`") + .replaceAll("(servername)", "`(servername)`") + .replaceAll("(count)", "`(count)`"); + + return humanized; +}