diff --git a/.eslintrc.json b/.eslintrc.json index aceaad1aa..d32e1ceee 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,30 +1,26 @@ { - "ignorePatterns": ["build/"], - "env": { - "node": true, - "es2021": true - }, + "ignorePatterns": ["build/", "node_modules/"], + "env": { "node": true, "es6": true }, "extends": [ "eslint:recommended", "plugin:@typescript-eslint/recommended" ], "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaVersion": "latest" - }, - "plugins": [ - "@typescript-eslint" - ], + "parserOptions": { "ecmaVersion": "latest" }, + "plugins": [ "@typescript-eslint" ], + "root": true, + "rules": { "@typescript-eslint/no-var-requires": "off", "@typescript-eslint/no-unused-vars": "error", + "@typescript-eslint/indent": ["error", 2], + "@typescript-eslint/no-explicit-any": "warn", + "arrow-spacing": "error", "brace-style": [ "error", "stroustrup", - { - "allowSingleLine": true - } + { "allowSingleLine": true } ], "comma-dangle": [ "error", @@ -43,7 +39,6 @@ ], "handle-callback-err": "off", "indent": "off", - "@typescript-eslint/indent": ["error", 2], "keyword-spacing": "error", "max-nested-callbacks": [ "error", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index b013c82e0..20d9ea0bf 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -62,7 +62,8 @@ model blacklistedUsers { model connectedList { id String @id @default(auto()) @map("_id") @db.ObjectId - channelId String @unique + channelId String @unique // channel can be thread, or a normal channel + parentId String? // ID of the parent channel, if it's a thread @map("parentChannelId") serverId String connected Boolean compact Boolean diff --git a/src/Commands/Main/hub.ts b/src/Commands/Main/hub.ts index f1545276e..11170a9bf 100644 --- a/src/Commands/Main/hub.ts +++ b/src/Commands/Main/hub.ts @@ -36,7 +36,7 @@ export default { .addChannelOption(channelOption => channelOption .setName('channel') - .addChannelTypes(ChannelType.GuildText) + .addChannelTypes(ChannelType.GuildText, ChannelType.PublicThread, ChannelType.PrivateThread) .setDescription('The channel that will be used to connect to the hub') .setRequired(true), ) @@ -242,11 +242,9 @@ export default { async execute(interaction: ChatInputCommandInteraction) { const subcommand = interaction.options.getSubcommand(); const subcommandGroup = interaction.options.getSubcommandGroup(); - const extra = subcommand === 'leave' + const extra = subcommand === 'leave' || subcommand === 'delete' ? interaction.options.getString('hub', true) - : subcommand === 'delete' - ? interaction.options.getString('hub', true) - : null; + : null; require(`../../Scripts/hub/${subcommandGroup || subcommand}`).execute(interaction, extra); }, diff --git a/src/Events/messageCreate.ts b/src/Events/messageCreate.ts index 6efe8a2d9..e93c69c6e 100644 --- a/src/Events/messageCreate.ts +++ b/src/Events/messageCreate.ts @@ -105,6 +105,7 @@ export default { content: connection?.profFilter ? message.censored_content : message.content, embeds: replyEmbed ? [replyEmbed] : undefined, files: attachment ? [attachment] : [], + threadId: connection.parentId ? connection.channelId : undefined, allowedMentions: { parse: [] }, }; } @@ -115,6 +116,7 @@ export default { username: message.client.user.username, avatarURL: message.client.user.avatarURL() || undefined, files: attachment ? [attachment] : [], + threadId: connection.parentId ? connection.channelId : undefined, allowedMentions: { parse: [] }, }; } diff --git a/src/Scripts/hub/join.ts b/src/Scripts/hub/join.ts index 56c648887..0114e6533 100644 --- a/src/Scripts/hub/join.ts +++ b/src/Scripts/hub/join.ts @@ -1,8 +1,7 @@ import { ChatInputCommandInteraction, ChannelType } from 'discord.js'; import { getDb } from '../../Utils/functions/utils'; -import initialize from '../network/initialize'; +import createConnection from '../network/createConnection'; import displaySettings from '../network/displaySettings'; -import { connectedList, hubs } from '@prisma/client'; export async function execute(interaction: ChatInputCommandInteraction) { if (!interaction.inCachedGuild()) return; @@ -10,9 +9,8 @@ export async function execute(interaction: ChatInputCommandInteraction) { const db = getDb(); const name = interaction.options.getString('name') || undefined; const invite = interaction.options.getString('invite') || undefined; - const channel = interaction.options.getChannel('channel', true, [ChannelType.GuildText]); - const channelConnected = await db.connectedList.findFirst({ where: { channelId: channel.id } }); - let hubExists: hubs | null = null; + const channel = interaction.options.getChannel('channel', true, [ChannelType.GuildText, ChannelType.PublicThread, ChannelType.PrivateThread]); + let hubExists; if (!interaction.member.permissionsIn(channel).has(['ManageChannels'])) { return await interaction.reply({ @@ -27,9 +25,10 @@ export async function execute(interaction: ChatInputCommandInteraction) { ephemeral: true, }); } + const channelConnected = await db.connectedList.findFirst({ where: { channelId: channel.id } }); if (channelConnected) { return await interaction.reply({ - content: `${channel} is already connected to a hub! Please leave the hub or choose a different channel.`, + content: `${channel} is already part of a hub! Please leave the hub or choose a different channel.`, ephemeral: true, }); } @@ -46,9 +45,10 @@ export async function execute(interaction: ChatInputCommandInteraction) { ephemeral: true, }); } - else if (inviteExists.hub.connections.find((c) => c.channelId === channel.id)) { + const guildInHub = inviteExists.hub.connections.find((c) => c.serverId === channel.guildId); + if (guildInHub) { return await interaction.reply({ - content: `This server is already connected to hub **${inviteExists.hub.name}** from another channel!`, + content: `This server has already joined hub **${inviteExists.hub.name}** from from <#${guildInHub.channelId}>! Please leave the hub from that channel first, or change the channel using \`/network manage\`.!`, ephemeral: true, }); } @@ -69,9 +69,10 @@ export async function execute(interaction: ChatInputCommandInteraction) { }); } - else if ((hubExists as hubs & { connections: connectedList}).connections.channelId === channel.id) { + const guildInHub = hubExists.connections.find(c => c.serverId === channel.guildId); + if (guildInHub) { return await interaction.reply({ - content: `This server is already connected to hub **${hubExists?.name}** from another channel!`, + content: `This server has already joined hub **${hubExists?.name}** from <#${guildInHub.channelId}>! Please leave the hub from that channel first, or change the channel using \`/network manage\`.`, ephemeral: true, }); } @@ -110,7 +111,6 @@ export async function execute(interaction: ChatInputCommandInteraction) { return; } - // TODO: make an onboarding function and show them rules and stuff - initialize.execute(interaction, hubExists, channel) - .then(success => { if (success) displaySettings.execute(interaction, channel.id); }); + createConnection.execute(interaction, hubExists, channel) + .then(success => { if (success) displaySettings.execute(interaction, success.channelId); }); } diff --git a/src/Scripts/network/initialize.ts b/src/Scripts/network/createConnection.ts similarity index 53% rename from src/Scripts/network/initialize.ts rename to src/Scripts/network/createConnection.ts index 8ec74b08a..8c53c5c36 100644 --- a/src/Scripts/network/initialize.ts +++ b/src/Scripts/network/createConnection.ts @@ -1,5 +1,5 @@ import { stripIndents } from 'common-tags'; -import { ChannelType, ChatInputCommandInteraction, Collection, GuildTextBasedChannel } from 'discord.js'; +import { ChannelType, ChatInputCommandInteraction, Collection, TextChannel, ThreadChannel } from 'discord.js'; import { disconnect } from '../../Structures/network'; import { hubs } from '@prisma/client'; import logger from '../../Utils/logger'; @@ -8,13 +8,16 @@ import { getDb } from '../../Utils/functions/utils'; const onboardingInProgress = new Collection(); -export = { - async execute(interaction: ChatInputCommandInteraction, hub: hubs, networkChannel: GuildTextBasedChannel) { +export default { + async execute(interaction: ChatInputCommandInteraction, hub: hubs, networkChannel: TextChannel | ThreadChannel) { const emoji = interaction.client.emotes.normal; // Check if server is already attempting to join a hub if (onboardingInProgress.has(networkChannel.id)) { - const err = `${emoji.no} There has already been an attempt to join a hub in ${networkChannel}. Please wait for that to finish before trying again!`; + const err = { + content: `${emoji.no} There has already been an attempt to join a hub in ${networkChannel}. Please wait for that to finish before trying again!`, + ephemeral: true, + }; interaction.deferred || interaction.replied ? interaction.followUp(err) : interaction.reply(err); @@ -24,43 +27,60 @@ export = { onboardingInProgress.set(networkChannel.id, networkChannel.id); // Show new users rules & info about network - const onboardingStatus = await onboarding.execute(interaction); - if (!onboardingStatus) { - onboardingInProgress.delete(networkChannel.id); - return; - } + const onboardingStatus = await onboarding.execute(interaction, hub.name); + // remove in-progress marker as onboarding has either been cancelled or completed + onboardingInProgress.delete(networkChannel.id); + // if user cancelled onboarding or didn't click any buttons, stop here + if (!onboardingStatus) return; + let createdConnection; try { - if (networkChannel.type !== ChannelType.GuildText) { - interaction.followUp(`${emoji.no} You can only connect **text channels** to the InterChat network!`); - return; + let webhook; + if (networkChannel.isThread() && networkChannel.parent) { + const webhooks = await networkChannel.parent.fetchWebhooks(); + const webhookCreated = webhooks.find(w => w.owner?.id === interaction.client.user?.id); + + if (webhookCreated) { + webhook = webhookCreated; + } + else { + webhook = await networkChannel.parent.createWebhook({ + name: 'InterChat Network', + avatar: interaction.client.user?.avatarURL(), + }); + } + } + else if (networkChannel.type === ChannelType.GuildText) { + webhook = await networkChannel.createWebhook({ + name: 'InterChat Network', + avatar: interaction.client.user?.avatarURL(), + }); + } + else { + return interaction.followUp('This channel is not supported for InterChat. Please use a text channel or a thread.'); } - const webhook = await networkChannel.createWebhook({ - name: 'InterChat Network', - avatar: interaction.client.user?.avatarURL(), - }); const { connectedList } = getDb(); - await connectedList.create({ + createdConnection = await connectedList.create({ data:{ channelId: networkChannel.id, + parentId: networkChannel.isThread() ? networkChannel.id : undefined, serverId: networkChannel.guild.id, + webhookURL: webhook.url, connected: true, profFilter: true, compact: false, - webhookURL: webhook.url, hub: { connect: { id: hub.id } }, }, }); const numOfConnections = await connectedList.count({ where: { hub: { id: hub.id } } }); - if (numOfConnections > 1) { - await networkChannel?.send(`This channel has been connected with ${hub.name}. You are currently with ${numOfConnections - 1} other servers, Enjoy! ${emoji.clipart}`); - } - else { - await networkChannel?.send(`This channel has been connected with ${hub.name}, though no one else is there currently... *cricket noises* ${emoji.clipart}`); - } + await networkChannel?.send(`This channel has been connected with **${hub.name}**. ${ + numOfConnections > 1 + ? `You are currently with ${numOfConnections - 1} other servers, Enjoy! ${emoji.clipart}` + : `It seems no one else is there currently... *cricket noises* ${emoji.clipart}` + }`); } // eslint-disable-next-line @typescript-eslint/no-explicit-any catch (err: any) { @@ -72,7 +92,7 @@ export = { : interaction.reply(errMsg); } else { - const errMsg = `An error occurred while connecting to the InterChat network! \`\`\`js\n${err.message}\`\`\``; + const errMsg = `An error occurred while connecting to the InterChat network! Please report this to the developers: \`\`\`js\n${err.message}\`\`\``; interaction.deferred || interaction.replied ? interaction.followUp(errMsg) : interaction.reply(errMsg); @@ -88,6 +108,8 @@ export = { **Server Name:** __${interaction.guild?.name}__ **Member Count:** __${interaction.guild?.memberCount}__ `, { id: hub.id }); - return true; // just a marker to show that the setup was successful + + // return the created connection so we can use it in the next step + return createdConnection; }, }; diff --git a/src/Scripts/network/displaySettings.ts b/src/Scripts/network/displaySettings.ts index 3d1995248..739833c8d 100644 --- a/src/Scripts/network/displaySettings.ts +++ b/src/Scripts/network/displaySettings.ts @@ -1,4 +1,4 @@ -import { ChatInputCommandInteraction, ButtonBuilder, ActionRowBuilder, ButtonStyle, GuildTextBasedChannel, EmbedBuilder, ChannelType, ComponentType, StringSelectMenuBuilder, StringSelectMenuOptionBuilder, Interaction, ChannelSelectMenuBuilder, ModalBuilder, TextInputBuilder, TextInputStyle, TextChannel, ButtonInteraction, AnySelectMenuInteraction } from 'discord.js'; +import { ChatInputCommandInteraction, ButtonBuilder, ActionRowBuilder, ButtonStyle, GuildTextBasedChannel, EmbedBuilder, ChannelType, ComponentType, StringSelectMenuBuilder, StringSelectMenuOptionBuilder, Interaction, ChannelSelectMenuBuilder, ModalBuilder, TextInputBuilder, TextInputStyle, TextChannel, ButtonInteraction, AnySelectMenuInteraction, Webhook, ThreadChannel } from 'discord.js'; import { reconnect, disconnect } from '../../Structures/network'; import { colors, getDb } from '../../Utils/functions/utils'; import logger from '../../Utils/logger'; @@ -98,11 +98,6 @@ export = { filter, componentType: ComponentType.Button, }); - const selectCollector = setupMessage.createMessageComponentCollector({ - filter, - idle: 60_000 * 5, - componentType: ComponentType.StringSelect, - }); /* ------------------- Button Responce collectors ---------------------- */ buttonCollector.on('collect', async (component) => { @@ -171,7 +166,14 @@ export = { }); - /* ------------------- SelectMenu Responce collectors ---------------------- */ + + /* ------------------- Replying to SelectMenus ---------------------- */ + const selectCollector = setupMessage.createMessageComponentCollector({ + filter, + idle: 60_000 * 5, + componentType: ComponentType.StringSelect, + }); + selectCollector.on('collect', async (settingsMenu) => { const updConnection = await db.connectedList.findFirst({ where: { channelId: connection.channelId } }); if (!updConnection) { @@ -184,7 +186,7 @@ export = { switch (settingsMenu.values[0]) { /* Compact / Normal mode toggle */ - case 'compact':{ + case 'compact': { await db.connectedList.update({ where: { channelId: updConnection.channelId }, data: { compact: !updConnection.compact }, @@ -192,68 +194,85 @@ export = { break; } /* Profanity toggle */ - case 'profanity': + case 'profanity': { await db.connectedList.update({ where: { channelId: updConnection.channelId }, data: { profFilter: !updConnection.profFilter }, }); break; + } - /* Change channel request Response */ case 'change_channel': { const channelMenu = new ActionRowBuilder() .addComponents( new ChannelSelectMenuBuilder() .setCustomId('newChannelSelect') .setPlaceholder('Select new channel') - .addChannelTypes(ChannelType.GuildText), + .addChannelTypes(ChannelType.GuildText, ChannelType.PublicThread, ChannelType.PrivateThread), ); const changeMsg = await settingsMenu.reply({ content: 'Please select a channel within the next 20 seconds.', components: [channelMenu], ephemeral: true, + fetchReply: true, }); const selected = await changeMsg.awaitMessageComponent({ componentType: ComponentType.ChannelSelect, - idle: 20_000, + time: 20_000, }).catch(() => null); if (!selected) return; - const oldchannel = selected.guild?.channels.cache.get(`${updConnection?.channelId}`) as TextChannel; - const channel = selected.guild?.channels.cache.get(selected?.values[0]) as TextChannel; + const newchannel = selected.guild?.channels.cache.get(selected?.values[0]) as TextChannel | ThreadChannel; + const newchannelInDb = await db.connectedList.findFirst({ where: { channelId: newchannel.id } }); - if (await db.connectedList.findFirst({ where: { channelId: channel.id } })) { + // if the hubId doesn't match with the already connected channel + // don't let to switch channel as it is already connected to another hub + if (newchannelInDb && newchannelInDb.channelId !== updConnection.channelId) { await selected.update({ - content: `${emoji.normal.no} Channel ${channel} is already connected to a hub. Please leave that hub first or select another channel.`, + content: `${emoji.normal.no} Channel ${newchannel} has already joined a hub. Either leave that hub first or select another channel.`, components: [], }); return; } - // delete the old webhook - oldchannel?.fetchWebhooks() - .then(promisehook => promisehook.find((hook) => hook.owner?.id === hook.client.user?.id)?.delete().catch(() => null)) - .catch(() => null); + let webhook: Webhook | null = null; + if (newchannel.type === ChannelType.GuildText) { + const webhooks = await newchannel.fetchWebhooks(); + const interchatHook = webhooks?.find((hook) => hook.owner?.id === hook.client.user?.id); - // create a webhook in the new channel - const webhook = await channel?.createWebhook({ - name: 'InterChat Network', - avatar: selected.client.user.avatarURL(), - }); + // create a webhook in the new channel + webhook = interchatHook || + await newchannel.createWebhook({ + name: 'InterChat Network', + avatar: newchannel.client.user.avatarURL(), + }); + } + + else if (newchannel.isThread() && newchannel.parent) { + const webhooks = await newchannel.parent.fetchWebhooks(); + const interchatHook = webhooks?.find((hook) => hook.owner?.id === hook.client.user?.id); + + webhook = interchatHook || + await newchannel.parent.createWebhook({ + name: 'InterChat Network', + avatar: newchannel.client.user.avatarURL(), + }); + } await db.connectedList.update({ - where: { channelId: updConnection.channelId }, + where: { channelId: connection.channelId }, data: { - channelId: channel?.id, - webhookURL: webhook.url, + channelId: newchannel.id, + parentId: newchannel?.isThread() ? newchannel.parentId : { unset: true }, + webhookURL: webhook?.url, }, }); - await selected?.update({ - content: `Successfully switched connection from ${oldchannel} to ${channel}!`, + await selected.update({ + content: `${emoji.normal.yes} Channel has been changed to ${newchannel}!`, components: [], }); break; @@ -342,3 +361,6 @@ export = { }); }, }; + +// TODO: Hub leave command shows channel and now thread names in autocomplete +// TODO: channelId is no longer unique, either make it unique or fix the whole code diff --git a/src/Scripts/network/onboarding.ts b/src/Scripts/network/onboarding.ts index be923257e..5e7136618 100644 --- a/src/Scripts/network/onboarding.ts +++ b/src/Scripts/network/onboarding.ts @@ -4,17 +4,17 @@ import { colors, rulesEmbed } from '../../Utils/functions/utils'; /* Make user accept and understand important info on first setup */ export default { - async execute(interaction: ChatInputCommandInteraction) { + async execute(interaction: ChatInputCommandInteraction, hubName: string) { const embed = new EmbedBuilder() - .setTitle('👋 Hey there! Welcome to the InterChat network.') + .setTitle(`👋 Hey there! Welcome to ${hubName}!`) .setDescription(stripIndents` - To keep things organized, it's recommended to create a separate channel for the network. But don't worry, you can always change this later. + To keep things organized, it's recommended to use a separate channel for just for this hub. But don't worry, you can always change this later. - Before we dive in, take a moment to review our network rules. We want everyone to have a smooth and fun experience. + Before we dive in, take a moment to review our rules. We want everyone to have a smooth and fun experience. - **How it works:** the InterChat Network is like a magic bridge that links channels on different servers. So, you can chat with people from all over! + **How it works:** the InterChat Network is like a magic bridge that links channels on different servers that are with us in this hub. So, you can chat with people from all over! - And hey, if you have any cool ideas for new features, let us know! We're always looking to improve. + Developer Note: And hey, if you have any cool ideas for new features, let us know! We're always looking to improve. `) .setColor(colors('chatbot')) .setFooter({ text: `InterChat Network | Version ${interaction.client.version}` }); diff --git a/src/Scripts/support/server.ts b/src/Scripts/support/server.ts index 49fc6fc4c..725601c52 100644 --- a/src/Scripts/support/server.ts +++ b/src/Scripts/support/server.ts @@ -4,7 +4,7 @@ import { colors } from '../../Utils/functions/utils'; export = { async execute(interaction: ChatInputCommandInteraction) { const embed = new EmbedBuilder() - .setTitle('InterChat HQ') + .setTitle('InterChat Central') .setDescription('[Click Here]()') .setColor(colors('chatbot')) .setTimestamp();