summaryrefslogtreecommitdiff
path: root/discordrpc/presence.py
diff options
context:
space:
mode:
Diffstat (limited to 'discordrpc/presence.py')
-rw-r--r--discordrpc/presence.py252
1 files changed, 252 insertions, 0 deletions
diff --git a/discordrpc/presence.py b/discordrpc/presence.py
new file mode 100644
index 0000000..858d847
--- /dev/null
+++ b/discordrpc/presence.py
@@ -0,0 +1,252 @@
+import json
+import os
+import socket
+import sys
+import struct
+import time
+from .exceptions import *
+
+
+OP_HANDSHAKE = 0
+OP_FRAME = 1
+OP_CLOSE = 2
+OP_PING = 3
+OP_PONG = 4
+
+
+class RPC:
+ def __init__(self, client_id):
+ self.client_id = client_id
+ self._connect()
+ self._do_handshake()
+ self._output = None
+ self.show_output = False
+
+
+ @classmethod
+ def set_id(self, app_id):
+ app_id = str(app_id)
+ platform = sys.platform
+ if platform == 'win32':
+ return DiscordWindows(app_id)
+ else:
+ return DiscordUnix(app_id)
+
+
+ def _connect(self):
+ pass
+
+
+ def _do_handshake(self):
+ op, data = self.send_recv({'v': 1, 'client_id': self.client_id}, op=OP_HANDSHAKE)
+ if op == OP_FRAME and data['cmd'] == 'DISPATCH' and data['evt'] == 'READY':
+ return
+ else:
+ if op == OP_CLOSE:
+ self.close()
+ if data['code'] == 4000:
+ raise InvalidID()
+ raise RPCException(f"Unknown error : {data}")
+
+
+ def _write(self, data:bytes):
+ pass
+
+ def _recv(self, size:int) -> bytes:
+ pass
+
+ def _recv_header(self):
+ header = self._recv_exact(8)
+ return struct.unpack("<II", header)
+
+ def _recv_exact(self, size_remaining) -> bytes:
+ buf = b""
+ while size_remaining:
+ chunk = self._recv(size_remaining)
+ buf += chunk
+ size_remaining -= len(chunk)
+ return buf
+
+ def close(self):
+ try:
+ self.send({}, op=OP_CLOSE)
+ finally:
+ self._close()
+
+
+ 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):
+ data = json.dumps(data, separators=(',', ':')).encode('UTF-8')
+ header = struct.pack("<II", op, len(data))
+ self._write(header)
+ self._write(data)
+
+ def recv(self):
+ op, length = self._recv_header()
+ payload = self._recv_exact(length)
+ data = json.loads(payload.decode('UTF-8'))
+ return op, data
+
+ timestamp = time.mktime(time.localtime())
+
+ def set_activity(
+ self,
+ state:str=None,
+ details:str=None,
+ timestamp=None,
+ small_text:str='null',
+ large_text:str='null',
+ small_image:str='null',
+ large_image:str='null',
+ buttons=None
+ ):
+
+ if len(large_text) <= 1:
+ raise Error('"large text" must be at least above 1 characters')
+
+ if len(small_text) <= 1:
+ raise Error('"small text" must be at least above 1 characters')
+
+ act = {
+ "state": state,
+ "details": details,
+ "timestamps": {
+ "start": timestamp
+ },
+ "assets": {
+ "small_text": small_text,
+ "large_text": large_text,
+ "small_image": str(small_image),
+ "large_image": str(large_image)
+ },
+ "buttons": buttons,
+ }
+
+ if state == None or state == '' or state == 'null':
+ act.pop('state', None)
+ if details == None or details == '' or details == 'null':
+ act.pop('details', None)
+ if small_text == None or small_text == '' or small_text == 'null':
+ act['assets'].pop('small_text', None)
+ if large_text == None or large_text == '' or large_text == 'null':
+ act['assets'].pop('large_text', None)
+ if timestamp == None:
+ act.pop('timestamps', None)
+ if buttons == None:
+ act.pop('buttons', None)
+
+
+ data = {
+ 'cmd': 'SET_ACTIVITY',
+ 'args': {
+ 'pid': os.getpid(),
+ 'activity': act
+ },
+ 'nonce': '{:.20f}'.format(time.time())
+ }
+
+ self.send(data)
+
+ op, length = self._recv_header()
+ payload = self._recv_exact(length)
+ output = json.loads(payload.decode('UTF-8'))
+ if output['evt'] == 'ERROR':
+ raise ActivityError
+
+ else:
+ if self.show_output==True:print(f"Successfully set RPC for {self.client_id}")
+
+ return op, output
+
+
+
+ def run(self):
+ try:
+ while True:
+ time.sleep(1)
+ except KeyboardInterrupt:
+ self.close()
+
+
+
+class DiscordWindows(RPC):
+ _pipe_path = R'\\?\pipe\discord-ipc-{}'
+
+ def _connect(self):
+ for i in range(10):
+ path = self._pipe_path.format(i)
+
+ try:
+ self._open = open(path, "w+b")
+ except OSError as e:
+ raise Error("Failed to open {!r}: {}".format(path, e))
+ else:
+ break
+
+ else:
+ raise DiscordNotOpened
+
+ self.path = path
+
+ def _write(self, data:bytes):
+ self._open.write(data)
+ self._open.flush()
+
+ def _recv(self, size:int) -> bytes:
+ return self._open.read(size)
+
+ def _close(self):
+ self._open.close()
+
+
+
+class DiscordUnix(RPC):
+ def _connect(self):
+ self._socket = socket.socket(socket.AF_UNIX)
+ pipe_path = self._get_pipe_path()
+
+ for i in range(10):
+ path = pipe_path.format(i)
+ if not os.path.exists(path):
+ continue
+ try:
+ self._socket.connect(path)
+ except OSError as e:
+ raise Error("Failed to open {!r}: {}".format(path, e))
+ else:
+ break
+ else:
+ raise DiscordNotOpened
+
+
+ def _get_pipe_path():
+ 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._socket.sendall(data)
+
+ def _recv(self, size:int) -> bytes:
+ return self._socket.recv(size)
+
+ def _close(self):
+ self._socket.close()