Skip to content

Commit 751f6be

Browse files
committed
2 parents e6a91c3 + 1c6884e commit 751f6be

File tree

6 files changed

+476
-21
lines changed

6 files changed

+476
-21
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/**
2+
* @type {import('node-pg-migrate').ColumnDefinitions | undefined}
3+
*/
4+
export const shorthands = undefined;
5+
6+
/**
7+
* @param pgm {import('node-pg-migrate').MigrationBuilder}
8+
* @param run {() => void | undefined}
9+
* @returns {Promise<void> | void}
10+
*/
11+
export const up = (pgm) => {
12+
pgm.createTable('channel_pool', {
13+
id: 'id',
14+
channel_id: { type: 'varchar(255)', notNull: true, unique: true },
15+
in_use: { type: 'boolean', notNull: true, default: false },
16+
match_id: { type: 'integer', notNull: false, default: null },
17+
created_at: {
18+
type: 'timestamp',
19+
notNull: true,
20+
default: pgm.func('current_timestamp'),
21+
},
22+
});
23+
24+
// Add index for faster lookups of available channels
25+
pgm.createIndex('channel_pool', 'in_use');
26+
};
27+
28+
/**
29+
* @param pgm {import('node-pg-migrate').MigrationBuilder}
30+
* @param run {() => void | undefined}
31+
* @returns {Promise<void> | void}
32+
*/
33+
export const down = (pgm) => {
34+
pgm.dropTable('channel_pool');
35+
};

src/events/ready.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
import { Events, Client } from 'discord.js'
22
import { incrementEloCronJobAllQueues } from '../utils/cronJobs'
3+
import { initializeChannelPool } from '../utils/channelPool'
34

45
export default {
56
name: Events.ClientReady,
67
once: true,
78
async execute(client: Client) {
89
await incrementEloCronJobAllQueues()
910
console.log('Started up queues.')
11+
12+
// Initialize the channel pool
13+
await initializeChannelPool()
14+
1015
console.log(`Ready! Logged in as ${client.user?.tag}`)
1116
},
1217
}

src/utils/channelPool.ts

