-
Notifications
You must be signed in to change notification settings - Fork 3
Reserve channels #58
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Reserve channels #58
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| /** | ||
| * @type {import('node-pg-migrate').ColumnDefinitions | undefined} | ||
| */ | ||
| export const shorthands = undefined; | ||
|
|
||
| /** | ||
| * @param pgm {import('node-pg-migrate').MigrationBuilder} | ||
| * @param run {() => void | undefined} | ||
| * @returns {Promise<void> | void} | ||
| */ | ||
| export const up = (pgm) => { | ||
| pgm.createTable('channel_pool', { | ||
| id: 'id', | ||
| channel_id: { type: 'varchar(255)', notNull: true, unique: true }, | ||
| in_use: { type: 'boolean', notNull: true, default: false }, | ||
| match_id: { type: 'integer', notNull: false, default: null }, | ||
| created_at: { | ||
| type: 'timestamp', | ||
| notNull: true, | ||
| default: pgm.func('current_timestamp'), | ||
| }, | ||
| }); | ||
|
|
||
| // Add index for faster lookups of available channels | ||
| pgm.createIndex('channel_pool', 'in_use'); | ||
| }; | ||
|
|
||
| /** | ||
| * @param pgm {import('node-pg-migrate').MigrationBuilder} | ||
| * @param run {() => void | undefined} | ||
| * @returns {Promise<void> | void} | ||
| */ | ||
| export const down = (pgm) => { | ||
| pgm.dropTable('channel_pool'); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,12 +1,17 @@ | ||
| import { Events, Client } from 'discord.js' | ||
| import { incrementEloCronJobAllQueues } from '../utils/cronJobs' | ||
| import { initializeChannelPool } from '../utils/channelPool' | ||
|
|
||
| export default { | ||
| name: Events.ClientReady, | ||
| once: true, | ||
| async execute(client: Client) { | ||
| await incrementEloCronJobAllQueues() | ||
| console.log('Started up queues.') | ||
|
|
||
| // Initialize the channel pool | ||
| await initializeChannelPool() | ||
|
|
||
| console.log(`Ready! Logged in as ${client.user?.tag}`) | ||
| }, | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,325 @@ | ||
| import { | ||
| ChannelType, | ||
| OverwriteType, | ||
| PermissionFlagsBits, | ||
| TextChannel, | ||
| } from 'discord.js' | ||
| import { pool } from '../db' | ||
| import { getGuild } from '../client' | ||
| import { getAllUsersInQueue, getSettings } from './queryDB' | ||
|
|
||
| const POOL_SIZE = 10 // Number of channels to keep in the pool | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we can make these editable with a command for dynamic load handling |
||
| const MIN_POOL_SIZE = 5 // Minimum channels before creating more | ||
|
|
||
| /** | ||
| * Initializes the channel pool by creating pre-made channels | ||
| * Call this on bot startup | ||
| */ | ||
| export async function initializeChannelPool(): Promise<void> { | ||
| try { | ||
| console.log('Initializing channel pool...') | ||
|
|
||
| // Check how many channels are already in the pool | ||
| const existingChannels = await pool.query( | ||
| 'SELECT COUNT(*) FROM channel_pool WHERE in_use = false', | ||
| ) | ||
| const availableCount = parseInt(existingChannels.rows[0].count) | ||
|
|
||
| console.log(`Found ${availableCount} available channels in pool`) | ||
|
|
||
| // Calculate how many we need to create | ||
| const channelsToCreate = POOL_SIZE - availableCount | ||
|
|
||
| if (channelsToCreate <= 0) { | ||
| console.log('Channel pool is already full') | ||
| return | ||
| } | ||
|
|
||
| const guild = await getGuild() | ||
| const amountInQueue = await getAllUsersInQueue() | ||
| if (amountInQueue.length >= 2) { | ||
| console.log('Too many users in queue, waiting to create channels') | ||
| return | ||
| } | ||
|
|
||
| console.log(`Creating ${channelsToCreate} channels for the pool...`) | ||
|
|
||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this will just get us rate limited - we should only create pool channels when there is less than x players in queue - add to pool when numbers are low, use pool when numbers are high |
||
| // Create channels with a delay between each to avoid rate limiting | ||
| for (let i = 0; i < channelsToCreate; i++) { | ||
| try { | ||
| const channel = await guild.channels.create({ | ||
| name: `reserve-channel-${i + 1}`, | ||
| type: ChannelType.GuildText, | ||
| parent: null, // No category for reserve channels | ||
| permissionOverwrites: [ | ||
| { | ||
| id: guild.roles.everyone, | ||
| deny: [ | ||
| PermissionFlagsBits.ViewChannel, | ||
| PermissionFlagsBits.ReadMessageHistory, | ||
| ], | ||
| }, | ||
| ], | ||
| }) | ||
|
|
||
| // Add to database pool | ||
| await pool.query( | ||
| 'INSERT INTO channel_pool (channel_id, in_use) VALUES ($1, false)', | ||
| [channel.id], | ||
| ) | ||
|
|
||
| console.log( | ||
| `Created pooled channel ${i + 1}/${channelsToCreate}: ${channel.id}`, | ||
| ) | ||
|
|
||
| // Wait 2 seconds between channel creations to avoid rate limiting | ||
| if (i < channelsToCreate - 1) { | ||
| await new Promise((resolve) => setTimeout(resolve, 2000)) | ||
| } | ||
| } catch (err) { | ||
| console.error(`Failed to create pooled channel ${i + 1}:`, err) | ||
| // Continue trying to create remaining channels | ||
| } | ||
| } | ||
|
|
||
| console.log('Channel pool initialization complete') | ||
| } catch (err) { | ||
| console.error('Error initializing channel pool:', err) | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Gets an available channel from the pool for a match | ||
| * @param matchId - The match ID that will use this channel | ||
| * @param userIds - Array of user IDs who should have access to the channel | ||
| * @param channelName - Name to set for the channel | ||
| * @returns The text channel, or null if none available | ||
| */ | ||
| export async function getAvailableChannel( | ||
| matchId: number, | ||
| userIds: string[], | ||
| channelName: string, | ||
| ): Promise<TextChannel | null> { | ||
| const dbClient = await pool.connect() | ||
|
|
||
| try { | ||
| await dbClient.query('BEGIN') | ||
|
|
||
| // Get an available channel (with row lock to prevent race conditions) | ||
| const result = await dbClient.query( | ||
| `SELECT channel_id FROM channel_pool | ||
| WHERE in_use = false | ||
| LIMIT 1 | ||
| FOR UPDATE SKIP LOCKED`, | ||
| ) | ||
|
|
||
| if (result.rows.length === 0) { | ||
| await dbClient.query('ROLLBACK') | ||
| console.warn('No available channels in pool!') | ||
| return null | ||
| } | ||
|
|
||
| const channelId = result.rows[0].channel_id | ||
|
|
||
| // Mark channel as in use | ||
| await dbClient.query( | ||
| 'UPDATE channel_pool SET in_use = true, match_id = $1 WHERE channel_id = $2', | ||
| [matchId, channelId], | ||
| ) | ||
|
|
||
| await dbClient.query('COMMIT') | ||
|
|
||
| // Configure the channel | ||
| const guild = await getGuild() | ||
| const channel = (await guild.channels.fetch(channelId)) as TextChannel | ||
|
|
||
| if (!channel) { | ||
| console.error(`Channel ${channelId} not found in Discord`) | ||
| // Return it to pool since it's invalid | ||
| await pool.query( | ||
| 'UPDATE channel_pool SET in_use = false, match_id = null WHERE channel_id = $1', | ||
| [channelId], | ||
| ) | ||
| return null | ||
| } | ||
|
|
||
| const settings = await getSettings() | ||
|
|
||
| // Set up permissions for match users | ||
| const permissionOverwrites = [ | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. turn off message history (we need this for all match creation)
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. idk how to do this lol
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. but then we dont need to delete all the messages nescessarily, which might help with rate limiting (?) |
||
| { | ||
| id: guild.roles.everyone, | ||
| deny: [PermissionFlagsBits.ViewChannel], | ||
| }, | ||
| ...userIds.map((userId) => ({ | ||
| id: userId, | ||
| allow: [PermissionFlagsBits.ViewChannel], | ||
| type: OverwriteType.Member, | ||
| })), | ||
| ] | ||
|
|
||
| if (settings?.queue_helper_role_id) { | ||
| permissionOverwrites.push({ | ||
| id: settings.queue_helper_role_id, | ||
| allow: [PermissionFlagsBits.ViewChannel], | ||
| type: OverwriteType.Role, | ||
| }) | ||
| } | ||
|
|
||
| // Get the category for active matches | ||
| const categoryId = settings.queue_category_id | ||
| const backupCat = '1427367817803464914' | ||
|
|
||
| const category = await guild.channels.fetch(categoryId) | ||
| const channelCount = | ||
| category && category.type === ChannelType.GuildCategory | ||
| ? category.children.cache.size | ||
| : 0 | ||
|
|
||
| // Update channel name, permissions, and move to match category | ||
| await channel.edit({ | ||
| name: channelName, | ||
| parent: channelCount > 45 ? backupCat : categoryId, | ||
| permissionOverwrites: permissionOverwrites, | ||
| }) | ||
|
|
||
| console.log(`Assigned channel ${channelId} to match ${matchId}`) | ||
|
|
||
| // Check if pool needs refilling (async, don't wait) | ||
| maintainChannelPool().catch((err) => | ||
| console.error('Error maintaining channel pool:', err), | ||
| ) | ||
|
|
||
| return channel | ||
| } catch (err) { | ||
| await dbClient.query('ROLLBACK') | ||
| console.error('Error getting available channel:', err) | ||
| return null | ||
| } finally { | ||
| dbClient.release() | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Returns a channel to the pool after a match ends | ||
| * @param channelId - The Discord channel ID to return to pool | ||
| */ | ||
| export async function returnChannelToPool(channelId: string): Promise<void> { | ||
| try { | ||
| const guild = await getGuild() | ||
| const channel = (await guild.channels.fetch(channelId)) as TextChannel | ||
|
|
||
| if (!channel) { | ||
| console.warn(`Channel ${channelId} not found, removing from pool`) | ||
| await pool.query('DELETE FROM channel_pool WHERE channel_id = $1', [ | ||
| channelId, | ||
| ]) | ||
| return | ||
| } | ||
|
|
||
| // Reset channel to default state (no category, hidden, history hidden) | ||
| await channel.edit({ | ||
| name: 'reserve-channel', | ||
| parent: null, // Remove from category | ||
| permissionOverwrites: [ | ||
| { | ||
| id: guild.roles.everyone, | ||
| deny: [ | ||
| PermissionFlagsBits.ViewChannel, | ||
| PermissionFlagsBits.ReadMessageHistory, | ||
| ], | ||
| }, | ||
| ], | ||
| }) | ||
|
|
||
| // Mark as available in database | ||
| await pool.query( | ||
| 'UPDATE channel_pool SET in_use = false, match_id = null WHERE channel_id = $1', | ||
| [channelId], | ||
| ) | ||
|
|
||
| console.log(`Returned channel ${channelId} to pool`) | ||
| } catch (err) { | ||
| console.error(`Error returning channel ${channelId} to pool:`, err) | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Maintains the channel pool by creating new channels if below minimum | ||
| * Can be called periodically or after channels are used | ||
| */ | ||
| export async function maintainChannelPool(): Promise<void> { | ||
| try { | ||
| const result = await pool.query( | ||
| 'SELECT COUNT(*) FROM channel_pool WHERE in_use = false', | ||
| ) | ||
| const availableCount = parseInt(result.rows[0].count) | ||
|
|
||
| if (availableCount >= MIN_POOL_SIZE) { | ||
| return | ||
| } | ||
|
|
||
| console.log( | ||
| `Channel pool low (${availableCount}), creating more channels...`, | ||
| ) | ||
|
|
||
| const channelsToCreate = POOL_SIZE - availableCount | ||
| const guild = await getGuild() | ||
|
|
||
| for (let i = 0; i < channelsToCreate; i++) { | ||
| try { | ||
| const channel = await guild.channels.create({ | ||
| name: `reserve-channel`, | ||
| type: ChannelType.GuildText, | ||
| parent: null, // No category for reserve channels | ||
| permissionOverwrites: [ | ||
| { | ||
| id: guild.roles.everyone, | ||
| deny: [PermissionFlagsBits.ViewChannel], | ||
| }, | ||
| ], | ||
| }) | ||
|
|
||
| await pool.query( | ||
| 'INSERT INTO channel_pool (channel_id, in_use) VALUES ($1, false)', | ||
| [channel.id], | ||
| ) | ||
|
|
||
| console.log(`Created maintenance channel ${i + 1}/${channelsToCreate}`) | ||
|
|
||
| // Wait 2 seconds between creations | ||
| if (i < channelsToCreate - 1) { | ||
| await new Promise((resolve) => setTimeout(resolve, 2000)) | ||
| } | ||
| } catch (err) { | ||
| console.error(`Failed to create maintenance channel ${i + 1}:`, err) | ||
| } | ||
| } | ||
| } catch (err) { | ||
| console.error('Error maintaining channel pool:', err) | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Cleans up orphaned channels (channels in pool that no longer exist in Discord) | ||
| */ | ||
| export async function cleanupChannelPool(): Promise<void> { | ||
| try { | ||
| const result = await pool.query('SELECT channel_id FROM channel_pool') | ||
| const guild = await getGuild() | ||
|
|
||
| for (const row of result.rows) { | ||
| try { | ||
| await guild.channels.fetch(row.channel_id) | ||
| } catch (err) { | ||
| // Channel doesn't exist, remove from pool | ||
| console.log(`Removing orphaned channel ${row.channel_id} from pool`) | ||
| await pool.query('DELETE FROM channel_pool WHERE channel_id = $1', [ | ||
| row.channel_id, | ||
| ]) | ||
| } | ||
| } | ||
| } catch (err) { | ||
| console.error('Error cleaning up channel pool:', err) | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this shouldnt be renamed seeming as the timestamp already comes after the last one