Compare commits
125 Commits
3d57521581
...
main
Author | SHA1 | Date | |
---|---|---|---|
550d089b35 | |||
347cbb2365 | |||
540bc45bfe | |||
8a52816c8c | |||
6b84cd6196 | |||
1500d4c6f8 | |||
67a8ce60bd | |||
8329290d38 | |||
fd0cd973bb | |||
200a8022f8 | |||
5fa2d7719f | |||
226942273c | |||
fd96ae9ea8 | |||
f410d470f0 | |||
1eb9017f12 | |||
8376e51dab | |||
5e0b267813 | |||
46875a4d6a | |||
66504294e6 | |||
4e43d46c00 | |||
9e500693f5 | |||
fbaaf0e28a | |||
115cc95722 | |||
aa12387202 | |||
3610e784f5 | |||
47df5486b6 | |||
08d9ae36b4 | |||
ad6925d320 | |||
0cf870da66 | |||
f4d98cc094 | |||
a9d6bedde1 | |||
e13cc3f7a8 | |||
7e3d3cd189 | |||
f74d202bc5 | |||
27da635494 | |||
8de1b89159 | |||
fcc556af33 | |||
6bc2686ab5 | |||
c2f49c0861 | |||
e7d1e5833e | |||
359dfcbf98 | |||
bcd6e8be45 | |||
72905f85a2 | |||
aaf0699f1d | |||
508223142e | |||
834af6d1b1 | |||
d196d3cba8 | |||
95b2dd2c79 | |||
111bebb2ea | |||
c39c654e67 | |||
12ea31902f | |||
6a44597d5a | |||
8fc0bcf85d | |||
9f1b8bbc10 | |||
834874526d | |||
4699557f0d | |||
b8cd55c763 | |||
dcac84dc67 | |||
25073a4278 | |||
54d97467c3 | |||
0b6fb3379d | |||
41012f11c3 | |||
603a169c39 | |||
a9db65f8b6 | |||
f1ce1bff30 | |||
7ea995f1ec | |||
cee70ab57d | |||
d66becf13a | |||
e61029fdf5 | |||
204e3aff28 | |||
e09c2ebd5b | |||
fdf8739fb8 | |||
308534b711 | |||
f7d07d2ed6 | |||
e784ea0f4d | |||
489a06504f | |||
d3772eae5e | |||
6ab4083f54 | |||
978f7535f3 | |||
e652c25709 | |||
5531063035 | |||
233fc7babf | |||
666d778c3d | |||
3c897ff9a6 | |||
b00fc8ac0c | |||
2297205327 | |||
1e3f1c0e5d | |||
d334d7b5bd | |||
465132d940 | |||
9b856fe289 | |||
df3a601fae | |||
43728f8807 | |||
697d3af498 | |||
7d1d710740 | |||
f7034de2fd | |||
0b1117fae2 | |||
6bdfe32e87 | |||
a56fc45b60 | |||
750faadf1b | |||
f6b5791916 | |||
b0bc592fc4 | |||
c6728a401b | |||
b7ecbe2630 | |||
3b90aa1d49 | |||
e1b575ecc8 | |||
4bbcb794c6 | |||
85a19d7eb0 | |||
b19b21dcf3 | |||
b430daa5ee | |||
685319042b | |||
a342c1b01d | |||
aab5c17527 | |||
ce0aba8b4d | |||
37eab5bc6f | |||
3fd5837a27 | |||
85e9ed46fc | |||
5d71faee8e | |||
1046c10d82 | |||
ea4f3ecf8f | |||
3514b3a32e | |||
5402b16f44 | |||
8391231390 | |||
7ae775276a | |||
eb89cbb240 | |||
44165c18a5 |
18
.gitignore
vendored
@ -1,7 +1,23 @@
|
||||
# Config
|
||||
config.ini
|
||||
|
||||
# Environment
|
||||
.env/
|
||||
|
||||
# Database
|
||||
db.sqlite3
|
||||
|
||||
# Build garbage
|
||||
__pycache__/
|
||||
migrations/
|
||||
build/
|
||||
dist/
|
||||
dist/
|
||||
main.spec
|
||||
src/client/custoMM_installer.exe
|
||||
|
||||
# VSCode garbage
|
||||
.vscode/
|
||||
|
||||
# LCU_connector leftover
|
||||
riotgames.pem
|
||||
test.py
|
||||
|
2
LICENSE
@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) <year> <copyright holders>
|
||||
Copyright (c) 2023 Boyan Karakostov
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
|
18
README.md
@ -1,32 +1,32 @@
|
||||
# custoMM
|
||||
<p align="center">
|
||||
<img src="images/logo.png"/>
|
||||
<img src="images/logo.png"/><br>
|
||||
<img alt="Python => 3.11" src="https://img.shields.io/badge/Python-%3E%3D3.11-green)"/>
|
||||
<img alt="Version 1.1.1" src="https://img.shields.io/badge/Version-1.1.1-blueviolet" />
|
||||
</p>
|
||||
|
||||
|
||||
|
||||
## What is this?
|
||||
This is a project inspired by a heavy loss while playing customs in league of legends. The idea is to create fair teams, without taking rank into account(cuz that's boring and we're antiestablishmentarians 🗿♿🥶).
|
||||
|
||||
|
||||
## Mechanism
|
||||
|
||||

|
||||
|
||||
CORRECTION, the client will be just a simple exe file that one runs once they want/finish a game. Lol.
|
||||