Lines changed: 325 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,325 @@
1+
import {
2+
ChannelType,
3+
OverwriteType,
4+
PermissionFlagsBits,
5+
TextChannel,
6+
} from 'discord.js'
7+
import { pool } from '../db'
8+
import { getGuild } from '../client'
9+
import { getAllUsersInQueue, getSettings } from './queryDB'
10+
11+
const POOL_SIZE = 10 // Number of channels to keep in the pool
12+
const MIN_POOL_SIZE = 5 // Minimum channels before creating more
13+
14+
/**
15+
* Initializes the channel pool by creating pre-made channels
16+
* Call this on bot startup
17+
*/
18+
export async function initializeChannelPool(): Promise<void> {
19+
try {
20+
console.log('Initializing channel pool...')
21+
22+
// Check how many channels are already in the pool
23+
const existingChannels = await pool.query(
24+
'SELECT COUNT(*) FROM channel_pool WHERE in_use = false',
25+
)
26+
const availableCount = parseInt(existingChannels.rows[0].count)
27+
28+
console.log(`Found ${availableCount} available channels in pool`)
29+
30+
// Calculate how many we need to create
31+
const channelsToCreate = POOL_SIZE - availableCount
32+
33+
if (channelsToCreate <= 0) {
34+
console.log('Channel pool is already full')
35+
return
36+
}
37+
38+
const guild = await getGuild()
39+
const amountInQueue = await getAllUsersInQueue()
40+
if (amountInQueue.length >= 2) {
41+
console.log('Too many users in queue, waiting to create channels')
42+
return
43+
}
44+
45+
console.log(`Creating ${channelsToCreate} channels for the pool...`)
46+
47+
// Create channels with a delay between each to avoid rate limiting
48+
for (let i = 0; i < channelsToCreate; i++) {
49+
try {
50+
const channel = await guild.channels.create({
51+
name: `reserve-channel-${i + 1}`,
52+
type: ChannelType.GuildText,
53+
parent: null, // No category for reserve channels
54+
permissionOverwrites: [
55+
{
56+
id: guild.roles.everyone,
57+
deny: [
58+
PermissionFlagsBits.ViewChannel,
59+
PermissionFlagsBits.ReadMessageHistory,
60+
],
61+
},
62+
],
63+
})
64+
65+
// Add to database pool
66+
await pool.query(
67+
'INSERT INTO channel_pool (channel_id, in_use) VALUES ($1, false)',
68+
[channel.id],
69+
)
70+
71+
console.log(
72+
`Created pooled channel ${i + 1}/${channelsToCreate}: ${channel.id}`,
73+
)
74+
75+
// Wait 2 seconds between channel creations to avoid rate limiting
76+
if (i < channelsToCreate - 1) {
77+
await new Promise((resolve) => setTimeout(resolve, 2000))
78+
}
79+
} catch (err) {
80+
console.error(`Failed to create pooled channel ${i + 1}:`, err)
81+
// Continue trying to create remaining channels
82+
}
83+
}
84+
85+
console.log('Channel pool initialization complete')
86+
} catch (err) {
87+
console.error('Error initializing channel pool:', err)
88+
}
89+
}
90+
91+
/**
92+
* Gets an available channel from the pool for a match
93+
* @param matchId - The match ID that will use this channel
94+
* @param userIds - Array of user IDs who should have access to the channel
95+
* @param channelName - Name to set for the channel
96+
* @returns The text channel, or null if none available
97+
*/
98+
export async function getAvailableChannel(
99+
matchId: number,
100+
userIds: string[],
101+
channelName: string,
102+
): Promise<TextChannel | null> {
103+
const dbClient = await pool.connect()
104+
105+
try {
106+
await dbClient.query('BEGIN')
107+
108+
// Get an available channel (with row lock to prevent race conditions)
109+
const result = await dbClient.query(
110+
`SELECT channel_id FROM channel_pool
111+
WHERE in_use = false
112+
LIMIT 1
113+
FOR UPDATE SKIP LOCKED`,
114+
)
115+
116+
if (result.rows.length === 0) {
117+
await dbClient.query('ROLLBACK')
118+
console.warn('No available channels in pool!')
119+
return null
120+
}
121+
122+
const channelId = result.rows[0].channel_id
123+
124+
// Mark channel as in use
125+
await dbClient.query(
126+
'UPDATE channel_pool SET in_use = true, match_id = $1 WHERE channel_id = $2',
127+
[matchId, channelId],
128+
)
129+
130+
await dbClient.query('COMMIT')
131+
132+
// Configure the channel
133+
const guild = await getGuild()
134+
const channel = (await guild.channels.fetch(channelId)) as TextChannel
135+
136+
if (!channel) {
137+
console.error(`Channel ${channelId} not found in Discord`)
138+
// Return it to pool since it's invalid
139+
await pool.query(
140+
'UPDATE channel_pool SET in_use = false, match_id = null WHERE channel_id = $1',
141+
[channelId],
142+
)
143+
return null
144+
}
145+
146+
const settings = await getSettings()
147+
148+
// Set up permissions for match users
149+
const permissionOverwrites = [
150+
{
151+
id: guild.roles.everyone,
152+
deny: [PermissionFlagsBits.ViewChannel],
153+
},
154+
...userIds.map((userId) => ({
155+
id: userId,
156+
allow: [PermissionFlagsBits.ViewChannel],
157+
type: OverwriteType.Member,
158+
})),
159+
]
160+
161+
if (settings?.queue_helper_role_id) {
162+
permissionOverwrites.push({
163+
id: settings.queue_helper_role_id,
164+
allow: [PermissionFlagsBits.ViewChannel],
165+
type: OverwriteType.Role,
166+
})
167+
}
168+
169+
// Get the category for active matches
170+
const categoryId = settings.queue_category_id
171+
const backupCat = '1427367817803464914'
172+
173+
const category = await guild.channels.fetch(categoryId)
174+
const channelCount =
175+
category && category.type === ChannelType.GuildCategory
176+
? category.children.cache.size
177+
: 0
178+
179+
// Update channel name, permissions, and move to match category
180+
await channel.edit({
181+
name: channelName,
182+
parent: channelCount > 45 ? backupCat : categoryId,
183+
permissionOverwrites: permissionOverwrites,
184+
})
185+
186+
console.log(`Assigned channel ${channelId} to match ${matchId}`)
187+
188+
// Check if pool needs refilling (async, don't wait)
189+
maintainChannelPool().catch((err) =>
190+
console.error('Error maintaining channel pool:', err),
191+
)
192+
193+
return channel
194+
} catch (err) {
195+
await dbClient.query('ROLLBACK')
196+
console.error('Error getting available channel:', err)
197+
return null
198+
} finally {
199+
dbClient.release()
200+
}
201+
}
202+
203+
/**
204+
* Returns a channel to the pool after a match ends
205+
* @param channelId - The Discord channel ID to return to pool
206+
*/
207+
export async function returnChannelToPool(channelId: string): Promise<void> {
208+
try {
209+
const guild = await getGuild()
210+
const channel = (await guild.channels.fetch(channelId)) as TextChannel
211+
212+
if (!channel) {
213+
console.warn(`Channel ${channelId} not found, removing from pool`)
214+
await pool.query('DELETE FROM channel_pool WHERE channel_id = $1', [
215+
channelId,
216+
])
217+
return
218+
}
219+
220+
// Reset channel to default state (no category, hidden, history hidden)
221+
await channel.edit({
222+
name: 'reserve-channel',
223+
parent: null, // Remove from category
224+
permissionOverwrites: [
225+
{
226+
id: guild.roles.everyone,
227+
deny: [
228+
PermissionFlagsBits.ViewChannel,
229+
PermissionFlagsBits.ReadMessageHistory,
230+
],
231+
},
232+
],
233+
})
234+
235+
// Mark as available in database
236+
await pool.query(
237+
'UPDATE channel_pool SET in_use = false, match_id = null WHERE channel_id = $1',
238+
[channelId],
239+
)
240+
241+
console.log(`Returned channel ${channelId} to pool`)
242+
} catch (err) {
243+
console.error(`Error returning channel ${channelId} to pool:`, err)
244+
}
245+
}
246+
247+
/**
248+
* Maintains the channel pool by creating new channels if below minimum
249+
* Can be called periodically or after channels are used
250+
*/
251+
export async function maintainChannelPool(): Promise<void> {
252+
try {
253+
const result = await pool.query(
254+
'SELECT COUNT(*) FROM channel_pool WHERE in_use = false',
255+
)
256+
const availableCount = parseInt(result.rows[0].count)
257+
258+
if (availableCount >= MIN_POOL_SIZE) {
259+
return
260+
}
261+
262+
console.log(
263+
`Channel pool low (${availableCount}), creating more channels...`,
264+
)
265+
266+
const channelsToCreate = POOL_SIZE - availableCount
267+
const guild = await getGuild()
268+
269+
for (let i = 0; i < channelsToCreate; i++) {
270+
try {
271+
const channel = await guild.channels.create({
272+
name: `reserve-channel`,
273+
type: ChannelType.GuildText,
274+
parent: null, // No category for reserve channels
275+
permissionOverwrites: [
276+
{
277+
id: guild.roles.everyone,
278+
deny: [PermissionFlagsBits.ViewChannel],
279+
},
280+
],
281+
})
282+
283+
await pool.query(
284+
'INSERT INTO channel_pool (channel_id, in_use) VALUES ($1, false)',
285+
[channel.id],
286+
)
287+
288+
console.log(`Created maintenance channel ${i + 1}/${channelsToCreate}`)
289+
290+
// Wait 2 seconds between creations
291+
if (i < channelsToCreate - 1) {
292+
await new Promise((resolve) => setTimeout(resolve, 2000))
293+
}
294+
} catch (err) {
295+
console.error(`Failed to create maintenance channel ${i + 1}:`, err)
296+
}
297+
}
298+
} catch (err) {
299+
console.error('Error maintaining channel pool:', err)
300+
}
301+
}
302+
303+
/**
304+
* Cleans up orphaned channels (channels in pool that no longer exist in Discord)
305+
*/
306+
export async function cleanupChannelPool(): Promise<void> {
307+
try {
308+
const result = await pool.query('SELECT channel_id FROM channel_pool')
309+
const guild = await getGuild()
310+
311+
for (const row of result.rows) {
312+
try {
313+
await guild.channels.fetch(row.channel_id)
314+
} catch (err) {
315+
// Channel doesn't exist, remove from pool
316+
console.log(`Removing orphaned channel ${row.channel_id} from pool`)
317+
await pool.query('DELETE FROM channel_pool WHERE channel_id = $1', [
318+
row.channel_id,
319+
])
320+
}
321+
}
322+
} catch (err) {
323+
console.error('Error cleaning up channel pool:', err)
324+
}
325+
}

0 commit comments

Comments
 (0)