|  |  | @@ -0,0 +1,206 @@ | 
		
	
		
			
			|  |  |  | # References: | 
		
	
		
			
			|  |  |  | # * https://github.com/devsnek/discord-rpc/tree/master/src/transports/IPC.js | 
		
	
		
			
			|  |  |  | # * https://github.com/devsnek/discord-rpc/tree/master/example/main.js | 
		
	
		
			
			|  |  |  | # * https://github.com/discordapp/discord-rpc/tree/master/documentation/hard-mode.md | 
		
	
		
			
			|  |  |  | # * https://github.com/discordapp/discord-rpc/tree/master/src | 
		
	
		
			
			|  |  |  | # * https://discordapp.com/developers/docs/rich-presence/how-to#updating-presence-update-presence-payload-fields | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | from abc import ABCMeta, abstractmethod | 
		
	
		
			
			|  |  |  | import json | 
		
	
		
			
			|  |  |  | import logging | 
		
	
		
			
			|  |  |  | import os | 
		
	
		
			
			|  |  |  | import socket | 
		
	
		
			
			|  |  |  | import sys | 
		
	
		
			
			|  |  |  | import struct | 
		
	
		
			
			|  |  |  | import uuid | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | OP_HANDSHAKE = 0 | 
		
	
		
			
			|  |  |  | OP_FRAME = 1 | 
		
	
		
			
			|  |  |  | OP_CLOSE = 2 | 
		
	
		
			
			|  |  |  | OP_PING = 3 | 
		
	
		
			
			|  |  |  | OP_PONG = 4 | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | logger = logging.getLogger(__name__) | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | class DiscordIpcError(Exception): | 
		
	
		
			
			|  |  |  | pass | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | class DiscordIpcClient(metaclass=ABCMeta): | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | """Work with an open Discord instance via its JSON IPC for its rich presence API. | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | In a blocking way. | 
		
	
		
			
			|  |  |  | Classmethod `for_platform` | 
		
	
		
			
			|  |  |  | will resolve to one of WinDiscordIpcClient or UnixDiscordIpcClient, | 
		
	
		
			
			|  |  |  | depending on the current platform. | 
		
	
		
			
			|  |  |  | Supports context handler protocol. | 
		
	
		
			
			|  |  |  | """ | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | def __init__(self, client_id): | 
		
	
		
			
			|  |  |  | self.client_id = client_id | 
		
	
		
			
			|  |  |  | self._connect() | 
		
	
		
			
			|  |  |  | self._do_handshake() | 
		
	
		
			
			|  |  |  | logger.info("connected via ID %s", client_id) | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | @classmethod | 
		
	
		
			
			|  |  |  | def for_platform(cls, client_id, platform=sys.platform): | 
		
	
		
			
			|  |  |  | if platform == 'win32': | 
		
	
		
			
			|  |  |  | return WinDiscordIpcClient(client_id) | 
		
	
		
			
			|  |  |  | else: | 
		
	
		
			
			|  |  |  | return UnixDiscordIpcClient(client_id) | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | @abstractmethod | 
		
	
		
			
			|  |  |  | def _connect(self): | 
		
	
		
			
			|  |  |  | pass | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | def _do_handshake(self): | 
		
	
		
			
			|  |  |  | ret_op, ret_data = self.send_recv({'v': 1, 'client_id': self.client_id}, op=OP_HANDSHAKE) | 
		
	
		
			
			|  |  |  | # {'cmd': 'DISPATCH', 'data': {'v': 1, 'config': {...}}, 'evt': 'READY', 'nonce': None} | 
		
	
		
			
			|  |  |  | if ret_op == OP_FRAME and ret_data['cmd'] == 'DISPATCH' and ret_data['evt'] == 'READY': | 
		
	
		
			
			|  |  |  | return | 
		
	
		
			
			|  |  |  | else: | 
		
	
		
			
			|  |  |  | if ret_op == OP_CLOSE: | 
		
	
		
			
			|  |  |  | self.close() | 
		
	
		
			
			|  |  |  | raise RuntimeError(ret_data) | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | @abstractmethod | 
		
	
		
			
			|  |  |  | def _write(self, date: bytes): | 
		
	
		
			
			|  |  |  | pass | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | @abstractmethod | 
		
	
		
			
			|  |  |  | def _recv(self, size: int) -> bytes: | 
		
	
		
			
			|  |  |  | pass | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | def _recv_header(self) -> (int, int): | 
		
	
		
			
			|  |  |  | header = self._recv_exactly(8) | 
		
	
		
			
			|  |  |  | return struct.unpack("<II", header) | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | def _recv_exactly(self, size) -> bytes: | 
		
	
		
			
			|  |  |  | buf = b"" | 
		
	
		
			
			|  |  |  | size_remaining = size | 
		
	
		
			
			|  |  |  | while size_remaining: | 
		
	
		
			
			|  |  |  | chunk = self._recv(size_remaining) | 
		
	
		
			
			|  |  |  | buf += chunk | 
		
	
		
			
			|  |  |  | size_remaining -= len(chunk) | 
		
	
		
			
			|  |  |  | return buf | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | def close(self): | 
		
	
		
			
			|  |  |  | logger.warning("closing connection") | 
		
	
		
			
			|  |  |  | try: | 
		
	
		
			
			|  |  |  | self.send({}, op=OP_CLOSE) | 
		
	
		
			
			|  |  |  | finally: | 
		
	
		
			
			|  |  |  | self._close() | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | @abstractmethod | 
		
	
		
			
			|  |  |  | def _close(self): | 
		
	
		
			
			|  |  |  | pass | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | def __enter__(self): | 
		
	
		
			
			|  |  |  | return self | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | def __exit__(self, *_): | 
		
	
		
			
			|  |  |  | self.close() | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | def send_recv(self, data, op=OP_FRAME): | 
		
	
		
			
			|  |  |  | self.send(data, op) | 
		
	
		
			
			|  |  |  | return self.recv() | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | def send(self, data, op=OP_FRAME): | 
		
	
		
			
			|  |  |  | logger.debug("sending %s", data) | 
		
	
		
			
			|  |  |  | data_str = json.dumps(data, separators=(',', ':')) | 
		
	
		
			
			|  |  |  | data_bytes = data_str.encode('utf-8') | 
		
	
		
			
			|  |  |  | header = struct.pack("<II", op, len(data_bytes)) | 
		
	
		
			
			|  |  |  | self._write(header) | 
		
	
		
			
			|  |  |  | self._write(data_bytes) | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | def recv(self) -> (int, "JSON"): | 
		
	
		
			
			|  |  |  | """Receives a packet from discord. | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | Returns op code and payload. | 
		
	
		
			
			|  |  |  | """ | 
		
	
		
			
			|  |  |  | op, length = self._recv_header() | 
		
	
		
			
			|  |  |  | payload = self._recv_exactly(length) | 
		
	
		
			
			|  |  |  | data = json.loads(payload.decode('utf-8')) | 
		
	
		
			
			|  |  |  | logger.debug("received %s", data) | 
		
	
		
			
			|  |  |  | return op, data | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | def set_activity(self, act): | 
		
	
		
			
			|  |  |  | # act | 
		
	
		
			
			|  |  |  | data = { | 
		
	
		
			
			|  |  |  | 'cmd': 'SET_ACTIVITY', | 
		
	
		
			
			|  |  |  | 'args': {'pid': os.getpid(), | 
		
	
		
			
			|  |  |  | 'activity': act}, | 
		
	
		
			
			|  |  |  | 'nonce': str(uuid.uuid4()) | 
		
	
		
			
			|  |  |  | } | 
		
	
		
			
			|  |  |  | self.send(data) | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | class WinDiscordIpcClient(DiscordIpcClient): | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | _pipe_pattern = R'\\?\pipe\discord-ipc-{}' | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | def _connect(self): | 
		
	
		
			
			|  |  |  | for i in range(10): | 
		
	
		
			
			|  |  |  | path = self._pipe_pattern.format(i) | 
		
	
		
			
			|  |  |  | try: | 
		
	
		
			
			|  |  |  | self._f = open(path, "w+b") | 
		
	
		
			
			|  |  |  | except OSError as e: | 
		
	
		
			
			|  |  |  | logger.error("failed to open {!r}: {}".format(path, e)) | 
		
	
		
			
			|  |  |  | else: | 
		
	
		
			
			|  |  |  | break | 
		
	
		
			
			|  |  |  | else: | 
		
	
		
			
			|  |  |  | return DiscordIpcError("Failed to connect to Discord pipe") | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | self.path = path | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | def _write(self, data: bytes): | 
		
	
		
			
			|  |  |  | self._f.write(data) | 
		
	
		
			
			|  |  |  | self._f.flush() | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | def _recv(self, size: int) -> bytes: | 
		
	
		
			
			|  |  |  | return self._f.read(size) | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | def _close(self): | 
		
	
		
			
			|  |  |  | self._f.close() | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | class UnixDiscordIpcClient(DiscordIpcClient): | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | def _connect(self): | 
		
	
		
			
			|  |  |  | self._sock = socket.socket(socket.AF_UNIX) | 
		
	
		
			
			|  |  |  | pipe_pattern = self._get_pipe_pattern() | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | for i in range(10): | 
		
	
		
			
			|  |  |  | path = pipe_pattern.format(i) | 
		
	
		
			
			|  |  |  | if not os.path.exists(path): | 
		
	
		
			
			|  |  |  | continue | 
		
	
		
			
			|  |  |  | try: | 
		
	
		
			
			|  |  |  | self._sock.connect(path) | 
		
	
		
			
			|  |  |  | except OSError as e: | 
		
	
		
			
			|  |  |  | logger.error("failed to open {!r}: {}".format(path, e)) | 
		
	
		
			
			|  |  |  | else: | 
		
	
		
			
			|  |  |  | break | 
		
	
		
			
			|  |  |  | else: | 
		
	
		
			
			|  |  |  | return DiscordIpcError("Failed to connect to Discord pipe") | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | @staticmethod | 
		
	
		
			
			|  |  |  | def _get_pipe_pattern(): | 
		
	
		
			
			|  |  |  | env_keys = ('XDG_RUNTIME_DIR', 'TMPDIR', 'TMP', 'TEMP') | 
		
	
		
			
			|  |  |  | for env_key in env_keys: | 
		
	
		
			
			|  |  |  | dir_path = os.environ.get(env_key) | 
		
	
		
			
			|  |  |  | if dir_path: | 
		
	
		
			
			|  |  |  | break | 
		
	
		
			
			|  |  |  | else: | 
		
	
		
			
			|  |  |  | dir_path = '/tmp' | 
		
	
		
			
			|  |  |  | return os.path.join(dir_path, 'discord-ipc-{}') | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | def _write(self, data: bytes): | 
		
	
		
			
			|  |  |  | self._sock.sendall(data) | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | def _recv(self, size: int) -> bytes: | 
		
	
		
			
			|  |  |  | return self._sock.recv(size) | 
		
	
		
			
			|  |  |  | 
 | 
		
	
		
			
			|  |  |  | def _close(self): | 
		
	
		
			
			|  |  |  | self._sock.close() |