Adding new Discord RPC Package

This commit is contained in:
gascarcella 2022-09-03 20:29:14 -03:00 committed by Mathieu Comandon
parent afb2c4d9f6
commit b85fc83073
8 changed files with 115 additions and 141 deletions

View file

@ -1,99 +0,0 @@
"""Discord integration"""
# Standard Library
import asyncio
import time
# Lutris Modules
from lutris.util.log import logger
try:
from pypresence import Presence as PyPresence
from pypresence.exceptions import PyPresenceException
except ImportError:
PyPresence = None
PyPresenceException = None
class DiscordPresence(object):
"""Provide rich presence integration with Discord for games"""
def __init__(self):
self.available = bool(PyPresence)
self.game_name = ""
self.runner_name = ""
self.last_rpc = 0
self.rpc_interval = 60
self.presence_connected = False
self.rpc_client = None
self.client_id = None
def connect(self):
"""Make sure we are actually connected before trying to send requests"""
if not self.presence_connected:
self.rpc_client = PyPresence(self.client_id)
try:
self.rpc_client.connect()
self.presence_connected = True
except (ConnectionError, FileNotFoundError):
logger.error("Could not connect to Discord")
return self.presence_connected
def disconnect(self):
"""Ensure we are definitely disconnected and fix broken event loop from pypresence
That method is a huge mess of non-deterministic bs and should be nuked from orbit.
"""
if self.rpc_client:
try:
self.rpc_client.close()
except Exception as e:
logger.exception("Unable to close Discord RPC connection: %s", e)
if self.rpc_client.sock_writer is not None:
try:
self.rpc_client.sock_writer.close()
except Exception:
logger.exception("Sock writer could not be closed.")
try:
logger.debug("Forcefully closing event loop.")
self.rpc_client.loop.close()
except Exception:
logger.debug("Could not close event loop.")
try:
logger.debug("Forcefully replacing event loop.")
self.rpc_client.loop = None
asyncio.set_event_loop(asyncio.new_event_loop())
except Exception as e:
logger.exception("Could not replace event loop: %s", e)
try:
logger.debug("Forcefully deleting RPC client.")
self.rpc_client = None
except Exception as ex:
logger.exception(ex)
self.rpc_client = None
self.presence_connected = False
def update_discord_rich_presence(self):
"""Dispatch a request to Discord to update presence"""
if int(time.time()) - self.rpc_interval < self.last_rpc:
logger.debug("Not enough time since last RPC")
return
self.last_rpc = int(time.time())
if not self.connect():
return
try:
self.rpc_client.update(details="Playing %s" % self.game_name,
large_image="large_image",
large_text=self.game_name,
small_image="small_image")
except PyPresenceException as ex:
logger.error("Unable to update Discord: %s", ex)
def clear_discord_rich_presence(self):
"""Dispatch a request to Discord to clear presence"""
if self.connect():
try:
self.rpc_client.clear()
except PyPresenceException as ex:
logger.error("Unable to clear Discord: %s", ex)
self.disconnect()

View file

@ -1,42 +0,0 @@
import base64
import json
import requests
def set_discord_status(token, status):
"""Set a custom status for a user referenced by its token"""
if not token:
return
payload = json.dumps({"custom_status": {"text": status}})
super_properties_raw = (
'{"os":"Linux","browser":"Firefox","device":"","system_locale":"en-US",'
'"browser_user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0",'
'"browser_version":"102.0","os_version":"","referrer":"","referring_domain":"",'
'"referrer_current":"","referring_domain_current":"","release_channel":"stable",'
'"client_build_number":135341,"client_event_source":null}'
)
headers = {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate, br",
"Accept-Language": "en-US",
"Alt-Used": "discord.com",
"Authorization": token,
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"Content-Length": str(len(payload)),
"Content-Type": "application/json",
"Host": "discord.com",
"Origin": "https://discord.com",
"Pragma": "no-cache",
"Referer": "",
"Sec-Fetch-Dest": "empty",
"Sec-Fetch-Mode": "no-cors",
"Sec-Fetch-Site": "same-origin",
"TE": "trailers",
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0",
"X-Debug-Options": "bugReporterEnabled",
"X-Discord-Locale": "en-US",
"X-Super-Properties": base64.b64encode(super_properties_raw.encode('utf-8'))
}
return requests.patch("https://discord.com/api/v9/users/@me/settings", payload, headers=headers)

View file

@ -0,0 +1,3 @@
__all__ = ['client']
from .rpc import client

View file

@ -0,0 +1,30 @@
"""
Discord Rich Presence Base Objects
"""
from abc import ABCMeta
class DiscordRichPresenceBase(metaclass=ABCMeta):
"""
Discord Rich Presence Interface
"""
def update(self, game_identifier: str) -> None:
raise NotImplementedError()
def clear(self) -> None:
raise NotImplementedError()
class DiscordRPCNull(DiscordRichPresenceBase):
"""
Null client for disabled Discord RPC
"""
def update(self, game_identifier: str) -> None:
pass
def clear(self) -> None:
pass

View file

@ -0,0 +1,45 @@
from pypresence import Presence
from lutris.util.discord.base import DiscordRichPresenceBase
from lutris.util.discord.games import GAMES_IDS
from lutris.util.log import logger
class DiscordRichPresenceClient(DiscordRichPresenceBase):
playing: str | None # Identifier of the running game
rpc: Presence | None # Presence Object
def __init__(self):
self.playing = None
self.rpc = None
def update(self, game_identifier: str) -> None:
logger.debug(f"Updating Discord RPC to game {game_identifier}")
if game_identifier not in GAMES_IDS:
logger.error(f"Discord APP ID for {game_identifier} not found")
return
elif self.rpc is not None:
# Clear the old RPC before creating a new one
self.clear()
# Create a new Presence object with the desired app id
self.rpc = Presence(GAMES_IDS[game_identifier])
# Connect to discord endpoint
self.rpc.connect()
# Trigger an update making the status available
self.rpc.update()
# Internal Reference for Game Identifier
self.playing = game_identifier
def clear(self) -> None:
logger.debug("Clearing Discord RPC")
if self.rpc is None:
# Skip already deleted rpc
return
# Clear and Close Presence connection
self.rpc.clear()
self.rpc.close()
# Clear Presence Object
self.rpc = None
# Clear Internal Reference
self.playing = None

View file

@ -0,0 +1,18 @@
__all__ = ['GAMES_IDS']
import os
import json
from lutris.util import datapath
from lutris.util.log import logger
_GAME_IDS_PATH = os.path.join(datapath.get(), 'discord')
_GAME_IDS_JSON = os.path.join(_GAME_IDS_PATH, 'games-ids.json')
if not os.path.exists(_GAME_IDS_JSON):
logger.exception("game-ids.json for Discord Rich Presence not found")
GAME_IDS = {}
else:
with open(_GAME_IDS_JSON, 'r') as games_json:
GAMES_IDS = json.load(games_json)

View file

@ -0,0 +1,18 @@
"""
Discord Rich Presence Loader
This will enable DiscordRichPresenceClient if pypresence is installed.
Otherwise, it will provide a dummy client that does nothing
"""
from lutris.util.discord.base import DiscordRPCNull
try:
from lutris.util.discord.client import DiscordRichPresenceClient
except ImportError:
# If PyPresence is not present, and ImportError will raise, so we provide dummy client
client = DiscordRPCNull()
else:
# PyPresence is present, so we provide the client
client = DiscordRichPresenceClient()

File diff suppressed because one or more lines are too long