Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
24 changes: 24 additions & 0 deletions migrations/1760733866474_queue-log-things.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* @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.addColumn('matches', { results_msg_id: { type: 'varchar(255)', notNull: false, default: null }})
pgm.addColumn('matches', { queue_log_msg_id: { type: 'varchar(255)', notNull: false, default: null }})
};

/**
* @param pgm {import('node-pg-migrate').MigrationBuilder}
* @param run {() => void | undefined}
* @returns {Promise<void> | void}
*/
export const down = (pgm) => {
pgm.dropColumn('matches', 'results_msg_id');
pgm.dropColumn('matches', 'queue_log_msg_id');
};
4 changes: 2 additions & 2 deletions src/@types/global.d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Types for the database stuff

declare module 'psqlDB' {
import { EmbedField } from 'discord.js'
import { ColorResolvable, EmbedField } from 'discord.js'

export interface Queues {
id: number
Expand Down Expand Up @@ -188,7 +188,7 @@ declare module 'psqlDB' {
export type EmbedType = {
title: string | null
description: string | null
color: number | null
color: ColorResolvable | null
fields: EmbedField[] | null
footer: { text: string } | null
blame: string | null
Expand Down
184 changes: 139 additions & 45 deletions src/api/routers/commands/stats.router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,10 @@ const statsRouter = new OpenAPIHono()
statsRouter.openapi(
createRoute({
method: 'get',
path: '/{user_id}/{queue_id}',
description: 'Get player statistics for a specific queue.',
path: '/leaderboard/{queue_id}',
description: 'Get leaderboard for a specific queue.',
request: {
params: z.object({
user_id: z.string().openapi({
param: {
name: 'user_id',
in: 'path',
},
example: '123456789012345678',
}),
queue_id: z
.string()
.regex(/^\d+$/)
Expand All @@ -29,37 +22,39 @@ statsRouter.openapi(
example: '1',
}),
}),
query: z.object({
limit: z
.string()
.regex(/^\d+$/)
.transform(Number)
.optional()
.openapi({
param: {
name: 'limit',
in: 'query',
},
example: '100',
}),
}),
},
responses: {
200: {
content: {
'application/json': {
schema: z.object({
mmr: z.number(),
wins: z.number(),
losses: z.number(),
streak: z.number(),
totalgames: z.number(),
decay: z.number(),
ign: z.any().nullable(),
peak_mmr: z.number(),
peak_streak: z.number(),
rank: z.number(),
winrate: z.number(),
}),
},
},
description: 'Player statistics retrieved successfully.',
},
404: {
content: {
'application/json': {
schema: z.object({
error: z.string(),
leaderboard: z.array(
z.object({
rank: z.number(),
user_id: z.string(),
mmr: z.number(),
wins: z.number(),
losses: z.number(),
}),
),
}),
},
},
description: 'Player not found in this queue.',
description: 'Leaderboard retrieved successfully.',
},
500: {
content: {
Expand All @@ -74,26 +69,24 @@ statsRouter.openapi(
},
}),
async (c) => {
const { user_id, queue_id } = c.req.valid('param')
const { queue_id } = c.req.valid('param')
const { limit } = c.req.valid('query')
const finalLimit = limit || 100

try {
const stats = await COMMAND_HANDLERS.STATS.GET_PLAYER_STATS(
user_id,
const leaderboard = await COMMAND_HANDLERS.STATS.GET_LEADERBOARD(
queue_id,
finalLimit,
)

if (!stats) {
return c.json(
{
error: 'Player not found in this queue.',
},
404,
)
}

return c.json(stats, 200)
return c.json(
{
leaderboard,
},
200,
)
} catch (error) {
console.error('Error fetching player stats:', error)
console.error('Error fetching leaderboard:', error)
return c.json(
{
error: 'Internal server error',
Expand Down Expand Up @@ -211,4 +204,105 @@ statsRouter.openapi(
},
)

statsRouter.openapi(
createRoute({
method: 'get',
path: '/{user_id}/{queue_id}',
description: 'Get player statistics for a specific queue.',
request: {
params: z.object({
user_id: z.string().openapi({
param: {
name: 'user_id',
in: 'path',
},
example: '123456789012345678',
}),
queue_id: z
.string()
.regex(/^\d+$/)
.transform(Number)
.openapi({
param: {
name: 'queue_id',
in: 'path',
},
example: '1',
}),
}),
},
responses: {
200: {
content: {
'application/json': {
schema: z.object({
mmr: z.number(),
wins: z.number(),
losses: z.number(),
streak: z.number(),
totalgames: z.number(),
decay: z.number(),
ign: z.any().nullable(),
peak_mmr: z.number(),
peak_streak: z.number(),
rank: z.number(),
winrate: z.number(),
}),
},
},
description: 'Player statistics retrieved successfully.',
},
404: {
content: {
'application/json': {
schema: z.object({
error: z.string(),
}),
},
},
description: 'Player not found in this queue.',
},
500: {
content: {
'application/json': {
schema: z.object({
error: z.string(),
}),
},
},
description: 'Internal server error.',
},
},
}),
async (c) => {
const { user_id, queue_id } = c.req.valid('param')

try {
const stats = await COMMAND_HANDLERS.STATS.GET_PLAYER_STATS(
user_id,
queue_id,
)

if (!stats) {
return c.json(
{
error: 'Player not found in this queue.',
},
404,
)
}

return c.json(stats, 200)
} catch (error) {
console.error('Error fetching player stats:', error)
return c.json(
{
error: 'Internal server error',
},
500,
)
}
},
)

export { statsRouter }
28 changes: 28 additions & 0 deletions src/command-handlers/stats/getLeaderboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { getQueueLeaderboard } from '../../utils/queryDB'

export type LeaderboardEntry = {
rank: number
user_id: string
mmr: number
wins: number
losses: number
}

/**
* Gets leaderboard data for a specific queue.
*
* @param {number} queueId - The queue ID to fetch leaderboard for.
* @param {number} limit - Maximum number of entries to return.
* @return {Promise<LeaderboardEntry[]>} A promise that resolves to the leaderboard data.
*/
export async function getLeaderboard(
queueId: number,
limit: number = 100,
): Promise<LeaderboardEntry[]> {
try {
return await getQueueLeaderboard(queueId, limit)
} catch (error) {
console.error('Error fetching leaderboard:', error)
throw error
}
}
2 changes: 2 additions & 0 deletions src/command-handlers/stats/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { getPlayerStats } from './getPlayerStats'
import { getMatchHistory } from './getMatchHistory'
import { getLeaderboard } from './getLeaderboard'

export const STATS_COMMAND_HANDLERS = {
GET_PLAYER_STATS: getPlayerStats,
GET_MATCH_HISTORY: getMatchHistory,
GET_LEADERBOARD: getLeaderboard,
}
34 changes: 15 additions & 19 deletions src/commands/moderation/giveWin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,14 @@ export default {
.setAutocomplete(true),
),
async execute(interaction: ChatInputCommandInteraction) {
await interaction.deferReply({ flags: MessageFlags.Ephemeral })

try {
const matchId = interaction.options.getInteger('match-id')
const userId = interaction.options.getString('user', true)
if (!matchId) {
await interaction.reply({
await interaction.editReply({
content: 'No match found.',
flags: MessageFlags.Ephemeral,
})
return
}
Expand All @@ -50,23 +51,18 @@ export default {
matchId,
])

await endMatch(matchId)
// End match
await endMatch(matchId, false)

await interaction.reply(
`Assigned win to <@${interaction.options.getString('user', true)}>.`,
)
await interaction.editReply({
content: `Successfully assigned win to <@${userId}> (Team ${winningTeam}) for Match #${matchId}.`,
})
} catch (err: any) {
if (interaction.deferred || interaction.replied) {
await interaction.editReply({
content: `Failed to assign win. Reason: ${err}`,
})
} else {
await interaction.reply({
content: `Failed to assign win. Reason: ${err}`,
flags: MessageFlags.Ephemeral,
})
}
console.error(err)
console.error('Error assigning win:', err)
const errorMessage = err instanceof Error ? err.message : String(err)
await interaction.editReply({
content: `Failed to assign win.\nError: ${errorMessage}`,
})
}
},

Expand Down Expand Up @@ -98,15 +94,15 @@ export default {
} else if (currentValue.name === 'match-id') {
const input = currentValue.value
const matches = await pool.query(
'SELECT id FROM matches WHERE open = true',
'SELECT id FROM matches ORDER BY id DESC',
)
if (!matches.rows || matches.rows.length === 0) {
await interaction.respond([])
return
}
const matchIds = matches.rows.map((match: any) => ({
name: (interaction.channelId = match.id.toString())
? `${match.id.toString()} (current channel)`
? `${match.id.toString()}`
: match.id.toString(),
value: match.id.toString(),
}))
Expand Down
Loading