Skip to content
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
17 changes: 15 additions & 2 deletions src/commands/moderation/giveWin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,22 @@ export default {
const usersIdInMatch = await getUsersInMatch(matchId)
const users = await Promise.all(
usersIdInMatch.map(async (userId) => {
const user = await interaction.client.users.fetch(userId)
// Try to get member first for displayName, fallback to user
let displayName: string
try {
const member = await interaction.guild?.members.fetch(userId)
displayName = member?.displayName ?? ''
} catch {
try {
const user = await interaction.client.users.fetch(userId)
displayName = user.username
} catch {
displayName = 'name not found'
}
}

return {
name: user.username,
name: displayName,
value: userId,
}
}),
Expand Down
21 changes: 8 additions & 13 deletions src/commands/other/randomStake.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,28 @@ import { getRandomStake } from '../../utils/matchHelpers'
import {
getMatchData,
getMatchIdFromChannel,
getStakeList,
setPickedMatchStake,
} from '../../utils/queryDB'

export default {
async execute(interaction: ChatInputCommandInteraction) {
try {
const customStake =
interaction.options.getString('custom-stake', false) ?? false
const custom = customStake == 'yes'
const matchId = await getMatchIdFromChannel(interaction.channelId)
const stakeChoice = await getRandomStake(custom)

if (matchId) {
// In a match channel
const stakeList = await getStakeList()
const randomStake =
stakeList[Math.floor(Math.random() * stakeList.length)]
const matchData = await getMatchData(matchId)

if (!matchData.stake_vote_ended)
await setPickedMatchStake(matchId, randomStake.stake_name)

const stakeStr = `${randomStake.stake_emote} ${randomStake.stake_name}`
await interaction.reply({ content: stakeStr })
} else {
// Not in a match channel - use normal logic
const stakeChoice = await getRandomStake()
const stakeStr = `${stakeChoice.stake_emote} ${stakeChoice.stake_name}`
await interaction.reply({ content: stakeStr })
await setPickedMatchStake(matchId, stakeChoice.stake_name)
}

const stakeStr = `${stakeChoice.stake_emote} ${stakeChoice.stake_name}`
await interaction.reply({ content: stakeStr })
} catch (err: any) {
console.error(err)
const errorMsg = err.detail || err.message || 'Unknown'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { ChatInputCommandInteraction, MessageFlags } from 'discord.js'
import { drawPlayerStatsCanvas } from '../../utils/canvasHelpers'
import { getQueueIdFromName, getStatsCanvasUserData } from '../../utils/queryDB'
import { setupViewStatsButtons } from '../../utils/queueHelpers'
import {
setupViewStatsButtons,
setUserQueueRole,
} from '../../utils/queueHelpers'

export default {
async execute(interaction: ChatInputCommandInteraction) {
Expand All @@ -19,6 +22,10 @@ export default {
files: [statFile],
components: [viewStatsButtons],
})

// Update queue role, just to be sure it's correct when they check
// This is a nice bandaid fix till we update leaderboard roles better
await setUserQueueRole(queueId, targetUser.id)
} catch (err: any) {
console.error(err)
const errorMsg = err.detail || err.message || 'Unknown'
Expand Down
11 changes: 10 additions & 1 deletion src/commands/superCommands/random.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,16 @@ export default {
sub.setName('deck').setDescription('Roll a random deck'),
)
.addSubcommand((sub) =>
sub.setName('stake').setDescription('Roll a random stake'),
sub
.setName('stake')
.setDescription('Roll a random stake')
.addStringOption((option) =>
option
.setName('custom-stake')
.setDescription('Whether to include custom stakes or not')
.addChoices([{ name: 'yes', value: 'yes' }])
.setRequired(false),
),
),

async execute(interaction: ChatInputCommandInteraction) {
Expand Down
2 changes: 1 addition & 1 deletion src/commands/superCommands/stats.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import viewStats from '../queues/viewStats'
import viewStats from '../queues/statsQueue'
import {
AutocompleteInteraction,
ChatInputCommandInteraction,
Expand Down
48 changes: 48 additions & 0 deletions src/events/interactionCreate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -877,6 +877,20 @@ export default {
console.error('Failed to fetch original message embed:', err)
}

// Add delete button for helpers
const deleteButtonRow =
new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder()
.setCustomId(`delete-contest-${matchId}`)
.setLabel('Delete Contest Channel')
.setStyle(ButtonStyle.Danger),
)

await contestChannel.send({
content: 'Helpers can delete this contest channel when resolved:',
components: [deleteButtonRow],
})

await interaction.update({
content: `Contest channel created! Please go here to contest this matchup with the staff: ${contestChannel}`,
components: [],
Expand All @@ -888,6 +902,40 @@ export default {
await interaction.deleteReply()
}

if (interaction.customId.startsWith('delete-contest-')) {
const matchId = parseInt(interaction.customId.split('-')[2])
const botSettings = await getSettings()
const member = interaction.member as GuildMember

// Check if user is a helper
if (
member &&
(member.roles.cache.has(botSettings.helper_role_id) ||
member.roles.cache.has(botSettings.queue_helper_role_id))
) {
// User is a helper, delete the channel
await interaction.reply({
content: 'Deleting contest channel...',
flags: MessageFlags.Ephemeral,
})

try {
await interaction.channel?.delete()
} catch (err) {
console.error(
`Failed to delete contest channel for match ${matchId}:`,
err,
)
}
} else {
// User is not a helper
await interaction.reply({
content: 'Only helpers can delete contest channels.',
flags: MessageFlags.Ephemeral,
})
}
}

if (interaction.customId.startsWith('rematch-')) {
const matchId = parseInt(interaction.customId.split('-')[1])
const matchData = await getMatchData(matchId)
Expand Down
42 changes: 29 additions & 13 deletions src/utils/queryDB.ts
Original file line number Diff line number Diff line change
Expand Up @@ -385,20 +385,22 @@ export async function getLeaderboardPosition(
queueId: number,
userId: string,
): Promise<number | null> {
const playersRes = await pool.query(
const result = await pool.query(
`
SELECT user_id
FROM queue_users
WHERE queue_id = $1
ORDER BY elo DESC
SELECT rank
FROM (
SELECT user_id, ROW_NUMBER() OVER (ORDER BY elo DESC) as rank
FROM queue_users
WHERE queue_id = $1
) ranked
WHERE user_id = $2
`,
[queueId],
[queueId, userId],
)

if (playersRes.rowCount === 0) return null
if (result.rowCount === 0) return null

const players: { user_id: string }[] = playersRes.rows
return players.findIndex((p) => p.user_id === userId) + 1
return result.rows[0].rank
}

export async function getLeaderboardQueueRole(
Expand All @@ -419,8 +421,6 @@ export async function getLeaderboardQueueRole(
[queueId, rank],
)

console.log(roleRes.rowCount)

if (roleRes.rowCount === 0) return null
return roleRes.rows[0]
}
Expand Down Expand Up @@ -1513,11 +1513,27 @@ export async function getStatsCanvasUserData(
date: r.date as Date,
}))

// Get queue default_elo for initial data point
const queueSettings = await getQueueSettings(queueId)
const defaultElo = queueSettings.default_elo

const totalChange = eloChanges.reduce((sum, r) => sum + r.change, 0)
let running = (p.elo ?? 0) - totalChange
const elo_graph_data = eloChanges.map((r) => {

const elo_graph_data: { date: Date; rating: number }[] = []

// Add initial starting point if there are any matches
if (eloChanges.length > 0) {
const firstMatchDate = new Date(eloChanges[0].date)
const startDate = new Date(firstMatchDate.getTime() - 1000) // 1 second before
elo_graph_data.push({ date: startDate, rating: defaultElo })
}

// Add all match data points
eloChanges.forEach((r) => {
running += r.change
return { date: r.date, rating: running }
const clampedRating = Math.max(0, running) // Makes sure it stays within the range of 0-9999
elo_graph_data.push({ date: r.date, rating: clampedRating })
})

// Calculate percentiles for each stat using CTEs
Expand Down
76 changes: 44 additions & 32 deletions src/utils/queueHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -585,53 +585,65 @@ export async function setUserQueueRole(
): Promise<void> {
console.log(`setting queue role for user ${userId} in queue ${queueId}`)
const currentRole = await getUserQueueRole(queueId, userId)
const leaderboardRole = await getLeaderboardQueueRole(queueId, userId)
const allQueueRoles = await getAllQueueRoles(queueId, false)
const leaderboardRole = await getLeaderboardQueueRole(queueId, userId)

const guild = await getGuild()
const member = await guild.members.fetch(userId)

// Remove all MMR-based roles (where mmr_threshold is not null)
// Get current member roles
const currentRoleIds = new Set(member.roles.cache.keys())

// Determine which queue roles should be added/removed
const mmrRoles = allQueueRoles.filter((role) => role.mmr_threshold !== null)
for (const role of mmrRoles) {
try {
await member.roles.remove(role.role_id)
} catch (err) {
console.error(`Failed to remove MMR role ${role.role_id}:`, err)
const leaderboardRoles = allQueueRoles.filter(
(role) => role.leaderboard_min !== null,
)

const rolesToRemove: string[] = []
const rolesToAdd: string[] = []

// Check leaderboard roles
for (const role of leaderboardRoles) {
const expectedRole =
leaderboardRole && role.role_id === leaderboardRole.role_id

if (expectedRole && !currentRoleIds.has(role.role_id)) {
rolesToAdd.push(role.role_id)
} else if (!expectedRole && currentRoleIds.has(role.role_id)) {
rolesToRemove.push(role.role_id)
}
}

// Add the current MMR-based role if one exists
if (currentRole) {
try {
await member.roles.add(currentRole.role_id)
} catch (err) {
console.error(`Failed to add MMR role ${currentRole.role_id}:`, err)
// Check MMR roles
for (const role of mmrRoles) {
const expectedRole = currentRole && role.role_id === currentRole.role_id

// If the user has the expected role, check if they are within the MMR range
if (expectedRole && !currentRoleIds.has(role.role_id)) {
rolesToAdd.push(role.role_id)
} else if (!expectedRole && currentRoleIds.has(role.role_id)) {
rolesToRemove.push(role.role_id)
}
}

// Remove all leaderboard roles (where leaderboard_min is not null)
const leaderboardRoles = allQueueRoles.filter(
(role) => role.leaderboard_min !== null,
)
for (const role of leaderboardRoles) {
try {
await member.roles.remove(role.role_id)
} catch (err) {
console.error(`Failed to remove leaderboard role ${role.role_id}:`, err)
}
// Do nothing if no role changes are needed
if (rolesToRemove.length === 0 && rolesToAdd.length === 0) {
console.log(`No role changes needed for user ${userId}`)
return
}

// Add the current leaderboard role if one exists
if (leaderboardRole) {
try {
await member.roles.add(leaderboardRole.role_id)
} catch (err) {
console.error(
`Failed to add leaderboard role ${leaderboardRole.role_id}:`,
err,
)
try {
if (rolesToRemove.length > 0) {
await member.roles.remove(rolesToRemove)
console.log(`Removed ${rolesToRemove.length} roles from user ${userId}`)
}
if (rolesToAdd.length > 0) {
await member.roles.add(rolesToAdd)
console.log(`Added ${rolesToAdd.length} roles to user ${userId}`)
}
} catch (err) {
console.error(`Failed to update roles for user ${userId}:`, err)
}
}

Expand Down