controller: Initial polling and parsing of Valorant presence
This commit is contained in:
		@@ -1,7 +1,6 @@
 | 
			
		||||
import logging
 | 
			
		||||
import os
 | 
			
		||||
import sys
 | 
			
		||||
import time
 | 
			
		||||
 | 
			
		||||
import infi.systray
 | 
			
		||||
 | 
			
		||||
@@ -11,33 +10,40 @@ import valconomy
 | 
			
		||||
DATA_DIR = os.path.join(os.environ['LOCALAPPDATA'], 'Valconomy')
 | 
			
		||||
LOG_FILE = os.path.join(DATA_DIR, 'app.log')
 | 
			
		||||
 | 
			
		||||
log = logging.getLogger('valconomy')
 | 
			
		||||
log = logging.getLogger('valconomy-app')
 | 
			
		||||
 | 
			
		||||
def data_file(fname: str) -> str:
 | 
			
		||||
  return os.path.abspath(os.path.join(os.path.dirname(__file__), fname))
 | 
			
		||||
 | 
			
		||||
running = True
 | 
			
		||||
def do_quit(tray: infi.systray.SysTrayIcon):
 | 
			
		||||
  global running
 | 
			
		||||
  log.info('Shutting down')
 | 
			
		||||
  running = False
 | 
			
		||||
 | 
			
		||||
def do_view_logs(tray: infi.systray.SysTrayIcon):
 | 
			
		||||
def do_open_datadir(tray: infi.systray.SysTrayIcon):
 | 
			
		||||
  os.startfile(DATA_DIR)
 | 
			
		||||
 | 
			
		||||
def main():
 | 
			
		||||
  os.makedirs(DATA_DIR, exist_ok=True)
 | 
			
		||||
 | 
			
		||||
  if sys.stderr is None:
 | 
			
		||||
    # running in console-less mode, redirect to log file
 | 
			
		||||
    sys.stderr = open(LOG_FILE, 'a')
 | 
			
		||||
 | 
			
		||||
  logging.basicConfig(
 | 
			
		||||
    format='%(asctime)s %(levelname)s %(message)s', level=logging.INFO,
 | 
			
		||||
    handlers=(
 | 
			
		||||
      logging.StreamHandler(sys.stderr), # log to stderr as well as file
 | 
			
		||||
      logging.FileHandler(LOG_FILE),
 | 
			
		||||
    ))
 | 
			
		||||
    stream=sys.stderr)
 | 
			
		||||
 | 
			
		||||
  log.info('Starting up')
 | 
			
		||||
  with infi.systray.SysTrayIcon(data_file('icon.ico'), 'Valconomy', menu_options=(('View logs', None, do_view_logs),), on_quit=do_quit) as tray:
 | 
			
		||||
    while running:
 | 
			
		||||
      time.sleep(0.5)
 | 
			
		||||
  def cb(i: valconomy.RiotPlayerInfo):
 | 
			
		||||
     print(repr(i))
 | 
			
		||||
 | 
			
		||||
  val_client = valconomy.ValorantLocalClient(cb)
 | 
			
		||||
  def do_quit(tray: infi.systray.SysTrayIcon):
 | 
			
		||||
    log.info('Shutting down')
 | 
			
		||||
    val_client.stop()
 | 
			
		||||
 | 
			
		||||
  with infi.systray.SysTrayIcon(
 | 
			
		||||
      data_file('icon.ico'), 'Valconomy', on_quit=do_quit,
 | 
			
		||||
      menu_options=(
 | 
			
		||||
        ('Open data directory', None, do_open_datadir),
 | 
			
		||||
      )) as tray:
 | 
			
		||||
        val_client.run()
 | 
			
		||||
 | 
			
		||||
if __name__ == '__main__':
 | 
			
		||||
  main()
 | 
			
		||||
 
 | 
			
		||||
@@ -1 +1,128 @@
 | 
			
		||||
import base64
 | 
			
		||||
from dataclasses import dataclass
 | 
			
		||||
import json
 | 
			
		||||
import logging
 | 
			
		||||
from pprint import pprint
 | 
			
		||||
import os
 | 
			
		||||
import time
 | 
			
		||||
from typing import Callable
 | 
			
		||||
 | 
			
		||||
import hid
 | 
			
		||||
import requests
 | 
			
		||||
import urllib3
 | 
			
		||||
 | 
			
		||||
# Local HTTPS connection, we need to not verify the cert
 | 
			
		||||
urllib3.disable_warnings()
 | 
			
		||||
 | 
			
		||||
log = logging.getLogger('valconomy')
 | 
			
		||||
 | 
			
		||||
@dataclass
 | 
			
		||||
class ValorantPlayerInfo:
 | 
			
		||||
  is_idle: bool = False
 | 
			
		||||
 | 
			
		||||
  is_party_owner: bool = False
 | 
			
		||||
  max_party_size: int = 0
 | 
			
		||||
  party_size: int = 0
 | 
			
		||||
  party_state: str = None
 | 
			
		||||
 | 
			
		||||
  queue_type: str = None
 | 
			
		||||
  game_state: str = None
 | 
			
		||||
 | 
			
		||||
  map: str = None
 | 
			
		||||
  team: str = None
 | 
			
		||||
  score: int = 0
 | 
			
		||||
  enemy_score: int = 0
 | 
			
		||||
 | 
			
		||||
