diff --git a/L10n/de.strings b/L10n/de.strings new file mode 100644 index 0000000..fd51082 --- /dev/null +++ b/L10n/de.strings @@ -0,0 +1,44 @@ +/* Language name */ +"!Language" = "Deutsch"; + +/* Idle message. [load.py] */ +"Connecting CMDR Interface" = "Verbindet mit KMDT-Schnittstelle"; + +/* Details of system. [load.py] */ +"In system {system}" = "Im System {system}"; + +/* If docked. [load.py] */ +"Docked at {station}" = "Angedockt an {station}"; + +/* While jumping. [load.py] */ +"Jumping" = "Springt"; + +/* If Hyperspace jumping. [load.py] */ +"Jumping to system {system}" = "Springt zum System {system}"; + +/* If Supercruise jumping. [load.py] */ +"Preparing for supercruise" = "Macht sich bereit für Supercruise"; + +/* When supercruising. [load.py] */ +"Supercruising" = "Im Supercruise"; + +/* When in normal space. [load.py] */ +"Flying in normal space" = "Fliegt im normalen Raum"; + +/* When in normal space and near a station. [load.py] */ +"Flying near {station}" = "Fliegt nahe {station}"; + +/* When approaching a body. [load.py] */ +"Approaching {body}" = "Nähert sich {body}"; + +/* When landed on a body. [load.py] */ +"Landed on {body}" = "Gelandet auf {body}"; + +/* After taking off from a body. [load.py] */ +"Flying around {body}" = "Fliegt um {body}"; + +/* When in SRV. [load.py] */ +"In SRV on {body}" = "Im SRV auf {body}"; + +/* When in SRV and ship has taken off. [load.py] */ +"In SRV on {body}, ship in orbit" = "Im SRV auf {body}, Schiff im Orbit"; diff --git a/L10n/en.template b/L10n/en.template new file mode 100644 index 0000000..b4e3b89 --- /dev/null +++ b/L10n/en.template @@ -0,0 +1,44 @@ +/* Language name */ +"!Language" = "English"; + +/* Idle message. [load.py] */ +"Connecting CMDR Interface" = "Connecting CMDR Interface"; + +/* Details of system. [load.py] */ +"In system {system}" = "In system {system}"; + +/* If docked. [load.py] */ +"Docked at {station}" = "Docked at {station}"; + +/* While jumping. [load.py] */ +"Jumping" = "Jumping"; + +/* If Hyperspace jumping. [load.py] */ +"Jumping to system {system}" = "Jumping to system {system}"; + +/* If Supercruise jumping. [load.py] */ +"Preparing for supercruise" = "Preparing for supercruise"; + +/* When supercruising. [load.py] */ +"Supercruising" = "Supercruising"; + +/* When in normal space. [load.py] */ +"Flying in normal space" = "Flying in normal space"; + +/* When in normal space and near a station. [load.py] */ +"Flying near {station}" = "Flying near {station}"; + +/* When approaching a body. [load.py] */ +"Approaching {body}" = "Approaching {body}"; + +/* When landed on a body. [load.py] */ +"Landed on {body}" = "Landed on {body}"; + +/* After taking off from a body. [load.py] */ +"Flying around {body}" = "Flying around {body}"; + +/* When in SRV. [load.py] */ +"In SRV on {body}" = "In SRV on {body}"; + +/* When in SRV and ship has taken off. [load.py] */ +"In SRV on {body}, ship in orbit" = "In SRV on {body}, ship in orbit"; diff --git a/L10n/fr.strings b/L10n/fr.strings new file mode 100644 index 0000000..a8d1691 --- /dev/null +++ b/L10n/fr.strings @@ -0,0 +1,44 @@ +/* Language name */ +"!Language" = "Français"; + +/* Idle message. [load.py] */ +"Connecting CMDR Interface" = "Interfaçage au Vaisseau"; + +/* Details of system. [load.py] */ +"In system {system}" = "Dans le système {system}"; + +/* If docked. [load.py] */ +"Docked at {station}" = "Docké à {station}"; + +/* While jumping. [load.py] */ +"Jumping" = "Saut"; + +/* If Hyperspace jumping. [load.py] */ +"Jumping to system {system}" = "Saut vers {system}"; + +/* If Supercruise jumping. [load.py] */ +"Preparing for supercruise" = "Préparation d\'un saut en supercruise"; + +/* When supercruising. [load.py] */ +"Supercruising" = "Supercruise"; + +/* When in normal space. [load.py] */ +"Flying in normal space" = "En vol dans l\'espace"; + +/* When in normal space and near a station. [load.py] */ +"Flying near {station}" = "En vol près de {station}"; + +/* When approaching a body. [load.py] */ +"Approaching {body}" = "En approche de {body}"; + +/* When landed on a body. [load.py] */ +"Landed on {body}" = "Posé sur {body}"; + +/* After taking off from a body. [load.py] */ +"Flying around {body}" = "Vol autour de {body}"; + +/* When in SRV. [load.py] */ +"In SRV on {body}" = "En SRV sur {body}"; + +/* When in SRV and ship has taken off. [load.py] */ +"In SRV on {body}, ship in orbit" = "En SRV sur {body}, vaisseau en orbite"; diff --git a/L10n/pt-BR.strings b/L10n/pt-BR.strings new file mode 100644 index 0000000..89e1de2 --- /dev/null +++ b/L10n/pt-BR.strings @@ -0,0 +1,44 @@ +/* Language name */ +"!Language" = "Português (Brasil)"; + +/* Idle message. [load.py] */ +"Connecting CMDR Interface" = "Conectando Interface do CMDT"; + +/* Details of system. [load.py] */ +"In system {system}" = "No sistema {system}"; + +/* If docked. [load.py] */ +"Docked at {station}" = "Docado em {station}"; + +/* While jumping. [load.py] */ +"Jumping" = "Saltando"; + +/* If Hyperspace jumping. [load.py] */ +"Jumping to system {system}" = "Saltando para systema {system}"; + +/* If Supercruise jumping. [load.py] */ +"Preparing for supercruise" = "Preparando para supercruseiro"; + +/* When supercruising. [load.py] */ +"Supercruising" = "Supercrusando"; + +/* When in normal space. [load.py] */ +"Flying in normal space" = "Voando em espaço normal"; + +/* When in normal space and near a station. [load.py] */ +"Flying near {station}" = "Voando perto de {station}"; + +/* When approaching a body. [load.py] */ +"Approaching {body}" = "Aproximando-se de {body}"; + +/* When landed on a body. [load.py] */ +"Landed on {body}" = "Pousado em {body}"; + +/* After taking off from a body. [load.py] */ +"Flying around {body}" = "Voando ao redor de {body}"; + +/* When in SRV. [load.py] */ +"In SRV on {body}" = "No SRV em {body}"; + +/* When in SRV and ship has taken off. [load.py] */ +"In SRV on {body}, in orbit" = "No SRV em {body}, nave em órbita"; diff --git a/L10n/ru.strings b/L10n/ru.strings new file mode 100644 index 0000000..4e22833 --- /dev/null +++ b/L10n/ru.strings @@ -0,0 +1,44 @@ +/* Language name */ +"!Language" = "Русский"; + +/* Idle message. [load.py] */ +"Connecting CMDR Interface" = "Подключение интерфейса КМДР"; + +/* Details of system. [load.py] */ +"In system {system}" = "В системе {system}"; + +/* If docked. [load.py] */ +"Docked at {station}" = "Пристыкован к {station}"; + +/* While jumping. [load.py] */ +"Jumping" = "Гиперпрыжок"; + +/* If Hyperspace jumping. [load.py] */ +"Jumping to system {system}" = "Гиперпрыжок в систему {system}"; + +/* If Supercruise jumping. [load.py] */ +"Preparing for supercruise" = "Подготовка к переходу в супереркруиз"; + +/* When supercruising. [load.py] */ +"Supercruising" = "Полет в суперкруизе"; + +/* When in normal space. [load.py] */ +"Flying in normal space" = "Полет в обычном космосе"; + +/* When in normal space and near a station. [load.py] */ +"Flying near {station}" = "Полет вблизи {station}"; + +/* When approaching a body. [load.py] */ +"Approaching {body}" = "Приближение к {body}"; + +/* When landed on a body. [load.py] */ +"Landed on {body}" = "Посадка на {body}"; + +/* After taking off from a body. [load.py] */ +"Flying around {body}" = "Полет возле {body}"; + +/* When in SRV. [load.py] */ +"In SRV on {body}" = "В ТРП на поверхности {body}"; + +/* When in SRV and ship has taken off. [load.py] */ +"In SRV on {body}, ship in orbit" = "В ТРП на поверхности {body}, судно на орбите"; diff --git a/README.md b/README.md index 217577e..1eb5188 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,46 @@ -# EliteRPC - -Discord Rich Presence for Elite Dangerous \ No newline at end of file +# EDMC-Discord-Presence + +A plugin for [Elite Dangerous Market Connector](https://github.com/Marginal/EDMarketConnector) that enables [Discord Rich Presence](https://discordapp.com/rich-presence) for [Elite Dangerous](https://www.elitedangerous.com/) + +Show your current location to your friends on Discord from your user profile. + +![Presence Screenshot](EDMC_Discord_Presence_1.png?raw=true) + +## Installation + +1. [Install EDMC according to instructions](https://github.com/Marginal/EDMarketConnector) +2. Download the latest version of the plugin from [here](https://github.com/SayakMukhopadhyay/EDMC-Discord-Presence/releases). Make sure to download the release `.zip` and not the source code bundle. +3. Open Elite Dangerous Market Connector and go to File -> Settings. Then browse to the plugins tab: + +![Plugin Installation](EDMC_Discord_Presence_2.png?raw=true) + +4. Click "Open" to open the plugins directory. +5. Open the Zip file we have downloaded and drag the folder from within into the plugins directory +6. Restart EDMC for the plugin to take effect. + +To check if the plugin is loaded correctly, go File -> Settings. Then browse to the plugins tab. `DiscordPresence` must be listed under `Enabled Plugins` + +![Plugin Installation Check](EDMC_Discord_Presence_3.png?raw=true) + +## Options + +You can set the plugin to not show your game data. Go to File -> Settings. Under the `DiscordPresence` tab, check `Disable Presence` + +![Plugin Disable](EDMC_Discord_Presence_4.png?raw=true) + +## Contributing + +If you find a bug, please create an issue in the issue tracker in Github, properly detailing the bug and reproduction steps. + +If you are willing to contribute to the project, please work on a fork and create a pull request. + +## Credits + +For the CMDRs by a CMDR. Created by [CMDR Garud](https://forums.frontier.co.uk/member.php/136073-Garud) for an awesome gaming community. +A big thanks to [Jonathan Harris (Marginal)](https://github.com/Marginal) for creating the Python boilerplate code for the Discord Rich Presence SDK. Without his input, the plugin would not have been done. Special mention for the awesome group I am in, [Knights of Karma](http://knightsofkarma.com/), for their continuous support. + +Translate to french, migrate from python2 to python3 by [Poneyy](https://github.com/Poneyy) + +## License + +Developed under [Apache License 2.0](https://choosealicense.com/licenses/apache-2.0/). diff --git a/__pycache__/load.cpython-312.pyc b/__pycache__/load.cpython-312.pyc new file mode 100644 index 0000000..8f23c2e Binary files /dev/null and b/__pycache__/load.cpython-312.pyc differ diff --git a/lib/discord_game_sdk.dll b/lib/discord_game_sdk.dll new file mode 100644 index 0000000..8f1ee0a Binary files /dev/null and b/lib/discord_game_sdk.dll differ diff --git a/lib/discord_game_sdk.dylib b/lib/discord_game_sdk.dylib new file mode 100644 index 0000000..24045f7 Binary files /dev/null and b/lib/discord_game_sdk.dylib differ diff --git a/lib/discord_game_sdk.so b/lib/discord_game_sdk.so new file mode 100644 index 0000000..e465760 Binary files /dev/null and b/lib/discord_game_sdk.so differ diff --git a/load.py b/load.py new file mode 100644 index 0000000..07894f3 --- /dev/null +++ b/load.py @@ -0,0 +1,356 @@ +# +# KodeBlox Copyright 2019 Sayak Mukhopadhyay +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http: //www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import functools +import logging +import threading +import tkinter as tk +from os.path import dirname, join + +import semantic_version +import sys +import time + +import l10n +import myNotebook as nb +from config import config, appname, appversion +from py_discord_sdk import discordsdk as dsdk + +plugin_name = "DiscordPresenceNG" + +logger = logging.getLogger(f'{appname}.{plugin_name}') + +_ = functools.partial(l10n.Translations.translate, context=__file__) + +CLIENT_ID = 1013117837310693438 +VERSION = '1.0' + +# Add global var for Planet name (landing + around) +planet = '' +landingPad = '2' + +this = sys.modules[__name__] # For holding module globals + +def ship(ship_type): + ship_name = '' + match ship_type: + case "sidewinder": + ship_name = "Sidewinder" + case "eagle": + ship_name = "Eagle" + case "hauler": + ship_name = "Hauler" + case "adder": + ship_name = "Adder" + case "adder_taxi": + ship_name = "Apex Shuttle" + case "empire_eagle": + ship_name = "Imperial Eagle" + case "viper": + ship_name = "Viper Mk III" + case "cobramkiii": + ship_name = "Cobra Mk III" + case "viper_mkiv": + ship_name = "Viper Mk IV" + case "diamondback": + ship_name = "Diamondback Scout" + case "cobramkiv": + ship_name = "Cobra Mk IV" + case "type6": + ship_name = "Type-6 Transporter" + case "dolphin": + ship_name = "Dolphin" + case "diamondbackxl": + ship_name = "Diamondback Explorer" + case "empire_courier": + ship_name = "Imperial Courier" + case "independant_trader": + ship_name = "Keelback" + case "asp_scout": + ship_name = "Asp Scout" + case "vulture": + ship_name = "Vulture" + case "asp": + ship_name = "Asp Explorer" + case "federation_dropship": + ship_name = "Federal Dropship" + case "type7": + ship_name = "Type-7 Transporter" + case "typex": + ship_name = "Alliance Chieftain" + case "federation_dropship_mkii": + ship_name = "Federal Assault Ship" + case "empire_trader": + ship_name = "Imperial Clipper" + case "typex_2": + ship_name = "Alliance Crusader" + case "typex_3": + ship_name = "Alliance Challenger" + case "federation_gunship": + ship_name = "Federal Gunship" + case "krait_light": + ship_name = "Krait Phantom" + case "krait_mkii": + ship_name = "Krait Mk II" + case "orca": + ship_name = "Orca" + case "ferdelance": + ship_name = "Fer-de-Lance" + case "mamba": + ship_name = "Mamba" + case "python": + ship_name = "Python" + case "type9": + ship_name = "Type-9 Heavy" + case "belugaliner": + ship_name = "Beluga Liner" + case "type9_military": + ship_name = "Type-10 Defender" + case "anaconda": + ship_name = "Anaconda" + case "federation_corvette": + ship_name = "Federal Corvette" + case "cutter": + ship_name = "Imperial Cutter" + case _: # Default case for any other value + ship_name = "Unknown Ship" + return ship_name + +def callback(result): + logger.info(f'Callback: {result}') + if result == dsdk.Result.ok: + logger.info("Successfully set the activity!") + elif result == dsdk.Result.transaction_aborted: + logger.warning(f'Transaction aborted due to SDK shutting down: {result}') + else: + logger.error(f'Error in callback: {result}') + raise Exception(result) + + +def update_presence(): + if isinstance(appversion, str): + core_version = semantic_version.Version(appversion) + + elif callable(appversion): + core_version = appversion() + + logger.info(f'Core EDMC version: {core_version}') + if core_version < semantic_version.Version('5.0.0-beta1'): + logger.info('EDMC core version is before 5.0.0-beta1') + if config.getint("disable_presence") == 0: + this.activity.state = this.presence_state + this.activity.details = this.presence_details + this.activity.assets.large_image = this.presence_large_image + this.activity.assets.large_text = this.presence_large_text + this.activity.assets.small_image = this.presence_small_image + this.activity.assets.small_text = this.presence_small_text + else: + logger.info('EDMC core version is at least 5.0.0-beta1') + if config.get_int("disable_presence") == 0: + this.activity.state = this.presence_state + this.activity.details = this.presence_details + this.activity.assets.large_image = this.presence_large_image + this.activity.assets.large_text = this.presence_large_text + this.activity.assets.small_image = this.presence_small_image + this.activity.assets.small_text = this.presence_small_text + + this.activity.timestamps.start = int(this.time_start) + this.activity_manager.update_activity(this.activity, callback) + + +def plugin_prefs(parent, cmdr, is_beta): + """ + Return a TK Frame for adding to the EDMC settings dialog. + """ + if isinstance(appversion, str): + core_version = semantic_version.Version(appversion) + + elif callable(appversion): + core_version = appversion() + + logger.info(f'Core EDMC version: {core_version}') + if core_version < semantic_version.Version('5.0.0-beta1'): + logger.info('EDMC core version is before 5.0.0-beta1') + this.disablePresence = tk.IntVar(value=config.get_int("disable_presence")) + else: + logger.info('EDMC core version is at least 5.0.0-beta1') + this.disablePresence = tk.IntVar(value=config.get_int("disable_presence")) + + frame = nb.Frame(parent) + nb.Checkbutton(frame, text="Disable Presence", variable=this.disablePresence).grid() + nb.Label(frame, text='Version %s' % VERSION).grid(padx=10, pady=10, sticky=tk.W) + + return frame + + +def prefs_changed(cmdr, is_beta): + """ + Save settings. + """ + config.set('disable_presence', this.disablePresence.get()) + config.set('disable_presence', this.disableName.get()) + update_presence() + + +def plugin_start3(plugin_dir): + this.plugin_dir = plugin_dir + this.discord_thread = threading.Thread(target=check_run, args=(plugin_dir,)) + this.discord_thread.setDaemon(True) + this.discord_thread.start() + return 'DiscordPresenceNG' + + +def plugin_stop(): + this.activity_manager.clear_activity(callback) + this.call_back_thread = None + + +def journal_entry(cmdr, is_beta, system, station, entry, state): + global planet + global landingPad + presence_state = this.presence_state + presence_details = this.presence_details + presence_largeimage = this.presence_large_image + presence_largetext = this.presence_large_text + presence_smallimage = this.presence_small_image + presence_smalltext = this.presence_small_text + if entry['event'] == 'StartUp': + presence_largetext = ship(('{ship}').format(ship=state['ShipType'])) + presence_smalltext = ('CMDR {cmdr}').format(cmdr=cmdr) + presence_largeimage = ('{ship}').format(ship=state['ShipType']) + presence_smallimage = 'edlogo' + #presence_smalltext = ('{cmdr}cr').format(credits=state['Credits']) + presence_state = _('In system {system}').format(system=system) + if station is None: + presence_details = _('Flying in normal space') + else: + presence_details = _('Docked at {station}').format(station=station) + elif entry['event'] == 'Location': + presence_state = _('In system {system}').format(system=system) + if station is None: + presence_details = _('Flying in normal space') + else: + presence_details = _('Docked at {station}').format(station=station) + elif entry['event'] == 'StartJump': + presence_state = _('Jumping') + if entry['JumpType'] == 'Hyperspace': + presence_details = _('Jumping to system {system}').format(system=entry['StarSystem']) + elif entry['JumpType'] == 'Supercruise': + presence_details = _('Preparing for supercruise') + elif entry['event'] == 'SupercruiseEntry': + presence_state = _('In system {system}').format(system=system) + presence_details = _('Supercruising') + elif entry['event'] == 'SupercruiseExit': + presence_state = _('In system {system}').format(system=system) + presence_details = _('Flying in normal space') + elif entry['event'] == 'FSDJump': + presence_state = _('In system {system}').format(system=system) + presence_details = _('Supercruising') + elif entry['event'] == 'Docked': + presence_state = _('In system {system}').format(system=system) + presence_details = _('Docked at {station}').format(station=station) + elif entry['event'] == 'Undocked': + presence_state = _('In system {system}').format(system=system) + presence_details = _('Flying in normal space') + elif entry['event'] == 'ShutDown': + presence_state = _('Main Menu') + presence_details = '' + elif entry['event'] == 'DockingGranted': + landingPad = entry['LandingPad'] + elif entry['event'] == 'Music': + if entry['MusicTrack'] == 'MainMenu': + presence_state = _('In Main Menu') + presence_details = '' + # Todo: This elif might not be executed on undocked. Functionality can be improved + elif entry['event'] == 'Undocked' or entry['event'] == 'DockingCancelled' or entry['event'] == 'DockingTimeout': + presence_details = _('Flying near {station}').format(station=entry['StationName']) + # Planetary events + elif entry['event'] == 'ApproachBody': + presence_details = _('Approaching {body}').format(body=entry['Body']) + planet = entry['Body'] + elif entry['event'] == 'Touchdown' and entry['PlayerControlled']: + presence_details = _('Landed on {body}').format(body=planet) + elif entry['event'] == 'Liftoff' and entry['PlayerControlled']: + if entry['PlayerControlled']: + presence_details = _('Flying around {body}').format(body=planet) + else: + presence_details = _('In SRV on {body}, ship in orbit').format(body=planet) + elif entry['event'] == 'LeaveBody': + presence_details = _('Supercruising') + + elif entry['event'] == 'Loadout': + presence_largeimage = ('{ship}').format(ship=state['ShipType']) + presence_largetext = ship(('{ship}').format(ship=state['ShipType'])) + + # EXTERNAL VEHICLE EVENTS + elif entry['event'] == 'LaunchSRV': + presence_details = _('In SRV on {body}').format(body=planet) + elif entry['event'] == 'DockSRV': + presence_details = _('Landed on {body}').format(body=planet) + + if (presence_state != this.presence_state or + presence_details != this.presence_details or + presence_largeimage != this.presence_large_image or + presence_largetext != this.presence_large_text or + presence_smallimage != this.presence_small_image or + presence_smalltext != this.presence_small_text) : + this.presence_state = presence_state + this.presence_details = presence_details + this.presence_large_image = presence_largeimage + this.presence_large_text = presence_largetext + this.presence_small_image = presence_smallimage + this.presence_small_text = presence_smalltext + update_presence() + + +def check_run(plugin_dir): + plugin_path = join(dirname(plugin_dir), plugin_name) + retry = True + while retry: + time.sleep(1 / 10) + try: + this.app = dsdk.Discord(CLIENT_ID, dsdk.CreateFlags.no_require_discord, plugin_path) + retry = False + except Exception: + pass + + this.activity_manager = this.app.get_activity_manager() + this.activity = dsdk.Activity() + + this.call_back_thread = threading.Thread(target=run_callbacks) + this.call_back_thread.setDaemon(True) + this.call_back_thread.start() + this.presence_state = _('Connecting CMDR Interface') + this.presence_details = '' + this.presence_large_image = 'edlogo' + this.presence_large_text = 'EliteRPC' + this.presence_small_image = '' + this.presence_small_text = '' + this.time_start = time.time() + + this.disablePresence = None + this.disableName = None + + update_presence() + + +def run_callbacks(): + try: + while True: + time.sleep(1 / 10) + this.app.run_callbacks() + except Exception: + check_run(this.plugin_dir) \ No newline at end of file diff --git a/py_discord_sdk/.gitignore b/py_discord_sdk/.gitignore new file mode 100644 index 0000000..8c5ac37 --- /dev/null +++ b/py_discord_sdk/.gitignore @@ -0,0 +1,157 @@ +# Created by https://www.toptal.com/developers/gitignore/api/python,visualstudiocode +# Edit at https://www.toptal.com/developers/gitignore?templates=python,visualstudiocode + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Pycharm files +.idea/* +.idea + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +pytestdebug.log + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history + +# End of https://www.toptal.com/developers/gitignore/api/python,visualstudiocode + +.idea/vcs.xml +.DS_Store + +.vscode/ diff --git a/py_discord_sdk/LICENSE b/py_discord_sdk/LICENSE new file mode 100644 index 0000000..1e67ec4 --- /dev/null +++ b/py_discord_sdk/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2020 NathaanTFM +Copyright (c) 2020 LennyPhoenix + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/py_discord_sdk/README.md b/py_discord_sdk/README.md new file mode 100644 index 0000000..0c2a4b0 --- /dev/null +++ b/py_discord_sdk/README.md @@ -0,0 +1,134 @@ +# Discord Game SDK for Python + +A Python wrapper around Discord's Game SDK. + +**NOTE**: This is entirely experimental, and may not work as intended. Please report all bugs to the [GitHub issue tracker](https://github.com/LennyPhoenix/py-discord-sdk/issues). + +**Credit to [NathaanTFM](https://github.com/NathaanTFM) for creating the [original library](https://github.com/NathaanTFM/discord-game-sdk-python).** + +## Installation + +- Install the module: + - With `PIP`: + - Stable: `python -m pip install discordsdk` + - Latest: `python -m pip install git+https://github.com/LennyPhoenix/py-discord-sdk.git` + - With `setup.py` (latest): + - `git clone https://github.com/LennyPhoenix/py-discord-sdk.git` + - `cd py-discord-sdk` + - `python -m setup install` +- Download [Discord Game SDK (2.5.6)](https://dl-game-sdk.discordapp.net/2.5.6/discord_game_sdk.zip). +- Grab the DLL from `discord_game_sdk.zip` in the `lib` directory and put it in your project's `lib` directory. + +## Documentation + +If you need documentation, look at [**the official Game SDK docs**](https://discord.com/developers/docs/game-sdk/sdk-starter-guide); this was made following the official documentation. +We also have some [work-in-progress wiki docs](https://github.com/LennyPhoenix/py-discord-sdk/wiki). + +## Features + +- Should be working: + - **ActivityManager** + - **ImageManager** + - **NetworkManager** + - **RelationshipManager** + - **StorageManager** + - **UserManager** + +- Should be working, but need more testing: + - **AchievementManager** (not tested at all) + - **ApplicationManager** (especially the functions `GetTicket` and `ValidateOrExit`) + - **LobbyManager** + - **OverlayManager** + - **StoreManager** (not tested at all) + - **VoiceManager** + +## Contributing + +The code needs **more comments, type hinting**. You can also implement the **missing features**, or add **more tests**. Feel free to open a **pull request**! + +You can also **report issues**. Just open an issue and I will look into it! + +### Todo List + +- Better organisation of submodules. +- CI/CD. +- Update sdk.py to use type annotations. +- Update to Discord SDK 3.2.0. + +## Examples + +You can find more examples in the `examples/` directory. + +### Create a Discord instance + +```python +import time + +import discordsdk as dsdk + +app = dsdk.Discord(APPLICATION_ID, dsdk.CreateFlags.default) + +# Don't forget to call run_callbacks +while 1: + time.sleep(1/10) + app.run_callbacks() +``` + +### Get current user + +```python +import time + +import discordsdk as dsdk + +app = dsdk.Discord(APPLICATION_ID, dsdk.CreateFlags.default) + +user_manager = app.get_user_manager() + + +def on_curr_user_update(): + user = user_manager.get_current_user() + print(f"Current user : {user.username}#{user.discriminator}") + + +user_manager.on_current_user_update = on_curr_user_update + +# Don't forget to call run_callbacks +while 1: + time.sleep(1/10) + app.run_callbacks() +``` + +### Set activity + +```python +import time + +import discordsdk as dsdk + +app = dsdk.Discord(APPLICATION_ID, dsdk.CreateFlags.default) + +activity_manager = app.get_activity_manager() + +activity = dsdk.Activity() +activity.state = "Testing Game SDK" +activity.party.id = "my_super_party_id" +activity.party.size.current_size = 4 +activity.party.size.max_size = 8 +activity.secrets.join = "my_super_secret" + + +def callback(result): + if result == dsdk.Result.ok: + print("Successfully set the activity!") + else: + raise Exception(result) + + +activity_manager.update_activity(activity, callback) + +# Don't forget to call run_callbacks +while 1: + time.sleep(1/10) + app.run_callbacks() +``` diff --git a/py_discord_sdk/discordsdk/__init__.py b/py_discord_sdk/discordsdk/__init__.py new file mode 100644 index 0000000..5e69221 --- /dev/null +++ b/py_discord_sdk/discordsdk/__init__.py @@ -0,0 +1,55 @@ +from . import sdk +from .achievement import AchievementManager +from .activity import ActivityManager +from .application import ApplicationManager +from .discord import Discord +from .enum import ( + ActivityActionType, ActivityJoinRequestReply, ActivityType, CreateFlags, + EntitlementType, ImageType, InputModeType, LobbySearchCast, + LobbySearchComparison, LobbySearchDistance, LobbyType, LogLevel, + PremiumType, RelationshipType, Result, SkuType, Status, UserFlag) +from .event import bind_events +from .exception import DiscordException, exceptions, get_exception +from .image import ImageManager +from .lobby import (LobbyManager, LobbyMemberTransaction, LobbySearchQuery, + LobbyTransaction) +from .model import ( + Activity, ActivityAssets, ActivityParty, ActivitySecrets, + ActivityTimestamps, Entitlement, FileStat, ImageDimensions, ImageHandle, + InputMode, Lobby, Model, OAuth2Token, PartySize, Presence, Relationship, + Sku, SkuPrice, User, UserAchievement) +from .network import NetworkManager +from .overlay import OverlayManager +from .relationship import RelationshipManager +from .storage import StorageManager +from .store import StoreManager +from .user import UserManager +from .voice import VoiceManager + +__all__ = [ + "sdk", + "AchievementManager", + "ActivityManager", + "ApplicationManager", + "Discord", + "ActivityActionType", "ActivityJoinRequestReply", "ActivityType", "CreateFlags", + "EntitlementType", "ImageType", "InputModeType", "LobbySearchCast", + "LobbySearchComparison", "LobbySearchDistance", "LobbyType", "LogLevel", + "PremiumType", "RelationshipType", "Result", "SkuType", "Status", "UserFlag", + "bind_events", + "DiscordException", "exceptions", "get_exception", + "ImageManager", + "LobbyManager", "LobbyMemberTransaction", "LobbySearchQuery", + "LobbyTransaction", + "Activity", "ActivityAssets", "ActivityParty", "ActivitySecrets", + "ActivityTimestamps", "Entitlement", "FileStat", "ImageDimensions", "ImageHandle", + "InputMode", "Lobby", "Model", "OAuth2Token", "PartySize", "Presence", "Relationship", + "Sku", "SkuPrice", "User", "UserAchievement", + "NetworkManager", + "OverlayManager", + "RelationshipManager", + "StorageManager", + "StoreManager", + "UserManager", + "VoiceManager", +] diff --git a/py_discord_sdk/discordsdk/achievement.py b/py_discord_sdk/discordsdk/achievement.py new file mode 100644 index 0000000..e6cfdaf --- /dev/null +++ b/py_discord_sdk/discordsdk/achievement.py @@ -0,0 +1,110 @@ +import ctypes +import typing as t + +from . import sdk +from .enum import Result +from .event import bind_events +from .exception import get_exception +from .model import UserAchievement + + +class AchievementManager: + _internal: sdk.IDiscordAchievementManager = None + _garbage: t.List[t.Any] + _events: sdk.IDiscordAchievementEvents + + def __init__(self): + self._garbage = [] + self._events = bind_events( + sdk.IDiscordAchievementEvents, + self._on_user_achievement_update + ) + + def _on_user_achievement_update(self, event_data, user_achievement): + self.on_user_achievement_update(UserAchievement(copy=user_achievement.contents)) + + def set_user_achievement( + self, + achievement_id: int, + percent_complete: int, + callback: t.Callable[[Result], None] + ) -> None: + """ + Updates the current user's status for a given achievement. + + Returns discordsdk.enum.Result via callback. + """ + def c_callback(callback_data, result): + self._garbage.remove(c_callback) + result = Result(result) + callback(result) + + c_callback = self._internal.set_user_achievement.argtypes[-1](c_callback) + self._garbage.append(c_callback) # prevent it from being garbage collected + + self._internal.set_user_achievement( + self._internal, + achievement_id, + percent_complete, + ctypes.c_void_p(), + c_callback + ) + + def fetch_user_achievements(self, callback: t.Callable[[Result], None]) -> None: + """ + Loads a stable list of the current user's achievements to iterate over. + + Returns discordsdk.enum.Result via callback. + """ + def c_callback(callback_data, result): + self._garbage.remove(c_callback) + result = Result(result) + callback(result) + + c_callback = self._internal.fetch_user_achievements.argtypes[-1](c_callback) + self._garbage.append(c_callback) # prevent it from being garbage collected + + self._internal.fetch_user_achievements(self._internal, ctypes.c_void_p(), c_callback) + + def count_user_achievements(self) -> int: + """ + Counts the list of a user's achievements for iteration. + """ + count = ctypes.c_int32() + self._internal.count_user_achievements(self._internal, count) + return count.value + + def get_user_achievement_at(self, index: int) -> UserAchievement: + """ + Gets the user's achievement at a given index of their list of achievements. + """ + achievement = sdk.DiscordUserAchievement() + result = Result(self._internal.get_user_achievement_at( + self._internal, + index, + achievement + )) + if result != Result.ok: + raise get_exception(result) + + return UserAchievement(internal=achievement) + + def get_user_achievement(self, achievement_id: int) -> None: + """ + Gets the user achievement for the given achievement id. + """ + achievement = sdk.DiscordUserAchievement() + result = Result(self._internal.get_user_achievement( + self._internal, + achievement_id, + achievement + )) + if result != Result.ok: + raise get_exception(result) + + return UserAchievement(internal=achievement) + + def on_user_achievement_update(self, achievement: UserAchievement) -> None: + """ + Fires when an achievement is updated for the currently connected user + """ diff --git a/py_discord_sdk/discordsdk/activity.py b/py_discord_sdk/discordsdk/activity.py new file mode 100644 index 0000000..e066574 --- /dev/null +++ b/py_discord_sdk/discordsdk/activity.py @@ -0,0 +1,179 @@ +import ctypes +import typing as t + +from . import sdk +from .enum import ActivityActionType, ActivityJoinRequestReply, Result +from .event import bind_events +from .model import Activity, User + + +class ActivityManager: + _internal: sdk.IDiscordActivityManager = None + _garbage: t.List[t.Any] + _events: sdk.IDiscordActivityEvents + + def __init__(self): + self._garbage = [] + self._events = bind_events( + sdk.IDiscordActivityEvents, + self._on_activity_join, + self._on_activity_spectate, + self._on_activity_join_request, + self._on_activity_invite + ) + + def _on_activity_join(self, event_data, secret): + self.on_activity_join(secret.decode("utf8")) + + def _on_activity_spectate(self, event_data, secret): + self.on_activity_spectate(secret.decode("utf8")) + + def _on_activity_join_request(self, event_data, user): + self.on_activity_join_request(User(copy=user.contents)) + + def _on_activity_invite(self, event_data, type, user, activity): + self.on_activity_invite(type, User(copy=user.contents), Activity(copy=activity.contents)) + + def register_command(self, command: str) -> Result: + """ + Registers a command by which Discord can launch your game. + """ + result = Result(self._internal.register_command(self._internal, command.encode("utf8"))) + return result + + def register_steam(self, steam_id: int) -> Result: + """ + Registers your game's Steam app id for the protocol `steam://run-game-id/`. + """ + result = Result(self._internal.register_steam(self._internal, steam_id)) + return result + + def update_activity(self, activity: Activity, callback: t.Callable[[Result], None]) -> None: + """ + Set a user's presence in Discord to a new activity. + + Returns discordsdk.enum.Result (int) via callback. + """ + def c_callback(callback_data, result): + self._garbage.remove(c_callback) + result = Result(result) + callback(result) + + c_callback = self._internal.update_activity.argtypes[-1](c_callback) + self._garbage.append(c_callback) # prevent it from being garbage collected + + self._internal.update_activity( + self._internal, + activity._internal, + ctypes.c_void_p(), + c_callback + ) + + def clear_activity(self, callback: t.Callable[[Result], None]) -> None: + """ + Clears a user's presence in Discord to make it show nothing. + + Returns discordsdk.enum.Result (int) via callback. + """ + def c_callback(callback_data, result): + self._garbage.remove(c_callback) + result = Result(result) + callback(result) + + c_callback = self._internal.clear_activity.argtypes[-1](c_callback) + self._garbage.append(c_callback) # prevent it from being garbage collected + + self._internal.clear_activity(self._internal, ctypes.c_void_p(), c_callback) + + def send_request_reply( + self, + user_id: int, + reply: ActivityJoinRequestReply, + callback: t.Callable[[Result], None] + ) -> None: + """ + Sends a reply to an Ask to Join request. + + Returns discordsdk.enum.Result (int) via callback. + """ + def c_callback(callback_data, result): + self._garbage.remove(c_callback) + result = Result(result) + callback(result) + + c_callback = self._internal.send_request_reply.argtypes[-1](c_callback) + self._garbage.append(c_callback) # prevent it from being garbage collected + + self._internal.send_request_reply( + self._internal, + user_id, + reply, + ctypes.c_void_p(), + c_callback + ) + + def send_invite( + self, + user_id: int, + type: ActivityActionType, + content: str, + callback: t.Callable[[Result], None] + ) -> None: + """ + Sends a game invite to a given user. + + Returns discordsdk.enum.Result (int) via callback. + """ + def c_callback(callback_data, result): + self._garbage.remove(c_callback) + result = Result(result) + callback(result) + + c_callback = self._internal.send_invite.argtypes[-1](c_callback) + self._garbage.append(c_callback) # prevent it from being garbage collected + + self._internal.send_invite( + self._internal, + user_id, + type, + content.encode("utf8"), + ctypes.c_void_p(), + c_callback + ) + + def accept_invite(self, user_id: int, callback: t.Callable[[Result], None]) -> None: + """ + Accepts a game invitation from a given userId. + + Returns discordsdk.enum.Result (int) via callback. + """ + def c_callback(callback_data, result): + self._garbage.remove(c_callback) + result = Result(result) + callback(result) + + c_callback = self._internal.accept_invite.argtypes[-1](c_callback) + self._garbage.append(c_callback) # prevent it from being garbage collected + + self._internal.accept_invite(self._internal, user_id, ctypes.c_void_p(), c_callback) + + def on_activity_join(self, join_secret: str) -> None: + """ + Fires when a user accepts a game chat invite or receives confirmation from Asking to Join. + """ + + def on_activity_spectate(self, spectate_secret: str) -> None: + """ + Fires when a user accepts a spectate chat invite or clicks the Spectate button on a user's + profile. + """ + + def on_activity_join_request(self, user: User) -> None: + """ + Fires when a user asks to join the current user's game. + """ + + def on_activity_invite(self, type: ActivityActionType, user: User, activity: Activity) -> None: + """ + Fires when the user receives a join or spectate invite. + """ diff --git a/py_discord_sdk/discordsdk/application.py b/py_discord_sdk/discordsdk/application.py new file mode 100644 index 0000000..d39ec0f --- /dev/null +++ b/py_discord_sdk/discordsdk/application.py @@ -0,0 +1,85 @@ +import ctypes +import typing as t + +from . import sdk +from .enum import Result +from .model import OAuth2Token + + +class ApplicationManager: + _internal: sdk.IDiscordApplicationManager = None + _garbage: t.List[t.Any] + _events: sdk.IDiscordApplicationEvents = None + + def __init__(self): + self._garbage = [] + + def get_current_locale(self) -> str: + """ + Get the locale the current user has Discord set to. + """ + locale = sdk.DiscordLocale() + self._internal.get_current_locale(self._internal, locale) + return locale.value.decode("utf8") + + def get_current_branch(self) -> str: + """ + Get the name of pushed branch on which the game is running. + """ + branch = sdk.DiscordBranch() + self._internal.get_current_branch(self._internal, branch) + return branch.value.decode("utf8") + + def get_oauth2_token( + self, + callback: t.Callable[[Result, t.Optional[OAuth2Token]], None] + ) -> None: + """ + Retrieve an oauth2 bearer token for the current user. + + Returns discordsdk.enum.Result (int) and OAuth2Token (str) via callback. + """ + def c_callback(callback_data, result, oauth2_token): + self._garbage.remove(c_callback) + if result == Result.ok: + callback(result, OAuth2Token(copy=oauth2_token.contents)) + else: + callback(result, None) + + c_callback = self._internal.get_oauth2_token.argtypes[-1](c_callback) + self._garbage.append(c_callback) # prevent it from being garbage collected + + self._internal.get_oauth2_token(self._internal, ctypes.c_void_p(), c_callback) + + def validate_or_exit(self, callback: t.Callable[[Result], None]) -> None: + """ + Checks if the current user has the entitlement to run this game. + + Returns discordsdk.enum.Result (int) via callback. + """ + def c_callback(callback_data, result): + self._garbage.remove(c_callback) + callback(result) + + c_callback = self._internal.validate_or_exit.argtypes[-1](c_callback) + self._garbage.append(c_callback) # prevent it from being garbage collected + + self._internal.validate_or_exit(self._internal, ctypes.c_void_p(), c_callback) + + def get_ticket(self, callback: t.Callable[[Result, t.Optional[str]], None]) -> None: + """ + Get the signed app ticket for the current user. + + Returns discordsdk.Enum.Result (int) and str via callback. + """ + def c_callback(callback_data, result, data): + self._garbage.remove(c_callback) + if result == Result.ok: + callback(result, data.contents.value.decode("utf8")) + else: + callback(result, None) + + c_callback = self._internal.get_ticket.argtypes[-1](c_callback) + self._garbage.append(c_callback) # prevent it from being garbage collected + + self._internal.get_ticket(self._internal, ctypes.c_void_p(), c_callback) diff --git a/py_discord_sdk/discordsdk/discord.py b/py_discord_sdk/discordsdk/discord.py new file mode 100644 index 0000000..d3624a1 --- /dev/null +++ b/py_discord_sdk/discordsdk/discord.py @@ -0,0 +1,202 @@ +import ctypes +import typing as t + +from . import sdk +from .achievement import AchievementManager +from .activity import ActivityManager +from .application import ApplicationManager +from .enum import CreateFlags, LogLevel, Result +from .exception import get_exception +from .image import ImageManager +from .lobby import LobbyManager +from .network import NetworkManager +from .overlay import OverlayManager +from .relationship import RelationshipManager +from .storage import StorageManager +from .store import StoreManager +from .user import UserManager +from .voice import VoiceManager + + +class Discord: + _garbage: t.List[t.Any] + + core: sdk.IDiscordCore = None + + def __init__(self, client_id: int, flags: CreateFlags, dll_base_path: str): + self._garbage = [] + + self._activity_manager = ActivityManager() + self._relationship_manager = RelationshipManager() + self._image_manager = ImageManager() + self._user_manager = UserManager() + self._lobby_manager = LobbyManager() + self._network_manager = NetworkManager() + self._overlay_manager = OverlayManager() + self._application_manager = ApplicationManager() + self._storage_manager = StorageManager() + self._store_manager = StoreManager() + self._voice_manager = VoiceManager() + self._achievement_manager = AchievementManager() + + version = sdk.DiscordVersion(2) + + params = sdk.DiscordCreateParams() + params.client_id = client_id + params.flags = flags + + sdk.DiscordCreateParamsSetDefault(params) + params.activity_events = self._activity_manager._events + params.relationship_events = self._relationship_manager._events + params.image_events = self._image_manager._events + params.user_events = self._user_manager._events + params.lobby_events = self._lobby_manager._events + params.network_events = self._network_manager._events + params.overlay_events = self._overlay_manager._events + params.application_events = self._application_manager._events + params.storage_events = self._storage_manager._events + params.store_events = self._store_manager._events + params.voice_events = self._voice_manager._events + params.achievement_events = self._achievement_manager._events + + pointer = ctypes.POINTER(sdk.IDiscordCore)() + + result = Result(sdk.build_sdk(dll_base_path)(version, ctypes.pointer(params), ctypes.pointer(pointer))) + if result != Result.ok: + raise get_exception(result) + + self.core = pointer.contents + + def __del__(self): + if self.core: + self.core.destroy(self.core) + self.core = None + + def set_log_hook(self, min_level: LogLevel, hook: t.Callable[[LogLevel, str], None]) -> None: + """ + Registers a logging callback function with the minimum level of message to receive. + """ + def c_hook(hook_data, level, message): + level = LogLevel(level) + hook(level, message.decode("utf8")) + + c_hook = self.core.set_log_hook.argtypes[-1](c_hook) + self._garbage.append(c_hook) + + self.core.set_log_hook(self.core, min_level.value, ctypes.c_void_p(), c_hook) + + def run_callbacks(self) -> None: + """ + Runs all pending SDK callbacks. + """ + result = Result(self.core.run_callbacks(self.core)) + if result != Result.ok: + raise get_exception(result) + + def get_activity_manager(self) -> ActivityManager: + """ + Fetches an instance of the manager for interfacing with activies in the SDK. + """ + if not self._activity_manager._internal: + self._activity_manager._internal = self.core.get_activity_manager(self.core).contents + + return self._activity_manager + + def get_relationship_manager(self) -> RelationshipManager: + """ + Fetches an instance of the manager for interfacing with relationships in the SDK. + """ + if not self._relationship_manager._internal: + self._relationship_manager._internal = self.core.get_relationship_manager(self.core).contents # noqa: E501 + + return self._relationship_manager + + def get_image_manager(self) -> ImageManager: + """ + Fetches an instance of the manager for interfacing with images in the SDK. + """ + if not self._image_manager._internal: + self._image_manager._internal = self.core.get_image_manager(self.core).contents + + return self._image_manager + + def get_user_manager(self) -> UserManager: + """ + Fetches an instance of the manager for interfacing with users in the SDK. + """ + if not self._user_manager._internal: + self._user_manager._internal = self.core.get_user_manager(self.core).contents + + return self._user_manager + + def get_lobby_manager(self) -> LobbyManager: + """ + Fetches an instance of the manager for interfacing with lobbies in the SDK. + """ + if not self._lobby_manager._internal: + self._lobby_manager._internal = self.core.get_lobby_manager(self.core).contents + + return self._lobby_manager + + def get_network_manager(self) -> NetworkManager: + """ + Fetches an instance of the manager for interfacing with networking in the SDK. + """ + if not self._network_manager._internal: + self._network_manager._internal = self.core.get_network_manager(self.core).contents + + return self._network_manager + + def get_overlay_manager(self) -> OverlayManager: + """ + Fetches an instance of the manager for interfacing with the overlay in the SDK. + """ + if not self._overlay_manager._internal: + self._overlay_manager._internal = self.core.get_overlay_manager(self.core).contents + + return self._overlay_manager + + def get_application_manager(self) -> ApplicationManager: + """ + Fetches an instance of the manager for interfacing with applications in the SDK. + """ + if not self._application_manager._internal: + self._application_manager._internal = self.core.get_application_manager(self.core).contents # noqa: E501 + + return self._application_manager + + def get_storage_manager(self) -> StorageManager: + """ + Fetches an instance of the manager for interfacing with storage in the SDK. + """ + if not self._storage_manager._internal: + self._storage_manager._internal = self.core.get_storage_manager(self.core).contents + + return self._storage_manager + + def get_store_manager(self) -> StoreManager: + """ + Fetches an instance of the manager for interfacing with SKUs and Entitlements in the SDK. + """ + if not self._store_manager._internal: + self._store_manager._internal = self.core.get_store_manager(self.core).contents + + return self._store_manager + + def get_voice_manager(self) -> VoiceManager: + """ + Fetches an instance of the manager for interfacing with voice chat in the SDK. + """ + if not self._voice_manager._internal: + self._voice_manager._internal = self.core.get_voice_manager(self.core).contents + + return self._voice_manager + + def get_achievement_manager(self) -> AchievementManager: + """ + Fetches an instance of the manager for interfacing with achievements in the SDK. + """ + if not self._achievement_manager._internal: + self._achievement_manager._internal = self.core.get_achievement_manager(self.core).contents # noqa: E501 + + return self._achievement_manager diff --git a/py_discord_sdk/discordsdk/enum.py b/py_discord_sdk/discordsdk/enum.py new file mode 100644 index 0000000..d7c81c1 --- /dev/null +++ b/py_discord_sdk/discordsdk/enum.py @@ -0,0 +1,166 @@ +import sys + +if sys.version_info >= (3, 6): + from enum import IntEnum, IntFlag +else: + from enum import IntEnum, IntEnum as IntFlag + + +class Result(IntEnum): + ok = 0 + service_unavailable = 1 + invalid_version = 2 + lock_failed = 3 + internal_error = 4 + invalid_payload = 5 + invalid_command = 6 + invalid_permissions = 7 + not_fetched = 8 + not_found = 9 + conflict = 10 + invalid_secret = 11 + invalid_join_secret = 12 + no_eligible_activity = 13 + invalid_invite = 14 + not_authenticated = 15 + invalid_access_token = 16 + application_mismatch = 17 + invalid_data_url = 18 + invalid_base_64 = 19 + not_filtered = 20 + lobby_full = 21 + invalid_lobby_secret = 22 + invalid_filename = 23 + invalid_file_size = 24 + invalid_entitlement = 25 + not_installed = 26 + not_running = 27 + insufficient_buffer = 28 + purchase_canceled = 29 + invalid_guild = 30 + invalid_event = 31 + invalid_channel = 32 + invalid_origin = 33 + rate_limited = 34 + oauth2_error = 35 + select_channel_timeout = 36 + get_guild_timeout = 37 + select_voice_force_required = 38 + capture_shortcut_already_listening = 39 + unauthorized_for_achievement = 40 + invalid_gift_code = 41 + purchase_error = 42 + transaction_aborted = 43 + drawing_init_failed = 44 + + +class LogLevel(IntEnum): + error = 0 + warning = 1 + info = 2 + debug = 3 + + +class CreateFlags(IntFlag): + default = 0 + no_require_discord = 1 + + +class UserFlag(IntFlag): + partner = 2 + hype_squad_events = 4 + hype_squad_house_1 = 64 + hype_squad_house_2 = 128 + hype_squad_house_3 = 256 + + +class PremiumType(IntEnum): + none_ = 0 + tier_1 = 1 + tier_2 = 2 + + +class ActivityType(IntEnum): + playing = 0 + streaming = 1 + listening = 2 + custom = 4 + + +class ActivityJoinRequestReply(IntEnum): + no = 0 + yes = 1 + ignore = 2 + + +class ActivityActionType(IntEnum): + join = 1 + spectate = 2 + + +class RelationshipType(IntEnum): + none_ = 0 + friend = 1 + blocked = 2 + pending_incoming = 3 + pending_outgoing = 4 + implicit = 5 + + +class Status(IntEnum): + offline = 0 + online = 1 + idle = 2 + do_not_disturb = 3 + + +class ImageType(IntEnum): + user = 0 + + +class LobbyType(IntEnum): + private = 1 + public = 2 + + +class LobbySearchComparison(IntEnum): + LessThanOrEqual = -2 + LessThan = -1 + Equal = 0 + GreaterThan = 1 + GreaterThanOrEqual = 2 + NotEqual = 3 + + +class LobbySearchCast(IntEnum): + String = 1 + Number = 2 + + +class LobbySearchDistance(IntEnum): + Local = 0 + Default = 1 + Extended = 2 + Global = 3 + + +class InputModeType(IntEnum): + VoiceActivity = 0 + PushToTalk = 1 + + +class SkuType(IntEnum): + Application = 1 + DLC = 2 + Consumable = 3 + Bundle = 4 + + +class EntitlementType(IntEnum): + Purchase = 1 + PremiumSubscription = 2 + DeveloperGift = 3 + TestModePurchase = 4 + FreePurchase = 5 + UserGift = 6 + PremiumPurchase = 7 diff --git a/py_discord_sdk/discordsdk/event.py b/py_discord_sdk/discordsdk/event.py new file mode 100644 index 0000000..fd9e46f --- /dev/null +++ b/py_discord_sdk/discordsdk/event.py @@ -0,0 +1,11 @@ +import ctypes +import typing as t + + +def bind_events(structure: ctypes.Structure, *methods: t.List[t.Callable[..., None]]): + contents = structure() + for index, (name, func) in enumerate(structure._fields_): + setattr(contents, name, func(methods[index])) + + pointer = ctypes.pointer(contents) + return pointer diff --git a/py_discord_sdk/discordsdk/exception.py b/py_discord_sdk/discordsdk/exception.py new file mode 100644 index 0000000..cd95f8e --- /dev/null +++ b/py_discord_sdk/discordsdk/exception.py @@ -0,0 +1,19 @@ +from .enum import Result + + +class DiscordException(Exception): + pass + + +exceptions = {} + +# we dynamically create the exceptions +for res in Result: + exception = type(res.name, (DiscordException,), {}) + + globals()[res.name] = exception + exceptions[res] = exception + + +def get_exception(result): + return exceptions.get(result, DiscordException)("result " + str(result.value)) diff --git a/py_discord_sdk/discordsdk/image.py b/py_discord_sdk/discordsdk/image.py new file mode 100644 index 0000000..764c0fe --- /dev/null +++ b/py_discord_sdk/discordsdk/image.py @@ -0,0 +1,71 @@ +import ctypes +import typing as t + +from . import sdk +from .enum import Result +from .exception import get_exception +from .model import ImageDimensions, ImageHandle + + +class ImageManager: + _internal: sdk.IDiscordImageManager = None + _garbage: t.List[t.Any] + _events: sdk.IDiscordImageEvents = None + + def __init__(self): + self._garbage = [] + + def fetch( + self, + handle: ImageHandle, + refresh: bool, + callback: t.Callable[[Result, t.Optional[ImageHandle]], None] + ) -> None: + """ + Prepares an image to later retrieve data about it. + + Returns discordsdk.enum.Result (int) and ImageHandle via callback. + """ + def c_callback(callback_data, result, handle): + self._garbage.remove(c_callback) + result = Result(result) + if result == Result.ok: + callback(result, ImageHandle(internal=handle)) + else: + callback(result, None) + + c_callback = self._internal.fetch.argtypes[-1](c_callback) + self._garbage.append(c_callback) # prevent it from being garbage collected + + self._internal.fetch( + self._internal, + handle._internal, + refresh, + ctypes.c_void_p(), + c_callback + ) + + def get_dimensions(self, handle: ImageHandle) -> ImageDimensions: + """ + Gets the dimension for the given user's avatar's source image + """ + dimensions = sdk.DiscordImageDimensions() + result = Result(self._internal.get_dimensions(self._internal, handle._internal, dimensions)) + if result != Result.ok: + raise get_exception(result) + + return ImageDimensions(internal=dimensions) + + def get_data(self, handle: ImageHandle) -> bytes: + """ + Gets the image data for a given user's avatar. + """ + dimensions = self.get_dimensions(handle) + buffer = (ctypes.c_uint8 * (dimensions.width * dimensions.height * 4))() + + result = Result(self._internal.get_data( + self._internal, handle._internal, buffer, len(buffer))) + if result != Result.ok: + raise get_exception(result) + + return bytes(buffer) diff --git a/py_discord_sdk/discordsdk/lobby.py b/py_discord_sdk/discordsdk/lobby.py new file mode 100644 index 0000000..874ae42 --- /dev/null +++ b/py_discord_sdk/discordsdk/lobby.py @@ -0,0 +1,801 @@ +import ctypes +import typing as t + +from . import sdk +from .enum import ( + LobbySearchCast, LobbySearchComparison, LobbySearchDistance, LobbyType, + Result) +from .event import bind_events +from .exception import get_exception +from .model import Lobby, User + + +class LobbyTransaction: + _internal: sdk.IDiscordLobbyTransaction + + def __init__(self, internal: sdk.IDiscordLobbyTransaction): + self._internal = internal + + def set_type(self, type: LobbyType) -> None: + """ + Marks a lobby as private or public. + """ + result = Result(self._internal.set_type(self._internal, type)) + if result != Result.ok: + raise get_exception(result) + + def set_owner(self, user_id: int) -> None: + """ + Sets a new owner for the lobby. + """ + result = Result(self._internal.set_owner(self._internal, user_id)) + if result != Result.ok: + raise get_exception(result) + + def set_capacity(self, capacity: int) -> None: + """ + Sets a new capacity for the lobby. + """ + result = Result(self._internal.set_capacity(self._internal, capacity)) + if result != Result.ok: + raise get_exception(result) + + def set_metadata(self, key: str, value: str) -> None: + """ + Sets metadata value under a given key name for the lobby. + """ + metadata_key = sdk.DiscordMetadataKey() + metadata_key.value = key.encode("utf8") + + metadata_value = sdk.DiscordMetadataValue() + metadata_value.value = value.encode("utf8") + + result = Result(self._internal.set_metadata(self._internal, metadata_key, metadata_value)) + if result != Result.ok: + raise get_exception(result) + + def delete_metadata(self, key: str) -> None: + """ + Deletes the lobby metadata for a key. + """ + metadata_key = sdk.DiscordMetadataKey() + metadata_key.value = key.encode("utf8") + + result = Result(self._internal.delete_metadata(self._internal, metadata_key)) + if result != Result.ok: + raise get_exception(result) + + def set_locked(self, locked: bool) -> None: + """ + Sets the lobby to locked or unlocked. + """ + result = Result(self._internal.set_locked(self._internal, locked)) + if result != Result.ok: + raise get_exception(result) + + +class LobbyMemberTransaction: + _internal: sdk.IDiscordLobbyMemberTransaction + + def __init__(self, internal: sdk.IDiscordLobbyMemberTransaction): + self._internal = internal + + def set_metadata(self, key: str, value: str) -> None: + """ + Sets metadata value under a given key name for the current user. + """ + metadata_key = sdk.DiscordMetadataKey() + metadata_key.value = key.encode("utf8") + + metadata_value = sdk.DiscordMetadataValue() + metadata_value.value = value.encode("utf8") + + result = Result(self._internal.set_metadata(self._internal, metadata_key, metadata_value)) + if result != Result.ok: + raise get_exception(result) + + def delete_metadata(self, key: str) -> None: + """ + Sets metadata value under a given key name for the current user. + """ + metadata_key = sdk.DiscordMetadataKey() + metadata_key.value = key.encode("utf8") + + result = Result(self._internal.delete_metadata(self._internal, metadata_key)) + if result != Result.ok: + raise get_exception(result) + + +class LobbySearchQuery: + _internal: sdk.IDiscordLobbySearchQuery + + def __init__(self, internal: sdk.IDiscordLobbySearchQuery): + self._internal = internal + + def filter( + self, + key: str, + comp: LobbySearchComparison, + cast: LobbySearchCast, + value: str + ) -> None: + """ + Filters lobbies based on metadata comparison. + """ + metadata_key = sdk.DiscordMetadataKey() + metadata_key.value = key.encode("utf8") + + metadata_value = sdk.DiscordMetadataValue() + metadata_value.value = value.encode("utf8") + + result = Result(self._internal.filter( + self._internal, + metadata_key, + comp, + cast, + metadata_value + )) + if result != Result.ok: + raise get_exception(result) + + def sort(self, key: str, cast: LobbySearchCast, value: str) -> None: + """ + Sorts the filtered lobbies based on "near-ness" to a given value. + """ + metadata_key = sdk.DiscordMetadataKey() + metadata_key.value = key.encode("utf8") + + metadata_value = sdk.DiscordMetadataValue() + metadata_value.value = value.encode("utf8") + + result = Result(self._internal.sort(self._internal, metadata_key, cast, metadata_value)) + if result != Result.ok: + raise get_exception(result) + + def limit(self, limit: int) -> None: + """ + Limits the number of lobbies returned in a search. + """ + result = Result(self._internal.limit(self._internal, limit)) + if result != Result.ok: + raise get_exception(result) + + def distance(self, distance: LobbySearchDistance) -> None: + """ + Filters lobby results to within certain regions relative to the user's location. + """ + result = Result(self._internal.distance(self._internal, distance)) + if result != Result.ok: + raise get_exception(result) + + +class LobbyManager: + _internal: sdk.IDiscordLobbyManager = None + _garbage: t.List[t.Any] + _events: sdk.IDiscordLobbyEvents + + def __init__(self): + self._garbage = [] + self._events = bind_events( + sdk.IDiscordLobbyEvents, + self._on_lobby_update, + self._on_lobby_delete, + self._on_member_connect, + self._on_member_update, + self._on_member_disconnect, + self._on_lobby_message, + self._on_speaking, + self._on_network_message + ) + + def _on_lobby_update(self, event_data, lobby_id): + self.on_lobby_update(lobby_id) + + def _on_lobby_delete(self, event_data, lobby_id, reason): + self.on_lobby_delete(lobby_id, reason) + + def _on_member_connect(self, event_data, lobby_id, user_id): + self.on_member_connect(lobby_id, user_id) + + def _on_member_update(self, event_data, lobby_id, user_id): + self.on_member_update(lobby_id, user_id) + + def _on_member_disconnect(self, event_data, lobby_id, user_id): + self.on_member_disconnect(lobby_id, user_id) + + def _on_lobby_message(self, event_data, lobby_id, user_id, data, data_length): + message = bytes(data[:data_length]).decode("utf8") + self.on_lobby_message(lobby_id, user_id, message) + + def _on_speaking(self, event_data, lobby_id, user_id, speaking): + self.on_speaking(lobby_id, user_id, speaking) + + def _on_network_message(self, event_data, lobby_id, user_id, channel_id, data, data_length): + data = bytes(data[:data_length]) + self.on_network_message(lobby_id, user_id, channel_id, data) + + def get_lobby_create_transaction(self) -> LobbyTransaction: + """ + Gets a Lobby transaction used for creating a new lobby + """ + transaction = ctypes.POINTER(sdk.IDiscordLobbyTransaction)() + result = Result(self._internal.get_lobby_create_transaction(self._internal, transaction)) + if result != Result.ok: + raise get_exception(result) + + return LobbyTransaction(internal=transaction.contents) + + def get_lobby_update_transaction(self, lobby_id: int) -> LobbyTransaction: + """ + Gets a lobby transaction used for updating an existing lobby. + """ + transaction = ctypes.POINTER(sdk.IDiscordLobbyTransaction)() + result = Result(self._internal.get_lobby_update_transaction( + self._internal, + lobby_id, + transaction + )) + if result != Result.ok: + raise get_exception(result) + + return LobbyTransaction(internal=transaction.contents) + + def get_member_update_transaction(self, lobby_id: int, user_id: int) -> LobbyMemberTransaction: + """ + Gets a new member transaction for a lobby member in a given lobby. + """ + transaction = ctypes.POINTER(sdk.IDiscordLobbyMemberTransaction)() + result = Result(self._internal.get_member_update_transaction( + self._internal, lobby_id, user_id, transaction)) + if result != Result.ok: + raise get_exception(result) + + return LobbyMemberTransaction(internal=transaction.contents) + + def create_lobby( + self, + transaction: LobbyTransaction, + callback: t.Callable[[Result, t.Optional[Lobby]], None] + ) -> None: + """ + Creates a lobby. + + Returns discordsdk.enum.Result (int) and Lobby via callback. + """ + def c_callback(callback_data, result, lobby): + self._garbage.remove(c_callback) + result = Result(result) + if result == Result.ok: + callback(result, Lobby(copy=lobby.contents)) + else: + callback(result, None) + + c_callback = self._internal.create_lobby.argtypes[-1](c_callback) + self._garbage.append(c_callback) # prevent it from being garbage collected + + self._internal.create_lobby( + self._internal, + transaction._internal, + ctypes.c_void_p(), + c_callback + ) + + def update_lobby( + self, + lobby_id: int, + transaction: LobbyTransaction, + callback: t.Callable[[Result], None] + ) -> None: + """ + Updates a lobby with data from the given transaction. + """ + def c_callback(callback_data, result): + self._garbage.remove(c_callback) + result = Result(result) + callback(result) + + c_callback = self._internal.update_lobby.argtypes[-1](c_callback) + self._garbage.append(c_callback) # prevent it from being garbage collected + + self._internal.update_lobby( + self._internal, + lobby_id, + transaction._internal, + ctypes.c_void_p(), + c_callback + ) + + def delete_lobby(self, lobby_id: int, callback: t.Callable[[Result], None]) -> None: + """ + Deletes a given lobby. + """ + def c_callback(callback_data, result): + self._garbage.remove(c_callback) + result = Result(result) + callback(result) + + c_callback = self._internal.delete_lobby.argtypes[-1](c_callback) + self._garbage.append(c_callback) # prevent it from being garbage collected + + self._internal.delete_lobby(self._internal, lobby_id, ctypes.c_void_p(), c_callback) + + def connect_lobby( + self, + lobby_id: int, + lobby_secret: str, + callback: t.Callable[[Result], None] + ) -> None: + """ + Connects the current user to a given lobby. + """ + def c_callback(callback_data, result, lobby): + self._garbage.remove(c_callback) + result = Result(result) + if result == Result.ok: + callback(result, Lobby(copy=lobby.contents)) + else: + callback(result, None) + + c_callback = self._internal.connect_lobby.argtypes[-1](c_callback) + self._garbage.append(c_callback) # prevent it from being garbage collected + + _lobby_secret = sdk.DiscordLobbySecret() + _lobby_secret.value = lobby_secret.encode("utf8") + + self._internal.connect_lobby( + self._internal, + lobby_id, + _lobby_secret, + ctypes.c_void_p(), + c_callback + ) + + def connect_lobby_with_activity_secret( + self, + activity_secret: str, + callback: t.Callable[[Result, t.Optional[Lobby]], None] + ) -> None: + """ + Connects the current user to a lobby; requires the special activity secret from the lobby + which is a concatenated lobby_id and secret. + """ + def c_callback(callback_data, result, lobby): + self._garbage.remove(c_callback) + result = Result(result) + if result == Result.ok: + callback(result, Lobby(copy=lobby.contents)) + else: + callback(result, None) + + c_callback = self._internal.connect_lobby_with_activity_secret.argtypes[-1](c_callback) + self._garbage.append(c_callback) # prevent it from being garbage collected + + _activity_secret = sdk.DiscordLobbySecret() + _activity_secret.value = activity_secret.encode("utf8") + + self._internal.connect_lobby_with_activity_secret( + self._internal, + _activity_secret, + ctypes.c_void_p(), + c_callback + ) + + def get_lobby_activity_secret(self, lobby_id: int) -> str: + """ + Gets the special activity secret for a given lobby. + """ + lobby_secret = sdk.DiscordLobbySecret() + + result = self._internal.get_lobby_activity_secret(self._internal, lobby_id, lobby_secret) + if result != Result.ok: + raise get_exception(result) + + return lobby_secret.value.decode("utf8") + + def disconnect_lobby(self, lobby_id: int, callback: t.Callable[[Result], None]) -> None: + """ + Disconnects the current user from a lobby. + + Returns discordsdk.enum.Result (int) via callback. + """ + def c_callback(callback_data, result): + self._garbage.remove(c_callback) + result = Result(result) + callback(result) + + c_callback = self._internal.disconnect_lobby.argtypes[-1](c_callback) + self._garbage.append(c_callback) # prevent it from being garbage collected + + self._internal.disconnect_lobby(self._internal, lobby_id, ctypes.c_void_p(), c_callback) + + def get_lobby(self, lobby_id: int) -> Lobby: + """ + Gets the lobby object for a given lobby id. + """ + lobby = sdk.DiscordLobby() + + result = Result(self._internal.get_lobby(self._internal, lobby_id, lobby)) + if result != Result.ok: + raise get_exception(result) + + return Lobby(internal=lobby) + + def lobby_metadata_count(self, lobby_id: int) -> int: + """ + Returns the number of metadata key/value pairs on a given lobby. + """ + count = ctypes.c_int32() + + result = Result(self._internal.lobby_metadata_count(self._internal, lobby_id, count)) + if result != Result.ok: + raise get_exception(result) + + return count.value + + def get_lobby_metadata_key(self, lobby_id: int, index: int) -> str: + """ + Returns the key for the lobby metadata at the given index. + """ + metadata_key = sdk.DiscordMetadataKey() + + result = Result(self._internal.get_lobby_metadata_key( + self._internal, + lobby_id, + index, + metadata_key + )) + if result != Result.ok: + raise get_exception(result) + + return metadata_key.value.decode("utf8") + + def get_lobby_metadata_value(self, lobby_id: int, key: str) -> str: + """ + Returns lobby metadata value for a given key and id. + """ + metadata_key = sdk.DiscordMetadataKey() + metadata_key.value = key.encode("utf8") + + metadata_value = sdk.DiscordMetadataValue() + + result = Result(self._internal.get_lobby_metadata_value( + self._internal, + lobby_id, + metadata_key, + metadata_value + )) + if result != Result.ok: + raise get_exception(result) + + return metadata_value.value.decode("utf8") + + def member_count(self, lobby_id: int) -> int: + """ + Get the number of members in a lobby. + """ + count = ctypes.c_int32() + + result = Result(self._internal.member_count(self._internal, lobby_id, count)) + if result != Result.ok: + raise get_exception(result) + + return count.value + + def get_member_user_id(self, lobby_id: int, index: int) -> int: + """ + Gets the user id of the lobby member at the given index. + """ + user_id = sdk.DiscordUserId() + + result = Result(self._internal.get_member_user_id(self._internal, lobby_id, index, user_id)) + if result != Result.ok: + raise get_exception(result) + + return user_id.value + + def get_member_user(self, lobby_id: int, user_id: int) -> User: + """ + Gets the user object for a given user id. + """ + user = sdk.DiscordUser() + + result = Result(self._internal.get_member_user(self._internal, lobby_id, user_id, user)) + if result != Result.ok: + raise get_exception(result) + + return User(internal=user) + + def member_metadata_count(self, lobby_id: int, user_id: int) -> int: + """ + Gets the number of metadata key/value pairs for the given lobby member. + """ + count = ctypes.c_int32() + + result = Result(self._internal.member_metadata_count( + self._internal, + lobby_id, + user_id, + count + )) + if result != Result.ok: + raise get_exception(result) + + return count.value + + def get_member_metadata_key(self, lobby_id: int, user_id: int, index: int) -> str: + """ + Gets the key for the lobby metadata at the given index on a lobby member. + """ + metadata_key = sdk.DiscordMetadataKey() + + result = Result(self._internal.get_member_metadata_key( + self._internal, + lobby_id, + user_id, + index, + metadata_key + )) + if result != Result.ok: + raise get_exception(result) + + return metadata_key.value.decode("utf8") + + def get_member_metadata_value(self, lobby_id: int, user_id: int, key: str) -> str: + """ + Returns user metadata for a given key. + """ + metadata_key = sdk.DiscordMetadataKey() + metadata_key.value = key.encode("utf8") + + metadata_value = sdk.DiscordMetadataValue() + + result = Result(self._internal.get_member_metadata_value( + self._internal, + lobby_id, + user_id, + metadata_key, + metadata_value + )) + if result != Result.ok: + raise get_exception(result) + + return metadata_value.value.decode("utf8") + + def update_member( + self, + lobby_id: int, + user_id: int, + transaction: LobbyMemberTransaction, + callback: t.Callable[[Result], None] + ) -> None: + """ + Updates lobby member info for a given member of the lobby. + + Returns discordsdk.enum.Result (int) via callback. + """ + def c_callback(callback_data, result): + self._garbage.remove(c_callback) + result = Result(result) + callback(result) + + c_callback = self._internal.update_member.argtypes[-1](c_callback) + self._garbage.append(c_callback) # prevent it from being garbage collected + + self._internal.update_member( + self._internal, + lobby_id, + user_id, + transaction._internal, + ctypes.c_void_p(), + c_callback + ) + + def send_lobby_message( + self, + lobby_id: int, + data: str, + callback: t.Callable[[Result], None] + ) -> None: + """ + Sends a message to the lobby on behalf of the current user. + + Returns discordsdk.Result (int) via callback. + """ + def c_callback(callback_data, result): + self._garbage.remove(c_callback) + result = Result(result) + callback(result) + + c_callback = self._internal.send_lobby_message.argtypes[-1](c_callback) + self._garbage.append(c_callback) # prevent it from being garbage collected + + data = data.encode("utf8") + data = (ctypes.c_uint8 * len(data))(*data) + self._internal.send_lobby_message( + self._internal, lobby_id, data, len(data), ctypes.c_void_p(), c_callback) + + def get_search_query(self) -> LobbySearchQuery: + """ + Creates a search object to search available lobbies. + """ + search_query = (ctypes.POINTER(sdk.IDiscordLobbySearchQuery))() + result = Result(self._internal.get_search_query(self._internal, ctypes.byref(search_query))) + if result != Result.ok: + raise get_exception(result) + + return LobbySearchQuery(internal=search_query.contents) + + def search(self, search: LobbySearchQuery, callback: t.Callable[[Result], None]) -> None: + """ + Searches available lobbies based on the search criteria chosen in the LobbySearchQuery + member functions. + + Lobbies that meet the criteria are then globally filtered, and can be accessed via + iteration with lobby_count() and get_lobby_id(). The callback fires when the list of lobbies + is stable and ready for iteration. + + Returns discordsdk.enum.Result (int) via callback. + """ + def c_callback(callback_data, result): + self._garbage.remove(c_callback) + result = Result(result) + callback(result) + + c_callback = self._internal.search.argtypes[-1](c_callback) + self._garbage.append(c_callback) # prevent it from being garbage collected + + self._internal.search(self._internal, search._internal, ctypes.c_void_p(), c_callback) + + def lobby_count(self) -> int: + """ + Get the number of lobbies that match the search. + """ + count = ctypes.c_int32() + self._internal.lobby_count(self._internal, count) + return count.value + + def get_lobby_id(self, index: int) -> int: + """ + Returns the id for the lobby at the given index. + """ + + lobby_id = sdk.DiscordLobbyId() + + result = Result(self._internal.get_lobby_id(self._internal, index, lobby_id)) + if result != Result.ok: + raise get_exception(result) + + return lobby_id.value + + def connect_voice(self, lobby_id: int, callback: t.Callable[[Result], None]) -> None: + """ + Connects to the voice channel of the current lobby. + + Returns discordsdk.enum.Result (int) via callback. + """ + def c_callback(callback_data, result): + self._garbage.remove(c_callback) + result = Result(result) + callback(result) + + c_callback = self._internal.connect_voice.argtypes[-1](c_callback) + self._garbage.append(c_callback) # prevent it from being garbage collected + + self._internal.connect_voice(self._internal, lobby_id, ctypes.c_void_p(), c_callback) + + def disconnect_voice(self, lobby_id: int, callback: t.Callable[[Result], None]) -> None: + """ + Disconnects from the voice channel of a given lobby. + + Returns discordsdk.enum.Result (int) via callback. + """ + def c_callback(callback_data, result): + self._garbage.remove(c_callback) + result = Result(result) + callback(result) + + c_callback = self._internal.disconnect_voice.argtypes[-1](c_callback) + self._garbage.append(c_callback) # prevent it from being garbage collected + + self._internal.disconnect_voice(self._internal, lobby_id, ctypes.c_void_p(), c_callback) + + def on_lobby_update(self, lobby_id: int) -> None: + """ + Fires when a lobby is updated. + """ + + def on_lobby_delete(self, lobby_id: int, reason: str) -> None: + """ + Fired when a lobby is deleted. + """ + + def on_member_connect(self, lobby_id: int, user_id: int) -> None: + """ + Fires when a new member joins the lobby. + """ + + def on_member_update(self, lobby_id: int, user_id: int) -> None: + """ + Fires when data for a lobby member is updated. + """ + + def on_member_disconnect(self, lobby_id: int, user_id: int) -> None: + """ + Fires when a member leaves the lobby. + """ + + def on_lobby_message(self, lobby_id: int, user_id: int, message: str) -> None: + """ + Fires when a message is sent to the lobby. + """ + + def on_speaking(self, lobby_id: int, user_id: int, speaking: bool) -> None: + """ + Fires when a user connected to voice starts or stops speaking. + """ + + def connect_network(self, lobby_id: int) -> None: + """ + Connects to the networking layer for the given lobby ID. + """ + result = Result(self._internal.connect_network(self._internal, lobby_id)) + if result != Result.ok: + raise get_exception(result) + + def disconnect_network(self, lobby_id: int) -> None: + """ + Disconnects from the networking layer for the given lobby ID. + """ + result = Result(self._internal.disconnect_network(self._internal, lobby_id)) + if result != Result.ok: + raise get_exception(result) + + def flush_network(self) -> None: + """ + Flushes the network. Call this when you're done sending messages. + """ + result = Result(self._internal.flush_network(self._internal)) + if result != Result.ok: + raise get_exception(result) + + def open_network_channel(self, lobby_id: int, channel_id: int, reliable: bool) -> None: + """ + Opens a network channel to all users in a lobby on the given channel number. No need to + iterate over everyone! + """ + result = Result(self._internal.open_network_channel( + self._internal, + lobby_id, + channel_id, + reliable + )) + if result != Result.ok: + raise get_exception(result) + + def send_network_message( + self, + lobby_id: int, + user_id: int, + channel_id: int, + data: bytes + ) -> None: + """ + Sends a network message to the given user ID that is a member of the given lobby ID over + the given channel ID. + """ + data = (ctypes.c_uint8 * len(data))(*data) + result = Result(self._internal.send_network_message( + self._internal, + lobby_id, + user_id, + channel_id, + data, + len(data) + )) + if result != Result.ok: + raise get_exception(result) + + def on_network_message(self, lobby_id: int, user_id: int, channel_id: int, data: bytes) -> None: + """ + Fires when the user receives a message from the lobby's networking layer. + """ diff --git a/py_discord_sdk/discordsdk/model.py b/py_discord_sdk/discordsdk/model.py new file mode 100644 index 0000000..cabe83b --- /dev/null +++ b/py_discord_sdk/discordsdk/model.py @@ -0,0 +1,329 @@ +import ctypes +from enum import Enum + +from . import sdk +from .enum import (EntitlementType, ImageType, InputModeType, LobbyType, + RelationshipType, SkuType, Status) + + +class Model: + def __init__(self, **kwargs): + self._internal = kwargs.get("internal", self._struct_()) + if "copy" in kwargs: + ctypes.memmove( + ctypes.byref(self._internal), + ctypes.byref(kwargs["copy"]), + ctypes.sizeof(self._struct_) + ) + + self._fields = {} + + for field, ftype in self._fields_: + self._fields[field] = ftype + if issubclass(ftype, Model): + setattr(self, "_" + field, ftype(internal=getattr(self._internal, field))) + + def __getattribute__(self, key): + if key.startswith("_"): + return super().__getattribute__(key) + else: + ftype = self._fields[key] + value = getattr(self._internal, key) + if ftype == int: + return int(value) + elif ftype == str: + return value.decode("utf8") + elif ftype == bool: + return bool(value) + elif issubclass(ftype, Model): + return getattr(self, "_" + key) + elif issubclass(ftype, Enum): + return ftype(int(value)) + else: + raise TypeError(ftype) + + def __setattr__(self, key, value): + if key.startswith("_"): + super().__setattr__(key, value) + else: + ftype = self._fields[key] + if ftype == int: + value = int(value) + setattr(self._internal, key, value) + elif ftype == str: + value = value.encode("utf8") + setattr(self._internal, key, value) + elif ftype == bool: + value = bool(value) + setattr(self._internal, key, value) + elif issubclass(ftype, Model): + setattr(self, "_" + key, value) + setattr(self._internal, key, value._internal) + elif issubclass(ftype, Enum): + setattr(self._internal, key, value.value) + else: + raise TypeError(ftype) + + def __dir__(self): + return super().__dir__() + list(self._fields.keys()) + + +class User(Model): + _struct_ = sdk.DiscordUser + _fields_ = [ + ("id", int), + ("username", str), + ("discriminator", str), + ("avatar", str), + ("bot", bool), + ] + + id: int + username: str + discriminator: str + avatar: str + bot: str + + +class ActivityTimestamps(Model): + _struct_ = sdk.DiscordActivityTimestamps + _fields_ = [ + ("start", int), + ("end", int), + ] + + start: int + end: int + + +class ActivityAssets(Model): + _struct_ = sdk.DiscordActivityAssets + _fields_ = [ + ("large_image", str), + ("large_text", str), + ("small_image", str), + ("small_text", str), + ] + + large_image: str + large_text: str + small_image: str + small_text: str + + +class PartySize(Model): + _struct_ = sdk.DiscordPartySize + _fields_ = [ + ("current_size", int), + ("max_size", int), + ] + + current_size: int + max_size: int + + +class ActivityParty(Model): + _struct_ = sdk.DiscordActivityParty + _fields_ = [ + ("id", str), + ("size", PartySize), + ] + + id: str + size: PartySize + + +class ActivitySecrets(Model): + _struct_ = sdk.DiscordActivitySecrets + _fields_ = [ + ("match", str), + ("join", str), + ("spectate", str), + ] + + match: str + join: str + spectate: str + + +class Activity(Model): + _struct_ = sdk.DiscordActivity + _fields_ = [ + ("application_id", int), + ("name", str), + ("state", str), + ("details", str), + ("timestamps", ActivityTimestamps), + ("assets", ActivityAssets), + ("party", ActivityParty), + ("secrets", ActivitySecrets), + ("instance", bool), + ] + + application_id: int + name: str + state: str + details: str + timestamps: ActivityTimestamps + assets: ActivityAssets + party: ActivityParty + secrets: ActivitySecrets + instance: bool + + +class Presence(Model): + _struct_ = sdk.DiscordPresence + _fields_ = [ + ("status", Status), + ("activity", Activity), + ] + + status: Status + activity: Activity + + +class Relationship(Model): + _struct_ = sdk.DiscordRelationship + _fields_ = [ + ("type", RelationshipType), + ("user", User), + ("presence", Presence), + ] + + type: RelationshipType + user: User + presence: Presence + + +class ImageDimensions(Model): + _struct_ = sdk.DiscordImageDimensions + _fields_ = [ + ("width", int), + ("height", int), + ] + + width: int + height: int + + +class ImageHandle(Model): + _struct_ = sdk.DiscordImageHandle + _fields_ = [ + ("type", ImageType), + ("id", int), + ("size", int), + ] + + type: ImageType + id: int + size: int + + +class OAuth2Token(Model): + _struct_ = sdk.DiscordOAuth2Token + _fields_ = [ + ("access_token", str), + ("scopes", str), + ("expires", int), + ] + + access_token: str + scopes: str + expires: str + + +class Lobby(Model): + _struct_ = sdk.DiscordLobby + _fields_ = [ + ("id", int), + ("type", LobbyType), + ("owner_id", int), + ("secret", str), + ("capacity", int), + ("locked", bool), + ] + + id: int + type: LobbyType + owner_id: int + secret: str + capacity: int + locked: bool + + +class InputMode(Model): + _struct_ = sdk.DiscordInputMode + _fields_ = [ + ("type", InputModeType), + ("shortcut", str), + ] + + type: InputModeType + shortcut: str + + +class FileStat(Model): + _struct_ = sdk.DiscordFileStat + _fields_ = [ + ("filename", str), + ("size", int), + ("last_modified", int), + ] + + filename: str + size: int + last_modified: int + + +class UserAchievement(Model): + _struct_ = sdk.DiscordUserAchievement + _fields_ = [ + ("user_id", str), + ("achievement_id", int), + ("percent_complete", int), + ("unlocked_at", str), + ] + + user_id: str + achievement_id: int + percent_complete: int + unlocked_at: str + + +class SkuPrice(Model): + _struct_ = sdk.DiscordSkuPrice + _fields_ = [ + ("amount", int), + ("currency", str), + ] + + amount: int + currency: str + + +class Sku(Model): + _struct_ = sdk.DiscordSku + _fields_ = [ + ("id", int), + ("type", SkuType), + ("name", str), + ("price", SkuPrice), + ] + + id: int + type: SkuType + name: str + price: SkuPrice + + +class Entitlement(Model): + _struct_ = sdk.DiscordEntitlement + _fields_ = [ + ("id", int), + ("type", EntitlementType), + ("sku_id", int), + ] + + id: int + type: EntitlementType + sku_id: int diff --git a/py_discord_sdk/discordsdk/network.py b/py_discord_sdk/discordsdk/network.py new file mode 100644 index 0000000..6c10a4b --- /dev/null +++ b/py_discord_sdk/discordsdk/network.py @@ -0,0 +1,111 @@ +import ctypes +import typing as t + +from . import sdk +from .enum import Result +from .event import bind_events +from .exception import get_exception + + +class NetworkManager: + _internal: sdk.IDiscordNetworkManager = None + _garbage: t.List[t.Any] + _events: sdk.IDiscordNetworkEvents + + def __init__(self): + self._events = bind_events( + sdk.IDiscordNetworkEvents, + self._on_message, + self._on_route_update + ) + + def _on_message(self, event_data, peer_id, channel_id, data, data_length): + data = bytes(data[:data_length]) + self.on_message(peer_id, channel_id, data) + + def _on_route_update(self, event_data, route_data): + self.on_route_update(route_data.decode("utf8")) + + def get_peer_id(self) -> int: + """ + Get the networking peer_id for the current user, allowing other users to send packets to + them. + """ + peerId = sdk.DiscordNetworkPeerId() + self._internal.get_peer_id(self._internal, peerId) + return peerId.value + + def flush(self) -> None: + """ + Flushes the network. + """ + result = Result(self._internal.flush(self._internal)) + if result != Result.ok: + raise get_exception(result) + + def open_channel(self, peer_id: int, channel_id: int, reliable: bool) -> None: + """ + Opens a channel to a user with their given peer_id on the given channel number. + """ + result = Result(self._internal.open_channel(self._internal, peer_id, channel_id, reliable)) + if result != Result.ok: + raise get_exception(result) + + def open_peer(self, peer_id: int, route: str) -> None: + """ + Opens a network connection to another Discord user. + """ + route_data = ctypes.create_string_buffer(route.encode("utf8")) + result = Result(self._internal.open_peer(self._internal, peer_id, route_data)) + if result != Result.ok: + raise get_exception(result) + + def update_peer(self, peer_id: int, route: str) -> None: + """ + Updates the network connection to another Discord user. + """ + route_data = ctypes.create_string_buffer(route.encode("utf8")) + result = Result(self._internal.update_peer(self._internal, peer_id, route_data)) + if result != Result.ok: + raise get_exception(result) + + def send_message(self, peer_id: int, channel_id: int, data: bytes) -> None: + """ + Sends data to a given peer_id through the given channel. + """ + data = (ctypes.c_uint8 * len(data))(*data) + result = Result(self._internal.send_message( + self._internal, + peer_id, + channel_id, + data, + len(data) + )) + if result != Result.ok: + raise get_exception(result) + + def close_channel(self, peer_id: int, channel_id: int) -> None: + """ + Close the connection to a given user by peer_id on the given channel. + """ + result = Result(self._internal.close_channel(self._internal, peer_id, channel_id)) + if result != Result.ok: + raise get_exception(result) + + def close_peer(self, peer_id: int) -> None: + """ + Disconnects the network session to another Discord user. + """ + result = Result(self._internal.close_peer(self._internal, peer_id)) + if result != Result.ok: + raise get_exception(result) + + def on_message(self, peer_id: int, channel_id: int, data: bytes) -> None: + """ + Fires when you receive data from another user. + """ + + def on_route_update(self, route: str) -> None: + """ + Fires when your networking route has changed. + """ diff --git a/py_discord_sdk/discordsdk/overlay.py b/py_discord_sdk/discordsdk/overlay.py new file mode 100644 index 0000000..b8b9b53 --- /dev/null +++ b/py_discord_sdk/discordsdk/overlay.py @@ -0,0 +1,104 @@ +import ctypes +import typing as t + +from . import sdk +from .enum import ActivityActionType, Result +from .event import bind_events + + +class OverlayManager: + _internal: sdk.IDiscordOverlayManager = None + _garbage: t.List[t.Any] + _events: sdk.IDiscordOverlayEvents + + def __init__(self): + self._garbage = [] + self._events = bind_events( + sdk.IDiscordOverlayEvents, + self._on_toggle + ) + + def _on_toggle(self, event_data, locked): + self.on_toggle(locked) + + def is_enabled(self) -> bool: + """ + Check whether the user has the overlay enabled or disabled. + """ + enabled = ctypes.c_bool() + self._internal.is_enabled(self._internal, enabled) + return enabled.value + + def is_locked(self) -> bool: + """ + Check if the overlay is currently locked or unlocked + """ + locked = ctypes.c_bool() + self._internal.is_locked(self._internal, locked) + return locked.value + + def set_locked(self, locked: bool, callback: t.Callable[[Result], None]) -> None: + """ + Locks or unlocks input in the overlay. + """ + def c_callback(event_data, result): + self._garbage.remove(c_callback) + result = Result(result) + callback(result) + + c_callback = self._internal.set_locked.argtypes[-1](c_callback) + self._garbage.append(c_callback) # prevent it from being garbage collected + + self._internal.set_locked(self._internal, locked, ctypes.c_void_p(), c_callback) + + def open_activity_invite( + self, + type: ActivityActionType, + callback: t.Callable[[Result], None] + ) -> None: + """ + Opens the overlay modal for sending game invitations to users, channels, and servers. + """ + def c_callback(event_data, result): + self._garbage.remove(c_callback) + result = Result(result) + callback(result) + + c_callback = self._internal.open_activity_invite.argtypes[-1](c_callback) + self._garbage.append(c_callback) # prevent it from being garbage collected + + self._internal.open_activity_invite(self._internal, type, ctypes.c_void_p(), c_callback) + + def open_guild_invite(self, code: str, callback: t.Callable[[Result], None]) -> None: + """ + Opens the overlay modal for joining a Discord guild, given its invite code. + """ + def c_callback(event_data, result): + self._garbage.remove(c_callback) + result = Result(result) + callback(result) + + c_callback = self._internal.open_guild_invite.argtypes[-1](c_callback) + self._garbage.append(c_callback) # prevent it from being garbage collected + + code = ctypes.c_char_p(code.encode("utf8")) + self._internal.open_guild_invite(self._internal, code, ctypes.c_void_p(), c_callback) + + def open_voice_settings(self, callback: t.Callable[[Result], None]) -> None: + """ + Opens the overlay widget for voice settings for the currently connected application. + """ + def c_callback(event_data, result): + self._garbage.remove(c_callback) + result = Result(result) + callback(result) + + c_callback = self._internal.open_voice_settings.argtypes[-1](c_callback) + self._garbage.append(c_callback) # prevent it from being garbage collected + + self._internal.open_voice_settings(self._internal, ctypes.c_void_p(), c_callback) + + def on_toggle(self, locked: bool) -> None: + """ + Fires when the overlay is locked or unlocked (a.k.a. opened or closed) + """ diff --git a/py_discord_sdk/discordsdk/relationship.py b/py_discord_sdk/discordsdk/relationship.py new file mode 100644 index 0000000..9769d80 --- /dev/null +++ b/py_discord_sdk/discordsdk/relationship.py @@ -0,0 +1,84 @@ +import ctypes +import typing as t + +from . import sdk +from .enum import Result +from .event import bind_events +from .exception import get_exception +from .model import Relationship + + +class RelationshipManager: + _internal: sdk.IDiscordRelationshipManager = None + _garbage: t.List[t.Any] + _events: sdk.IDiscordRelationshipEvents + + def __init__(self): + self._garbage = [] + self._events = bind_events( + sdk.IDiscordRelationshipEvents, + self._on_refresh, + self._on_relationship_update + ) + + def _on_refresh(self, event_data): + self.on_refresh() + + def _on_relationship_update(self, event_data, relationship): + self.on_relationship_update(Relationship(copy=relationship.contents)) + + def filter(self, filter: t.Callable[[Relationship], None]) -> None: + """ + Filters a user's relationship list by a boolean condition. + """ + def c_filter(filter_data, relationship): + return bool(filter(Relationship(copy=relationship.contents))) + + c_filter = self._internal.filter.argtypes[-1](c_filter) + + self._internal.filter(self._internal, ctypes.c_void_p(), c_filter) + + def get(self, user_id: int) -> Relationship: + """ + Get the relationship between the current user and a given user by id. + """ + pointer = sdk.DiscordRelationship() + result = Result(self._internal.get(self._internal, user_id, pointer)) + if result != Result.ok: + raise get_exception(result) + + return Relationship(internal=pointer) + + def get_at(self, index: int) -> Relationship: + """ + Get the relationship at a given index when iterating over a list of relationships. + """ + pointer = sdk.DiscordRelationship() + result = Result(self._internal.get_at(self._internal, index, pointer)) + if result != Result.ok: + raise get_exception(result) + + return Relationship(internal=pointer) + + def count(self) -> int: + """ + Get the number of relationships that match your filter. + """ + count = ctypes.c_int32() + result = Result(self._internal.count(self._internal, count)) + if result != Result.ok: + raise get_exception(result) + + return count.value + + def on_refresh(self) -> None: + """ + Fires at initialization when Discord has cached a snapshot of the current status of all + your relationships. + """ + + def on_relationship_update(self, relationship: Relationship) -> None: + """ + Fires when a relationship in the filtered list changes, like an updated presence or user + attribute. + """ diff --git a/py_discord_sdk/discordsdk/sdk.py b/py_discord_sdk/discordsdk/sdk.py new file mode 100644 index 0000000..8fcf2ad --- /dev/null +++ b/py_discord_sdk/discordsdk/sdk.py @@ -0,0 +1,1538 @@ +import ctypes +import typing as t +from os.path import join +from sys import platform + + +def build_sdk(dll_base_path): + try: + dllPath = "lib/discord_game_sdk.dll" + if platform == "darwin": + dllPath = "lib/discord_game_sdk.dylib" + elif platform == 'linux' or platform == 'linux2': + dllPath = "lib/discord_game_sdk.so" + dll = ctypes.CDLL(join(dll_base_path, dllPath)) + except FileNotFoundError: + raise FileNotFoundError( + "Could not locate Discord's SDK DLLs. Check that they are in the /lib directory relative to the folder that the program is executed from.") # noqa: E501 + + return dll.DiscordCreate + + +DiscordClientId = ctypes.c_int64 +DiscordVersion = ctypes.c_int32 +DiscordSnowflake = ctypes.c_int64 +DiscordTimestamp = ctypes.c_int64 +DiscordUserId = DiscordSnowflake +DiscordLocale = ctypes.c_char * 128 +DiscordBranch = ctypes.c_char * 4096 +DiscordLobbyId = DiscordSnowflake +DiscordLobbySecret = ctypes.c_char * 128 +DiscordMetadataKey = ctypes.c_char * 256 +DiscordMetadataValue = ctypes.c_char * 4096 +DiscordNetworkPeerId = ctypes.c_uint64 +DiscordNetworkChannelId = ctypes.c_uint8 +DiscordPath = ctypes.c_char * 4096 +DiscordDateTime = ctypes.c_char * 64 + + +class DiscordUser(ctypes.Structure): + _fields_ = [ + ("id", DiscordUserId), + ("username", ctypes.c_char * 256), + ("discriminator", ctypes.c_char * 8), + ("avatar", ctypes.c_char * 128), + ("bot", ctypes.c_bool), + ] + + +class DiscordOAuth2Token(ctypes.Structure): + _fields_ = [ + ("access_token", ctypes.c_char * 128), + ("scopes", ctypes.c_char * 1024), + ("expires", DiscordTimestamp), + ] + + +class DiscordImageHandle(ctypes.Structure): + _fields_ = [ + ("type", ctypes.c_int32), + ("id", ctypes.c_int64), + ("size", ctypes.c_uint32), + ] + + +class DiscordImageDimensions(ctypes.Structure): + _fields_ = [ + ("width", ctypes.c_uint32), + ("height", ctypes.c_uint32), + ] + + +class DiscordActivityTimestamps(ctypes.Structure): + _fields_ = [ + ("start", DiscordTimestamp), + ("end", DiscordTimestamp), + ] + + +class DiscordActivityAssets(ctypes.Structure): + _fields_ = [ + ("large_image", ctypes.c_char * 128), + ("large_text", ctypes.c_char * 128), + ("small_image", ctypes.c_char * 128), + ("small_text", ctypes.c_char * 128), + ] + + +class DiscordPartySize(ctypes.Structure): + _fields_ = [ + ("current_size", ctypes.c_int32), + ("max_size", ctypes.c_int32), + ] + + +class DiscordActivityParty(ctypes.Structure): + _fields_ = [ + ("id", ctypes.c_char * 128), + ("size", DiscordPartySize), + ] + + +class DiscordActivitySecrets(ctypes.Structure): + _fields_ = [ + ("match", ctypes.c_char * 128), + ("join", ctypes.c_char * 128), + ("spectate", ctypes.c_char * 128), + ] + + +class DiscordActivity(ctypes.Structure): + _fields_ = [ + ("type", ctypes.c_int32), + ("application_id", ctypes.c_uint64), + ("name", ctypes.c_char * 128), + ("state", ctypes.c_char * 128), + ("details", ctypes.c_char * 128), + ("timestamps", DiscordActivityTimestamps), + ("assets", DiscordActivityAssets), + ("party", DiscordActivityParty), + ("secrets", DiscordActivitySecrets), + ("instance", ctypes.c_bool), + ] + + +class DiscordPresence(ctypes.Structure): + _fields_ = [ + ("status", ctypes.c_int32), + ("activity", DiscordActivity), + ] + + +class DiscordRelationship(ctypes.Structure): + _fields_ = [ + ("type", ctypes.c_int32), + ("user", DiscordUser), + ("presence", DiscordPresence), + ] + + +class DiscordLobby(ctypes.Structure): + _fields_ = [ + ("id", DiscordLobbyId), + ("type", ctypes.c_int32), + ("owner_id", DiscordUserId), + ("secret", DiscordLobbySecret), + ("capacity", ctypes.c_uint32), + ("locked", ctypes.c_bool), + ] + + +# SDK VERSION 2.5.7+ STUFF +""" +class DiscordImeUnderline(ctypes.Structure): + _fields_ = [ + ("from", ctypes.c_int32), + ("to", ctypes.c_int32), + ("color", ctypes.c_int32), + ("background_color", ctypes.c_uint32), + ("thick", ctypes.c_bool), + ] + +class DiscordRect(ctypes.Structure): + _fields_ = [ + ("left", ctypes.c_int32), + ("top", ctypes.c_int32), + ("right", ctypes.c_int32), + ("bottom", ctypes.c_int32), + ] +""" + + +class DiscordFileStat(ctypes.Structure): + _fields_ = [ + ("filename", ctypes.c_char * 260), + ("size", ctypes.c_uint64), + ("last_modified", ctypes.c_uint64), + ] + + +class DiscordEntitlement(ctypes.Structure): + _fields_ = [ + ("id", DiscordSnowflake), + ("type", ctypes.c_int32), + ("sku_id", DiscordSnowflake), + ] + + +class DiscordSkuPrice(ctypes.Structure): + _fields_ = [ + ("amount", ctypes.c_uint32), + ("currency", ctypes.c_char * 16), + ] + + +class DiscordSku(ctypes.Structure): + _fields_ = [ + ("id", DiscordSnowflake), + ("type", ctypes.c_int32), + ("name", ctypes.c_char * 256), + ("price", DiscordSkuPrice), + ] + + +class DiscordInputMode(ctypes.Structure): + _fields_ = [ + ("type", ctypes.c_int32), + ("shortcut", ctypes.c_char * 256), + ] + + +class DiscordUserAchievement(ctypes.Structure): + _fields_ = [ + ("user_id", DiscordSnowflake), + ("achievement_id", DiscordSnowflake), + ("percent_complete", ctypes.c_uint8), + ("unlocked_at", DiscordDateTime), + ] + + +class IDiscordLobbyTransaction(ctypes.Structure): + pass + + +IDiscordLobbyTransaction._fields_ = [ + ("set_type", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordLobbyTransaction), + ctypes.c_int32 + )), + ("set_owner", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordLobbyTransaction), + DiscordUserId + )), + ("set_capacity", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordLobbyTransaction), + ctypes.c_uint32 + )), + ("set_metadata", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordLobbyTransaction), + DiscordMetadataKey, + DiscordMetadataValue + )), + ("delete_metadata", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordLobbyTransaction), + DiscordMetadataKey + )), + ("set_locked", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordLobbyTransaction), + ctypes.c_bool + )), +] + + +class IDiscordLobbyMemberTransaction(ctypes.Structure): + pass + + +IDiscordLobbyMemberTransaction._fields_ = [ + ("set_metadata", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordLobbyMemberTransaction), + DiscordMetadataKey, DiscordMetadataValue + )), + ("delete_metadata", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordLobbyMemberTransaction), + DiscordMetadataKey + )), +] + + +class IDiscordLobbySearchQuery(ctypes.Structure): + pass + + +IDiscordLobbySearchQuery._fields_ = [ + ("filter", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordLobbySearchQuery), + DiscordMetadataKey, + ctypes.c_int32, + ctypes.c_int32, + DiscordMetadataValue + )), + ("sort", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordLobbySearchQuery), + DiscordMetadataKey, + ctypes.c_int32, + DiscordMetadataValue + )), + ("limit", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordLobbySearchQuery), + ctypes.c_uint32 + )), + ("distance", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordLobbySearchQuery), + ctypes.c_int32 + )), +] + +IDiscordApplicationEvents = ctypes.c_void_p + + +class IDiscordApplicationManager(ctypes.Structure): + pass + + +IDiscordApplicationManager._fields_ = [ + ("validate_or_exit", ctypes.CFUNCTYPE( + None, + ctypes.POINTER(IDiscordApplicationManager), + ctypes.c_void_p, + ctypes.CFUNCTYPE( + None, + ctypes.c_void_p, + ctypes.c_int32 + ) + )), + ("get_current_locale", ctypes.CFUNCTYPE( + None, + ctypes.POINTER(IDiscordApplicationManager), + ctypes.POINTER(DiscordLocale) + )), + ("get_current_branch", ctypes.CFUNCTYPE( + None, + ctypes.POINTER(IDiscordApplicationManager), + ctypes.POINTER(DiscordBranch) + )), + ("get_oauth2_token", ctypes.CFUNCTYPE( + None, + ctypes.POINTER(IDiscordApplicationManager), + ctypes.c_void_p, + ctypes.CFUNCTYPE( + None, + ctypes.c_void_p, + ctypes.c_int32, + ctypes.POINTER(DiscordOAuth2Token) + ) + )), + ("get_ticket", ctypes.CFUNCTYPE( + None, + ctypes.POINTER(IDiscordApplicationManager), + ctypes.c_void_p, + ctypes.CFUNCTYPE( + None, + ctypes.c_void_p, + ctypes.c_int32, + ctypes.c_char_p + ) + )), +] + + +class IDiscordUserEvents(ctypes.Structure): + _fields_ = [ + ("on_current_user_update", ctypes.CFUNCTYPE( + None, + ctypes.c_void_p + )), + ] + + +class IDiscordUserManager(ctypes.Structure): + pass + + +IDiscordUserManager._fields_ = [ + ("get_current_user", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordUserManager), + ctypes.POINTER(DiscordUser) + )), + ("get_user", ctypes.CFUNCTYPE( + None, + ctypes.POINTER(IDiscordUserManager), + DiscordUserId, + ctypes.c_void_p, + ctypes.CFUNCTYPE( + None, + ctypes.c_void_p, + ctypes.c_int32, + ctypes.POINTER(DiscordUser) + ) + )), + ("get_current_user_premium_type", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordUserManager), + ctypes.POINTER(ctypes.c_int32) + )), + ("current_user_has_flag", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordUserManager), + ctypes.c_int32, + ctypes.POINTER(ctypes.c_bool) + )), +] + +IDiscordImageEvents = ctypes.c_void_p + + +class IDiscordImageManager(ctypes.Structure): + pass + + +IDiscordImageManager._fields_ = [ + ("fetch", ctypes.CFUNCTYPE( + None, + ctypes.POINTER(IDiscordImageManager), + DiscordImageHandle, + ctypes.c_bool, + ctypes.c_void_p, + ctypes.CFUNCTYPE( + None, + ctypes.c_void_p, + ctypes.c_int32, + DiscordImageHandle + ) + )), + ("get_dimensions", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordImageManager), + DiscordImageHandle, + ctypes.POINTER(DiscordImageDimensions) + )), + ("get_data", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordImageManager), + DiscordImageHandle, + ctypes.POINTER(ctypes.c_uint8), + ctypes.c_uint32 + )), +] + + +class IDiscordActivityEvents(ctypes.Structure): + _fields_ = [ + ("on_activity_join", ctypes.CFUNCTYPE( + None, + ctypes.c_void_p, + ctypes.c_char_p + )), + ("on_activity_spectate", ctypes.CFUNCTYPE( + None, + ctypes.c_void_p, + ctypes.c_char_p + )), + ("on_activity_join_request", ctypes.CFUNCTYPE( + None, + ctypes.c_void_p, + ctypes.POINTER(DiscordUser) + )), + ("on_activity_invite", ctypes.CFUNCTYPE( + None, + ctypes.c_void_p, + ctypes.c_int32, + ctypes.POINTER(DiscordUser), + ctypes.POINTER(DiscordActivity) + )), + ] + + on_activity_join: t.Callable[..., None] + on_activity_spectate: t.Callable[..., None] + on_activity_join_request: t.Callable[..., None] + on_activity_invite: t.Callable[..., None] + + +class IDiscordActivityManager(ctypes.Structure): + register_command: t.Callable[..., ctypes.c_int32] + register_steam: t.Callable[..., ctypes.c_int32] + update_activity: t.Callable[..., None] + clear_activity: t.Callable[..., None] + send_request_reply: t.Callable[..., None] + send_invite: t.Callable[..., None] + accept_invite: t.Callable[..., None] + + +IDiscordActivityManager._fields_ = [ + ("register_command", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordActivityManager), + ctypes.c_char_p + )), + ("register_steam", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordActivityManager), + ctypes.c_int32 + )), + ("update_activity", ctypes.CFUNCTYPE( + None, + ctypes.POINTER(IDiscordActivityManager), + ctypes.POINTER(DiscordActivity), + ctypes.c_void_p, + ctypes.CFUNCTYPE( + None, + ctypes.c_void_p, + ctypes.c_uint32 + ) + )), + ("clear_activity", ctypes.CFUNCTYPE( + None, + ctypes.POINTER(IDiscordActivityManager), + ctypes.c_void_p, + ctypes.CFUNCTYPE( + None, + ctypes.c_void_p, + ctypes.c_int32 + ) + )), + ("send_request_reply", ctypes.CFUNCTYPE( + None, + ctypes.POINTER(IDiscordActivityManager), + DiscordUserId, + ctypes.c_int32, + ctypes.c_void_p, + ctypes.CFUNCTYPE( + None, + ctypes.c_void_p, + ctypes.c_int32 + ) + )), + ("send_invite", ctypes.CFUNCTYPE( + None, + ctypes.POINTER(IDiscordActivityManager), + DiscordUserId, + ctypes.c_int32, + ctypes.c_char_p, + ctypes.c_void_p, + ctypes.CFUNCTYPE( + None, + ctypes.c_void_p, + ctypes.c_int32 + ) + )), + ("accept_invite", ctypes.CFUNCTYPE( + None, + ctypes.POINTER(IDiscordActivityManager), + DiscordUserId, + ctypes.c_void_p, + ctypes.CFUNCTYPE( + None, + ctypes.c_void_p, + ctypes.c_int32 + ) + )), +] + + +class IDiscordRelationshipEvents(ctypes.Structure): + _fields_ = [ + ("on_refresh", ctypes.CFUNCTYPE( + None, + ctypes.c_void_p + )), + ("on_relationship_update", ctypes.CFUNCTYPE( + None, + ctypes.c_void_p, + ctypes.POINTER(DiscordRelationship) + )), + ] + + +class IDiscordRelationshipManager(ctypes.Structure): + pass + + +IDiscordRelationshipManager._fields_ = [ + ("filter", ctypes.CFUNCTYPE( + None, + ctypes.POINTER(IDiscordRelationshipManager), + ctypes.c_void_p, + ctypes.CFUNCTYPE( + ctypes.c_bool, + ctypes.c_void_p, + ctypes.POINTER(DiscordRelationship) + ) + )), + ("count", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordRelationshipManager), + ctypes.POINTER(ctypes.c_int32) + )), + ("get", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordRelationshipManager), + DiscordUserId, + ctypes.POINTER(DiscordRelationship) + )), + ("get_at", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordRelationshipManager), + ctypes.c_uint32, + ctypes.POINTER(DiscordRelationship) + )), +] + + +class IDiscordLobbyEvents(ctypes.Structure): + _fields_ = [ + ("on_lobby_update", ctypes.CFUNCTYPE( + None, + ctypes.c_void_p, + ctypes.c_int64 + )), + ("on_lobby_delete", ctypes.CFUNCTYPE( + None, + ctypes.c_void_p, + ctypes.c_int64, + ctypes.c_uint32 + )), + ("on_member_connect", ctypes.CFUNCTYPE( + None, + ctypes.c_void_p, + ctypes.c_int64, + ctypes.c_int64 + )), + ("on_member_update", ctypes.CFUNCTYPE( + None, + ctypes.c_void_p, + ctypes.c_int64, + ctypes.c_int64 + )), + ("on_member_disconnect", ctypes.CFUNCTYPE( + None, + ctypes.c_void_p, + ctypes.c_int64, + ctypes.c_int64 + )), + ("on_lobby_message", ctypes.CFUNCTYPE( + None, + ctypes.c_void_p, + ctypes.c_int64, + ctypes.c_int64, + ctypes.POINTER(ctypes.c_uint8), + ctypes.c_uint32 + )), + ("on_speaking", ctypes.CFUNCTYPE( + None, + ctypes.c_void_p, + ctypes.c_int64, + ctypes.c_int64, + ctypes.c_bool + )), + ("on_network_message", ctypes.CFUNCTYPE( + None, + ctypes.c_void_p, + ctypes.c_int64, + ctypes.c_int64, + ctypes.c_uint8, + ctypes.POINTER(ctypes.c_uint8), + ctypes.c_uint32 + )), + ] + + +class IDiscordLobbyManager(ctypes.Structure): + pass + + +IDiscordLobbyManager._fields_ = [ + ("get_lobby_create_transaction", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordLobbyManager), + ctypes.POINTER(ctypes.POINTER(IDiscordLobbyTransaction)) + )), + ("get_lobby_update_transaction", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordLobbyManager), + DiscordLobbyId, + ctypes.POINTER(ctypes.POINTER(IDiscordLobbyTransaction)) + )), + ("get_member_update_transaction", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordLobbyManager), + DiscordLobbyId, + DiscordUserId, + ctypes.POINTER(ctypes.POINTER(IDiscordLobbyMemberTransaction)) + )), + ("create_lobby", ctypes.CFUNCTYPE( + None, + ctypes.POINTER(IDiscordLobbyManager), + ctypes.POINTER(IDiscordLobbyTransaction), + ctypes.c_void_p, + ctypes.CFUNCTYPE( + None, + ctypes.c_void_p, + ctypes.c_int32, + ctypes.POINTER(DiscordLobby) + ) + )), + ("update_lobby", ctypes.CFUNCTYPE( + None, + ctypes.POINTER(IDiscordLobbyManager), + DiscordLobbyId, + ctypes.POINTER(IDiscordLobbyTransaction), + ctypes.c_void_p, + ctypes.CFUNCTYPE( + None, + ctypes.c_void_p, + ctypes.c_int32 + ) + )), + ("delete_lobby", ctypes.CFUNCTYPE( + None, + ctypes.POINTER(IDiscordLobbyManager), + DiscordLobbyId, + ctypes.c_void_p, + ctypes.CFUNCTYPE( + None, + ctypes.c_void_p, + ctypes.c_int32 + ) + )), + ("connect_lobby", ctypes.CFUNCTYPE( + None, + ctypes.POINTER(IDiscordLobbyManager), + DiscordLobbyId, + DiscordLobbySecret, + ctypes.c_void_p, + ctypes.CFUNCTYPE( + None, + ctypes.c_void_p, + ctypes.c_int32, + ctypes.POINTER(DiscordLobby) + ) + )), + ("connect_lobby_with_activity_secret", ctypes.CFUNCTYPE( + None, + ctypes.POINTER(IDiscordLobbyManager), + DiscordLobbySecret, + ctypes.c_void_p, + ctypes.CFUNCTYPE( + None, + ctypes.c_void_p, + ctypes.c_int32, + ctypes.POINTER(DiscordLobby) + ) + )), + ("disconnect_lobby", ctypes.CFUNCTYPE( + None, + ctypes.POINTER(IDiscordLobbyManager), + DiscordLobbyId, + ctypes.c_void_p, + ctypes.CFUNCTYPE( + None, + ctypes.c_void_p, + ctypes.c_int32 + ) + )), + ("get_lobby", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordLobbyManager), + DiscordLobbyId, + ctypes.POINTER(DiscordLobby) + )), + ("get_lobby_activity_secret", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordLobbyManager), + DiscordLobbyId, + ctypes.POINTER(DiscordLobbySecret) + )), + ("get_lobby_metadata_value", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordLobbyManager), + DiscordLobbyId, + DiscordMetadataKey, + ctypes.POINTER(DiscordMetadataValue) + )), + ("get_lobby_metadata_key", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordLobbyManager), + DiscordLobbyId, + ctypes.c_int32, + ctypes.POINTER(DiscordMetadataKey) + )), + ("lobby_metadata_count", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordLobbyManager), + DiscordLobbyId, + ctypes.POINTER(ctypes.c_int32) + )), + ("member_count", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordLobbyManager), + DiscordLobbyId, + ctypes.POINTER(ctypes.c_int32) + )), + ("get_member_user_id", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordLobbyManager), + DiscordLobbyId, + ctypes.c_int32, + ctypes.POINTER(DiscordUserId) + )), + ("get_member_user", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordLobbyManager), + DiscordLobbyId, + DiscordUserId, + ctypes.POINTER(DiscordUser) + )), + ("get_member_metadata_value", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordLobbyManager), + DiscordLobbyId, + DiscordUserId, + DiscordMetadataKey, + ctypes.POINTER(DiscordMetadataValue) + )), + ("get_member_metadata_key", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordLobbyManager), + DiscordLobbyId, + DiscordUserId, + ctypes.c_int32, + ctypes.POINTER(DiscordMetadataKey) + )), + ("member_metadata_count", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordLobbyManager), + DiscordLobbyId, + DiscordUserId, + ctypes.POINTER(ctypes.c_int32) + )), + ("update_member", ctypes.CFUNCTYPE( + None, + ctypes.POINTER(IDiscordLobbyManager), + DiscordLobbyId, + DiscordUserId, + ctypes.POINTER(IDiscordLobbyMemberTransaction), + ctypes.c_void_p, + ctypes.CFUNCTYPE( + None, + ctypes.c_void_p, + ctypes.c_int32 + ) + )), + ("send_lobby_message", ctypes.CFUNCTYPE( + None, + ctypes.POINTER(IDiscordLobbyManager), + DiscordLobbyId, + ctypes.POINTER(ctypes.c_uint8), + ctypes.c_uint32, + ctypes.c_void_p, + ctypes.CFUNCTYPE( + None, + ctypes.c_void_p, + ctypes.c_int32 + ) + )), + ("get_search_query", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordLobbyManager), + ctypes.POINTER(ctypes.POINTER(IDiscordLobbySearchQuery)) + )), + ("search", ctypes.CFUNCTYPE( + None, + ctypes.POINTER(IDiscordLobbyManager), + ctypes.POINTER(IDiscordLobbySearchQuery), + ctypes.c_void_p, + ctypes.CFUNCTYPE( + None, + ctypes.c_void_p, + ctypes.c_int32 + ) + )), + ("lobby_count", ctypes.CFUNCTYPE( + None, + ctypes.POINTER(IDiscordLobbyManager), + ctypes.POINTER(ctypes.c_int32) + )), + ("get_lobby_id", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordLobbyManager), + ctypes.c_int32, + ctypes.POINTER(DiscordLobbyId) + )), + ("connect_voice", ctypes.CFUNCTYPE( + None, + ctypes.POINTER(IDiscordLobbyManager), + DiscordLobbyId, + ctypes.c_void_p, + ctypes.CFUNCTYPE( + None, + ctypes.c_void_p, + ctypes.c_int32 + ) + )), + ("disconnect_voice", ctypes.CFUNCTYPE( + None, + ctypes.POINTER(IDiscordLobbyManager), + DiscordLobbyId, + ctypes.c_void_p, + ctypes.CFUNCTYPE( + None, + ctypes.c_void_p, + ctypes.c_int32 + ) + )), + ("connect_network", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordLobbyManager), + DiscordLobbyId + )), + ("disconnect_network", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordLobbyManager), + DiscordLobbyId + )), + ("flush_network", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordLobbyManager) + )), + ("open_network_channel", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordLobbyManager), + DiscordLobbyId, + ctypes.c_uint8, + ctypes.c_bool + )), + ("send_network_message", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordLobbyManager), + DiscordLobbyId, + DiscordUserId, + ctypes.c_uint8, + ctypes.POINTER(ctypes.c_uint8), + ctypes.c_uint32 + )), +] + + +class IDiscordNetworkEvents(ctypes.Structure): + _fields_ = [ + ("on_message", ctypes.CFUNCTYPE( + None, + ctypes.c_void_p, + DiscordNetworkPeerId, + DiscordNetworkChannelId, + ctypes.POINTER(ctypes.c_uint8), + ctypes.c_uint32 + )), + ("on_route_update", ctypes.CFUNCTYPE( + None, + ctypes.c_void_p, + ctypes.c_char_p + )), + ] + + +class IDiscordNetworkManager(ctypes.Structure): + pass + + +IDiscordNetworkManager._fields_ = [ + ("get_peer_id", ctypes.CFUNCTYPE( + None, + ctypes.POINTER(IDiscordNetworkManager), + ctypes.POINTER(DiscordNetworkPeerId) + )), + ("flush", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordNetworkManager) + )), + ("open_peer", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordNetworkManager), + DiscordNetworkPeerId, + ctypes.c_char_p + )), + ("update_peer", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordNetworkManager), + DiscordNetworkPeerId, + ctypes.c_char_p + )), + ("close_peer", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordNetworkManager), + DiscordNetworkPeerId + )), + ("open_channel", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordNetworkManager), + DiscordNetworkPeerId, + DiscordNetworkChannelId, + ctypes.c_bool + )), + ("close_channel", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordNetworkManager), + DiscordNetworkPeerId, + DiscordNetworkChannelId + )), + ("send_message", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordNetworkManager), + DiscordNetworkPeerId, + DiscordNetworkChannelId, + ctypes.POINTER(ctypes.c_uint8), + ctypes.c_uint32 + )), +] + + +class IDiscordOverlayEvents(ctypes.Structure): + _fields_ = [ + ("on_toggle", ctypes.CFUNCTYPE( + None, + ctypes.c_void_p, + ctypes.c_bool + )), + ] + + +class IDiscordOverlayManager(ctypes.Structure): + pass + + +IDiscordOverlayManager._fields_ = [ + ("is_enabled", ctypes.CFUNCTYPE( + None, + ctypes.POINTER(IDiscordOverlayManager), + ctypes.POINTER(ctypes.c_bool) + )), + ("is_locked", ctypes.CFUNCTYPE( + None, + ctypes.POINTER(IDiscordOverlayManager), + ctypes.POINTER(ctypes.c_bool) + )), + ("set_locked", ctypes.CFUNCTYPE( + None, + ctypes.POINTER(IDiscordOverlayManager), + ctypes.c_bool, + ctypes.c_void_p, + ctypes.CFUNCTYPE( + None, + ctypes.c_void_p, + ctypes.c_int32 + ) + )), + ("open_activity_invite", ctypes.CFUNCTYPE( + None, + ctypes.POINTER(IDiscordOverlayManager), + ctypes.c_int32, + ctypes.c_void_p, + ctypes.CFUNCTYPE( + None, + ctypes.c_void_p, + ctypes.c_int32 + ) + )), + ("open_guild_invite", ctypes.CFUNCTYPE( + None, + ctypes.POINTER(IDiscordOverlayManager), + ctypes.c_char_p, + ctypes.c_void_p, + ctypes.CFUNCTYPE( + None, + ctypes.c_void_p, + ctypes.c_int32 + ) + )), + ("open_voice_settings", ctypes.CFUNCTYPE( + None, + ctypes.POINTER(IDiscordOverlayManager), + ctypes.c_void_p, + ctypes.CFUNCTYPE( + None, + ctypes.c_void_p, + ctypes.c_int32 + ) + )), +] + +IDiscordStorageEvents = ctypes.c_void_p + + +class IDiscordStorageManager(ctypes.Structure): + pass + + +IDiscordStorageManager._fields_ = [ + ("read", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordStorageManager), + ctypes.c_char_p, + ctypes.POINTER(ctypes.c_uint8), + ctypes.c_uint32, + ctypes.POINTER(ctypes.c_uint32) + )), + ("read_async", ctypes.CFUNCTYPE( + None, + ctypes.POINTER(IDiscordStorageManager), + ctypes.c_char_p, + ctypes.c_void_p, + ctypes.CFUNCTYPE( + None, + ctypes.c_void_p, + ctypes.c_int32, + ctypes.POINTER(ctypes.c_uint8), + ctypes.c_uint32) + )), + ("read_async_partial", ctypes.CFUNCTYPE( + None, + ctypes.POINTER(IDiscordStorageManager), + ctypes.c_char_p, + ctypes.c_uint64, + ctypes.c_uint64, + ctypes.c_void_p, + ctypes.CFUNCTYPE( + None, + ctypes.c_void_p, + ctypes.c_int32, + ctypes.POINTER(ctypes.c_uint8), + ctypes.c_uint32 + ) + )), + ("write", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordStorageManager), + ctypes.c_char_p, + ctypes.POINTER(ctypes.c_uint8), + ctypes.c_uint32 + )), + ("write_async", ctypes.CFUNCTYPE( + None, + ctypes.POINTER(IDiscordStorageManager), + ctypes.c_char_p, + ctypes.POINTER(ctypes.c_uint8), + ctypes.c_uint32, + ctypes.c_void_p, + ctypes.CFUNCTYPE( + None, + ctypes.c_void_p, + ctypes.c_int32 + ) + )), + ("delete_", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordStorageManager), + ctypes.c_char_p + )), + ("exists", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordStorageManager), + ctypes.c_char_p, + ctypes.POINTER(ctypes.c_bool) + )), + ("count", ctypes.CFUNCTYPE( + None, + ctypes.POINTER(IDiscordStorageManager), + ctypes.POINTER(ctypes.c_int32) + )), + ("stat", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordStorageManager), + ctypes.c_char_p, + ctypes.POINTER(DiscordFileStat) + )), + ("stat_at", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordStorageManager), + ctypes.c_int32, + ctypes.POINTER(DiscordFileStat) + )), + ("get_path", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordStorageManager), + ctypes.POINTER(DiscordPath) + )), +] + + +class IDiscordStoreEvents(ctypes.Structure): + _fields_ = [ + ("on_entitlement_create", ctypes.CFUNCTYPE( + None, + ctypes.c_void_p, + ctypes.POINTER(DiscordEntitlement) + )), + ("on_entitlement_delete", ctypes.CFUNCTYPE( + None, + ctypes.c_void_p, + ctypes.POINTER(DiscordEntitlement) + )), + ] + + +class IDiscordStoreManager(ctypes.Structure): + pass + + +IDiscordStoreManager._fields_ = [ + ("fetch_skus", ctypes.CFUNCTYPE( + None, + ctypes.POINTER(IDiscordStoreManager), + ctypes.c_void_p, + ctypes.CFUNCTYPE( + None, + ctypes.c_void_p, + ctypes.c_int32 + ) + )), + ("count_skus", ctypes.CFUNCTYPE( + None, ctypes.POINTER(IDiscordStoreManager), + ctypes.POINTER(ctypes.c_int32) + )), + ("get_sku", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordStoreManager), + DiscordSnowflake, + ctypes.POINTER(DiscordSku) + )), + ("get_sku_at", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordStoreManager), + ctypes.c_int32, + ctypes.POINTER(DiscordSku) + )), + ("fetch_entitlements", ctypes.CFUNCTYPE( + None, + ctypes.POINTER(IDiscordStoreManager), + ctypes.c_void_p, + ctypes.CFUNCTYPE( + None, + ctypes.c_void_p, + ctypes.c_int32 + ) + )), + ("count_entitlements", ctypes.CFUNCTYPE( + None, + ctypes.POINTER(IDiscordStoreManager), + ctypes.POINTER(ctypes.c_int32) + )), + ("get_entitlement", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordStoreManager), + DiscordSnowflake, + ctypes.POINTER(DiscordEntitlement) + )), + ("get_entitlement_at", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordStoreManager), + ctypes.c_int32, + ctypes.POINTER(DiscordEntitlement) + )), + ("has_sku_entitlement", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordStoreManager), + DiscordSnowflake, + ctypes.POINTER(ctypes.c_bool) + )), + ("start_purchase", ctypes.CFUNCTYPE( + None, + ctypes.POINTER(IDiscordStoreManager), + DiscordSnowflake, + ctypes.c_void_p, + ctypes.CFUNCTYPE( + None, + ctypes.c_void_p, + ctypes.c_int32 + ) + )), +] + + +class IDiscordVoiceEvents(ctypes.Structure): + _fields_ = [ + ("on_settings_update", ctypes.CFUNCTYPE( + None, + ctypes.c_void_p + )), + ] + + +class IDiscordVoiceManager(ctypes.Structure): + pass + + +IDiscordVoiceManager._fields_ = [ + ("get_input_mode", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordVoiceManager), + ctypes.POINTER(DiscordInputMode) + )), + ("set_input_mode", ctypes.CFUNCTYPE( + None, + ctypes.POINTER(IDiscordVoiceManager), + DiscordInputMode, + ctypes.c_void_p, + ctypes.CFUNCTYPE( + None, + ctypes.c_void_p, + ctypes.c_int32 + ) + )), + ("is_self_mute", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordVoiceManager), + ctypes.POINTER(ctypes.c_bool) + )), + ("set_self_mute", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordVoiceManager), + ctypes.c_bool + )), + ("is_self_deaf", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordVoiceManager), + ctypes.POINTER(ctypes.c_bool) + )), + ("set_self_deaf", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordVoiceManager), + ctypes.c_bool + )), + ("is_local_mute", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordVoiceManager), + DiscordSnowflake, + ctypes.POINTER(ctypes.c_bool) + )), + ("set_local_mute", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordVoiceManager), + DiscordSnowflake, + ctypes.c_bool + )), + ("get_local_volume", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordVoiceManager), + DiscordSnowflake, + ctypes.POINTER(ctypes.c_uint8) + )), + ("set_local_volume", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordVoiceManager), + DiscordSnowflake, + ctypes.c_uint8 + )), +] + + +class IDiscordAchievementEvents(ctypes.Structure): + _fields_ = [ + ("on_user_achievement_update", ctypes.CFUNCTYPE( + None, + ctypes.c_void_p, + ctypes.POINTER(DiscordUserAchievement) + )), + ] + + on_user_achievement_update: t.Callable[[DiscordUserAchievement], None] + + +class IDiscordAchievementManager(ctypes.Structure): + set_user_achievement: t.Callable[..., None] + fetch_user_achievements: t.Callable[..., None] + count_user_achievements: t.Callable[..., None] + get_user_achievement: t.Callable[..., ctypes.c_int32] + get_user_achievement_at: t.Callable[..., ctypes.c_int32] + + +IDiscordAchievementManager._fields_ = [ + ("set_user_achievement", ctypes.CFUNCTYPE( + None, + ctypes.POINTER(IDiscordAchievementManager), + DiscordSnowflake, + ctypes.c_uint8, + ctypes.c_void_p, + ctypes.CFUNCTYPE( + None, + ctypes.c_void_p, + ctypes.c_int32 + ) + )), + ("fetch_user_achievements", ctypes.CFUNCTYPE( + None, + ctypes.POINTER(IDiscordAchievementManager), + ctypes.c_void_p, + ctypes.CFUNCTYPE( + None, + ctypes.c_void_p, + ctypes.c_int32 + ) + )), + ("count_user_achievements", ctypes.CFUNCTYPE( + None, + ctypes.POINTER(IDiscordAchievementManager), + ctypes.POINTER(ctypes.c_int32) + )), + ("get_user_achievement", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordAchievementManager), + DiscordSnowflake, + ctypes.POINTER(DiscordUserAchievement) + )), + ("get_user_achievement_at", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordAchievementManager), + ctypes.c_int32, + ctypes.POINTER(DiscordUserAchievement) + )), +] + +IDiscordCoreEvents = ctypes.c_void_p + + +class IDiscordCore(ctypes.Structure): + pass + + +IDiscordCore._fields_ = [ + ("destroy", ctypes.CFUNCTYPE( + None, + ctypes.POINTER(IDiscordCore) + )), + ("run_callbacks", ctypes.CFUNCTYPE( + ctypes.c_int32, + ctypes.POINTER(IDiscordCore) + )), + ("set_log_hook", ctypes.CFUNCTYPE( + None, + ctypes.POINTER(IDiscordCore), + ctypes.c_int32, + ctypes.c_void_p, + ctypes.CFUNCTYPE( + None, + ctypes.c_void_p, + ctypes.c_int32, + ctypes.c_char_p + ) + )), + ("get_application_manager", ctypes.CFUNCTYPE( + ctypes.POINTER(IDiscordApplicationManager), + ctypes.POINTER(IDiscordCore) + )), + ("get_user_manager", ctypes.CFUNCTYPE( + ctypes.POINTER(IDiscordUserManager), + ctypes.POINTER(IDiscordCore) + )), + ("get_image_manager", ctypes.CFUNCTYPE( + ctypes.POINTER(IDiscordImageManager), + ctypes.POINTER(IDiscordCore) + )), + ("get_activity_manager", ctypes.CFUNCTYPE( + ctypes.POINTER(IDiscordActivityManager), + ctypes.POINTER(IDiscordCore) + )), + ("get_relationship_manager", ctypes.CFUNCTYPE( + ctypes.POINTER(IDiscordRelationshipManager), + ctypes.POINTER(IDiscordCore) + )), + ("get_lobby_manager", ctypes.CFUNCTYPE( + ctypes.POINTER(IDiscordLobbyManager), + ctypes.POINTER(IDiscordCore) + )), + ("get_network_manager", ctypes.CFUNCTYPE( + ctypes.POINTER(IDiscordNetworkManager), + ctypes.POINTER(IDiscordCore) + )), + ("get_overlay_manager", ctypes.CFUNCTYPE( + ctypes.POINTER(IDiscordOverlayManager), + ctypes.POINTER(IDiscordCore) + )), + ("get_storage_manager", ctypes.CFUNCTYPE( + ctypes.POINTER(IDiscordStorageManager), + ctypes.POINTER(IDiscordCore) + )), + ("get_store_manager", ctypes.CFUNCTYPE( + ctypes.POINTER(IDiscordStoreManager), + ctypes.POINTER(IDiscordCore) + )), + ("get_voice_manager", ctypes.CFUNCTYPE( + ctypes.POINTER(IDiscordVoiceManager), + ctypes.POINTER(IDiscordCore) + )), + ("get_achievement_manager", ctypes.CFUNCTYPE( + ctypes.POINTER(IDiscordAchievementManager), + ctypes.POINTER(IDiscordCore) + )), +] + + +class DiscordCreateParams(ctypes.Structure): + _fields_ = [ + ("client_id", DiscordClientId), + ("flags", ctypes.c_uint64), + ("events", ctypes.POINTER(IDiscordCoreEvents)), + ("event_data", ctypes.c_void_p), + ("application_events", ctypes.POINTER(IDiscordApplicationEvents)), + ("application_version", DiscordVersion), + ("user_events", ctypes.POINTER(IDiscordUserEvents)), + ("user_version", DiscordVersion), + ("image_events", ctypes.POINTER(IDiscordImageEvents)), + ("image_version", DiscordVersion), + ("activity_events", ctypes.POINTER(IDiscordActivityEvents)), + ("activity_version", DiscordVersion), + ("relationship_events", ctypes.POINTER(IDiscordRelationshipEvents)), + ("relationship_version", DiscordVersion), + ("lobby_events", ctypes.POINTER(IDiscordLobbyEvents)), + ("lobby_version", DiscordVersion), + ("network_events", ctypes.POINTER(IDiscordNetworkEvents)), + ("network_version", DiscordVersion), + ("overlay_events", ctypes.POINTER(IDiscordOverlayEvents)), + ("overlay_version", DiscordVersion), + ("storage_events", ctypes.POINTER(IDiscordStorageEvents)), + ("storage_version", DiscordVersion), + ("store_events", ctypes.POINTER(IDiscordStoreEvents)), + ("store_version", DiscordVersion), + ("voice_events", ctypes.POINTER(IDiscordVoiceEvents)), + ("voice_version", DiscordVersion), + ("achievement_events", ctypes.POINTER(IDiscordAchievementEvents)), + ("achievement_version", DiscordVersion) + ] + + +def DiscordCreateParamsSetDefault(params): + params.application_version = 1 + params.user_version = 1 + params.image_version = 1 + params.activity_version = 1 + params.relationship_version = 1 + params.lobby_version = 1 + params.network_version = 1 + params.overlay_version = 1 + params.storage_version = 1 + params.store_version = 1 + params.voice_version = 1 + params.achievement_version = 1 diff --git a/py_discord_sdk/discordsdk/storage.py b/py_discord_sdk/discordsdk/storage.py new file mode 100644 index 0000000..187fac3 --- /dev/null +++ b/py_discord_sdk/discordsdk/storage.py @@ -0,0 +1,200 @@ +import ctypes +import typing as t + +from . import sdk +from .enum import Result +from .exception import get_exception +from .model import FileStat + + +class StorageManager: + _internal: sdk.IDiscordStorageManager = None + _garbage: t.List[t.Any] + _events: sdk.IDiscordStorageEvents = None + + def __init__(self): + self._garbage = [] + + def get_path(self) -> str: + """ + Returns the filepath to which Discord saves files if you were to use the SDK's storage + manager. + """ + path = sdk.DiscordPath() + + result = Result(self._internal.get_path(self._internal, path)) + if result != Result.ok: + raise get_exception(result) + + return path.value.decode("utf8") + + def read(self, name: str) -> bytes: + """ + Reads data synchronously from the game's allocated save file. + """ + # we need the file stat for this one, as length-fixed buffers does not exist in python + file_stat = self.stat(name) + file_size = file_stat.Size + + name = ctypes.c_char_p(name.encode("utf8")) + buffer = (ctypes.c_uint8 * file_size)() + read = ctypes.c_uint32() + + result = Result(self._internal.read(self._internal, name, buffer, len(buffer), read)) + if result != Result.ok: + raise get_exception(result) + + if read.value != file_size: + print("discord/storage.py: warning: attempting to read " + + str(file_size) + " bytes, but read " + str(read.value)) + + return bytes(buffer[:read.value]) + + def read_async( + self, + name: str, + callback: t.Callable[[Result, t.Optional[bytes]], None] + ) -> None: + """ + Reads data asynchronously from the game's allocated save file. + + Returns discordsdk.enum.Result (int) and data (bytes) via callback. + """ + def c_callback(callback_data, result, data, data_length): + self._garbage.remove(c_callback) + result = Result(result) + if result == Result.ok: + data = bytes(data[:data_length]) + callback(result, data) + else: + callback(result, None) + + c_callback = self._internal.read_async.argtypes[-1](c_callback) + self._garbage.append(c_callback) # prevent it from being garbage collected + + name = ctypes.c_char_p(name.encode("utf8")) + self._internal.read_async(self._internal, name, ctypes.c_void_p(), c_callback) + + def read_async_partial( + self, + name: str, + offset: int, + length: int, + callback: t.Callable[[Result], None] + ) -> None: + """ + Reads data asynchronously from the game's allocated save file, starting at a given offset + and up to a given length. + """ + def c_callback(callback_data, result, data, data_length): + self._garbage.remove(c_callback) + result = Result(result) + if result == Result.ok: + data = bytes(data[:data_length]) + callback(result, data) + else: + callback(result, None) + + c_callback = self._internal.read_async.argtypes[-1](c_callback) + self._garbage.append(c_callback) # prevent it from being garbage collected + + name = ctypes.c_char_p(name.encode("utf8")) + self._internal.read_async_partial( + self._internal, + name, + offset, + length, + ctypes.c_void_p(), + c_callback + ) + + def write(self, name: str, data: bytes) -> None: + """ + Writes data synchronously to disk, under the given key name. + """ + name = ctypes.c_char_p(name.encode("utf8")) + data = (ctypes.c_uint8 * len(data))(*data) + + result = Result(self._internal.write(self._internal, name, data, len(data))) + if result != Result.ok: + raise get_exception(result) + + def write_async(self, name: str, data: bytes, callback: t.Callable[[Result], None]) -> None: + """ + Writes data asynchronously to disk under the given keyname. + """ + def c_callback(callback_data, result): + self._garbage.remove(c_callback) + result = Result(result) + callback(result) + + c_callback = self._internal.write_async.argtypes[-1](c_callback) + self._garbage.append(c_callback) # prevent it from being garbage collected + + name = ctypes.c_char_p(name.encode("utf8")) + data = (ctypes.c_uint8 * len(data))(*data) + + self._internal.write_async( + self._internal, + name, + data, + len(data), + ctypes.c_void_p(), + c_callback + ) + + def delete(self, name: str) -> None: + """ + Deletes written data for the given key name. + """ + name = ctypes.c_char_p(name.encode("utf8")) + + result = Result(self._internal.delete_(self._internal, name)) + if result != Result.ok: + raise get_exception(result) + + def exists(self, name: str) -> bool: + """ + Checks if data exists for a given key name. + """ + exists = ctypes.c_bool() + name = ctypes.c_char_p(name.encode("utf8")) + + result = Result(self._internal.exists(self._internal, name, exists)) + if result != Result.ok: + raise get_exception(result) + + return exists.value + + def stat(self, name: str) -> FileStat: + """ + Returns file info for the given key name. + """ + stat = sdk.DiscordFileStat() + + name = ctypes.c_char_p(name.encode("utf8")) + result = Result(self._internal.stat(self._internal, name, stat)) + if result != Result.ok: + raise get_exception(result) + + return FileStat(internal=stat) + + def count(self) -> int: + """ + Returns the count of files, for iteration. + """ + count = ctypes.c_int32() + self._internal.count(self._internal, count) + return count.value + + def stat_at(self, index: int) -> FileStat: + """ + Returns file info for the given index when iterating over files. + """ + stat = sdk.DiscordFileStat() + + result = Result(self._internal.stat_at(self._internal, index, stat)) + if result != Result.ok: + raise get_exception(result) + + return FileStat(internal=stat) diff --git a/py_discord_sdk/discordsdk/store.py b/py_discord_sdk/discordsdk/store.py new file mode 100644 index 0000000..7c3f951 --- /dev/null +++ b/py_discord_sdk/discordsdk/store.py @@ -0,0 +1,164 @@ +import ctypes +import typing as t + +from . import sdk +from .enum import Result +from .event import bind_events +from .exception import get_exception +from .model import Entitlement, Sku + + +class StoreManager: + _internal: sdk.IDiscordStoreManager = None + _garbage: t.List[t.Any] + _events: sdk.IDiscordStoreEvents + + def __init__(self): + self._garbage = [] + self._events = bind_events( + sdk.IDiscordStoreEvents, + self._on_entitlement_create, + self._on_entitlement_delete + ) + + def _on_entitlement_create(self, event_data, entitlement): + self.on_entitlement_create(Entitlement(copy=entitlement)) + + def _on_entitlement_delete(self, event_data, entitlement): + self.on_entitlement_delete(Entitlement(copy=entitlement)) + + def fetch_skus(self, callback: t.Callable[[Result], None]) -> None: + """ + Fetches the list of SKUs for the connected application, readying them for iteration. + + Returns discordsdk.enum.Result (int) via callback. + """ + def c_callback(callback_data, result): + self._garbage.remove(c_callback) + result = Result(result) + callback(result) + + c_callback = self._internal.fetch_skus.argtypes[-1](c_callback) + self._garbage.append(c_callback) # prevent it from being garbage collected + + self._internal.fetch_skus(self._internal, ctypes.c_void_p(), c_callback) + + def count_skus(self) -> int: + """ + Get the number of SKUs readied by FetchSkus(). + """ + count = ctypes.c_int32() + self._internal.count_skus(self._internal, count) + return count.value + + def get_sku(self, sku_id: int) -> Sku: + """ + Gets a SKU by its ID. + """ + sku = sdk.DiscordSku() + + result = Result(self._internal.get_sku(sku_id, sku)) + if result != Result.ok: + raise get_exception(result) + + return Sku(internal=sku) + + def get_sku_at(self, index: int) -> Sku: + """ + Gets a SKU by index when iterating over SKUs. + """ + sku = sdk.DiscordSku() + + result = Result(self._internal.get_sku_at(index, sku)) + if result != Result.ok: + raise get_exception(result) + + return Sku(internal=sku) + + def fetch_entitlements(self, callback: t.Callable[[Result], None]) -> None: + """ + Fetches a list of entitlements to which the user is entitled. + + Returns discordsdk.enum.Result (int) via callback. + """ + def c_callback(callback_data, result): + self._garbage.remove(c_callback) + result = Result(result) + callback(result) + + c_callback = self._internal.fetch_entitlements.argtypes[-1](c_callback) + self._garbage.append(c_callback) # prevent it from being garbage collected + + self._internal.fetch_entitlements(self._internal, ctypes.c_void_p(), c_callback) + + def count_entitlements(self) -> int: + """ + Get the number of entitlements readied by FetchEntitlements(). + """ + count = ctypes.c_int32() + self._internal.count_entitlements(self._internal, count) + return count.value + + def get_entitlement(self, entitlement_id: int) -> Entitlement: + """ + Gets an entitlement by its id. + """ + entitlement = sdk.DiscordEntitlement() + + result = Result(self._internal.get_entitlement(entitlement_id, entitlement)) + if result != Result.ok: + raise get_exception(result) + + return Entitlement(internal=Sku) + + def get_entitlement_at(self, index: int) -> Entitlement: + """ + Gets an entitlement by index when iterating over a user's entitlements. + """ + entitlement = sdk.DiscordEntitlement() + + result = Result(self._internal.get_entitlement_at(index, entitlement)) + if result != Result.ok: + raise get_exception(result) + + return Entitlement(internal=Sku) + + def has_sku_entitlement(self, sku_id: int) -> bool: + """ + Returns whether or not the user is entitled to the given SKU ID. + """ + has_entitlement = ctypes.c_bool() + + result = Result(self._internal.has_sku_entitlement(sku_id, has_entitlement)) + if result != Result.ok: + raise get_exception(result) + + return has_entitlement.value + + def start_purchase(self, sku_id: int, callback: t.Callable[[Result], None]) -> None: + """ + Opens the overlay to begin the in-app purchase dialogue for the given SKU ID. + + Returns discordsdk.enum.Result (int) via callback. + """ + def c_callback(callback_data, result): + self._garbage.remove(c_callback) + result = Result(result) + callback(result) + + c_callback = self._internal.start_purchase.argtypes[-1](c_callback) + self._garbage.append(c_callback) # prevent it from being garbage collected + + self._internal.start_purchase(self._internal, sku_id, ctypes.c_void_p(), c_callback) + + def on_entitlement_create(self, entitlement: Entitlement) -> None: + """ + Fires when the connected user receives a new entitlement, either through purchase or + through a developer grant. + """ + + def on_entitlement_delete(self, entitlement: Entitlement) -> None: + """ + Fires when the connected user loses an entitlement, either by expiration, revocation, or + consumption in the case of consumable entitlements. + """ diff --git a/py_discord_sdk/discordsdk/user.py b/py_discord_sdk/discordsdk/user.py new file mode 100644 index 0000000..a561edd --- /dev/null +++ b/py_discord_sdk/discordsdk/user.py @@ -0,0 +1,81 @@ +import ctypes +import typing as t + +from . import sdk +from .enum import PremiumType, Result, UserFlag +from .event import bind_events +from .exception import get_exception +from .model import User + + +class UserManager: + _internal: sdk.IDiscordUserManager = None + _garbage: t.List[t.Any] + _events: sdk.IDiscordUserEvents + + def __init__(self): + self._garbage = [] + self._events = bind_events( + sdk.IDiscordUserEvents, + self._on_current_user_update + ) + + def _on_current_user_update(self, event_data): + self.on_current_user_update() + + def get_current_user(self) -> User: + """ + Fetch information about the currently connected user account. + """ + user = sdk.DiscordUser() + result = Result(self._internal.get_current_user(self._internal, user)) + if result != Result.ok: + raise get_exception(result) + + return User(internal=user) + + def get_user(self, user_id: int, callback: t.Callable[[Result], None]) -> None: + """ + Get user information for a given id. + + Returns discordsdk.enum.Result (int) and User via callback. + """ + def c_callback(callback_data, result, user): + self._garbage.remove(c_callback) + result = Result(result) + if result == Result.ok: + callback(result, User(copy=user.contents)) + else: + callback(result, None) + + c_callback = self._internal.get_user.argtypes[-1](c_callback) + self._garbage.append(c_callback) # prevent it from being garbage collected + + self._internal.get_user(self._internal, user_id, ctypes.c_void_p(), c_callback) + + def get_current_user_premium_type(self) -> PremiumType: + """ + Get the PremiumType for the currently connected user. + """ + premium_type = ctypes.c_int32() + result = Result(self._internal.get_current_user_premium_type(self._internal, premium_type)) + if result != Result.ok: + raise get_exception(result) + + return PremiumType(premium_type.value) + + def current_user_has_flag(self, flag: UserFlag) -> bool: + """ + See whether or not the current user has a certain UserFlag on their account. + """ + has_flag = ctypes.c_bool() + result = Result(self._internal.current_user_has_flag(self._internal, flag, has_flag)) + if result != Result.ok: + raise get_exception(result) + + return has_flag.value + + def on_current_user_update(self) -> None: + """ + Fires when the User struct of the currently connected user changes. + """ diff --git a/py_discord_sdk/discordsdk/voice.py b/py_discord_sdk/discordsdk/voice.py new file mode 100644 index 0000000..a28df9c --- /dev/null +++ b/py_discord_sdk/discordsdk/voice.py @@ -0,0 +1,136 @@ +import ctypes +import typing as t + +from . import sdk +from .enum import Result +from .event import bind_events +from .exception import get_exception +from .model import InputMode + + +class VoiceManager: + _internal: sdk.IDiscordVoiceManager = None + _garbage: t.List[t.Any] + _events: sdk.IDiscordVoiceEvents + + def __init__(self): + self._garbage = [] + self._events = bind_events( + sdk.IDiscordVoiceEvents, + self._on_settings_update + ) + + def _on_settings_update(self, event_data): + self.on_settings_update() + + def get_input_mode(self) -> InputMode: + """ + Get the current voice input mode for the user + """ + input_mode = sdk.DiscordInputMode() + result = Result(self._internal.get_input_mode(self._internal, input_mode)) + if result != Result.ok: + raise get_exception(result) + + return InputMode(internal=input_mode) + + def set_input_mode(self, inputMode: InputMode, callback: t.Callable[[Result], None]) -> None: + """ + Sets a new voice input mode for the uesr. + + Returns discordsdk.enum.Result (int) via callback. + """ + def c_callback(callback_data, result): + self._garbage.remove(c_callback) + result = Result(result) + callback(result) + + c_callback = self._internal.set_input_mode.argtypes[-1](c_callback) + self._garbage.append(c_callback) # prevent it from being garbage collected + + self._internal.set_input_mode( + self._internal, + inputMode._internal, + ctypes.c_void_p(), + c_callback + ) + + def is_self_mute(self) -> bool: + """ + Whether the connected user is currently muted. + """ + mute = ctypes.c_bool() + result = Result(self._internal.is_self_mute(self._internal, mute)) + if result != Result.ok: + raise get_exception(result) + + return mute.value + + def set_self_mute(self, mute: bool) -> None: + """ + Mutes or unmutes the currently connected user. + """ + result = Result(self._internal.set_self_mute(self._internal, mute)) + if result != Result.ok: + raise get_exception(result) + + def is_self_deaf(self) -> bool: + """ + Whether the connected user is currently deafened. + """ + deaf = ctypes.c_bool() + result = Result(self._internal.is_self_deaf(self._internal, deaf)) + if result != Result.ok: + raise get_exception(result) + + return deaf.value + + def set_self_deaf(self, deaf: bool) -> None: + """ + Deafens or undefeans the currently connected user. + """ + result = Result(self._internal.set_self_deaf(self._internal, deaf)) + if result != Result.ok: + raise get_exception(result) + + def is_local_mute(self, user_id: int) -> bool: + """ + Whether the given user is currently muted by the connected user. + """ + mute = ctypes.c_bool() + result = Result(self._internal.is_local_mute(self._internal, user_id, mute)) + if result != Result.ok: + raise get_exception(result) + + return mute.value + + def set_local_mute(self, user_id: int, mute: bool) -> None: + """ + Mutes or unmutes the given user for the currently connected user. + """ + result = Result(self._internal.set_local_mute(self._internal, user_id, mute)) + if result != Result.ok: + raise get_exception(result) + + def get_local_volume(self, user_id: int) -> int: + """ + Gets the local volume for a given user. + """ + volume = ctypes.c_uint8() + result = Result(self._internal.get_local_volume(self._internal, user_id, volume)) + if result != Result.ok: + raise get_exception(result) + + return volume.value + + def set_local_volume(self, user_id: int, volume: int) -> None: + """ + Sets the local volume for a given user. + """ + result = Result(self._internal.set_local_volume(self._internal, user_id, volume)) + if result != Result.ok: + raise get_exception(result) + + def on_settings_update(self) -> None: + # This event is not documented anywhere (yet?) + pass diff --git a/py_discord_sdk/setup.py b/py_discord_sdk/setup.py new file mode 100644 index 0000000..b70fdf3 --- /dev/null +++ b/py_discord_sdk/setup.py @@ -0,0 +1,23 @@ +import setuptools + +with open("README.md", "r") as fh: + long_description = fh.read() + +setuptools.setup( + name="discordsdk", + version="0.3dev", + author="LennyPhoenix & NathaanTFM", + author_email="lennyphoenixc@gmail.com", + description="Python wrapper around Discord's Game SDK library.", + license="LICENSE", + long_description=long_description, + long_description_content_type="text/markdown", + url="https://github.com/LennyPhoenix/py-discord-sdk", + packages=setuptools.find_packages(), + classifiers=[ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + ], + python_requires=">= 3.5", +) diff --git a/test.py b/test.py new file mode 100644 index 0000000..4182328 --- /dev/null +++ b/test.py @@ -0,0 +1,6 @@ +ShipType = 'sidewinder' +ShipName = '' +match ShipType: + case 'sidewinder': + ShipName = 'Sidewinder' + print(ShipName) \ No newline at end of file