Compare commits

...

125 Commits

Author SHA1 Message Date
550d089b35 Started work on ultimate bravery implementation 2023-07-08 00:19:48 +03:00
347cbb2365 🐛 Added bugfix TODO 🐛 2023-06-29 18:22:39 +03:00
540bc45bfe Began work on instructions. Remade exception. 2023-06-29 18:09:50 +03:00
8a52816c8c Decluttered the register function. 🗑 2023-06-16 16:52:21 +03:00
6b84cd6196 Added some new logos 2023-06-16 16:39:05 +03:00
1500d4c6f8 Bug disappeared by itself. How wonderful. 2023-06-16 15:38:55 +02:00
67a8ce60bd Updated gitignore 2023-06-16 15:37:19 +02:00
8329290d38 please god find the bug 2023-06-02 05:03:14 +03:00
fd0cd973bb Shortened leaderboard + small bug fix in random 2023-06-01 00:35:53 +03:00
200a8022f8 Tidied up discord bot in order to fix bugs easily 2023-05-24 19:32:11 +03:00
5fa2d7719f Added config in discord 2023-05-24 17:24:44 +03:00
226942273c Intents? 2023-05-24 02:09:22 +03:00
fd96ae9ea8 Integer channel id? 2023-05-24 02:08:51 +03:00
f410d470f0 wiw 2023-05-24 02:07:55 +03:00
1eb9017f12 test 2023-05-24 02:07:08 +03:00
8376e51dab TEST 2023-05-24 02:06:12 +03:00
5e0b267813 aaa 2023-05-24 02:05:20 +03:00
46875a4d6a wtf?? 2023-05-24 02:04:25 +03:00
66504294e6 DEBUG 2023-05-24 02:02:51 +03:00
4e43d46c00 debug 2023-05-24 01:59:39 +03:00
9e500693f5 debug 2023-05-24 01:59:01 +03:00
fbaaf0e28a sugoma 2023-05-24 01:57:45 +03:00
115cc95722 DEBUG 2023-05-24 01:55:20 +03:00
aa12387202 HOTFIX 2023-05-24 01:12:24 +03:00
3610e784f5 fewrewr 2023-05-24 01:10:07 +03:00
47df5486b6 hotfix 2023-05-24 01:08:39 +03:00
08d9ae36b4 hotfix 2023-05-24 01:07:59 +03:00
ad6925d320 LMAO 2023-05-24 01:07:19 +03:00
0cf870da66 HOTFIX 2023-05-24 01:05:46 +03:00
f4d98cc094 hotfix 2023-05-24 01:04:17 +03:00
a9d6bedde1 Ben 2023-05-24 00:03:39 +02:00
e13cc3f7a8 Created placeholder for website 2023-05-22 04:35:36 +03:00
7e3d3cd189 l 2023-05-22 04:09:28 +03:00
f74d202bc5 TEST 2023-05-22 04:08:26 +03:00
27da635494 LOOOL 2023-05-22 04:07:37 +03:00
8de1b89159 TEST 2023-05-22 04:05:24 +03:00
fcc556af33 Really? 2023-05-22 04:04:39 +03:00
6bc2686ab5 testing 2023-05-22 04:02:31 +03:00
c2f49c0861 LOL 2023-05-22 04:00:29 +03:00
e7d1e5833e Making website 2023-05-22 03:59:38 +03:00
359dfcbf98 Yeet 2023-05-22 03:41:24 +03:00
bcd6e8be45 Server updating 2023-05-22 03:39:10 +03:00
72905f85a2 Reformatting and renaming 2023-05-22 03:37:33 +03:00
aaf0699f1d Made self update better by including it in the UI 2023-05-22 01:59:37 +03:00
508223142e don't need the max functionality on second thought 2023-05-17 18:45:41 +03:00
834af6d1b1 better fix 2023-05-17 18:44:34 +03:00
d196d3cba8 Fix 2023-05-17 18:42:45 +03:00
95b2dd2c79 NASTY ASS FIX 2023-05-17 18:41:58 +03:00
111bebb2ea nasty workaround 2023-05-17 18:40:31 +03:00
c39c654e67 Added max capability for leaderboard 2023-05-17 18:39:26 +03:00
12ea31902f Aliases + logical mistake 2023-05-17 18:38:09 +03:00
6a44597d5a made leaderboard look pretty 2023-05-17 18:35:21 +03:00
8fc0bcf85d fixed logical mistake 2023-05-17 18:27:14 +03:00
9f1b8bbc10 Added real check for registration 2023-05-17 18:26:22 +03:00
834874526d Fixed bot(?) 2023-05-17 17:22:03 +02:00
4699557f0d 📝 + 🗑️ + Notifications + Updater 2023-05-17 18:19:31 +03:00
b8cd55c763 Merge branch 'main' of dumtaxat-git:/boyan_k/custoMM 2023-05-17 17:17:35 +02:00
dcac84dc67 Fixed some stuff 2023-05-17 17:17:26 +02:00
25073a4278 Merge pull request 'Created self updater, fixed joining, long timeout' (#12) from windows into main
Reviewed-on: boyan_k/custoMM#12
2023-05-17 02:53:54 +02:00
54d97467c3 Created self updater, fixed joining, long timeout 2023-05-17 02:52:27 +02:00
0b6fb3379d Merge pull request 'Fixed file paths, LICENSE and README for client.' (#11) from windows into main
Reviewed-on: boyan_k/custoMM#11
2023-05-16 22:57:17 +02:00
41012f11c3 Fixed README and file paths, time to merge!😊 2023-05-16 22:55:08 +02:00
603a169c39 Windows folders :( 2023-05-16 21:33:25 +02:00
a9db65f8b6 cleanup 2023-05-16 21:12:22 +02:00
f1ce1bff30 actually fixed readme 2023-05-16 21:11:51 +02:00
7ea995f1ec fixed readme 2023-05-16 21:11:29 +02:00
cee70ab57d Added timouts and ready to ship ver 1.1.1 :party: 2023-05-16 21:10:07 +02:00
d66becf13a Merge branch 'main' of dumtaxat-git:boyan_k/customm 2023-05-16 17:59:51 +03:00
e61029fdf5 Small change in server 2023-05-16 17:59:48 +03:00
204e3aff28 amogus 2023-05-11 06:25:38 +02:00
e09c2ebd5b Automatic team movement kinda works, have to test 2023-05-11 05:16:41 +02:00
fdf8739fb8 Done enough ⚰️ 2023-05-09 05:33:19 +02:00
308534b711 Current 2023-05-09 05:05:22 +02:00
f7d07d2ed6 Class viewset 2023-05-09 05:04:27 +02:00
e784ea0f4d COME ON 2023-05-09 05:00:00 +02:00
489a06504f Last try 2023-05-09 04:53:50 +02:00
d3772eae5e back to all 2023-05-09 04:50:02 +02:00
6ab4083f54 trying to access data 2023-05-09 04:49:00 +02:00
978f7535f3 sdsd 2023-05-09 04:46:01 +02:00
e652c25709 testing 2023-05-09 04:42:52 +02:00
5531063035 primary keyyy 2023-05-09 04:42:13 +02:00
233fc7babf sdasdsd 2023-05-09 04:34:57 +02:00
666d778c3d drfwdrwer 2023-05-09 04:34:19 +02:00
3c897ff9a6 LOL? 2023-05-09 04:32:12 +02:00
b00fc8ac0c PLS 2023-05-09 04:28:33 +02:00
2297205327 Pls 2023-05-09 04:27:34 +02:00
1e3f1c0e5d 4 AM fixes 2023-05-09 04:23:04 +02:00
d334d7b5bd Some fixes 2023-05-09 04:20:57 +02:00
465132d940 Fixed some import errors 2023-05-09 04:11:19 +02:00
9b856fe289 formatting 2023-05-09 04:05:41 +02:00
df3a601fae lol 2023-05-09 04:04:50 +02:00
43728f8807 sdsds 2023-05-09 04:03:48 +02:00
697d3af498 Im getting the hang of copilot 2023-05-09 04:02:38 +02:00
7d1d710740 Small mistake 2023-05-09 03:57:35 +02:00
f7034de2fd Holy shit almost version 1.1.1 2023-05-09 03:56:46 +02:00
0b1117fae2 kfhrekjtrentkjrentkjernewkjret 2023-05-08 22:29:25 +02:00
6bdfe32e87 NULL 2023-05-08 22:27:47 +02:00
a56fc45b60 NULL 2023-05-08 22:25:56 +02:00
750faadf1b PLS GOD BE GRACIOUS 2023-05-08 22:24:36 +02:00
f6b5791916 Minor fixes 2023-05-08 17:48:08 +02:00
b0bc592fc4 Merge branch 'main' of dumtaxat-git:boyan_k/customm 2023-05-08 00:55:31 +02:00
c6728a401b Updated flowcharts 2023-05-08 00:54:49 +02:00
b7ecbe2630 Merge branch 'main' of https://git.confest.im/boyan_k/custoMM 2023-05-07 23:31:25 +02:00
3b90aa1d49 Tidied 2023-05-07 23:31:22 +02:00
e1b575ecc8 Delet 2023-05-07 17:09:20 +02:00
4bbcb794c6 Removed vscode.json 2023-05-07 16:18:54 +02:00
85a19d7eb0 Changed the lib for League scraping, old one bad. 2023-05-07 16:18:37 +02:00
b19b21dcf3 wirks 2023-05-07 14:40:51 +02:00
b430daa5ee Some aesthetic fixes 2023-05-06 23:11:02 +02:00
685319042b Async consistency fix 2023-05-06 23:09:10 +02:00
a342c1b01d sdaasdas 2023-05-06 23:05:32 +02:00
aab5c17527 Small mistake 2023-05-06 23:03:33 +02:00
ce0aba8b4d kekw 2023-05-06 22:58:58 +02:00
37eab5bc6f Reformed the entire repo, might revert if no work. 2023-05-06 22:55:40 +02:00
3fd5837a27 added new diagram 2023-05-06 02:22:32 +02:00
85e9ed46fc added new diagram 2023-05-06 02:20:23 +02:00
5d71faee8e added new diagram 2023-05-06 02:19:44 +02:00
1046c10d82 added new diagram 2023-05-06 02:10:29 +02:00
ea4f3ecf8f Merge branch 'main' of https://git.confest.im/boyan_k/custoMM 2023-05-06 02:08:07 +02:00
3514b3a32e Fixed params mistake in discord bot 2023-05-06 02:58:50 +02:00
5402b16f44 Fixes in discord bot, I don't want to say this, but it might work 2023-05-06 02:16:27 +02:00
8391231390 added new diagram 2023-05-06 02:07:53 +02:00
7ae775276a Added shitty check for username validty in discord bot 2023-05-06 01:37:42 +02:00
eb89cbb240 Merge branch 'main' of dumtaxat-git:/boyan_k/custoMM 2023-05-06 01:35:17 +02:00
44165c18a5 Post-merge cleanup 2023-05-06 01:35:06 +02:00
83 changed files with 1982 additions and 658 deletions

18
.gitignore vendored
View File

@ -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

View File

@ -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:

View File

@ -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
![Mechanism](images/Mechanism.png)
CORRECTION, the client will be just a simple exe file that one runs once they want/finish a game. Lol.
![Mechanism](images/Mechanism.png)
## 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:
![Registration](images/Registration.png)
## 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.

View File

@ -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

View File

@ -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()

View File

@ -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,
)

View File

@ -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

View File

@ -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'])

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

BIN
images/inverted_logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
images/logo_wip.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View File

@ -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)),
],
),
]

View File

@ -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',
),
]

View File

@ -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,
),
]

View File

@ -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),
),
]

View File

@ -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),
),
]

View File

@ -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)

View File

@ -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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View 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
View 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"]]]

View 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()

View 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

View 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()

View 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
View 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

View 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
View 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
View 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
View 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)

View 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'])

View 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

View 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)

View 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

View 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...")

View 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

View 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))

View 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))

View 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))

View 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))

View 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
View 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

View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class LeaderboardConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'leaderboard'

View File

@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

View 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;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View 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;
}

View 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>

View 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 %}

View File

@ -0,0 +1,7 @@
from django.urls import include, path
from . import views
urlpatterns = [
path('', views.home),
]

View File

@ -0,0 +1,4 @@
from django.shortcuts import render
def home(request):
return render(request, 'index.html', {})

View File

@ -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': [

View File

@ -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.
]

View 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)

View 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

View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

@ -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'))
]

View File

@ -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")