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
28 changes: 25 additions & 3 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
generator client {
provider = "prisma-client-js"
provider = "prisma-client-js"
}

datasource db {
Expand Down Expand Up @@ -31,6 +31,11 @@ type userBan {
reason String
}

type RecentLobbyData {
lobbyId String
timestamp Int
}

enum BlockWordAction {
BLOCK_MESSAGE
BLACKLIST
Expand Down Expand Up @@ -171,7 +176,7 @@ model UserData {
locale String?
lastVoted DateTime?
banMeta userBan?
mentionOnReply Boolean? @default(false)
mentionOnReply Boolean @default(true)
acceptedRules Boolean @default(false)
infractions UserInfraction[]
}
Expand All @@ -185,4 +190,21 @@ model LobbyChatHistory {
date DateTime @default(now())

@@index([serverId, channelId, lobbyId])
}
}

model ServerHistory {
id String @id @default(auto()) @map("_id") @db.ObjectId
serverId String @unique
recentLobbies RecentLobbyData[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

model ServerPreference {
id String @id @default(auto()) @map("_id") @db.ObjectId
serverId String @unique
premiumStatus Boolean @default(false)
maxServersInLobby Int @default(3)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
136 changes: 104 additions & 32 deletions src/commands/prefix/connect.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import BasePrefixCommand, { CommandData } from '#main/core/BasePrefixCommand.js';
import { msToReadable } from '#main/utils/Utils.js';
import { LobbyManager } from '#main/managers/LobbyManager.js';
import { MatchingService } from '#main/services/LobbyMatchingService.js';
import { emojis } from '#main/utils/Constants.js';
import db from '#main/utils/Db.js';
import { t } from '#main/utils/Locale.js';
import { getOrCreateWebhook } from '#main/utils/Utils.js';
import { stripIndents } from 'common-tags';
import { Message, PermissionsBitField } from 'discord.js';

Expand All @@ -8,48 +13,115 @@ export default class BlacklistPrefixCommand extends BasePrefixCommand {
name: 'connect',
description: 'Connect to a random lobby',
category: 'Moderation',
usage: 'connect',
examples: ['c', 'call'],
usage: 'connect ` [minimum number of servers] `',
examples: ['c', 'call', 'call 3'],
aliases: ['call', 'c', 'conn', 'joinlobby', 'jl'],
requiredBotPermissions: new PermissionsBitField(['SendMessages', 'EmbedLinks', 'ReadMessageHistory']),
requiredBotPermissions: new PermissionsBitField([
'SendMessages',
'EmbedLinks',
'ReadMessageHistory',
]),
dbPermission: false,
requiredArgs: 0,
};

protected async run(message: Message<true>) {
// TODO: uncomment this after final release
// const voteLimiter = new VoteLimitManager('editMsg', message.author.id, message.client.userManager);

// if (await voteLimiter.hasExceededLimit()) {
// await message.reply({
// content: `${emojis.topggSparkles} Daily limit for lobbies reached. [Vote on top.gg](${Constants.Links.Vote}) to chat non-stop for the next 12 hours! Or join a [permanent hub](https://interchat.fun/hubs) to chat without voting.`,
// });
// return;
// }

// check if already connected
const { lobbyService } = message.client;
const alreadyConnected = await lobbyService.getChannelLobby(message.channelId);
if (alreadyConnected) {
await message.reply('This server is already connected to a lobby!');
private readonly lobbyManager = new LobbyManager();
private readonly matching = new MatchingService();

protected async run(message: Message<true>, args: string[]) {
const minServers = parseInt(args[0], 10) || 2;

const alreadyInLobby = await this.lobbyManager.getLobbyByChannelId(message.channelId);
if (alreadyInLobby) {
await message.reply('You are already chatting in a lobby. Please leave it first.');
return;
}

const inWaitingPool = await this.lobbyManager.getChannelFromWaitingPool(message.guildId);
if (inWaitingPool) {
await message.reply(
stripIndents`
-# **💡 Did you know?** You can change the minimum number of servers required to find a match: \`c!call 2\`. It can be faster sometimes!
You are already in the waiting pool for a lobby.
-# You will be notified once a lobby is found.
`,
);
return;
}

const webhook = await getOrCreateWebhook(
message.channel,
'https://i.imgur.com/80nqtSg.png',
'InterChat Lobby',
);
if (!webhook) {
await message.reply(
t('errors.missingPermissions', 'en', { emoji: emojis.no, permissions: 'Manage Webhooks' }),
);
return;
}

const { queued, lobby } = await lobbyService.connectChannel(message.guildId, message.channelId);
const serverId = message.guildId;
const channelId = message.channelId;

// TODO: a way to modify this
const serverPrefs = await db.serverPreference.findUnique({
where: { serverId },
});

const preferences = {
premiumStatus: serverPrefs?.premiumStatus ?? false,
maxServersInLobby: minServers ?? 2,
};

// Add server to waiting pool for matching
await this.lobbyManager.addToWaitingPool(
{ serverId, channelId, webhookUrl: webhook.url },
preferences,
);

if (lobby) {
await message.reply(stripIndents`
Connected to a lobby with ${lobby.connections.length} server(s)!
Activity level: ${lobby.activityLevel} messages/5min
-# Messages in this channel will be shared with other servers.
`);
// Set timeout for 5 minutes
setTimeout(
async () => {
await this.lobbyManager.removeFromWaitingPool(serverId);
await message.reply('Please try again later. No lobbies were found for this server.');
},
5 * 60 * 1000, // 5 minutes
);

const match = await this.matching.findMatch(serverId, preferences);
if (match) {
const lobbyId = await this.lobbyManager.createLobby([
{ serverId, channelId, webhookUrl: webhook.url },
match,
]);

// Update server histories
await this.updateServerHistory(serverId, lobbyId);
await this.updateServerHistory(match.serverId, lobbyId);
}
else if (queued) {
const info = await lobbyService.getPoolInfo(message.channelId);
await message.channel.send(stripIndents`
else {
await message.reply(
stripIndents`
-# **💡 Did you know?** You can change the minimum number of servers required to find a match: \`c!call 2\`. It can be faster sometimes!
Finding a lobby for this server... Hang tight!
-# You will be notified once a lobby is found. Estimated wait time: ${info.estimatedWaitTime ? msToReadable(info.estimatedWaitTime) : 'now'}.
`);
-# You will be notified once a lobby is found.
`,
);
}
}
private async updateServerHistory(serverId: string, lobbyId: string): Promise<void> {
await db.serverHistory.upsert({
where: { serverId },
update: {
recentLobbies: {
push: { lobbyId, timestamp: Date.now() },
},
},
create: {
serverId,
recentLobbies: [{ lobbyId, timestamp: Date.now() }],
},
});
}
}
21 changes: 9 additions & 12 deletions src/commands/prefix/disconnect.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import BasePrefixCommand, { CommandData } from '#main/core/BasePrefixCommand.js';
import { LobbyManager } from '#main/managers/LobbyManager.js';
import { emojis } from '#main/utils/Constants.js';
import { Message, PermissionsBitField } from 'discord.js';

Expand All @@ -15,24 +16,20 @@ export default class BlacklistPrefixCommand extends BasePrefixCommand {
requiredArgs: 0,
};

private readonly lobbyManager = new LobbyManager();

protected async run(message: Message<true>) {
const { lobbyService } = message.client;
const alreadyConnected = await lobbyService.getChannelLobby(message.channelId);
const alreadyConnected = await this.lobbyManager.getLobbyByChannelId(message.channelId);

if (alreadyConnected) {
await lobbyService.disconnectChannel(message.channelId);

await this.lobbyManager.removeServerFromLobby(alreadyConnected.id, message.guildId);
}
else {
const poolInfo = await lobbyService.getPoolInfo(message.channelId);
if (!poolInfo.position) {
await message.reply(`${emojis.no} This channel is not connected to a lobby!`);
return;
}

await lobbyService.removeFromPoolByChannelId(message.channelId);
await this.lobbyManager.removeFromWaitingPool(message.guildId);
await message.reply(`${emojis.disconnect} Not connected to any lobby. Removed from waiting pool if exists.`);
return;
}

await message.reply(`${emojis.disconnect} You have disconnected from the lobby.`);
await message.reply(`${emojis.disconnect} You have left the lobby.`);
}
}
4 changes: 2 additions & 2 deletions src/commands/slash/Information/about.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export default class About extends BaseCommand {
`,
)
.setFooter({
text: ` InterChat v${Constants.ProjectVersion} • Made with ❤️ by the InterChat Team`,
text: ` InterChat v${interaction.client.version} • Made with ❤️ by the InterChat Team`,
});

const linkButtons = new ActionRowBuilder<ButtonBuilder>().addComponents(
Expand Down Expand Up @@ -106,7 +106,7 @@ export default class About extends BaseCommand {
`,
)
.setFooter({
text: ` InterChat v${Constants.ProjectVersion} • Made with ❤️ by the InterChat Team`,
text: ` InterChat v${interaction.client.version} • Made with ❤️ by the InterChat Team`,
});

await interaction.editReply({ embeds: [creditsEmbed] });
Expand Down
4 changes: 2 additions & 2 deletions src/commands/slash/Main/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ export default class SetupCommand extends BaseCommand {

public async execute(interaction: ChatInputCommandInteraction) {
const subcommand = interaction.options.getSubcommand(true);
if (subcommand === 'lobby') return this.setupLobby(interaction);
else if (subcommand === 'interchat') return this.setupHub(interaction);
if (subcommand === 'lobby') await this.setupLobby(interaction);
else if (subcommand === 'interchat') await this.setupHub(interaction);
}

private async setupLobby(interaction: ChatInputCommandInteraction): Promise<void> {
Expand Down
54 changes: 34 additions & 20 deletions src/core/BaseClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import type { InteractionFunction } from '#main/decorators/RegisterInteractionHa
import AntiSpamManager from '#main/managers/AntiSpamManager.js';
import UserDbManager from '#main/managers/UserDbManager.js';
import EventLoader from '#main/modules/Loaders/EventLoader.js';
import LobbyNotifier from '#main/modules/LobbyNotifier.js';
import ChatLobbyService from '#main/services/ChatLobbyService.js';
import CooldownService from '#main/services/CooldownService.js';
import Scheduler from '#main/services/SchedulerService.js';
import type { RemoveMethods } from '#types/CustomClientProps.d.ts';
Expand All @@ -17,34 +15,31 @@ import { ClusterClient, getInfo } from 'discord-hybrid-sharding';
import {
type Guild,
type Snowflake,
ActivityType,
Client,
Collection,
GatewayIntentBits,
Options,
Sweepers,
} from 'discord.js';

export default class InterChatClient extends Client {
static instance: InterChatClient;

private readonly scheduler = new Scheduler();

public readonly description = 'The only cross-server chatting bot you\'ll ever need.';
public readonly version = Constants.ProjectVersion;

public readonly reactionCooldowns = new Collection<string, number>();
public readonly commands = new Collection<string, BaseCommand>();
public readonly interactions = new Collection<string, InteractionFunction>();
public readonly prefixCommands = new Collection<string, BasePrefixCommand>();

public readonly version = Constants.ProjectVersion;
public readonly reactionCooldowns = new Collection<string, number>();
public readonly userManager = new UserDbManager();
public readonly cluster = new ClusterClient(this);
public readonly eventLoader = new EventLoader(this);
public readonly commandCooldowns = new CooldownService();
public readonly lobbyService = new ChatLobbyService(new LobbyNotifier(this.cluster));
public readonly antiSpamManager = new AntiSpamManager({
spamThreshold: 4,
timeWindow: 5000,
timeWindow: 3000,
spamCountExpirySecs: 60,
});

Expand All @@ -53,10 +48,36 @@ export default class InterChatClient extends Client {
shards: getInfo().SHARD_LIST, // An array of shards that will get spawned
shardCount: getInfo().TOTAL_SHARDS, // Total number of shards
makeCache: Options.cacheWithLimits({
MessageManager: 200,
PresenceManager: 0,
ThreadManager: {
maxSize: 1000,
},
ReactionManager: 200,
PresenceManager: 0,
AutoModerationRuleManager: 0,
VoiceStateManager: 0,
GuildScheduledEventManager: 0,
ApplicationCommandManager: 0,
BaseGuildEmojiManager: 0,
StageInstanceManager: 0,
GuildStickerManager: 0,
ThreadMemberManager: 0,
GuildInviteManager: 0,
GuildEmojiManager: 0,
GuildBanManager: 0,
}),
sweepers: {
messages: {
interval: 60,
filter: Sweepers.filterByLifetime({
lifetime: 150,
getComparisonTimestamp: (message) => message.createdTimestamp,
}),
},
threads: {
interval: 60,
filter: () => (thread) => Boolean(thread.archived),
},
},
intents: [
GatewayIntentBits.MessageContent,
GatewayIntentBits.Guilds,
Expand All @@ -65,15 +86,8 @@ export default class InterChatClient extends Client {
GatewayIntentBits.GuildMessageReactions,
GatewayIntentBits.GuildWebhooks,
],
presence: {
status: 'online',
activities: [
{
state: '🔗 Watching over 700+ cross-server chats',
name: 'custom',
type: ActivityType.Custom,
},
],
allowedMentions: {
repliedUser: false,
},
});
}
Expand Down
Loading