How it works (and snippets) - floxay/py-valorant-rich-presence GitHub Wiki
Basically how it works:
- Reads the lockfile
- Sends a GET request to
https://127.0.0.1:{port}/chat/v1/session
to get the player's PUUID - Sends a GET request to
https://127.0.0.1:{port}/chat/v4/presences
and filters out the player using their PUUID to set the user's Discord status on startup - Connects to the locally running websocket and subscribes to presence event to update the Discord status when needed.
The process is pretty similar to how one would connect to and communicate with the LCU websocket, if you are familiar with that feel free to scroll down to the next 'Horizontal Rule' :D
1.)
To connect to the websocket you need the password (or auth key, I'll call it password) and the port, you can get those by reading the lockfile which gets re-generated each time you start the Riot Client, meaning; the password changes every time you start the game and there is a HIGH chance that the port will change as well.
You can find the lockfile under this location; %LOCALAPPDATA%\Riot Games\Riot Client\Config
The file is literally called lockfile
without extension
To read it you can do the following in Python:
lockfilePath = os.path.join(os.getenv('LOCALAPPDATA'), R'Riot Games\Riot Client\Config\lockfile')
with open(lockfilePath) as lockfile:
data = lockfile.read().split(':')
keys = ['name', 'PID', 'port', 'password', 'protocol']
return(dict(zip(keys, data)))
It will return you all the data as a dictionary.
2.)
Next is getting the currently logged in user's PUUID:
headers['Authorization'] = 'Basic ' + base64.b64encode(('riot:' + lockfile['password']).encode()).decode()
response = requests.get(endpoints['session'].format(port=lockfile['port']), headers=headers, verify=False)
return(response.json()['puuid'])
3.)
Next is sending another GET request so we can set the user's Discord status without waiting for an event:
headers['Authorization'] = 'Basic ' + base64.b64encode(('riot:' + lockfile['password']).encode()).decode()
response = requests.get(endpoints['presences'].format(port=lockfile['port']), headers=headers, verify=False)
presences = response.json()
for presence in presences['presences']:
if presence['puuid'] == user['puuid']: # compares to the PUUID we got in the previous response to aviod setting someone else's state as our status
update_RPC(presence['private']) # a method which takes a string, decodes it using base64, loads it into a dict,
# does 'magic' and then updates Discord presence using PyPresence
4.)
You can connect to the websocket using the following:
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE
async def listen():
async with websockets.connect(f'wss://riot:{lockfile["password"]}@localhost:{lockfile["port"]}', ssl=ssl_context) as websocket:
await websocket.send('[5, "OnJsonApiEvent_chat_v4_presences"]') # subscribing to presence event
while True:
response = json.loads(await websocket.recv())
if response[2]['data']['presences'][0]['puuid'] == user['puuid']: # comparing PUUIDs again for the same reason
update_RPC(response[2]['data']['presences'][0]['private'])
update_RPC(str)
is a function in my code which does most of the work.
I'm passing the value of the 'private' key of a presence to it. That value is a base64 encoded string which if you decoded you can load into a dictionary work relatively easily with that.
def update_RPC(state):
data = json.loads(base64.b64decode(state))
Why am I saying that it's relatively easy to work with?
Well, there is no official documentation to it at all plus some things just does not make sense (at least to me)...
Below you can see what you get if you decode and load that data into a dict, and values I've encountered so far ([?] means that I'm unsure if it is still used or not);
{
"isValid": bool
"sessionLoopState": "MENUS", "PREGAME", "INGAME"
"partyOwnerSessionLoopState": "MENUS", "PREGAME", "INGAME"
"customGameName": "TeamOne", "TeamTwo", "TeamSpectate"
"customGameTeam": "TeamOne", "TeamTwo", "TeamSpectate"
"partyOwnerMatchMap": "/Game/Maps/Ascent/Ascent", "/Game/Maps/Duality/Duality", "/Game/Maps/Triad/Triad", "/Game/Maps/Port/Port", "/Game/Maps/Bonsai/Bonsai", "/Game/Maps/Poveglia/Range" (possibly more, use the official content api to get maps)
"partyOwnerMatchCurrentTeam": "Blue", "Red"
"partyOwnerMatchScoreAllyTeam": int
"partyOwnerMatchScoreEnemyTeam": int
"partyOwnerProvisioningFlow": "CustomGame", "SkillTest", "Matchmaking", "NewPlayerExperience", "Invalid" [?]
"provisioningFlow": "CustomGame", "SkillTest", "Matchmaking", "NewPlayerExperience", "Invalid" [?]
"matchMap": "/Game/Maps/Ascent/Ascent", "/Game/Maps/Duality/Duality", "/Game/Maps/Triad/Triad", "/Game/Maps/Port/Port", "/Game/Maps/Bonsai/Bonsai", "/Game/Maps/Poveglia/Range"
"partyId": UUID
"isPartyOwner": bool
"partyName": empty str
"partyState": "DEFAULT", "CUSTOM_GAME_SETUP" [?] "CUSTOM_GAME_STARTING", "MATCHMAKING", "STARTING_MATCHMAKING", "LEAVING_MATCHMAKING", "MATCHMADE_GAME_STARTING", "SOLO_EXPERIENCE_STARTING"
"partyAccessibility": "CLOSED", "OPEN"
"maxPartySize": int
"queueId": "spikerush", "competitive", "deathmatch", "unrated", "snowball" or empty str (custom game)
"partyLFM": bool
"partyClientVersion": "release-02.01-shipping-6-511946"
"partySize": int
"partyVersion": unix epoch timestamp
"queueEntryTime": "2021.01.01-01.01.01"
"playerCardId": UUID
"playerTitleId": UUID
"isIdle": bool (away state)
}
So yeah, 'queueId' being an empty string when playing custom games is one of the things which is weird...
Now you can do whatever you want with this data.
I went with the nice 'if tree' solution, setting everything in a dict, then a nice dict comprehension to .lower() every value where the key contains the string 'image' as Discord assets are lowercase only, eventually passing this dict as keyword arguments with RPC.update(**data)
, if you are going to use PyPresence then I feel like you should definitely do this too.
Additionnaly you can get further information about the agent select and match (current agent, mode etc..) using endpoints like: https://glz-{shard}-1.{region}.a.pvp.net/pregame/v1/players/{PUUID}
and https://glz-{shard}-1.{region}.a.pvp.net/pregame/v1/matches/{matchID}
but these require different headers and auth so I'm not using these.
PS: You can make a GET request to https://127.0.0.1:{port}/help
and receive all available events, functions and types. I have uploaded the response to the repo, you can find it as help.json