summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJesusaves <cpntb1@ymail.com>2024-02-05 18:35:33 -0300
committerJesusaves <cpntb1@ymail.com>2024-02-05 18:35:33 -0300
commitd6d698227b4c1ba1004d371ab5fb35ec8024b5da (patch)
treef7f59ab57e081aaa8e493e1324edabca1dc4c5ae
parent29ffe5de3c308013742b5bd97f7d75b09bd3b427 (diff)
downloadtkinter-d6d698227b4c1ba1004d371ab5fb35ec8024b5da.tar.gz
tkinter-d6d698227b4c1ba1004d371ab5fb35ec8024b5da.tar.bz2
tkinter-d6d698227b4c1ba1004d371ab5fb35ec8024b5da.tar.xz
tkinter-d6d698227b4c1ba1004d371ab5fb35ec8024b5da.zip
Update discord_rpc to 4.0
https://pypi.org/project/discord-rpc
-rwxr-xr-xdiscord_rpc/__init__.py919
-rw-r--r--discord_rpc/codes/__init__.py0
-rw-r--r--discord_rpc/codes/errorcodes.py3
-rw-r--r--discord_rpc/codes/opcodes.py5
-rw-r--r--discord_rpc/codes/statecodes.py4
-rw-r--r--discord_rpc/connection/__init__.py0
-rw-r--r--discord_rpc/connection/ipc.py387
-rw-r--r--discord_rpc/connection/rpc.py175
-rw-r--r--discord_rpc/util/__init__.py0
-rw-r--r--discord_rpc/util/backoff.py35
-rw-r--r--discord_rpc/util/limits.py32
-rw-r--r--discord_rpc/util/types.py349
-rw-r--r--discord_rpc/util/utils.py180
-rw-r--r--discordrpc/__init__.py9
-rw-r--r--discordrpc/button.py38
-rw-r--r--discordrpc/exceptions.py29
-rw-r--r--discordrpc/presence.py252
17 files changed, 328 insertions, 2089 deletions
diff --git a/discord_rpc/__init__.py b/discord_rpc/__init__.py
deleted file mode 100755
index 73f111c..0000000
--- a/discord_rpc/__init__.py
+++ /dev/null
@@ -1,919 +0,0 @@
-from __future__ import absolute_import, print_function
-"""
-Python Rich Presence library for Discord
-"""
-from .util.backoff import Backoff
-from copy import deepcopy
-import logging
-from threading import Lock, Thread
-from .connection.rpc import RpcConnection
-from .util.utils import get_process_id, is_callable, iter_items, iter_keys, is_python3, bytes, unicode, is_linux, \
- is_windows, get_executable_path
-from .util.types import Int32, Int64
-import json
-import time
-try:
- from Queue import Queue
- from Queue import Empty as QueueEmpty
-except ImportError:
- try:
- from queue import Queue
- from queue import Empty as QueueEmpty
- except ImportError:
- # we somehow can't import either python Queue's
- # create a fake Queue class that'll do nothing
- # and without killing the program
- from .util.utils import DummyQueue as Queue
- from .util.utils import Empty as QueueEmpty
-from os import path, makedirs
-if not is_python3():
- import requests
-else:
- from urllib.request import urlopen, Request
-from os import environ, system
-from sys import stderr
-if is_windows():
- if is_python3():
- import winreg
- else:
- import _winreg as winreg
-
-
-VERSION = "1.3.0"
-PROJECT_URL = "https://gitlab.com/somberdemise/discord-rpc.py"
-
-DISCORD_REPLY_NO = 0
-DISCORD_REPLY_YES = 1
-DISCORD_REPLY_IGNORE = 2
-
-
-_discord_rpc = None
-_auto_update_connection = False
-_update_thread = None
-_connection_lock = Lock()
-_http_rate_limit = None
-
-
-class _DiscordRpc(object):
- connection = None
- _time_call = time.time
- _just_connected = False
- _just_disconnected = False
- _got_error = False
- _was_joining = False
- _was_spectating = False
- _spectate_secret = ''
- _join_secret = ''
- _last_err = [0, '']
- _last_disconnect = [0, '']
- __presence_lock = Lock()
- __handler_lock = Lock()
- __send_queue = Queue(8)
- __join_ask_queue = Queue(8)
- __connected_user = {
- 'id': None,
- 'username': None,
- 'discriminator': None,
- 'avatar': None,
- }
- __current_presence = None
- __pid = 0
- __nonce = 1
- __reconnect_time = Backoff(500, 60000)
- __next_connect = None
- __callbacks = {
- 'ready': None,
- 'disconnected': None,
- 'joinGame': None,
- 'spectateGame': None,
- 'joinRequest': None,
- 'error': None,
- }
- __registered_handlers = {
- 'joinGame': False,
- 'spectateGame': False,
- 'joinRequest': False,
- }
- __http_rate_limit = None
-
- def __init__(self, app_id, pid=None, pipe_no=0, log=True, logger=None, log_file=None, log_level=logging.INFO,
- callbacks=None):
- if pid is not None:
- if not isinstance(pid, int):
- raise TypeError('PID must be of int type!')
- self.__pid = pid
- else:
- self.__pid = get_process_id()
- if callbacks:
- self.set_callbacks(**callbacks)
- if self.connection is not None:
- return
-
- self.connection = RpcConnection(app_id, pipe_no=pipe_no, log=log, logger=logger, log_file=log_file,
- log_level=log_level)
- self.connection.on_connect = self._on_connect
- self.connection.on_disconnect = self._on_disconnect
-
- def shutdown(self):
- if self.connection is None:
- self._log('debug', 'Connection hasn\'t been established or recently shutdown; ignoring.')
- return
- self.connection.on_connect = None
- self.connection.on_disconnect = None
- self.__callbacks = {
- 'ready': None,
- 'disconnected': None,
- 'joinGame': None,
- 'spectateGame': None,
- 'joinRequest': None,
- 'error': None,
- }
- self.connection.destroy()
-
- def run_callbacks(self):
- if self.connection is None:
- return
-
- was_disconnected = self._just_disconnected
- self._just_disconnected = False
- is_connected = self.connection.is_open
-
- if is_connected:
- with self.__handler_lock:
- if was_disconnected:
- self._run_callback('disconnected', self._last_disconnect[0], self._last_disconnect[1])
-
- if self._just_connected:
- with self.__handler_lock:
- self._run_callback('ready', self.current_user)
- self._just_connected = False
-
- if self._got_error:
- with self.__handler_lock:
- self._run_callback('error', self._last_err[0], self._last_err[1])
- self._got_error = False
-
- if self._was_joining:
- with self.__handler_lock:
- self._run_callback('joinGame', self._join_secret)
- self._was_joining = False
-
- if self._was_spectating:
- with self.__handler_lock:
- self._run_callback('spectateGame', self._spectate_secret)
- self._was_spectating = False
-
- users = list()
- with self.__handler_lock:
- if not self.__join_ask_queue.empty():
- # instead of trying to use a guesstimated value (qsize),
- # grab data w/o blocking, and break when it raises the "empty" error
- while True:
- try:
- user = self.__join_ask_queue.get(False)
- users.append(deepcopy(user))
- # just in case
- self.__join_ask_queue.task_done()
- except QueueEmpty:
- break
- if len(users) > 0:
- self._run_callback('joinRequest', users)
- self._log('debug', "Users requesting to join: {}".format(users))
- else:
- self._log('debug', "No users requesting to join game.")
-
- if not is_connected and was_disconnected:
- with self.__handler_lock:
- self._run_callback('disconnected', self._last_disconnect[0], self._last_disconnect[1])
-
- def update_connection(self):
- if self.connection is None:
- return
-
- if not self.connection.is_open:
- if self.__next_connect is None:
- self.__next_connect = self.time_now()
- if self.time_now() >= self.__next_connect:
- self.update_reconnect_time()
- self._log('debug', 'Next connection in {} seconds.'.format(self.__next_connect - self.time_now()))
- self.connection.open()
- else:
- # reads
- while True:
- did_read, data = self.connection.read()
- if not did_read:
- break
- evt = data.get('evt')
- nonce = data.get('nonce')
- if nonce is not None:
- err_data = data.get('data', dict())
- if evt is not None and evt == 'ERROR':
- self._last_err = [err_data.get('code', 0), err_data.get('message', '')]
- self._got_error = True
- else:
- if evt is None:
- self._log('debug', 'No event sent by Discord.')
- continue
- read_data = data.get('data', dict())
- if evt == 'ACTIVITY_JOIN':
- self._join_secret = read_data.get('secret', '')
- self._was_joining = True
- elif evt == 'ACTIVITY_SPECTATE':
- self._spectate_secret = read_data.get('secret', '')
- self._was_spectating = True
- elif evt == 'ACTIVITY_JOIN_REQUEST':
- user = read_data.get('user', None)
- uid = user.get('id', None)
- uname = user.get('username', None)
- discrim = user.get('discriminator', None)
- if any(x is None for x in (uid, uname, discrim)):
- # something is wrong with Discord (or we need to update this library)
- self._log('warning', 'Discord failed to send required data for join request!')
- continue
- # the avatar hash to grab from https://cdn.discordapp.com/
- # can be None/empty string
- avatar = user.get('avatar', None)
- if not self.__join_ask_queue.full():
- user_data = {'id': uid,
- 'username': uname,
- 'discriminator': discrim,
- 'avatar': avatar}
- self.__join_ask_queue.put(user_data)
- # writes
- if self.__current_presence is not None and len(self.__current_presence) > 0:
- with self.__presence_lock:
- if self.connection.write(self.__current_presence):
- # only erase if we successfully wrote
- self.__current_presence = None
- self._log('debug', 'Wrote presence data to IPC.')
- if not self.__send_queue.empty():
- # instead of trying to use a guesstimated value (qsize),
- # grab data w/o blocking, and break when it raises the "empty" error
- while True:
- try:
- # attempt to grab data
- sdata = self.__send_queue.get(False)
- # apparently, we don't care for the write results, according to Discord's library
- self.connection.write(sdata)
- # just in case
- self.__send_queue.task_done()
- except QueueEmpty:
- self._log('debug', 'Wrote queue of send data to IPC.')
- break
-
- def __presence_to_json(self, **kwargs):
- """Creates json rich presence info."""
- # See https://discordapp.com/developers/docs/rich-presence/how-to#updating-presence-update-presence-payload
- # for max values for payload
- if self.pid <= 0:
- raise AttributeError('PID is required for payload!')
- rp = dict()
- rp['cmd'] = 'SET_ACTIVITY'
- rp['args'] = dict()
- # I don't really know why PID is required, but alright
- rp['args']['pid'] = int(self.pid)
-
- rp['args']['activity'] = dict()
-
- state = kwargs.get('state', None)
- if state is not None and len(state) > 0:
- rp['args']['activity']['state'] = str(state[:128])
-
- details = kwargs.get('details', None)
- if details is not None and len(details) > 0:
- rp['args']['activity']['details'] = str(details[:128])
-
- # NOTE: Sending endTimestamp will always have the time displayed as "remaining" until the given time.
- # Sending startTimestamp will show "elapsed" as long as there is no endTimestamp sent.
- start_timestamp = kwargs.get('start_timestamp', None)
- end_timestamp = kwargs.get('end_timestamp', None)
- # we do int calculations beforehand, in case it turns out that the number ends up as 0
- if start_timestamp is not None:
- start_timestamp = Int64(int(start_timestamp)).get_number()
- if end_timestamp is not None:
- end_timestamp = Int64(int(end_timestamp)).get_number()
- if any(x for x in (start_timestamp, end_timestamp)):
- rp['args']['activity']['timestamps'] = dict()
- # start_timestamp != None && start_timestamp != 0
- if start_timestamp:
- rp['args']['activity']['timestamps']['start'] = start_timestamp
- # same for end
- if end_timestamp:
- rp['args']['activity']['timestamps']['end'] = end_timestamp
-
- large_image_key = kwargs.get('large_image_key', None)
- large_image_text = kwargs.get('large_image_text', None)
- small_image_key = kwargs.get('small_image_key', None)
- small_image_text = kwargs.get('small_image_text', None)
- if any(x is not None and len(x) > 0 for x in
- (large_image_key, large_image_text, small_image_key, small_image_text)):
- rp['args']['activity']['assets'] = dict()
- if large_image_key is not None and len(large_image_key) > 0:
- rp['args']['activity']['assets']['large_image'] = str(large_image_key[:128]).lower()
- if large_image_text is not None and len(large_image_text) > 0:
- rp['args']['activity']['assets']['large_text'] = str(large_image_text[:128])
- if small_image_key is not None and len(small_image_key) > 0:
- rp['args']['activity']['assets']['small_image'] = str(small_image_key[:128]).lower()
- if small_image_text is not None and len(small_image_text) > 0:
- rp['args']['activity']['assets']['small_text'] = str(small_image_text[:128])
-
- party_id = kwargs.get('party_id', None)
- party_size = kwargs.get('party_size', None)
- party_max = kwargs.get('party_max', None)
- # we do int calculations beforehand, in case it turns out that the number ends up as 0
- if party_size is not None:
- party_size = Int32(int(party_size)).get_number()
- if party_max is not None:
- party_max = Int32(int(party_max)).get_number()
- # for some reason, Discord does partySize || partyMax, but then requires partySize & partyMax > 0...
- # we shall correct that by throwing an all()
- if (party_id is not None and len(party_id) > 0) or all(x for x in (party_size, party_max)):
- rp['args']['activity']['party'] = dict()
- if party_id is not None and len(party_id) > 0:
- rp['args']['activity']['party']['id'] = str(party_id[:128])
- if party_size and party_max:
- rp['args']['activity']['party']['size'] = [party_size, party_max]
-
- # match_secret = kwargs.get('match_secret', None) # deprecated
- join_secret = kwargs.get('join_secret', None)
- spectate_secret = kwargs.get('spectate_secret', None)
- # if any(x is not None and len(x) > 0 for x in (match_secret, join_secret, spectate_secret)):
- if any(x is not None and len(x) > 0 for x in (join_secret, spectate_secret)):
- rp['args']['activity']['secrets'] = dict()
- # if match_secret is not None and len(match_secret) > 0:
- # rp['args']['secrets']['match'] = str(match_secret[:128])
- if join_secret is not None and len(join_secret) > 0:
- rp['args']['activity']['secrets']['join'] = str(join_secret[:128])
- if spectate_secret is not None and len(spectate_secret) > 0:
- rp['args']['activity']['secrets']['spectate'] = str(spectate_secret[:128])
-
- # rp['args']['instance'] = bool(kwargs.get('instance', False)) # deprecated
- rp['nonce'] = str(self.nonce)
- self.__nonce += 1
-
- self._log('debug', 'Presence data to be written: {}'.format(rp))
- return json.dumps(rp)
-
- def update_presence(self, **kwargs):
- """
- :param kwargs: kwargs must consist of any of the following:
- (optional) state (string)
- (optional) details (string)
- (optional) start_timestamp (int)
- (optional) end_timestamp (int)
- (optional) large_image_key (string, lowercase)
- (optional) large_image_text (string)
- (optional) small_image_key (string, lowercase)
- (optional) small_image_text (string)
- (optional) party_id (string)
- (optional) party_size (int), party_max (int) (both are required if using either)
- (optional) join_secret (string)
- (optional) spectate_secret (string)
- Note: see here https://discordapp.com/developers/docs/rich-presence/how-to#updating-presence
- Note 2: We do not use deprecated parameters at this time
- :return: N/A
- """
- json_data = self.__presence_to_json(**kwargs)
- with self.__presence_lock:
- self.__current_presence = json_data
-
- def clear_presence(self):
- self.update_presence()
-
- def respond(self, user_id, reply):
- if self.connection is None or not self.connection.is_open:
- self._log('warning', 'Cannot reply to discord user {}; connection not established!'.format(user_id))
- return
- response = dict()
- if reply == DISCORD_REPLY_YES:
- cmd = 'SEND_ACTIVITY_JOIN_INVITE'
- else:
- cmd = 'CLOSE_ACTIVITY_JOIN_REQUEST'
- response['cmd'] = cmd
- response['args'] = dict()
- response['args']['user_id'] = str(user_id)
- response['nonce'] = str(self.nonce)
- self.__nonce += 1
- if not self.__send_queue.full():
- self.__send_queue.put(json.dumps(response))
- self._log('debug', 'Queued reply: {}'.format(response))
- else:
- self._log('warning', 'Cannot reply to discord user {}; send queue is full!')
-
- def last_error(self):
- return self._last_err[0], self._last_err[1]
-
- def last_disconnect(self):
- return self._last_disconnect[0], self._last_disconnect[1]
-
- def update_reconnect_time(self):
- current_time = self.time_now()
- delay = self.__reconnect_time.next_delay() / 1000
- self.__next_connect = current_time + delay
- self._log('debug', 'Updating next connect to {}. Current time: {}, delay: {}'.format(self.__next_connect,
- current_time,
- delay))
-
- def set_callbacks(self, **kwargs):
- for name, callback_info in iter_items(kwargs):
- self.set_callback(name, callback_info)
-
- def set_callback(self, callback_name, callback):
- callback_name = callback_name.strip()
- if callback_name in ('ready', 'disconnected', 'joinGame', 'spectateGame', 'joinRequest', 'error'):
- if callback and not is_callable(callback):
- raise TypeError('Callback must be callable! Callback name: {}, callback: {}'.format(callback_name,
- callback))
- self.__callbacks[callback_name] = callback
-
- def update_handlers(self):
- for handler in iter_keys(self.__registered_handlers):
- if handler == 'joinGame':
- event = 'ACTIVITY_JOIN'
- elif handler == 'spectateGame':
- event = 'ACTIVITY_SPECTATE'
- elif handler == 'joinRequest':
- event = 'ACTIVITY_JOIN_REQUEST'
- else:
- # unknown handler
- self._log('warning', 'Unknown handler name "{}".'.format(handler))
- continue
- if not self.__registered_handlers[handler] and self.__callbacks[handler] is not None:
- if not self.__register_event(event):
- self._log('warning', 'Unable to register event "{}"'.format(event))
- else:
- self._log('info', "Registered handler {}".format(handler))
- elif self.__registered_handlers[handler] and self.__callbacks[handler] is None:
- if not self.__unregister_event(event):
- self._log('warning', 'Unable to unregister event "{}"'.format(event))
- else:
- self._log('debug', 'Unregistered event {}'.format(event))
-
- def _run_callback(self, callback_name, *args):
- callback_name = callback_name.strip()
- if callback_name in self.__callbacks:
- if self.__callbacks[callback_name] is not None:
- callback = self.__callbacks[callback_name]
- if len(args) > 0:
- callback(*args)
- else:
- callback()
- else:
- self._log('debug', 'No callback set for event "{}"'.format(callback_name))
- else:
- self._log('debug', 'No such event name "{}"'.format(callback_name))
-
- def _log(self, *args):
- if self.connection is not None:
- self.connection.log(*args)
-
- def _on_connect(self, data):
- self.update_handlers()
- self._log('debug', 'Data received: {}'.format(data))
- user_data = data.get('data', None)
- if user_data is not None:
- user = user_data.get('user', None)
- uid = user.get('id', None)
- uname = user.get('username', None)
- if any(x is None for x in (uid, uname)):
- self._log('warning', 'Discord failed to send current user data.')
- else:
- discrim = user.get('discriminator', None) # i'm not sure why this can be None for current user, but
- # not others...
- avatar = user.get('avatar', None)
- self.__connected_user['id'] = uid
- self.__connected_user['username'] = uname
- self.__connected_user['discriminator'] = discrim
- self.__connected_user['avatar'] = avatar
- self._log('debug', 'Current discord user: {}'.format(self.__connected_user))
- else:
- self._log('warning', 'Discord failed to send current user data.')
- self._just_connected = True
- self.__reconnect_time.reset()
-
- def _on_disconnect(self, err, msg):
- self._just_disconnected = True
- self._last_disconnect = [err, msg]
- self.__registered_handlers['joinGame'] = False
- self.__registered_handlers['joinRequest'] = False
- self.__registered_handlers['spectateGame'] = False
- self.update_reconnect_time()
-
- def __register_event(self, event):
- data = dict()
- data['nonce'] = str(self.nonce)
- self.__nonce += 1
- data['cmd'] = 'SUBSCRIBE'
- data['evt'] = event
- if not self.__send_queue.full():
- self.__send_queue.put(json.dumps(data))
- return True
- return False
-
- def __unregister_event(self, event):
- data = dict()
- data['nonce'] = str(self.nonce)
- self.__nonce += 1
- data['cmd'] = 'UNSUBSCRIBE'
- data['evt'] = event
- if not self.__send_queue.full():
- self.__send_queue.put(json.dumps(data))
- return True
- return False
-
- @property
- def got_error(self):
- return self._got_error
-
- @property
- def was_joining(self):
- return self._was_joining
-
- @property
- def was_spectating(self):
- return self._was_spectating
-
- @property
- def current_user(self):
- return deepcopy(self.__connected_user)
-
- @property
- def spectate_secret(self):
- return self._spectate_secret
-
- @property
- def join_secret(self):
- return self._join_secret
-
- @property
- def pid(self):
- return self.__pid
-
- @property
- def nonce(self):
- return self.__nonce
-
- @property
- def time_now(self):
- return self._time_call
-
- @time_now.setter
- def time_now(self, callback):
- if is_callable(callback):
- self._time_call = callback
- else:
- self._log('warning', 'time_now must be callable!')
-
- @property
- def app_id(self):
- if self.connection is not None:
- return self.connection.app_id
- else:
- return '0xDEADBEEF'
-
-
-class _UpdateConnection(Thread):
- def run(self):
- global _discord_rpc
- global _connection_lock
- while True:
- time.sleep(1)
- with _connection_lock:
- if _discord_rpc is None:
- # we have shut down, break and return
- break
- _discord_rpc.update_connection()
- _discord_rpc.run_callbacks()
-
-
-def initialize(app_id, pid=None, callbacks=None, pipe_no=0, time_call=None, auto_update_connection=False,
- log=True, logger=None, log_file=None, log_level=logging.INFO,
- auto_register=False, steam_id=None, command=None):
- """
- Initializes and connects to the Discord Rich Presence RPC
- :param app_id: The Client ID from Discord (see https://github.com/discordapp/discord-rpc#basic-usage)
- (NOTE: Must be a string)
- :param pid: The main program ID (is automatically set if not passed)
- :param callbacks: The callbacks and any extra args to run when events are fired ('ready', 'disconnected',
- 'joinGame', 'spectateGame',
- 'joinRequest', 'error')
- :param time_call: The time function to call for epoch seconds (defaults to time.time())
- :param auto_update_connection: Do you want the library to automagically update the connection for you?
- (defaults to False)
- :param log: Do we want to use logging for the RPC connection (defaults to True)
- :param logger: Your own logger to use (must be already set up) (defaults to automatically setting one up
- internally)
- :param log_file: The location of where the log file should reside (defaults to stdout only, ignored if
- rpc_logger is used)
- :param log_level: The log level to use (defaults to logging.INFO)
- :param pipe_no: The pipe number to use in the RPC connection (must be 0-10, default 0)
- :param auto_register: Do you want us to auto-register your program (defaults to False) (NOTE: currently does
- nothing)
- :param steam_id: The applications steam ID for auto-register (defaults to regular program registration, or
- nothing if auto_register is False) (NOTE: Also does nothing currently)
- :param command: The command to use for protocol registration (ex: /path/to/file --discord)
- :return: N/A
- """
- global _discord_rpc
- global _auto_update_connection
- global _update_thread
-
- if _discord_rpc is not None:
- # don't initialize more than once
- return
- if auto_register:
- register_game(app_id=app_id, steam_id=steam_id, command=command)
- _discord_rpc = _DiscordRpc(app_id, pid=pid, pipe_no=pipe_no, log=log, logger=logger, log_file=log_file,
- log_level=log_level, callbacks=callbacks)
- if time_call is not None:
- _discord_rpc.time_now = time_call
-
- if auto_update_connection:
- _auto_update_connection = True
- _update_thread = _UpdateConnection()
- _update_thread.start()
-
-
-def shutdown():
- """
- Shuts down the Discord Rich Presence connection
- :return: N/A
- """
- global _discord_rpc
- global _auto_update_connection
- global _connection_lock
- if _discord_rpc is not None:
- with _connection_lock:
- _discord_rpc.shutdown()
- # make sure user/programmer doesn't try to call stuff afterwards on 'discord_rpc'
- _discord_rpc = None
- if _auto_update_connection and _update_thread is not None:
- _update_thread.join()
- # always set to False
- _auto_update_connection = False
-
-
-def run_callbacks():
- """
- Runs the rich presence callbacks
- :return: N/A
- """
- global _discord_rpc
- if _discord_rpc is not None:
- _discord_rpc.run_callbacks()
-
-
-def update_connection():
- """
- Updates the rich presence connection
- :return: N/A
- """
- global _discord_rpc
- global _auto_update_connection
- if _discord_rpc is not None and not _auto_update_connection:
- _discord_rpc.update_connection()
-
-
-def update_presence(**kwargs):
- """
- :param kwargs: kwargs must consist of any of the following:
- (optional) state (string)
- (optional) details (string)
- (optional) start_timestamp (int)
- (optional) end_timestamp (int)
- (optional) large_image_key (string, lowercase)
- (optional) large_image_text (string)
- (optional) small_image_key (string, lowercase)
- (optional) small_image_text (string)
- (optional) party_id (string)
- (optional) party_size (int), party_max (int) (both are required if using either)
- (optional) join_secret (string)
- (optional) spectate_secret (string)
- Note: see here https://discordapp.com/developers/docs/rich-presence/how-to#updating-presence
- Note 2: We do not use deprecated parameters at this time
- :return: N/A
- """
- global _discord_rpc
- if _discord_rpc is not None:
- _discord_rpc.update_presence(**kwargs)
-
-
-def clear_presence():
- """
- Clears the rich presence data last sent
- :return: N/A
- """
- global _discord_rpc
- if _discord_rpc is not None:
- _discord_rpc.clear_presence()
-
-
-def respond(user_id, response):
- """
- Respond to a discord user
- :param user_id: The Discord user's snowflake ID (the '64 char' long one or so)
- :param response: The response to send to the user (one of type DISCORD_REPLY_NO, DISCORD_REPLY_YES,
- DISCORD_REPLY_IGNORE)
- :return: N/A
- """
- global _discord_rpc
- if _discord_rpc is not None:
- _discord_rpc.respond(user_id, response)
-
-
-def download_profile_picture(user_id, discriminator, avatar_hash=None, cache_dir="cache", default_dir="default",
- cert_file=None, game_name=None, game_version=None, game_url=None):
- """
- Download a discord user's profile picture.
- :param user_id: The discord user's ID
- :param discriminator: The discord user's discriminator; required and used for when avatar_hash is None
- :param avatar_hash: (optional) The discord user's avatar hash. NOTE: if None, defaults to a default avatar image
- :param cache_dir: (optional) Path to store the profile picture
- :param default_dir: (optional) The path within the cache_dir to use for default avatars
- param cert_file: (optional) The path to the cacert file to use
- :param game_name: (optional) The name of the game that is running
- :param game_version: (optional) The game's version number
- :param game_url: (optional) The game's website
- :return: Path to profile picture, or None
- """
- global _http_rate_limit
- if avatar_hash is None:
- url = "https://cdn.discordapp.com/embed/avatars/{}.png".format(int(discriminator) % 5)
- # NOTE: we default to "./cache/default/" if no path specified
- # NOTE 2: we use a "default" directory to save disk space and download calls in the long run
- download_folder = path.join(cache_dir, default_dir)
- else:
- url = "https://cdn.discordapp.com/avatars/{}/{}.jpg?size=2048".format(user_id, avatar_hash)
- # NOTE: we default to "./cache/user_id/" if no path specified
- download_folder = path.join(cache_dir, user_id)
- if not path.exists(download_folder):
- makedirs(download_folder, 0o755)
- if avatar_hash is not None:
- avatar_file = path.join(download_folder, avatar_hash) + '.jpg'
- else:
- avatar_file = path.join(download_folder, str(int(discriminator) % 5)) + '.png'
- if path.exists(avatar_file):
- # technically, we downloaded it, so no need to worry about downloading
- return avatar_file
- # we check this after just in case we already have a cached image
- if _http_rate_limit is not None:
- if not _http_rate_limit > time.time():
- return None
- # we're required to have a ua string
- ua_str = "discord-rpc.py ({url}, {version})".format(url=PROJECT_URL, version=VERSION)
- if game_name is not None and isinstance(game_name, (bytes, unicode)) and game_name.strip() != '':
- # if we have a game name, append that
- ua_str += ' {}'.format(game_name)
- if all((x is not None and isinstance(x, (bytes, unicode)) and x.strip() != '') for x in (game_version,
- game_url)):
- # if we have both a url and version number, append those too
- ua_str += " ({url}, {version}".format(url=game_url, version=game_version)
- headers = {'User-Agent': ua_str}
- if is_python3():
- if cert_file is not None:
- r = Request(
- url,
- data=None,
- headers=headers,
- cafile=cert_file
- )
- else:
- r = Request(
- url,
- data=None,
- headers=headers
- )
- req = urlopen(r)
- status_code = req.getcode()
- else:
- if cert_file is not None:
- req = requests.get(url, headers=headers, verify=cert_file)
- else:
- req = requests.get(url, headers=headers)
- status_code = req.status_code
- if status_code != 200:
- if status_code == 404:
- # nonexistent avatar/hash; return None
- return None
- if 'X-RateLimit-Reset' in req.headers:
- _http_rate_limit = int(req.headers['X-RateLimit-Reset'])
- else:
- try:
- if is_python3():
- data = req.read()
- json_data = json.loads(data.decode(req.info().get_content_charset('utf-8')))
- else:
- json_data = req.json()
- if 'retry_after' in json_data:
- _http_rate_limit = time.time() + (int(json_data['retry_after']) / 1000.0)
- except Exception:
- pass
- if _http_rate_limit is None:
- # try again in 15 min (Discord shouldn't kill us for waiting 15 min anyways...)
- _http_rate_limit = time.time() + (15 * 60)
- return None
- with open(avatar_file, 'wb') as f:
- if is_python3():
- f.write(req.read())
- else:
- f.write(req.content)
- return avatar_file
-
-
-def register_game(app_id, steam_id=None, command=None):
- """
- Registers a protocol Discord can use to run your game.
- :param app_id: The Client ID from Discord (see https://github.com/discordapp/discord-rpc#basic-usage)
- (NOTE: Must be a string)
- :param steam_id: The applications steam ID for auto-register (defaults to regular program registration)
- :param command: The command to use for protocol registration (ex: /path/to/file --discord)
- :return:
- """
- if command is None and steam_id is None and (is_windows() or is_linux()):
- command = get_executable_path()
- if is_linux():
- # linux is the easiest
- if steam_id:
- command = "xdg-open steam://rungameid/{}".format(steam_id)
- home = environ.get('HOME')
- if home is None or home.strip() == '':
- # no home? no registration!
- return
- file_contents = "[Desktop Entry]\nName=Game {app_id}\nExec={command} %u\nType=Application\n" + \
- "NoDisplay=true\nCategories=Discord;Games;\nMimeType=x-scheme-handler/discord-{app_id};\n"
- file_contents = file_contents.format(app_id=app_id, command=command)
- if home.endswith('/'):
- home = home[:-1]
- # we don't really need to here, but oh well
- path_location = path.join(home, ".local", "share", "applications")
- if not path.exists(path_location):
- makedirs(path_location, 0o700)
- with open(path.join(path_location, "discord-{}.desktop".format(app_id)), 'w') as f:
- f.write(file_contents)
- sys_call = "xdg-mime default discord-{0}.desktop x-scheme-handler/discord-{0}".format(app_id)
- if system(sys_call) < 0:
- print("Failed to register mime handler!", file=stderr)
- elif is_windows():
- def read_key(reg_path, name):
- try:
- root_key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, reg_path, 0, winreg.KEY_READ)
- value, reg_type = winreg.QueryValueEx(root_key, name)
- winreg.CloseKey(root_key)
- return value
- except WindowsError:
- return None
-
- def write_key(reg_path, name, value):
- try:
- # I know this can return a key if it exists, but oh well
- winreg.CreateKey(winreg.HKEY_CURRENT_USER, reg_path)
- root_key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, reg_path, 0, winreg.KEY_WRITE)
- winreg.SetValueEx(root_key, name, 0, winreg.REG_SZ, value)
- winreg.CloseKey(root_key)
- return True
- except WindowsError:
- return False
-
- if steam_id:
- tmp = read_key(r"Software\Valve\Steam", "SteamExe")
- if tmp is not None and tmp.strip() != '':
- command = "\"{}\" steam://rungameid/{}".format(tmp.replace("/", "\\"), steam_id)
-
- protocol_desc = "URL:Run game {} protocol".format(app_id)
- protocol_path = r"Software\Classes\{}".format("discord-{}".format(app_id))
- if not write_key(protocol_path, None, protocol_desc):
- # failed to write the key
- print("Error writing description!", file=stderr)
- if not write_key(protocol_path, "URL Protocol", None):
- print("Error writing description!", file=stderr)
- if not write_key(protocol_path + r"\DefaultIcon", None, get_executable_path()):
- print("Error writing key!", file=stderr)
- if not write_key(protocol_path + r"\shell\open\command", None, command):
- print("Error writing command!", file=stderr)
- else:
- # assume Mac OSX here
- def register_url(aid):
- # TODO: figure out a feasable way to get this to work
- print("Url registration under Mac OSX unimplemented. Cannot create for app ID {}".format(aid), file=stderr)
-
- def register_command(aid, cmd):
- home = path.expanduser("~")
- if home is None or home.strip() == '':
- return
- discord_path = path.join(home, "Library", "Application Support", "discord", "games")
- if not path.exists(discord_path):
- makedirs(discord_path)
- with open(path.join(discord_path, "{}.json".format(aid)), 'w') as f:
- f.write("{\"command\": \"{}\"}".format(cmd))
-
- if steam_id:
- command = "steam://rungameid/{}".format(steam_id)
- if command:
- register_command(app_id, command)
- else:
- register_url(app_id)
-
-
-__all__ = ['DISCORD_REPLY_NO', 'DISCORD_REPLY_YES', 'DISCORD_REPLY_IGNORE', 'initialize', 'shutdown', 'run_callbacks',
- 'update_connection', 'update_presence', 'clear_presence', 'respond', 'VERSION', 'PROJECT_URL',
- 'download_profile_picture', 'register_game']
diff --git a/discord_rpc/codes/__init__.py b/discord_rpc/codes/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/discord_rpc/codes/__init__.py
+++ /dev/null
diff --git a/discord_rpc/codes/errorcodes.py b/discord_rpc/codes/errorcodes.py
deleted file mode 100644
index d501301..0000000
--- a/discord_rpc/codes/errorcodes.py
+++ /dev/null
@@ -1,3 +0,0 @@
-Success = 0
-PipeClosed = 1
-ReadCorrupt = 2
diff --git a/discord_rpc/codes/opcodes.py b/discord_rpc/codes/opcodes.py
deleted file mode 100644
index 29c3baf..0000000
--- a/discord_rpc/codes/opcodes.py
+++ /dev/null
@@ -1,5 +0,0 @@
-Handshake = 0
-Frame = 1
-Close = 2
-Ping = 3
-Pong = 4
diff --git a/discord_rpc/codes/statecodes.py b/discord_rpc/codes/statecodes.py
deleted file mode 100644
index 51f1a46..0000000
--- a/discord_rpc/codes/statecodes.py
+++ /dev/null
@@ -1,4 +0,0 @@
-Disconnected = 0
-SentHandshake = 1
-AwaitingResponse = 2
-Connected = 3
diff --git a/discord_rpc/connection/__init__.py b/discord_rpc/connection/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/discord_rpc/connection/__init__.py
+++ /dev/null
diff --git a/discord_rpc/connection/ipc.py b/discord_rpc/connection/ipc.py
deleted file mode 100644
index bbb26a7..0000000
--- a/discord_rpc/connection/ipc.py
+++ /dev/null
@@ -1,387 +0,0 @@
-from __future__ import absolute_import
-import errno
-import logging
-from ..codes import errorcodes
-from ..util.utils import is_windows, range, get_temp_path, to_bytes, bytes, to_unicode, is_python3, is_callable
-import struct
-import sys
-if is_windows():
- # we're going to have to do some ugly things, because Windows sucks
- import ctypes
- GENERIC_READ = 0x80000000
- GENERIC_WRITE = 0x40000000
- OPEN_EXISTING = 0x3
- INVALID_HANDLE_VALUE = -1
- PIPE_READMODE_MESSAGE = 0x2
- ERROR_FILE_NOT_FOUND = 0x2
- ERROR_PIPE_BUSY = 0xE7
- ERROR_MORE_DATA = 0xEA
- BUFSIZE = 512
-else:
- try:
- from socket import MSG_NOSIGNAL
- _msg_flags = MSG_NOSIGNAL
- except ImportError:
- _msg_flags = 0
- try:
- from socket import SO_NOSIGPIPE
- _do_sock_opt = True
- except ImportError:
- _do_sock_opt = False
- import socket
- import fcntl
- from os import O_NONBLOCK
-
-
-class BaseConnection(object):
- """Generate IPC Connection handler."""
- # *nix specific
- __sock = None
- # Windows specific
- __pipe = None
-
- __open = False
- __logger = None
- __is_logging = False
-
- def __init__(self, log=True, logger=None, log_file=None, log_level=logging.INFO):
- if not isinstance(log, bool):
- raise TypeError('log must be of bool type!')
- if log:
- if logger is not None:
- # Panda3D notifies are similar, so we simply check if we can make the same calls as logger
- if not hasattr(logger, 'debug'):
- raise TypeError('logger must be of type logging!')
- self.__logger = logger
- else:
- self.__logger = logging.getLogger(__name__)
- log_fmt = logging.Formatter('[%(asctime)s][%(levelname)s] ' + '%(name)s - %(message)s')
- if log_file is not None and hasattr(log_file, 'strip'):
- fhandle = logging.FileHandler(log_file)
- fhandle.setLevel(log_level)
- fhandle.setFormatter(log_fmt)
- self.__logger.addHandler(fhandle)
- shandle = logging.StreamHandler(sys.stdout)
- shandle.setLevel(log_level)
- shandle.setFormatter(log_fmt)
- self.__logger.addHandler(shandle)
- self.__is_logging = True
-
- def log(self, callback_name, *args):
- if self.__logger is not None:
- if hasattr(self.__logger, callback_name) and is_callable(self.__logger.__getattribute__(callback_name)):
- self.__logger.__getattribute__(callback_name)(*args)
-
- def __open_pipe(self, pipe_name, log_type='warning'):
- """
- :param pipe_name: the named pipe string
- :param log_type: the log type to use (default 'warning')
- :return: opened(bool), try_again(bool)
- """
- if not is_windows():
- self.log('error', 'Attempted to call a Windows call on a non-Windows OS.')
- return
- pipe = ctypes.windll.kernel32.CreateFileW(pipe_name, GENERIC_READ | GENERIC_WRITE, 0, None, OPEN_EXISTING, 0,
- None)
- if pipe != INVALID_HANDLE_VALUE:
- self.__pipe = pipe
- return True, False
- err = ctypes.windll.kernel32.GetLastError()
- if err == ERROR_FILE_NOT_FOUND:
- self.log(log_type, 'File not found.')
- self.log(log_type, 'Pipe name: {}'.format(pipe_name))
- return False, False
- elif err == ERROR_PIPE_BUSY:
- if ctypes.windll.kernel32.WaitNamedPipeW(pipe_name, 10000) == 0:
- self.log(log_type, 'Pipe busy.')
- return False, False
- else:
- # try again, should be free now
- self.log('debug', 'Pipe was busy, but should be free now. Try again.')
- return False, True
- # some other error we don't care about
- self.log('debug', 'Unknown error: {}'.format(err))
- return False, False
-
- def open(self, pipe_no=None):
- if pipe_no is not None:
- if not isinstance(pipe_no, int):
- raise TypeError('pipe_no must be of type int!')
- if pipe_no not in range(0, 10):
- raise ValueError('pipe_no must be within range (0 <= pipe number < 10)!')
- if is_windows():
- # NOTE: don't forget to use a number after ipc-
- pipe_name = u'\\\\.\\pipe\\discord-ipc-{}'
- if pipe_no is not None:
- # we only care about the first value if pipe_no isn't None
- opened, try_again = self.__open_pipe(pipe_name.format(pipe_no))
- if opened:
- self.__open = True
- self.log('info', 'Connected to pipe {}, as user requested.'.format(pipe_no))
- return
- elif try_again:
- self.open(pipe_no=pipe_no)
- return
- else:
- num = 0
- while True:
- if num >= 10:
- break
- opened, try_again = self.__open_pipe(pipe_name.format(num), log_type='debug')
- if opened:
- self.__open = True
- self.log('debug', 'Automatically connected to pipe {}.'.format(num))
- return
- if try_again:
- continue
- num += 1
- # we failed to get a pipe
- self.__pipe = None
- self.log('warning', 'Could not open a connection.')
- else:
- self.__sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
- if self.__sock is None or self.__sock == -1:
- self.log('warning', 'Could not open socket.')
- self.close()
- return
- try:
- fcntl.fcntl(self.__sock, fcntl.F_SETFL, O_NONBLOCK)
- except Exception as e:
- self.log('warning', e)
- self.close()
- return
- if _do_sock_opt:
- try:
- socket.setsockopt(socket.SOL_SOCKET, SO_NOSIGPIPE)
- except Exception as e:
- self.log('warning', e)
- self.log('debug', 'Attempting to use sock as is. Notify a developer if an error occurs.')
- sock_addr = get_temp_path()
- if sock_addr.endswith('/'):
- sock_addr = sock_addr[:-1]
- sock_addr += '/discord-ipc-{}'
- if pipe_no is not None:
- ret_val = self.__sock.connect_ex(sock_addr.format(pipe_no))
- if ret_val == 0:
- self.__open = True
- self.log('info', 'Connected to socket {}, as user requested.'.format(pipe_no))
- return
- else:
- self.log('warning', 'Could not open socket {}.'.format(pipe_no))
- self.close()
- else:
- for num in range(0, 10):
- ret_val = self.__sock.connect_ex(sock_addr.format(num))
- if ret_val == 0:
- self.__open = True
- self.log('debug', 'Automatically connected to socket {}.'.format(num))
- return
- self.log('warning', 'Could not open socket.')
- self.close()
-
- def write(self, data, opcode):
- if not self.connected():
- self.log('warning', 'Cannot write if we aren\'t connected yet!')
- return False
- if not isinstance(opcode, int):
- raise TypeError('Opcode must be of int type!')
- if data is None:
- data = ''
- try:
- data = to_bytes(data)
- except Exception as e:
- self.log('warning', e)
- return False
- data_len = len(data)
- # the following data must be little endian unsigned ints
- # see: https://github.com/discordapp/discord-rpc/blob/master/documentation/hard-mode.md#notes
- header = struct.pack('<II', opcode, data_len)
- # append header to data
- data = header + data
- # get new data size
- data_len = len(data)
- if self.__pipe is not None:
- written = ctypes.c_ulong(0)
- success = ctypes.windll.kernel32.WriteFile(self.__pipe, ctypes.c_char_p(data), data_len,
- ctypes.byref(written), None)
- if (not success) or (data_len != written.value):
- self.log('warning', 'Failed to write data onto pipe.')
- return False
- return True
- elif self.__sock is not None:
- data_sent = 0
- while data_sent < data_len:
- try:
- sent = self.__sock.send(data[data_sent:], _msg_flags)
- except Exception as e:
- self.log('warning', e)
- return False
- if sent == 0:
- self.log('warning', 'Socket connection broken!')
- if data_sent == 0:
- self.log('warning', 'No data sent; closing connection.')
- self.close()
- return False
- data_sent += sent
- return True
- self.log('warning', 'write() executed code that shouldn\'t have run.')
- return False
-
- def read(self):
- ret_val = [False, None, None]
- if not self.connected():
- self.log('warning', 'Cannot read if we haven\'t opened a connection!')
- return ret_val
- data = bytes()
- header_size = struct.calcsize('<II')
- # (is_successful_read, OpCode, data)
- if self.__pipe is not None:
- available = ctypes.c_ulong(0)
- if not ctypes.windll.kernel32.PeekNamedPipe(self.__pipe, None, 0, None, ctypes.byref(available), None):
- self.log('warning', 'Peek on pipe for header failed.')
- self.close()
- ret_val[2] = [errorcodes.PipeClosed, 'Pipe closed']
- return ret_val
- if available.value < header_size:
- self.log('debug', 'Pipe doesn\'t have enough data to read in header.')
- # assume this is like errno.EAGAIN
- ret_val[2] = [errorcodes.PipeClosed, 'Pipe closed']
- return ret_val
- cb_read = ctypes.c_ulong(0)
- buff = ctypes.create_string_buffer(header_size)
- success = 0
- while not success:
- success = ctypes.windll.kernel32.ReadFile(self.__pipe, buff, header_size, ctypes.byref(cb_read), None)
- if success == 1:
- # we successfully read the HEADER :O
- # Note: we use RAW here, otherwise it'll be a 1 byte kinda weird thing
- header = buff.raw
- break
- elif ctypes.windll.kernel32.GetLastError() != ERROR_MORE_DATA:
- # we don't have more data; close pipe
- self.log('warning', 'Failed to read in header from pipe.')
- self.close()
- ret_val[2] = [errorcodes.PipeClosed, 'Pipe closed']
- return ret_val
- opcode, data_len = struct.unpack('<II', header)
- cb_read = ctypes.c_ulong(0)
- buff = ctypes.create_string_buffer(data_len)
- success = 0
- available = ctypes.c_ulong(0)
- if not ctypes.windll.kernel32.PeekNamedPipe(self.__pipe, None, 0, None, ctypes.byref(available), None):
- self.log('warning', 'Peek on pipe for data failed.')
- self.close()
- ret_val[2] = [errorcodes.ReadCorrupt, 'Partial data in frame']
- return ret_val
- if available.value < data_len:
- self.log('warning', 'Pipe doesn\'t have enough data to read in data.')
- # assume this is like errno.EAGAIN
- ret_val[2] = [errorcodes.ReadCorrupt, 'Partial data in frame']
- return ret_val
- while not success:
- success = ctypes.windll.kernel32.ReadFile(self.__pipe, buff, data_len, ctypes.byref(cb_read), None)
- if success == 1:
- # we successfully read the DATA :O
- ret_val[0] = True
- ret_val[1] = opcode
- # value here actually works okay, so use that
- # Note: raw also seems to work, but meh
- data = buff.value
- break
- elif ctypes.windll.kernel32.GetLastError() != ERROR_MORE_DATA:
- # we don't have more data; close pipe
- self.log('warning', 'Failed to read in data from pipe.')
- self.close()
- ret_val[2] = [errorcodes.ReadCorrupt, 'Partial data in frame']
- return ret_val
- elif self.__sock is not None:
- packets = list()
- while len(bytes().join(packets)) < header_size:
- try:
- packet = self.__sock.recv(header_size - len(bytes().join(packets)))
- except Exception as e:
- ret_val[2] = [errorcodes.PipeClosed, 'Pipe closed']
- if hasattr(e, 'errno'):
- if e.errno == errno.EAGAIN:
- self.log('debug', e)
- self.log('debug', 'errno == EAGAIN')
- return ret_val
- self.log('warning', 'Failed to read in header!')
- self.log('warning', e)
- self.close()
- if packet is None or len(packet) == 0:
- self.log('warning', 'Socket connection broken!')
- if len(bytes().join(packets)) == 0:
- self.log('warning', 'No data sent; closing connection.')
- self.close()
- ret_val[2] = [errorcodes.PipeClosed, 'Pipe closed']
- return ret_val
- packets.append(packet)
- header = bytes().join(packets)
- packets = list()
- opcode, data_len = struct.unpack('<II', header)
- self.log('debug', 'Opcode: {}, data length: {}'.format(opcode, data_len))
- while len(bytes().join(packets)) < data_len:
- try:
- packet = self.__sock.recv(data_len - len(bytes().join(packets)))
- except Exception as e:
- ret_val[2] = [errorcodes.ReadCorrupt, 'Partial data in frame']
- if hasattr(e, 'errno'):
- if e.errno == errno.EAGAIN:
- self.log('debug', e)
- self.log('debug', 'errno == EAGAIN')
- return ret_val
- self.log('warning', 'Failed to read in data!')
- self.log('warning', e)
- if packet is None or len(packet) == 0:
- self.log('warning', 'Socket connection broken!')
- if len(bytes().join(packets)) == 0:
- self.log('warning', 'No data sent; closing connection.')
- self.close()
- ret_val[2] = [errorcodes.ReadCorrupt, 'Partial data in frame']
- return ret_val
- packets.append(packet)
- data = bytes().join(packets)
- ret_val[0] = True
- ret_val[1] = opcode
- if ret_val[0]:
- if is_python3():
- data = to_unicode(data)
- ret_val[2] = data
- self.log('debug', 'Return values: {}'.format(ret_val))
- return ret_val
-
- def close(self):
- # ensure we're using Windows before trying to close a pipe
- # Note: This should **never** execute on a non-Windows machine!
- if self.__pipe is not None and is_windows():
- ctypes.windll.kernel32.CloseHandle(self.__pipe)
- self.__pipe = None
- if self.__sock is not None:
- try:
- self.__sock.shutdown(socket.SHUT_RDWR)
- self.__sock.close()
- except Exception as e:
- self.log('warning', e)
- finally:
- self.__sock = None
- if self.__open:
- self.__open = False
- self.log('debug', 'Closed IPC connection.')
-
- def destroy(self):
- # make sure we close everything
- self.close()
- # if we automatically set our own logger, clean it up
- if self.__is_logging:
- for handle in self.__logger.handlers[:]:
- handle.close()
- self.__logger.removeHandler(handle)
- self.__logger = None
-
- @property
- def is_open(self):
- return self.__open
-
- def connected(self):
- return self.is_open
diff --git a/discord_rpc/connection/rpc.py b/discord_rpc/connection/rpc.py
deleted file mode 100644
index ad11007..0000000
--- a/discord_rpc/connection/rpc.py
+++ /dev/null
@@ -1,175 +0,0 @@
-from __future__ import absolute_import
-import logging
-import json
-from ..codes import errorcodes
-from ..codes import opcodes
-from ..codes import statecodes
-from ..util.utils import is_callable, json2dict, range
-from .ipc import BaseConnection
-
-
-_RPC_VERSION = 1
-
-
-class RpcConnection(object):
- _connection = None
- _state = statecodes.Disconnected
- _app_id = None
- _last_err_code = 0
- _last_err_msg = ''
- _pipe_no = 0
- _on_connect = None
- _on_disconnect = None
-
- def __init__(self, app_id, pipe_no=0, log=True, logger=None, log_file=None, log_level=logging.INFO):
- self._connection = BaseConnection(log=log, logger=logger, log_file=log_file, log_level=log_level)
- self._app_id = str(app_id)
- if pipe_no in range(0, 10):
- self._pipe_no = pipe_no
-
- def open(self):
- if self.state == statecodes.Connected:
- self.log('debug', 'Already connected; no need to open.')
- return
-
- if self.state == statecodes.Disconnected:
- self.connection.open(pipe_no=self._pipe_no)
- if not self.connection.is_open:
- self.log('warning', 'Failed to open IPC connection.')
- return
-
- if self.state == statecodes.SentHandshake:
- did_read, data = self.read()
- if did_read:
- cmd = data.get('cmd', None)
- evt = data.get('evt', None)
- if all(x is not None for x in (cmd, evt)) and cmd == 'DISPATCH' and evt == 'READY':
- self.state = statecodes.Connected
- if self.on_connect is not None:
- self.on_connect(data)
- self.log('info', 'IPC connected successfully.')
- else:
- data = {'v': _RPC_VERSION, 'client_id': self.app_id}
- if self.connection.write(json.dumps(data), opcodes.Handshake):
- self.state = statecodes.SentHandshake
- self.log('debug', 'IPC connection sent handshake.')
- else:
- self.log('warning', 'IPC failed to send handshake.')
- self.close()
-
- def close(self):
- if self.on_disconnect is not None and self.state in (statecodes.Connected, statecodes.SentHandshake):
- self.on_disconnect(self._last_err_code, self._last_err_msg)
- self.log('debug', 'Attempting to close IPC connection.')
- if self.connection is not None:
- self.connection.close()
- else:
- self.log('warning', 'Called close without a connection!')
- self.state = statecodes.Disconnected
-
- def write(self, data):
- if isinstance(data, dict):
- data = json.dumps(data)
- if not self.connection.write(data, opcodes.Frame):
- self.log('warning', 'Failed to write frame to IPC connection.')
- self.close()
- return False
- return True
-
- def read(self):
- if self.state not in (statecodes.Connected, statecodes.SentHandshake):
- self.log('debug', 'We aren\'t connected, therefore we cannot read data yet.')
- return False
- while True:
- did_read, opcode, data = self.connection.read()
- self.log('debug', 'ipc.read(): read: {}, Opcode: {}, data: {}'.format(did_read, opcode, data))
- if not did_read:
- err_reason = data[0]
- if (err_reason == errorcodes.PipeClosed and not self.connection.is_open) \
- or err_reason == errorcodes.ReadCorrupt:
- self._last_err_code = err_reason
- self._last_err_msg = data[1]
- self.log('debug', 'Failed to read; Connection closed. {}'.format(data))
- self.close()
- return False, None
- if opcode == opcodes.Close:
- data = json2dict(data)
- self._last_err_code = data.get('code', -1)
- self._last_err_msg = data.get('message', '')
- self.log('debug', 'Opcode == Close. Closing connection.')
- self.close()
- return False, None
- elif opcode == opcodes.Frame:
- data = json2dict(data)
- self.log('debug', 'Successful read: {}'.format(data))
- return True, data
- elif opcode == opcodes.Ping:
- if not self.connection.write('', opcodes.Pong):
- self.log('warning', 'Failed to send Pong message.')
- self.close()
- elif opcode == opcodes.Pong:
- # Discord does nothing here
- pass
- else:
- # something bad happened
- self._last_err_code = errorcodes.ReadCorrupt
- self._last_err_msg = 'Bad IPC frame.'
- self.log('warning', 'Got a bad frame from IPC connection.')
- self.close()
- return False, None
-
- def destroy(self):
- self.log('info', 'Destroying RPC connection.')
- self.close()
- self.connection.destroy()
- self._connection = None
-
- def log(self, *args):
- if self._connection is not None:
- self._connection.log(*args)
-
- @property
- def connection(self):
- return self._connection
-
- @property
- def state(self):
- return self._state
-
- @state.setter
- def state(self, state):
- if isinstance(state, int) and state in (statecodes.Connected, statecodes.SentHandshake,
- statecodes.Disconnected, statecodes.AwaitingResponse):
- self._state = state
- else:
- self.log('warning', 'Invalid state number!')
-
- @property
- def app_id(self):
- return self._app_id
-
- @property
- def is_open(self):
- return self.state == statecodes.Connected
-
- @property
- def on_connect(self):
- return self._on_connect
-
- @on_connect.setter
- def on_connect(self, callback):
- if callback is None or is_callable(callback):
- self._on_connect = callback
- else:
- self.log('warning', 'on_connect must be callable/None!')
-
- @property
- def on_disconnect(self):
- return self._on_disconnect
-
- @on_disconnect.setter
- def on_disconnect(self, callback):
- if callback is None or is_callable(callback):
- self._on_disconnect = callback
- else:
- self.log('warning', 'on_disconnect must be callable/None!')
diff --git a/discord_rpc/util/__init__.py b/discord_rpc/util/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/discord_rpc/util/__init__.py
+++ /dev/null
diff --git a/discord_rpc/util/backoff.py b/discord_rpc/util/backoff.py
deleted file mode 100644
index 650a871..0000000
--- a/discord_rpc/util/backoff.py
+++ /dev/null
@@ -1,35 +0,0 @@
-from __future__ import absolute_import
-import random
-from .types import Int32, Int64
-
-
-class Backoff(object):
- _min_amt = None
- _max_amt = None
- _current = None
- _fails = Int32()
-
- def __init__(self, min_amt, max_amt):
- min_amt = max(min_amt, 1)
- max_amt = max(max_amt, 1)
- self._min_amt = Int64(min_amt)
- self._max_amt = Int64(max_amt)
- self._current = Int64(min_amt)
-
- def reset(self):
- self._fails = Int32(0)
- self._current = self._min_amt.get_copy()
-
- def next_delay(self):
- self._fails += 1
- delay = Int64(self._current.get_number() * 2.0 * random.random())
- self._current = Int64(min(self._current.get_number() + delay.get_number(), self._max_amt))
- return self._current
-
- @property
- def fails(self):
- return self._fails
-
- @property
- def current(self):
- return self._current
diff --git a/discord_rpc/util/limits.py b/discord_rpc/util/limits.py
deleted file mode 100644
index 6e84b30..0000000
--- a/discord_rpc/util/limits.py
+++ /dev/null
@@ -1,32 +0,0 @@
-def get_min_max(bit_size, unsigned=False):
- if unsigned:
- bit_min = 0
- bit_max = (2**bit_size) - 1
- else:
- bit_min = -(2**(bit_size - 1))
- bit_max = 2**(bit_size - 1) - 1
- return bit_min, bit_max
-
-
-# limits for c types
-CHAR_MIN = -128
-CHAR_MAX = 127
-UCHAR_MAX = 255
-
-SHORT_MIN = -32768
-SHORT_MAX = 32767
-USHORT_MAX = 65535
-
-INT_MIN = -2147483648
-INT_MAX = 2147483647
-UINT_MAX = 4294967295
-INT32_MIN = INT_MIN
-INT32_MAX = INT_MAX
-UINT32_MAX = UINT_MAX
-
-LONG_MIN = -9223372036854775808
-LONG_MAX = 9223372036854775807
-ULONG_MAX = 18446744073709551615
-INT64_MIN = LONG_MIN
-INT64_MAX = LONG_MAX
-UINT64_MAX = ULONG_MAX
diff --git a/discord_rpc/util/types.py b/discord_rpc/util/types.py
deleted file mode 100644
index fdfd246..0000000
--- a/discord_rpc/util/types.py
+++ /dev/null
@@ -1,349 +0,0 @@
-from __future__ import absolute_import
-from .limits import INT32_MIN, INT32_MAX, UINT32_MAX, INT64_MIN, INT64_MAX, UINT64_MAX
-
-
-_number_types = [int]
-try:
- _number_types.append(float)
-except NameError:
- pass
-try:
- _number_types.append(long)
-except NameError:
- pass
-_number_types = tuple(_number_types)
-
-
-class UnderflowError(ArithmeticError):
- pass
-
-
-class Number(object):
- _min = None
- _max = None
- _bits = None
- _raise_exceptions = False
-
- def __init__(self, number=0, raise_exceptions=False):
- self._raise_exceptions = raise_exceptions
- if isinstance(number, Number):
- number = number.get_number()
- else:
- # validate number, ret_val should only be number, not a class
- number = self._check_number(number, number_only=True)
- if not isinstance(number, _number_types):
- raise TypeError('Number must be of type int/float/long!')
- self._number = number
-
- def _check_number(self, num, number_only=False):
- if self._max is not None and self._min is not None:
- if self._raise_exceptions:
- if num > self._max:
- raise OverflowError()
- elif num < self._min:
- raise UnderflowError()
- if self._min == 0:
- if not number_only:
- return self.__class__(num & self._max)
- else:
- return num & self._max
- else:
- if num & (1 << (self._bits-1)):
- if not number_only:
- return self.__class__(num | ~self._max)
- else:
- return num | ~self._max
- else:
- if not number_only:
- return self.__class__(num & self._max)
- else:
- return num & self._max
- else:
- # there are no min/max to check with, so just return the number
- if not number_only:
- return self.__class__(num)
- else:
- return num
-
- def __lt__(self, other):
- if isinstance(other, Number):
- other = other.get_number()
- return self._number < other
-
- def __le__(self, other):
- if isinstance(other, Number):
- other = other.get_number()
- return self._number <= other
-
- def __eq__(self, other):
- if isinstance(other, Number):
- other = other.get_number()
- return self._number == other
-
- def __ne__(self, other):
- if isinstance(other, Number):
- other = other.get_number()
- return self._number != other
-
- def __gt__(self, other):
- if isinstance(other, Number):
- other = other.get_number()
- return self._number > other
-
- def __ge__(self, other):
- if isinstance(other, Number):
- other = other.get_number()
- return self._number >= other
-
- def __bool__(self):
- return self._number
-
- def __repr__(self):
- return "{class_name}({number})".format(class_name=self.__class__.__name__, number=self._number)
-
- def __str__(self):
- return str(self._number)
-
- def __cmp__(self, other):
- if isinstance(other, Number):
- other = other.get_number()
- if self._number == other:
- return 0
- elif self._number < other:
- return -1
- elif self._number > other:
- return 1
-
- def __nonzero__(self):
- return self._number != 0
-
- def __add__(self, other):
- if isinstance(other, Number):
- other = other.get_number()
- return self._check_number(self._number + other)
-
- def __sub__(self, other):
- if isinstance(other, Number):
- other = other.get_number()
- return self._check_number(self._number - other)
-
- def __mul__(self, other):
- if isinstance(other, Number):
- other = other.get_number()
- return self._check_number(self._number * other)
-
- def __div__(self, other):
- if isinstance(other, Number):
- other = other.get_number()
- return self._check_number(self._number / other)
-
- def __truediv__(self, other):
- if isinstance(other, Number):
- other = other.get_number()
- return self.__div__(other)
-
- def __floordiv__(self, other):
- if isinstance(other, Number):
- other = other.get_number()
- return self._check_number(self._number // other)
-
- def __mod__(self, other):
- if isinstance(other, Number):
- other = other.get_number()
- return self._check_number(self._number % other)
-
- def __divmod__(self, other):
- if isinstance(other, Number):
- other = other.get_number()
- return self._check_number(divmod(self._number, other))
-
- def __pow__(self, power, modulo=None):
- if isinstance(power, Number):
- power = power.get_number()
- if modulo is not None and isinstance(modulo, Number):
- modulo = modulo.get_number()
- return self._check_number(pow(self._number, power, modulo))
-
- def __lshift__(self, other):
- if isinstance(other, Number):
- other = other.get_number()
- return self._check_number(self._number << other)
-
- def __rshift__(self, other):
- if isinstance(other, Number):
- other = other.get_number()
- return self._check_number(self._number >> other)
-
- def __and__(self, other):
- if isinstance(other, Number):
- other = other.get_number()
- return self._check_number(self._number & other)
-
- def __xor__(self, other):
- if isinstance(other, Number):
- other = other.get_number()
- return self._check_number(self._number ^ other)
-
- def __or__(self, other):
- if isinstance(other, Number):
- other = other.get_number()
- return self._check_number(self._number | other)
-
- def __radd__(self, other):
- if isinstance(other, Number):
- other = other.get_number()
- return self._check_number(other + self._number)
-
- def __rsub__(self, other):
- if isinstance(other, Number):
- other = other.get_number()
- return self._check_number(other - self._number)
-
- def __rmul__(self, other):
- if isinstance(other, Number):
- other = other.get_number()
- return self._check_number(other * self._number)
-
- def __rdiv__(self, other):
- if isinstance(other, Number):
- other = other.get_number()
- return self._check_number(other / self._number)
-
- def __rtruediv__(self, other):
- if isinstance(other, Number):
- other = other.get_number()
- return self.__rdiv__(other)
-
- def __rfloordiv__(self, other):
- if isinstance(other, Number):
- other = other.get_number()
- return self._check_number(other // self._number)
-
- def __rmod__(self, other):
- if isinstance(other, Number):
- other = other.get_number()
- return self._check_number(other % self._number)
-
- def __rdivmod__(self, other):
- if isinstance(other, Number):
- other = other.get_number()
- return self._check_number(divmod(other, self._number))
-
- def __rpow__(self, other):
- if isinstance(other, Number):
- other = other.get_number()
- return self._check_number(pow(other, self._number))
-
- def __rlshift__(self, other):
- if isinstance(other, Number):
- other = other.get_number()
- return self._check_number(other << self._number)
-
- def __rrshift__(self, other):
- if isinstance(other, Number):
- other = other.get_number()
- return self._check_number(other >> self._number)
-
- def __rand__(self, other):
- if isinstance(other, Number):
- other = other.get_number()
- return self._check_number(other & self._number)
-
- def __rxor__(self, other):
- if isinstance(other, Number):
- other = other.get_number()
- return self._check_number(other ^ self._number)
-
- def __ror__(self, other):
- if isinstance(other, Number):
- other = other.get_number()
- return self._check_number(other | self._number)
-
- def __neg__(self):
- return self._check_number(-self._number)
-
- def __pos__(self):
- return self._check_number(+self._number)
-
- def __abs__(self):
- return self._check_number(abs(self._number))
-
- def __invert__(self):
- return self._check_number(~self._number)
-
- def __complex__(self):
- return complex(self._number)
-
- def __int__(self):
- return int(self._number)
-
- def __long__(self):
- return long(self._number)
-
- def __float__(self):
- return float(self._number)
-
- def __oct__(self):
- return oct(self._number)
-
- def __hex__(self):
- return hex(self._number)
-
- def __round__(self, ndigits=None):
- if ndigits is not None and isinstance(ndigits, Number):
- ndigits = ndigits.get_number()
- return round(self._number, ndigits)
-
- def __trunc__(self):
- from math import trunc
- return trunc(self._number)
-
- def __floor__(self):
- from math import floor
- return floor(self._number)
-
- def __ceil__(self):
- from math import ceil
- return ceil(self._number)
-
- def get_number(self):
- return self._number
-
- def get_copy(self):
- return self.__class__(self._number)
-
-
-class _Int(Number):
- def __init__(self, number=0, raise_exceptions=False):
- if isinstance(number, Number):
- number = number.get_number()
- number = int(number)
- Number.__init__(self, number, raise_exceptions)
-
- def _check_number(self, num, number_only=False):
- if isinstance(num, Number):
- num = num.get_number()
- num = int(num)
- return Number._check_number(self, num, number_only)
-
-
-class Int32(_Int):
- _min = INT32_MIN
- _max = INT32_MAX
- _bits = 32
-
-
-class Int64(_Int):
- _min = INT64_MIN
- _max = INT64_MAX
- _bits = 64
-
-
-class UInt32(_Int):
- _min = 0
- _max = UINT32_MAX
-
-
-class UInt64(_Int):
- _min = 0
- _max = UINT64_MAX
diff --git a/discord_rpc/util/utils.py b/discord_rpc/util/utils.py
deleted file mode 100644
index 8142596..0000000
--- a/discord_rpc/util/utils.py
+++ /dev/null
@@ -1,180 +0,0 @@
-from copy import deepcopy
-import json
-from os import getenv, getpid, path
-import platform
-from sys import version_info, argv
-
-
-class Empty(Exception):
- pass
-
-
-class DummyQueue(object):
- """
- Dummy queue thread that does nothing. Should only be used if imports fail.
- """
-
- def __init__(self, maxsize=0):
- pass
-
- def qsize(self):
- return 0
-
- def empty(self):
- return True
-
- def full(self):
- return False
-
- def put(self, obj, *args, **kwargs):
- pass
-
- def put_nowait(self, obj):
- pass
-
- def get(self, *args, **kwargs):
- raise Empty
-
- def get_nowait(self):
- raise Empty
-
- def task_done(self):
- pass
-
- def join(self):
- pass
-
-
-def is_python3():
- return version_info[0] == 3
-
-
-def is_windows():
- return platform.system() == 'Windows'
-
-
-def is_linux():
- return platform.system() == 'Linux'
-
-
-def is_mac_osx():
- # this may not be accurate, just going off of what I find off the internet
- return platform.system() == 'Darwin'
-
-
-def get_temp_path():
- if is_windows():
- return None
- for val in ('XDG_RUNTIME_DIR', 'TMPDIR', 'TMP', 'TEMP'):
- tmp = getenv(val)
- if tmp is not None:
- return tmp
- return '/tmp'
-
-
-def get_process_id():
- return getpid()
-
-
-def is_callable(obj):
- try:
- # for Python 2.x or Python 3.2+
- return callable(obj)
- except Exception:
- # for Python version: 3 - 3.2
- return hasattr(obj, '__call__')
-
-
-# python 2 + 3 compatibility
-if is_python3():
- unicode = str
- bytes = bytes
-else:
- bytes = str
- unicode = unicode
-
-
-def to_bytes(obj):
- if isinstance(obj, type(b'')):
- return obj
- if hasattr(obj, 'encode') and is_callable(obj.encode):
- return obj.encode('ascii', 'replace')
- raise TypeError('Could not convert object type "{}" to bytes!'.format(type(obj)))
-
-
-def to_unicode(obj):
- if isinstance(obj, type(u'')):
- return obj
- if hasattr(obj, 'decode') and is_callable(obj.decode):
- return obj.decode(encoding='utf-8')
- raise TypeError('Could not convert object type "{}" to unicode!'.format(type(obj)))
-
-
-def iter_keys(obj):
- if not isinstance(obj, dict):
- raise TypeError('Object must be of type dict!')
- if is_python3():
- return obj.keys()
- return obj.iterkeys()
-
-
-def iter_items(obj):
- if not isinstance(obj, dict):
- raise TypeError('Object must be of type dict!')
- if is_python3():
- return obj.items()
- return obj.iteritems()
-
-
-def iter_values(obj):
- if not isinstance(obj, dict):
- raise TypeError('Object must be of type dict!')
- if is_python3():
- return obj.values()
- return obj.itervalues()
-
-
-def _py_dict(obj):
- if not isinstance(obj, dict):
- raise TypeError('Object must be of type dict!')
- new_dict = dict()
- for name, val in iter_items(obj):
- if isinstance(name, type(b'')) and is_python3():
- name = to_unicode(name)
- elif isinstance(name, type(u'')) and not is_python3():
- name = to_bytes(name)
- if isinstance(val, dict):
- val = _py_dict(val)
- elif isinstance(val, type(b'')) and is_python3():
- val = to_unicode(val)
- elif isinstance(val, type(u'')) and not is_python3():
- val = to_bytes(val)
- new_dict[name] = val
- return deepcopy(new_dict)
-
-
-def json2dict(obj):
- if isinstance(obj, dict):
- return deepcopy(_py_dict(obj))
- if obj is None:
- return dict()
- if hasattr(obj, 'strip'):
- if obj.strip() == '':
- return dict()
- else:
- return deepcopy(_py_dict(deepcopy(json.loads(obj))))
- raise TypeError('Object must be of string type!')
-
-
-if not is_python3():
- range = xrange
-else:
- range = range
-
-
-def get_executable_directory():
- return path.abspath(path.dirname(argv[0]))
-
-
-def get_executable_path():
- return path.join(get_executable_directory(), argv[0])
diff --git a/discordrpc/__init__.py b/discordrpc/__init__.py
new file mode 100644
index 0000000..8ff82d5
--- /dev/null
+++ b/discordrpc/__init__.py
@@ -0,0 +1,9 @@
+from .presence import RPC
+from .button import button
+from .exceptions import *
+
+__title__ = "Discord RPC"
+__version__ = "4.0"
+__authors__ = "LyQuid"
+__license__ = "MIT License"
+__copyright__ = "Copyright 2021-present LyQuid"
diff --git a/discordrpc/button.py b/discordrpc/button.py
new file mode 100644
index 0000000..b5d21ef
--- /dev/null
+++ b/discordrpc/button.py
@@ -0,0 +1,38 @@
+from .exceptions import *
+
+valid_url = ["https://", "http://"]
+
+def button_one(label:str, url:str):
+ if any(v in url for v in valid_url):
+ payloads = {"label": label, "url": url}
+ return payloads
+ else:
+ raise InvalidURL
+
+def button_two(label:str, url:str):
+ if any(v in url for v in valid_url):
+ payloads = {"label": label, "url": url}
+ return payloads
+ else:
+ raise InvalidURL
+
+def button(
+ button_one_label:str=None,
+ button_two_label:str=None,
+ button_one_url:str=None,
+ button_two_url:str=None):
+
+ if button_one_label == None:
+ raise ButtonError('"button_one_label" cannot None')
+ if button_one_url == None:
+ raise ButtonError('"button_one_url" cannot None')
+ if button_two_label == None:
+ raise ButtonError('"button_two_label" cannot None')
+ if button_two_url == None:
+ raise ButtonError('"button_two_url" cannot None')
+
+ btn_one = button_one(label=button_one_label, url=button_one_url)
+ btn_two = button_two(label=button_two_label, url=button_two_url)
+ payloads = [btn_one, btn_two]
+
+ return payloads
diff --git a/discordrpc/exceptions.py b/discordrpc/exceptions.py
new file mode 100644
index 0000000..29c3d5c
--- /dev/null
+++ b/discordrpc/exceptions.py
@@ -0,0 +1,29 @@
+class RPCException(Exception):
+ def __init__(self, message: str= None):
+ if message is None:
+ message = 'An error has occurred within DiscordRPC'
+ super().__init__(message)
+
+class Error(RPCException):
+ def __init__(self, message:str):
+ super().__init__(message)
+
+class DiscordNotOpened(RPCException):
+ def __init__(self):
+ super().__init__("Error, could not find Discord. is Discord running?")
+
+class ActivityError(RPCException):
+ def __init__(self):
+ super().__init__("An error has occurred in activity payload, do you have set your activity correctly?")
+
+class InvalidURL(RPCException):
+ def __init__(self):
+ super().__init__("Invalid URL. Must include: http:// or https://")
+
+class InvalidID(RPCException):
+ def __init__(self):
+ super().__init__("Invalid ID, is the ID correct? Get Application ID on https://discord.com/developers/applications")
+
+class ButtonError(RPCException):
+ def __init__(self, message: str = None):
+ super().__init__(message=message)
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()