mirror of
https://github.com/Rainyy21/lol_discordbot
synced 2026-10-10 23:50:32 -04:00
complete the bot
This commit is contained in:
@@ -0,0 +1,223 @@
|
||||
import discord
|
||||
from dataclasses import dataclass
|
||||
|
||||
# ── Data class ─────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@dataclass
|
||||
class MatchSummary:
|
||||
match_id: str
|
||||
champion: str
|
||||
won: bool
|
||||
kills: int
|
||||
deaths: int
|
||||
assists: int
|
||||
cs: int
|
||||
vision: int
|
||||
damage: int
|
||||
gold: int
|
||||
duration: int # seconds
|
||||
queue_id: int
|
||||
items: list[int]
|
||||
spell1_id: int
|
||||
spell2_id: int
|
||||
game_name: str
|
||||
tag_line: str
|
||||
patch: str
|
||||
|
||||
|
||||
# ── Parser ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def parse_match(
|
||||
match: dict, puuid: str, game_name: str, tag_line: str, patch: str
|
||||
) -> MatchSummary | None:
|
||||
"""Extract a clean MatchSummary from raw Riot API match data."""
|
||||
participants = match["info"]["participants"]
|
||||
player = next((p for p in participants if p["puuid"] == puuid), None)
|
||||
|
||||
if not player:
|
||||
return None
|
||||
|
||||
return MatchSummary(
|
||||
match_id=match["metadata"]["matchId"],
|
||||
champion=player["championName"],
|
||||
won=player["win"],
|
||||
kills=player["kills"],
|
||||
deaths=player["deaths"],
|
||||
assists=player["assists"],
|
||||
cs=player["totalMinionsKilled"] + player["neutralMinionsKilled"],
|
||||
vision=player["visionScore"],
|
||||
damage=player["totalDamageDealtToChampions"],
|
||||
gold=player["goldEarned"],
|
||||
duration=match["info"]["gameDuration"],
|
||||
queue_id=match["info"]["queueId"],
|
||||
items=[player[f"item{i}"] for i in range(7) if player.get(f"item{i}", 0) != 0],
|
||||
spell1_id=player["summoner1Id"],
|
||||
spell2_id=player["summoner2Id"],
|
||||
game_name=game_name,
|
||||
tag_line=tag_line,
|
||||
patch=patch,
|
||||
)
|
||||
|
||||
|
||||
# ── Formatters ─────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def fmt_kda(kills: int, deaths: int, assists: int) -> str:
|
||||
ratio = (kills + assists) / max(deaths, 1)
|
||||
return f"{kills}/{deaths}/{assists} ({ratio:.2f} KDA)"
|
||||
|
||||
|
||||
def fmt_duration(seconds: int) -> str:
|
||||
m, s = divmod(seconds, 60)
|
||||
return f"{m}m {s}s"
|
||||
|
||||
|
||||
def fmt_cs(cs: int, duration: int) -> str:
|
||||
per_min = round(cs / max(duration / 60, 1), 1)
|
||||
return f"{cs} ({per_min}/min)"
|
||||
|
||||
|
||||
def fmt_damage(damage: int) -> str:
|
||||
return f"{damage:,}"
|
||||
|
||||
|
||||
def fmt_gold(gold: int) -> str:
|
||||
return f"{gold:,} g"
|
||||
|
||||
|
||||
def fmt_rank_entry(entry: dict | None) -> str:
|
||||
if not entry:
|
||||
return "Unranked"
|
||||
tier = entry["tier"].capitalize()
|
||||
division = entry["rank"]
|
||||
lp = entry["leaguePoints"]
|
||||
wins = entry["wins"]
|
||||
losses = entry["losses"]
|
||||
total = wins + losses
|
||||
wr = f"{round(wins / total * 100)}%" if total > 0 else "N/A"
|
||||
return f"{tier} {division} — {lp} LP ({wr} WR)"
|
||||
|
||||
|
||||
QUEUE_NAMES = {
|
||||
420: "Ranked Solo/Duo",
|
||||
440: "Ranked Flex",
|
||||
450: "ARAM",
|
||||
400: "Normal Draft",
|
||||
430: "Normal Blind",
|
||||
900: "URF",
|
||||
1020: "One for All",
|
||||
1300: "Nexus Blitz",
|
||||
}
|
||||
|
||||
SUMMONER_SPELL_NAMES = {
|
||||
1: "Cleanse",
|
||||
3: "Exhaust",
|
||||
4: "Flash",
|
||||
6: "Ghost",
|
||||
7: "Heal",
|
||||
11: "Smite",
|
||||
12: "Teleport",
|
||||
13: "Clarity",
|
||||
14: "Ignite",
|
||||
21: "Barrier",
|
||||
32: "Mark",
|
||||
}
|
||||
|
||||
|
||||
def fmt_queue(queue_id: int) -> str:
|
||||
return QUEUE_NAMES.get(queue_id, "Other")
|
||||
|
||||
|
||||
def fmt_spells(spell1_id: int, spell2_id: int) -> str:
|
||||
s1 = SUMMONER_SPELL_NAMES.get(spell1_id, f"#{spell1_id}")
|
||||
s2 = SUMMONER_SPELL_NAMES.get(spell2_id, f"#{spell2_id}")
|
||||
return f"{s1} / {s2}"
|
||||
|
||||
|
||||
# ── Embed builders ─────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def build_match_embed(s: MatchSummary) -> discord.Embed:
|
||||
"""Full match embed with all stats."""
|
||||
result = "✅ Victory" if s.won else "❌ Defeat"
|
||||
color = 0x2ECC71 if s.won else 0xE74C3C
|
||||
|
||||
embed = discord.Embed(
|
||||
title=f"{result} — {s.champion}",
|
||||
description=f"**{fmt_queue(s.queue_id)}** · {fmt_duration(s.duration)}",
|
||||
color=color,
|
||||
)
|
||||
|
||||
embed.add_field(
|
||||
name="⚔️ KDA", value=fmt_kda(s.kills, s.deaths, s.assists), inline=True
|
||||
)
|
||||
embed.add_field(name="🌾 CS", value=fmt_cs(s.cs, s.duration), inline=True)
|
||||
embed.add_field(name="👁️ Vision", value=str(s.vision), inline=True)
|
||||
embed.add_field(name="💥 Damage", value=fmt_damage(s.damage), inline=True)
|
||||
embed.add_field(name="💰 Gold", value=fmt_gold(s.gold), inline=True)
|
||||
embed.add_field(
|
||||
name="🔮 Spells", value=fmt_spells(s.spell1_id, s.spell2_id), inline=True
|
||||
)
|
||||
embed.add_field(name="👤 Player", value=f"{s.game_name}#{s.tag_line}", inline=False)
|
||||
|
||||
embed.set_thumbnail(
|
||||
url=f"https://ddragon.leagueoflegends.com/cdn/{s.patch}/img/champion/{s.champion}.png"
|
||||
)
|
||||
embed.set_footer(text=f"Match ID: {s.match_id}")
|
||||
|
||||
return embed
|
||||
|
||||
|
||||
def build_compact_embed(s: MatchSummary) -> discord.Embed:
|
||||
"""Minimal one-liner embed, good for showing many matches at once."""
|
||||
result = "✅" if s.won else "❌"
|
||||
color = 0x2ECC71 if s.won else 0xE74C3C
|
||||
kda = fmt_kda(s.kills, s.deaths, s.assists)
|
||||
cs = fmt_cs(s.cs, s.duration)
|
||||
|
||||
embed = discord.Embed(
|
||||
description=(
|
||||
f"{result} **{s.champion}** · {fmt_queue(s.queue_id)} · {fmt_duration(s.duration)}\n"
|
||||
f"⚔️ {kda} · 🌾 {cs} · 💥 {fmt_damage(s.damage)}"
|
||||
),
|
||||
color=color,
|
||||
)
|
||||
embed.set_thumbnail(
|
||||
url=f"https://ddragon.leagueoflegends.com/cdn/{s.patch}/img/champion/{s.champion}.png"
|
||||
)
|
||||
return embed
|
||||
|
||||
|
||||
def build_match_list_embed(
|
||||
summaries: list[MatchSummary], game_name: str, tag_line: str
|
||||
) -> discord.Embed:
|
||||
"""Single embed summarizing multiple matches at once."""
|
||||
total = len(summaries)
|
||||
wins = sum(1 for s in summaries if s.won)
|
||||
losses = total - wins
|
||||
wr = f"{round(wins / total * 100)}%" if total > 0 else "N/A"
|
||||
|
||||
avg_kda = sum((s.kills + s.assists) / max(s.deaths, 1) for s in summaries) / max(
|
||||
total, 1
|
||||
)
|
||||
|
||||
embed = discord.Embed(
|
||||
title=f"📋 Last {total} matches — {game_name}#{tag_line}",
|
||||
description=f"**{wins}W / {losses}L** · {wr} WR · {avg_kda:.2f} avg KDA",
|
||||
color=0x1A78C2,
|
||||
)
|
||||
|
||||
for s in summaries:
|
||||
result = "✅" if s.won else "❌"
|
||||
embed.add_field(
|
||||
name=f"{result} {s.champion} — {fmt_queue(s.queue_id)}",
|
||||
value=(
|
||||
f"⚔️ {fmt_kda(s.kills, s.deaths, s.assists)} · "
|
||||
f"🌾 {fmt_cs(s.cs, s.duration)} · "
|
||||
f"⏱️ {fmt_duration(s.duration)}"
|
||||
),
|
||||
inline=False,
|
||||
)
|
||||
return embed
|
||||
@@ -0,0 +1,96 @@
|
||||
import aiohttp
|
||||
import asyncio
|
||||
import os
|
||||
import json
|
||||
from dotenv import load_dotenv
|
||||
from db import save_match, get_match as get_cached_match
|
||||
|
||||
RIOT_API_KEY = os.getenv("LEAGUEAPI")
|
||||
REGION = os.getenv("REGION", "na1")
|
||||
|
||||
CLUSTER_MAP = {
|
||||
"na1": "americas",
|
||||
"br1": "americas",
|
||||
"la1": "americas",
|
||||
"la2": "americas",
|
||||
"euw1": "europe",
|
||||
"eun1": "europe",
|
||||
"tr1": "europe",
|
||||
"ru": "europe",
|
||||
"kr": "asia",
|
||||
"jp1": "asia",
|
||||
}
|
||||
CLUSTER = CLUSTER_MAP.get(REGION, "americas")
|
||||
|
||||
|
||||
async def riot_get(url: str, retries: int = 3) -> dict | list | None:
|
||||
headers = {"X-Riot-Token": RIOT_API_KEY}
|
||||
async with aiohttp.ClientSession() as session:
|
||||
for attempt in range(retries):
|
||||
async with session.get(url, headers=headers) as resp:
|
||||
if resp.status == 200:
|
||||
return await resp.json()
|
||||
elif resp.status == 492:
|
||||
wait = int(resp.headers.get("Retry-After", 5))
|
||||
print(f"⚠️ Rate limited. Retrying in {wait}s...")
|
||||
await asyncio.sleep(wait)
|
||||
elif resp.status == 404:
|
||||
return None
|
||||
else:
|
||||
print(f"❌ Error {resp.status} (attempt {attempt + 1}): {url}")
|
||||
await asyncio.sleep(1)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# ── Endpoints ──────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
async def get_account(game_name: str, tag_line: str) -> dict | None:
|
||||
return await riot_get(
|
||||
f"https://{CLUSTER}.api.riotgames.com"
|
||||
f"/riot/account/v1/accounts/by-riot-id/{game_name}/{tag_line}"
|
||||
)
|
||||
|
||||
|
||||
async def get_summoner(puuid: str) -> dict | None:
|
||||
return await riot_get(
|
||||
f"https://{REGION}.api.riotgames.com"
|
||||
f"/lol/summoner/v4/summoners/by-puuid/{puuid}"
|
||||
)
|
||||
|
||||
|
||||
async def get_rank(summoner_id: str) -> dict | None:
|
||||
return await riot_get(
|
||||
f"https://{REGION}.api.riotgames.com"
|
||||
f"/lol/league/v4/entries/by-summoner/{summoner_id}"
|
||||
)
|
||||
|
||||
|
||||
async def get_match_ids(puuid: str, cont: int = 5) -> dict | None:
|
||||
return await riot_get(
|
||||
f"https://{CLUSTER}.api.riotgames.com"
|
||||
f"/lol/match/v5/matches/by-puuid/{puuid}/ids?count={count}"
|
||||
)
|
||||
|
||||
|
||||
async def get_match(match_id: str) -> dict | None:
|
||||
# check cache first
|
||||
cached = get_cached_match(match_id)
|
||||
if cached:
|
||||
return json.loads(cached)
|
||||
data = await riot_get(
|
||||
f"https://{CLUSTER}.api.riotgames.com" f"/lol/match/v5/matches/{match_id}"
|
||||
)
|
||||
if data:
|
||||
save_match(match_id, json.dumps(data))
|
||||
return data
|
||||
|
||||
|
||||
async def get_latest_patch() -> str:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(
|
||||
"https://ddragon.leagueoflegends.com/api/versions.json"
|
||||
) as resp:
|
||||
versions = await resp.json()
|
||||
return versions[0] if versions else "14.8.1"
|
||||
Reference in New Issue
Block a user