Skip to content
This repository was archived by the owner on Oct 9, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
190 changes: 163 additions & 27 deletions src/Scripts/hub/browse.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,48 @@
import { ActionRowBuilder, ButtonBuilder, ButtonInteraction, ButtonStyle, ChatInputCommandInteraction, ModalBuilder, TextInputBuilder, TextInputStyle } from 'discord.js';
import {
ActionRowBuilder,
ButtonBuilder,
ButtonInteraction,
ButtonStyle,
ChannelSelectMenuBuilder,
ChannelType,
ChatInputCommandInteraction,
EmbedBuilder,
GuildTextBasedChannel,
ModalBuilder,
TextInputBuilder,
TextInputStyle,
} from 'discord.js';
import { calculateAverageRating, createHubListingsEmbed, getDb } from '../../Utils/functions/utils';
import { paginate } from '../../Utils/functions/paginator';
import { hubs } from '@prisma/client';
import logger from '../../Utils/logger';
import { captureException } from '@sentry/node';
import createConnection from '../network/createConnection';

export async function execute(interaction: ChatInputCommandInteraction) {
const sortBy = interaction.options.getString('sort') as 'connections' | 'active' | 'popular' | 'recent' | undefined;
const sortBy = interaction.options.getString('sort') as
| 'connections'
| 'active'
| 'popular'
| 'recent'
| undefined;
const hubName = interaction.options.getString('search') || undefined;

const db = getDb();
let sortedHubs: hubs[] = [];


switch (sortBy) {
case 'active':
sortedHubs = await db.hubs.findMany({
where: { name: hubName, private: false },
orderBy: { messages: { _count: 'desc' } },
});
break;
case 'popular':
sortedHubs = (await db.hubs
.findMany({ where: { name: hubName, private: false } }))
.sort((a, b) => {
const aAverage = calculateAverageRating(a.rating.map((rating) => rating.rating));
const bAverage = calculateAverageRating(b.rating.map((rating) => rating.rating));
return bAverage - aAverage;
});
sortedHubs = (
await db.hubs.findMany({
where: { name: hubName, private: false },
include: { connections: true },
})
).sort((a, b) => {
const aAverage = calculateAverageRating(a.rating.map((rating) => rating.rating));
const bAverage = calculateAverageRating(b.rating.map((rating) => rating.rating));
return bAverage - aAverage;
});
break;
case 'recent':
sortedHubs = await db.hubs.findMany({
Expand All @@ -41,19 +56,25 @@ export async function execute(interaction: ChatInputCommandInteraction) {
orderBy: { connections: { _count: 'desc' } },
});
break;

case 'active':
default:
sortedHubs = await db.hubs.findMany({ where: { name: hubName, private: false } });
sortedHubs = await db.hubs.findMany({
where: { name: hubName, private: false },
orderBy: { messages: { _count: 'desc' } },
});
break;
}

const hubList = await Promise.all(
sortedHubs?.map(async (hub) => {
const totalNetworks = await db.connectedList
.count({ where: { hubId: hub.id } })
.catch(() => 0);

const hubList = sortedHubs?.map(async (hub) => {
const totalNetworks = await db.connectedList
.count({ where: { hubId: hub.id } })
.catch(() => 0);

return createHubListingsEmbed(hub, { totalNetworks });
});
return createHubListingsEmbed(hub, { totalNetworks });
}),
);

if (!hubList || hubList.length === 0) {
interaction.reply({
Expand All @@ -68,14 +89,18 @@ export async function execute(interaction: ChatInputCommandInteraction) {
.setCustomId(`rate-${sortedHubs[0].id}`)
.setLabel('Rate')
.setStyle(ButtonStyle.Secondary),
new ButtonBuilder()
.setCustomId(`join-${sortedHubs[0].id}`)
.setLabel('Join')
.setStyle(ButtonStyle.Success),
);


paginate(interaction, await Promise.all(hubList), {
paginate(interaction, hubList, {
extraComponent: {
actionRow: [paginateBtns],
updateComponents(pageNumber) {
paginateBtns.components[0].setCustomId(`rate-${sortedHubs[pageNumber].id}`);
paginateBtns.components[1].setCustomId(`join-${sortedHubs[pageNumber].id}`);
return paginateBtns;
},
async execute(i: ButtonInteraction) {
Expand All @@ -97,7 +122,7 @@ export async function execute(interaction: ChatInputCommandInteraction) {
);
await i.showModal(ratingModal);
i.awaitModalSubmit({ time: 30_000 })
.then(async m => {
.then(async (m) => {
const rating = parseInt(m.fields.getTextInputValue('rating'));
if (isNaN(rating) || rating < 1 || rating > 5) {
return m.reply({
Expand Down Expand Up @@ -142,6 +167,117 @@ export async function execute(interaction: ChatInputCommandInteraction) {
}
});
}
else if (i.customId.startsWith('join-')) {
const hubDetails = await db.hubs.findFirst({
where: { id: i.customId.replace('join-', '') },
include: { connections: true },
});

if (!hubDetails) {
i.reply({
content: 'Hub not found.',
ephemeral: true,
});
return;
}

const alreadyJoined = hubDetails.connections.find((c) => c.serverId === i.guildId);
if (alreadyJoined) {
i.reply({
content: `You have already joined **${hubDetails.name}** from <#${alreadyJoined.channelId}>!`,
ephemeral: true,
});
return;
}

let channel = i.channel;

const channelSelect = new ActionRowBuilder<ChannelSelectMenuBuilder>().addComponents(
new ChannelSelectMenuBuilder()
.setCustomId('channel_select')
.setPlaceholder('Select a different channel.')
.setChannelTypes([
ChannelType.PublicThread,
ChannelType.PrivateThread,
ChannelType.GuildText,
]),
);

const buttons = new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder()
.setCustomId('confirm')
.setLabel('Confirm')
.setStyle(ButtonStyle.Success),
new ButtonBuilder()
.setCustomId('cancel')
.setLabel('Cancel')
.setStyle(ButtonStyle.Danger),
);

// use current channel embed
const embed = new EmbedBuilder()
.setDescription(`
Are you sure you wish to join **${hubDetails.name}** from ${interaction.channel}?

**Note:** You can always change this later using \`/network manage\`.
`,
)
.setColor('Aqua')
.setFooter({ text: 'Use a different channel? Use the dropdown below.' });

const reply = await i.reply({
embeds: [embed],
components: [channelSelect, buttons],
fetchReply: true,
ephemeral: true,
});

const response = await reply
.awaitMessageComponent({
time: 60_000 * 2,
filter: (e) => e.user.id === i.user.id,
})
.catch(() => null);

if (!response?.inCachedGuild() || response.customId === 'cancel') {
i.deleteReply().catch(() => null);
return;
}

if (response.isChannelSelectMenu()) {
channel = response.guild.channels.cache.get(response.values[0]) as GuildTextBasedChannel;
}

if (
(channel?.type === ChannelType.GuildText || channel?.isThread()) &&
(response.customId === 'confirm' || response.customId === 'channel_select')
) {
const channelConnected = await db.connectedList.findFirst({
where: { channelId: channel.id },
});

if (channelConnected) {
response.update({
content: 'This channel is already connected to another hub!',
embeds: [],
components: [],
});
return;
}

createConnection.execute(response, hubDetails, channel, true).then((success) => {
if (success) {
response.editReply({
content: `Successfully joined hub ${hubDetails.name} from ${channel}! Use \`/network manage\` to manage your connection. And \`/hub leave\` to leave the hub.`,
embeds: [],
components: [],
});
return;
}
response.message.delete().catch(() => null);
});
}
}
},
},
});
Expand Down
62 changes: 28 additions & 34 deletions src/Scripts/network/createConnection.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,22 @@
import { stripIndents } from 'common-tags';
import { ChannelType, ChatInputCommandInteraction, Collection, TextChannel, ThreadChannel } from 'discord.js';
import { ChannelType, AnySelectMenuInteraction, ChatInputCommandInteraction, TextChannel, ThreadChannel, ButtonInteraction } from 'discord.js';
import { disconnect } from '../../Structures/network';
import { hubs } from '@prisma/client';
import logger from '../../Utils/logger';
import onboarding from './onboarding';
import { getDb } from '../../Utils/functions/utils';

const onboardingInProgress = new Collection<string, string>();

export default {
async execute(interaction: ChatInputCommandInteraction, hub: hubs, networkChannel: TextChannel | ThreadChannel) {
async execute(
interaction: AnySelectMenuInteraction | ButtonInteraction | ChatInputCommandInteraction,
hub: hubs,
networkChannel: TextChannel | ThreadChannel,
ephemeral = false,
) {
const emoji = interaction.client.emotes.normal;

// Check if server is already attempting to join a hub
if (onboardingInProgress.has(networkChannel.id)) {
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);
return;
}
// Mark this as in-progress so server can't join twice
onboardingInProgress.set(networkChannel.id, networkChannel.id);

// Show new users rules & info about network
const onboardingStatus = await onboarding.execute(interaction, hub.name);
// remove in-progress marker as onboarding has either been cancelled or completed
onboardingInProgress.delete(networkChannel.id);
const onboardingStatus = await onboarding.execute(interaction, hub.name, networkChannel.id, ephemeral);
// if user cancelled onboarding or didn't click any buttons, stop here
if (!onboardingStatus) return;

Expand All @@ -38,7 +25,7 @@ export default {
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);
const webhookCreated = webhooks.find((w) => w.owner?.id === interaction.client.user?.id);

if (webhookCreated) {
webhook = webhookCreated;
Expand All @@ -57,13 +44,14 @@ export default {
});
}
else {
return interaction.followUp('This channel is not supported for InterChat. Please use a text channel or a thread.');
return interaction.followUp(
'This channel is not supported for InterChat. Please use a text channel or a thread.',
);
}


const { connectedList } = getDb();
createdConnection = await connectedList.create({
data:{
data: {
channelId: networkChannel.id,
parentId: networkChannel.isThread() ? networkChannel.id : undefined,
serverId: networkChannel.guild.id,
Expand All @@ -76,14 +64,18 @@ export default {
});

const numOfConnections = await connectedList.count({ where: { hub: { id: hub.id } } });
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}`
}`);
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) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
logger.error(err);
if (err.message === 'Missing Permissions' || err.message === 'Missing Access') {
const errMsg = `${emoji.no} Please make sure you have granted me \`Manage Webhooks\` and \`View Channel\` permissions for the selected channel.`;
Expand All @@ -97,17 +89,19 @@ export default {
? interaction.followUp(errMsg)
: interaction.reply(errMsg);
}
onboardingInProgress.delete(networkChannel.id);
disconnect(networkChannel.id);
return;
}

interaction.client.sendInNetwork(stripIndents`
interaction.client.sendInNetwork(
stripIndents`
A new server has joined us! ${emoji.clipart}

**Server Name:** __${interaction.guild?.name}__
**Member Count:** __${interaction.guild?.memberCount}__
`, { id: hub.id });
`,
{ id: hub.id },
);

// return the created connection so we can use it in the next step
return createdConnection;
Expand Down
Loading