From d6d698227b4c1ba1004d371ab5fb35ec8024b5da Mon Sep 17 00:00:00 2001 From: Jesusaves Date: Mon, 5 Feb 2024 18:35:33 -0300 Subject: Update discord_rpc to 4.0 https://pypi.org/project/discord-rpc --- discord_rpc/__init__.py | 919 ------------------------------------- discord_rpc/codes/__init__.py | 0 discord_rpc/codes/errorcodes.py | 3 - discord_rpc/codes/opcodes.py | 5 - discord_rpc/codes/statecodes.py | 4 - discord_rpc/connection/__init__.py | 0 discord_rpc/connection/ipc.py | 387 ---------------- discord_rpc/connection/rpc.py | 175 ------- discord_rpc/util/__init__.py | 0 discord_rpc/util/backoff.py | 35 -- discord_rpc/util/limits.py | 32 -- discord_rpc/util/types.py | 349 -------------- discord_rpc/util/utils.py | 180 -------- discordrpc/__init__.py | 9 + discordrpc/button.py | 38 ++ discordrpc/exceptions.py | 29 ++ discordrpc/presence.py | 252 ++++++++++ 17 files changed, 328 insertions(+), 2089 deletions(-) delete mode 100755 discord_rpc/__init__.py delete mode 100644 discord_rpc/codes/__init__.py delete mode 100644 discord_rpc/codes/errorcodes.py delete mode 100644 discord_rpc/codes/opcodes.py delete mode 100644 discord_rpc/codes/statecodes.py delete mode 100644 discord_rpc/connection/__init__.py delete mode 100644 discord_rpc/connection/ipc.py delete mode 100644 discord_rpc/connection/rpc.py delete mode 100644 discord_rpc/util/__init__.py delete mode 100644 discord_rpc/util/backoff.py delete mode 100644 discord_rpc/util/limits.py delete mode 100644 discord_rpc/util/types.py delete mode 100644 discord_rpc/util/utils.py create mode 100644 discordrpc/__init__.py create mode 100644 discordrpc/button.py create mode 100644 discordrpc/exceptions.py create mode 100644 discordrpc/presence.py 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 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 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(' 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(" 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(" 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() -- cgit v1.2.3-60-g2f50