@dataclass
 | 
			
		||||
class RiotPlayerInfo:
 | 
			
		||||
  uuid: str
 | 
			
		||||
  name: str
 | 
			
		||||
  tag: str
 | 
			
		||||
  state: str
 | 
			
		||||
 | 
			
		||||
  valorant: ValorantPlayerInfo = None
 | 
			
		||||
 | 
			
		||||
class ValorantLocalClient:
 | 
			
		||||
  lockfile_path = os.path.join(os.environ['LOCALAPPDATA'], r'Riot Games\Riot Client\Config\lockfile')
 | 
			
		||||
  poll_interval = 0.5
 | 
			
		||||
 | 
			
		||||
  def __init__(self, callback: Callable[[RiotPlayerInfo], None]):
 | 
			
		||||
    self.callback = callback
 | 
			
		||||
 | 
			
		||||
    self.port, self.password = None, None
 | 
			
		||||
    self.running = True
 | 
			
		||||
    self.players = {}
 | 
			
		||||
 | 
			
		||||
  def _load_credentials(self):
 | 
			
		||||
    if not os.path.exists(self.lockfile_path):
 | 
			
		||||
      return False
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
      with open(self.lockfile_path) as f:
 | 
			
		||||
        for line in f:
 | 
			
		||||
          if line.startswith('Riot Client'):
 | 
			
		||||
            client, pid, self.port, self.password, proto = line.rstrip().split(':')
 | 
			
		||||
            assert proto == 'https'
 | 
			
		||||
            return True
 | 
			
		||||
    except FileNotFoundError:
 | 
			
		||||
      return False
 | 
			
		||||
 | 
			
		||||
    return False
 | 
			
		||||
 | 
			
		||||
  def _do_poll(self):
 | 
			
		||||
    try:
 | 
			
		||||
      resp = requests.get(
 | 
			
		||||
        f'https://127.0.0.1:{self.port}/chat/v4/presences',
 | 
			
		||||
        auth=('riot', self.password), verify=False)
 | 
			
		||||
      resp.raise_for_status()
 | 
			
		||||
      data = resp.json()
 | 
			
		||||
    except (requests.HTTPError, requests.ConnectionError) as ex:
 | 
			
		||||
      # Most likely one of these means the Riot Client shut down
 | 
			
		||||
      log.error(f'Failed to make request to Riot Client: {ex}')
 | 
			
		||||
      return False
 | 
			
		||||
 | 
			
		||||
    for p in data['presences']:
 | 
			
		||||
      if p['product'] != 'valorant':
 | 
			
		||||
        continue
 | 
			
		||||
 | 
			
		||||
      v = json.loads(base64.b64decode(p.pop('private'))) # Valorant-specfic
 | 
			
		||||
      # pprint(p)
 | 
			
		||||
      # pprint(v)
 | 
			
		||||
 | 
			
		||||
      val_info = ValorantPlayerInfo(
 | 
			
		||||
        is_idle=v['isIdle'], is_party_owner=v['isPartyOwner'],
 | 
			
		||||
        max_party_size=v['maxPartySize'], party_size=v['partySize'],
 | 
			
		||||
        party_state=v['partyState'],
 | 
			
		||||
        queue_type=v['queueId'] or None, game_state=v['sessionLoopState'],
 | 
			
		||||
        map=v['matchMap'] or None,
 | 
			
		||||
        score=v['partyOwnerMatchScoreAllyTeam'], enemy_score=v['partyOwnerMatchScoreEnemyTeam'])
 | 
			
		||||
      match v['partyOwnerMatchCurrentTeam']:
 | 
			
		||||
        case 'Red':
 | 
			
		||||
          val_info.team = 'attackers'
 | 
			
		||||
        case 'Blue':
 | 
			
		||||
          val_info.team = 'defenders'
 | 
			
		||||
 | 
			
		||||
      info = RiotPlayerInfo(p['puuid'], p['game_name'], p['game_tag'], p['state'], valorant=val_info)
 | 
			
		||||
 | 
			
		||||
      last = self.players.get(info.uuid)
 | 
			
		||||
      if info != last:
 | 
			
		||||
        self.players[info.uuid] = info
 | 
			
		||||
        self.callback(info)
 | 
			
		||||
 | 
			
		||||
    return True
 | 
			
		||||
 | 
			
		||||
  def run(self):
 | 
			
		||||
    while self.running:
 | 
			
		||||
      if self.password is None:
 | 
			
		||||
        if not self._load_credentials():
 | 
			
		||||
          time.sleep(self.poll_interval)
 | 
			
		||||
          continue
 | 
			
		||||
        log.info('Detected Riot Client credentials, starting polling')
 | 
			
		||||
 | 
			
		||||
      if not self._do_poll():
 | 
			
		||||
        self.port, self.password = None, None
 | 
			
		||||
 | 
			
		||||
      time.sleep(self.poll_interval)
 | 
			
		||||
 | 
			
		||||
  def stop(self):
 | 
			
		||||
    self.running = False
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user