From 500891fb7d884eddfe6e4c92bd999d789f02413c Mon Sep 17 00:00:00 2001 From: Zefanja Jobse Date: Sat, 13 Apr 2024 15:30:22 +0200 Subject: [PATCH] opensource unused --- .github/workflows/docker.yml | 51 +++ .github/workflows/main.yml | 83 +++++ .gitignore | 1 + Dockerfile | 16 + app.py | 61 ++++ battlefield/api/__init__.py | 0 battlefield/api/api_requests.py | 423 ++++++++++++++++++++++ battlefield/api/authSession.py | 88 +++++ battlefield/api/constants.py | 81 +++++ battlefield/api/loginProcess.py | 70 ++++ battlefield/api/redisDB.py | 39 ++ battlefield/api/searchPlayer.py | 311 ++++++++++++++++ battlefield/bftracker/bf1.py | 107 ++++++ battlefield/bftracker/bf5.py | 75 ++++ battlefield/commands/bfstats.py | 256 +++++++++++++ battlefield/commands/grapher.py | 56 +++ battlefield/commands/vehicles.py | 50 +++ battlefield/commands/weapons.py | 52 +++ battlefield/handlers/__init__.py | 3 + battlefield/handlers/bfstats.py | 126 +++++++ battlefield/handlers/vehicles.py | 110 ++++++ battlefield/handlers/weapons.py | 110 ++++++ battlefield/imageStats/fonts/Futura.ttf | Bin 0 -> 38980 bytes battlefield/imageStats/images/bf1-1.png | Bin 0 -> 511538 bytes battlefield/imageStats/images/bf2-1.png | Bin 0 -> 676111 bytes battlefield/imageStats/images/bf3-1.png | Bin 0 -> 388778 bytes battlefield/imageStats/images/bf4-1.png | Bin 0 -> 387095 bytes battlefield/imageStats/images/bfh-1.png | Bin 0 -> 480742 bytes battlefield/imageStats/images/bfv-1.png | Bin 0 -> 466773 bytes battlefield/imageStats/main.py | 134 +++++++ battlefield/imageStats/platforms/pc.png | Bin 0 -> 843 bytes battlefield/imageStats/platforms/psn.png | Bin 0 -> 2358 bytes battlefield/imageStats/platforms/xbox.png | Bin 0 -> 1190 bytes battlefield/mongo.py | 25 ++ requirements.txt | 21 ++ task-definition.json | 49 +++ 36 files changed, 2398 insertions(+) create mode 100644 .github/workflows/docker.yml create mode 100644 .github/workflows/main.yml create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 app.py create mode 100644 battlefield/api/__init__.py create mode 100644 battlefield/api/api_requests.py create mode 100644 battlefield/api/authSession.py create mode 100644 battlefield/api/constants.py create mode 100644 battlefield/api/loginProcess.py create mode 100644 battlefield/api/redisDB.py create mode 100644 battlefield/api/searchPlayer.py create mode 100644 battlefield/bftracker/bf1.py create mode 100644 battlefield/bftracker/bf5.py create mode 100644 battlefield/commands/bfstats.py create mode 100644 battlefield/commands/grapher.py create mode 100644 battlefield/commands/vehicles.py create mode 100644 battlefield/commands/weapons.py create mode 100644 battlefield/handlers/__init__.py create mode 100644 battlefield/handlers/bfstats.py create mode 100644 battlefield/handlers/vehicles.py create mode 100644 battlefield/handlers/weapons.py create mode 100644 battlefield/imageStats/fonts/Futura.ttf create mode 100644 battlefield/imageStats/images/bf1-1.png create mode 100644 battlefield/imageStats/images/bf2-1.png create mode 100644 battlefield/imageStats/images/bf3-1.png create mode 100644 battlefield/imageStats/images/bf4-1.png create mode 100644 battlefield/imageStats/images/bfh-1.png create mode 100644 battlefield/imageStats/images/bfv-1.png create mode 100644 battlefield/imageStats/main.py create mode 100644 battlefield/imageStats/platforms/pc.png create mode 100644 battlefield/imageStats/platforms/psn.png create mode 100644 battlefield/imageStats/platforms/xbox.png create mode 100644 battlefield/mongo.py create mode 100644 requirements.txt create mode 100644 task-definition.json diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..cbf6357 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,51 @@ +name: Docker + +on: + pull_request: + # Publish `main` as Docker `latest` image. + branches: + - master + + # Publish `v1.2.3` tags as releases. + tags: + - v* + + # Run tests for any PRs. + push: + +env: + # Image name + IMAGE_NAME: gamestats-telegram + +jobs: + # Push image to GitHub Packages. + # See also https://docs.docker.com/docker-hub/builds/ + push: + + runs-on: ubuntu-latest + if: github.event_name == 'push' + + steps: + - uses: actions/checkout@v2 + + - name: Build image + run: docker build . --file Dockerfile --tag $IMAGE_NAME + + - name: Log into registry + run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login docker.pkg.github.com -u ${{ github.actor }} --password-stdin + + - name: Push image + run: | + IMAGE_ID=docker.pkg.github.com/${{ github.repository }}/$IMAGE_NAME + # Change all uppercase to lowercase + IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]') + # Strip git ref prefix from version + VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,') + # Strip "v" prefix from tag name + [[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//') + # Use Docker `latest` tag convention + [ "$VERSION" == "main" ] && VERSION=latest + echo IMAGE_ID=$IMAGE_ID + echo VERSION=$VERSION + docker tag $IMAGE_NAME $IMAGE_ID:$VERSION + docker push $IMAGE_ID:$VERSION diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..b30fe40 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,83 @@ +# This workflow will build and push a new container image to Amazon ECR, +# and then will deploy a new task definition to Amazon ECS, when a release is created +# +# To use this workflow, you will need to complete the following set-up steps: +# +# 1. Create an ECR repository to store your images. +# For example: `aws ecr create-repository --repository-name my-ecr-repo --region us-east-2`. +# Replace the value of `ECR_REPOSITORY` in the workflow below with your repository's name. +# Replace the value of `aws-region` in the workflow below with your repository's region. +# +# 2. Create an ECS task definition, an ECS cluster, and an ECS service. +# For example, follow the Getting Started guide on the ECS console: +# https://us-east-2.console.aws.amazon.com/ecs/home?region=us-east-2#/firstRun +# Replace the values for `service` and `cluster` in the workflow below with your service and cluster names. +# +# 3. Store your ECS task definition as a JSON file in your repository. +# The format should follow the output of `aws ecs register-task-definition --generate-cli-skeleton`. +# Replace the value of `task-definition` in the workflow below with your JSON file's name. +# Replace the value of `container-name` in the workflow below with the name of the container +# in the `containerDefinitions` section of the task definition. +# +# 4. Store an IAM user access key in GitHub Actions secrets named `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`. +# See the documentation for each action used below for the recommended IAM policies for this IAM user, +# and best practices on handling the access key credentials. + +on: + # Triggers the workflow on push or pull request events but only for the main branch + push: + branches: [ main ] + workflow_dispatch: + +name: Deploy to Amazon ECS + +jobs: + deploy: + name: Deploy + runs-on: ubuntu-latest + environment: production + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: eu-central-1 + + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v1 + + - name: Build, tag, and push image to Amazon ECR + id: build-image + env: + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + ECR_REPOSITORY: gamestats-telegram + IMAGE_TAG: ${{ github.sha }} + run: | + # Build a docker container and + # push it to ECR so that it can + # be deployed to ECS. + docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG . + docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG + echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" + + - name: Fill in the new image ID in the Amazon ECS task definition + id: task-def + uses: aws-actions/amazon-ecs-render-task-definition@v1 + with: + task-definition: task-definition.json + container-name: gamestats-telegram + image: ${{ steps.build-image.outputs.image }} + + - name: Deploy Amazon ECS task definition + uses: aws-actions/amazon-ecs-deploy-task-definition@v1 + with: + task-definition: ${{ steps.task-def.outputs.task-definition }} + service: gamestats-telegram + cluster: gametools + wait-for-service-stability: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ed8ebf5 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..049398b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +FROM python:3 + +WORKDIR /usr/src/app + +ENV token 2023757880:AAEJxQ4483PnCDHPmrKgh7w1C4HBI4mP5Ng +ENV redishost redis-availability.1e8uve.ng.0001.euc1.cache.amazonaws.com +ENV redisport 6379 +ENV redispass '' + +COPY ./requirements.txt requirements.txt +RUN pip install --no-cache-dir -r requirements.txt + +# https://github.com/goverfl0w/discord-interactions/ +COPY . . + +CMD python ./app.py diff --git a/app.py b/app.py new file mode 100644 index 0000000..bafca58 --- /dev/null +++ b/app.py @@ -0,0 +1,61 @@ +# https://docs.aiogram.dev/en/latest/ +import re +import os +import asyncio +from battlefield.api.api_requests import onReadyCheck +import logging +from aiogram import Bot, Dispatcher, executor, types +from aiogram.contrib.fsm_storage.memory import MemoryStorage +from aiogram.dispatcher.filters.state import State, StatesGroup + +API_TOKEN = os.environ["token"] + +# Configure logging +logging.basicConfig(level=logging.INFO) + + +class StatState(StatesGroup): + bf2 = State() + bf3 = State() + bfh = State() + bf4 = State() + bf1 = State() + bf5 = State() + + bf2weapongraph = State() + bf3weapongraph = State() + bfhweapongraph = State() + bf4weapongraph = State() + bf1weapongraph = State() + bf5weapongraph = State() + + bf2vehiclegraph = State() + bf3vehiclegraph = State() + bfhvehiclegraph = State() + bf4vehiclegraph = State() + bf1vehiclegraph = State() + bf5vehiclegraph = State() + + +# Initialize bot and dispatcher +bot = Bot(token=API_TOKEN) + +storage = MemoryStorage() +dp = Dispatcher(bot, storage=storage) + + +@dp.message_handler(commands=["start", "help"]) +async def send_welcome(message: types.Message): + """ + This handler will be called when user sends `/start` or `/help` command + """ + await message.reply( + "This is the first initial version of the gamestats bot for telegram\nWe will start by adding the main commands:\n/bf1stats, /bf5stats, /bf4stats etc..." + ) + + +from battlefield.handlers import * + +if __name__ == "__main__": + asyncio.ensure_future(onReadyCheck()) + executor.start_polling(dp, skip_updates=True) diff --git a/battlefield/api/__init__.py b/battlefield/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/battlefield/api/api_requests.py b/battlefield/api/api_requests.py new file mode 100644 index 0000000..38e27c6 --- /dev/null +++ b/battlefield/api/api_requests.py @@ -0,0 +1,423 @@ +import pymongo + +from ..mongo import SingletonClient +from . import constants +import json +import re +import uuid +from time import time +import urllib.parse +import sys +import os +import datetime + +from tabulate import tabulate + +"""be able to do api's""" +import aiohttp +from ..api import authSession +from ..api.loginProcess import requestCookie +from .searchPlayer import checkMe, searchName, originID, platformSelect, getAccessToken +from influxdb_client.client.write_api import SYNCHRONOUS + +"""make the command sleep while others run""" +import asyncio + +regions = {} + +BLREGIONS = {1: "NAm", 2: "SAm", 4: "AU", 8: "Africa", 16: "EU", 32: "Asia", 64:"OC"} + +async def ingame_request_headers(code): + return { + "X-GatewaySession": f"{code}", + "X-ClientVersion": "release-bf1-lsu35_26385_ad7bf56a_tunguska_all_prod", + "X-DbId": "Tunguska.Shipping2PC.Win32", + "X-CodeCL": "3779779", + "X-DataCL": "3779779", + "X-SaveGameVersion": "26", + "X-HostingGameId": "tunguska", + "X-Sparta-Info": "tenancyRootEnv=unknown; tenancyBlazeEnv=unknown", + } + +async def onReadyCheck(): + """this script will run every 30 minutes""" + constants.COOKIE["sid"], constants.COOKIE["remid"], access_code = await requestCookie("api3@gametools.network", "FY5d9uEsv4Js9pQjygyDpzvRFgQUaEHE") + while True: + try: + await getAccessToken() + except Exception as e: + print(f"on_ready: {e}") + await asyncio.sleep(1800) + +async def otherNameApi(message): + db = SingletonClient.get_data_base() + playersInfo = await db.playerList.find_insensitive_more({"usedNames": message}) + results = "" + + if len(playersInfo) != 0: + for player in playersInfo: + otherNames = "" + for name in player["usedNames"]: + otherNames += f"{name}\n" + results += f"{player['playerName']}'s names: \n {otherNames}" + results += f"Last seen in server: \n{player['lastServer']}" + return {"title": "Manager", "description": f"{results}"} + else: + return {"title": "Manager", "description": "player not found"} + +async def bfBanApi(message, strUserId): + """used for checking if he's on bfban""" + async with aiohttp.ClientSession() as session: + if strUserId is None: + return {"color":0xe74c3c, "title":"BFBan", "description":"Not a valid origin username\n[BFBan.com](https://bfban.com/#/)"} # user not found + else: + try: + url = f"https://bfban.gametools.network/api/cheaters/{strUserId}" + async with session.get(url) as r: + userInfo = await r.json() + for i in range(len(userInfo["data"]["cheater"])): + if strUserId == userInfo["data"]["cheater"][i]["originUserId"]: + if userInfo["data"]["cheater"][i]["status"] == "1": + otherNames = "" + for i in range(len(userInfo["data"]["origins"])): + otherNames += f'{userInfo["data"]["origins"][i]["cheaterGameName"]}\n' + if otherNames == "": + otherNames = "[BFBan.com](https://bfban.com/#/)" + else: + otherNames += f"[BFBan.com](https://bfban.com/#/cheaters/{strUserId})" + return {"color":0xFFA500, "title":f"BFBan", "description":f"{message} found in BFBan, all usernames he used according to bfban:\n {otherNames}"} + return {"color":0xe74c3c, "title":"BFBan", "description":f"{message} found in BFBan, but not labeled as cheater\n[BFBan.com](https://bfban.com/#/cheaters/{strUserId})"} + return {"color":0xe74c3c, "title":"BFBan", "description":f"{message} isn't in BFBan\n[BFBan.com](https://bfban.com/#/)"} + except: + return {"color":0xe74c3c, "title":"BFBan", "description":"BFBan is down\n[BFBan.com](https://bfban.com/#/)"} # api down + + +async def getEA(playerId, method, game, code, session): + if game == "casablanca": + url = "https://sparta-gw-bfv.battlelog.com/jsonrpc/pc/api" + else: + url = "https://sparta-gw.battlelog.com/jsonrpc/web/api" + ids = uuid.uuid1() + headers = { + "content-type": "application/json", + "X-GatewaySession": f"{code}", + "X-ClientVersion": "companion-4569f32f" + } + payload = { + "jsonrpc":"2.0", + "method":f"{method}", + "params":{"game":f"{game}", "personaId":f"{playerId}"}, + "id":f"{ids}" + } + async with session.post(url, data=json.dumps(payload), headers=headers) as r: + return await r.json() + +async def getBothWeVeFromEa(message, game): + async with aiohttp.ClientSession() as session: + strpersonaId = await searchName(None, message, session, game) + tasks = getEA(strpersonaId['personaId'], "Progression.getWeaponsByPersonaId", game, authSession.code[f'{game}:pc'], session), \ + getEA(strpersonaId['personaId'], "Progression.getVehiclesByPersonaId", game, authSession.code[f'{game}:pc'], session) + statsWeapon, statsVehicle = await asyncio.gather(*tasks) + + statsWeapon["pid"] = strpersonaId['personaId'] + statsWeapon["avatar"] = strpersonaId["avatar"] + statsWeapon["personaName"] = strpersonaId["personaName"] + + return statsWeapon, statsVehicle + + + +async def getMultipleFromEa(message, firstMethod, game): + async with aiohttp.ClientSession() as session: + strpersonaId = await searchName(None, message, session, game) + tasks = getEA(strpersonaId['personaId'], firstMethod, game, authSession.code[f'{game}:pc'], session), \ + getEA(strpersonaId['personaId'], "Stats.detailedStatsByPersonaId", game, authSession.code[f'{game}:pc'], session), \ + getEA(strpersonaId['personaId'], "Platoons.getActivePlatoon", game, authSession.code[f'{game}:pc'], session) + stats, detailed_stats, activePlatoon = await asyncio.gather(*tasks) + + stats["pid"] = strpersonaId['personaId'] + stats["avatar"] = strpersonaId["avatar"] + stats["personaName"] = strpersonaId["personaName"] + + stats["detailed"] = detailed_stats + stats["activePlatoon"] = activePlatoon + return stats + + +async def getDataFromEa(message, method, game): + """get requested method and detailed stats in ['detailed'], personaId == ['personaId']""" + async with aiohttp.ClientSession() as session: + if game == "casablanca": + url = "https://sparta-gw-bfv.battlelog.com/jsonrpc/pc/api" + elif game == "bf4": + url = "https://sparta-gw.battlelog.com/jsonrpc/web/api" + else: + url = "https://sparta-gw.battlelog.com/jsonrpc/web/api" + strpersonaId = await searchName(None, message, session, game) + # request based on method + ids = uuid.uuid1() + headers = { + "content-type": "application/json", + "X-GatewaySession": f"{authSession.code[f'{game}:pc']}", + "X-ClientVersion": "companion-4569f32f" + } + payload = { + "jsonrpc":"2.0", + "method":f"{method}", + "params":{"game":f"{game}", "personaId":f"{strpersonaId['personaId']}"}, + "id":f"{ids}" + } + async with session.post(url, data=json.dumps(payload), headers=headers) as r: + stats = await r.json() + stats["pid"] = strpersonaId['personaId'] + if strpersonaId["battlelog"]: + stats["platformid"] = strpersonaId['platformid'] + if strpersonaId["platformName"] == "cem_ea_id": + stats["platformName"] = "pc" + else: + stats["platformName"] = strpersonaId["platformName"] + else: + stats["platformName"] = "pc" + stats["personaId"] = strpersonaId['personaId'] + stats["avatar"] = strpersonaId["avatar"] + stats["personaName"] = strpersonaId["personaName"] + return stats + +async def getServerDataFromEa(game, method, params, platform: str = "pc"): + """get details about a server ingame based on given method""" + async with aiohttp.ClientSession() as session: + code = await authSession.checkGatewaySession(game, session, platform=platform) + ids = uuid.uuid1() + headers = await ingame_request_headers(code[f'{game}:{platform}']) + payload = { + "jsonrpc":"2.0", + "method": method, + "params": params, + "id":f"{ids}" + } + url = f"https://sparta-gw.battlelog.com/jsonrpc/{platform}/api" + if game == "casablanca": + url = f"https://sparta-gw-bfv.battlelog.com/jsonrpc/{platform}/api" + async with session.post(url, data=json.dumps(payload), headers=headers) as r: + items = await r.json() + return items + +async def getServerPrefixes(serverName): + """for some special servers, change prefix""" + bobPrefix = serverName[0:9] + if "AMG" in bobPrefix: + serverDetails = serverName.split("-") + prefix = f'[AMG]#{int(list(filter(str.isdigit, serverName))[0])}' + elif "epiX" in bobPrefix: + epixPrefix = serverName[0:8].replace(" ","") + serverDetails = serverName.split("-") + prefix = epixPrefix + " " + serverDetails[1].strip() + elif "Community" in bobPrefix: + serverNumber = re.search(r'\d+', serverName).group() + prefix = f"Operations #{serverNumber}" + else: + prefix = serverName[0:30] + return prefix + +async def getBattlelogServersBasedOnName(message, game): + """gets list of servers for bf3, bf4 and bfh""" + async with aiohttp.ClientSession() as session: + headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:6.0.2) Gecko/20100101 Firefox/6.0.2', + 'Accept': '*/*', + 'Accept-Language': 'en-us,en;q=0.5', + 'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.7', + 'X-Requested-With': 'XMLHttpRequest', + 'X-AjaxNavigation': '1' + } + url = f"https://battlelog.battlefield.com/{game}/servers/pc/?q={urllib.parse.quote(message)}" + async with session.get(url, headers=headers) as r: + items = await r.json() + return items + +async def getBf2DataFromEa(message): + """gets stats for bf2 players""" + # if message.lower() in ["me"]: + # message = await checkMe(self, ctx) + async with aiohttp.ClientSession() as session: + newmessage = re.escape(message).replace("_","\_") + url = f'http://bf2web.bf2hub.com/ASP/searchforplayers.aspx?nick={newmessage}' + async with session.get(url) as r: + webData = await r.text() + statsData = str(webData).split("\n") + cols = statsData[3].split("\t") + data = statsData[4].split("\t") + response = dict(zip(cols, data)) + pid = str(response["pid"]) + info = 'per*,cmb*,twsc,cpcp,cacp,dfcp,kila,heal,rviv,rsup,rpar,tgte,dkas,dsab,cdsc,rank,cmsc,kick,kill,deth,suic,ospm,klpm,klpr,dtpr,bksk,wdsk,bbrs,tcdr,ban,dtpm,lbtl,osaa,vrk,tsql,tsqm,tlwf,mvks,vmks,mvn*,vmr*,fkit,fmap,fveh,fwea,wtm-,wkl-,wdt-,wac-,wkd-,vtm-,vkl-,vdt-,vkd-,vkr-,atm-,awn-,alo-,abr-,ktm-,kkl-,kdt-,kkd-' + headers = {'User-agent': 'GameSpyHTTP/1.0'} + url = f'http://bf2web.bf2hub.com/ASP/getplayerinfo.aspx?pid={pid}&info={info}&nocache={round(time())}' + async with session.get(url, headers=headers) as r: + response = await r.text() + statsData = str(response).split("\n") + cols = statsData[3].split("\t") + data = statsData[4].split("\t") + stats = dict(zip(cols, data)) + return stats + +async def getDataFromBattlelog(method, message, game): + """get stats for bf3, bf4 and bfh players""" + async with aiohttp.ClientSession() as session: + platformData = await platformSelect(message) + strpersonaId = await searchName(None, platformData["new_message"], session, game, platformData["current"]) + if method == "overviewPopulateStats": + url = f"http://battlelog.battlefield.com/{game}/overviewPopulateStats/{strpersonaId['personaId']}/None/{strpersonaId['platformid']}/" + else: + url = f"http://battlelog.battlefield.com/{game}/{method}/{strpersonaId['personaId']}/{strpersonaId['platformid']}/" + headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:6.0.2) Gecko/20100101 Firefox/6.0.2', + 'Accept': '*/*', + 'Accept-Language': 'en-us,en;q=0.5', + 'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.7', + 'X-Requested-With': 'XMLHttpRequest', + 'X-AjaxNavigation': '1'} + async with session.get(url, headers=headers) as r: + stats = await r.json() + stats["platformName"] = platformData['current'] + stats["platformid"] = strpersonaId['platformid'] + stats["personaId"] = stats["data"]["personaId"] + stats["avatar"] = strpersonaId["avatar"] + stats["personaName"] = strpersonaId["personaName"] + return stats + +async def doServerChanges(game, request, params, cookie): + async with aiohttp.ClientSession() as session: + code = await authSession.checkGatewaySession(game, session, cookie) + ids = uuid.uuid1() + url = "https://sparta-gw.battlelog.com/jsonrpc/web/api" + headers = await ingame_request_headers(code) + payload = { + "jsonrpc": "2.0", + "method": f"{request}", + "params": params, + "id": f"{ids}" + } + async with session.post(url, data=json.dumps(payload), headers=headers) as r: + items = await r.json() + return items + +async def checkBanApi(message): + async with aiohttp.ClientSession() as session: + db = SingletonClient.get_data_base() + userId = await originID(session, message) + if userId["totalCount"] == 0: + return {"color":0xe74c3c, "title":"", "description":"Not a valid origin username", "strUserId": None, "playerId": None} # user not found + else: + strUserId = userId["userId"] + playerId = userId["strpersonaId"] + playerInfo = await db.globalIngameBans.find_one({'_id': str(playerId)}) + if playerInfo is not None: + servers = "" + for item in playerInfo["servers"]: + servers += f'{item}\n' + return {"color":0xFFA500, "title": f"{playerInfo['displayName']} is banned in:", "description":servers, "strUserId": strUserId, "playerId": playerId} + else: + return {"color":0xe74c3c, "title": "", "description":f"{message} isn't in global banlist of manager", "strUserId": strUserId, "playerId": playerId} + +async def getMostRecentSession(ctx, playerName): + db = SingletonClient.get_data_base() + async with aiohttp.ClientSession() as session: + strpersonaId = await searchName(None, playerName, session, "tunguska") + recentStats = [] + i = 1 + async for document in db.playerDiffStats.find({'playerId': int(strpersonaId['personaId'])}, sort=[( 'timeStamp', pymongo.DESCENDING )]): + recentStats.append(document) + if i == 5: + break + i+=1 + # embed=discord.Embed(color=0xFFA500, title=f"{strpersonaId['personaName']}\'s most recent playsessions") + + # add current stats when the player is still ingame + try: + join = await db.playerJoinStats.fine_one({'playerId': int(strpersonaId['personaId'])}, sort=[( 'timeStamp', pymongo.DESCENDING )]) + mostRecentPlayerupdate = await db.playerList.find_one({'_id': int(strpersonaId['personaId'])}) + if len(recentStats) != 0: + runStats = recentStats[0]["timeStamp"] < mostRecentPlayerupdate["timeStamp"] and join["timeStamp"] < mostRecentPlayerupdate["timeStamp"] + elif join is None or mostRecentPlayerupdate is None: + runStats = False + else: + runStats = join["timeStamp"] < mostRecentPlayerupdate["timeStamp"] + joinStats = join["stats"] + if runStats: + detailedStats = (await getDataFromEa(ctx, playerName, "Stats.detailedStatsByPersonaId", "tunguska")) + statsList = [{ + "Wins": int(detailedStats["result"]["basicStats"]["wins"] - joinStats["wins"]), + "Losses": int(detailedStats["result"]["basicStats"]["losses"] - joinStats["losses"]), + "Kills": int(detailedStats["result"]["basicStats"]["kills"] - joinStats["kills"]), + "Deaths": int(detailedStats["result"]["basicStats"]["deaths"] - joinStats["deaths"]), + "Time played": int(detailedStats["result"]["basicStats"]["timePlayed"] - joinStats["timePlayed"]), + }] + + kits = {} + for kit in joinStats["kits"]: + kits[kit["name"]] = kit + for newKit in detailedStats["result"]["kitStats"]: + kits[newKit["name"]]["score"] = int(newKit["score"] - kits[newKit["name"]]["score"]) + kits[newKit["name"]]["kills"] = int(newKit["kills"] - kits[newKit["name"]]["kills"]) + kits[newKit["name"]]["timePlayed"] = datetime.timedelta(seconds=newKit["secondsAs"] - kits[newKit["name"]]["timePlayed"]) + kits = list(kits.values()) + + gamemodes = {} + for gamemode in joinStats["gamemodes"]: + gamemodes[gamemode["name"]] = gamemode + for newGamemode in detailedStats["result"]["gameModeStats"]: + gamemodes[newGamemode["name"]]["score"] = int(newGamemode["score"] - gamemodes[newGamemode["name"]]["score"]) + gamemodes[newGamemode["name"]]["wins"] = int(newGamemode["wins"] - gamemodes[newGamemode["name"]]["wins"]) + gamemodes[newGamemode["name"]]["losses"] = int(newGamemode["losses"] - gamemodes[newGamemode["name"]]["losses"]) + gamemodes = list(gamemodes.values()) + + gamemodeList = [constants.GAMEMODES[d["name"]] for d in gamemodes if d['score'] != 0] + + kitList = [d.values() for d in kits if d['timePlayed'] != datetime.timedelta(seconds=0)] + if statsList[0]["Time played"] != 0: + statsStr = f""" + ```{tabulate(statsList, headers="keys")} + \n{tabulate(kitList, ["Classes", "Score", "Kills", "Time played as"])} + \n(servername not shown for privacy) - {"/".join(gamemodeList)}``` + """ + embed.add_field(name="current KD ingame", value=statsStr, inline=False) + except Exception as e: + print(e) + pass + i = 1 + for info in recentStats: + stats = info["stats"] + serverInfo = await db.communityPlayers.find_one({"_id": info["serverId"]}) + for item in stats["kits"]: + item["timePlayed"] = datetime.timedelta(seconds=item["timePlayed"]) + kitList = [d.values() for d in stats["kits"] if d['timePlayed'] != datetime.timedelta(seconds=0)] + gamemodeList = [constants.GAMEMODES[d["name"]] for d in stats["gamemodes"] if d['score'] != 0] + + statsList = [{ + "Wins": stats['wins'], + "Losses": stats['losses'], + "Kills": stats['kills'], + "Deaths": stats['deaths'], + "Time played": datetime.timedelta(seconds=stats['timePlayed']) + }] + statsStr = f""" + ```{tabulate(statsList, headers="keys")} + \n{tabulate(kitList, ["Classes", "Score", "Kills", "Time played as"])} + \n({info["timeStamp"].strftime("%d-%m-%Y - %H:%M")}) - {"/".join(gamemodeList)}``` + """ + embed.add_field(name=serverInfo["info"]["prefix"], value=statsStr, inline=False) + i+=1 + if len(recentStats) == 0 and runStats == False: + embed.add_field(name="Play more rounds before trying again", value="It only tracks the session within this week", inline=False) + embed.set_footer(text="(Only works on servers using our manager)") + await ctx.send(embed=embed) + +async def platoonCheck(playerName): + try: + stats = await getDataFromEa(playerName, "Platoons.getActivePlatoon", "tunguska") + if stats.get("result"): + return f'[{stats["result"]["name"]}](https://gametools.network/platoons/{stats["result"]["guid"]})' + else: + return "player isn't in any platoon" + except Exception as e: + return "player not found" \ No newline at end of file diff --git a/battlefield/api/authSession.py b/battlefield/api/authSession.py new file mode 100644 index 0000000..c3db1fe --- /dev/null +++ b/battlefield/api/authSession.py @@ -0,0 +1,88 @@ +import uuid +import urllib +import json + +from . import constants + +code = {} + +async def checkGatewaySession(game, session, cookie: str = None, platform: str = "pc"): + """check if X-GatewaySession is valid for bf5""" + custom = False + ids = uuid.uuid1() + global code + if game == "casablanca": + gameUrl = f"https://sparta-gw-bfv.battlelog.com/jsonrpc/{platform}/api" + else: + gameUrl = f"https://sparta-gw.battlelog.com/jsonrpc/{platform}/api" + payload = { + "jsonrpc":"2.0", + "method":"Stats.detailedStatsByPersonaId", + "params":{"personaId": "794397421", "game": game}, + "id":f"{ids}" + } + # check if still valid + if code.get(f'{game}:{platform}', None) is not None and cookie is None: + headers = { + "content-type": "application/json", + "X-GatewaySession": f"{code[f'{game}:{platform}']}", + "X-ClientVersion": "companion-4569f32f" + } + async with session.post(url=gameUrl, headers=headers, data=json.dumps(payload)) as r: + result = await r.json() + else: + result = "error" + if cookie is not None: + custom = True + else: + # set cookie after cookie check + cookie = constants.COOKIE + if "error" in result: + # get new one if fails + if platform == "xboxone": + authPlatform = "xone" + else: + authPlatform = platform + url = f"https://accounts.ea.com/connect/auth?client_id=sparta-backend-as-user-{authPlatform}&response_type=code&release_type=none" + headers = { + "User-Agent": "Mozilla/5.0 EA Download Manager Origin/10.5.89.45622", + "X-Origin-Platform": "PCWIN", + "Cookie": f"sid={constants.COOKIE['sid']}; remid={constants.COOKIE['remid']}; _ga=GA1.2.1989678628.1594206869; _nx_mpcid=c002360c-1ab0-4ce2-83d8-9a850d649e13; ealocale=nl-nl; ak_bmsc=E1AB16946A53CE2E1495B6BBF258CB7758DDA1073361000061E5405FB2BC7035~pl1h1xXQwIfdbF9zU3hMOujtpJ70BXJaFRqFWYPsE32q4qwE4onR+N6sn+T0KrqdpyZaf2ir3u+Xh68W3cEjOb+VLrQvI0jwTr1GFp5bgZXU812telOSjDjYPK3ddYJ7kOfbHtEglOs9UhdzOFi+UIWIAFdwcNq5ZoPv9Z0i53SspWuI4W3POlFqX70LxwwXcLMTQZgnGVs8R98goScJjo0Oo4D/+8LLzJsJfIUpr4AcgSC5B/ks2m5yz8ft9nh+9w; bm_sv=0B8CB6864011B573F81CA53CE1A570AD~qrTfU4mNBNCod1jfAFpeU+tG5OJ7w4r7lIg/ezmX6RLIIemuAT6EnFMcuhkUGHhJQSwjeV1kpadZtdXJAlbDoPxAafNmbaeQC/hteaiVlTMVV7R6gKPT8kWM22cFv6Y3Axp/S5XFhd6/Aw4cH6+Bng==; notice_behavior=implied,eu; _gid=GA1.2.827595775.1598088932; _gat=1", + "Host": "accounts.ea.com" + } + async with session.get(url=url, headers=headers, allow_redirects=False) as r: + redirect = r.headers["Location"] + access_code = urllib.parse.urlparse(redirect).query.split("=")[1] + headers = { + "User-Agent": "ProtoHttp 1.3/DS 15.1.2.1.0 (Windows)", + "X-Guest": "no-session-id", + "X-ClientVersion": "release-bf1-lsu35_26385_ad7bf56a_tunguska_all_prod", + "X-DbId": "Tunguska.Shipping2PC.Win32", + "X-CodeCL": "3779779", + "X-DataCL": "3779779", + "X-SaveGameVersion": "26", + "X-HostingGameId": "tunguska", + "X-Sparta-Info": "tenancyRootEnv=unknown; tenancyBlazeEnv=unknown", + } + payload = { + "jsonrpc": "2.0", + "method": "Authentication.getEnvIdViaAuthCode", + "params": { + "authCode": f"{access_code}", + "product": None, + "dtab": None, + "locale": "en-us", + "branchName": "Tunguska", + "changeListNumber": "3779779", + "minutesToUTC": "60" + }, + "id": f"{ids}" + } + async with session.post(url=gameUrl, headers=headers, data=json.dumps(payload)) as r: + result = await r.json() + if custom: + return result["result"]["sessionId"] + code[f'{game}:{platform}'] = result["result"]["sessionId"] + return code + else: + return code \ No newline at end of file diff --git a/battlefield/api/constants.py b/battlefield/api/constants.py new file mode 100644 index 0000000..c3ca501 --- /dev/null +++ b/battlefield/api/constants.py @@ -0,0 +1,81 @@ +COOKIE = { + "sid": "", + "remid": "" +} + +SPARTA_GAMES = ["tunguska", "casablanca"] +SPARTA_REGIONS = ["EU", "Asia", "NAm", "SAm", "AU", "OC"] +SPARTA_TITLES = ["pc", "xbox", "psn"] +ALL_BF_GAMES = ["bf2", "bf3", "bf4", "bfh", "bf1", "bf5"] +BATTLELOG_GAMES = ["bf3", "bf4", "bfh"] +BATTLELOG_REGIONS = {1: "NAm", 2: "SAm", 4: "AU", 8: "Africa", 16: "EU", 32: "Asia", 64: "OC"} +BATTLELOG_PLATFORM_NUMBERS = {'cem_ea_id': 1, 'xbox360': 2, 'ps3': 4, 'xbox': 8, 'ps2': 16, 'ps4': 32, 'xboxone': 64} +BATTLELOG_GAME_NUMBERS = {'bfbc2': 1, 'bf3': 2, 'bf1942': 4, 'bf1943': 8, 'bfvietnam': 16, 'bf2': 32, 'bf2142': 64, + 'bfbc': 128, 'bfheroes': 256, 'bfmc': 512, 'bfp4f': 1024, 'bf4': 2048, 'mohw': 4096, 'bfh': 8192} +BATTLELOG_BF4_KIT = {1: "Assault", 2: "Engineer", 32: "Support", 8: "Recon", 2048: "Commander"} +BATTLELOG_BFH_KIT = {2048: "Commander", 4096: "Enforcer", 8192: "Mechanic", 16384: "Operator", 32768: "Professional"} +BATTLELOG_BF3_KIT = {1: "Assault", 2: "Engineer", 8: "Recon", 32: "Support"} +FACTIONS = { + "BFFactionId_FactionAHU": { + "image": "https://eaassets-a.akamaihd.net/battlelog/battlebinary/gamedata/Tunguska/30/21/FactionsColorAustroHungarianEmpireLarge-e2ebe691.png", + "key": "AHU", + "name": "Austro-Hungarian Empire", + }, + "BFFactionId_FactionGER": { + "image": "https://eaassets-a.akamaihd.net/battlelog/battlebinary/gamedata/Tunguska/117/41/FactionsColorGermanEmpireLarge-8bd7e888.png", + "key": "GER", + "name": "German Empire", + }, + "BFFactionId_FactionOTM": { + "image": "https://eaassets-a.akamaihd.net/battlelog/battlebinary/gamedata/Tunguska/69/73/FactionsColorOttomanEmpireLarge-45b70933.png", + "key": "OTM", + "name": "Ottoman Empire", + }, + "BFFactionId_FactionITA": { + "image": "https://eaassets-a.akamaihd.net/battlelog/battlebinary/gamedata/Tunguska/22/73/FactionsColorKingdomOfItalyLarge-eab7049b.png", + "key": "ITA", + "name": "Kingdom of Italy", + }, + "BFFactionId_FactionUK": { + "image": "https://eaassets-a.akamaihd.net/battlelog/battlebinary/gamedata/Tunguska/103/37/FactionsColorUnitedKingdomLarge-672527a9.png", + "key": "UK", + "name": "United Kingdom", + }, + "BFFactionId_FactionUSA": { + "image": "https://eaassets-a.akamaihd.net/battlelog/battlebinary/gamedata/Tunguska/10/110/FactionsColorUnitedStatesOfAmericaLarge-0a6e66a7.png", + "key": "USA", + "name": "United States of America", + }, + "BFFactionId_FactionFRA": { + "image": "https://eaassets-a.akamaihd.net/battlelog/battlebinary/gamedata/Tunguska/93/43/FactionsColorFranceLarge-a3d5b514.png", + "key": "FRA", + "name": "France", + }, + "BFFactionId_FactionRUS": { + "image": "https://eaassets-a.akamaihd.net/battlelog/battlebinary/gamedata/Tunguska/113/64/FactionsColorRussia-8f40526a.png", + "key": "RUS", + "name": "Russian Empire", + }, + "BFFactionId_FactionBOL": { + "image": "https://eaassets-a.akamaihd.net/battlelog/battlebinary/gamedata/Tunguska/63/106/FactionsLargeColorBolshevik-3f6ac72d.png", + "key": "BOL", + "name": "Red Army", + }, + "BFFactionId_FactionWA": { # BFFactionId_FactionRUS image + "image": "https://eaassets-a.akamaihd.net/battlelog/battlebinary/gamedata/Tunguska/113/64/FactionsColorRussia-8f40526a.png", + "key": "WA", + "name": "White Army", + }, + + + + "BFFactionId_FactionUKM": { # BFFactionId_FactionUK image + "image": "https://eaassets-a.akamaihd.net/battlelog/battlebinary/gamedata/Tunguska/103/37/FactionsColorUnitedKingdomLarge-672527a9.png", + "key": "UK", + "name": "Royal Marines", + }, +} +GAMEMODES = {"TugOfWar": "Frontlines", "Domination": "Domination", "Breakthrough": "Shock Operations", "Rush": "Rush", + "BreakthroughLarge": "Operations", "Conquest": "Conquest", "Possession": "War pigeons", "TeamDeathMatch": "Team Deathmatch", + "AirAssault": "Air assault", "ZoneControl": "Rush", "Airborne": "Airborne", "FinalStand": "Grand Operations", "Outpost": "Outpost", + "RushLarge": "Rush large", "Frontlines": "Frontlines", "FrontlinesMedium": "Frontlines Medium", "SquadConquest": "Squad Conquest"} \ No newline at end of file diff --git a/battlefield/api/loginProcess.py b/battlefield/api/loginProcess.py new file mode 100644 index 0000000..168f883 --- /dev/null +++ b/battlefield/api/loginProcess.py @@ -0,0 +1,70 @@ +import asyncio +import re +import aiohttp +import urllib.parse +import random +import string + +# main +async def requestCookie(email: str, passw: str): + async with aiohttp.ClientSession() as session: + fid = await requestFid(session) + jSessionId, signInCookie, location = await requestJsSessionID(session, fid) + secondFid = await requestSecondFid(session, jSessionId, signInCookie, location, email, passw) + return await requestCookieAuth(session, secondFid) + +async def requestFid(session: aiohttp.ClientSession): + data = await session.get("https://accounts.ea.com:443/connect/auth?display=originXWeb%2Flogin&response_type=code&release_type=prod&redirect_uri=https%3A%2F%2Fwww.origin.com%2Fviews%2Flogin.html&locale=zh_TW&client_id=ORIGIN_SPA_ID", allow_redirects=False) + redirect = data.headers["Location"] + return redirect.split("=")[1] + +async def requestJsSessionID(session: aiohttp.ClientSession, fid: str): + data = await session.get(f"https://signin.ea.com/p/originX/login?fid={fid}", allow_redirects=False) + location = data.headers["Location"] + i = 0 + jSessionId = "" + signInCookie = "" + for name, data in data.headers.items(): + if name == "Set-Cookie": + if i == 0: + jSessionId = re.search(r'=.+?;', data)[0].strip('=').strip(";") + i += 1 + else: + signInCookie = re.search(r'\".+\"', data)[0].strip('"') + return jSessionId, signInCookie, location + +async def random_char(y): + return ''.join(random.choice(string.ascii_letters) for x in range(y)) + +async def requestSecondFid(session: aiohttp.ClientSession, jSessionId: str, signInCookie: str, location: str, email: str, passw: str): + random = await random_char(5) + data = await session.post( + url = f"https://signin.ea.com{location}", + headers = { + "Content-Type": "application/x-www-form-urlencoded", + "Referer": f"https://signin.ea.com{location}", + "cookie": f"JSESSIONID={jSessionId};", + "cookie": f"signin-cookie={signInCookie}" + }, + data = f"email={urllib.parse.quote(email)}&password={passw}&_eventId=submit&cid={random*42}&showAgeUp=true&thirdPartyCaptchaResponse=&_rememberMe=on&rememberMe=on" + ) + html = await data.text() + htmlPart = re.search(r"window\.location(.+?)\n", html)[0] + redirect = re.search(r"\"(.+?)\"", htmlPart)[0] + return redirect.split("=")[-1] + +async def requestCookieAuth(session: aiohttp.ClientSession, secondFid: str): + data = await session.get(f"https://accounts.ea.com:443/connect/auth?display=originXWeb%2Flogin&response_type=code&release_type=prod&redirect_uri=https%3A%2F%2Fwww.origin.com%2Fviews%2Flogin.html&locale=zh_TW&client_id=ORIGIN_SPA_ID&fid={secondFid}", allow_redirects=False) + redirect = data.headers["Location"] + i = 0 + sid = "" + remid = "" + for name, data in data.headers.items(): + if name == "Set-Cookie": + if i == 0: + sid = re.search(r'=.+?;', data)[0].strip('=').strip(";") + i += 1 + else: + remid = re.search(r'=.+?;', data)[0].strip('=').strip(";") + access_code = urllib.parse.urlparse(redirect).query.split("=")[1] + return sid, remid, access_code \ No newline at end of file diff --git a/battlefield/api/redisDB.py b/battlefield/api/redisDB.py new file mode 100644 index 0000000..41fa954 --- /dev/null +++ b/battlefield/api/redisDB.py @@ -0,0 +1,39 @@ +import sys +from datetime import timedelta + +import redis +import os + +redishost = os.environ['redishost'] +redisport = os.environ['redisport'] +redispass = os.environ['redispass'] + +def redis_connect() -> redis.client.Redis: + try: + client = redis.RedisCluster( + host=redishost, + port=redisport, + password=redispass, + socket_timeout=5, + ) + ping = client.ping() + if ping is True: + return client + except redis.AuthenticationError: + print("AuthenticationError") + sys.exit(1) + +client = redis_connect() + +async def get_routes_from_cache(key: str) -> str: + """Data from redis.""" + + val = client.get(key) + return val + + +async def set_routes_to_cache(key: str, value: str, ttl: int = 600) -> bool: + """Data to redis, default == 10 minutes""" + + state = client.setex(key, timedelta(seconds=ttl), value=value) + return state diff --git a/battlefield/api/searchPlayer.py b/battlefield/api/searchPlayer.py new file mode 100644 index 0000000..4dd77ef --- /dev/null +++ b/battlefield/api/searchPlayer.py @@ -0,0 +1,311 @@ +# default bf1 api stuff +import aiohttp +import json +import re + +from pymongo.collation import Collation + +from ..api import constants +from .authSession import checkGatewaySession +from .redisDB import get_routes_from_cache, set_routes_to_cache +import requests +from ..mongo import SingletonClient + +access_token = "" + +async def getAccessToken(): + global access_token + headers = {"Cookie": f"sid={constants.COOKIE['sid']}; remid={constants.COOKIE['remid']};"} + x = requests.get("https://accounts.ea.com/connect/auth?client_id=ORIGIN_JS_SDK&response_type=token&redirect_uri=nucleus:rest&prompt=none&release_type=prod", headers=headers) + jsonaccess_token = x.json()["access_token"] + access_token = str(jsonaccess_token) + +async def bfTrackerPlatformSelect(message): + oldmessage = message.split() + current = "origin" + platforms = ["pc", "origin", "ps4", "psn", "xbox", "xbl", "one"] + for platform in platforms: + if platform in oldmessage: + if platform == "pc": + current = "origin" + elif platform == "ps4" or platform == "psn": + current = "psn" + elif platform == "xbox" or platform == "xbl" or platform == "one": + current = "xbl" + else: + current = platform + new_message = [string for string in oldmessage if string not in platforms] + # if new_message[0].lower() in ["me"]: + # new_message = [await checkMe(self, ctx)] + return {"current": current, "new_message": new_message} + +async def platformSelect(message): + """if platform is filled in, find platform. otherwise use default: origin (pc)""" + oldmessage = message.split() + current = "cem_ea_id" + platformid = 1 + platforms = ["pc", "origin", "ps4", "psn", "playstation", "ps", "ps3", "xboxone", "xbl", "one", "xbox", "360", "xbox360"] + for platform in platforms: + if platform in oldmessage: + if platform.lower() in ["pc", "origin"]: + current = "cem_ea_id" + platformid = 1 + elif platform.lower() in ["ps4", "psn", "playstation", "ps"]: + current = "ps4" + platformid = 32 + elif platform.lower() in ["ps3"]: + current = "ps3" + platformid = 4 + elif platform.lower() in ["xboxone", "xbl", "one", "xbox"]: + current = "xboxone" + platformid = 64 + elif platform.lower() in ["360", "xbox360"]: + current = "xbox360" + platformid = 2 + else: + current = platform + platformid = 1 + new_message = [string for string in oldmessage if string not in platforms] + + return {"current": current, "new_message": ' '.join(map(str, new_message)), "platformid": platformid} + +async def originID(session, message): + """get origin id""" + db = SingletonClient.get_data_base() + playerInfo = await db.playerList.find_one({'playerName': message}, collation=Collation(locale='en', strength=2, alternate="shifted")) + if playerInfo is not None: + if playerInfo["userId"] is not None: + return {"totalCount": 1, "userId": str(playerInfo["userId"]), "strpersonaId": str(playerInfo["_id"])} + + global access_token + url = f"https://api4.origin.com/xsearch/users?userId=2800753812&searchTerm={message}&start=0" + headers = {"authtoken": f"{access_token}"} + userId = {} + try: + async with session.get(url, headers=headers) as r: + userIdGet = await r.json() + userId["totalCount"] = userIdGet["totalCount"] + userId["userId"] = userIdGet["infoList"][0]["friendUserId"] + url = f"https://api2.origin.com/atom/users?userIds={userId['userId']}" + async with session.get(url, headers=headers) as r: + personaId = await r.text() + regexPersonaId = r'(?<=\).+?(?=\)' + userId["strpersonaId"] = re.findall(regexPersonaId, personaId)[0] + except: + playerInfo = await getPlayerFromProxy(message) + return {"totalCount": 1, "userId": str(playerInfo["originId"]), "strpersonaId": str(playerInfo["personaId"])} + return userId + +async def eaDesktopSearch(name): + async with aiohttp.ClientSession() as session: + db = SingletonClient.get_data_base() + accessToken = await db.globals.find_one({"_id": "deskopToken"}) + url = "https://service-aggregation-layer.juno.ea.com/graphql" + headers = { + "Host": "service-aggregation-layer.juno.ea.com", + "accept": "*/*", + "authorization": "Bearer " + accessToken["key"], + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) QtWebEngine/5.15.2 Chrome/83.0.4103.122 Safari/537.36", + "content-type": "application/json", "Origin": "https://pc.ea.com", "Referer": "https://pc.ea.com/zh-hans", + "Accept-Encoding": "gzip, deflate, br"} + + payload = {"operationName": "SearchPlayer", + "variables": {"searchText": name, "pageNumber": 1, "pageSize": 20, "locale": "zh-hans"}, + "extensions": {"persistedQuery": {"version": 1, + "sha256Hash": "83da6f3045ee524f6cb62a1c23eea908c9432f15e87b30dd33b89974ff83c657"}}, + "query": "query SearchPlayer($searchText: String!, $pageNumber: Int!, $pageSize: Int!) {\n players(searchText: $searchText, paging: {pageNumber: $pageNumber, pageSize: $pageSize}) {\n items {\n ...PlayerWithMutualFriendsCount\n __typename\n }\n __typename\n }\n}\n\nfragment PlayerWithMutualFriendsCount on Player {\n ...Player\n mutualFriends {\n totalCount\n __typename\n }\n __typename\n}\n\nfragment Player on Player {\n id: pd\n pd\n psd\n displayName\n uniqueName\n nickname\n avatar {\n ...Avatar\n __typename\n }\n relationship\n __typename\n}\n\nfragment Avatar on AvatarList {\n large {\n ...image\n __typename\n }\n medium {\n ...image\n __typename\n }\n small {\n ...image\n __typename\n }\n __typename\n}\n\nfragment image on Image {\n height\n width\n path\n __typename\n}\n"} + + async with session.post(url=url, headers=headers, data=json.dumps(payload)) as r: + result = await r.json() + player = result["data"]["players"]["items"][0] + return { + "personaId": player["psd"], + "personaName": player["displayName"], + "avatar": player["avatar"]["large"]["path"], + "battlelog": False + } + +async def searchNameBattlelog(player_name: str, game: str, platform: str): + async with aiohttp.ClientSession() as session: + # Get Battlelog platform id, defaulting to pc (1) + platform_id = constants.BATTLELOG_PLATFORM_NUMBERS.get(platform, 1) + # Get Battlelog game id (bfh: 8192, bf4: 2048, bf3: 2) + game_id = constants.BATTLELOG_GAME_NUMBERS.get(game) + payload = aiohttp.FormData() + payload.add_field("query", player_name) + url = "https://battlelog.battlefield.com/search/query/" + async with session.post(url, data=payload) as r: + results = await r.json() + if game not in ["tunguska", "casablanca"]: + for persona in results["data"]: + """ + Return persona which matches name exactly and owns current game on current platform, which is determined + based on Battlelog's enumeration flags using bitwise AND. + If a player owns only BF3, BF4 and BFH, on PC, their "games" attribute will be dict equal to: + {'1': '10242'} with '1' being the platform id (1 for pc) and 10242 being the sum of the game ids: + 2 (bf3) + 2048 (bf4) + 8192 (bfh). Using that, we can check if a player owns bf4 by checking if the game + id sum contains bf4's id like so: 10242 & 2048 (which is 2048) == 2048 + """ + if persona["personaName"].lower() == player_name.lower() and str(platform_id) in persona["games"] and \ + int(persona["games"][str(platform_id)]) & game_id == game_id: + return { + "personaId": persona["personaId"], + "personaName": persona["personaName"], + "platformid": platform_id, + "platformName": platform, + "avatar": f'https://secure.gravatar.com/avatar/{persona["user"]["gravatarMd5"]}?' + f's=204&d=https://eaassets-a.akamaihd.net/battlelog/defaultavatars/default-avatar-36.png', + "battlelog": True + } + else: + for persona in results["data"]: + if "cem_ea_id" in persona["namespace"] and persona["personaName"].lower() == player_name.lower(): + return { + "personaId": persona["personaId"], + "personaName": persona["personaName"], + "platformid": platform_id, + "platformName": platform, + "avatar": f'https://secure.gravatar.com/avatar/{persona["user"]["gravatarMd5"]}?' + f's=204&d=https://eaassets-a.akamaihd.net/battlelog/defaultavatars/default-avatar-36.png', + "battlelog": True + } + +async def getFromDb(message): + try: + db = SingletonClient.get_data_base() + playerInfo = await db.playerList.find_one({'playerName': message}, collation=Collation(locale='en', strength=2, alternate="shifted")) + if playerInfo is not None: + if playerInfo["userId"] is not None: + try: + async with aiohttp.ClientSession() as session: + global access_token + headers = {"authtoken": f"{access_token}"} + url = f"https://api3.origin.com/avatar/user/{playerInfo['userId']}/avatars?size=1" + async with session.get(url=url, headers={'Accept': 'application/json', **headers}) as r: + avatarJson = await r.json() + avatar = avatarJson['users'][0]['avatar']['link'] + except Exception as e: + print(f"getFromDb: {e}") + return None # if failed to get avatar + else: + return None + return { + "personaId": playerInfo["_id"], + "personaName": playerInfo["playerName"], + "platformid": 1, + "avatar": avatar, + "battlelog": False + } + except: + return None + +async def getPlayerFromProxy(message): + try: + async with aiohttp.ClientSession() as session: + global access_token + url = f"https://gateway.ea.com/proxy/identity/personas?displayName={message}&namespaceName=cem_ea_id" + headers = { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:91.0) Gecko/20100101 Firefox/91.0", + "Accept": "application/vnd.origin.v3+json; x-cache/force-write", + "Accept-Encoding": "gzip, deflate, br", + "X-ZuluTime-Format": "true", + "Origin": "https://www.origin.com", + "Referer": "https://www.origin.com/", + 'X-Expand-Results': 'true', + "Authorization": f"Bearer {access_token}", + } + async with session.get(url=url, headers=headers) as r: + playerInfo = await r.json() + user = playerInfo["personas"]["persona"][0] + try: + headers = {"authtoken": f"{access_token}"} + url = f"https://api3.origin.com/avatar/user/{user['pidId']}/avatars?size=1" + async with session.get(url=url, headers={'Accept': 'application/json', **headers}) as r: + avatarJson = await r.json() + return { + "personaName": user["displayName"], + "originId": user["pidId"], + "personaId": user["personaId"], + "avatar": avatarJson['users'][0]['avatar']['link'], + "battlelog": False, + "platformid": 1 + } + except Exception as e: + print(f"getAvatarSeachPlayer: {e}") + return { + "personaName": user["displayName"], + "originId": user["pidId"], + "personaId": user["personaId"], + "avatar": "https://eaassets-a.akamaihd.net/battlelog/defaultavatars/default-avatar-36.png", + "battlelog": False, + "platformid": 1 + } + except: + return None + +async def searchName(originId, name, session, game, platform="pc"): + # if name.lower() in ["me"]: + # name = await checkMe(self, ctx) + data = await get_routes_from_cache(key=f'names:{originId}:{name}:{game}:{platform}') + await checkGatewaySession(game, session) + if data is not None: + strpersonaId = json.loads(data) + return strpersonaId + else: + global access_token + # If game is a sparta game (BF1, BFV), try searching via Origin API + userId = {} + headers = {"authtoken": f"{access_token}"} + if platform == "pc" and originId is None: + try: + url = f"https://api1.origin.com/xsearch/users?userId=2800753812&searchTerm={name}&start=0" + async with session.get(url, headers=headers) as r: + userId = await r.json() + except: + pass + # If player was not found (or no search was attempted because game is not a sparta game), search via Battlelog + if userId.get("totalCount", 0) == 0 and originId is None: + try: + strpersonaId = await searchNameBattlelog(name, game, platform) + except Exception as e: + strpersonaId = None + if strpersonaId is None and game == "tunguska" and platform == "pc": + strpersonaId = await getFromDb(name) + if strpersonaId is None and game in constants.SPARTA_GAMES: + strpersonaId = await getPlayerFromProxy(name) + if strpersonaId is None and game in constants.SPARTA_GAMES: + strpersonaId = await eaDesktopSearch(name) + data = json.dumps(strpersonaId) + await set_routes_to_cache(key=f'names:{originId}:{name}:{game}:{platform}', value=data, ttl=604800) #1 week + return strpersonaId + else: + # get personaId based on userId + if originId is None: + strUserId = userId["infoList"][0]["friendUserId"] + else: + strUserId = originId + strpersonaId = {"personaId": None} + url = f"https://api2.origin.com/atom/users?userIds={strUserId}" + async with session.get(url, headers={'Accept': 'application/json', **headers}) as r: + userJson = await r.json() + strpersonaId = { + 'personaId': userJson['users'][0]['personaId'], + 'personaName': userJson['users'][0]['eaId'] + } + # get user avatar + url = f"https://api3.origin.com/avatar/user/{strUserId}/avatars?size=1" + async with session.get(url=url, headers={'Accept': 'application/json', **headers}) as r: + avatarJson = await r.json() + strpersonaId["avatar"] = avatarJson['users'][0]['avatar']['link'] + strpersonaId["battlelog"] = False + data = json.dumps(strpersonaId) + await set_routes_to_cache(key=f'names:{originId}:{name}:{game}:{platform}', value=data, ttl=604800) #1 week + return strpersonaId + +async def checkMe(self, ctx): + playerInfo = await self.bot.stickyPlayerName.find(ctx.author.id) + if playerInfo is not None: + return playerInfo["name"] + else: + await ctx.send("You haven't setup the \"me\" option (!bfsetme playername), trying with your nickname in this server..") + return re.sub(r'\[[^)]*\]', '', ctx.author.display_name) \ No newline at end of file diff --git a/battlefield/bftracker/bf1.py b/battlefield/bftracker/bf1.py new file mode 100644 index 0000000..133d434 --- /dev/null +++ b/battlefield/bftracker/bf1.py @@ -0,0 +1,107 @@ +import aiohttp +from lxml import html +import re +from ..imageStats.main import imageStats +import urllib.parse +from aiogram import types +from ..commands import grapher + +async def bf1TrackerPlatformSelect(message: str): + oldmessage = message.split() + current = "pc" + platforms = ["pc", "origin", "ps4", "psn", "xbox", "xbl", "one"] + for platform in platforms: + if platform in oldmessage: + if platform == "pc": + current = "pc" + elif platform == "ps4" or platform == "psn": + current = "psn" + elif platform == "xbox" or platform == "xbl" or platform == "one": + current = "xbox" + else: + current = platform + new_message = [string for string in oldmessage if string not in platforms] + return {"current": current, "new_message": str(' '.join(new_message))} + +async def top10WeaponGraph(message, result): + """returns top weapons for bf1 graph""" + try: + async with aiohttp.ClientSession() as session: + url = f"https://battlefieldtracker.com/bf1/profile/{result['current']}/{urllib.parse.quote(result['new_message'])}/weapons" + async with session.get(url=url) as r: + tree = html.fromstring(await r.text()) + graphing = [] + i = 1 + while i != 20: + try: + weaponName = tree.xpath(f'/html/body/div[1]/div[1]/div[3]/div/div[2]/div[4]/table/tbody/tr[{i}]/td[1]/div[1]/text()')[0].strip() + kills = int(''.join(filter(str.isdigit, tree.xpath(f'/html/body/div[1]/div[1]/div[3]/div/div[2]/div[4]/table/tbody/tr[{i}]/td[2]/div[1]/text()')[0].strip()))) + try: + graphing.append({"name": weaponName, "kills": int(kills)}) + i+=1 + except: + pass + except: + break + stats = {"platformid": result['current']} + await grapher.main(message, graphing, stats, "bf1", "weapons") + except: + await message.reply("player not found") + +async def top10VehicleGraph(message, result): + """returns top weapons for bf1""" + try: + async with aiohttp.ClientSession() as session: + url = f"https://battlefieldtracker.com/bf1/profile/{result['current']}/{urllib.parse.quote(result['new_message'])}/vehicles" + async with session.get(url=url) as r: + tree = html.fromstring(await r.text()) + graphing = [] + i = 1 + while i != 20: + try: + weaponName = tree.xpath(f'/html/body/div[1]/div[1]/div[3]/div/div[1]/div[3]/table/tbody/tr[{i}]/td[1]/div/text()')[0].strip() + kills = tree.xpath(f'/html/body/div[1]/div[1]/div[3]/div/div[1]/div[3]/table/tbody/tr[{i}]/td[2]/div[1]/text()')[0].strip() + try: + graphing.append({"name": weaponName, "kills": int(kills)}) + i+=1 + except: + pass + except: + break + stats = {"platformid": result['current']} + await grapher.main(message, graphing, stats, "bf1", "vehicles") + except: + await message.reply("player not found") + + +async def bf1Tracker(message: types.Message, result): + """returns stats for bf4 and bf1""" + try: + async with aiohttp.ClientSession() as session: + url = f"https://battlefieldtracker.com/bf1/profile/{result['current']}/{urllib.parse.quote(result['new_message'])}" + async with session.get(url=url) as r: + tree = html.fromstring(await r.text()) + stats = { + "onlyName": str('%20'.join(result['new_message'])), + "name": str(result['new_message']), + "avatar": "https://eaassets-a.akamaihd.net/battlelog/defaultavatars/default-avatar-204.png?ssl=1", + "rank": re.findall(r'\d+',tree.xpath('/html/body/div[1]/div[1]/div[3]/div[1]/div[1]/div/span/text()')[0])[0], + "rankImg": "https://battlefieldtracker.com" + tree.xpath('/html/body/div[1]/div[1]/div[3]/div[1]/div[1]/img/@src')[0], + "0.0": tree.xpath('/html/body/div[1]/div[1]/div[3]/div[2]/div[1]/div[2]/div/div[8]/div[2]/text()')[0].strip(), + "0.1": tree.xpath('/html/body/div[1]/div[1]/div[3]/div[2]/div[1]/div[2]/div/div[3]/div[2]/text()')[0].strip(), + "0.2": tree.xpath('/html/body/div[1]/div[1]/div[3]/div[2]/div[1]/div[6]/div/div[6]/div[2]/text()')[0].strip(), + "0.3": tree.xpath('/html/body/div[1]/div[1]/div[3]/div[2]/div[1]/div[2]/div/div[5]/div[2]/text()')[0].strip(), + "0.4": tree.xpath('/html/body/div[1]/div[1]/div[3]/div[2]/div[1]/div[16]/div/div[7]/div[2]/text()')[0].strip(), + "1.0": tree.xpath('/html/body/div[1]/div[1]/div[3]/div[1]/div[2]/div[1]/div[3]/div[2]/text()')[0].strip(), + "1.1": tree.xpath('/html/body/div[1]/div[1]/div[3]/div[2]/div[1]/div[16]/div/div[5]/div[2]/text()')[0].strip(), + "1.2": tree.xpath('/html/body/div[1]/div[1]/div[3]/div[2]/div[1]/div[6]/div/div[7]/div[2]/text()')[0].strip(), + "1.3": tree.xpath('/html/body/div[1]/div[1]/div[3]/div[2]/div[1]/div[2]/div/div[10]/div[2]/text()')[0].strip(), + "1.4": tree.xpath('/html/body/div[1]/div[1]/div[3]/div[2]/div[1]/div[16]/div/div[6]/div[2]/text()')[0].strip(), + "2.0": tree.xpath('/html/body/div[1]/div[1]/div[3]/div[2]/div[1]/div[12]/div/div[6]/div[2]/text()')[0].strip(), + "2.1": tree.xpath('/html/body/div[1]/div[1]/div[3]/div[2]/div[1]/div[10]/div/div[1]/div[2]/text()')[0].strip(), + "2.2": tree.xpath('/html/body/div[1]/div[1]/div[3]/div[2]/div[1]/div[10]/div/div[2]/div[2]/text()')[0].strip(), + "2.3": tree.xpath('/html/body/div[1]/div[1]/div[3]/div[2]/div[1]/div[2]/div/div[9]/div[2]/text()')[0].strip() + } + await imageStats.imageRender(message, stats, url, "bf1", result['current']) + except: + await message.reply("player not found or no playername given, usage:\n /bf1stat (platform) name") \ No newline at end of file diff --git a/battlefield/bftracker/bf5.py b/battlefield/bftracker/bf5.py new file mode 100644 index 0000000..b533e73 --- /dev/null +++ b/battlefield/bftracker/bf5.py @@ -0,0 +1,75 @@ + +from ..imageStats.main import imageStats +from ..api.searchPlayer import bfTrackerPlatformSelect +import urllib.parse +import aiohttp +from aiogram import types +from ..commands import grapher + +async def bf5stat(message: types.Message): + try: + result = await bfTrackerPlatformSelect(message.text) + async with aiohttp.ClientSession() as session: + gameUrl = f"https://battlefieldtracker.com/bfv/profile/{result['current']}/{urllib.parse.quote(' '.join(result['new_message']))}/overview" + url = f"https://api.tracker.gg/api/v2/bfv/standard/profile/{result['current']}/{urllib.parse.quote(' '.join(result['new_message']))}" + async with session.get(url) as r: + stats = await r.json() + mainStats = stats["data"]["segments"][0]["stats"] + try: + headshotPercent = str(round((mainStats["headshots"]["value"]/mainStats["kills"]["value"])*100,2))+"%" + except: + headshotPercent = "0%" + stats = { + "onlyName": stats["data"]["platformInfo"]["platformUserHandle"], + "name": stats["data"]["platformInfo"]["platformUserHandle"], + "avatar": stats["data"]["platformInfo"]["avatarUrl"], + "rank": mainStats["rank"]["displayValue"], + "rankImg": mainStats["rank"]["metadata"]["imageUrl"], + "0.0": mainStats["killsPerMinute"]["displayValue"], + "0.1": mainStats["scorePerMinute"]["displayValue"], + "0.2": mainStats["saviorKills"]["displayValue"], + "0.3": mainStats["kdRatio"]["displayValue"], + "0.4": mainStats["longestHeadshot"]["displayValue"], + "1.0": mainStats["wlPercentage"]["displayValue"], + "1.1": headshotPercent, + "1.2": mainStats["shotsAccuracy"]["displayValue"], + "1.3": mainStats["assists"]["displayValue"], + "1.4": mainStats["killStreak"]["displayValue"], + "2.0": mainStats["revives"]["displayValue"], + "2.1": mainStats["repairs"]["displayValue"], + "2.2": mainStats["resupplies"]["displayValue"], + "2.3": mainStats["timePlayed"]["displayValue"], + } + await imageStats.imageRender(message, stats, gameUrl, "bfv", result['current']) + except: + await message.reply("player not found") + +async def bf5weapongraph(message: types.Message): + try: + result = await bfTrackerPlatformSelect(message.text) + async with aiohttp.ClientSession() as session: + url = f"http://api.tracker.gg/api/v1/bfv/profile/{result['current']}/{urllib.parse.quote(' '.join(result['new_message']))}/weapons" + async with session.get(url) as r: + stats = await r.json() + weapons = [] + for weapon in stats["data"]["children"]: + weapons.append({"name": weapon["metadata"]["name"], "kills": weapon["stats"][0]["value"]}) + graphing = sorted(weapons, key=lambda k: k['kills'], reverse=True) + await grapher.main(message, graphing, stats, "bf5", "weapons") + except: + await message.reply("player not found") + +async def bf5vehiclegraph(message: types.Message): + try: + result = await bfTrackerPlatformSelect(message.text) + async with aiohttp.ClientSession() as session: + url = f"http://api.tracker.gg/api/v1/bfv/profile/{result['current']}/{urllib.parse.quote(' '.join(result['new_message']))}/vehicles" + async with session.get(url) as r: + stats = await r.json() + vehicles = [] + for vehicle in stats["data"]["children"]: + vehicles.append({"name": vehicle["metadata"]["name"], "kills": vehicle["stats"][0]["value"]}) + graphing = sorted(vehicles, key=lambda k: k['kills'], reverse=True) + await grapher.main(message, graphing, stats, "bf5", "vehicles") + except: + await message.reply("player not found") diff --git a/battlefield/commands/bfstats.py b/battlefield/commands/bfstats.py new file mode 100644 index 0000000..a69e5e6 --- /dev/null +++ b/battlefield/commands/bfstats.py @@ -0,0 +1,256 @@ +from aiogram import types +from ..api.api_requests import getBf2DataFromEa, getDataFromBattlelog, getMultipleFromEa +import datetime +import urllib.parse +from ..imageStats.main import imageStats +from aiogram import types + +async def bf2(message: types.Message): + try: + stats = (await getBf2DataFromEa(message)) + gameUrl = f"https://www.bf2hub.com/stats/{str(stats['pid'])}" + kits = {int(stats['kkl-0']): "Anti-Tank", int(stats['kkl-1']): "Assault", int(stats['kkl-2']): "Engineer", int(stats['kkl-3']): "Medic", int(stats['kkl-4']): "Spec-Ops", int(stats['kkl-5']): "Support", int(stats['kkl-6']): "Sniper"} + sort = {} + [sort.update({key: value}) for (key, value) in sorted(kits.items(), reverse=True)] + stats = { + "name": stats['nick'], + "rank": stats['rank'], + "rankImg": f"https://www.bf2hub.com/home/images/ranks/rank_{stats['rank']}.png", + "0.0": round(int(stats["kill"])/(int(stats['time'])/60),2), + "0.1": int(int(stats['scor'])/(int(stats['time'])/60)), + "0.2": round(int(stats["kill"])/int(stats["deth"]),2), + + "0.3": stats["kill"], + "0.4": stats["deth"], + + "1.0": str(round(float(stats['osaa']),2)) + "%", + "1.1": list(sort.values())[0], + "1.2": str(int(int(stats["wins"])/(int(stats["wins"])+int(stats["loss"]))*100)) + "%", + + "1.3": stats["wins"], + "1.4": stats["loss"], + + "2.0": str(datetime.timedelta(seconds=int(stats['time']))) + } + await imageStats.oldBfImageRender(message, stats, gameUrl, "bf2") + except: + await message.reply("player not found") + +async def bf3(message: types.Message): + try: + stats = (await getDataFromBattlelog("overviewPopulateStats", message.text, "bf3")) + if stats['platformName'] == "cem_ea_id": + platformName = "pc" + else: + platformName = stats['platformName'] + gameUrl = f"https://gametools.network/stats/{platformName}/name/{urllib.parse.quote(stats['personaName'])}?game=bf3" + + platform = stats["platformName"] + #get Best Class: + items = [] + kits = {} + sort = {} + [items.append(item) for item in stats["data"]["overviewStats"]["kitScores"]] + [kits.update({stats["data"]["overviewStats"]["kitScores"][i]: i}) for i in items] + [sort.update({key: value}) for (key, value) in sorted(kits.items(), reverse=True)] + try: + kpm = round(stats["data"]["overviewStats"]["kills"]/(stats["data"]["overviewStats"]["timePlayed"]/60),2) + except: + kpm = 0 + try: + killDeath = round(stats["data"]["overviewStats"]["kills"]/stats["data"]["overviewStats"]["deaths"],2) + except: + killDeath = 0 + try: + winPercent = str(int(stats["data"]["overviewStats"]["numWins"]/(stats["data"]["overviewStats"]["numWins"]+stats["data"]["overviewStats"]["numLosses"])*100)) + "%" + except: + winPercent = "0%" + stats = { + "onlyName": stats['personaName'], + "name": stats['personaName'], + "avatar": stats["avatar"], + "rank": stats["data"]["overviewStats"]["rank"], + "rankImg": f'https://cdn.gametools.network/bf3/{stats["data"]["currentRankNeeded"]["texture"].replace("UI/Art/Persistence/Ranks/Rank", "").replace("UI/art/Persistence/Ranks/Rank", "").replace("UI/Art/Persistence/Ranks/rank", "").replace("S0100", "S100")}.png', + "0.0": kpm, + "0.1": stats["data"]["overviewStats"]["scorePerMinute"], + "0.2": stats["data"]["overviewStats"]["elo"], + "0.3": killDeath, + "0.4": stats["data"]["overviewStats"]["longestHeadshot"], + "1.0": winPercent, + "1.1": stats["data"]["overviewStats"]["headshots"], + "1.2": str(round(stats["data"]["overviewStats"]["accuracy"],2))+"%", + "1.3": stats["data"]["kitMap"][list(sort.values())[0]]["name"].capitalize(), + "1.4": stats["data"]["overviewStats"]["killStreakBonus"], + "2.0": stats["data"]["overviewStats"]["revives"], + "2.1": stats["data"]["overviewStats"]["repairs"], + "2.2": stats["data"]["overviewStats"]["resupplies"], + "2.3": str(datetime.timedelta(seconds=stats["data"]["overviewStats"]["timePlayed"])) + } + await imageStats.imageRender(message, stats, gameUrl, "bf3", platform) + except: + await message.reply("player not found") + +async def bfh(message: types.Message): + try: + stats = (await getDataFromBattlelog("bfhoverviewpopulate", message.text, "bfh")) + if stats['platformName'] == "cem_ea_id": + platformName = "pc" + else: + platformName = stats['platformName'] + gameUrl = f"https://gametools.network/stats/{platformName}/name/{urllib.parse.quote(stats['personaName'])}?game=bfh" + platform = stats["platformName"] + #get Best Class: + classes = {"2048": "commander", "4096": "enforcer", "8192": "mechanic", "16384": "operator", "32768": "professional"} + items = [] + kits = {} + sort = {} + [items.append(item) for item in stats["data"]["overviewStats"]["kitScores"]] + [kits.update({int(stats["data"]["overviewStats"]["kitScores"][i]): i}) for i in items] + [sort.update({key: value}) for (key, value) in sorted(kits.items(), reverse=True)] + try: + kpm = round(stats["data"]["overviewStats"]["kills"]/(stats["data"]["overviewStats"]["timePlayed"]/60),2) + except: + kpm = 0 + try: + winPercent = str(int(stats["data"]["overviewStats"]["numWins"]/(stats["data"]["overviewStats"]["numWins"]+stats["data"]["overviewStats"]["numLosses"])*100)) + "%" + except: + winPercent = "0%" + stats = { + "onlyName": stats['personaName'], + "name": stats['personaName'], + "avatar": stats["avatar"], + "rank": stats["data"]["overviewStats"]["rank"], + "rankImg": f'https://cdn.gametools.network/bfh/{stats["data"]["overviewStats"]["rank"]}.png', + "0.0": kpm, + "0.1": stats["data"]["overviewStats"]["scorePerMinute"], + "0.2": winPercent, + "0.3": stats["data"]["overviewStats"]["kdRatio"], + "0.4": str(round(stats["data"]["overviewStats"]["accuracy"],2))+"%", + "1.0": stats["data"]["overviewStats"]["sc_enforcer"], + "1.1": stats["data"]["overviewStats"]["sc_mechanic"], + "1.2": stats["data"]["overviewStats"]["sc_operator"], + "1.3": stats["data"]["overviewStats"]["sc_professional"], + "1.4": stats["data"]["overviewStats"]["sc_hacker"], + "2.0": classes[list(sort.values())[0]][0:4].capitalize(), + "2.1": stats["data"]["overviewStats"]["cashPerMinute"], + "2.2": stats["data"]["overviewStats"]["killAssists"], + "2.3": str(datetime.timedelta(seconds=stats["data"]["overviewStats"]["timePlayed"])) + } + await imageStats.imageRender(message, stats, gameUrl, "bfh", platform) + except: + await message.reply("player not found") + + +async def bf4(message: types.Message): + try: + stats = (await getDataFromBattlelog("warsawdetailedstatspopulate", message.text, "bf4")) + if stats['platformName'] == "cem_ea_id": + platformName = "pc" + else: + platformName = stats['platformName'] + gameUrl = f"https://gametools.network/stats/{platformName}/name/{urllib.parse.quote(stats['personaName'])}?game=bf4" + platform = stats["platformName"] + #get Best Class: + classes = {"1":"Assault","2":"engineer","32":"support","8":"recon","2048":"commander"} + items = [] + kits = {} + sort = {} + [items.append(item) for item in stats["data"]["generalStats"]["kitScores"]] + [kits.update({int(stats["data"]["generalStats"]["kitScores"][i]): i}) for i in items] + [sort.update({key: value}) for (key, value) in sorted(kits.items(), reverse=True)] + try: + winPercent = str(int(int(stats["data"]["generalStats"]["numWins"])/(int(stats["data"]["generalStats"]["numWins"])+int(stats["data"]["generalStats"]["numLosses"]))*100)) + "%" + except: + winPercent = "0%" + stats = { + "onlyName": stats['personaName'], + "name": stats['personaName'], + "avatar": stats["avatar"], + "rank": stats["data"]["generalStats"]["rank"], + "rankImg": f'https://cdn.gametools.network/bf4/{stats["data"]["generalStats"]["rank"]}.png', + "0.0": stats["data"]["generalStats"]["killsPerMinute"], + "0.1": stats["data"]["generalStats"]["scorePerMinute"], + "0.2": stats["data"]["generalStats"]["skill"], + "0.3": stats["data"]["generalStats"]["kdRatio"], + "0.4": stats["data"]["generalStats"]["longestHeadshot"], + "1.0": winPercent, + "1.1": stats["data"]["generalStats"]["headshots"], + "1.2": str(round(stats["data"]["generalStats"]["accuracy"],2))+"%", + "1.3": classes[list(sort.values())[0]].capitalize(), + "1.4": stats["data"]["generalStats"]["killStreakBonus"], + "2.0": stats["data"]["generalStats"]["revives"], + "2.1": stats["data"]["generalStats"]["repairs"], + "2.2": stats["data"]["generalStats"]["resupplies"], + "2.3": str(datetime.timedelta(seconds=int(stats["data"]["generalStats"]["timePlayed"]))) + } + await imageStats.imageRender(message, stats, gameUrl, "bf4", platform) + except: + await message.reply("player not found") + +async def bf1(message: types.Message, name, game, rGameName): + """returns stats for bf4 and bf1""" + try: + stats = (await getMultipleFromEa(name, "Stats.detailedStatsByPersonaId", game)) + if game == "tunguska": + gameUrl = f"https://gametools.network/stats/pc/name/{urllib.parse.quote(stats['personaName'])}?game=bf1" + else: #bf4 + gameUrl = f"https://gametools.network/stats/pc/name/{urllib.parse.quote(stats['personaName'])}?game=bf4" + vehicleKills = 0 + vehicleKills = 0 + try: + for vehicle in stats["result"]["vehicleStats"]: + vehicleKills += vehicle["killsAs"] + except: + pass + try: + winLoseProcent = str(int(float(stats["result"]["basicStats"]["wins"])/(float(stats["result"]["basicStats"]["wins"])+float(stats["result"]["basicStats"]["losses"]))*100)) + "%" + except: + winLoseProcent = "0%" + try: + headshotProcent = str(round((stats["result"]["headShots"]/stats["result"]["basicStats"]["kills"])*100,2))+"%" + except: + headshotProcent = "0%" + try: + kd = round(float(stats["result"]["basicStats"]["kills"])/float(stats["result"]["basicStats"]["deaths"]),2) + infantryKD = round((float(stats["result"]["basicStats"]["kills"])-float(vehicleKills))/float(stats["result"]["basicStats"]["deaths"]),2) + infantryKPM = round((float(stats["result"]["basicStats"]["kills"])-float(vehicleKills))/float((stats["result"]["basicStats"]["timePlayed"])/60),2) + except: + kd = 0 + infantryKD = 0 + infantryKPM = 0 + try: + bestClass = stats["result"]["favoriteClass"].capitalize() + except: + bestClass = "" + try: + accuracy = str(round(stats["result"]["accuracyRatio"]*100,2))+"%" + except: + accuracy = "0%" + if stats['activePlatoon']["result"] == None: + name = stats['personaName'] + else: + name = f"[{stats['activePlatoon']['result']['tag']}]{stats['personaName']}" + stats = { + "onlyName": stats['personaName'], + "name": name, + "avatar": stats["avatar"], + "rank": "", + "rankImg": "", + "0.0": stats["result"]["basicStats"]["kpm"], + "0.1": stats["result"]["basicStats"]["spm"], + "0.2": stats["result"]["basicStats"]["skill"], + "0.3": kd, + "0.4": stats["result"]["longestHeadShot"], + "1.0": winLoseProcent, + "1.1": headshotProcent, + "1.2": accuracy, + "1.3": bestClass, + "1.4": stats["result"]["highestKillStreak"], + "2.0": stats["result"]["revives"], + "2.1": infantryKD, + "2.2": infantryKPM, + "2.3": datetime.timedelta(seconds=stats["result"]["basicStats"]["timePlayed"]) + } + await imageStats.imageRender(message, stats, gameUrl, rGameName, "pc") + except: + await message.reply("player not found") \ No newline at end of file diff --git a/battlefield/commands/grapher.py b/battlefield/commands/grapher.py new file mode 100644 index 0000000..5134e94 --- /dev/null +++ b/battlefield/commands/grapher.py @@ -0,0 +1,56 @@ +import urllib.parse +import io +from aiogram import types + +"""add plotting(graphs)""" +import matplotlib.pyplot as plt +#matplotlib inline +plt.style.use('ggplot') + +async def main(message: types.Message, graphing, stats, game, type): + """makes the graphs for all the bfgraph commands""" + try: + if game == "tunguska": + gameUrl = f"https://gametools.network/stats/pc/name/{urllib.parse.quote(stats['personaName'])}?game=bf1" + elif game == "bf1": # bftracker + gameUrl = f"https://battlefieldtracker.com/bf1/profile/{stats['platformid']}/{urllib.parse.quote(message.text)}/{type}" + elif game == "bf2": + gameUrl = f"https://www.bf2hub.com/stats/{urllib.parse.quote(stats['pid'])}" + elif game == "bf5": + gameUrl = f"https://battlefieldtracker.com/bfv/profile/origin/{urllib.parse.quote(message.text)}/{type}" + else: + pid = str(stats["personaId"]) + gameUrl = f"https://battlelog.battlefield.com/{game}/soldier/{urllib.parse.quote(message.text)}/{type}/{pid}/{stats['platformid']}/" + x1 = [] + y1 = [] + x = [] + y = [] + t = 0 + [ x1.append(key["name"]) for (key) in graphing ] + [ y1.append(key["kills"]) for (key) in graphing ] + for i in range(len(x1)): + if t != 20: + x.append(x1[i]) + y.append(y1[i]) + else: + break + t+=1 + x_pos = [i for i, _ in enumerate(x)] + with io.BytesIO() as data_stream: + plt.figure(facecolor="#151829") + plt.bar(x_pos, y, color='#00aaff') + plt.xticks(x_pos, x, color='#ffffff', rotation=45, horizontalalignment='right') + plt.yticks(color='#ffffff') + plt.grid(b=False) + ax=plt.gca() + ax.set_facecolor('#151829') + ax.spines["top"].set_visible(False) + ax.spines["right"].set_visible(False) + plt.tight_layout(pad=0.4, w_pad=0.5, h_pad=1.0) + + plt.savefig(data_stream, format='png', transparent=False) + plt.close() + data_stream.seek(0) + await message.reply_photo(data_stream, caption=f"Top 20 {type}\n{gameUrl}") + except: + await message.reply("player not found") diff --git a/battlefield/commands/vehicles.py b/battlefield/commands/vehicles.py new file mode 100644 index 0000000..acb1488 --- /dev/null +++ b/battlefield/commands/vehicles.py @@ -0,0 +1,50 @@ +from aiogram import types +from ..api.api_requests import getBf2DataFromEa, getDataFromBattlelog, getDataFromEa, getMultipleFromEa +from . import grapher + +async def bf2Graph(message: types.Message): + try: + stats = (await getBf2DataFromEa(message.text)) + vehicleNames = ["Armor", "Jet", "Helicopter", "Transport", "Anti-Air", "Ground-Def."] + graphing = [] + for i in range(len(vehicleNames)): + try: + graphing.append({"name": vehicleNames[i], "kills": int(stats["vkl-"+str(i)])}) + except: + pass + await grapher.main(message, graphing, stats, "bf2", "vehicles") + except: + await message.reply("player not found") + +async def graph(message: types.Message, game): + """returns top vehicles for bf1 in a graph""" + try: + stats = (await getDataFromEa(message.text, "Progression.getVehiclesByPersonaId", game)) + vehicles = [] + for weapon in stats["result"]: + try: + vehicles.append({"name": weapon["name"].capitalize(), "kills": int(weapon["stats"]["values"]["kills"])}) + except: + pass + graphing = sorted(vehicles, key=lambda k: k['kills'], reverse=True) + await grapher.main(message, graphing, stats, game, "vehicles") + except: + await message.reply("player not found") + +async def battlelogGraph(message: types.Message, type, game): + """returns top vehicles for bfh, bf3 and bf4 as graph""" + try: + stats = (await getDataFromBattlelog(type, message.text, game)) + naming = "name" + if game in ["bfh", "bf4"]: + naming = "slug" + vehicles = [] + for weapon in stats["data"]["mainVehicleStats"]: + try: + vehicles.append({"name": weapon[naming], "kills": int(weapon["kills"])}) + except: + pass + graphing = sorted(vehicles, key=lambda k: k['kills'], reverse=True) + await grapher.main(message, graphing, stats, game, "vehicles") + except: + await message.reply("player not found") \ No newline at end of file diff --git a/battlefield/commands/weapons.py b/battlefield/commands/weapons.py new file mode 100644 index 0000000..382d71e --- /dev/null +++ b/battlefield/commands/weapons.py @@ -0,0 +1,52 @@ +from aiogram import types +from ..api.api_requests import getBf2DataFromEa, getDataFromBattlelog, getDataFromEa, getMultipleFromEa +from . import grapher + +async def graph(message: types.Message, game): + """returns top weapons for bf1 in a graph""" + try: + stats = (await getDataFromEa(message.text, "Progression.getWeaponsByPersonaId", game)) + weapons = [] + for weaponGroup in stats["result"]: + for weapon in weaponGroup["weapons"]: + try: + weapons.append({"name": weapon["name"], "kills": int(weapon["stats"]["values"]["kills"])}) + except: + pass + graphing = sorted(weapons, key=lambda k: k['kills'], reverse=True) + await grapher.main(message, graphing, stats, game, "weapons") + except: + await message.reply("player not found") + +async def battlelogGraph(message: types.Message, type, game): + """returns top weapons for bfh, bf3 and bf4 as graph""" + try: + stats = (await getDataFromBattlelog(type, message.text, game)) + weapons = [] + naming = "name" + if game in ["bfh", "bf4"]: + naming = "slug" + for weapon in stats["data"]["mainWeaponStats"]: + try: + weapons.append({"name": weapon[naming], "kills": int(weapon["kills"])}) + except: + pass + graphing = sorted(weapons, key=lambda k: k['kills'], reverse=True) + await grapher.main(message, graphing, stats, game, "weapons") + except: + await message.reply("player not found") + +async def bf2Graph(message: types.Message): + try: + stats = (await getBf2DataFromEa(message.text)) + weaponNames = ["Assault-Rifle", "Grenade-Launcher", "Carbine", "Light Machine Gun", "Sniper Rifle", "Pistol", "Anti-Tank", "Sub-Machine Gun", "Shotgun", "Knife", "Defibrillator", "Explosives", "Grenade"] + weapons = [] + for i in range(len(weaponNames)): + try: + weapons.append({"name": weaponNames[i], "kills": int(stats["wkl-"+str(i)])}) + except: + pass + graphing = sorted(weapons, key=lambda k: k['kills'], reverse=True) + await grapher.main(message, graphing, stats, "bf2", "weapons") + except: + await message.reply("player not found") diff --git a/battlefield/handlers/__init__.py b/battlefield/handlers/__init__.py new file mode 100644 index 0000000..ae21e83 --- /dev/null +++ b/battlefield/handlers/__init__.py @@ -0,0 +1,3 @@ +from .bfstats import * +from .weapons import * +from .vehicles import * \ No newline at end of file diff --git a/battlefield/handlers/bfstats.py b/battlefield/handlers/bfstats.py new file mode 100644 index 0000000..5582d3e --- /dev/null +++ b/battlefield/handlers/bfstats.py @@ -0,0 +1,126 @@ +from aiogram.dispatcher import FSMContext +from aiogram.dispatcher.filters import Text +from aiogram import types +from ..bftracker import bf1, bf5 +from ..commands import bfstats +import re +from app import StatState, dp + +@dp.message_handler(commands=['bf2', 'bf2stat', 'bf2stats']) +async def bf2stats(message: types.Message): + message.text = re.sub(r"^/[a-zA-Z0-9_.-@]*", "", message.text).lstrip() + if message.text == "": + await StatState.bf2.set() + await message.reply("Give me a playername (\"/cancel\" to cancel)") + else: + await bfstats.bf2(message) + +@dp.message_handler(state=StatState.bf2) +async def bf2stats_return(message: types.Message, state: FSMContext): + message.text = re.sub(r"^/[a-zA-Z0-9_.-@]*", "", message.text).lstrip() + await bfstats.bf2(message) + await state.finish() + + +@dp.message_handler(commands=['bf3', 'bf3stat', 'bf3stats']) +async def bf3stats(message: types.Message): + message.text = re.sub(r"^/[a-zA-Z0-9_.-@]*", "", message.text).lstrip() + if message.text == "": + await StatState.bf3.set() + await message.reply("Give me a platform and playername (pc is default platform if none given, \"/cancel\" to cancel)") + else: + await bfstats.bf3(message) + +@dp.message_handler(state=StatState.bf3) +async def bf3stats_return(message: types.Message, state: FSMContext): + message.text = re.sub(r"^/[a-zA-Z0-9_.-@]*", "", message.text).lstrip() + await bfstats.bf3(message) + await state.finish() + + +@dp.message_handler(commands=['bfh', 'bfhstat', 'bfhstats']) +async def bfhstats(message: types.Message): + message.text = re.sub(r"^/[a-zA-Z0-9_.-@]*", "", message.text).lstrip() + if message.text == "": + await StatState.bfh.set() + await message.reply("Give me a platform and playername (pc is default platform if none given, \"/cancel\" to cancel)") + else: + await bfstats.bfh(message) + +@dp.message_handler(state=StatState.bfh) +async def bfhstats_return(message: types.Message, state: FSMContext): + message.text = re.sub(r"^/[a-zA-Z0-9_.-@]*", "", message.text).lstrip() + await bfstats.bfh(message) + await state.finish() + + +@dp.message_handler(commands=['bf4', 'bf4stat', 'bf4stats']) +async def bf4stats(message: types.Message): + message.text = re.sub(r"^/[a-zA-Z0-9_.-@]*", "", message.text).lstrip() + if message.text == "": + await StatState.bf4.set() + await message.reply("Give me a platform and playername (pc is default platform if none given, \"/cancel\" to cancel)") + else: + await bfstats.bf4(message) + +@dp.message_handler(state=StatState.bf4) +async def bf4stats_return(message: types.Message, state: FSMContext): + message.text = re.sub(r"^/[a-zA-Z0-9_.-@]*", "", message.text).lstrip() + await bfstats.bf4(message) + await state.finish() + + +@dp.message_handler(commands=['bf1', 'bf1stat', 'bf1stats']) +async def bf1stats(message: types.Message): + message.text = re.sub(r"^/[a-zA-Z0-9_.-@]*", "", message.text).lstrip() + if message.text == "": + await StatState.bf1.set() + await message.reply("Give me a platform and playername (pc is default platform if none given, \"/cancel\" to cancel)") + else: + result = await bf1.bf1TrackerPlatformSelect(message.text) + if result["current"] == "pc": + await bfstats.bf1(message, result["new_message"], "tunguska", "bf1") + else: + await bf1.bf1Tracker(message, result) + +@dp.message_handler(state=StatState.bf1) +async def bf1stats_return(message: types.Message, state: FSMContext): + message.text = re.sub(r"^/[a-zA-Z0-9_.-@]*", "", message.text).lstrip() + result = await bf1.bf1TrackerPlatformSelect(message.text) + if result["current"] == "pc": + await bfstats.bf1(message, result["new_message"], "tunguska", "bf1") + else: + await bf1.bf1Tracker(message, result) + await state.finish() + + +@dp.message_handler(commands=['bf5', 'bf5stat', 'bf5stats', 'bfv', 'bfvstat', 'bfvstats']) +async def bf5stats(message: types.Message): + message.text = re.sub(r"^/[a-zA-Z0-9_.-@]*", "", message.text).lstrip() + if message.text == "": + await StatState.bf5.set() + await message.reply("Give me a platform and playername (pc is default platform if none given, \"/cancel\" to cancel)") + else: + await bf5.bf5stat(message) + +@dp.message_handler(state=StatState.bf5) +async def bf5stats_return(message: types.Message, state: FSMContext): + message.text = re.sub(r"^/[a-zA-Z0-9_.-@]*", "", message.text).lstrip() + await bf5.bf5stat(message) + await state.finish() + + +@dp.message_handler(state='*', commands='cancel') +@dp.message_handler(Text(equals='cancel', ignore_case=True), state='*') +async def cancel_handler(message: types.Message, state: FSMContext): + """ + Allow user to cancel any action + """ + current_state = await state.get_state() + if current_state is None: + return + + # Cancel state and inform user about it + await state.finish() + # And remove keyboard (just in case) + await message.reply('Cancelled action.') diff --git a/battlefield/handlers/vehicles.py b/battlefield/handlers/vehicles.py new file mode 100644 index 0000000..5c7a54e --- /dev/null +++ b/battlefield/handlers/vehicles.py @@ -0,0 +1,110 @@ +from aiogram.dispatcher import FSMContext +from aiogram import types +from ..commands import vehicles +import re +from app import StatState, dp +from ..bftracker import bf1, bf5 + +@dp.message_handler(commands=['bf2vehiclegraph']) +async def bf2stats(message: types.Message): + message.text = re.sub(r"^/[a-zA-Z0-9_.-@]*", "", message.text).lstrip() + if message.text == "": + await StatState.bf2vehiclegraph.set() + await message.reply("Give me a playername (\"/cancel\" to cancel)") + else: + await vehicles.bf2Graph(message) + +@dp.message_handler(state=StatState.bf2vehiclegraph) +async def bf2stats_return(message: types.Message, state: FSMContext): + message.text = re.sub(r"^/[a-zA-Z0-9_.-@]*", "", message.text).lstrip() + await vehicles.bf2Graph(message) + await state.finish() + + +@dp.message_handler(commands=['bf3vehiclegraph']) +async def bf3stats(message: types.Message): + message.text = re.sub(r"^/[a-zA-Z0-9_.-@]*", "", message.text).lstrip() + if message.text == "": + await StatState.bf3vehiclegraph.set() + await message.reply("Give me a platform and playername (pc is default platform if none given, \"/cancel\" to cancel)") + else: + await vehicles.battlelogGraph(message, "vehiclesPopulateStats", "bf3") + +@dp.message_handler(state=StatState.bf3vehiclegraph) +async def bf3stats_return(message: types.Message, state: FSMContext): + message.text = re.sub(r"^/[a-zA-Z0-9_.-@]*", "", message.text).lstrip() + await vehicles.battlelogGraph(message, "vehiclesPopulateStats", "bf3") + await state.finish() + + +@dp.message_handler(commands=['bfhvehiclegraph']) +async def bfhstats(message: types.Message): + message.text = re.sub(r"^/[a-zA-Z0-9_.-@]*", "", message.text).lstrip() + if message.text == "": + await StatState.bfhvehiclegraph.set() + await message.reply("Give me a platform and playername (pc is default platform if none given, \"/cancel\" to cancel)") + else: + await vehicles.battlelogGraph(message, "bfhvehiclesPopulateStats", "bfh") + +@dp.message_handler(state=StatState.bfhvehiclegraph) +async def bfhstats_return(message: types.Message, state: FSMContext): + message.text = re.sub(r"^/[a-zA-Z0-9_.-@]*", "", message.text).lstrip() + await vehicles.battlelogGraph(message, "bfhvehiclesPopulateStats", "bfh") + await state.finish() + + +@dp.message_handler(commands=['bf4vehiclegraph']) +async def bf4stats(message: types.Message): + message.text = re.sub(r"^/[a-zA-Z0-9_.-@]*", "", message.text).lstrip() + if message.text == "": + await StatState.bf4vehiclegraph.set() + await message.reply("Give me a platform and playername (pc is default platform if none given, \"/cancel\" to cancel)") + else: + await vehicles.battlelogGraph(message, "warsawvehiclesPopulateStats", "bf4") + +@dp.message_handler(state=StatState.bf4vehiclegraph) +async def bf4stats_return(message: types.Message, state: FSMContext): + message.text = re.sub(r"^/[a-zA-Z0-9_.-@]*", "", message.text).lstrip() + await vehicles.battlelogGraph(message, "warsawvehiclesPopulateStats", "bf4") + await state.finish() + + +@dp.message_handler(commands=['bfvehiclegraph', 'bf1vehiclegraph']) +async def bf1stats(message: types.Message): + message.text = re.sub(r"^/[a-zA-Z0-9_.-@]*", "", message.text).lstrip() + if message.text == "": + await StatState.bf1.set() + await message.reply("Give me a platform and playername (pc is default platform if none given, \"/cancel\" to cancel)") + else: + result = await bf1.bf1TrackerPlatformSelect(message.text) + if result["current"] == "pc": + message.text = result["new_message"] + await vehicles.graph(message, "tunguska") + else: + await bf1.top10VehicleGraph(message, result) + +@dp.message_handler(state=StatState.bf1vehiclegraph) +async def bf1stats_return(message: types.Message, state: FSMContext): + message.text = re.sub(r"^/[a-zA-Z0-9_.-@]*", "", message.text).lstrip() + result = await bf1.bf1TrackerPlatformSelect(message.text) + if result["current"] == "pc": + message.text = result["new_message"] + await vehicles.graph(message, "tunguska") + else: + await bf1.top10VehicleGraph(message, result) + + +@dp.message_handler(commands=['bf5vehiclegraph', 'bfvvehiclegraph']) +async def bf5stats(message: types.Message): + message.text = re.sub(r"^/[a-zA-Z0-9_.-@]*", "", message.text).lstrip() + if message.text == "": + await StatState.bf5vehiclegraph.set() + await message.reply("Give me a platform and playername (pc is default platform if none given, \"/cancel\" to cancel)") + else: + await bf5.bf5vehiclegraph(message) + +@dp.message_handler(state=StatState.bf5vehiclegraph) +async def bf5stats_return(message: types.Message, state: FSMContext): + message.text = re.sub(r"^/[a-zA-Z0-9_.-@]*", "", message.text).lstrip() + await bf5.bf5vehiclegraph(message) + await state.finish() diff --git a/battlefield/handlers/weapons.py b/battlefield/handlers/weapons.py new file mode 100644 index 0000000..2068a55 --- /dev/null +++ b/battlefield/handlers/weapons.py @@ -0,0 +1,110 @@ +from aiogram.dispatcher import FSMContext +from aiogram import types +from ..commands import weapons +import re +from app import StatState, dp +from ..bftracker import bf1, bf5 + +@dp.message_handler(commands=['bf2weapongraph']) +async def bf2stats(message: types.Message): + message.text = re.sub(r"^/[a-zA-Z0-9_.-@]*", "", message.text).lstrip() + if message.text == "": + await StatState.bf2weapongraph.set() + await message.reply("Give me a playername (\"/cancel\" to cancel)") + else: + await weapons.bf2Graph(message) + +@dp.message_handler(state=StatState.bf2weapongraph) +async def bf2stats_return(message: types.Message, state: FSMContext): + message.text = re.sub(r"^/[a-zA-Z0-9_.-@]*", "", message.text).lstrip() + await weapons.bf2Graph(message) + await state.finish() + + +@dp.message_handler(commands=['bf3weapongraph']) +async def bf3stats(message: types.Message): + message.text = re.sub(r"^/[a-zA-Z0-9_.-@]*", "", message.text).lstrip() + if message.text == "": + await StatState.bf3weapongraph.set() + await message.reply("Give me a platform and playername (pc is default platform if none given, \"/cancel\" to cancel)") + else: + await weapons.battlelogGraph(message, "weaponsPopulateStats", "bf3") + +@dp.message_handler(state=StatState.bf3weapongraph) +async def bf3stats_return(message: types.Message, state: FSMContext): + message.text = re.sub(r"^/[a-zA-Z0-9_.-@]*", "", message.text).lstrip() + await weapons.battlelogGraph(message, "weaponsPopulateStats", "bf3") + await state.finish() + + +@dp.message_handler(commands=['bfhweapongraph']) +async def bfhstats(message: types.Message): + message.text = re.sub(r"^/[a-zA-Z0-9_.-@]*", "", message.text).lstrip() + if message.text == "": + await StatState.bfhweapongraph.set() + await message.reply("Give me a platform and playername (pc is default platform if none given, \"/cancel\" to cancel)") + else: + await weapons.battlelogGraph(message, "BFHWeaponsPopulateStats", "bfh") + +@dp.message_handler(state=StatState.bfhweapongraph) +async def bfhstats_return(message: types.Message, state: FSMContext): + message.text = re.sub(r"^/[a-zA-Z0-9_.-@]*", "", message.text).lstrip() + await weapons.battlelogGraph(message, "BFHWeaponsPopulateStats", "bfh") + await state.finish() + + +@dp.message_handler(commands=['bf4weapongraph']) +async def bf4stats(message: types.Message): + message.text = re.sub(r"^/[a-zA-Z0-9_.-@]*", "", message.text).lstrip() + if message.text == "": + await StatState.bf4weapongraph.set() + await message.reply("Give me a platform and playername (pc is default platform if none given, \"/cancel\" to cancel)") + else: + await weapons.battlelogGraph(message, "warsawWeaponsPopulateStats", "bf4") + +@dp.message_handler(state=StatState.bf4weapongraph) +async def bf4stats_return(message: types.Message, state: FSMContext): + message.text = re.sub(r"^/[a-zA-Z0-9_.-@]*", "", message.text).lstrip() + await weapons.battlelogGraph(message, "warsawWeaponsPopulateStats", "bf4") + await state.finish() + + +@dp.message_handler(commands=['bfweapongraph', 'bf1weapongraph']) +async def bf1stats(message: types.Message): + message.text = re.sub(r"^/[a-zA-Z0-9_.-@]*", "", message.text).lstrip() + if message.text == "": + await StatState.bf1.set() + await message.reply("Give me a platform and playername (pc is default platform if none given, \"/cancel\" to cancel)") + else: + result = await bf1.bf1TrackerPlatformSelect(message.text) + if result["current"] == "pc": + message.text = result["new_message"] + await weapons.graph(message, "tunguska") + else: + await bf1.top10WeaponGraph(message, result) + +@dp.message_handler(state=StatState.bf1weapongraph) +async def bf1stats_return(message: types.Message, state: FSMContext): + message.text = re.sub(r"^/[a-zA-Z0-9_.-@]*", "", message.text).lstrip() + result = await bf1.bf1TrackerPlatformSelect(message.text) + if result["current"] == "pc": + message.text = result["new_message"] + await weapons.graph(message, "tunguska") + else: + await bf1.top10WeaponGraph(message, result) + + +@dp.message_handler(commands=['bf5weapongraph', 'bfvweapongraph']) +async def bf5stats(message: types.Message): + message.text = re.sub(r"^/[a-zA-Z0-9_.-@]*", "", message.text).lstrip() + if message.text == "": + await StatState.bf5weapongraph.set() + await message.reply("Give me a platform and playername (pc is default platform if none given, \"/cancel\" to cancel)") + else: + await bf5.bf5weapongraph(message) + +@dp.message_handler(state=StatState.bf5weapongraph) +async def bf5stats_return(message: types.Message, state: FSMContext): + message.text = re.sub(r"^/[a-zA-Z0-9_.-@]*", "", message.text).lstrip() + await bf5.bf5weapongraph(message) + await state.finish() diff --git a/battlefield/imageStats/fonts/Futura.ttf b/battlefield/imageStats/fonts/Futura.ttf new file mode 100644 index 0000000000000000000000000000000000000000..97eded13a2e44285191f18a635967fe8803b97ce GIT binary patch literal 38980 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

086ZsKgOtwh}1o6d0u?ZwK8%`bd+6*2sIMS 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 DC3xDw literal 0 HcmV?d00001 diff --git a/battlefield/imageStats/images/bf1-1.png b/battlefield/imageStats/images/bf1-1.png new file mode 100644 index 0000000000000000000000000000000000000000..39b0907e6625e017ca079ac3142f835605b6aa42 GIT binary patch literal 511538 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