|
||||
|
||||
## Registration
|
||||
A user registers like so:
|
||||
1. Opens the client of custoMM
|
||||
2. Tells the designated bot that they want to register with their IGN(the bot assumes EUNE unless changed in the code) (e.g. !register leomesipicha)
|
||||
3. The client prompts the user if they really want to proceed, showing the discord username which initiated the registration. When a username is registered, it cannot be registered again(do not worry, even if you change your name in either LoL or Discord you will still be registered). You can still lend out your account, however, using the [guest mode](#guest-mode).
|
||||
|
||||
|
||||
Here's a flowchart explaining the technical stuff about this process:
|
||||
|
||||
|
||||

|
||||
|
||||
## Guest mode
|
||||
[Currently work in progress](https://git.confest.im/boyan_k/custoMM/issues/5#issue-17)
|
||||
|
||||
|
||||
|
||||
|
||||
If you want to use somebody else's account while playing customs, you can temporarily claim it, which will make the actual owner not gain MMR from the current game. That's about it. You, as the temporary player, do receive some MMR, however it is reduced by 50%, bc god damn get your own account wtf.
|
||||
|
||||
|
@ -1,24 +0,0 @@
|
||||
# Client
|
||||
|
||||
Heavily W.I.P. at the moment.
|
||||
|
||||
|
||||
## Config
|
||||
|
||||
Create a file called `config.py` and fill it with the following values(relevant to you):
|
||||
|
||||
```
|
||||
SITE_URL = "your-server.com"
|
||||
|
||||
```
|
||||
|
||||
## Releases
|
||||
Check out the releases for pre-compiled binaries of the client and server.
|
||||
|
||||
## TODOs
|
||||
|
||||
1. [x] Make the server and client talk to each other
|
||||
* [x] Based on MMR
|
||||
* [x] Based on cached IDs
|
||||
|
||||
|
@ -1,203 +0,0 @@
|
||||
from lcu_driver import Connector
|
||||
import requests
|
||||
# Edit config.json when running for the first time
|
||||
import asyncio
|
||||
import time, sys
|
||||
import configparser
|
||||
|
||||
# Config section
|
||||
config = configparser.ConfigParser()
|
||||
config.read("../config.ini")
|
||||
URL = config["DEFAULT"]["URL"]
|
||||
|
||||
# Test connection to server
|
||||
try:
|
||||
test = requests.get(URL).json()
|
||||
|
||||
except Exception:
|
||||
# NEVER DO THIS
|
||||
# although, what could go wrong...
|
||||
print("Server seems to be down, please contact admin if it keeps doing this")
|
||||
sys.exit()
|
||||
|
||||
|
||||
# needed for windows
|
||||
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
||||
|
||||
|
||||
# Init connection
|
||||
connector = Connector()
|
||||
|
||||
class WhatTheFuckDidYouDo(Exception):
|
||||
# We raise this when we don't know what the fuck happened
|
||||
def __init__(self, message="Congrats, you broke this program, please explain your steps as an issue at https://github.com/confestim/custoMM"):
|
||||
self.message = message
|
||||
super().__init__(self.message)
|
||||
|
||||
|
||||
def calculate_kda(kills:int, assists:int, deaths:int):
|
||||
"""
|
||||
Calculates kill, death, assist ratio
|
||||
Input: kills, assists, deaths
|
||||
Output: KDA ratio
|
||||
"""
|
||||
if deaths == 0:
|
||||
deaths = 1
|
||||
return round((kills+assists)/deaths, 3)
|
||||
|
||||
|
||||
async def parse_history(connection, history:dict, old_ids:list) -> list:
|
||||
"""
|
||||
Parses current player's history
|
||||
Input: Logged in player's match history
|
||||
Output: New data about unaccounted for custom games, ready to send to server
|
||||
"""
|
||||
|
||||
parsed_matches = []
|
||||
new = 0
|
||||
for i in history["games"]["games"]:
|
||||
if i["gameType"] == "CUSTOM_GAME" and str(i["gameId"]) not in old_ids and not i["gameMode"] == "PRACTICETOOL":
|
||||
|
||||
new += 1
|
||||
match = await connection.request('get', f'/lol-match-history/v1/games/{i["gameId"]}')
|
||||
match = await match.json()
|
||||
print(match)
|
||||
parsed_match = {
|
||||
"game_id": match["gameId"],
|
||||
"participants": {
|
||||
"t1": {
|
||||
"won": True if match["teams"][0]["win"] == "Won" else False,
|
||||
"summoners": []
|
||||
},
|
||||
"t2": {
|
||||
"won": True if match["teams"][1]["win"] == "Won" else False,
|
||||
"summoners": []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Sloppy solution, find fix.
|
||||
print("Extracting data...")
|
||||
for player in range(10):
|
||||
current_player = match["participants"][player]["stats"]
|
||||
kills = current_player["kills"]
|
||||
assists = current_player["assists"]
|
||||
deaths = current_player["deaths"]
|
||||
if player <= 5:
|
||||
parsed_match["participants"]["t1"]["summoners"].append({"name":match["participantIdentities"][player]["player"]["summonerName"], "kda": calculate_kda(kills, assists, deaths)})
|
||||
else:
|
||||
parsed_match["participants"]["t2"]["summoners"].append({"name":match["participantIdentities"][player]["player"]["summonerName"], "kda": calculate_kda(kills, assists, deaths)})
|
||||
parsed_matches.append(parsed_match)
|
||||
if not new:
|
||||
print("Already up to date, thanks. Program will now close.")
|
||||
time.sleep(3)
|
||||
sys.exit()
|
||||
return parsed_matches
|
||||
|
||||
# TODO: Format this list in the form of [{gid: GAME-ID, puuid:puuid}]
|
||||
|
||||
|
||||
# Get current summoner
|
||||
@connector.ready
|
||||
async def connect(connection):
|
||||
"""Data scraper for the league client"""
|
||||
|
||||
# TODO: Check if league is running
|
||||
|
||||
# Summoner
|
||||
summoner = await connection.request('get', '/lol-summoner/v1/current-summoner')
|
||||
summoner = await summoner.json()
|
||||
|
||||
|
||||
# Check if account is claimed
|
||||
try:
|
||||
claimed = requests.get(f"{URL}/players/?search={summoner['displayName']}").json()[0]
|
||||
except IndexError:
|
||||
|
||||
print("User does not exist, register through discord please.")
|
||||
print("Program will now close")
|
||||
time.sleep(5)
|
||||
sys.exit()
|
||||
# Case 3: It belongs to nobody and has yet to be claimed.
|
||||
if not claimed["discord"]:
|
||||
print("This account has not yet been claimed. Please claim it (if its yours) by typing in !claim ACCOUNTNAME to the bot in Discord and running this program again.")
|
||||
print(claimed)
|
||||
for i in range(10):
|
||||
time.sleep(.5)
|
||||
sys.stdout.write(".")
|
||||
sys.stdout.flush()
|
||||
|
||||
sys.exit()
|
||||
|
||||
if claimed:
|
||||
# Case 1: It belongs to somebody
|
||||
if claimed['lol_id'] and claimed['lol']:
|
||||
print(f"Welcome, {claimed['discord']}. Thank you for contributing to custoMM!")
|
||||
print(claimed)
|
||||
print("If this is not you, contact admin.")
|
||||
|
||||
# Notify them (if that is the case) that we will do nothing about their new name (slight TODO).
|
||||
if claimed['lol'] != summoner['displayName']:
|
||||
print("Ah, you have changed your league handle. Oh well. I will still refer to you with your old one, as this has not been handled properly yet.")
|
||||
|
||||
# Case 2: Registration has begun, but hasn't finished
|
||||
elif claimed['lol'] and not claimed['lol_id']:
|
||||
prompt = input(f"{claimed['discord']} is trying to claim this account(which you obviously own). Do you want to do that? [y/N] ")
|
||||
if prompt == ("y" or "Y"):
|
||||
# TODO: Update api entry
|
||||
account = requests.put(f"{URL}/players/{claimed['lol']}/", data={
|
||||
"lol": summoner["displayName"],
|
||||
"lol_id": summoner["puuid"],
|
||||
"discord_id":claimed["discord_id"],
|
||||
"discord":claimed["discord"]
|
||||
})
|
||||
if account.status_code == 200:
|
||||
print(f"Alright, the account is now yours, {claimed['discord']}.")
|
||||
else:
|
||||
print("Something went wrong when claiming your account...")
|
||||
else:
|
||||
#TODO: Delete api entry
|
||||
requests.delete(f"{URL}/players/{claimed['discord_id']}")
|
||||
print("Okay, deleting the attempt.")
|
||||
sys.exit()
|
||||
|
||||
# All cases are covered, everything else will be considered a bug.
|
||||
else:
|
||||
raise WhatTheFuckDidYouDo()
|
||||
|
||||
time.sleep(5)
|
||||
|
||||
|
||||
|
||||
# Match History
|
||||
match_history = await connection.request('get', '/lol-match-history/v1/products/lol/current-summoner/matches?endIndex=99')
|
||||
match_history = await match_history.json()
|
||||
|
||||
# Stage old ids in order for them to be parsed
|
||||
old_ids = requests.get(f"{URL}/games/").json()
|
||||
old_ids = [x["game_id"] for x in old_ids]
|
||||
|
||||
|
||||
# TODO: Optimize the process of acquisition of new matches
|
||||
games = await parse_history(connection, match_history, old_ids)
|
||||
|
||||
# Post the new games to your server(change in config.json)
|
||||
for i in games:
|
||||
req = requests.post(f"{URL}/games/", json=i)
|
||||
print(req.content)
|
||||
if req.status_code == 500:
|
||||
print("Serverside error! Contact maintainer!")
|
||||
sys.exit()
|
||||
print("We have added " + str(len(games)) + " games that were unaccounted for to the db.")
|
||||
|
||||
@connector.close
|
||||
async def disconnect(connection):
|
||||
"""Disconnects from the league client"""
|
||||
print('Harvesting is over!')
|
||||
time.sleep(5)
|
||||
|
||||
# Begin
|
||||
|
||||
connector.start()
|
||||
|
||||
|
@ -1,44 +0,0 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
|
||||
|
||||
block_cipher = None
|
||||
|
||||
|
||||
a = Analysis(
|
||||
['dataScraper.py'],
|
||||
pathex=[],
|
||||
binaries=[],
|
||||
datas=[],
|
||||
hiddenimports=[],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
win_no_prefer_redirects=False,
|
||||
win_private_assemblies=False,
|
||||
cipher=block_cipher,
|
||||
noarchive=False,
|
||||
)
|
||||
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
[],
|
||||
name='dataScraper',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
console=True,
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
)
|
13
config.ini
@ -1,13 +0,0 @@
|
||||
[DEFAULT]
|
||||
; Your URL here (with no slash at the end)
|
||||
URL = https://ongod.fr
|
||||
|
||||
[DISCORD]
|
||||
; Your token here
|
||||
TOKEN = RANDOMAHHTOKEN39048209483294028
|
||||
; Developer settings in discord have to be set to on
|
||||
; Hover over server -> right click -> copy ID
|
||||
GUILD_ID = j4543koj534kl5h4j3kl5jh
|
||||
; Voice channel IDs for the channels you want your players to be split into
|
||||
TEAM_1 = 1054534012963659826
|
||||
TEAM_2 = 1054534093641089084
|
196
discord_bot.py
@ -1,196 +0,0 @@
|
||||
import asyncio
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
import random
|
||||
import requests
|
||||
import configparser
|
||||
|
||||
config = configparser.ConfigParser()
|
||||
config.read("config.ini")
|
||||
URL = config["DEFAULT"]["URL"]
|
||||
|
||||
|
||||
intents = discord.Intents.default()
|
||||
intents.message_content = True
|
||||
|
||||
bot = commands.Bot(intents=intents, command_prefix='!')
|
||||
|
||||
|
||||
@bot.event
|
||||
async def on_ready():
|
||||
print(f'We have logged in as {bot.user}')
|
||||
game = discord.Game("Customki - !randomize")
|
||||
await bot.change_presence(activity=game, status=discord.Status.dnd)
|
||||
|
||||
|
||||
@bot.command()
|
||||
async def randomize(ctx,
|
||||
team_1 = config['DISCORD']['TEAM_1'],
|
||||
team_2 = config['DISCORD']['TEAM_2']):
|
||||
"""Randomizes 10 people into 2 teams: !randomize"""
|
||||
|
||||
# Change these to designated team voice channels in config.ini
|
||||
team_1 = bot.get_channel(team_1)
|
||||
team_2 = bot.get_channel(team_2)
|
||||
trying_prompt = await ctx.send('Trying to randomize!')
|
||||
|
||||
# Fetching user channel
|
||||
try:
|
||||
channel = bot.get_channel(ctx.author.voice.channel.id)
|
||||
except AttributeError:
|
||||
await trying_prompt.delete()
|
||||
return await ctx.send("You think you're cool or something? Get in a channel first.")
|
||||
|
||||
players = channel.members
|
||||
if len(players) < 10:
|
||||
await trying_prompt.delete()
|
||||
await ctx.send(f"Get more people in ({len(players)}/10)")
|
||||
return
|
||||
random.shuffle(players)
|
||||
print(players)
|
||||
|
||||
# Embedding
|
||||
one_em = discord.Embed(title=f"Team 1", colour=discord.Colour(0x8c0303))
|
||||
|
||||
two_em = discord.Embed(title=f"Team 2", colour=discord.Colour(0x0B5394))
|
||||
# Splitting logic
|
||||
modulus = len(players) % 10
|
||||
if modulus != 0:
|
||||
# If total players not divisible by 10, can't split
|
||||
return await ctx.send("Can't split(remove bots and non-players)")
|
||||
|
||||
# Else, start at bottom and at top and move to middle
|
||||
for i in range(0, int(len(players)/2)):
|
||||
await players[i].move_to(team_1)
|
||||
one_em.add_field(name=players[i].name, value=players[i].id)
|
||||
await players[len(players) - (i + 1)].move_to(team_2)
|
||||
two_em.add_field(name=players[len(
|
||||
players) - (i + 1)].name, value=players[len(players) - (i + 1)].id)
|
||||
|
||||
await ctx.send(embed=one_em)
|
||||
await ctx.send(embed=two_em)
|
||||
await trying_prompt.delete()
|
||||
return
|
||||
|
||||
|
||||
@bot.command()
|
||||
async def begin_game(ctx,
|
||||
team_1 = config['DISCORD']['TEAM_1'],
|
||||
team_2 = config['DISCORD']['TEAM_2']):
|
||||
"""Tries to start a fair game: !begin_game"""
|
||||
# Change these to designated team voice channels
|
||||
team_1 = bot.get_channel(team_1)
|
||||
team_2 = bot.get_channel(team_2)
|
||||
|
||||
trying_prompt = await ctx.send('Trying to start fair game!')
|
||||
|
||||
# Fetching user channel
|
||||
try:
|
||||
channel = bot.get_channel(ctx.author.voice.channel.id)
|
||||
except AttributeError:
|
||||
await trying_prompt.delete()
|
||||
return await ctx.send("You think you're cool or something? Get in a channel first.")
|
||||
|
||||
players = channel.members
|
||||
if len(players) < 10:
|
||||
await trying_prompt.delete()
|
||||
await ctx.send(f"Get more people in ({len(players)}/10)")
|
||||
return
|
||||
|
||||
valid_players = []
|
||||
# This does not support multiple accounts, refer to issue #7
|
||||
# Initial check for existing players
|
||||
for i in players:
|
||||
print(i.name)
|
||||
player = requests.get(f"{URL}/players/?search={i.id}").json()
|
||||
print(player)
|
||||
if not player:
|
||||
await ctx.send(f"<@{i.id}> has no league account associated with their name. Please register in order to calculate MMR more accurately.")
|
||||
else:
|
||||
valid_players.append(player[0]["lol"])
|
||||
|
||||
# If not enough players, return
|
||||
if len(valid_players) < 10:
|
||||
return await ctx.send("Couldn't create fair game. Whoever isn't registered, please do.")
|
||||
|
||||
# Getting the players
|
||||
query_string = "&".join(["".format(player) for player in valid_players])
|
||||
|
||||
teams = requests.get(
|
||||
f"{URL}/game?{query_string}")
|
||||
|
||||
# Second check for existing players
|
||||
# Also, funny status code
|
||||
if teams.status_code == 451:
|
||||
for i in teams.content:
|
||||
await ctx.send(f"{i} has not been found by the API. Please register in order to calculate MMR more accurately.")
|
||||
return
|
||||
teams = teams.json()
|
||||
# Embedding
|
||||
one_em = discord.Embed(title=f"Team 1", colour=discord.Colour(0x8c0303))
|
||||
|
||||
two_em = discord.Embed(title=f"Team 2", colour=discord.Colour(0x0B5394))
|
||||
|
||||
# TODO: DEBUG, remove
|
||||
print(teams)
|
||||
|
||||
# Splitting logic
|
||||
for i in teams[0]:
|
||||
player = await bot.get_user(i["discord_id"])
|
||||
await player.move_to(team_1)
|
||||
one_em.add_field(name=player.name, value=i["lol"])
|
||||
for i in teams[1]:
|
||||
player = await bot.get_user(i["discord_id"])
|
||||
await player.move_to(team_2)
|
||||
two_em.add_field(name=player.name, value=i["lol"])
|
||||
|
||||
# Sending embeds and cleanup
|
||||
await ctx.send(embed=one_em)
|
||||
await ctx.send(embed=two_em)
|
||||
await trying_prompt.delete()
|
||||
return
|
||||
|
||||
|
||||
@bot.command()
|
||||
async def register(ctx, *args):
|
||||
"""Registers a user to the database: !register <league_name>"""
|
||||
name = " ".join(args)
|
||||
print(name)
|
||||
league_name = requests.get(f"{URL}/players/{name}").json()
|
||||
<<<<<<< HEAD
|
||||
print(league_name)
|
||||
=======
|
||||
|
||||
try:
|
||||
if not league_name["detail"] == "Not found.":
|
||||
return await ctx.send("Someone already claimed this account")
|
||||
except KeyError:
|
||||
if league_name["discord_id"]:
|
||||
return await ctx.send(f"{league_name['discord']} has claimed this account.")
|
||||
>>>>>>> 724a78ec70c1093baf80abc93712303e1291cc94
|
||||
|
||||
claim_account = requests.post(f"{URL}/players/", data={
|
||||
"discord": ctx.author.name,
|
||||
"discord_id": ctx.author.id,
|
||||
"lol": name
|
||||
})
|
||||
|
||||
if not claim_account.json().get('discord_id') and claim_account.json().get("lol"):
|
||||
|
||||
# In case that account doesn't exist at all
|
||||
claim_account = requests.put(f"{URL}/players/{name}/", data={
|
||||
"discord": ctx.author.name,
|
||||
"discord_id": ctx.author.id,
|
||||
"lol": name
|
||||
})
|
||||
print(claim_account.content)
|
||||
|
||||
# TODO: In case the account exists, but not yet claimed
|
||||
|
||||
print(claim_account.status_code)
|
||||
if claim_account.status_code == 201 or claim_account.status_code == 200:
|
||||
return await ctx.send("Success, now approve from client")
|
||||
return await ctx.send("Something went wrong...")
|
||||
|
||||
# Change to your bot token in config.ini
|
||||
bot.run(config['DISCORD']['TOKEN'])
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 21 KiB |
BIN
images/bob.png
Normal file
After Width: | Height: | Size: 52 KiB |
BIN
images/inverted_logo.png
Normal file
After Width: | Height: | Size: 26 KiB |
BIN
images/logo_wip.png
Normal file
After Width: | Height: | Size: 27 KiB |
@ -1,23 +0,0 @@
|
||||
# Generated by Django 4.0.3 on 2023-03-15 22:50
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Player',
|
||||
fields=[
|
||||
('username', models.CharField(max_length=60)),
|
||||
('ign', models.CharField(max_length=60)),
|
||||
('lol_id', models.CharField(max_length=60)),
|
||||
('discord_id', models.IntegerField(primary_key=True, serialize=False)),
|
||||
],
|
||||
),
|
||||
]
|
@ -1,23 +0,0 @@
|
||||
# Generated by Django 4.0.3 on 2023-03-15 22:52
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('server_client_api', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='player',
|
||||
old_name='ign',
|
||||
new_name='discord',
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='player',
|
||||
old_name='username',
|
||||
new_name='lol',
|
||||
),
|
||||
]
|
@ -1,19 +0,0 @@
|
||||
# Generated by Django 4.0.3 on 2023-03-15 23:02
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('server_client_api', '0002_rename_ign_player_discord_rename_username_player_lol'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='player',
|
||||
name='mmr',
|
||||
field=models.IntegerField(default=None),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
@ -1,28 +0,0 @@
|
||||
# Generated by Django 4.1.7 on 2023-03-16 00:32
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('server_client_api', '0003_player_mmr'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='player',
|
||||
name='lol',
|
||||
field=models.CharField(blank=True, max_length=60, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='player',
|
||||
name='lol_id',
|
||||
field=models.CharField(blank=True, max_length=60, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='player',
|
||||
name='mmr',
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
]
|
@ -1,18 +0,0 @@
|
||||
# Generated by Django 4.1.7 on 2023-03-16 00:35
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('server_client_api', '0004_alter_player_lol_alter_player_lol_id_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='player',
|
||||
name='mmr',
|
||||
field=models.IntegerField(default=0, editable=False),
|
||||
),
|
||||
]
|
@ -1,16 +0,0 @@
|
||||
from django.db import models
|
||||
|
||||
class Player(models.Model):
|
||||
discord = models.CharField(max_length=60, unique=True, null=True)
|
||||
lol = models.CharField(primary_key=True, max_length=60, unique=True )
|
||||
lol_id = models.CharField(max_length=60, null=True, blank=True , unique=True)
|
||||
discord_id = models.IntegerField(null=True, blank=True)
|
||||
mmr = models.IntegerField(default=600, editable=False)
|
||||
games_played = models.IntegerField(default=0)
|
||||
def __str__(self):
|
||||
return f"{self.discord}:{self.lol}"
|
||||
|
||||
class Game(models.Model):
|
||||
game_id = models.CharField(max_length=30, unique=True)
|
||||
|
||||
|
@ -1,25 +0,0 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from .models import Player, Game
|
||||
|
||||
|
||||
class PlayerSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
model = Player
|
||||
fields = ('discord', 'lol', 'lol_id', 'discord_id')
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
instance.discord_id = validated_data.get(
|
||||
'discord_id', instance.discord_id)
|
||||
instance.discord = validated_data.get('discord', instance.discord)
|
||||
instance.lol = validated_data.get('lol', instance.lol)
|
||||
instance.lol_id = validated_data.get('lol_id', instance.lol_id)
|
||||
instance.save()
|
||||
return instance
|
||||
|
||||
|
||||
class GameSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Game
|
||||
fields = ("game_id",)
|
39
src/client/README.md
Normal file
@ -0,0 +1,39 @@
|
||||
# Client
|
||||
|
||||
Heavily W.I.P. at the moment.
|
||||
|
||||
|
||||
## Config
|
||||
|
||||
Create a file called `config.py` and fill it with the following values(relevant to you):
|
||||
|
||||
```
|
||||
SITE_URL = "your-server.com"
|
||||
|
||||
```
|
||||
|
||||
## Releases
|
||||
Check out the releases for pre-compiled binaries/installers of the client. Remember to edit the config.ini file before running. That will be done in the setup, but still.
|
||||
|
||||
## Manual compilationn
|
||||
Compile the client like so.
|
||||
|
||||
Firstly, check if your Python version is => 3.11
|
||||
```
|
||||
$ python --version
|
||||
> Python 3.11.3
|
||||
```
|
||||
Then, clone the repository.
|
||||
`$ git clone https://github.com/confestim/custoMM`
|
||||
`$ cd custoMM/src/client`
|
||||
If you don't have pyinstaller installed, do:
|
||||
`$ pip install pyinstaller`
|
||||
Once that's ready, you're ready to compile!
|
||||
`$ python -m PyInstaller --onefile main.py --noconsole`
|
||||
|
||||
The exe file will reside in the newly created dist/ folder. Don't forget to copy the assets and the config to the same folder that your EXE resides(that means you will have to send them over to your friends along with the EXE file):
|
||||
`cp assets ../config.ini dist`
|
||||
|
||||
From here, do whatever with the dist folder, I would recommend zipping it.
|
||||
|
||||
|
BIN
src/client/assets/icon.png
Normal file
After Width: | Height: | Size: 14 KiB |
4
src/client/classes/Exceptions.py
Normal file
@ -0,0 +1,4 @@
|
||||
class RegistrationError(Exception):
|
||||
def __init__(self, message="Error in registration process. Edge case detected. Report as an issue or PR."):
|
||||
self.message = message
|
||||
super().__init__(self.message)
|
102
src/client/classes/Game.py
Normal file
@ -0,0 +1,102 @@
|
||||
from random import choice, randint
|
||||
from time import sleep
|
||||
from logging import info
|
||||
|
||||
class Game:
|
||||
def __init__(self, *, loop=None, connection,config):
|
||||
self.password = str(config["LEAGUE"]["LOBBY_PASS"])
|
||||
# Loop until we get connection
|
||||
self.connection = connection
|
||||
|
||||
|
||||
|
||||
|
||||
def list_all(self):
|
||||
# List all games
|
||||
sleep(1)
|
||||
self.connection.post("/lol-lobby/v1/custom-games/refresh", data={})
|
||||
return self.connection.get("/lol-lobby/v1/custom-games").json()
|
||||
|
||||
def search(self, query):
|
||||
# Searches for game by its name
|
||||
return [x["id"] for x in self.list_all() if x["lobbyName"] == query]
|
||||
|
||||
def join_by_id(self, id):
|
||||
# Joins a game, given an id
|
||||
return self.connection.post(f"/lol-lobby/v1/custom-games/{id}/join", data={"password":self.password})
|
||||
|
||||
def join_by_name(self,name):
|
||||
# Joins a game given its name
|
||||
try:
|
||||
return self.join_by_id(self.search(str(name))[0])
|
||||
except IndexError:
|
||||
info("Game not found, try again.")
|
||||
|
||||
|
||||
def join_random(self):
|
||||
# Joins a random public game
|
||||
# mainly debug reasons
|
||||
return self.join_by_id(choice([x["id"] for x in self.list_all() if not x["hasPassword"]]))
|
||||
|
||||
def in_game_with_name(self, lobby_name):
|
||||
try:
|
||||
name = self.connection.get("/lol-lobby/v2/lobby").json()["gameConfig"]["customLobbyName"]
|
||||
if lobby_name == name:
|
||||
return True
|
||||
except Exception as e:
|
||||
info(e)
|
||||
return False
|
||||
|
||||
def leave_with_creator(self, creator):
|
||||
members = [x["summonerName"] for x in self.connection.get("/lol-lobby/v2/lobby/").json()["members"]]
|
||||
if creator not in members:
|
||||
self.leave()
|
||||
|
||||
def wait_for_champ_select(self):
|
||||
select = self.connection.get("/lol-champ-select/v1/session").json()
|
||||
in_champ_select = False
|
||||
while not in_champ_select:
|
||||
try:
|
||||
select["isCustomGame"]
|
||||
except KeyError:
|
||||
select = self.connection.get("/lol-champ-select/v1/session").json()
|
||||
self.leave_with_creator()
|
||||
sleep(1)
|
||||
else:
|
||||
in_champ_select = True
|
||||
return select
|
||||
|
||||
|
||||
def create(self):
|
||||
# Creates game
|
||||
conn = self.connection
|
||||
name = "CustoMM " + str(randint(100000, 10000000))
|
||||
game = conn.post("/lol-lobby/v2/lobby/", data={
|
||||
"customGameLobby": {
|
||||
"configuration": {
|
||||
"gameMode": f"CLASSIC", "gameServerRegion": "", "mapId": 11, "mutators": {"id": 6}, "spectatorPolicy": "AllAllowed", "teamSize": 5
|
||||
},
|
||||
"lobbyName": name,
|
||||
"lobbyPassword": self.password
|
||||
},
|
||||
"isCustom": True
|
||||
})
|
||||
|
||||
return str(name)
|
||||
|
||||
def start(self):
|
||||
# Starts champ select
|
||||
return self.connection.post("/lol-lobby/v1/lobby/custom/start-champ-select", data={})
|
||||
|
||||
def move(self):
|
||||
return self.connection.post("/lol-lobby/v1/lobby/custom/switch-teams", data={})
|
||||
|
||||
def leave(self):
|
||||
return self.connection.delete("/lol-lobby/v2/lobby")
|
||||
|
||||
def get_teams(self):
|
||||
# Gets team
|
||||
cfg = self.connection.get("/lol-lobby/v2/lobby").json()["gameConfig"]
|
||||
return [[x["summonerInternalName"] for x in cfg["customTeam100"]],
|
||||
[x["summonerInternalName"] for x in cfg["customTeam200"]]]
|
||||
|
50
src/client/classes/Notify.py
Normal file
@ -0,0 +1,50 @@
|
||||
import pystray
|
||||
from PIL import Image
|
||||
import sys, os
|
||||
from time import sleep
|
||||
|
||||
class Notify():
|
||||
|
||||
def __init__(self, base_dir, exit_after=False):
|
||||
"""Standalone notification module
|
||||
|
||||
Args:
|
||||
base_dir (os.path): base dir of application
|
||||
exit_after (bool, optional): Specify if you want to exit after a notif. Defaults to False.
|
||||
"""
|
||||
self.exit_after = exit_after
|
||||
|
||||
image = Image.open(os.path.join(base_dir, os.path.join("assets", "icon.png")))
|
||||
|
||||
# Singular menu option
|
||||
self.menu = pystray.Menu(
|
||||
pystray.MenuItem(
|
||||
"Exit", self.quit
|
||||
)
|
||||
)
|
||||
|
||||
self.icon = pystray.Icon(
|
||||
"name", image, "CustoMM", self.menu)
|
||||
|
||||
self.icon.run_detached()
|
||||
|
||||
self.notified = False
|
||||
self.exit = False
|
||||
|
||||
def notification(self, message:str):
|
||||
"""Notification method
|
||||
|
||||
Args:
|
||||
message (str): Message you want to send
|
||||
"""
|
||||
sleep(2)
|
||||
# Not
|
||||
if not self.notified:
|
||||
self.icon.notify(message, title="custoMM")
|
||||
if self.exit_after:
|
||||
self.quit()
|
||||
|
||||
def quit(self):
|
||||
self.exit = True
|
||||
self.icon.stop()
|
||||
|
43
src/client/classes/PeriodicScraper.py
Normal file
@ -0,0 +1,43 @@
|
||||
from time import sleep
|
||||
from threading import Thread
|
||||
from .Scraper import Scraper
|
||||
from logging import info
|
||||
from sys import exit
|
||||
from configparser import ConfigParser
|
||||
|
||||
|
||||
class PeriodicScraper(Thread):
|
||||
"""Scraper that runs every `offset` seconds
|
||||
|
||||
Args:
|
||||
config (ConfigParser): Config instance
|
||||
base_dir (_type_): Base dir of program
|
||||
offset (int, optional): Seconds to sleep before repeat. Defaults to 5.
|
||||
"""
|
||||
|
||||
def __init__(self, config:ConfigParser, base_dir, offset:int=5):
|
||||
|
||||
|
||||
Thread.__init__(self)
|
||||
self.offset=offset
|
||||
self.daemon = True
|
||||
self.connector:Scraper = Scraper(config=config, base_dir=base_dir)
|
||||
self.closed = not self.connector.conn
|
||||
|
||||
def run(self):
|
||||
|
||||
while True:
|
||||
if self.closed:
|
||||
try:
|
||||
self.connector.connection.stop()
|
||||
except AttributeError:
|
||||
pass
|
||||
break
|
||||
|
||||
info("Checking for game...")
|
||||
game_state = self.connector.check_for_game()
|
||||
sleep(self.offset/2)
|
||||
info("Scraping...")
|
||||
self.connector.scrape()
|
||||
sleep(self.offset/22)
|
||||
return
|
373
src/client/classes/Scraper.py
Normal file
@ -0,0 +1,373 @@
|
||||
# League of Legends Client API Wrapper
|
||||
from lcu_connector import Connector
|
||||
from lcu_connector.exceptions import ClientProcessError, MissingLockfileError
|
||||
# TODO: Bug when trying to access previous lockfile
|
||||
|
||||
# Logging
|
||||
from logging import info, error
|
||||
# Requests
|
||||
from requests import get,put,post,delete
|
||||
from requests.exceptions import ConnectionError
|
||||
|
||||
# Custom imports
|
||||
from .Exceptions import RegistrationError
|
||||
from .Game import Game
|
||||
from .Notify import Notify
|
||||
from .UltimateBravery import UltimateBravery
|
||||
|
||||
# Config
|
||||
from configparser import ConfigParser
|
||||
|
||||
from time import sleep
|
||||
from json import dumps
|
||||
|
||||
TIME_DELAY = 20
|
||||
GAMES_TO_SCRAPE = 1000
|
||||
|
||||
JUNGLE = "JGL"
|
||||
TOP = "TOP"
|
||||
MID = "MID"
|
||||
ADC = "ADC"
|
||||
SUPPORT = "SUP"
|
||||
FILL = "FILL"
|
||||
|
||||
ROLE_CHOICES = [
|
||||
(JUNGLE, "Jungle"),
|
||||
(TOP, "Top"),
|
||||
(MID, "Middle"),
|
||||
(ADC, "Carry"),
|
||||
(SUPPORT, "Support"),
|
||||
(FILL, "Fill")
|
||||
]
|
||||
class Scraper:
|
||||
""" Scraper
|
||||
|
||||
Args:
|
||||
config (ConfigParser): Config instance
|
||||
base_dir (str): Base dir of program
|
||||
"""
|
||||
|
||||
def __init__(self, *, config:ConfigParser, base_dir:str):
|
||||
|
||||
# Config
|
||||
self.config = config
|
||||
self.URL = self.config["DEFAULT"]["URL"]
|
||||
self.connection = None
|
||||
self.base_dir = base_dir
|
||||
|
||||
# Loop until we get connection or user closes the polling program
|
||||
self.conn = self.get_connection()
|
||||
if not self.conn:
|
||||
return
|
||||
|
||||
# Get current summoner
|
||||
self.summoner = self.connection.get('/lol-summoner/v1/current-summoner').json()
|
||||
self.name = self.summoner["displayName"]
|
||||
|
||||
def get_connection(self):
|
||||
""" Tries to get connnection with local LCU.
|
||||
Returns:
|
||||
bool: Connection failed or established
|
||||
"""
|
||||
notification = Notify(base_dir=self.base_dir)
|
||||
while not self.connection:
|
||||
try:
|
||||
self.connection = Connector()
|
||||
self.connection.start()
|
||||
except ClientProcessError or MissingLockfileError:
|
||||
if notification.exit:
|
||||
return False
|
||||
info("Polling...")
|
||||
notification.notification("custoMM is waiting for LoL to start in the background.")
|
||||
notification.notified = True
|
||||
sleep(TIME_DELAY)
|
||||
|
||||
notification.quit()
|
||||
return True
|
||||
|
||||
def calculate_kda(self, kills:int, assists:int, deaths:int, decimals:int=3):
|
||||
""" Calculates KDA ratio
|
||||
|
||||
Args:
|
||||
kills (int): Kills
|
||||
assists (int): Assists
|
||||
deaths (int): Deaths
|
||||
decimals (int,optional): Decimal points to round to. Defaults to 3.
|
||||
|
||||
Returns:
|
||||
float: rounded
|
||||
"""
|
||||
if deaths == 0:
|
||||
deaths = 1
|
||||
return round((kills+assists)/deaths, decimals)
|
||||
|
||||
def parse_history(self, history:dict, old_ids:list) -> list:
|
||||
"""Parses current player's history
|
||||
|
||||
Args:
|
||||
history (dict): Current player's history
|
||||
old_ids (list): Cached games (on server)
|
||||
|
||||
Returns:
|
||||
list: Serialized games ready for the server
|
||||
"""
|
||||
|
||||
connection = self.connection
|
||||
|
||||
|
||||
parsed_matches = []
|
||||
new = 0
|
||||
for i in history["games"]["games"]:
|
||||
if i["gameType"] == "CUSTOM_GAME" and str(i["gameId"]) not in old_ids and not i["gameMode"] == "PRACTICETOOL":
|
||||
|
||||
new += 1
|
||||
match = connection.get(f'/lol-match-history/v1/games/{i["gameId"]}').json()
|
||||
parsed_match = {
|
||||
"game_id": match["gameId"],
|
||||
"participants": {
|
||||
"t1": {
|
||||
"won": True if match["teams"][0]["win"] == "Won" else False,
|
||||
"summoners": []
|
||||
},
|
||||
"t2": {
|
||||
"won": True if match["teams"][1]["win"] == "Won" else False,
|
||||
"summoners": []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Sloppy solution, find fix.
|
||||
for player in range(10):
|
||||
current_player = match["participants"][player]
|
||||
stats = current_player["stats"]
|
||||
lane = current_player["timeline"]["lane"]
|
||||
role = current_player["timeline"]["lane"]
|
||||
|
||||
kda = self.calculate_kda(stats["kills"],stats["assists"],stats["deaths"])
|
||||
current_player[""]
|
||||
|
||||
parsed_match["participants"]["t1" if player<=5 else "t2"]["summoners"].append({"name":match["participantIdentities"][player]["player"]["summonerName"],
|
||||
"kda": kda,
|
||||
"lane":lane,
|
||||
"role":role})
|
||||
parsed_matches.append(parsed_match)
|
||||
if not new:
|
||||
# Notify player that we're up to date
|
||||
sleep(3)
|
||||
|
||||
|
||||
return parsed_matches
|
||||
|
||||
def register_summoner(self, claim:bool, claimed):
|
||||
"""Attempts to register the current summoner
|
||||
|
||||
Args:
|
||||
claim (bool): Register or Deregister(Delete)
|
||||
claimed (dict): Claimed user data ["lol", "lol_id", "discord", "discord_id"]
|
||||
"""
|
||||
if claim:
|
||||
account = put(f"{self.URL}/players/{claimed['lol']}/", data={
|
||||
"lol": claimed["lol"],
|
||||
"lol_id": claimed["lol_id"],
|
||||
"discord_id":claimed["discord_id"],
|
||||
"discord":claimed["discord"]
|
||||
})
|
||||
if account.status_code == 200:
|
||||
Notify(base_dir=self.base_dir, exit_after=True).notification(f"Alright, the account is now yours, {claimed['discord']}.")
|
||||
else:
|
||||
Notify(base_dir=self.base_dir, exit_after=True).notification("Something went wrong when claiming your account...")
|
||||
else:
|
||||
delete(f"{self.URL}/players/{claimed['discord_id']}")
|
||||
|
||||
|
||||
def check_summoner(self):
|
||||
"""
|
||||
Checks if logged in summoner is registered
|
||||
"""
|
||||
|
||||
connection = self.connection
|
||||
# Summoner
|
||||
|
||||
|
||||
# Check if account is claimed
|
||||
try:
|
||||
claimed = get(f"{self.URL}/players/?search={self.summoner['displayName']}").json()[0]
|
||||
except Exception as e:
|
||||
error(e)
|
||||
return "USER_DOES_NOT_EXIST"
|
||||
|
||||
# Case 1: It belongs to nobody and has yet to be claimed.
|
||||
if not claimed["discord"]:
|
||||
return "UNCLAIMED"
|
||||
|
||||
if claimed:
|
||||
# Case 2: It belongs to somebody
|
||||
if claimed['lol_id'] and claimed['lol']:
|
||||
|
||||
# Change name in db if different in-game
|
||||
if claimed['lol'] != self.summoner['displayName']:
|
||||
self.register_summoner(True, claimed)
|
||||
|
||||
return (claimed['discord'], claimed['lol'])
|
||||
|
||||
# Case 3: Registration has begun, but hasn't finished
|
||||
elif claimed['lol'] and not claimed['lol_id']:
|
||||
claimed["lol_id"]=self.summoner["puuid"]
|
||||
return ["REGISTRATION_IN_PROGRESS", claimed]
|
||||
|
||||
# All cases are covered, everything else will be considered a bug.
|
||||
else:
|
||||
raise RegistrationError()
|
||||
|
||||
def move_needed(self, checker, game:Game, name:str):
|
||||
""" Moves player into other team if needed
|
||||
|
||||
Args:
|
||||
checker (dict): Information about the remote current game, we need the teams.
|
||||
game (Game): Local game instance
|
||||
name (str): Username we're looking for
|
||||
"""
|
||||
|
||||
# This is buggy, try to find a better way to do this.
|
||||
# Like for example, letting team 1 pass first, and then team 2.
|
||||
local_teams = game.get_teams()
|
||||
checker = checker["teams"]
|
||||
if name in local_teams[0] and not name in checker[0]:
|
||||
game.move()
|
||||
info("Moving to Team 2")
|
||||
elif name in local_teams[1] and not name in checker[1]:
|
||||
game.move()
|
||||
info("Moving to Team 1")
|
||||
|
||||
def start(self, checker, game:Game, timeout:int=120):
|
||||
""" Waits for 10 players and starts game
|
||||
|
||||
Args:
|
||||
checker (dict): Information about current game (http://yourser.ver/current/)
|
||||
game (Game): Local game instance
|
||||
timeout (int, optional): Timeout for lobby in seconds
|
||||
"""
|
||||
# Move if needed
|
||||
self.move_needed(checker, game, self.name)
|
||||
timeout_counter = 0
|
||||
|
||||
# Wait until 10 players
|
||||
while response := get(f"{self.URL}/current/{self.name}").json()["players"] != 10:
|
||||
info("Waiting for players...")
|
||||
timeout_counter += 5
|
||||
if timeout_counter == timeout:
|
||||
Notify(base_dir=self.base_dir, exit_after=True).notification(message="Timed out, not enough players joined, leaving.")
|
||||
break
|
||||
sleep(5)
|
||||
|
||||
# Start or leave
|
||||
if response == 10:
|
||||
Notify(base_dir=self.base_dir, exit_after=True).notification("Starting game...")
|
||||
game.start()
|
||||
else:
|
||||
game.leave()
|
||||
|
||||
# Current game gets deleted either way
|
||||
sleep(30)
|
||||
delete(f"{self.URL}/current/{self.name}")
|
||||
|
||||
def check_for_game(self):
|
||||
"""
|
||||
Checks if a game is going on right now.
|
||||
"""
|
||||
# Initial check
|
||||
try:
|
||||
checker = get(f"{self.URL}/current").json()[0]
|
||||
except Exception:
|
||||
return "NO_GAME"
|
||||
if checker["lobby_name"] == "null":
|
||||
return
|
||||
# Local game instance
|
||||
game = Game(connection=self.connection, config=self.config)
|
||||
|
||||
# If you are the creator, create the game and disclose its name to the server
|
||||
if checker["creator"] == self.name:
|
||||
Notify(base_dir=self.base_dir, exit_after=True).notification("You are the creator! Creating lobby...")
|
||||
created = game.create()
|
||||
|
||||
r = put(f"{self.URL}/current/{self.name}/", data={
|
||||
"lobby_name": created,
|
||||
"creator": self.name,
|
||||
"players": 1,
|
||||
"teams": dumps(checker["teams"], indent=4),
|
||||
"bravery": checker["bravery"]
|
||||
})
|
||||
|
||||
|
||||
# Start the game
|
||||
self.start(checker, game)
|
||||
return "CREATED"
|
||||
|
||||
else:
|
||||
# If you have to join
|
||||
try:
|
||||
name = checker["lobby_name"]
|
||||
except KeyError:
|
||||
# Wait until lobby name becomes available
|
||||
while not get(f"{self.URL}/current").json()[0].get("lobby_name"):
|
||||
sleep(10)
|
||||
checker = get(f"{self.URL}/current").json()
|
||||
name = checker["lobby_name"]
|
||||
Notify(base_dir=self.base_dir, exit_after=True).notification(f"Joining {name}...")
|
||||
|
||||
# Join the lobby and move if needed
|
||||
game.join_by_name(name)
|
||||
self.move_needed(checker, game, self.name)
|
||||
|
||||
# Update count of players
|
||||
put(f"{self.URL}/current/{checker['creator']}/", data={
|
||||
"lobby_name": checker["lobby_name"],
|
||||
"creator": name,
|
||||
"players": int(checker["players"])+1,
|
||||
"teams": dumps(checker["teams"], indent=4),
|
||||
"bravery": checker["bravery"]
|
||||
|
||||
})
|
||||
game.wait_for_champ_select()
|
||||
if checker["bravery"]:
|
||||
self.__pick_bravely(checker["bravery"][self.name])
|
||||
|
||||
|
||||
def __pick_bravely(self, data):
|
||||
role = get(f"{self.URL}/players/?search={self.summoner['displayName']}").json()[0]["usual_role"]
|
||||
brave = UltimateBravery()
|
||||
# TODO:
|
||||
# * Automatic picking of champ, runes and spells
|
||||
|
||||
|
||||
|
||||
def scrape(self):
|
||||
"""Scrapes current account and sends it to server"""
|
||||
|
||||
try:
|
||||
connection = self.connection
|
||||
self.check_summoner()
|
||||
# Match History
|
||||
match_history = connection.get(f'/lol-match-history/v1/products/lol/current-summoner/matches?endIndex={GAMES_TO_SCRAPE}')
|
||||
match_history = match_history.json()
|
||||
|
||||
# Stage old ids in order for them to be parsed
|
||||
old_ids = get(f"{self.URL}/games/").json()
|
||||
old_ids = [x["game_id"] for x in old_ids]
|
||||
|
||||
|
||||
# TODO: Optimize the process of acquisition of new matches
|
||||
games = self.parse_history(match_history, old_ids)
|
||||
|
||||
# Post the new games to your server(change in config.json)
|
||||
for i in games:
|
||||
req = post(f"{self.URL}/games/", json=i)
|
||||
if req.status_code == 500:
|
||||
Notify(base_dir=self.base_dir, exit_after=True).notification("Serverside error! Contact maintainer!")
|
||||
|
||||
return len(games)
|
||||
|
||||
# If client is not opened, destroy past connection and try to get new one
|
||||
except ConnectionError:
|
||||
self.connection = None
|
||||
self.get_connection()
|
28
src/client/classes/SelfUpdate.py
Normal file
@ -0,0 +1,28 @@
|
||||
from requests import get, put, post, delete
|
||||
from wget import download
|
||||
from os import system, path
|
||||
from logging import info
|
||||
|
||||
class SelfUpdate():
|
||||
"""Checks for new updates and prompts user to install
|
||||
|
||||
Args:
|
||||
version (str): Version number
|
||||
base_dir(str): Base dir of program
|
||||
"""
|
||||
def __init__(self, version:str, base_dir:str):
|
||||
|
||||
self.version = version
|
||||
self.newest = get("https://api.github.com/repos/confestim/custoMM/releases/latest").json()["tag_name"]
|
||||
info(self.newest, self.version)
|
||||
self.new_version = False
|
||||
if self.version != self.newest:
|
||||
self.new_version = True
|
||||
return
|
||||
|
||||
def update(self):
|
||||
""" Updater
|
||||
"""
|
||||
# TODO: Implement possible asset download (if needed)
|
||||
download(f"https://github.com/confestim/custoMM/releases/download/{self.newest}/custoMM_installer.exe")
|
||||
system("custoMM_installer.exe")
|
113
src/client/classes/UI.py
Normal file
@ -0,0 +1,113 @@
|
||||
import pystray
|
||||
from PIL import Image
|
||||
import pyautogui
|
||||
from time import sleep
|
||||
import os
|
||||
from logging import info
|
||||
from .Scraper import Scraper
|
||||
from .PeriodicScraper import PeriodicScraper
|
||||
from .SelfUpdate import SelfUpdate
|
||||
|
||||
VERSION = "1.1.1"
|
||||
|
||||
class UI():
|
||||
"""Tray icon UI module
|
||||
|
||||
Args:
|
||||
scraper (Scraper): Scraper instance
|
||||
periodic (PeriodicScraper): PeriodicScraper instance
|
||||
base_dir (_type_): Base dir of program
|
||||
"""
|
||||
def __init__(self,scraper:Scraper, periodic:PeriodicScraper, base_dir):
|
||||
|
||||
self.version = VERSION
|
||||
image = Image.open(os.path.join(base_dir, os.path.join("assets", "icon.png")))
|
||||
self.base_dir = base_dir
|
||||
# Check if user has exited before making a connection with the LoL client
|
||||
if periodic.closed:
|
||||
return
|
||||
self.periodic = periodic
|
||||
self.scraper = scraper
|
||||
|
||||
# Menu items
|
||||
self.menu = pystray.Menu(
|
||||
pystray.MenuItem(
|
||||
"Check registration", self.check_registration, default=True
|
||||
),
|
||||
pystray.MenuItem(
|
||||
f"Manual game report", self.report
|
||||
),
|
||||
pystray.MenuItem(
|
||||
f"Check for game", self.check
|
||||
),
|
||||
pystray.MenuItem(
|
||||
"Check for updates", self.update_check
|
||||
),
|
||||
pystray.MenuItem(
|
||||
"Exit", self.quit
|
||||
)
|
||||
)
|
||||
|
||||
self.icon = pystray.Icon(
|
||||
"name", image, "custoMM", self.menu)
|
||||
self.icon.run_detached()
|
||||
|
||||
# After ui is running, check user registration
|
||||
self.check_registration()
|
||||
self.update_check()
|
||||
|
||||
def update_check(self):
|
||||
# TODO: Test this
|
||||
update = SelfUpdate(base_dir=self.base_dir, version=self.version)
|
||||
if update:
|
||||
prompt = pyautogui.confirm(f"New version available, do you want to update?", buttons=['OK', 'No'])
|
||||
if prompt == "OK":
|
||||
update.update()
|
||||
else:
|
||||
self.icon.notify("Please update as soon as possible.", "New version available.")
|
||||
|
||||
def check(self):
|
||||
""" Checks for ongoing game and notifies user
|
||||
"""
|
||||
|
||||
self.icon.notify("This is discouraged, as it is done automatically.", "Checking for game...")
|
||||
game = self.scraper.check_for_game()
|
||||
if game == "NO_GAME":
|
||||
self.icon.notify("Please create a game on discord.", "No game found.")
|
||||
elif game == "CREATED":
|
||||
self.icon.notify("GLHF!", "You are the host of a new game!")
|
||||
elif game == "JOINED":
|
||||
self.icon.notify("Waiting for players...", "Game joined!")
|
||||
|
||||
def report(self):
|
||||
""" Manual game reporting
|
||||
"""
|
||||
self.icon.notify("Game report initiated.")
|
||||
self.scraper.scrape()
|
||||
|
||||
|
||||
def check_registration(self):
|
||||
""" Checks the current logged in user's registration
|
||||
"""
|
||||
self.icon.notify("Checking summoner...", title="custoMM")
|
||||
|
||||
check = self.scraper.check_summoner()
|
||||
if check == "UNCLAIMED" or check == "USER_DOES_NOT_EXIST":
|
||||
self.icon.notify("You have not claimed your account yet, please claim it on discord -> !register <ACCOUNT_NAME>.")
|
||||
|
||||
elif check[0] == "REGISTRATION_IN_PROGRESS":
|
||||
prompt = pyautogui.confirm(f"Your account({check[1]['lol']}) is currently being registered by {check[1]['discord']} on Discord. Is this you?")
|
||||
if prompt == "OK":
|
||||
self.scraper.register_summoner(True, check[1])
|
||||
else:
|
||||
self.scraper.register_summoner(False, check[1])
|
||||
else:
|
||||
self.icon.notify(f"Your account is registered to {check[0]} and your account name is {check[1]}.")
|
||||
|
||||
|
||||
def quit(self, icon):
|
||||
""" Quit
|
||||
"""
|
||||
icon.stop()
|
||||
self.periodic.closed = True
|
||||
|
65
src/client/classes/UltimateBravery.py
Normal file
@ -0,0 +1,65 @@
|
||||
from requests import get
|
||||
from random import randint
|
||||
from collections import Counter
|
||||
|
||||
# TODO: Implement version check, which increases BOTTOM_END and TOP_END if the version is not the same as the current one
|
||||
BOTTOM_END = 45000000
|
||||
TOP_END = 45999999
|
||||
VERSION = [13,13,1]
|
||||
class UltimateBravery:
|
||||
|
||||
|
||||
def __init__(self, seed=randint(BOTTOM_END, TOP_END), role=None):
|
||||
|
||||
self.seed = seed
|
||||
self.data = self.__getRandom()
|
||||
self.version = self.data.get("version").split(".")
|
||||
self.role = role
|
||||
if Counter(VERSION) != Counter([int(x) for x in self.version]):
|
||||
raise NameError("Version mismatch! Program needs to be updated.")
|
||||
|
||||
|
||||
|
||||
def __getRandom(self):
|
||||
data = get(f"https://api2.ultimate-bravery.net/bo/api/ultimate-bravery/v1/classic/dataset/seed/{self.seed}?language=en").json()
|
||||
|
||||
while data.get("status") != "success" and (data.get("data").get("role") != self.role) if self.role else None:
|
||||
print(str(self.seed) + " doesn't work")
|
||||
self.seed = randint(BOTTOM_END, TOP_END)
|
||||
data = get(f"https://api2.ultimate-bravery.net/bo/api/ultimate-bravery/v1/classic/dataset/seed/{self.seed}?language=en").json()
|
||||
|
||||
return data.get("data")
|
||||
|
||||
|
||||
@property
|
||||
def raw_data(self):
|
||||
return self.data
|
||||
|
||||
@property
|
||||
def champion(self):
|
||||
return self.data.get("champion").get("name")
|
||||
|
||||
@property
|
||||
def spell(self):
|
||||
return self.data.get("champion").get("spell").get("key")
|
||||
|
||||
@property
|
||||
def items(self):
|
||||
return self.data.get("items")
|
||||
|
||||
@property
|
||||
def summonerSpells(self):
|
||||
return self.data.get("summonerSpells")
|
||||
|
||||
@property
|
||||
def runes(self):
|
||||
return self.data.get("runes")
|
||||
|
||||
@property
|
||||
def role(self):
|
||||
return self.data.get("role")
|
||||
|
||||
@property
|
||||
def itemSet(self):
|
||||
return self.data.get("itemSet")
|
||||
|
7
src/client/config.ini
Normal file
@ -0,0 +1,7 @@
|
||||
[DEFAULT]
|
||||
; Your URL here (with no slash at the end)
|
||||
URL = http://23.88.44.133:8000/api
|
||||
|
||||
[LEAGUE]
|
||||
; Password for the generated league lobbies
|
||||
LOBBY_PASS = password123
|
61
src/client/main.py
Normal file
@ -0,0 +1,61 @@
|
||||
import requests
|
||||
|
||||
# Edit config.ini when running for the first time
|
||||
import sys, os
|
||||
import configparser
|
||||
from time import sleep
|
||||
from logging import info, basicConfig, INFO
|
||||
|
||||
# Custom imports
|
||||
from classes.UI import UI
|
||||
from classes.PeriodicScraper import PeriodicScraper
|
||||
from classes.Scraper import Scraper
|
||||
from classes.SelfUpdate import SelfUpdate
|
||||
from classes.Notify import Notify
|
||||
|
||||
# Config section
|
||||
basicConfig(format='%(asctime)s - %(message)s', level=INFO)
|
||||
|
||||
# Check if bundled
|
||||
if getattr(sys, 'frozen', False):
|
||||
base_dir = os.path.dirname(sys.executable)
|
||||
elif __file__:
|
||||
base_dir = os.path.dirname(__file__)
|
||||
|
||||
info("Base directory - " + base_dir)
|
||||
|
||||
# Parse config
|
||||
config = configparser.ConfigParser()
|
||||
conf_path = os.path.join(base_dir, "config.ini")
|
||||
config.read(conf_path)
|
||||
URL = config["DEFAULT"]["URL"]
|
||||
|
||||
Notify(base_dir=base_dir, exit_after=True).notification("Starting custoMM...")
|
||||
|
||||
# Test connection to server
|
||||
try:
|
||||
test = requests.get(URL).raise_for_status()
|
||||
|
||||
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout):
|
||||
# NEVER DO THIS
|
||||
# although, what could go wrong...
|
||||
Notify(base_dir=base_dir, exit_after=True).notification("custoMM unavailable", "Try restarting and contact admin if it keeps doing this.")
|
||||
sys.exit()
|
||||
except requests.exceptions.HTTPError as error:
|
||||
Notify(base_dir=base_dir, exit_after=True).notification(f"Server issue", "Contact admin immediately. Error code: {error.response.status_code}")
|
||||
sys.exit()
|
||||
|
||||
# Get current summoner
|
||||
def main():
|
||||
# Match scraping
|
||||
# Periodic scraper
|
||||
periodic = PeriodicScraper(config=config, base_dir=base_dir, offset=30)
|
||||
|
||||
ui = UI(scraper=periodic.connector, periodic=periodic, base_dir=base_dir)
|
||||
# Self update only needs to run once, on start of program
|
||||
periodic.start()
|
||||
periodic.join()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
47
src/discord/bot.py
Normal file
@ -0,0 +1,47 @@
|
||||
import asyncio
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
from classes.Splitter import Splitter
|
||||
from classes.Config import Config
|
||||
from os import listdir
|
||||
intents = discord.Intents.all()
|
||||
|
||||
|
||||
config = Config()
|
||||
|
||||
class custoMM(commands.Bot):
|
||||
def __init__(self):
|
||||
intents = discord.Intents.all()
|
||||
super().__init__(
|
||||
command_prefix="!",
|
||||
intents=intents,
|
||||
)
|
||||
|
||||
async def load_extensions(self):
|
||||
for filename in listdir("./cogs"):
|
||||
if filename.endswith(".py"):
|
||||
# cut off the .py from the file name
|
||||
await bot.load_extension(f"cogs.{filename[:-3]}")
|
||||
|
||||
async def setup_hook(self):
|
||||
await self.load_extensions()
|
||||
bot.tree.clear_commands(guild=discord.Object(id=config.guild))
|
||||
await self.tree.sync(guild=discord.Object(id=config.guild))
|
||||
|
||||
async def on_ready(self):
|
||||
synced = await self.tree.sync()
|
||||
print("-------------------")
|
||||
print("Synced " + str(len(synced)) + " commands")
|
||||
[print(f"Synced {x.name}") for x in synced]
|
||||
print("-------------------")
|
||||
print(f'Logged in as {bot.user}')
|
||||
game = discord.Game("Custom Matchmaking - !help")
|
||||
await bot.change_presence(activity=game, status=discord.Status.dnd)
|
||||
print("Displaying status as " + str(game))
|
||||
print("-------------------")
|
||||
# Load the cogs
|
||||
|
||||
bot = custoMM()
|
||||
|
||||
# Change to your bot token in config.ini
|
||||
bot.run(config.token,reconnect=True)
|
11
src/discord/classes/Config.py
Normal file
@ -0,0 +1,11 @@
|
||||
from configparser import ConfigParser
|
||||
|
||||
class Config:
|
||||
def __init__(self) -> None:
|
||||
config = ConfigParser()
|
||||
config.read("config.ini")
|
||||
self.URL = config["DEFAULT"]["URL"]
|
||||
self.team_1 = int(config['DISCORD']['TEAM_1'])
|
||||
self.team_2 = int(config['DISCORD']['TEAM_2'])
|
||||
self.token = config['DISCORD']['TOKEN']
|
||||
self.guild = int(config['DISCORD']['GUILD_ID'])
|
83
src/discord/classes/Fair.py
Normal file
@ -0,0 +1,83 @@
|
||||
import requests
|
||||
from .Splitter import Splitter
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
import asyncio
|
||||
from urllib.parse import quote
|
||||
from random import choice
|
||||
from json import dumps
|
||||
|
||||
class Fair(Splitter):
|
||||
def __init__(self,
|
||||
bot,
|
||||
author:discord.Member,
|
||||
ctx:commands.Context=None,
|
||||
interaction:discord.Interaction=None,
|
||||
slash=False,
|
||||
bravery=False):
|
||||
|
||||
|
||||
super().__init__(bot, author, ctx, interaction, slash)
|
||||
|
||||
self.bravery = bravery
|
||||
|
||||
if slash:
|
||||
splitter = Splitter(bot=bot, author =author, interaction=interaction, slash=True)
|
||||
else:
|
||||
splitter = Splitter(bot=bot, author =author, ctx=ctx)
|
||||
|
||||
|
||||
async def divide(self):
|
||||
players = await self.ready()
|
||||
if not players:
|
||||
return
|
||||
|
||||
valid_players = []
|
||||
|
||||
# This does not support multiple accounts, refer to issue #7
|
||||
# Initial check for existing players
|
||||
for i in players:
|
||||
|
||||
player = requests.get(f"{self.config.URL}/players/?search={i.id}").json()
|
||||
if not player:
|
||||
await self.send(f"<@{i.id}> has no league account associated with their name. Please register in order to calculate MMR more accurately.")
|
||||
else:
|
||||
valid_players.append(player[0]["lol"])
|
||||
|
||||
# If not enough players, return
|
||||
if len(valid_players) < 10:
|
||||
return await self.send("Couldn't create fair game. Whoever isn't registered, please do.")
|
||||
|
||||
# Getting the players
|
||||
query_string = "?"+"&".join(["players={}".format(quote(player)) for player in valid_players])
|
||||
print(query_string)
|
||||
teams = requests.get(
|
||||
f"{self.config.URL}/game/?{query_string}")
|
||||
|
||||
# Second check for existing players
|
||||
# Also, funny status code
|
||||
if teams.status_code == 451:
|
||||
for i in teams.content:
|
||||
await self.send(f"{i} has not been found by the API. Please register in order to calculate MMR more accurately.")
|
||||
return
|
||||
|
||||
teams = teams.json()
|
||||
# TODO: Debug, remove
|
||||
print(teams)
|
||||
|
||||
players_1 = [await self.bot.fetch_user(x["discord_id"]) for x in teams[0]]
|
||||
players_2 = [await self.bot.fetch_user(x["discord_id"]) for x in teams[1]]
|
||||
|
||||
await self.split(players_1, players_2)
|
||||
|
||||
# Create current game
|
||||
requests.post(f"{self.config.URL}/current/", data={
|
||||
"lobby_name": None,
|
||||
"players": 0,
|
||||
"creator": choice(valid_players),
|
||||
"teams": dumps(teams),
|
||||
"bravery": True if self.bravery else False
|
||||
})
|
||||
|
||||
|
||||
return
|
28
src/discord/classes/Leaderboard.py
Normal file
@ -0,0 +1,28 @@
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
from .Splitter import Splitter
|
||||
from .Config import Config
|
||||
import requests
|
||||
|
||||
class Leaderboard(Splitter):
|
||||
|
||||
def __init__(self, bot, players:int, author=None, ctx=None, interaction=None, slash=False):
|
||||
# Inherits send function from Splitter
|
||||
super().__init__(bot, author, ctx, interaction, slash)
|
||||
self.players = players
|
||||
|
||||
async def leaderboard(self):
|
||||
"""Shows the Top <players> leaderboard: !leaderboard <number_of_players> <max>"""
|
||||
config = Config()
|
||||
leaderboard = requests.get(f"{config.URL}/players").json()
|
||||
|
||||
if len(leaderboard) < self.players:
|
||||
return await self.send(f"We don't have that many self.players in the database. We have {len(leaderboard)}.")
|
||||
|
||||
leaderboard = leaderboard[:self.players]
|
||||
embed = discord.Embed(title=f"Top {self.players} players", description="Ordered by mmr", color=0xFF5733)
|
||||
embed.set_author(name="custoMM", icon_url="https://git.confest.im/boyan_k/custoMM/raw/branch/main/images/smol_logo.png")
|
||||
fields = [embed.add_field(name=x['lol'], value=x['mmr']) for x in leaderboard]
|
||||
return await self.send(message=None, embed=embed)
|
||||
|
||||
|
23
src/discord/classes/Randomizer.py
Normal file
@ -0,0 +1,23 @@
|
||||
from random import shuffle
|
||||
from .Splitter import Splitter
|
||||
|
||||
class Randomizer(Splitter):
|
||||
def __init__(self, bot, author, ctx=None, interaction=None, slash=False):
|
||||
# Inherits split function from Splitter
|
||||
super().__init__(bot, author, ctx, interaction, slash)
|
||||
|
||||
|
||||
async def randomize(self):
|
||||
"""
|
||||
Randomizes players into 2 teams
|
||||
"""
|
||||
players = await self.ready()
|
||||
print(players)
|
||||
if not players:
|
||||
return False
|
||||
|
||||
|
||||
shuffle(players)
|
||||
print([x.name for x in players[:int(len(players)/2)]], [x.name for x in players[int(len(players)/2):]])
|
||||
await self.split(players[:int(len(players)/2)], players[int(len(players)/2):])
|
||||
return True
|
50
src/discord/classes/Register.py
Normal file
@ -0,0 +1,50 @@
|
||||
from classes.Config import Config
|
||||
import requests
|
||||
from classes.Splitter import Splitter
|
||||
|
||||
class Register(Splitter):
|
||||
def __init__(self, bot, author, ctx, interaction = None, slash=False, player=None):
|
||||
super().__init__(bot, author, ctx, interaction, slash)
|
||||
self.config = Config()
|
||||
self.player = player
|
||||
|
||||
async def register(self):
|
||||
"""Registers a user to the database: !register <league_name>"""
|
||||
config = Config()
|
||||
if not self.slash:
|
||||
name = " ".join(self.player)
|
||||
else:
|
||||
name = self.player
|
||||
if (len(name) < 4) or (requests.get(f"https://lolprofile.net/summoner/eune/{name}").status_code != 200):
|
||||
return await self.send("Provide a normal username (cAsE sEnSiTiVe)")
|
||||
league_name = requests.get(f"{config.URL}/players/{name}").json()
|
||||
|
||||
try:
|
||||
if not league_name["detail"] == "Not found.":
|
||||
return await self.send("Someone already claimed this account")
|
||||
except KeyError:
|
||||
if league_name["discord_id"]:
|
||||
return await self.send(f"{league_name['discord']} has claimed this account.")
|
||||
|
||||
claim_account = requests.post(f"{config.URL}/players/", data={
|
||||
"discord": self.author,
|
||||
"discord_id": self.author.id,
|
||||
"lol": name
|
||||
})
|
||||
|
||||
if not claim_account.json().get('discord_id') and claim_account.json().get("lol"):
|
||||
|
||||
# In case that account doesn't exist at all
|
||||
claim_account = requests.put(f"{config.URL}/players/{name}/", data={
|
||||
"discord": self.author.name,
|
||||
"discord_id": self.author.id,
|
||||
"lol": name
|
||||
})
|
||||
print(claim_account.content)
|
||||
|
||||
# TODO: In case the account exists, but not yet claimed
|
||||
|
||||
print(claim_account.status_code)
|
||||
if claim_account.status_code == 201 or claim_account.status_code == 200:
|
||||
return await self.send("Success, now approve from client")
|
||||
return await self.send("Something went wrong...")
|
91
src/discord/classes/Splitter.py
Normal file
@ -0,0 +1,91 @@
|
||||
from .Config import Config
|
||||
from discord import Member, Interaction, Embed, Colour
|
||||
from discord.ext.commands import Bot, Context
|
||||
from typing import List
|
||||
import random
|
||||
|
||||
|
||||
class Splitter:
|
||||
def __init__(self,
|
||||
bot:Bot,
|
||||
author:Member,
|
||||
ctx:Context=None,
|
||||
interaction:Interaction=None,
|
||||
slash=False):
|
||||
# Config
|
||||
self.ctx = ctx
|
||||
self.interaction = interaction
|
||||
self.config = Config()
|
||||
self.bot = bot
|
||||
self.author = author
|
||||
self.slash = slash
|
||||
self.responses = 0
|
||||
team_1 = self.bot.get_channel(self.config.team_1)
|
||||
team_2 = self.bot.get_channel(self.config.team_2)
|
||||
|
||||
async def send(self, message, embed=None):
|
||||
"""
|
||||
Sends a message. Method depends on if the slash command or the prefix command was used.
|
||||
"""
|
||||
if not self.slash and self.ctx is not None:
|
||||
return await self.ctx.send(message, embed=embed)
|
||||
self.responses += 1
|
||||
if self.responses > 1:
|
||||
return await self.interaction.followup.send(message, embed=embed)
|
||||
return await self.interaction.response.send_message(message, embed=embed)
|
||||
|
||||
async def ready(self):
|
||||
|
||||
"""
|
||||
Checks if players are ready to be split
|
||||
"""
|
||||
|
||||
# Fetching user channel
|
||||
try:
|
||||
channel = self.bot.get_channel(self.author.voice.channel.id)
|
||||
except AttributeError:
|
||||
await self.send("You think you're cool or something? Get in a channel first.")
|
||||
return False
|
||||
|
||||
players = channel.members
|
||||
if len(players) < 10:
|
||||
await self.send(f"Get more people in ({len(players)}/10)")
|
||||
return False
|
||||
|
||||
if len(players) % 10 != 0:
|
||||
# If total players not divisible by 10, can't split
|
||||
await self.send("Can't split(remove bots and non-players)")
|
||||
return False
|
||||
|
||||
return players
|
||||
|
||||
async def split(self, players_1, players_2):
|
||||
"""
|
||||
Splits players into 2 teams
|
||||
"""
|
||||
|
||||
# Declaring channels
|
||||
team_1 = self.bot.get_channel(self.config.team_1)
|
||||
team_2 = self.bot.get_channel(self.config.team_2)
|
||||
# Embedding
|
||||
one_em = Embed(title=f"Team 1", colour=Colour(0x8c0303))
|
||||
two_em = Embed(title=f"Team 2", colour=Colour(0x0B5394))
|
||||
|
||||
|
||||
# Splitting logic
|
||||
for i in range(5):
|
||||
await players_1[i].move_to(team_1)
|
||||
one_em.add_field(name=players_1[i].name, value=f"<@{players_1[i].id}>")
|
||||
|
||||
await players_2[i].move_to(team_2)
|
||||
two_em.add_field(name=players_2[i].name, value=f"<@{players_2[i].id}>")
|
||||
|
||||
|
||||
# Sending embeds and cleanup
|
||||
await self.send(embed=one_em, message=None)
|
||||
await self.send(embed=two_em, message=None)
|
||||
return
|
||||
|
||||
|
||||
|
||||
|
24
src/discord/cogs/bravery.py
Normal file
@ -0,0 +1,24 @@
|
||||
from discord.ext import commands
|
||||
from discord import app_commands, Interaction
|
||||
from classes.Fair import Fair
|
||||
|
||||
|
||||
class BraveryCog(commands.Cog):
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
|
||||
# !Command
|
||||
@commands.command(aliases=['ultimate_bravery', 'ultimate'])
|
||||
async def bravery(self, ctx):
|
||||
fair = Fair(bot=self.bot, author=ctx.author, ctx=ctx,bravery=True)
|
||||
await fair.divide()
|
||||
|
||||
# /Command
|
||||
@app_commands.command(name="Ultimate Bravery", description="Splits the teams fairly, but everyone is forced to play ultimate bravery.")
|
||||
async def bravery(self, interaction: Interaction):
|
||||
fair = Fair(bot=self.bot, author=interaction.user, interaction=interaction, slash=True,bravery=True)
|
||||
await fair.divide()
|
||||
|
||||
|
||||
async def setup(bot):
|
||||
await bot.add_cog(BraveryCog(bot))
|
24
src/discord/cogs/ladder.py
Normal file
@ -0,0 +1,24 @@
|
||||
from discord.ext import commands
|
||||
from discord import app_commands, Interaction
|
||||
from classes.Fair import Fair
|
||||
|
||||
|
||||
class LadderCog(commands.Cog):
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
|
||||
# !Command
|
||||
@commands.command(aliases=['begin_game', 'game'])
|
||||
async def ladder(self, ctx):
|
||||
fair = Fair(bot=self.bot, author=ctx.author, ctx=ctx)
|
||||
await fair.divide()
|
||||
|
||||
# /Command
|
||||
@app_commands.command(name="ladder", description="Tries to start a fair game")
|
||||
async def ladder(self, interaction: Interaction):
|
||||
fair = Fair(bot=self.bot, author=interaction.user, interaction=interaction, slash=True)
|
||||
await fair.divide()
|
||||
|
||||
|
||||
async def setup(bot):
|
||||
await bot.add_cog(LadderCog(bot))
|
27
src/discord/cogs/leaderboard.py
Normal file
@ -0,0 +1,27 @@
|
||||
from discord.ext import commands
|
||||
from discord import app_commands, Interaction
|
||||
from classes.Config import Config
|
||||
from classes.Leaderboard import Leaderboard
|
||||
import requests
|
||||
import discord
|
||||
|
||||
class LeaderboardCog(commands.Cog):
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
|
||||
# !Command
|
||||
@commands.command(aliases=['lead', 'l'])
|
||||
async def leaderboard(self, ctx, players=5):
|
||||
"""Shows the Top <players> leaderboard: !leaderboard <number_of_players> <max>"""
|
||||
lead = Leaderboard(bot=self.bot, author=ctx.author, ctx=ctx, players=players)
|
||||
await lead.leaderboard()
|
||||
# /Command
|
||||
@app_commands.command(name="leaderboard", description="Shows the Top <players> leaderboard")
|
||||
async def leaderboard_slash(self, interaction: Interaction, players: int=5):
|
||||
"""Shows the Top <players> leaderboard: !leaderboard <number_of_players> <max>"""
|
||||
lead = Leaderboard(bot=self.bot, author = interaction.user, interaction=interaction, slash=True, players=players)
|
||||
await lead.leaderboard()
|
||||
|
||||
|
||||
async def setup(bot):
|
||||
await bot.add_cog(LeaderboardCog(bot))
|
24
src/discord/cogs/randomize.py
Normal file
@ -0,0 +1,24 @@
|
||||
from discord.ext import commands
|
||||
from classes.Randomizer import Randomizer
|
||||
from discord import app_commands, Interaction
|
||||
|
||||
class RandomizeCog(commands.Cog):
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
|
||||
@commands.command(aliases=['random'])
|
||||
async def randomize(self, ctx):
|
||||
"""Randomizes 10 players into teams: !randomize"""
|
||||
randomizer = Randomizer(bot=self.bot, author = ctx.author, ctx=ctx)
|
||||
await randomizer.randomize()
|
||||
return
|
||||
|
||||
@app_commands.command(name="randomize", description="Randomizes 10 players into teams")
|
||||
async def randomize_slash(self, interaction: Interaction):
|
||||
"""Randomizes 10 players into teams: !randomize"""
|
||||
randomizer = Randomizer(bot=self.bot, author = interaction.user, interaction=interaction, slash=True)
|
||||
await randomizer.randomize()
|
||||
return
|
||||
|
||||
async def setup(bot):
|
||||
await bot.add_cog(RandomizeCog(bot))
|
20
src/discord/cogs/register.py
Normal file
@ -0,0 +1,20 @@
|
||||
from discord.ext import commands
|
||||
from classes.Register import Register
|
||||
from discord import app_commands, Interaction
|
||||
|
||||
class RegisterCog(commands.Cog):
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
|
||||
@commands.command(aliases=['r', 'reg'])
|
||||
async def register(self, ctx, *args):
|
||||
registration = Register(self.bot, ctx.author, ctx, slash=False, player=args)
|
||||
await registration.register()
|
||||
|
||||
@app_commands.command(name="register", description="Creates a new account on server")
|
||||
async def registerSlash(self, interaction: Interaction, name: str):
|
||||
registration = Register(self.bot, interaction.author, interaction=interaction, slash=True, player=name)
|
||||
await registration.register()
|
||||
|
||||
async def setup(bot):
|
||||
await bot.add_cog(RegisterCog(bot))
|
20
src/requirements.txt
Normal file
@ -0,0 +1,20 @@
|
||||
aiohttp==3.8.4
|
||||
aiosignal==1.3.1
|
||||
asgiref==3.6.0
|
||||
async-timeout==4.0.2
|
||||
attrs==23.1.0
|
||||
certifi==2023.5.7
|
||||
charset-normalizer==3.1.0
|
||||
discord==2.2.3
|
||||
discord.py==2.2.3
|
||||
Django==4.2.1
|
||||
django-restframework==0.0.1
|
||||
frozenlist==1.3.3
|
||||
idna==3.4
|
||||
lcu-connector==1.0.2
|
||||
multidict==6.0.4
|
||||
psutil==5.9.5
|
||||
requests==2.30.0
|
||||
sqlparse==0.4.4
|
||||
urllib3==2.0.2
|
||||
yarl==1.9.2
|
3
src/server/leaderboard/admin.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
6
src/server/leaderboard/apps.py
Normal file
@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class LeaderboardConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'leaderboard'
|
3
src/server/leaderboard/models.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
289
src/server/leaderboard/static/animations.css
Normal file
@ -0,0 +1,289 @@
|
||||
|
||||
@-webkit-keyframes text-flicker-in-glow {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
10% {
|
||||
opacity: 0;
|
||||
text-shadow: none;
|
||||
}
|
||||
10.1% {
|
||||
opacity: 1;
|
||||
text-shadow: none;
|
||||
}
|
||||
10.2% {
|
||||
opacity: 0;
|
||||
text-shadow: none;
|
||||
}
|
||||
20% {
|
||||
opacity: 0;
|
||||
text-shadow: none;
|
||||
}
|
||||
20.1% {
|
||||
opacity: 1;
|
||||
text-shadow: 0 0 30px rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
20.6% {
|
||||
opacity: 0;
|
||||
text-shadow: none;
|
||||
}
|
||||
30% {
|
||||
opacity: 0;
|
||||
text-shadow: none;
|
||||
}
|
||||
30.1% {
|
||||
opacity: 1;
|
||||
text-shadow: 0 0 30px rgba(255, 255, 255, 0.45), 0 0 60px rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
30.5% {
|
||||
opacity: 1;
|
||||
text-shadow: 0 0 30px rgba(255, 255, 255, 0.45), 0 0 60px rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
30.6% {
|
||||
opacity: 0;
|
||||
text-shadow: none;
|
||||
}
|
||||
45% {
|
||||
opacity: 0;
|
||||
text-shadow: none;
|
||||
}
|
||||
45.1% {
|
||||
opacity: 1;
|
||||
text-shadow: 0 0 30px rgba(255, 255, 255, 0.45), 0 0 60px rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
text-shadow: 0 0 30px rgba(255, 255, 255, 0.45), 0 0 60px rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
55% {
|
||||
opacity: 1;
|
||||
text-shadow: 0 0 30px rgba(255, 255, 255, 0.45), 0 0 60px rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
55.1% {
|
||||
opacity: 0;
|
||||
text-shadow: none;
|
||||
}
|
||||
57% {
|
||||
opacity: 0;
|
||||
text-shadow: none;
|
||||
}
|
||||
57.1% {
|
||||
opacity: 1;
|
||||
text-shadow: 0 0 30px rgba(255, 255, 255, 0.55), 0 0 60px rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
60% {
|
||||
opacity: 1;
|
||||
text-shadow: 0 0 30px rgba(255, 255, 255, 0.55), 0 0 60px rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
60.1% {
|
||||
opacity: 0;
|
||||
text-shadow: none;
|
||||
}
|
||||
65% {
|
||||
opacity: 0;
|
||||
text-shadow: none;
|
||||
}
|
||||
65.1% {
|
||||
opacity: 1;
|
||||
text-shadow: 0 0 30px rgba(255, 255, 255, 0.55), 0 0 60px rgba(255, 255, 255, 0.35), 0 0 100px rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
75% {
|
||||
opacity: 1;
|
||||
text-shadow: 0 0 30px rgba(255, 255, 255, 0.55), 0 0 60px rgba(255, 255, 255, 0.35), 0 0 100px rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
75.1% {
|
||||
opacity: 0;
|
||||
text-shadow: none;
|
||||
}
|
||||
77% {
|
||||
opacity: 0;
|
||||
text-shadow: none;
|
||||
}
|
||||
77.1% {
|
||||
opacity: 1;
|
||||
text-shadow: 0 0 30px rgba(255, 255, 255, 0.55), 0 0 60px rgba(255, 255, 255, 0.4), 0 0 110px rgba(255, 255, 255, 0.2), 0 0 100px rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
85% {
|
||||
opacity: 1;
|
||||
text-shadow: 0 0 30px rgba(255, 255, 255, 0.55), 0 0 60px rgba(255, 255, 255, 0.4), 0 0 110px rgba(255, 255, 255, 0.2), 0 0 100px rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
85.1% {
|
||||
opacity: 0;
|
||||
text-shadow: none;
|
||||
}
|
||||
86% {
|
||||
opacity: 0;
|
||||
text-shadow: none;
|
||||
}
|
||||
86.1% {
|
||||
opacity: 1;
|
||||
text-shadow: 0 0 30px rgba(255, 255, 255, 0.6), 0 0 60px rgba(255, 255, 255, 0.45), 0 0 110px rgba(255, 255, 255, 0.25), 0 0 100px rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
text-shadow: 0 0 30px rgba(255, 255, 255, 0.6), 0 0 60px rgba(255, 255, 255, 0.45), 0 0 110px rgba(255, 255, 255, 0.25), 0 0 100px rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
@keyframes text-flicker-in-glow {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
10% {
|
||||
opacity: 0;
|
||||
text-shadow: none;
|
||||
}
|
||||
10.1% {
|
||||
opacity: 1;
|
||||
text-shadow: none;
|
||||
}
|
||||
10.2% {
|
||||
opacity: 0;
|
||||
text-shadow: none;
|
||||
}
|
||||
20% {
|
||||
opacity: 0;
|
||||
text-shadow: none;
|
||||
}
|
||||
20.1% {
|
||||
opacity: 1;
|
||||
text-shadow: 0 0 30px rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
20.6% {
|
||||
opacity: 0;
|
||||
text-shadow: none;
|
||||
}
|
||||
30% {
|
||||
opacity: 0;
|
||||
text-shadow: none;
|
||||
}
|
||||
30.1% {
|
||||
opacity: 1;
|
||||
text-shadow: 0 0 30px rgba(255, 255, 255, 0.45), 0 0 60px rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
30.5% {
|
||||
opacity: 1;
|
||||
text-shadow: 0 0 30px rgba(255, 255, 255, 0.45), 0 0 60px rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
30.6% {
|
||||
opacity: 0;
|
||||
text-shadow: none;
|
||||
}
|
||||
45% {
|
||||
opacity: 0;
|
||||
text-shadow: none;
|
||||
}
|
||||
45.1% {
|
||||
opacity: 1;
|
||||
text-shadow: 0 0 30px rgba(255, 255, 255, 0.45), 0 0 60px rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
text-shadow: 0 0 30px rgba(255, 255, 255, 0.45), 0 0 60px rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
55% {
|
||||
opacity: 1;
|
||||
text-shadow: 0 0 30px rgba(255, 255, 255, 0.45), 0 0 60px rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
55.1% {
|
||||
opacity: 0;
|
||||
text-shadow: none;
|
||||
}
|
||||
57% {
|
||||
opacity: 0;
|
||||
text-shadow: none;
|
||||
}
|
||||
57.1% {
|
||||
opacity: 1;
|
||||
text-shadow: 0 0 30px rgba(255, 255, 255, 0.55), 0 0 60px rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
60% {
|
||||
opacity: 1;
|
||||
text-shadow: 0 0 30px rgba(255, 255, 255, 0.55), 0 0 60px rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
60.1% {
|
||||
opacity: 0;
|
||||
text-shadow: none;
|
||||
}
|
||||
65% {
|
||||
opacity: 0;
|
||||
text-shadow: none;
|
||||
}
|
||||
65.1% {
|
||||
opacity: 1;
|
||||
text-shadow: 0 0 30px rgba(255, 255, 255, 0.55), 0 0 60px rgba(255, 255, 255, 0.35), 0 0 100px rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
75% {
|
||||
opacity: 1;
|
||||
text-shadow: 0 0 30px rgba(255, 255, 255, 0.55), 0 0 60px rgba(255, 255, 255, 0.35), 0 0 100px rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
75.1% {
|
||||
opacity: 0;
|
||||
text-shadow: none;
|
||||
}
|
||||
77% {
|
||||
opacity: 0;
|
||||
text-shadow: none;
|
||||
}
|
||||
77.1% {
|
||||
opacity: 1;
|
||||
text-shadow: 0 0 30px rgba(255, 255, 255, 0.55), 0 0 60px rgba(255, 255, 255, 0.4), 0 0 110px rgba(255, 255, 255, 0.2), 0 0 100px rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
85% {
|
||||
opacity: 1;
|
||||
text-shadow: 0 0 30px rgba(255, 255, 255, 0.55), 0 0 60px rgba(255, 255, 255, 0.4), 0 0 110px rgba(255, 255, 255, 0.2), 0 0 100px rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
85.1% {
|
||||
opacity: 0;
|
||||
text-shadow: none;
|
||||
}
|
||||
86% {
|
||||
opacity: 0;
|
||||
text-shadow: none;
|
||||
}
|
||||
86.1% {
|
||||
opacity: 1;
|
||||
text-shadow: 0 0 30px rgba(255, 255, 255, 0.6), 0 0 60px rgba(255, 255, 255, 0.45), 0 0 110px rgba(255, 255, 255, 0.25), 0 0 100px rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
text-shadow: 0 0 30px rgba(255, 255, 255, 0.6), 0 0 60px rgba(255, 255, 255, 0.45), 0 0 110px rgba(255, 255, 255, 0.25), 0 0 100px rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.flicker-in {
|
||||
-webkit-animation: text-flicker-in-glow 2s ease-in both;
|
||||
animation: text-flicker-in-glow 2s ease-in both;
|
||||
}
|
||||
|
||||
|
||||
@-webkit-keyframes tracking-in-expand {
|
||||
0% {
|
||||
letter-spacing: -0.5em;
|
||||
opacity: 0;
|
||||
}
|
||||
40% {
|
||||
opacity: 0.6;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@keyframes tracking-in-expand {
|
||||
0% {
|
||||
letter-spacing: -0.5em;
|
||||
opacity: 0;
|
||||
}
|
||||
40% {
|
||||
opacity: 0.6;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.tracking-in {
|
||||
-webkit-animation: tracking-in-expand 0.7s cubic-bezier(0.215, 0.610, 0.355, 1.000) 2s both;
|
||||
animation: tracking-in-expand 0.7s cubic-bezier(0.215, 0.610, 0.355, 1.000) 2s both;
|
||||
}
|
||||
|
BIN
src/server/leaderboard/static/customm.png
Normal file
After Width: | Height: | Size: 36 KiB |
BIN
src/server/leaderboard/static/favicon.png
Normal file
After Width: | Height: | Size: 14 KiB |
50
src/server/leaderboard/static/style.css
Normal file
@ -0,0 +1,50 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Roboto&display=swap');
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
|
||||
}
|
||||
|
||||
.body {
|
||||
/* Roboto for all text */
|
||||
font-family: 'Roboto', sans-serif;
|
||||
|
||||
}
|
||||
|
||||
@-webkit-keyframes color-change-2x {
|
||||
0% {
|
||||
background: #2e3030;
|
||||
}
|
||||
100% {
|
||||
background: #5f6b32;
|
||||
}
|
||||
}
|
||||
@keyframes color-change-2x {
|
||||
0% {
|
||||
background: #2e3030;
|
||||
}
|
||||
100% {
|
||||
background: #5f6b32;
|
||||
}
|
||||
}
|
||||
|
||||
.color-change-2x {
|
||||
-webkit-animation: color-change-2x 10s cubic-bezier(0.550, 0.085, 0.680, 0.530) infinite alternate both;
|
||||
animation: color-change-2x 10s cubic-bezier(0.550, 0.085, 0.680, 0.530) infinite alternate both;
|
||||
}
|
||||
|
||||
#logo{
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
margin-bottom: 20px;
|
||||
}
|
18
src/server/leaderboard/templates/base.html
Normal file
@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>custoMM</title>
|
||||
{% load static %}
|
||||
<link rel="shortcut icon" href="{% static 'favicon.png' %}" type="image/x-icon">
|
||||
<link rel="stylesheet" href="{% static 'style.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'animations.css' %}">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container color-change-2x">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
11
src/server/leaderboard/templates/index.html
Normal file
@ -0,0 +1,11 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<div class="logo flicker-in">
|
||||
<a href="https://github.com/confestim/custoMM"><img src="{% static 'customm.png' %}" alt="customm logo"></a>
|
||||
</div>
|
||||
|
||||
<h1 class= "flicker-in" style="animation-delay:1s;">Work in progress...</h1>
|
||||
{% endblock %}
|
7
src/server/leaderboard/urls.py
Normal file
@ -0,0 +1,7 @@
|
||||
from django.urls import include, path
|
||||
from . import views
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.home),
|
||||
]
|
4
src/server/leaderboard/views.py
Normal file
@ -0,0 +1,4 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
def home(request):
|
||||
return render(request, 'index.html', {})
|
@ -39,6 +39,7 @@ INSTALLED_APPS = [
|
||||
'django.contrib.staticfiles',
|
||||
'rest_framework',
|
||||
'server_client_api',
|
||||
'leaderboard',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
@ -56,7 +57,7 @@ ROOT_URLCONF = 'server.urls'
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [],
|
||||
'DIRS': ["leaderboard/templates"],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
@ -18,6 +18,7 @@ from django.urls import path, include
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('', include('server_client_api.urls')),
|
||||
path('api/', include('server_client_api.urls')),
|
||||
path('', include('leaderboard.urls')),
|
||||
# TODO: Add matchup calculation page.
|
||||
]
|
53
src/server/server_client_api/models.py
Normal file
@ -0,0 +1,53 @@
|
||||
from django.db import models
|
||||
|
||||
class Player(models.Model):
|
||||
discord = models.CharField(max_length=60, unique=True, null=True)
|
||||
lol = models.CharField(primary_key=True, max_length=60, unique=True )
|
||||
lol_id = models.CharField(max_length=60, null=True, blank=True , unique=True)
|
||||
discord_id = models.IntegerField(null=True, blank=True)
|
||||
mmr = models.IntegerField(default=600, editable=False)
|
||||
#mmr = models.IntegerField(default=600)
|
||||
games_played = models.IntegerField(default=0)
|
||||
def __str__(self):
|
||||
return f"{self.discord}:{self.lol}"
|
||||
|
||||
class Instructions(models.Model):
|
||||
game_id = models.CharField(max_length=30, unique=True)
|
||||
instructions = models.JSONField(default=dict)
|
||||
|
||||
class Stats(models.Model):
|
||||
"""LoL stats for each Player"""
|
||||
|
||||
# TODO: Implement in views, serializers and client
|
||||
|
||||
|
||||
|
||||
# Player, associated with Player model
|
||||
player = models.ForeignKey(Player, on_delete=models.CASCADE)
|
||||
|
||||
# Average KDA, Gold earned, Kill participation
|
||||
kda = models.FloatField(default=0)
|
||||
gold = models.FloatField(default=0)
|
||||
kp = models.FloatField(default=0)
|
||||
games_played = models.IntegerField(default=0)
|
||||
roles = models.JSONField(default=dict)
|
||||
# {"TOP":0, "JUNGLE":0, "MIDDLE":0, "ADC":0, "SUPPORT":0, "OTHER":0}
|
||||
usual_role = models.CharField(max_length=30, default="FILL")
|
||||
def __str__(self):
|
||||
return f"{self.player} - KDA:{self.kda}, Gold:{self.gold}, KP:{self.kp}"
|
||||
|
||||
class Game(models.Model):
|
||||
game_id = models.CharField(max_length=30, unique=True)
|
||||
|
||||
def __str__(self):
|
||||
return str(self.game_id)
|
||||
|
||||
class Current(models.Model):
|
||||
lobby_name = models.CharField(max_length=30, unique=True)
|
||||
players = models.IntegerField(default=0)
|
||||
creator = models.CharField(max_length=30, unique=True, primary_key=True,)
|
||||
teams = models.JSONField(default=dict)
|
||||
bravery = models.JSONField()
|
||||
|
||||
def __str__(self):
|
||||
return str(self.lobby_name)
|
49
src/server/server_client_api/serializers.py
Normal file
@ -0,0 +1,49 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from .models import Player, Game, Current, Instructions
|
||||
|
||||
|
||||
class PlayerSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
model = Player
|
||||
fields = ('discord', 'lol', 'lol_id', 'discord_id', 'mmr', 'usual_role', 'roles')
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
instance.discord_id = validated_data.get(
|
||||
'discord_id', instance.discord_id)
|
||||
instance.discord = validated_data.get('discord', instance.discord)
|
||||
instance.lol = validated_data.get('lol', instance.lol)
|
||||
instance.lol_id = validated_data.get('lol_id', instance.lol_id)
|
||||
instance.save()
|
||||
return instance
|
||||
|
||||
|
||||
class InstructionsSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Instructions
|
||||
fields = ("game_id", "instructions")
|
||||
|
||||
class GameSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Game
|
||||
fields = ("game_id",)
|
||||
|
||||
class CurrentSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Current
|
||||
fields = ("lobby_name", "creator", "players", "teams", "bravery")
|
||||
|
||||
def create(self, validated_data):
|
||||
"""
|
||||
Create and return a new `Current` instance, given the validated data.
|
||||
"""
|
||||
return Current.objects.create(**validated_data)
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
instance.players = validated_data.get('players', instance.players)
|
||||
instance.lobby_name = validated_data.get('lobby_name', instance.lobby_name)
|
||||
instance.save()
|
||||
return instance
|
3
src/server/server_client_api/tests.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
@ -4,10 +4,11 @@ from . import views
|
||||
|
||||
router = routers.DefaultRouter()
|
||||
router.register(r'players', views.PlayerViewSet)
|
||||
router.register(r'current', views.CurrentViewSet)
|
||||
|
||||
urlpatterns = [
|
||||
path('game/', views.game),
|
||||
path('games/', views.games),
|
||||
path('', include(router.urls)),
|
||||
path('games/', views.games),
|
||||
path('game/', views.game),
|
||||
path('api-auth/', include('rest_framework.urls', namespace='rest_framework'))
|
||||
]
|
@ -1,18 +1,13 @@
|
||||
from rest_framework import viewsets, filters, mixins
|
||||
from rest_framework.response import Response
|
||||
from .serializers import PlayerSerializer, GameSerializer
|
||||
from .models import Player, Game
|
||||
from .serializers import PlayerSerializer, GameSerializer, CurrentSerializer, InstructionsSerializer
|
||||
from .models import Player, Game, Current, Instructions
|
||||
|
||||
import math
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.response import Response
|
||||
|
||||
class WhatTheFuckDidYouDo(Exception):
|
||||
# We raise this when we don't know what the fuck happened
|
||||
def __init__(self, message="Congrats, you broke this program, please explain your steps as an issue at https://github.com/confestim/custoMM"):
|
||||
self.message = message
|
||||
super().__init__(self.message)
|
||||
|
||||
class PlayerViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
@ -20,7 +15,7 @@ class PlayerViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
search_fields = ['discord_id','lol']
|
||||
filter_backends = (filters.SearchFilter,)
|
||||
queryset = Player.objects.all().order_by('mmr')
|
||||
queryset = Player.objects.all().order_by('mmr').reverse()
|
||||
serializer_class = PlayerSerializer
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
@ -37,6 +32,70 @@ class PlayerViewSet(viewsets.ModelViewSet):
|
||||
self.perform_destroy(instance)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
class CurrentViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
Display and interaction with the active games in the database
|
||||
"""
|
||||
|
||||
queryset = Current.objects.all()
|
||||
serializer_class = CurrentSerializer
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
partial = kwargs.pop('partial', False)
|
||||
instance = self.get_object()
|
||||
serializer = self.get_serializer(instance, data=request.data, partial=partial)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
self.perform_update(serializer)
|
||||
return Response(serializer.data)
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
instance = Current.objects.get(creator=request.data.get("creator"))
|
||||
self.perform_destroy(instance)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@api_view(['GET', 'POST'])
|
||||
def instructions(request):
|
||||
# TODO: Implement
|
||||
"""
|
||||
Instructions for every player during the game.
|
||||
This a troll function, it should be disabled in the client if you don't want it.
|
||||
"""
|
||||
|
||||
possible_instructions = [
|
||||
"troll", # (everybody flashes at the same time)
|
||||
"emote", # (everybody spams emotes)
|
||||
"skin", # (everybody writes their skin)
|
||||
"summoner", # (everybody writes their summoner name)
|
||||
"dance", # (everybody dances)
|
||||
"laugh", # (everybody laughs)
|
||||
"joke", # (everybody tells a joke)
|
||||
"taunt", # (everybody taunts)
|
||||
"recall", # (everybody recalls)
|
||||
]
|
||||
if request.method == 'GET':
|
||||
instructions = Instructions.objects.all()
|
||||
serializer = InstructionsSerializer(instructions, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
if request.method == 'POST':
|
||||
if request.data.get("game_id") is None:
|
||||
return Response("No game id provided.", status=status.HTTP_400_BAD_REQUEST)
|
||||
if request.data.get("instructions") is None or request.data.get("user") is None:
|
||||
return Response("No instructions provided.", status=status.HTTP_400_BAD_REQUEST)
|
||||
if request.data.get("instructions") not in possible_instructions or request.data.get("user") not in Player.objects.all():
|
||||
return Response("Invalid instruction.", status=status.HTTP_400_BAD_REQUEST)
|
||||
instructions, created = Instructions.objects.get_or_create(game_id=request.data.get("game_id"))
|
||||
if not created:
|
||||
# Add to json field
|
||||
instructions.instructions[request.data.get("user")].append(request.data.get("instructions"))
|
||||
instructions.save()
|
||||
return Response("Added.", status=status.HTTP_201_CREATED)
|
||||
else:
|
||||
# Create json field
|
||||
instructions.instructions = {request.data.get("user"): [request.data.get("instructions")]}
|
||||
instructions.save()
|
||||
return Response("Added.", status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
@api_view(['GET', 'POST'])
|
||||
def games(request):
|
||||
@ -79,7 +138,6 @@ def game(request):
|
||||
if failed:
|
||||
return Response([x for x in players if x["found"] == False], status=status.HTTP_451_UNAVAILABLE_FOR_LEGAL_REASONS)
|
||||
|
||||
players.sort(key=lambda x: x['mmr'], reverse=True)
|
||||
|
||||
|
||||
# Sort the list of players based on their mmr values
|
||||
@ -154,7 +212,7 @@ def mmr_on_game(ign:str, avg_enemy_mmr:int, kda:float, outcome:bool):
|
||||
|
||||
# I feel like this could bug out(magically), so raise exception if neither of these is the case
|
||||
else:
|
||||
raise WhatTheFuckDidYouDo()
|
||||
raise Exception()
|
||||
|
||||
# Save to player
|
||||
player.mmr = mmr_change
|
||||
@ -200,7 +258,31 @@ def average_mmr(game):
|
||||
|
||||
return sum(winners)/5, sum(losers)/5
|
||||
|
||||
def update_roles(gamer, role, lane):
|
||||
try:
|
||||
player = Player.objects.get(lol=gamer)
|
||||
except Player.DoesNotExist:
|
||||
player = Player.objects.create(lol=gamer)
|
||||
|
||||
if lane == "TOP":
|
||||
player.roles["TOP"] += 1
|
||||
elif lane == "JUNGLE":
|
||||
player.roles["JUNGLE"] += 1
|
||||
elif lane == "MIDDLE":
|
||||
player.roles["MIDDLE"] += 1
|
||||
elif lane == "BOTTOM" and role == "CARRY":
|
||||
player.roles["ADC"] += 1
|
||||
elif lane == "BOTTOM" and role == "SUPPORT":
|
||||
player.roles["SUPPORT"]
|
||||
else:
|
||||
player.roles["OTHER"] += 1
|
||||
|
||||
most_played = max(player.roles, key=lambda x: player.roles[x])
|
||||
if most_played != player.usual_role:
|
||||
player.usual_role = most_played
|
||||
|
||||
player.save()
|
||||
|
||||
def parse_game(game):
|
||||
"""
|
||||
Parse game function
|
||||
@ -218,6 +300,7 @@ def parse_game(game):
|
||||
for player in teams["t1"]["summoners"]:
|
||||
print(player)
|
||||
mmr_on_game(player["name"], average, player["kda"], win_loss)
|
||||
update_roles(player["name"], player["role"],player["lane"])
|
||||
|
||||
|
||||
win_loss = not win_loss
|
||||
@ -226,7 +309,8 @@ def parse_game(game):
|
||||
for player in teams["t2"]["summoners"]:
|
||||
print(player)
|
||||
mmr_on_game(player["name"], average, player["kda"], win_loss)
|
||||
|
||||
update_roles(player["name"], player["role"], player["lane"])
|
||||
|
||||
return print("Done with changing mmr")
|
||||
|
||||
|