mirror of
https://github.com/lutris/lutris
synced 2024-09-15 22:09:55 +00:00
Adding new Discord RPC Package
This commit is contained in:
parent
afb2c4d9f6
commit
b85fc83073
|
@ -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()
|
|
@ -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)
|
3
lutris/util/discord/__init__.py
Normal file
3
lutris/util/discord/__init__.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
__all__ = ['client']
|
||||
|
||||
from .rpc import client
|
30
lutris/util/discord/base.py
Normal file
30
lutris/util/discord/base.py
Normal 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
|
45
lutris/util/discord/client.py
Normal file
45
lutris/util/discord/client.py
Normal 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
|
18
lutris/util/discord/games.py
Normal file
18
lutris/util/discord/games.py
Normal 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)
|
18
lutris/util/discord/rpc.py
Normal file
18
lutris/util/discord/rpc.py
Normal 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()
|
1
share/lutris/discord/games-ids.json
Normal file
1
share/lutris/discord/games-ids.json
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue