mirror of
https://github.com/ershisan99/www.git
synced 2026-01-02 12:35:09 +00:00
wip
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import { createTRPCRouter, publicProcedure } from '@/server/api/trpc'
|
||||
import { LeaderboardService } from '@/server/services/leaderboard'
|
||||
import { neatqueue_service } from '@/server/services/neatqueue.service'
|
||||
import { z } from 'zod'
|
||||
const service = new LeaderboardService()
|
||||
|
||||
export const leaderboard_router = createTRPCRouter({
|
||||
get_leaderboard: publicProcedure
|
||||
@@ -10,6 +12,16 @@ export const leaderboard_router = createTRPCRouter({
|
||||
})
|
||||
)
|
||||
.query(async ({ input }) => {
|
||||
return await neatqueue_service.get_leaderboard(input.channel_id)
|
||||
return await service.getLeaderboard(input.channel_id)
|
||||
}),
|
||||
get_user_rank: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
channel_id: z.string(),
|
||||
user_id: z.string(),
|
||||
})
|
||||
)
|
||||
.query(async ({ input }) => {
|
||||
return await service.getUserRank(input.channel_id, input.user_id)
|
||||
}),
|
||||
})
|
||||
|
||||
4
src/server/redis.ts
Normal file
4
src/server/redis.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { env } from '@/env'
|
||||
import { Redis } from 'ioredis'
|
||||
|
||||
export const redis = new Redis(env.REDIS_URL)
|
||||
93
src/server/services/leaderboard.ts
Normal file
93
src/server/services/leaderboard.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { redis } from '../redis'
|
||||
import { neatqueue_service } from './neatqueue.service'
|
||||
export class LeaderboardService {
|
||||
private getZSetKey(channel_id: string) {
|
||||
return `zset:leaderboard:${channel_id}`
|
||||
}
|
||||
|
||||
private getRawKey(channel_id: string) {
|
||||
return `raw:leaderboard:${channel_id}`
|
||||
}
|
||||
|
||||
async refreshLeaderboard(channel_id: string) {
|
||||
const fresh = await neatqueue_service.get_leaderboard(channel_id)
|
||||
const zsetKey = this.getZSetKey(channel_id)
|
||||
const rawKey = this.getRawKey(channel_id)
|
||||
|
||||
// store raw data for full queries
|
||||
await redis.setex(rawKey, 180, JSON.stringify(fresh))
|
||||
|
||||
// store sorted set for rank queries
|
||||
const pipeline = redis.pipeline()
|
||||
pipeline.del(zsetKey) // clear existing
|
||||
|
||||
for (const entry of fresh.alltime) {
|
||||
// store by mmr for ranking
|
||||
pipeline.zadd(zsetKey, entry.data.mmr, entry.id)
|
||||
|
||||
// store user data separately for quick lookups
|
||||
pipeline.hset(`user:${entry.id}`, {
|
||||
name: entry.name,
|
||||
mmr: entry.data.mmr,
|
||||
wins: entry.data.wins,
|
||||
losses: entry.data.losses,
|
||||
// add other fields you need for quick lookup
|
||||
})
|
||||
}
|
||||
|
||||
pipeline.expire(zsetKey, 180)
|
||||
await pipeline.exec()
|
||||
}
|
||||
|
||||
async getLeaderboard(channel_id: string) {
|
||||
const cached = await redis.get(this.getRawKey(channel_id))
|
||||
if (cached) return JSON.parse(cached)
|
||||
|
||||
// if not cached, refresh and return
|
||||
await this.refreshLeaderboard(channel_id)
|
||||
// @ts-ignore
|
||||
return redis.get(this.getRawKey(channel_id)).then(JSON.parse)
|
||||
}
|
||||
|
||||
async getUserRank(channel_id: string, user_id: string) {
|
||||
const zsetKey = this.getZSetKey(channel_id)
|
||||
|
||||
// zrevrank because higher mmr = better rank
|
||||
const rank = await redis.zrevrank(zsetKey, user_id)
|
||||
if (rank === null) return null
|
||||
|
||||
// get user data
|
||||
const userData = await redis.hgetall(`user:${user_id}`)
|
||||
if (!userData) return null
|
||||
|
||||
return {
|
||||
rank: rank + 1, // zero-based -> one-based
|
||||
...userData,
|
||||
}
|
||||
}
|
||||
|
||||
// get users around a specific rank
|
||||
async getRankRange(channel_id: string, rank: number, range = 5) {
|
||||
const zsetKey = this.getZSetKey(channel_id)
|
||||
|
||||
// get ids
|
||||
const ids = await redis.zrevrange(
|
||||
zsetKey,
|
||||
Math.max(0, rank - range),
|
||||
rank + range
|
||||
)
|
||||
|
||||
// get data for each id
|
||||
const pipeline = redis.pipeline()
|
||||
// biome-ignore lint/complexity/noForEach: <explanation>
|
||||
ids.forEach((id) => pipeline.hgetall(`user:${id}`))
|
||||
|
||||
const results = await pipeline.exec()
|
||||
return ids.map((id, i) => ({
|
||||
id,
|
||||
rank: rank - range + i + 1,
|
||||
// @ts-ignore
|
||||
...results[i][1],
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,10 @@ const NEATQUEUE_URL = 'https://api.neatqueue.com/api'
|
||||
|
||||
const instance = ky.create({
|
||||
prefixUrl: NEATQUEUE_URL,
|
||||
timeout: 10000,
|
||||
timeout: 60000,
|
||||
})
|
||||
|
||||
const BMM_SERVER_ID = '1352157545547960350'
|
||||
const BMM_SERVER_ID = '1226193436521267223'
|
||||
|
||||
export const neatqueue_service = {
|
||||
get_leaderboard: async (channel_id: string) => {
|
||||
@@ -15,7 +15,7 @@ export const neatqueue_service = {
|
||||
`leaderboard/${BMM_SERVER_ID}/${channel_id}`
|
||||
)
|
||||
|
||||
return response.json()
|
||||
return response.json<LeaderboardResponse>()
|
||||
},
|
||||
get_history: async (
|
||||
player_ids: string[],
|
||||
@@ -31,3 +31,25 @@ export const neatqueue_service = {
|
||||
return response
|
||||
},
|
||||
}
|
||||
export type Data = {
|
||||
mmr: number
|
||||
wins: number
|
||||
losses: number
|
||||
streak: number
|
||||
totalgames: number
|
||||
decay: number
|
||||
ign?: any
|
||||
peak_mmr: number
|
||||
peak_streak: number
|
||||
rank: number
|
||||
winrate: number
|
||||
}
|
||||
|
||||
export type LeaderboardEntry = {
|
||||
id: string
|
||||
data: Data
|
||||
name: string
|
||||
}
|
||||
export type LeaderboardResponse = {
|
||||
alltime: LeaderboardEntry[]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user