Compare commits
68 Commits
724a78ec70
...
1.1.1
Author | SHA1 | Date | |
---|---|---|---|
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 | |||
f30e7c7433 | |||
d46c835897 | |||
c9b66499f7 | |||
bcd9206340 | |||
3d57521581 |
19
.gitignore
vendored
19
.gitignore
vendored
@ -1,7 +1,22 @@
|
||||
config*
|
||||
# Config
|
||||
config.ini
|
||||
|
||||
# Environment
|
||||
.env/
|
||||
|
||||
# Database
|
||||
db.sqlite3
|
||||
|
||||
# Build garbage
|
||||
__pycache__/
|
||||
migrations/
|
||||
build/
|
||||
dist/
|
||||
dist/
|
||||
main.spec
|
||||
|
||||
# VSCode garbage
|
||||
.vscode/
|
||||
|
||||
# LCU_connector leftover
|
||||
riotgames.pem
|
||||
test.py
|
||||
|
18
README.md
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(URL+ f"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
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
|
192
discord_bot.py
192
discord_bot.py
@ -1,192 +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()
|
||||
|
||||
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.")
|
||||
|
||||
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'])
|
Binary file not shown.
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 24 KiB |
Binary file not shown.
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 21 KiB |
BIN
images/bob.png
Normal file
BIN
images/bob.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 52 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),
|
||||
),
|
||||
]
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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",)
|
37
src/client/README.md
Normal file
37
src/client/README.md
Normal file
@ -0,0 +1,37 @@
|
||||
# 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`
|
||||
|
||||
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`
|
||||
|
||||
|
BIN
src/client/assets/icon.png
Normal file
BIN
src/client/assets/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
71
src/client/classes/Game.py
Normal file
71
src/client/classes/Game.py
Normal file
@ -0,0 +1,71 @@
|
||||
import random
|
||||
import time
|
||||
import configparser
|
||||
from .Util import WhatTheFuckDidYouDo
|
||||
|
||||
class Game:
|
||||
def __init__(self, *, loop=None, connection):
|
||||
# Config
|
||||
self.config = configparser.ConfigParser()
|
||||
# Relative paths bad, fix this
|
||||
self.config.read ("../config.ini")
|
||||
self.URL = self.config["DEFAULT"]["URL"]
|
||||
self.password = self.config["LEAGUE"]["LOBBY_PASS"]
|
||||
# Loop until we get connection
|
||||
self.connection = connection
|
||||
|
||||
def list_all(self):
|
||||
# List all games
|
||||
time.sleep(2)
|
||||
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
|
||||
return self.join_by_id(self.search(str(name)))
|
||||
|
||||
def join_random(self):
|
||||
# Joins a random public game
|
||||
# mainly debug reasons
|
||||
return self.join_by_id(random.choice([x["id"] for x in self.list_all() if not x["hasPassword"]]))
|
||||
|
||||
|
||||
def create(self):
|
||||
# Creates game
|
||||
conn = self.connection
|
||||
name = "CustoMM " + str(random.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"]]]
|
23
src/client/classes/PeriodicScraper.py
Normal file
23
src/client/classes/PeriodicScraper.py
Normal file
@ -0,0 +1,23 @@
|
||||
from time import sleep
|
||||
from threading import Thread
|
||||
from .Scraper import Scraper
|
||||
import logging
|
||||
|
||||
class PeriodicScraper(Thread):
|
||||
def __init__(self):
|
||||
Thread.__init__(self)
|
||||
self.daemon = True
|
||||
self.connector:Scraper = Scraper()
|
||||
self.closed = False
|
||||
|
||||
def run(self):
|
||||
while True:
|
||||
if self.closed:
|
||||
self.connector.connection.stop()
|
||||
break
|
||||
|
||||
game_state = self.connector.check_for_game()
|
||||
|
||||
logging.info("Scraping...")
|
||||
self.connector.scrape()
|
||||
sleep(5)
|
258
src/client/classes/Scraper.py
Normal file
258
src/client/classes/Scraper.py
Normal file
@ -0,0 +1,258 @@
|
||||
from lcu_connector import Connector
|
||||
from lcu_connector.exceptions import ClientProcessError
|
||||
import logging
|
||||
import requests
|
||||
import time, sys
|
||||
import json
|
||||
import configparser
|
||||
from .Util import WhatTheFuckDidYouDo
|
||||
from .Game import Game
|
||||
|
||||
class Scraper:
|
||||
def __init__(self, *, loop=None, ui=None):
|
||||
self.ui = ui
|
||||
self.config = configparser.ConfigParser()
|
||||
# Relative paths bad, fix this
|
||||
self.config.read ("../config.ini")
|
||||
self.URL = self.config["DEFAULT"]["URL"]
|
||||
# Loop until we get connection
|
||||
self.connection = None
|
||||
|
||||
while not self.connection:
|
||||
try:
|
||||
self.connection = Connector()
|
||||
self.connection.start()
|
||||
except ClientProcessError:
|
||||
print("League client not open, sleeping...")
|
||||
time.sleep(90)
|
||||
self.summoner = self.connection.get('/lol-summoner/v1/current-summoner').json()
|
||||
self.name = self.summoner["displayName"]
|
||||
|
||||
def calculate_kda(self, 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)
|
||||
|
||||
def parse_history(self, 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
|
||||
"""
|
||||
|
||||
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.
|
||||
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": self.calculate_kda(kills, assists, deaths)})
|
||||
else:
|
||||
parsed_match["participants"]["t2"]["summoners"].append({"name":match["participantIdentities"][player]["player"]["summonerName"], "kda": self.calculate_kda(kills, assists, deaths)})
|
||||
parsed_matches.append(parsed_match)
|
||||
if not new:
|
||||
print("Already up to date.")
|
||||
time.sleep(3)
|
||||
|
||||
|
||||
return parsed_matches
|
||||
|
||||
def register_summoner(self, claim, claimed):
|
||||
if claim:
|
||||
account = requests.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:
|
||||
print(f"Alright, the account is now yours, {claimed['discord']}.")
|
||||
else:
|
||||
print("Something went wrong when claiming your account...")
|
||||
else:
|
||||
requests.delete(f"{self.URL}/players/{claimed['discord_id']}")
|
||||
|
||||
|
||||
def check_summoner(self):
|
||||
"""
|
||||
Checks if summoner is registered
|
||||
"""
|
||||
|
||||
connection = self.connection
|
||||
# Summoner
|
||||
|
||||
|
||||
# Check if account is claimed
|
||||
try:
|
||||
claimed = requests.get(f"{self.URL}/players/?search={self.summoner['displayName']}").json()[0]
|
||||
except Exception as e:
|
||||
print(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 WhatTheFuckDidYouDo()
|
||||
|
||||
def move_needed(self, checker, game, name):
|
||||
# Move if you have to
|
||||
|
||||
# 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()
|
||||
|
||||
if name in local_teams[0] and not name in checker["teams"][0]:
|
||||
game.move()
|
||||
logging.info("Moving to Team 2")
|
||||
elif name in local_teams[1] and not name in checker["teams"][1]:
|
||||
game.move()
|
||||
logging.info("Moving to Team 1")
|
||||
|
||||
def start(self, checker, game):
|
||||
self.move_needed(checker, game, self.name)
|
||||
time.sleep(5)
|
||||
# Wait until there are 10 players(confirmed) in the lobby
|
||||
timeout_counter = 0
|
||||
while response := requests.get(f"{self.URL}/current/{self.name}").json()["players"] != 10:
|
||||
logging.info("Waiting for players...")
|
||||
timeout_counter += 5
|
||||
if timeout_counter == 60:
|
||||
logging.info("Timeout, aborting...")
|
||||
break
|
||||
time.sleep(5)
|
||||
if response == 10:
|
||||
logging.info("Starting game...")
|
||||
game.start()
|
||||
else:
|
||||
game.leave()
|
||||
requests.delete(f"{self.URL}/current/{self.name}")
|
||||
|
||||
def check_for_game(self):
|
||||
"""
|
||||
Checks if a game is going on right now.
|
||||
"""
|
||||
try:
|
||||
checker = requests.get(f"{self.URL}/current").json()[0]
|
||||
except IndexError:
|
||||
return "NO_GAME"
|
||||
game = Game(connection=self.connection)
|
||||
|
||||
# If you are indeed the creator, create the game and disclose its name to the server
|
||||
if checker["creator"] == self.name:
|
||||
created = game.create()
|
||||
# TODO: DEBUG
|
||||
print(checker["teams"])
|
||||
r = requests.put(f"{self.URL}/current/{self.name}/", data={
|
||||
"lobby_name": created,
|
||||
"creator": self.name,
|
||||
"players": 1,
|
||||
"teams": json.dumps(checker["teams"], indent=4)
|
||||
})
|
||||
print(r.content)
|
||||
|
||||
|
||||
# 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 requests.get(f"{self.URL}/current").json()[0].get("lobby_name"):
|
||||
time.sleep(10)
|
||||
checker = requests.get(f"{self.URL}/current").json()
|
||||
name = checker["lobby_name"]
|
||||
|
||||
# Join the lobby
|
||||
game.join_by_name(name)
|
||||
|
||||
# Update count of players
|
||||
requests.put(f"{self.URL}/current/{checker['creator']}", data={
|
||||
"lobby_name": checker["lobby_name"],
|
||||
"creator": name,
|
||||
"players": int(checker["players"])+1,
|
||||
"teams": json.dumps(checker["teams"], indent=4)
|
||||
|
||||
})
|
||||
return "JOINED"
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def scrape(self):
|
||||
"""Scrapes current account and sends it to server"""
|
||||
|
||||
connection = self.connection
|
||||
self.check_summoner()
|
||||
# Match History
|
||||
match_history = connection.get('/lol-match-history/v1/products/lol/current-summoner/matches?endIndex=99')
|
||||
match_history = match_history.json()
|
||||
|
||||
# Stage old ids in order for them to be parsed
|
||||
old_ids = requests.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 = requests.post(f"{self.URL}/games/", json=i)
|
||||
if req.status_code == 500:
|
||||
print("Serverside error! Contact maintainer!")
|
||||
|
||||
return len(games)
|
69
src/client/classes/UI.py
Normal file
69
src/client/classes/UI.py
Normal file
@ -0,0 +1,69 @@
|
||||
import pystray
|
||||
from PIL import Image
|
||||
from .Scraper import Scraper
|
||||
import pyautogui
|
||||
from time import sleep
|
||||
|
||||
class UI():
|
||||
|
||||
def __init__(self,scraper, periodic, parent):
|
||||
image = Image.open(parent + "\\assets\\icon.png")
|
||||
|
||||
self.periodic = periodic
|
||||
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(
|
||||
"Exit", self.quit
|
||||
)
|
||||
)
|
||||
self.icon = pystray.Icon(
|
||||
"name", image, "CustoMM", self.menu)
|
||||
self.scraper = scraper
|
||||
self.check_registration()
|
||||
self.icon.run_detached()
|
||||
|
||||
|
||||
def check(self):
|
||||
self.icon.notify("This is discouraged, as it is done automatically anyway.", "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):
|
||||
self.icon.notify("Game report initiated.")
|
||||
self.scraper.scrape()
|
||||
self.icon.notify("Game reported", "Your game has been reported to the server.")
|
||||
|
||||
|
||||
def check_registration(self):
|
||||
check = self.scraper.check_summoner()
|
||||
if check == "USER_DOES_NOT_EXIST":
|
||||
self.icon.notify("You are not registered, please register on the website.")
|
||||
elif check == "UNCLAIMED":
|
||||
self.icon.notify("You have not claimed your account yet, please claim it on discord -> !registed <ACCOUNT_NAME>.")
|
||||
elif check[0] == "REGISTRATION_IN_PROGRESS":
|
||||
prompt = pyautogui.confirm(f"Your account is currently being registered by {check[1]}, do you want to proceed?")
|
||||
if prompt:
|
||||
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, query):
|
||||
icon.stop()
|
||||
self.periodic.closed = True
|
||||
|
5
src/client/classes/Util.py
Normal file
5
src/client/classes/Util.py
Normal file
@ -0,0 +1,5 @@
|
||||
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)
|
45
src/client/main.py
Normal file
45
src/client/main.py
Normal file
@ -0,0 +1,45 @@
|
||||
import requests
|
||||
|
||||
# Edit config.ini when running for the first time
|
||||
import sys
|
||||
import os
|
||||
import configparser
|
||||
from time import sleep
|
||||
import logging
|
||||
|
||||
# Custom imports
|
||||
from classes.Util import WhatTheFuckDidYouDo
|
||||
from classes.UI import UI
|
||||
from classes.PeriodicScraper import PeriodicScraper
|
||||
from classes.Scraper import Scraper
|
||||
|
||||
# Config section
|
||||
parent = os.path.dirname(os.path.abspath(__file__))
|
||||
config = configparser.ConfigParser()
|
||||
logging.info(parent + "\\config.ini")
|
||||
print(parent + "\\config.ini")
|
||||
URL = config["DEFAULT"]["URL"]
|
||||
logging.basicConfig(format='%(asctime)s - %(message)s', level=logging.INFO)
|
||||
|
||||
# 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()
|
||||
|
||||
# Get current summoner
|
||||
def main():
|
||||
# Match scraping
|
||||
# Running the UI
|
||||
periodic = PeriodicScraper()
|
||||
ui = UI(scraper=periodic.connector, periodic=periodic, parent=parent)
|
||||
periodic.start()
|
||||
periodic.join()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
137
src/discord/bot.py
Normal file
137
src/discord/bot.py
Normal file
@ -0,0 +1,137 @@
|
||||
import asyncio
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
import requests
|
||||
from urllib.parse import quote
|
||||
from classes.Target import Target
|
||||
import json
|
||||
import random
|
||||
|
||||
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):
|
||||
"""Randomizes 10 people into 2 teams: !randomize"""
|
||||
target = Target(ctx, bot)
|
||||
await target.randomize()
|
||||
return
|
||||
|
||||
|
||||
@bot.command()
|
||||
async def begin_game(ctx):
|
||||
"""Tries to start a fair game: !begin_game"""
|
||||
target = Target(ctx, bot)
|
||||
|
||||
players = await target.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:
|
||||
print(i.name)
|
||||
player = requests.get(f"{target.URL}/players/?search={quote(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(["players={}".format(quote(player)) for player in valid_players])
|
||||
|
||||
teams = requests.get(
|
||||
f"{target.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()
|
||||
# TODO: Debug, remove
|
||||
print(teams)
|
||||
|
||||
await target.split(teams[0], teams[1])
|
||||
requests.post(f"{target.URL}/current/", data={
|
||||
"lobby_name": None,
|
||||
"players": 0,
|
||||
"creator": random.choice(valid_players),
|
||||
"teams": json.dumps(teams)
|
||||
})
|
||||
return
|
||||
|
||||
|
||||
@bot.command()
|
||||
async def register(ctx, *args):
|
||||
"""Registers a user to the database: !register <league_name>"""
|
||||
target = Target(ctx, bot)
|
||||
name = " ".join(args)
|
||||
# TODO: add confirmation dialog
|
||||
if len(name) < 4:
|
||||
return await ctx.send("Provide a normal username (cAsE sEnSiTiVe)")
|
||||
print(target.URL)
|
||||
league_name = requests.get(f"{target.URL}/players/{name}").json()
|
||||
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.")
|
||||
|
||||
claim_account = requests.post(f"{target.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"{target.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...")
|
||||
|
||||
@bot.command()
|
||||
async def leaderboard(ctx):
|
||||
"""Shows the Top 5 leaderboard: !leaderboard"""
|
||||
target = Target(ctx, bot)
|
||||
leaderboard = requests.get(f"{target.URL}/players").json()[:5]
|
||||
leaderboard = "TOP 5 PLAYERS:\n-------------\n" + '\n'.join([f"{x['lol']} with {x['mmr']}" for x in leaderboard])
|
||||
print(leaderboard)
|
||||
await ctx.send(f"```{leaderboard}```")
|
||||
return
|
||||
|
||||
# Change to your bot token in config.ini
|
||||
bot.run(Target("no", bot).token)
|
82
src/discord/classes/Target.py
Normal file
82
src/discord/classes/Target.py
Normal file
@ -0,0 +1,82 @@
|
||||
import configparser
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
import asyncio
|
||||
from typing import List
|
||||
import random
|
||||
|
||||
class Target:
|
||||
def __init__(self, ctx, bot:commands.Bot):
|
||||
|
||||
config = configparser.ConfigParser()
|
||||
config.read("../config.ini")
|
||||
self.URL = config["DEFAULT"]["URL"]
|
||||
self.team_1 = config['DISCORD']['TEAM_1']
|
||||
self.team_2 = config['DISCORD']['TEAM_2']
|
||||
self.token = config['DISCORD']['TOKEN']
|
||||
self.ctx = ctx
|
||||
self.bot = bot
|
||||
|
||||
async def ready(self) -> bool:
|
||||
"""
|
||||
Checks if players are ready to be split
|
||||
"""
|
||||
self.trying_prompt = await self.ctx.send('Trying to start fair game!')
|
||||
|
||||
# Fetching user channel
|
||||
try:
|
||||
channel = self.bot.get_channel(self.ctx.author.voice.channel.id)
|
||||
except AttributeError:
|
||||
await self.trying_prompt.delete()
|
||||
await self.ctx.send("You think you're cool or something? Get in a channel first.")
|
||||
return False
|
||||
|
||||
players = channel.members
|
||||
if len(players) < 10:
|
||||
await self.trying_prompt.delete()
|
||||
await self.ctx.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.ctx.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.team_1)
|
||||
team_2 = self.bot.get_channel(self.team_2)
|
||||
|
||||
# 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
|
||||
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_2[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.ctx.send(embed=one_em)
|
||||
await self.ctx.send(embed=two_em)
|
||||
await self.trying_prompt.delete()
|
||||
return
|
||||
|
||||
async def randomize(self):
|
||||
players = await self.ready()
|
||||
if not players:
|
||||
return False
|
||||
|
||||
random.shuffle(players)
|
||||
|
||||
self.split(players[:int(len(players)/2)], players[int(len(players)/2):])
|
||||
return True
|
||||
|
20
src/requirements.txt
Normal file
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
|
@ -6,6 +6,7 @@ class Player(models.Model):
|
||||
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}"
|
||||
@ -14,3 +15,11 @@ class Game(models.Model):
|
||||
game_id = models.CharField(max_length=30, unique=True)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
def __str__(self):
|
||||
return str(self.lobby_name)
|
43
src/server/server_client_api/serializers.py
Normal file
43
src/server/server_client_api/serializers.py
Normal file
@ -0,0 +1,43 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from .models import Player, Game, Current
|
||||
|
||||
|
||||
class PlayerSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
model = Player
|
||||
fields = ('discord', 'lol', 'lol_id', 'discord_id', 'mmr')
|
||||
|
||||
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",)
|
||||
|
||||
class CurrentSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Current
|
||||
fields = ("lobby_name", "creator", "players", "teams")
|
||||
|
||||
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
|
@ -4,6 +4,7 @@ from . import views
|
||||
|
||||
router = routers.DefaultRouter()
|
||||
router.register(r'players', views.PlayerViewSet)
|
||||
router.register(r'current', views.CurrentViewSet)
|
||||
|
||||
urlpatterns = [
|
||||
path('game/', views.game),
|
@ -1,7 +1,7 @@
|
||||
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
|
||||
from .models import Player, Game, Current
|
||||
|
||||
import math
|
||||
from rest_framework import status
|
||||
@ -20,7 +20,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,7 +37,28 @@ 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 games(request):
|
||||
"""
|
||||
@ -64,7 +85,7 @@ def game(request):
|
||||
"""
|
||||
Returns fair team composition for a given list of players
|
||||
"""
|
||||
player_ids = request.data.getlist("players")
|
||||
player_ids = request.GET.getlist("players")
|
||||
players = list()
|
||||
failed = False
|
||||
for player in player_ids:
|
||||
@ -79,7 +100,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
|
||||
@ -123,7 +143,7 @@ def game(request):
|
||||
team1 = group1[::2] + group2[1::2]
|
||||
team2 = group2[::2] + group1[1::2]
|
||||
|
||||
return Response({team1, team2})
|
||||
return Response((team1, team2), status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user