summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJesusaves <cpntb1@ymail.com>2024-02-05 11:17:23 -0300
committerJesusaves <cpntb1@ymail.com>2024-02-05 11:17:23 -0300
commit29ffe5de3c308013742b5bd97f7d75b09bd3b427 (patch)
tree7199cecaf204701770de171d007e561589b19762
parentf6b8c0c64757c73b6f2063d3a6d93ce2f8f527d5 (diff)
downloadtkinter-29ffe5de3c308013742b5bd97f7d75b09bd3b427.tar.gz
tkinter-29ffe5de3c308013742b5bd97f7d75b09bd3b427.tar.bz2
tkinter-29ffe5de3c308013742b5bd97f7d75b09bd3b427.tar.xz
tkinter-29ffe5de3c308013742b5bd97f7d75b09bd3b427.zip
Some button aligning, a CI template, and Discord RPC
-rw-r--r--.gitignore1
-rw-r--r--.gitlab-ci.yml67
-rw-r--r--Config/config.xml263
-rw-r--r--Config/serverlistplus.xml143
-rw-r--r--Config/test.xml16
-rwxr-xr-x__main__.py108
-rwxr-xr-xdiscord_rpc/__init__.py919
-rw-r--r--discord_rpc/codes/__init__.py0
-rw-r--r--discord_rpc/codes/errorcodes.py3
-rw-r--r--discord_rpc/codes/opcodes.py5
-rw-r--r--discord_rpc/codes/statecodes.py4
-rw-r--r--discord_rpc/connection/__init__.py0
-rw-r--r--discord_rpc/connection/ipc.py387
-rw-r--r--discord_rpc/connection/rpc.py175
-rw-r--r--discord_rpc/util/__init__.py0
-rw-r--r--discord_rpc/util/backoff.py35
-rw-r--r--discord_rpc/util/limits.py32
-rw-r--r--discord_rpc/util/types.py349
-rw-r--r--discord_rpc/util/utils.py180
19 files changed, 2638 insertions, 49 deletions
diff --git a/.gitignore b/.gitignore
index 3ba8733..79f7cb3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
*.b64
+manaplus/
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000..50332e9
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,67 @@
+stages:
+ - test
+ - build
+
+pyflakes3:
+ stage: test
+ script:
+ - apt-get update
+ - apt-get -y -qq install python3 pyflakes3
+ - pyflakes3 .
+ image: ubuntu:18.04
+ allow_failure: true
+
+.compile:
+ stage: build
+ script:
+ - mkdir manaplus
+ - mv Config manaplus
+ - mkdir -p manaplus/Local/updates/updates.tmw2.org
+ - mkdir manaplus/Local/temp
+ - mkdir manaplus/Local/logs
+ - touch manaplus/Local/manaplus.log
+ - touch manaplus/Local/manaplustest.log
+ - echo "Fetching clients..."
+ - wget -t 0 -c "https://updates.tmw2.org/mana/linux/ManaPlus-x86_64.AppImage" -O manaplus/ManaVerse.AppImage
+ - wget -t 0 -c "https://updates.tmw2.org/mana/linux/Mana-x86_64.AppImage" -O manaplus/Mana.AppImage
+ - chmod +x manaplus/*.AppImage
+ - wget -t 0 -c "https://updates.tmw2.org/mana/windows/manaverse.zip" -O manaplus/manaverse.zip
+ # TODO: Unzip manaverse.zip
+ #####################################################
+ - echo "Fetching updates for - Moubootaur Legends"
+ - mkdir -p manaplus/Local/updates/updates.tmw2.org/ml/local
+ - mkdir -p manaplus/Local/updates/updates.tmw2.org/ml/fix
+ - wget "https://updates.tmw2.org/ml/TMW2.zip" -O manaplus/Local/updates/updates.tmw2.org/ml/TMW2.zip
+ - wget "https://updates.tmw2.org/ml/TMW2-2b90b80.zip" -O manaplus/Local/updates/updates.tmw2.org/ml/TMW2-2b90b80.zip
+ - wget "https://updates.tmw2.org/ml/music.zip" -O manaplus/Local/updates/updates.tmw2.org/ml/music.zip
+ - wget "https://updates.tmw2.org/ml/musicv2.zip" -O manaplus/Local/updates/updates.tmw2.org/ml/musicv2.zip
+ - wget "https://updates.tmw2.org/ml/musicv3.zip" -O manaplus/Local/updates/updates.tmw2.org/ml/musicv3.zip
+ - wget "https://updates.tmw2.org/ml/musicv4.zip" -O manaplus/Local/updates/updates.tmw2.org/ml/musicv4.zip
+ - wget "https://updates.tmw2.org/ml/Bugfix-TMW2.zip" -O manaplus/Local/updates/updates.tmw2.org/ml/Bugfix-TMW2.zip
+ #####################################################
+ - echo "Fetching updates for - TMW Classic"
+ - mkdir -p manaplus/Local/updates/updates.tmw2.org/legacy/local
+ - mkdir -p manaplus/Local/updates/updates.tmw2.org/legacy/fix
+ - wget "https://updates.tmw2.org/legacy/TMW.zip" -O manaplus/Local/updates/updates.tmw2.org/legacy/TMW.zip
+ - wget "https://updates.tmw2.org/legacy/TMW-music.zip" -O manaplus/Local/updates/updates.tmw2.org/legacy/TMW-music.zip
+ - wget "https://updates.tmw2.org/legacy/TMW-mods.zip" -O manaplus/Local/updates/updates.tmw2.org/legacy/TMW-mods.zip
+ #####################################################
+ - echo "Fetching updates for - TMW Crossroads"
+ - mkdir -p manaplus/Local/updates/updates.tmw2.org/CR/local
+ - mkdir -p manaplus/Local/updates/updates.tmw2.org/CR/fix
+ - wget "https://updates.tmw2.org/CR/TMW2.zip" -O manaplus/Local/updates/updates.tmw2.org/CR/TMW2.zip
+ #####################################################
+ # TODO: rEvolt updates, Mana updates
+ image: ubuntu:18.04
+ artifacts:
+ paths:
+ - "."
+ expire_in: 2 weeks
+ only:
+ - master
+
+sast:
+ stage: test
+include:
+- template: Security/SAST.gitlab-ci.yml
+
diff --git a/Config/config.xml b/Config/config.xml
new file mode 100644
index 0000000..5a47974
--- /dev/null
+++ b/Config/config.xml
@@ -0,0 +1,263 @@
+<?xml version="1.0"?>
+<configuration>
+ <option name="ColorAttackRange" value="0xffffff"/>
+ <option name="ColorAttackRangeBorder" value="0x000000"/>
+ <option name="ColorAttackRangeBorderDelay" value="76"/>
+ <option name="ColorAttackRangeBorderGradient" value="0"/>
+ <option name="ColorAttackRangeDelay" value="5"/>
+ <option name="ColorAttackRangeGradient" value="0"/>
+ <option name="ColorBeing" value="0xffffff"/>
+ <option name="ColorBeingDelay" value="40"/>
+ <option name="ColorBeingGradient" value="0"/>
+ <option name="ColorCollisionAirHighlight" value="0xe0e0ff"/>
+ <option name="ColorCollisionAirHighlightDelay" value="64"/>
+ <option name="ColorCollisionAirHighlightGradient" value="0"/>
+ <option name="ColorCollisionGroundtopHighlight" value="0xffff00"/>
+ <option name="ColorCollisionGroundtopHighlightDelay" value="20"/>
+ <option name="ColorCollisionGroundtopHighlightGradient" value="0"/>
+ <option name="ColorCollisionHighlight" value="0x0000c8"/>
+ <option name="ColorCollisionHighlightDelay" value="64"/>
+ <option name="ColorCollisionHighlightGradient" value="0"/>
+ <option name="ColorCollisionMonsterHighlight" value="0x2050e0"/>
+ <option name="ColorCollisionMonsterHighlightDelay" value="64"/>
+ <option name="ColorCollisionMonsterHighlightGradient" value="0"/>
+ <option name="ColorCollisionWaterHighlight" value="0x2050e0"/>
+ <option name="ColorCollisionWaterHighlightDelay" value="64"/>
+ <option name="ColorCollisionWaterHighlightGradient" value="0"/>
+ <option name="ColorDisregarded" value="0xa00000"/>
+ <option name="ColorDisregardedDelay" value="40"/>
+ <option name="ColorDisregardedGradient" value="0"/>
+ <option name="ColorElementalHp" value="0x00ff00"/>
+ <option name="ColorElementalHp2" value="0xff0000"/>
+ <option name="ColorElementalHp2Delay" value="50"/>
+ <option name="ColorElementalHp2Gradient" value="0"/>
+ <option name="ColorElementalHpDelay" value="50"/>
+ <option name="ColorElementalHpGradient" value="0"/>
+ <option name="ColorEnemy" value="0xff4040"/>
+ <option name="ColorEnemyDelay" value="40"/>
+ <option name="ColorEnemyGradient" value="0"/>
+ <option name="ColorErased" value="0xff0000"/>
+ <option name="ColorErasedDelay" value="40"/>
+ <option name="ColorErasedGradient" value="0"/>
+ <option name="ColorExpInfo" value="0xffff00"/>
+ <option name="ColorExpInfoDelay" value="40"/>
+ <option name="ColorExpInfoGradient" value="0"/>
+ <option name="ColorFloorItemText" value="0xffffff"/>
+ <option name="ColorFloorItemTextDelay" value="100"/>
+ <option name="ColorFloorItemTextGradient" value="0"/>
+ <option name="ColorFriend" value="0xb0ffb0"/>
+ <option name="ColorFriendDelay" value="40"/>
+ <option name="ColorFriendGradient" value="0"/>
+ <option name="ColorGM" value="0x00ff00"/>
+ <option name="ColorGMDelay" value="40"/>
+ <option name="ColorGMGradient" value="0"/>
+ <option name="ColorGuild" value="0xff00d8"/>
+ <option name="ColorGuildDelay" value="40"/>
+ <option name="ColorGuildGradient" value="0"/>
+ <option name="ColorHitCriticalDelay" value="40"/>
+ <option name="ColorHitCriticalGradient" value="3"/>
+ <option name="ColorHitLocalPlayerCriticalDelay" value="40"/>
+ <option name="ColorHitLocalPlayerCriticalGradient" value="3"/>
+ <option name="ColorHitLocalPlayerMiss" value="0x00ffa6"/>
+ <option name="ColorHitLocalPlayerMissDelay" value="40"/>
+ <option name="ColorHitLocalPlayerMissGradient" value="0"/>
+ <option name="ColorHitLocalPlayerMonster" value="0x00ff00"/>
+ <option name="ColorHitLocalPlayerMonsterDelay" value="40"/>
+ <option name="ColorHitLocalPlayerMonsterGradient" value="0"/>
+ <option name="ColorHitMonsterPlayer" value="0xff3232"/>
+ <option name="ColorHitMonsterPlayerDelay" value="40"/>
+ <option name="ColorHitMonsterPlayerGradient" value="0"/>
+ <option name="ColorHitPlayerMonster" value="0x0064ff"/>
+ <option name="ColorHitPlayerMonsterDelay" value="40"/>
+ <option name="ColorHitPlayerMonsterGradient" value="0"/>
+ <option name="ColorHitPlayerPlayer" value="0xff5050"/>
+ <option name="ColorHitPlayerPlayerDelay" value="40"/>
+ <option name="ColorHitPlayerPlayerGradient" value="0"/>
+ <option name="ColorHomePlace" value="0xffffff"/>
+ <option name="ColorHomePlaceBorder" value="0xffff00"/>
+ <option name="ColorHomePlaceBorderDelay" value="200"/>
+ <option name="ColorHomePlaceBorderGradient" value="0"/>
+ <option name="ColorHomePlaceDelay" value="20"/>
+ <option name="ColorHomePlaceGradient" value="0"/>
+ <option name="ColorHomunHp" value="0x00ff00"/>
+ <option name="ColorHomunHp2" value="0xff0000"/>
+ <option name="ColorHomunHp2Delay" value="50"/>
+ <option name="ColorHomunHp2Gradient" value="0"/>
+ <option name="ColorHomunHpDelay" value="50"/>
+ <option name="ColorHomunHpGradient" value="0"/>
+ <option name="ColorHomunculus" value="0xffffff"/>
+ <option name="ColorHomunculusDelay" value="40"/>
+ <option name="ColorHomunculusGradient" value="0"/>
+ <option name="ColorIgnored" value="0xff0000"/>
+ <option name="ColorIgnoredDelay" value="40"/>
+ <option name="ColorIgnoredGradient" value="0"/>
+ <option name="ColorMercHp" value="0x00ff00"/>
+ <option name="ColorMercHp2" value="0xff0000"/>
+ <option name="ColorMercHp2Delay" value="50"/>
+ <option name="ColorMercHp2Gradient" value="0"/>
+ <option name="ColorMercHpDelay" value="50"/>
+ <option name="ColorMercHpGradient" value="0"/>
+ <option name="ColorMercenary" value="0xffffff"/>
+ <option name="ColorMercenaryDelay" value="40"/>
+ <option name="ColorMercenaryGradient" value="0"/>
+ <option name="ColorMiss" value="0xffff00"/>
+ <option name="ColorMissDelay" value="40"/>
+ <option name="ColorMissGradient" value="0"/>
+ <option name="ColorMonster" value="0xff4040"/>
+ <option name="ColorMonsterAttackRange" value="0xff0000"/>
+ <option name="ColorMonsterAttackRangeDelay" value="20"/>
+ <option name="ColorMonsterAttackRangeGradient" value="0"/>
+ <option name="ColorMonsterDelay" value="40"/>
+ <option name="ColorMonsterGradient" value="0"/>
+ <option name="ColorMonsterHp" value="0x00ff00"/>
+ <option name="ColorMonsterHp2" value="0xff0000"/>
+ <option name="ColorMonsterHp2Delay" value="50"/>
+ <option name="ColorMonsterHp2Gradient" value="0"/>
+ <option name="ColorMonsterHpDelay" value="50"/>
+ <option name="ColorMonsterHpGradient" value="0"/>
+ <option name="ColorNPC" value="0xc8c8ff"/>
+ <option name="ColorNPCDelay" value="40"/>
+ <option name="ColorNPCGradient" value="0"/>
+ <option name="ColorNet" value="0x000000"/>
+ <option name="ColorNetDelay" value="64"/>
+ <option name="ColorNetGradient" value="0"/>
+ <option name="ColorParticle" value="0xffffff"/>
+ <option name="ColorParticleDelay" value="40"/>
+ <option name="ColorParticleGradient" value="0"/>
+ <option name="ColorParty" value="0xff00d8"/>
+ <option name="ColorPartyDelay" value="40"/>
+ <option name="ColorPartyGradient" value="0"/>
+ <option name="ColorPet" value="0xffffff"/>
+ <option name="ColorPetDelay" value="40"/>
+ <option name="ColorPetGradient" value="0"/>
+ <option name="ColorPickupInfo" value="0x28dc28"/>
+ <option name="ColorPickupInfoDelay" value="40"/>
+ <option name="ColorPickupInfoGradient" value="0"/>
+ <option name="ColorPlayer" value="0xffffff"/>
+ <option name="ColorPlayerDelay" value="40"/>
+ <option name="ColorPlayerGradient" value="0"/>
+ <option name="ColorPlayerHp" value="0x00ff00"/>
+ <option name="ColorPlayerHp2" value="0xff0000"/>
+ <option name="ColorPlayerHp2Delay" value="50"/>
+ <option name="ColorPlayerHp2Gradient" value="0"/>
+ <option name="ColorPlayerHpDelay" value="50"/>
+ <option name="ColorPlayerHpGradient" value="0"/>
+ <option name="ColorPortalHighlight" value="0xc80000"/>
+ <option name="ColorPortalHighlightDelay" value="40"/>
+ <option name="ColorPortalHighlightGradient" value="0"/>
+ <option name="ColorRoadPoint" value="0x000000"/>
+ <option name="ColorRoadPointDelay" value="100"/>
+ <option name="ColorRoadPointGradient" value="0"/>
+ <option name="ColorSelf" value="0xff8040"/>
+ <option name="ColorSelfDelay" value="40"/>
+ <option name="ColorSelfGradient" value="0"/>
+ <option name="ColorSkillAttackRange" value="0x000000"/>
+ <option name="ColorSkillAttackRangeDelay" value="76"/>
+ <option name="ColorSkillAttackRangeGradient" value="0"/>
+ <option name="ColorSkillUnit" value="0xffffff"/>
+ <option name="ColorSkillUnitDelay" value="40"/>
+ <option name="ColorSkillUnitGradient" value="0"/>
+ <option name="ColorTeam1" value="0x0000ff"/>
+ <option name="ColorTeam1Delay" value="40"/>
+ <option name="ColorTeam1Gradient" value="0"/>
+ <option name="ColorTeam2" value="0x00a020"/>
+ <option name="ColorTeam2Delay" value="40"/>
+ <option name="ColorTeam2Gradient" value="0"/>
+ <option name="ColorTeam3" value="0xffff20"/>
+ <option name="ColorTeam3Delay" value="40"/>
+ <option name="ColorTeam3Gradient" value="0"/>
+ <option name="ColorWalkableTileHighlight" value="0x00d000"/>
+ <option name="ColorWalkableTileHighlightDelay" value="255"/>
+ <option name="ColorWalkableTileHighlightGradient" value="0"/>
+ <option name="DebugChatSticky" value="0"/>
+ <option name="DebugChatVisible" value="0"/>
+ <option name="DebugChatWinHeight" value="123"/>
+ <option name="DebugChatWinWidth" value="600"/>
+ <option name="DebugChatWinX" value="0"/>
+ <option name="DebugChatWinY" value="477"/>
+ <option name="DebugDebugSticky" value="0"/>
+ <option name="DebugDebugVisible" value="0"/>
+ <option name="DebugDebugWinHeight" value="300"/>
+ <option name="DebugDebugWinWidth" value="400"/>
+ <option name="DebugDebugWinX" value="200"/>
+ <option name="DebugDebugWinY" value="150"/>
+ <option name="DidYouKnowSticky" value="0"/>
+ <option name="DidYouKnowWinHeight" value="400"/>
+ <option name="DidYouKnowWinWidth" value="500"/>
+ <option name="DidYouKnowWinX" value="150"/>
+ <option name="DidYouKnowWinY" value="100"/>
+ <option name="HelpSticky" value="0"/>
+ <option name="HelpWinHeight" value="400"/>
+ <option name="HelpWinWidth" value="500"/>
+ <option name="HelpWinX" value="150"/>
+ <option name="HelpWinY" value="100"/>
+ <option name="ReturnToggles" value="0"/>
+ <option name="altfpslimit" value="5"/>
+ <option name="boldFont" value="fonts/dejavusans-bold.ttf"/>
+ <option name="cfgver" value="14"/>
+ <option name="chinaFont" value="fonts/wqy-microhei.ttf"/>
+ <option name="compresstextures" value="0"/>
+ <option name="customcursor" value="1"/>
+ <option name="enableMapReduce" value="1"/>
+ <option name="enableMumble" value="0"/>
+ <option name="enableresize" value="1"/>
+ <option name="font" value="fonts/dejavusans.ttf"/>
+ <option name="fpslimit" value="60"/>
+ <option name="helpFont" value="fonts/dejavusansmono.ttf"/>
+ <option name="hwaccel" value="0"/>
+ <option name="japanFont" value="fonts/mplus-1p-regular.ttf"/>
+ <option name="npcFont" value="fonts/dejavusans.ttf"/>
+ <option name="opengl" value="0"/>
+ <option name="particleFont" value="fonts/dejavusans.ttf"/>
+ <option name="runcount" value="2"/>
+ <option name="safemode" value="0"/>
+ <option name="screen" value="0"/>
+ <option name="screenheight" value="720"/>
+ <option name="screenshotDirectory3" value="L2RhdGEvaG9tZS9qZXN1c2FsdmEvSW1hZ2Vucy9NYW5hUGx1cw=="/>
+ <option name="screenwidth" value="1280"/>
+ <option name="secureFont" value="fonts/dejavusansmono.ttf"/>
+ <option name="serverslistupdate" value="2022-11-19"/>
+ <option name="showBackground" value="1"/>
+ <option name="sound" value="1"/>
+ <option name="testInfo" value="0.0,0,0,0.0,0.0,0.0,0.0,0.,1024,0,0,0,1024,0,-"/>
+ <option name="textureSize" value="1024,0,0,0,1024,0"/>
+ <option name="useTextureSampler" value="0"/>
+ <option name="videodetected" value="1"/>
+ <!-- Customizations -->
+ <option name="MinimapShow" value="1"/>
+ <option name="MinimapSticky" value="1"/>
+ <option name="MinimapVisible" value="1"/>
+ <option name="MinimapWinHeight" value="111"/>
+ <option name="MinimapWinWidth" value="100"/>
+ <option name="MinimapWinX" value="1"/>
+ <option name="MinimapWinY" value="26"/>
+ <option name="addwatermark" value="1"/>
+ <option name="adjustPerfomance" value="1"/>
+ <option name="allowCommandsInChatTabs" value="1"/>
+ <option name="allowMoveByMouse" value="1"/>
+ <option name="allowscreensaver" value="1"/>
+ <option name="enableAttackFilter" value="1"/>
+ <option name="enableBattleTab" value="1"/>
+ <option name="enableChatLog" value="0"/>
+ <option name="enableCompoundSpriteDelay" value="1"/>
+ <option name="enableMapReduce" value="0"/>
+ <option name="enableReorderSprites" value="1"/>
+ <option name="enableTradeFilter" value="1"/>
+ <option name="enableTradeTab" value="0"/>
+ <option name="globalsFilter" value="Sagatha"/>
+ <option name="groupFriends" value="1"/>
+ <option name="keyHomunculusFire" value="-1"/>
+ <option name="showBackground" value="1"/>
+ <option name="showBadges" value="0"/>
+ <option name="showBattleEvents" value="1"/>
+ <option name="showOwnHP" value="1"/>
+ <option name="showgender" value="1"/>
+ <option name="showlevel" value="1"/>
+ <option name="showownname" value="1"/>
+ <option name="theme" value="jewelry"/>
+ <option name="useLocalTime" value="0"/>
+ <option name="visiblenames" value="1"/>
+ <option name="visiblenamespos" value="1"/>
+ <option name="ItemShortcutVisible" value="1"/>
+ <option name="ItemShortcutWinWidth" value="140"/>
+</configuration>
diff --git a/Config/serverlistplus.xml b/Config/serverlistplus.xml
new file mode 100644
index 0000000..929e68b
--- /dev/null
+++ b/Config/serverlistplus.xml
@@ -0,0 +1,143 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<serverlist version="1">
+ <server name="The Mana World" type="tmwathena" licenseType="free">
+ <connection hostname="server.themanaworld.org" althostname="52.174.196.146" port="6901" protocol="TCP" />
+ <state>PRODUCTION</state>
+ <site>https://themanaworld.org/</site>
+ <sources>
+ <free>
+ <url name="Client data">https://github.com/themanaworld/tmwa-client-data</url>
+ <url name="Server data">https://github.com/themanaworld/tmwa-server-data</url>
+ <url name="Music">https://github.com/themanaworld/tmw-music</url>
+ <url name="Server code">https://github.com/themanaworld/tmwa</url>
+ </free>
+ </sources>
+ <docs>
+ <url name="Game rules">https://www.themanaworld.org/index.php/Game_Rules</url>
+ <url name="Development">discontinued</url>
+ </docs>
+ <support>https://forums.themanaworld.org/viewtopic.php?f=1&amp;t=17737</support>
+ <registerurl>https://www.themanaworld.org/registration.php</registerurl>
+ <description>Join adventures with people from all over the world.</description>
+ <description_fr>Rejoignez de nouvelles aventures avec des personnes du monde entier.</description_fr>
+ <description_nl>Beleef avonturen met mensen van over de hele wereld.</description_nl>
+ <description_pt_BR>Faça parte de aventuras com pessoas de todo o mundo.</description_pt_BR>
+ <description_pt>Junte-se em aventuras com pessoas de todo o mundo.</description_pt>
+ <description_zh_CN>和来自全球的玩家一同冒险。</description_zh_CN>
+ <description_de>Trete Abenteuern mit Spielern aus aller Welt bei.</description_de>
+ <description_it>Inizia l'avventura insieme a giocatori da tutto il mondo.</description_it>
+ <description_ja>世界中のみんなと冒険にでかけませんか?</description_ja>
+ <description_pl>Przeżywaj przygody z ludźmi z całego świata!</description_pl>
+ <description_cs>Přidejte se k dobrodružství s lidmi z celého světa.</description_cs>
+ <description_ru>Присоединяйся к приключениям с людьми со всего мира.</description_ru>
+ <description_vls>Doe moa mee met mins'n van d'n 'ele wereld.</description_vls>
+ <description_nl_BE>Deel avonturen met mensen van over de hele wereld.</description_nl_BE>
+ <description_id>Bergabung berpetualangan bersama seluruh pemain dari berbagai belahan dunia.</description_id>
+ <description_es>Únete a la aventura con gente de todo el mundo.</description_es>
+ </server>
+
+ <server name="Moubootaur Legends (beta)" type="evol2" licenseType="free">
+ <connection hostname="server.moubootaurlegends.org" althostname="54.37.11.84" port="6901" protocol="TCP" />
+ <updateMirror>http://updates.moubootaurlegends.org</updateMirror>
+ <state>DEVELOPMENT</state>
+ <site>https://moubootaurlegends.org/</site>
+ <sources>
+ <free>
+ <url name="Client data">https://gitlab.com/TMW2/clientdata</url>
+ <url name="Server data">https://gitlab.com/TMW2/serverdata</url>
+ <url name="Music">https://gitlab.com/TMW2/evol-music</url>
+ <url name="Server code">https://gitlab.com/evol/hercules</url>
+ <url name="Server plugin">https://gitlab.com/evol/evol-hercules</url>
+ </free>
+ </sources>
+ <docs>
+ <url name="Docs">https://gitlab.com/TMW2/Docs</url>
+ </docs>
+ <description>Moubootaur Legends.</description>
+ </server>
+
+ <server name="The Mana World (Brazil)" type="tmwathena" licenseType="free">
+ <connection hostname="tmw-br.scall.org" port="6901" protocol="TCP" />
+ <state>PRODUCTION</state>
+ <site>http://tmw-br.scall.org/</site>
+ <support>http://tmw-br.scall.org/webchat</support>
+ <sources>
+ <free>
+ <url name="Server data (archive)">https://www.themanaworld.com.br/file/tmwa-server-data.txz</url>
+ </free>
+ </sources>
+ <docs>
+ <url name="Game rules">https://www.themanaworld.com.br/rules</url>
+ <url name="License info">https://www.themanaworld.com.br/license</url>
+ </docs>
+ <description>Invite your friends and explore an original world in portuguese.</description>
+ </server>
+
+ <server name="The Mana World test server" type="evol2" licenseType="free">
+ <connection hostname="testing.themanaworld.org" althostname="167.114.185.71" port="6902" protocol="TCP" />
+ <state>DEVELOPMENT</state>
+ <!--defaultUpdateHost name="TMW rEvolt updates" /-->
+ <updates name="TMW Evolved updates">
+ <host>http://updates.tmw2.org/messworld</host>
+ </updates>
+ <defaultUpdateHost name="Upstream updates" />
+ <updates name="TMW rEvolt updates">
+ <host>http://server.themanaworld.org/test-updates/</host>
+ </updates>
+ <site>http://themanaworld.org/</site>
+ <support>https://forums.themanaworld.org/viewforum.php?f=2</support>
+ <sources>
+ <free>
+ <url name="Client data">https://gitlab.com/evol/clientdata</url>
+ <url name="Server data">https://gitlab.com/evol/serverdata</url>
+ <url name="Music">https://gitlab.com/evol/evol-music</url>
+ <url name="Server code">https://gitlab.com/evol/hercules</url>
+ <url name="Server plugin">https://gitlab.com/evol/evol-hercules</url>
+ </free>
+ </sources>
+ <docs>
+ <url name="Game rules">https://www.themanaworld.org/index.php/Game_Rules</url>
+ <url name="Development">https://www.themanaworld.org/index.php/Dev:Main</url>
+ </docs>
+ <onlineListUrl>server.themanaworld.org/testing</onlineListUrl>
+ <updateMirror>http://updates.themanaworld.org/test-updates</updateMirror>
+ <description>New content can be tested here before release.</description>
+ <description_fr>Les nouveautés peuvent être testées ici avant leur sortie officielle.</description_fr>
+ <description_nl>Nieuwe inhoud kan hier, alvorens te worden uitgebracht, worden getest.</description_nl>
+ <description_pt_BR>Novo conteúdo pode ser testado aqui antes do lançamento.</description_pt_BR>
+ <description_pt>Novos conteúdos são testados aqui antes de serem lançados. </description_pt>
+ <description_zh_CN>新内容发布前可以在这里测试。</description_zh_CN>
+ <description_de>Neuer Inhalt kann hier vor Veröffentlichung getestet werden.</description_de>
+ <description_it>Server per testare i nuovi contenuti di gioco prima della release ufficiale.</description_it>
+ <description_ja>リリース前の新しいコンテンツはこちらでテストできます</description_ja>
+ <description_pl>Nowa zawartość The Mana World jest testowana tutaj przed oficjalnym wydaniem.</description_pl>
+ <description_cs>Zde můžeš otestovat nový obsah před vydáním.</description_cs>
+ <description_ru>Здесь тестируется новый контент перед релизом.</description_ru>
+ <description_vls>Nieuw'n inhod kan ier getest weur'n voar release.</description_vls>
+ <description_nl_BE>Nieuwe inhoud kan hier worden getest alvorens publiekelijk te gaan.</description_nl_BE>
+ <description_id> </description_id>
+ <description_es>Nuevos contenidos pueden ser probados aquí antes de ser liberados.</description_es>
+ </server>
+
+ <server name="The Mana World rEvolt" type="evol2" licenseType="free">
+ <connection hostname="testing.themanaworld.org" port="6904" protocol="TCP" />
+ <state>DEVELOPMENT</state>
+ <site>https://evol.tmw2.org/</site>
+ <description>TMW rEvolt</description>
+ </server>
+
+ <server name="Test-Server (M+/evol2)" type="evol2" licenseType="free">
+ <connection hostname="213.202.247.189" port="6901" protocol="TCP" />
+ <state>DEVELOPMENT</state>
+ <site>https://manaplus.germantmw.de/</site>
+ <description>ManaPlus Testserver based on evol2 (whitelist enabled)</description>
+ </server>
+
+ <server name="Test-Server (M+/tmwAthena)" type="tmwathena" licenseType="free">
+ <connection hostname="213.202.247.189" port="6902" protocol="TCP" />
+ <state>DEVELOPMENT</state>
+ <site>https://manaplus.germantmw.de/</site>
+ <description>ManaPlus Testserver based on tmwAthena (whitelist enabled)</description>
+ </server>
+
+</serverlist>
diff --git a/Config/test.xml b/Config/test.xml
new file mode 100644
index 0000000..170b073
--- /dev/null
+++ b/Config/test.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0"?>
+<configuration>
+ <option name="ChatLogLength" value="128"/>
+ <option name="customcursor" value="1"/>
+ <option name="fpslimit" value="0"/>
+ <option name="guialpha" value="0.800000"/>
+ <option name="hwaccel" value="0"/>
+ <option name="musicVolume" value="60"/>
+ <option name="opengl" value="3"/>
+ <option name="screen" value="0"/>
+ <option name="screenheight" value="600"/>
+ <option name="screenwidth" value="800"/>
+ <option name="sfxVolume" value="50"/>
+ <option name="sound" value="0"/>
+ <option name="useScreenshotDirectorySuffix" value="1"/>
+</configuration>
diff --git a/__main__.py b/__main__.py
index 26472e7..8ff730c 100755
--- a/__main__.py
+++ b/__main__.py
@@ -247,7 +247,7 @@ def launch_game(idx):
if pref["local"]:
CMD+=".AppImage"
else:
- ## Mana and M+ are not available on Windows (TODO)
+ ## Mana and M+ are not available on Windows yet (TODO)
if pref["local"]:
CMD+="Mana/manaplus.exe" # FIXME untested
else:
@@ -255,17 +255,23 @@ def launch_game(idx):
## Build the server options
OPT="-s %s -y %s -p %s -S" % (HOST, serverlist[idx]["Type"], PORT)
+ ## Mana "fix"
+ if pref["mana"]:
+ OPT=OPT[:-2]
print("%s %s" % (CMD, OPT))
+
## Local or System-Wide Config folders
if pref["local"]:
if not sys.platform.startswith('win'):
OPT+=" -C %s/Config -L %s/Local" % (CWD, CWD)
else:
OPT+=" -C %s\\Config -L %s\\Local" % (CWD.replace('/','\\'), CWD.replace('/','\\'))
- pass
+ ## Mana "fix" (TODO Untested)
+ if pref["mana"]:
+ OPT=OPT.replace(" -L ", " --localdata-dir ")
## Execute the app
- ## TODO: Threading, MLP
+ ## TODO: Threading?
if pref["shell"]:
app=execute(san("%s %s%s" % (CMD, OPT, PWD)), shell=True) # nosec
else:
@@ -280,8 +286,7 @@ def launch_game_master(idx):
stdout("[CLIENT] Mirror Lake triggered.")
## Set credentials
auth = {"vaultId": vaultId,
- "token": vaultToken,
- "world": serverlist[idx]["UUID"]}
+ "token": vaultToken}
r=vault.post(VAULT_HOST+"/getlake", json=auth, timeout=15.0)
if r.status_code == 200:
goto=r.json()["world"]
@@ -315,7 +320,33 @@ def world_select():
canva.pack()
label1 = tk.Label(root, text='World Selection', bg="#0c3251", fg="#fff")
label1.config(font=('helvetica', 14))
- canva.create_window(200, 20, window=label1)
+ canva.create_window(200, 30, window=label1)
+
+ ## Fetch soul data
+ auth = {"vaultId": vaultId,
+ "token": vaultToken}
+
+ r=vault.post(VAULT_HOST+"/souldata", json=auth, timeout=15.0)
+ if r.status_code != 200:
+ raise Exception("Request error: %d" % r.status_code)
+ dat=r.json()
+ mySoul={}
+ mySoul["level"]=dat["soulv"]
+ mySoul["exp"]=dat["soulx"]
+ mySoul["next"]=dat["varlv"]
+ mySoul["up"]=False
+ ## Newer versions of API may level you up - catch it
+ try:
+ mySoul["up"]=dat["lvlup"]
+ mySoul["home"]=dat["homew"]
+ for s in serverlist:
+ if mySoul["home"] == s["UUID"]:
+ mySoul["home"] = s["Name"]
+ break
+ if mySoul["home"] == "VAULT":
+ mySoul["home"]="Not Set"
+ except:
+ pass
## Not really necessary? Or just TODO?
## Without these, max is 10 worlds
@@ -325,19 +356,25 @@ def world_select():
## Create a list of all worlds
ypos = 60
for w in serverlist:
- ## TODO: Do not block main thread, launch this in a threading
+ ## TODO: Do not block main thread, launch this in a threading?
## TODO: Make the button width fixed, so they align better
## TODO: Image button if an icon can be found
- button = HoverButton(text=w["Name"], command=partial(launch_game_master, serverlist.index(w)), width=300, bg="#cc6600", fg="#fff", activebackground="#ee9933")
- ## TODO: Handle MLP (on the threading?)
- ## while app = 7 ...
+ button = HoverButton(text=w["Name"], command=partial(launch_game_master, serverlist.index(w)), width=40, anchor="w", bg="#cc6600", fg="#fff", activebackground="#ee9933")
## TODO: First login greeting?
- canva.create_window(100, ypos, window=button)
- ## TODO: World Info Button
+ canva.create_window(200, ypos, window=button)
+ ## TODO FIXME: World Info Button
button = HoverButton(text="?", command=partial(info_game, serverlist.index(w)), bg="#cc6600", fg="#fff", activebackground="#ee9933")
- canva.create_window(350, ypos, window=button)
+ canva.create_window(370, ypos, window=button)
ypos += 40
+ ## TODO: Footnote
+ labf = "Lv %d, %d/%d EXP" % (mySoul["level"], mySoul["exp"], mySoul["next"])
+ labelf = tk.Label(root, text=labf, bg="#0c3251", fg="#fff")
+ labelf.config(font=('helvetica', 14))
+ canva.create_window(200, 550, window=labelf)
+ labelf = tk.Label(root, text="Home: %s" % mySoul["home"], bg="#0c3251", fg="#fff")
+ labelf.config(font=('helvetica', 14))
+ canva.create_window(200, 570, window=labelf)
return
#################################################################################
@@ -416,65 +453,38 @@ savePass.set(pref["pass"] != "")
## Email
label1 = tk.Label(root, text='Email:', bg="#0c3251", fg="#fff")
label1.config(font=('helvetica', 14))
-canva.create_window(200, 40, window=label1)
+canva.create_window(180, 40, window=label1)
entry1 = tk.Entry(root)
entry1.insert(0, pref["user"])
-canva.create_window(200, 80, window=entry1)
+canva.create_window(180, 80, window=entry1)
c1 = tk.Checkbutton(root, text="Remember", variable=saveMail, bg="#0c3251", fg="#f70")
-canva.create_window(350, 80, window=c1)
+canva.create_window(320, 80, window=c1)
label2 = tk.Label(root, text='Password:', bg="#0c3251", fg="#fff")
label2.config(font=('helvetica', 14))
-canva.create_window(200, 120, window=label2)
+canva.create_window(180, 120, window=label2)
entry2 = tk.Entry(root, show="*")
entry2.insert(0, pref["pass"])
-canva.create_window(200, 160, window=entry2)
+canva.create_window(180, 160, window=entry2)
c1 = tk.Checkbutton(root, text="Remember", variable=savePass, bg="#0c3251", fg="#f70")
-canva.create_window(350, 160, window=c1)
+canva.create_window(320, 160, window=c1)
label3 = tk.Label(root, text='TOTP:', bg="#0c3251", fg="#fff")
label3.config(font=('helvetica', 14))
-canva.create_window(200, 200, window=label3)
+canva.create_window(180, 200, window=label3)
entry3 = tk.Entry(root)
#entry3.insert(0, pref["totp"])
-canva.create_window(200, 240, window=entry3)
+canva.create_window(180, 240, window=entry3)
button1 = HoverButton(text='Login', command=login, bg="#cc6600", fg="#fff", activebackground="#ee9933")
canva.create_window(200, 300, window=button1)
root.mainloop()
-# Check if you're now logged in
+# Check if you were logged in
if vaultId < 1:
exit(1)
print("Thanks for playing!")
-
-exit(0) # <- FIXME: Not necessary? Just delete stuff below
-
-#################################################################################
-"""
- while True:
- app=launch_game(idx)
-
- ## TODO: Handle MLP
- while app == 7:
- stdout("[CLIENT] Mirror Lake triggered.")
- r=vault.post(VAULT_HOST+"/getlake", json=auth, timeout=15.0)
- if r.status_code == 200:
- goto=r.json()["world"]
- stdout("MLP Target: %s" % str(goto))
- if goto == "" or goto.lower() == "vault":
- stdout("Mirror Lake False Positive")
- break
- try:
- idx=int(dl_search_idx(serverlist, "UUID", goto))
- app=launch_game(idx)
- except:
- traceback.print_exc()
- break
- else:
- stdout("ERROR: Unknown command. Try \"help\".")
-"""
diff --git a/discord_rpc/__init__.py b/discord_rpc/__init__.py
new file mode 100755
index 0000000..73f111c
--- /dev/null
+++ b/discord_rpc/__init__.py
@@ -0,0 +1,919 @@
+from __future__ import absolute_import, print_function
+"""
+Python Rich Presence library for Discord
+"""
+from .util.backoff import Backoff
+from copy import deepcopy
+import logging
+from threading import Lock, Thread
+from .connection.rpc import RpcConnection
+from .util.utils import get_process_id, is_callable, iter_items, iter_keys, is_python3, bytes, unicode, is_linux, \
+ is_windows, get_executable_path
+from .util.types import Int32, Int64
+import json
+import time
+try:
+ from Queue import Queue
+ from Queue import Empty as QueueEmpty
+except ImportError:
+ try:
+ from queue import Queue
+ from queue import Empty as QueueEmpty
+ except ImportError:
+ # we somehow can't import either python Queue's
+ # create a fake Queue class that'll do nothing
+ # and without killing the program
+ from .util.utils import DummyQueue as Queue
+ from .util.utils import Empty as QueueEmpty
+from os import path, makedirs
+if not is_python3():
+ import requests
+else:
+ from urllib.request import urlopen, Request
+from os import environ, system
+from sys import stderr
+if is_windows():
+ if is_python3():
+ import winreg
+ else:
+ import _winreg as winreg
+
+
+VERSION = "1.3.0"
+PROJECT_URL = "https://gitlab.com/somberdemise/discord-rpc.py"
+
+DISCORD_REPLY_NO = 0
+DISCORD_REPLY_YES = 1
+DISCORD_REPLY_IGNORE = 2
+
+
+_discord_rpc = None
+_auto_update_connection = False
+_update_thread = None
+_connection_lock = Lock()
+_http_rate_limit = None
+
+
+class _DiscordRpc(object):
+ connection = None
+ _time_call = time.time
+ _just_connected = False
+ _just_disconnected = False
+ _got_error = False
+ _was_joining = False
+ _was_spectating = False
+ _spectate_secret = ''
+ _join_secret = ''
+ _last_err = [0, '']
+ _last_disconnect = [0, '']
+ __presence_lock = Lock()
+ __handler_lock = Lock()
+ __send_queue = Queue(8)
+ __join_ask_queue = Queue(8)
+ __connected_user = {
+ 'id': None,
+ 'username': None,
+ 'discriminator': None,
+ 'avatar': None,
+ }
+ __current_presence = None
+ __pid = 0
+ __nonce = 1
+ __reconnect_time = Backoff(500, 60000)
+ __next_connect = None
+ __callbacks = {
+ 'ready': None,
+ 'disconnected': None,
+ 'joinGame': None,
+ 'spectateGame': None,
+ 'joinRequest': None,
+ 'error': None,
+ }
+ __registered_handlers = {
+ 'joinGame': False,
+ 'spectateGame': False,
+ 'joinRequest': False,
+ }
+ __http_rate_limit = None
+
+ def __init__(self, app_id, pid=None, pipe_no=0, log=True, logger=None, log_file=None, log_level=logging.INFO,
+ callbacks=None):
+ if pid is not None:
+ if not isinstance(pid, int):
+ raise TypeError('PID must be of int type!')
+ self.__pid = pid
+ else:
+ self.__pid = get_process_id()
+ if callbacks:
+ self.set_callbacks(**callbacks)
+ if self.connection is not None:
+ return
+
+ self.connection = RpcConnection(app_id, pipe_no=pipe_no, log=log, logger=logger, log_file=log_file,
+ log_level=log_level)
+ self.connection.on_connect = self._on_connect
+ self.connection.on_disconnect = self._on_disconnect
+
+ def shutdown(self):
+ if self.connection is None:
+ self._log('debug', 'Connection hasn\'t been established or recently shutdown; ignoring.')
+ return
+ self.connection.on_connect = None
+ self.connection.on_disconnect = None
+ self.__callbacks = {
+ 'ready': None,
+ 'disconnected': None,
+ 'joinGame': None,
+ 'spectateGame': None,
+ 'joinRequest': None,
+ 'error': None,
+ }
+ self.connection.destroy()
+
+ def run_callbacks(self):
+ if self.connection is None:
+ return
+
+ was_disconnected = self._just_disconnected
+ self._just_disconnected = False
+ is_connected = self.connection.is_open
+
+ if is_connected:
+ with self.__handler_lock:
+ if was_disconnected:
+ self._run_callback('disconnected', self._last_disconnect[0], self._last_disconnect[1])
+
+ if self._just_connected:
+ with self.__handler_lock:
+ self._run_callback('ready', self.current_user)
+ self._just_connected = False
+
+ if self._got_error:
+ with self.__handler_lock:
+ self._run_callback('error', self._last_err[0], self._last_err[1])
+ self._got_error = False
+
+ if self._was_joining:
+ with self.__handler_lock:
+ self._run_callback('joinGame', self._join_secret)
+ self._was_joining = False
+
+ if self._was_spectating:
+ with self.__handler_lock:
+ self._run_callback('spectateGame', self._spectate_secret)
+ self._was_spectating = False
+
+ users = list()
+ with self.__handler_lock:
+ if not self.__join_ask_queue.empty():
+ # instead of trying to use a guesstimated value (qsize),
+ # grab data w/o blocking, and break when it raises the "empty" error
+ while True:
+ try:
+ user = self.__join_ask_queue.get(False)
+ users.append(deepcopy(user))
+ # just in case
+ self.__join_ask_queue.task_done()
+ except QueueEmpty:
+ break
+ if len(users) > 0:
+ self._run_callback('joinRequest', users)
+ self._log('debug', "Users requesting to join: {}".format(users))
+ else:
+ self._log('debug', "No users requesting to join game.")
+
+ if not is_connected and was_disconnected:
+ with self.__handler_lock:
+ self._run_callback('disconnected', self._last_disconnect[0], self._last_disconnect[1])
+
+ def update_connection(self):
+ if self.connection is None:
+ return
+
+ if not self.connection.is_open:
+ if self.__next_connect is None:
+ self.__next_connect = self.time_now()
+ if self.time_now() >= self.__next_connect:
+ self.update_reconnect_time()
+ self._log('debug', 'Next connection in {} seconds.'.format(self.__next_connect - self.time_now()))
+ self.connection.open()
+ else:
+ # reads
+ while True:
+ did_read, data = self.connection.read()
+ if not did_read:
+ break
+ evt = data.get('evt')
+ nonce = data.get('nonce')
+ if nonce is not None:
+ err_data = data.get('data', dict())
+ if evt is not None and evt == 'ERROR':
+ self._last_err = [err_data.get('code', 0), err_data.get('message', '')]
+ self._got_error = True
+ else:
+ if evt is None:
+ self._log('debug', 'No event sent by Discord.')
+ continue
+ read_data = data.get('data', dict())
+ if evt == 'ACTIVITY_JOIN':
+ self._join_secret = read_data.get('secret', '')
+ self._was_joining = True
+ elif evt == 'ACTIVITY_SPECTATE':
+ self._spectate_secret = read_data.get('secret', '')
+ self._was_spectating = True
+ elif evt == 'ACTIVITY_JOIN_REQUEST':
+ user = read_data.get('user', None)
+ uid = user.get('id', None)
+ uname = user.get('username', None)
+ discrim = user.get('discriminator', None)
+ if any(x is None for x in (uid, uname, discrim)):
+ # something is wrong with Discord (or we need to update this library)
+ self._log('warning', 'Discord failed to send required data for join request!')
+ continue
+ # the avatar hash to grab from https://cdn.discordapp.com/
+ # can be None/empty string
+ avatar = user.get('avatar', None)
+ if not self.__join_ask_queue.full():
+ user_data = {'id': uid,
+ 'username': uname,
+ 'discriminator': discrim,
+ 'avatar': avatar}
+ self.__join_ask_queue.put(user_data)
+ # writes
+ if self.__current_presence is not None and len(self.__current_presence) > 0:
+ with self.__presence_lock:
+ if self.connection.write(self.__current_presence):
+ # only erase if we successfully wrote
+ self.__current_presence = None
+ self._log('debug', 'Wrote presence data to IPC.')
+ if not self.__send_queue.empty():
+ # instead of trying to use a guesstimated value (qsize),
+ # grab data w/o blocking, and break when it raises the "empty" error
+ while True:
+ try:
+ # attempt to grab data
+ sdata = self.__send_queue.get(False)
+ # apparently, we don't care for the write results, according to Discord's library
+ self.connection.write(sdata)
+ # just in case
+ self.__send_queue.task_done()
+ except QueueEmpty:
+ self._log('debug', 'Wrote queue of send data to IPC.')
+ break
+
+ def __presence_to_json(self, **kwargs):
+ """Creates json rich presence info."""
+ # See https://discordapp.com/developers/docs/rich-presence/how-to#updating-presence-update-presence-payload
+ # for max values for payload
+ if self.pid <= 0:
+ raise AttributeError('PID is required for payload!')
+ rp = dict()
+ rp['cmd'] = 'SET_ACTIVITY'
+ rp['args'] = dict()
+ # I don't really know why PID is required, but alright
+ rp['args']['pid'] = int(self.pid)
+
+ rp['args']['activity'] = dict()
+
+ state = kwargs.get('state', None)
+ if state is not None and len(state) > 0:
+ rp['args']['activity']['state'] = str(state[:128])
+
+ details = kwargs.get('details', None)
+ if details is not None and len(details) > 0:
+ rp['args']['activity']['details'] = str(details[:128])
+
+ # NOTE: Sending endTimestamp will always have the time displayed as "remaining" until the given time.
+ # Sending startTimestamp will show "elapsed" as long as there is no endTimestamp sent.
+ start_timestamp = kwargs.get('start_timestamp', None)
+ end_timestamp = kwargs.get('end_timestamp', None)
+ # we do int calculations beforehand, in case it turns out that the number ends up as 0
+ if start_timestamp is not None:
+ start_timestamp = Int64(int(start_timestamp)).get_number()
+ if end_timestamp is not None:
+ end_timestamp = Int64(int(end_timestamp)).get_number()
+ if any(x for x in (start_timestamp, end_timestamp)):
+ rp['args']['activity']['timestamps'] = dict()
+ # start_timestamp != None && start_timestamp != 0
+ if start_timestamp:
+ rp['args']['activity']['timestamps']['start'] = start_timestamp
+ # same for end
+ if end_timestamp:
+ rp['args']['activity']['timestamps']['end'] = end_timestamp
+
+ large_image_key = kwargs.get('large_image_key', None)
+ large_image_text = kwargs.get('large_image_text', None)
+ small_image_key = kwargs.get('small_image_key', None)
+ small_image_text = kwargs.get('small_image_text', None)
+ if any(x is not None and len(x) > 0 for x in
+ (large_image_key, large_image_text, small_image_key, small_image_text)):
+ rp['args']['activity']['assets'] = dict()
+ if large_image_key is not None and len(large_image_key) > 0:
+ rp['args']['activity']['assets']['large_image'] = str(large_image_key[:128]).lower()
+ if large_image_text is not None and len(large_image_text) > 0:
+ rp['args']['activity']['assets']['large_text'] = str(large_image_text[:128])
+ if small_image_key is not None and len(small_image_key) > 0:
+ rp['args']['activity']['assets']['small_image'] = str(small_image_key[:128]).lower()
+ if small_image_text is not None and len(small_image_text) > 0:
+ rp['args']['activity']['assets']['small_text'] = str(small_image_text[:128])
+
+ party_id = kwargs.get('party_id', None)
+ party_size = kwargs.get('party_size', None)
+ party_max = kwargs.get('party_max', None)
+ # we do int calculations beforehand, in case it turns out that the number ends up as 0
+ if party_size is not None:
+ party_size = Int32(int(party_size)).get_number()
+ if party_max is not None:
+ party_max = Int32(int(party_max)).get_number()
+ # for some reason, Discord does partySize || partyMax, but then requires partySize & partyMax > 0...
+ # we shall correct that by throwing an all()
+ if (party_id is not None and len(party_id) > 0) or all(x for x in (party_size, party_max)):
+ rp['args']['activity']['party'] = dict()
+ if party_id is not None and len(party_id) > 0:
+ rp['args']['activity']['party']['id'] = str(party_id[:128])
+ if party_size and party_max:
+ rp['args']['activity']['party']['size'] = [party_size, party_max]
+
+ # match_secret = kwargs.get('match_secret', None) # deprecated
+ join_secret = kwargs.get('join_secret', None)
+ spectate_secret = kwargs.get('spectate_secret', None)
+ # if any(x is not None and len(x) > 0 for x in (match_secret, join_secret, spectate_secret)):
+ if any(x is not None and len(x) > 0 for x in (join_secret, spectate_secret)):
+ rp['args']['activity']['secrets'] = dict()
+ # if match_secret is not None and len(match_secret) > 0:
+ # rp['args']['secrets']['match'] = str(match_secret[:128])
+ if join_secret is not None and len(join_secret) > 0:
+ rp['args']['activity']['secrets']['join'] = str(join_secret[:128])
+ if spectate_secret is not None and len(spectate_secret) > 0:
+ rp['args']['activity']['secrets']['spectate'] = str(spectate_secret[:128])
+
+ # rp['args']['instance'] = bool(kwargs.get('instance', False)) # deprecated
+ rp['nonce'] = str(self.nonce)
+ self.__nonce += 1
+
+ self._log('debug', 'Presence data to be written: {}'.format(rp))
+ return json.dumps(rp)
+
+ def update_presence(self, **kwargs):
+ """
+ :param kwargs: kwargs must consist of any of the following:
+ (optional) state (string)
+ (optional) details (string)
+ (optional) start_timestamp (int)
+ (optional) end_timestamp (int)
+ (optional) large_image_key (string, lowercase)
+ (optional) large_image_text (string)
+ (optional) small_image_key (string, lowercase)
+ (optional) small_image_text (string)
+ (optional) party_id (string)
+ (optional) party_size (int), party_max (int) (both are required if using either)
+ (optional) join_secret (string)
+ (optional) spectate_secret (string)
+ Note: see here https://discordapp.com/developers/docs/rich-presence/how-to#updating-presence
+ Note 2: We do not use deprecated parameters at this time
+ :return: N/A
+ """
+ json_data = self.__presence_to_json(**kwargs)
+ with self.__presence_lock:
+ self.__current_presence = json_data
+
+ def clear_presence(self):
+ self.update_presence()
+
+ def respond(self, user_id, reply):
+ if self.connection is None or not self.connection.is_open:
+ self._log('warning', 'Cannot reply to discord user {}; connection not established!'.format(user_id))
+ return
+ response = dict()
+ if reply == DISCORD_REPLY_YES:
+ cmd = 'SEND_ACTIVITY_JOIN_INVITE'
+ else:
+ cmd = 'CLOSE_ACTIVITY_JOIN_REQUEST'
+ response['cmd'] = cmd
+ response['args'] = dict()
+ response['args']['user_id'] = str(user_id)
+ response['nonce'] = str(self.nonce)
+ self.__nonce += 1
+ if not self.__send_queue.full():
+ self.__send_queue.put(json.dumps(response))
+ self._log('debug', 'Queued reply: {}'.format(response))
+ else:
+ self._log('warning', 'Cannot reply to discord user {}; send queue is full!')
+
+ def last_error(self):
+ return self._last_err[0], self._last_err[1]
+
+ def last_disconnect(self):
+ return self._last_disconnect[0], self._last_disconnect[1]
+
+ def update_reconnect_time(self):
+ current_time = self.time_now()
+ delay = self.__reconnect_time.next_delay() / 1000
+ self.__next_connect = current_time + delay
+ self._log('debug', 'Updating next connect to {}. Current time: {}, delay: {}'.format(self.__next_connect,
+ current_time,
+ delay))
+
+ def set_callbacks(self, **kwargs):
+ for name, callback_info in iter_items(kwargs):
+ self.set_callback(name, callback_info)
+
+ def set_callback(self, callback_name, callback):
+ callback_name = callback_name.strip()
+ if callback_name in ('ready', 'disconnected', 'joinGame', 'spectateGame', 'joinRequest', 'error'):
+ if callback and not is_callable(callback):
+ raise TypeError('Callback must be callable! Callback name: {}, callback: {}'.format(callback_name,
+ callback))
+ self.__callbacks[callback_name] = callback
+
+ def update_handlers(self):
+ for handler in iter_keys(self.__registered_handlers):
+ if handler == 'joinGame':
+ event = 'ACTIVITY_JOIN'
+ elif handler == 'spectateGame':
+ event = 'ACTIVITY_SPECTATE'
+ elif handler == 'joinRequest':
+ event = 'ACTIVITY_JOIN_REQUEST'
+ else:
+ # unknown handler
+ self._log('warning', 'Unknown handler name "{}".'.format(handler))
+ continue
+ if not self.__registered_handlers[handler] and self.__callbacks[handler] is not None:
+ if not self.__register_event(event):
+ self._log('warning', 'Unable to register event "{}"'.format(event))
+ else:
+ self._log('info', "Registered handler {}".format(handler))
+ elif self.__registered_handlers[handler] and self.__callbacks[handler] is None:
+ if not self.__unregister_event(event):
+ self._log('warning', 'Unable to unregister event "{}"'.format(event))
+ else:
+ self._log('debug', 'Unregistered event {}'.format(event))
+
+ def _run_callback(self, callback_name, *args):
+ callback_name = callback_name.strip()
+ if callback_name in self.__callbacks:
+ if self.__callbacks[callback_name] is not None:
+ callback = self.__callbacks[callback_name]
+ if len(args) > 0:
+ callback(*args)
+ else:
+ callback()
+ else:
+ self._log('debug', 'No callback set for event "{}"'.format(callback_name))
+ else:
+ self._log('debug', 'No such event name "{}"'.format(callback_name))
+
+ def _log(self, *args):
+ if self.connection is not None:
+ self.connection.log(*args)
+
+ def _on_connect(self, data):
+ self.update_handlers()
+ self._log('debug', 'Data received: {}'.format(data))
+ user_data = data.get('data', None)
+ if user_data is not None:
+ user = user_data.get('user', None)
+ uid = user.get('id', None)
+ uname = user.get('username', None)
+ if any(x is None for x in (uid, uname)):
+ self._log('warning', 'Discord failed to send current user data.')
+ else:
+ discrim = user.get('discriminator', None) # i'm not sure why this can be None for current user, but
+ # not others...
+ avatar = user.get('avatar', None)
+ self.__connected_user['id'] = uid
+ self.__connected_user['username'] = uname
+ self.__connected_user['discriminator'] = discrim
+ self.__connected_user['avatar'] = avatar
+ self._log('debug', 'Current discord user: {}'.format(self.__connected_user))
+ else:
+ self._log('warning', 'Discord failed to send current user data.')
+ self._just_connected = True
+ self.__reconnect_time.reset()
+
+ def _on_disconnect(self, err, msg):
+ self._just_disconnected = True
+ self._last_disconnect = [err, msg]
+ self.__registered_handlers['joinGame'] = False
+ self.__registered_handlers['joinRequest'] = False
+ self.__registered_handlers['spectateGame'] = False
+ self.update_reconnect_time()
+
+ def __register_event(self, event):
+ data = dict()
+ data['nonce'] = str(self.nonce)
+ self.__nonce += 1
+ data['cmd'] = 'SUBSCRIBE'
+ data['evt'] = event
+ if not self.__send_queue.full():
+ self.__send_queue.put(json.dumps(data))
+ return True
+ return False
+
+ def __unregister_event(self, event):
+ data = dict()
+ data['nonce'] = str(self.nonce)
+ self.__nonce += 1
+ data['cmd'] = 'UNSUBSCRIBE'
+ data['evt'] = event
+ if not self.__send_queue.full():
+ self.__send_queue.put(json.dumps(data))
+ return True
+ return False
+
+ @property
+ def got_error(self):
+ return self._got_error
+
+ @property
+ def was_joining(self):
+ return self._was_joining
+
+ @property
+ def was_spectating(self):
+ return self._was_spectating
+
+ @property
+ def current_user(self):
+ return deepcopy(self.__connected_user)
+
+ @property
+ def spectate_secret(self):
+ return self._spectate_secret
+
+ @property
+ def join_secret(self):
+ return self._join_secret
+
+ @property
+ def pid(self):
+ return self.__pid
+
+ @property
+ def nonce(self):
+ return self.__nonce
+
+ @property
+ def time_now(self):
+ return self._time_call
+
+ @time_now.setter
+ def time_now(self, callback):
+ if is_callable(callback):
+ self._time_call = callback
+ else:
+ self._log('warning', 'time_now must be callable!')
+
+ @property
+ def app_id(self):
+ if self.connection is not None:
+ return self.connection.app_id
+ else:
+ return '0xDEADBEEF'
+
+
+class _UpdateConnection(Thread):
+ def run(self):
+ global _discord_rpc
+ global _connection_lock
+ while True:
+ time.sleep(1)
+ with _connection_lock:
+ if _discord_rpc is None:
+ # we have shut down, break and return
+ break
+ _discord_rpc.update_connection()
+ _discord_rpc.run_callbacks()
+
+
+def initialize(app_id, pid=None, callbacks=None, pipe_no=0, time_call=None, auto_update_connection=False,
+ log=True, logger=None, log_file=None, log_level=logging.INFO,
+ auto_register=False, steam_id=None, command=None):
+ """
+ Initializes and connects to the Discord Rich Presence RPC
+ :param app_id: The Client ID from Discord (see https://github.com/discordapp/discord-rpc#basic-usage)
+ (NOTE: Must be a string)
+ :param pid: The main program ID (is automatically set if not passed)
+ :param callbacks: The callbacks and any extra args to run when events are fired ('ready', 'disconnected',
+ 'joinGame', 'spectateGame',
+ 'joinRequest', 'error')
+ :param time_call: The time function to call for epoch seconds (defaults to time.time())
+ :param auto_update_connection: Do you want the library to automagically update the connection for you?
+ (defaults to False)
+ :param log: Do we want to use logging for the RPC connection (defaults to True)
+ :param logger: Your own logger to use (must be already set up) (defaults to automatically setting one up
+ internally)
+ :param log_file: The location of where the log file should reside (defaults to stdout only, ignored if
+ rpc_logger is used)
+ :param log_level: The log level to use (defaults to logging.INFO)
+ :param pipe_no: The pipe number to use in the RPC connection (must be 0-10, default 0)
+ :param auto_register: Do you want us to auto-register your program (defaults to False) (NOTE: currently does
+ nothing)
+ :param steam_id: The applications steam ID for auto-register (defaults to regular program registration, or
+ nothing if auto_register is False) (NOTE: Also does nothing currently)
+ :param command: The command to use for protocol registration (ex: /path/to/file --discord)
+ :return: N/A
+ """
+ global _discord_rpc
+ global _auto_update_connection
+ global _update_thread
+
+ if _discord_rpc is not None:
+ # don't initialize more than once
+ return
+ if auto_register:
+ register_game(app_id=app_id, steam_id=steam_id, command=command)
+ _discord_rpc = _DiscordRpc(app_id, pid=pid, pipe_no=pipe_no, log=log, logger=logger, log_file=log_file,
+ log_level=log_level, callbacks=callbacks)
+ if time_call is not None:
+ _discord_rpc.time_now = time_call
+
+ if auto_update_connection:
+ _auto_update_connection = True
+ _update_thread = _UpdateConnection()
+ _update_thread.start()
+
+
+def shutdown():
+ """
+ Shuts down the Discord Rich Presence connection
+ :return: N/A
+ """
+ global _discord_rpc
+ global _auto_update_connection
+ global _connection_lock
+ if _discord_rpc is not None:
+ with _connection_lock:
+ _discord_rpc.shutdown()
+ # make sure user/programmer doesn't try to call stuff afterwards on 'discord_rpc'
+ _discord_rpc = None
+ if _auto_update_connection and _update_thread is not None:
+ _update_thread.join()
+ # always set to False
+ _auto_update_connection = False
+
+
+def run_callbacks():
+ """
+ Runs the rich presence callbacks
+ :return: N/A
+ """
+ global _discord_rpc
+ if _discord_rpc is not None:
+ _discord_rpc.run_callbacks()
+
+
+def update_connection():
+ """
+ Updates the rich presence connection
+ :return: N/A
+ """
+ global _discord_rpc
+ global _auto_update_connection
+ if _discord_rpc is not None and not _auto_update_connection:
+ _discord_rpc.update_connection()
+
+
+def update_presence(**kwargs):
+ """
+ :param kwargs: kwargs must consist of any of the following:
+ (optional) state (string)
+ (optional) details (string)
+ (optional) start_timestamp (int)
+ (optional) end_timestamp (int)
+ (optional) large_image_key (string, lowercase)
+ (optional) large_image_text (string)
+ (optional) small_image_key (string, lowercase)
+ (optional) small_image_text (string)
+ (optional) party_id (string)
+ (optional) party_size (int), party_max (int) (both are required if using either)
+ (optional) join_secret (string)
+ (optional) spectate_secret (string)
+ Note: see here https://discordapp.com/developers/docs/rich-presence/how-to#updating-presence
+ Note 2: We do not use deprecated parameters at this time
+ :return: N/A
+ """
+ global _discord_rpc
+ if _discord_rpc is not None:
+ _discord_rpc.update_presence(**kwargs)
+
+
+def clear_presence():
+ """
+ Clears the rich presence data last sent
+ :return: N/A
+ """
+ global _discord_rpc
+ if _discord_rpc is not None:
+ _discord_rpc.clear_presence()
+
+
+def respond(user_id, response):
+ """
+ Respond to a discord user
+ :param user_id: The Discord user's snowflake ID (the '64 char' long one or so)
+ :param response: The response to send to the user (one of type DISCORD_REPLY_NO, DISCORD_REPLY_YES,
+ DISCORD_REPLY_IGNORE)
+ :return: N/A
+ """
+ global _discord_rpc
+ if _discord_rpc is not None:
+ _discord_rpc.respond(user_id, response)
+
+
+def download_profile_picture(user_id, discriminator, avatar_hash=None, cache_dir="cache", default_dir="default",
+ cert_file=None, game_name=None, game_version=None, game_url=None):
+ """
+ Download a discord user's profile picture.
+ :param user_id: The discord user's ID
+ :param discriminator: The discord user's discriminator; required and used for when avatar_hash is None
+ :param avatar_hash: (optional) The discord user's avatar hash. NOTE: if None, defaults to a default avatar image
+ :param cache_dir: (optional) Path to store the profile picture
+ :param default_dir: (optional) The path within the cache_dir to use for default avatars
+ param cert_file: (optional) The path to the cacert file to use
+ :param game_name: (optional) The name of the game that is running
+ :param game_version: (optional) The game's version number
+ :param game_url: (optional) The game's website
+ :return: Path to profile picture, or None
+ """
+ global _http_rate_limit
+ if avatar_hash is None:
+ url = "https://cdn.discordapp.com/embed/avatars/{}.png".format(int(discriminator) % 5)
+ # NOTE: we default to "./cache/default/" if no path specified
+ # NOTE 2: we use a "default" directory to save disk space and download calls in the long run
+ download_folder = path.join(cache_dir, default_dir)
+ else:
+ url = "https://cdn.discordapp.com/avatars/{}/{}.jpg?size=2048".format(user_id, avatar_hash)
+ # NOTE: we default to "./cache/user_id/" if no path specified
+ download_folder = path.join(cache_dir, user_id)
+ if not path.exists(download_folder):
+ makedirs(download_folder, 0o755)
+ if avatar_hash is not None:
+ avatar_file = path.join(download_folder, avatar_hash) + '.jpg'
+ else:
+ avatar_file = path.join(download_folder, str(int(discriminator) % 5)) + '.png'
+ if path.exists(avatar_file):
+ # technically, we downloaded it, so no need to worry about downloading
+ return avatar_file
+ # we check this after just in case we already have a cached image
+ if _http_rate_limit is not None:
+ if not _http_rate_limit > time.time():
+ return None
+ # we're required to have a ua string
+ ua_str = "discord-rpc.py ({url}, {version})".format(url=PROJECT_URL, version=VERSION)
+ if game_name is not None and isinstance(game_name, (bytes, unicode)) and game_name.strip() != '':
+ # if we have a game name, append that
+ ua_str += ' {}'.format(game_name)
+ if all((x is not None and isinstance(x, (bytes, unicode)) and x.strip() != '') for x in (game_version,
+ game_url)):
+ # if we have both a url and version number, append those too
+ ua_str += " ({url}, {version}".format(url=game_url, version=game_version)
+ headers = {'User-Agent': ua_str}
+ if is_python3():
+ if cert_file is not None:
+ r = Request(
+ url,
+ data=None,
+ headers=headers,
+ cafile=cert_file
+ )
+ else:
+ r = Request(
+ url,
+ data=None,
+ headers=headers
+ )
+ req = urlopen(r)
+ status_code = req.getcode()
+ else:
+ if cert_file is not None:
+ req = requests.get(url, headers=headers, verify=cert_file)
+ else:
+ req = requests.get(url, headers=headers)
+ status_code = req.status_code
+ if status_code != 200:
+ if status_code == 404:
+ # nonexistent avatar/hash; return None
+ return None
+ if 'X-RateLimit-Reset' in req.headers:
+ _http_rate_limit = int(req.headers['X-RateLimit-Reset'])
+ else:
+ try:
+ if is_python3():
+ data = req.read()
+ json_data = json.loads(data.decode(req.info().get_content_charset('utf-8')))
+ else:
+ json_data = req.json()
+ if 'retry_after' in json_data:
+ _http_rate_limit = time.time() + (int(json_data['retry_after']) / 1000.0)
+ except Exception:
+ pass
+ if _http_rate_limit is None:
+ # try again in 15 min (Discord shouldn't kill us for waiting 15 min anyways...)
+ _http_rate_limit = time.time() + (15 * 60)
+ return None
+ with open(avatar_file, 'wb') as f:
+ if is_python3():
+ f.write(req.read())
+ else:
+ f.write(req.content)
+ return avatar_file
+
+
+def register_game(app_id, steam_id=None, command=None):
+ """
+ Registers a protocol Discord can use to run your game.
+ :param app_id: The Client ID from Discord (see https://github.com/discordapp/discord-rpc#basic-usage)
+ (NOTE: Must be a string)
+ :param steam_id: The applications steam ID for auto-register (defaults to regular program registration)
+ :param command: The command to use for protocol registration (ex: /path/to/file --discord)
+ :return:
+ """
+ if command is None and steam_id is None and (is_windows() or is_linux()):
+ command = get_executable_path()
+ if is_linux():
+ # linux is the easiest
+ if steam_id:
+ command = "xdg-open steam://rungameid/{}".format(steam_id)
+ home = environ.get('HOME')
+ if home is None or home.strip() == '':
+ # no home? no registration!
+ return
+ file_contents = "[Desktop Entry]\nName=Game {app_id}\nExec={command} %u\nType=Application\n" + \
+ "NoDisplay=true\nCategories=Discord;Games;\nMimeType=x-scheme-handler/discord-{app_id};\n"
+ file_contents = file_contents.format(app_id=app_id, command=command)
+ if home.endswith('/'):
+ home = home[:-1]
+ # we don't really need to here, but oh well
+ path_location = path.join(home, ".local", "share", "applications")
+ if not path.exists(path_location):
+ makedirs(path_location, 0o700)
+ with open(path.join(path_location, "discord-{}.desktop".format(app_id)), 'w') as f:
+ f.write(file_contents)
+ sys_call = "xdg-mime default discord-{0}.desktop x-scheme-handler/discord-{0}".format(app_id)
+ if system(sys_call) < 0:
+ print("Failed to register mime handler!", file=stderr)
+ elif is_windows():
+ def read_key(reg_path, name):
+ try:
+ root_key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, reg_path, 0, winreg.KEY_READ)
+ value, reg_type = winreg.QueryValueEx(root_key, name)
+ winreg.CloseKey(root_key)
+ return value
+ except WindowsError:
+ return None
+
+ def write_key(reg_path, name, value):
+ try:
+ # I know this can return a key if it exists, but oh well
+ winreg.CreateKey(winreg.HKEY_CURRENT_USER, reg_path)
+ root_key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, reg_path, 0, winreg.KEY_WRITE)
+ winreg.SetValueEx(root_key, name, 0, winreg.REG_SZ, value)
+ winreg.CloseKey(root_key)
+ return True
+ except WindowsError:
+ return False
+
+ if steam_id:
+ tmp = read_key(r"Software\Valve\Steam", "SteamExe")
+ if tmp is not None and tmp.strip() != '':
+ command = "\"{}\" steam://rungameid/{}".format(tmp.replace("/", "\\"), steam_id)
+
+ protocol_desc = "URL:Run game {} protocol".format(app_id)
+ protocol_path = r"Software\Classes\{}".format("discord-{}".format(app_id))
+ if not write_key(protocol_path, None, protocol_desc):
+ # failed to write the key
+ print("Error writing description!", file=stderr)
+ if not write_key(protocol_path, "URL Protocol", None):
+ print("Error writing description!", file=stderr)
+ if not write_key(protocol_path + r"\DefaultIcon", None, get_executable_path()):
+ print("Error writing key!", file=stderr)
+ if not write_key(protocol_path + r"\shell\open\command", None, command):
+ print("Error writing command!", file=stderr)
+ else:
+ # assume Mac OSX here
+ def register_url(aid):
+ # TODO: figure out a feasable way to get this to work
+ print("Url registration under Mac OSX unimplemented. Cannot create for app ID {}".format(aid), file=stderr)
+
+ def register_command(aid, cmd):
+ home = path.expanduser("~")
+ if home is None or home.strip() == '':
+ return
+ discord_path = path.join(home, "Library", "Application Support", "discord", "games")
+ if not path.exists(discord_path):
+ makedirs(discord_path)
+ with open(path.join(discord_path, "{}.json".format(aid)), 'w') as f:
+ f.write("{\"command\": \"{}\"}".format(cmd))
+
+ if steam_id:
+ command = "steam://rungameid/{}".format(steam_id)
+ if command:
+ register_command(app_id, command)
+ else:
+ register_url(app_id)
+
+
+__all__ = ['DISCORD_REPLY_NO', 'DISCORD_REPLY_YES', 'DISCORD_REPLY_IGNORE', 'initialize', 'shutdown', 'run_callbacks',
+ 'update_connection', 'update_presence', 'clear_presence', 'respond', 'VERSION', 'PROJECT_URL',
+ 'download_profile_picture', 'register_game']
diff --git a/discord_rpc/codes/__init__.py b/discord_rpc/codes/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/discord_rpc/codes/__init__.py
diff --git a/discord_rpc/codes/errorcodes.py b/discord_rpc/codes/errorcodes.py
new file mode 100644
index 0000000..d501301
--- /dev/null
+++ b/discord_rpc/codes/errorcodes.py
@@ -0,0 +1,3 @@
+Success = 0
+PipeClosed = 1
+ReadCorrupt = 2
diff --git a/discord_rpc/codes/opcodes.py b/discord_rpc/codes/opcodes.py
new file mode 100644
index 0000000..29c3baf
--- /dev/null
+++ b/discord_rpc/codes/opcodes.py
@@ -0,0 +1,5 @@
+Handshake = 0
+Frame = 1
+Close = 2
+Ping = 3
+Pong = 4
diff --git a/discord_rpc/codes/statecodes.py b/discord_rpc/codes/statecodes.py
new file mode 100644
index 0000000..51f1a46
--- /dev/null
+++ b/discord_rpc/codes/statecodes.py
@@ -0,0 +1,4 @@
+Disconnected = 0
+SentHandshake = 1
+AwaitingResponse = 2
+Connected = 3
diff --git a/discord_rpc/connection/__init__.py b/discord_rpc/connection/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/discord_rpc/connection/__init__.py
diff --git a/discord_rpc/connection/ipc.py b/discord_rpc/connection/ipc.py
new file mode 100644
index 0000000..bbb26a7
--- /dev/null
+++ b/discord_rpc/connection/ipc.py
@@ -0,0 +1,387 @@
+from __future__ import absolute_import
+import errno
+import logging
+from ..codes import errorcodes
+from ..util.utils import is_windows, range, get_temp_path, to_bytes, bytes, to_unicode, is_python3, is_callable
+import struct
+import sys
+if is_windows():
+ # we're going to have to do some ugly things, because Windows sucks
+ import ctypes
+ GENERIC_READ = 0x80000000
+ GENERIC_WRITE = 0x40000000
+ OPEN_EXISTING = 0x3
+ INVALID_HANDLE_VALUE = -1
+ PIPE_READMODE_MESSAGE = 0x2
+ ERROR_FILE_NOT_FOUND = 0x2
+ ERROR_PIPE_BUSY = 0xE7
+ ERROR_MORE_DATA = 0xEA
+ BUFSIZE = 512
+else:
+ try:
+ from socket import MSG_NOSIGNAL
+ _msg_flags = MSG_NOSIGNAL
+ except ImportError:
+ _msg_flags = 0
+ try:
+ from socket import SO_NOSIGPIPE
+ _do_sock_opt = True
+ except ImportError:
+ _do_sock_opt = False
+ import socket
+ import fcntl
+ from os import O_NONBLOCK
+
+
+class BaseConnection(object):
+ """Generate IPC Connection handler."""
+ # *nix specific
+ __sock = None
+ # Windows specific
+ __pipe = None
+
+ __open = False
+ __logger = None
+ __is_logging = False
+
+ def __init__(self, log=True, logger=None, log_file=None, log_level=logging.INFO):
+ if not isinstance(log, bool):
+ raise TypeError('log must be of bool type!')
+ if log:
+ if logger is not None:
+ # Panda3D notifies are similar, so we simply check if we can make the same calls as logger
+ if not hasattr(logger, 'debug'):
+ raise TypeError('logger must be of type logging!')
+ self.__logger = logger
+ else:
+ self.__logger = logging.getLogger(__name__)
+ log_fmt = logging.Formatter('[%(asctime)s][%(levelname)s] ' + '%(name)s - %(message)s')
+ if log_file is not None and hasattr(log_file, 'strip'):
+ fhandle = logging.FileHandler(log_file)
+ fhandle.setLevel(log_level)
+ fhandle.setFormatter(log_fmt)
+ self.__logger.addHandler(fhandle)
+ shandle = logging.StreamHandler(sys.stdout)
+ shandle.setLevel(log_level)
+ shandle.setFormatter(log_fmt)
+ self.__logger.addHandler(shandle)
+ self.__is_logging = True
+
+ def log(self, callback_name, *args):
+ if self.__logger is not None:
+ if hasattr(self.__logger, callback_name) and is_callable(self.__logger.__getattribute__(callback_name)):
+ self.__logger.__getattribute__(callback_name)(*args)
+
+ def __open_pipe(self, pipe_name, log_type='warning'):
+ """
+ :param pipe_name: the named pipe string
+ :param log_type: the log type to use (default 'warning')
+ :return: opened(bool), try_again(bool)
+ """
+ if not is_windows():
+ self.log('error', 'Attempted to call a Windows call on a non-Windows OS.')
+ return
+ pipe = ctypes.windll.kernel32.CreateFileW(pipe_name, GENERIC_READ | GENERIC_WRITE, 0, None, OPEN_EXISTING, 0,
+ None)
+ if pipe != INVALID_HANDLE_VALUE:
+ self.__pipe = pipe
+ return True, False
+ err = ctypes.windll.kernel32.GetLastError()
+ if err == ERROR_FILE_NOT_FOUND:
+ self.log(log_type, 'File not found.')
+ self.log(log_type, 'Pipe name: {}'.format(pipe_name))
+ return False, False
+ elif err == ERROR_PIPE_BUSY:
+ if ctypes.windll.kernel32.WaitNamedPipeW(pipe_name, 10000) == 0:
+ self.log(log_type, 'Pipe busy.')
+ return False, False
+ else:
+ # try again, should be free now
+ self.log('debug', 'Pipe was busy, but should be free now. Try again.')
+ return False, True
+ # some other error we don't care about
+ self.log('debug', 'Unknown error: {}'.format(err))
+ return False, False
+
+ def open(self, pipe_no=None):
+ if pipe_no is not None:
+ if not isinstance(pipe_no, int):
+ raise TypeError('pipe_no must be of type int!')
+ if pipe_no not in range(0, 10):
+ raise ValueError('pipe_no must be within range (0 <= pipe number < 10)!')
+ if is_windows():
+ # NOTE: don't forget to use a number after ipc-
+ pipe_name = u'\\\\.\\pipe\\discord-ipc-{}'
+ if pipe_no is not None:
+ # we only care about the first value if pipe_no isn't None
+ opened, try_again = self.__open_pipe(pipe_name.format(pipe_no))
+ if opened:
+ self.__open = True
+ self.log('info', 'Connected to pipe {}, as user requested.'.format(pipe_no))
+ return
+ elif try_again:
+ self.open(pipe_no=pipe_no)
+ return
+ else:
+ num = 0
+ while True:
+ if num >= 10:
+ break
+ opened, try_again = self.__open_pipe(pipe_name.format(num), log_type='debug')
+ if opened:
+ self.__open = True
+ self.log('debug', 'Automatically connected to pipe {}.'.format(num))
+ return
+ if try_again:
+ continue
+ num += 1
+ # we failed to get a pipe
+ self.__pipe = None
+ self.log('warning', 'Could not open a connection.')
+ else:
+ self.__sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ if self.__sock is None or self.__sock == -1:
+ self.log('warning', 'Could not open socket.')
+ self.close()
+ return
+ try:
+ fcntl.fcntl(self.__sock, fcntl.F_SETFL, O_NONBLOCK)
+ except Exception as e:
+ self.log('warning', e)
+ self.close()
+ return
+ if _do_sock_opt:
+ try:
+ socket.setsockopt(socket.SOL_SOCKET, SO_NOSIGPIPE)
+ except Exception as e:
+ self.log('warning', e)
+ self.log('debug', 'Attempting to use sock as is. Notify a developer if an error occurs.')
+ sock_addr = get_temp_path()
+ if sock_addr.endswith('/'):
+ sock_addr = sock_addr[:-1]
+ sock_addr += '/discord-ipc-{}'
+ if pipe_no is not None:
+ ret_val = self.__sock.connect_ex(sock_addr.format(pipe_no))
+ if ret_val == 0:
+ self.__open = True
+ self.log('info', 'Connected to socket {}, as user requested.'.format(pipe_no))
+ return
+ else:
+ self.log('warning', 'Could not open socket {}.'.format(pipe_no))
+ self.close()
+ else:
+ for num in range(0, 10):
+ ret_val = self.__sock.connect_ex(sock_addr.format(num))
+ if ret_val == 0:
+ self.__open = True
+ self.log('debug', 'Automatically connected to socket {}.'.format(num))
+ return
+ self.log('warning', 'Could not open socket.')
+ self.close()
+
+ def write(self, data, opcode):
+ if not self.connected():
+ self.log('warning', 'Cannot write if we aren\'t connected yet!')
+ return False
+ if not isinstance(opcode, int):
+ raise TypeError('Opcode must be of int type!')
+ if data is None:
+ data = ''
+ try:
+ data = to_bytes(data)
+ except Exception as e:
+ self.log('warning', e)
+ return False
+ data_len = len(data)
+ # the following data must be little endian unsigned ints
+ # see: https://github.com/discordapp/discord-rpc/blob/master/documentation/hard-mode.md#notes
+ header = struct.pack('<II', opcode, data_len)
+ # append header to data
+ data = header + data
+ # get new data size
+ data_len = len(data)
+ if self.__pipe is not None:
+ written = ctypes.c_ulong(0)
+ success = ctypes.windll.kernel32.WriteFile(self.__pipe, ctypes.c_char_p(data), data_len,
+ ctypes.byref(written), None)
+ if (not success) or (data_len != written.value):
+ self.log('warning', 'Failed to write data onto pipe.')
+ return False
+ return True
+ elif self.__sock is not None:
+ data_sent = 0
+ while data_sent < data_len:
+ try:
+ sent = self.__sock.send(data[data_sent:], _msg_flags)
+ except Exception as e:
+ self.log('warning', e)
+ return False
+ if sent == 0:
+ self.log('warning', 'Socket connection broken!')
+ if data_sent == 0:
+ self.log('warning', 'No data sent; closing connection.')
+ self.close()
+ return False
+ data_sent += sent
+ return True
+ self.log('warning', 'write() executed code that shouldn\'t have run.')
+ return False
+
+ def read(self):
+ ret_val = [False, None, None]
+ if not self.connected():
+ self.log('warning', 'Cannot read if we haven\'t opened a connection!')
+ return ret_val
+ data = bytes()
+ header_size = struct.calcsize('<II')
+ # (is_successful_read, OpCode, data)
+ if self.__pipe is not None:
+ available = ctypes.c_ulong(0)
+ if not ctypes.windll.kernel32.PeekNamedPipe(self.__pipe, None, 0, None, ctypes.byref(available), None):
+ self.log('warning', 'Peek on pipe for header failed.')
+ self.close()
+ ret_val[2] = [errorcodes.PipeClosed, 'Pipe closed']
+ return ret_val
+ if available.value < header_size:
+ self.log('debug', 'Pipe doesn\'t have enough data to read in header.')
+ # assume this is like errno.EAGAIN
+ ret_val[2] = [errorcodes.PipeClosed, 'Pipe closed']
+ return ret_val
+ cb_read = ctypes.c_ulong(0)
+ buff = ctypes.create_string_buffer(header_size)
+ success = 0
+ while not success:
+ success = ctypes.windll.kernel32.ReadFile(self.__pipe, buff, header_size, ctypes.byref(cb_read), None)
+ if success == 1:
+ # we successfully read the HEADER :O
+ # Note: we use RAW here, otherwise it'll be a 1 byte kinda weird thing
+ header = buff.raw
+ break
+ elif ctypes.windll.kernel32.GetLastError() != ERROR_MORE_DATA:
+ # we don't have more data; close pipe
+ self.log('warning', 'Failed to read in header from pipe.')
+ self.close()
+ ret_val[2] = [errorcodes.PipeClosed, 'Pipe closed']
+ return ret_val
+ opcode, data_len = struct.unpack('<II', header)
+ cb_read = ctypes.c_ulong(0)
+ buff = ctypes.create_string_buffer(data_len)
+ success = 0
+ available = ctypes.c_ulong(0)
+ if not ctypes.windll.kernel32.PeekNamedPipe(self.__pipe, None, 0, None, ctypes.byref(available), None):
+ self.log('warning', 'Peek on pipe for data failed.')
+ self.close()
+ ret_val[2] = [errorcodes.ReadCorrupt, 'Partial data in frame']
+ return ret_val
+ if available.value < data_len:
+ self.log('warning', 'Pipe doesn\'t have enough data to read in data.')
+ # assume this is like errno.EAGAIN
+ ret_val[2] = [errorcodes.ReadCorrupt, 'Partial data in frame']
+ return ret_val
+ while not success:
+ success = ctypes.windll.kernel32.ReadFile(self.__pipe, buff, data_len, ctypes.byref(cb_read), None)
+ if success == 1:
+ # we successfully read the DATA :O
+ ret_val[0] = True
+ ret_val[1] = opcode
+ # value here actually works okay, so use that
+ # Note: raw also seems to work, but meh
+ data = buff.value
+ break
+ elif ctypes.windll.kernel32.GetLastError() != ERROR_MORE_DATA:
+ # we don't have more data; close pipe
+ self.log('warning', 'Failed to read in data from pipe.')
+ self.close()
+ ret_val[2] = [errorcodes.ReadCorrupt, 'Partial data in frame']
+ return ret_val
+ elif self.__sock is not None:
+ packets = list()
+ while len(bytes().join(packets)) < header_size:
+ try:
+ packet = self.__sock.recv(header_size - len(bytes().join(packets)))
+ except Exception as e:
+ ret_val[2] = [errorcodes.PipeClosed, 'Pipe closed']
+ if hasattr(e, 'errno'):
+ if e.errno == errno.EAGAIN:
+ self.log('debug', e)
+ self.log('debug', 'errno == EAGAIN')
+ return ret_val
+ self.log('warning', 'Failed to read in header!')
+ self.log('warning', e)
+ self.close()
+ if packet is None or len(packet) == 0:
+ self.log('warning', 'Socket connection broken!')
+ if len(bytes().join(packets)) == 0:
+ self.log('warning', 'No data sent; closing connection.')
+ self.close()
+ ret_val[2] = [errorcodes.PipeClosed, 'Pipe closed']
+ return ret_val
+ packets.append(packet)
+ header = bytes().join(packets)
+ packets = list()
+ opcode, data_len = struct.unpack('<II', header)
+ self.log('debug', 'Opcode: {}, data length: {}'.format(opcode, data_len))
+ while len(bytes().join(packets)) < data_len:
+ try:
+ packet = self.__sock.recv(data_len - len(bytes().join(packets)))
+ except Exception as e:
+ ret_val[2] = [errorcodes.ReadCorrupt, 'Partial data in frame']
+ if hasattr(e, 'errno'):
+ if e.errno == errno.EAGAIN:
+ self.log('debug', e)
+ self.log('debug', 'errno == EAGAIN')
+ return ret_val
+ self.log('warning', 'Failed to read in data!')
+ self.log('warning', e)
+ if packet is None or len(packet) == 0:
+ self.log('warning', 'Socket connection broken!')
+ if len(bytes().join(packets)) == 0:
+ self.log('warning', 'No data sent; closing connection.')
+ self.close()
+ ret_val[2] = [errorcodes.ReadCorrupt, 'Partial data in frame']
+ return ret_val
+ packets.append(packet)
+ data = bytes().join(packets)
+ ret_val[0] = True
+ ret_val[1] = opcode
+ if ret_val[0]:
+ if is_python3():
+ data = to_unicode(data)
+ ret_val[2] = data
+ self.log('debug', 'Return values: {}'.format(ret_val))
+ return ret_val
+
+ def close(self):
+ # ensure we're using Windows before trying to close a pipe
+ # Note: This should **never** execute on a non-Windows machine!
+ if self.__pipe is not None and is_windows():
+ ctypes.windll.kernel32.CloseHandle(self.__pipe)
+ self.__pipe = None
+ if self.__sock is not None:
+ try:
+ self.__sock.shutdown(socket.SHUT_RDWR)
+ self.__sock.close()
+ except Exception as e:
+ self.log('warning', e)
+ finally:
+ self.__sock = None
+ if self.__open:
+ self.__open = False
+ self.log('debug', 'Closed IPC connection.')
+
+ def destroy(self):
+ # make sure we close everything
+ self.close()
+ # if we automatically set our own logger, clean it up
+ if self.__is_logging:
+ for handle in self.__logger.handlers[:]:
+ handle.close()
+ self.__logger.removeHandler(handle)
+ self.__logger = None
+
+ @property
+ def is_open(self):
+ return self.__open
+
+ def connected(self):
+ return self.is_open
diff --git a/discord_rpc/connection/rpc.py b/discord_rpc/connection/rpc.py
new file mode 100644
index 0000000..ad11007
--- /dev/null
+++ b/discord_rpc/connection/rpc.py
@@ -0,0 +1,175 @@
+from __future__ import absolute_import
+import logging
+import json
+from ..codes import errorcodes
+from ..codes import opcodes
+from ..codes import statecodes
+from ..util.utils import is_callable, json2dict, range
+from .ipc import BaseConnection
+
+
+_RPC_VERSION = 1
+
+
+class RpcConnection(object):
+ _connection = None
+ _state = statecodes.Disconnected
+ _app_id = None
+ _last_err_code = 0
+ _last_err_msg = ''
+ _pipe_no = 0
+ _on_connect = None
+ _on_disconnect = None
+
+ def __init__(self, app_id, pipe_no=0, log=True, logger=None, log_file=None, log_level=logging.INFO):
+ self._connection = BaseConnection(log=log, logger=logger, log_file=log_file, log_level=log_level)
+ self._app_id = str(app_id)
+ if pipe_no in range(0, 10):
+ self._pipe_no = pipe_no
+
+ def open(self):
+ if self.state == statecodes.Connected:
+ self.log('debug', 'Already connected; no need to open.')
+ return
+
+ if self.state == statecodes.Disconnected:
+ self.connection.open(pipe_no=self._pipe_no)
+ if not self.connection.is_open:
+ self.log('warning', 'Failed to open IPC connection.')
+ return
+
+ if self.state == statecodes.SentHandshake:
+ did_read, data = self.read()
+ if did_read:
+ cmd = data.get('cmd', None)
+ evt = data.get('evt', None)
+ if all(x is not None for x in (cmd, evt)) and cmd == 'DISPATCH' and evt == 'READY':
+ self.state = statecodes.Connected
+ if self.on_connect is not None:
+ self.on_connect(data)
+ self.log('info', 'IPC connected successfully.')
+ else:
+ data = {'v': _RPC_VERSION, 'client_id': self.app_id}
+ if self.connection.write(json.dumps(data), opcodes.Handshake):
+ self.state = statecodes.SentHandshake
+ self.log('debug', 'IPC connection sent handshake.')
+ else:
+ self.log('warning', 'IPC failed to send handshake.')
+ self.close()
+
+ def close(self):
+ if self.on_disconnect is not None and self.state in (statecodes.Connected, statecodes.SentHandshake):
+ self.on_disconnect(self._last_err_code, self._last_err_msg)
+ self.log('debug', 'Attempting to close IPC connection.')
+ if self.connection is not None:
+ self.connection.close()
+ else:
+ self.log('warning', 'Called close without a connection!')
+ self.state = statecodes.Disconnected
+
+ def write(self, data):
+ if isinstance(data, dict):
+ data = json.dumps(data)
+ if not self.connection.write(data, opcodes.Frame):
+ self.log('warning', 'Failed to write frame to IPC connection.')
+ self.close()
+ return False
+ return True
+
+ def read(self):
+ if self.state not in (statecodes.Connected, statecodes.SentHandshake):
+ self.log('debug', 'We aren\'t connected, therefore we cannot read data yet.')
+ return False
+ while True:
+ did_read, opcode, data = self.connection.read()
+ self.log('debug', 'ipc.read(): read: {}, Opcode: {}, data: {}'.format(did_read, opcode, data))
+ if not did_read:
+ err_reason = data[0]
+ if (err_reason == errorcodes.PipeClosed and not self.connection.is_open) \
+ or err_reason == errorcodes.ReadCorrupt:
+ self._last_err_code = err_reason
+ self._last_err_msg = data[1]
+ self.log('debug', 'Failed to read; Connection closed. {}'.format(data))
+ self.close()
+ return False, None
+ if opcode == opcodes.Close:
+ data = json2dict(data)
+ self._last_err_code = data.get('code', -1)
+ self._last_err_msg = data.get('message', '')
+ self.log('debug', 'Opcode == Close. Closing connection.')
+ self.close()
+ return False, None
+ elif opcode == opcodes.Frame:
+ data = json2dict(data)
+ self.log('debug', 'Successful read: {}'.format(data))
+ return True, data
+ elif opcode == opcodes.Ping:
+ if not self.connection.write('', opcodes.Pong):
+ self.log('warning', 'Failed to send Pong message.')
+ self.close()
+ elif opcode == opcodes.Pong:
+ # Discord does nothing here
+ pass
+ else:
+ # something bad happened
+ self._last_err_code = errorcodes.ReadCorrupt
+ self._last_err_msg = 'Bad IPC frame.'
+ self.log('warning', 'Got a bad frame from IPC connection.')
+ self.close()
+ return False, None
+
+ def destroy(self):
+ self.log('info', 'Destroying RPC connection.')
+ self.close()
+ self.connection.destroy()
+ self._connection = None
+
+ def log(self, *args):
+ if self._connection is not None:
+ self._connection.log(*args)
+
+ @property
+ def connection(self):
+ return self._connection
+
+ @property
+ def state(self):
+ return self._state
+
+ @state.setter
+ def state(self, state):
+ if isinstance(state, int) and state in (statecodes.Connected, statecodes.SentHandshake,
+ statecodes.Disconnected, statecodes.AwaitingResponse):
+ self._state = state
+ else:
+ self.log('warning', 'Invalid state number!')
+
+ @property
+ def app_id(self):
+ return self._app_id
+
+ @property
+ def is_open(self):
+ return self.state == statecodes.Connected
+
+ @property
+ def on_connect(self):
+ return self._on_connect
+
+ @on_connect.setter
+ def on_connect(self, callback):
+ if callback is None or is_callable(callback):
+ self._on_connect = callback
+ else:
+ self.log('warning', 'on_connect must be callable/None!')
+
+ @property
+ def on_disconnect(self):
+ return self._on_disconnect
+
+ @on_disconnect.setter
+ def on_disconnect(self, callback):
+ if callback is None or is_callable(callback):
+ self._on_disconnect = callback
+ else:
+ self.log('warning', 'on_disconnect must be callable/None!')
diff --git a/discord_rpc/util/__init__.py b/discord_rpc/util/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/discord_rpc/util/__init__.py
diff --git a/discord_rpc/util/backoff.py b/discord_rpc/util/backoff.py
new file mode 100644
index 0000000..650a871
--- /dev/null
+++ b/discord_rpc/util/backoff.py
@@ -0,0 +1,35 @@
+from __future__ import absolute_import
+import random
+from .types import Int32, Int64
+
+
+class Backoff(object):
+ _min_amt = None
+ _max_amt = None
+ _current = None
+ _fails = Int32()
+
+ def __init__(self, min_amt, max_amt):
+ min_amt = max(min_amt, 1)
+ max_amt = max(max_amt, 1)
+ self._min_amt = Int64(min_amt)
+ self._max_amt = Int64(max_amt)
+ self._current = Int64(min_amt)
+
+ def reset(self):
+ self._fails = Int32(0)
+ self._current = self._min_amt.get_copy()
+
+ def next_delay(self):
+ self._fails += 1
+ delay = Int64(self._current.get_number() * 2.0 * random.random())
+ self._current = Int64(min(self._current.get_number() + delay.get_number(), self._max_amt))
+ return self._current
+
+ @property
+ def fails(self):
+ return self._fails
+
+ @property
+ def current(self):
+ return self._current
diff --git a/discord_rpc/util/limits.py b/discord_rpc/util/limits.py
new file mode 100644
index 0000000..6e84b30
--- /dev/null
+++ b/discord_rpc/util/limits.py
@@ -0,0 +1,32 @@
+def get_min_max(bit_size, unsigned=False):
+ if unsigned:
+ bit_min = 0
+ bit_max = (2**bit_size) - 1
+ else:
+ bit_min = -(2**(bit_size - 1))
+ bit_max = 2**(bit_size - 1) - 1
+ return bit_min, bit_max
+
+
+# limits for c types
+CHAR_MIN = -128
+CHAR_MAX = 127
+UCHAR_MAX = 255
+
+SHORT_MIN = -32768
+SHORT_MAX = 32767
+USHORT_MAX = 65535
+
+INT_MIN = -2147483648
+INT_MAX = 2147483647
+UINT_MAX = 4294967295
+INT32_MIN = INT_MIN
+INT32_MAX = INT_MAX
+UINT32_MAX = UINT_MAX
+
+LONG_MIN = -9223372036854775808
+LONG_MAX = 9223372036854775807
+ULONG_MAX = 18446744073709551615
+INT64_MIN = LONG_MIN
+INT64_MAX = LONG_MAX
+UINT64_MAX = ULONG_MAX
diff --git a/discord_rpc/util/types.py b/discord_rpc/util/types.py
new file mode 100644
index 0000000..fdfd246
--- /dev/null
+++ b/discord_rpc/util/types.py
@@ -0,0 +1,349 @@
+from __future__ import absolute_import
+from .limits import INT32_MIN, INT32_MAX, UINT32_MAX, INT64_MIN, INT64_MAX, UINT64_MAX
+
+
+_number_types = [int]
+try:
+ _number_types.append(float)
+except NameError:
+ pass
+try:
+ _number_types.append(long)
+except NameError:
+ pass
+_number_types = tuple(_number_types)
+
+
+class UnderflowError(ArithmeticError):
+ pass
+
+
+class Number(object):
+ _min = None
+ _max = None
+ _bits = None
+ _raise_exceptions = False
+
+ def __init__(self, number=0, raise_exceptions=False):
+ self._raise_exceptions = raise_exceptions
+ if isinstance(number, Number):
+ number = number.get_number()
+ else:
+ # validate number, ret_val should only be number, not a class
+ number = self._check_number(number, number_only=True)
+ if not isinstance(number, _number_types):
+ raise TypeError('Number must be of type int/float/long!')
+ self._number = number
+
+ def _check_number(self, num, number_only=False):
+ if self._max is not None and self._min is not None:
+ if self._raise_exceptions:
+ if num > self._max:
+ raise OverflowError()
+ elif num < self._min:
+ raise UnderflowError()
+ if self._min == 0:
+ if not number_only:
+ return self.__class__(num & self._max)
+ else:
+ return num & self._max
+ else:
+ if num & (1 << (self._bits-1)):
+ if not number_only:
+ return self.__class__(num | ~self._max)
+ else:
+ return num | ~self._max
+ else:
+ if not number_only:
+ return self.__class__(num & self._max)
+ else:
+ return num & self._max
+ else:
+ # there are no min/max to check with, so just return the number
+ if not number_only:
+ return self.__class__(num)
+ else:
+ return num
+
+ def __lt__(self, other):
+ if isinstance(other, Number):
+ other = other.get_number()
+ return self._number < other
+
+ def __le__(self, other):
+ if isinstance(other, Number):
+ other = other.get_number()
+ return self._number <= other
+
+ def __eq__(self, other):
+ if isinstance(other, Number):
+ other = other.get_number()
+ return self._number == other
+
+ def __ne__(self, other):
+ if isinstance(other, Number):
+ other = other.get_number()
+ return self._number != other
+
+ def __gt__(self, other):
+ if isinstance(other, Number):
+ other = other.get_number()
+ return self._number > other
+
+ def __ge__(self, other):
+ if isinstance(other, Number):
+ other = other.get_number()
+ return self._number >= other
+
+ def __bool__(self):
+ return self._number
+
+ def __repr__(self):
+ return "{class_name}({number})".format(class_name=self.__class__.__name__, number=self._number)
+
+ def __str__(self):
+ return str(self._number)
+
+ def __cmp__(self, other):
+ if isinstance(other, Number):
+ other = other.get_number()
+ if self._number == other:
+ return 0
+ elif self._number < other:
+ return -1
+ elif self._number > other:
+ return 1
+
+ def __nonzero__(self):
+ return self._number != 0
+
+ def __add__(self, other):
+ if isinstance(other, Number):
+ other = other.get_number()
+ return self._check_number(self._number + other)
+
+ def __sub__(self, other):
+ if isinstance(other, Number):
+ other = other.get_number()
+ return self._check_number(self._number - other)
+
+ def __mul__(self, other):
+ if isinstance(other, Number):
+ other = other.get_number()
+ return self._check_number(self._number * other)
+
+ def __div__(self, other):
+ if isinstance(other, Number):
+ other = other.get_number()
+ return self._check_number(self._number / other)
+
+ def __truediv__(self, other):
+ if isinstance(other, Number):
+ other = other.get_number()
+ return self.__div__(other)
+
+ def __floordiv__(self, other):
+ if isinstance(other, Number):
+ other = other.get_number()
+ return self._check_number(self._number // other)
+
+ def __mod__(self, other):
+ if isinstance(other, Number):
+ other = other.get_number()
+ return self._check_number(self._number % other)
+
+ def __divmod__(self, other):
+ if isinstance(other, Number):
+ other = other.get_number()
+ return self._check_number(divmod(self._number, other))
+
+ def __pow__(self, power, modulo=None):
+ if isinstance(power, Number):
+ power = power.get_number()
+ if modulo is not None and isinstance(modulo, Number):
+ modulo = modulo.get_number()
+ return self._check_number(pow(self._number, power, modulo))
+
+ def __lshift__(self, other):
+ if isinstance(other, Number):
+ other = other.get_number()
+ return self._check_number(self._number << other)
+
+ def __rshift__(self, other):
+ if isinstance(other, Number):
+ other = other.get_number()
+ return self._check_number(self._number >> other)
+
+ def __and__(self, other):
+ if isinstance(other, Number):
+ other = other.get_number()
+ return self._check_number(self._number & other)
+
+ def __xor__(self, other):
+ if isinstance(other, Number):
+ other = other.get_number()
+ return self._check_number(self._number ^ other)
+
+ def __or__(self, other):
+ if isinstance(other, Number):
+ other = other.get_number()
+ return self._check_number(self._number | other)
+
+ def __radd__(self, other):
+ if isinstance(other, Number):
+ other = other.get_number()
+ return self._check_number(other + self._number)
+
+ def __rsub__(self, other):
+ if isinstance(other, Number):
+ other = other.get_number()
+ return self._check_number(other - self._number)
+
+ def __rmul__(self, other):
+ if isinstance(other, Number):
+ other = other.get_number()
+ return self._check_number(other * self._number)
+
+ def __rdiv__(self, other):
+ if isinstance(other, Number):
+ other = other.get_number()
+ return self._check_number(other / self._number)
+
+ def __rtruediv__(self, other):
+ if isinstance(other, Number):
+ other = other.get_number()
+ return self.__rdiv__(other)
+
+ def __rfloordiv__(self, other):
+ if isinstance(other, Number):
+ other = other.get_number()
+ return self._check_number(other // self._number)
+
+ def __rmod__(self, other):
+ if isinstance(other, Number):
+ other = other.get_number()
+ return self._check_number(other % self._number)
+
+ def __rdivmod__(self, other):
+ if isinstance(other, Number):
+ other = other.get_number()
+ return self._check_number(divmod(other, self._number))
+
+ def __rpow__(self, other):
+ if isinstance(other, Number):
+ other = other.get_number()
+ return self._check_number(pow(other, self._number))
+
+ def __rlshift__(self, other):
+ if isinstance(other, Number):
+ other = other.get_number()
+ return self._check_number(other << self._number)
+
+ def __rrshift__(self, other):
+ if isinstance(other, Number):
+ other = other.get_number()
+ return self._check_number(other >> self._number)
+
+ def __rand__(self, other):
+ if isinstance(other, Number):
+ other = other.get_number()
+ return self._check_number(other & self._number)
+
+ def __rxor__(self, other):
+ if isinstance(other, Number):
+ other = other.get_number()
+ return self._check_number(other ^ self._number)
+
+ def __ror__(self, other):
+ if isinstance(other, Number):
+ other = other.get_number()
+ return self._check_number(other | self._number)
+
+ def __neg__(self):
+ return self._check_number(-self._number)
+
+ def __pos__(self):
+ return self._check_number(+self._number)
+
+ def __abs__(self):
+ return self._check_number(abs(self._number))
+
+ def __invert__(self):
+ return self._check_number(~self._number)
+
+ def __complex__(self):
+ return complex(self._number)
+
+ def __int__(self):
+ return int(self._number)
+
+ def __long__(self):
+ return long(self._number)
+
+ def __float__(self):
+ return float(self._number)
+
+ def __oct__(self):
+ return oct(self._number)
+
+ def __hex__(self):
+ return hex(self._number)
+
+ def __round__(self, ndigits=None):
+ if ndigits is not None and isinstance(ndigits, Number):
+ ndigits = ndigits.get_number()
+ return round(self._number, ndigits)
+
+ def __trunc__(self):
+ from math import trunc
+ return trunc(self._number)
+
+ def __floor__(self):
+ from math import floor
+ return floor(self._number)
+
+ def __ceil__(self):
+ from math import ceil
+ return ceil(self._number)
+
+ def get_number(self):
+ return self._number
+
+ def get_copy(self):
+ return self.__class__(self._number)
+
+
+class _Int(Number):
+ def __init__(self, number=0, raise_exceptions=False):
+ if isinstance(number, Number):
+ number = number.get_number()
+ number = int(number)
+ Number.__init__(self, number, raise_exceptions)
+
+ def _check_number(self, num, number_only=False):
+ if isinstance(num, Number):
+ num = num.get_number()
+ num = int(num)
+ return Number._check_number(self, num, number_only)
+
+
+class Int32(_Int):
+ _min = INT32_MIN
+ _max = INT32_MAX
+ _bits = 32
+
+
+class Int64(_Int):
+ _min = INT64_MIN
+ _max = INT64_MAX
+ _bits = 64
+
+
+class UInt32(_Int):
+ _min = 0
+ _max = UINT32_MAX
+
+
+class UInt64(_Int):
+ _min = 0
+ _max = UINT64_MAX
diff --git a/discord_rpc/util/utils.py b/discord_rpc/util/utils.py
new file mode 100644
index 0000000..8142596
--- /dev/null
+++ b/discord_rpc/util/utils.py
@@ -0,0 +1,180 @@
+from copy import deepcopy
+import json
+from os import getenv, getpid, path
+import platform
+from sys import version_info, argv
+
+
+class Empty(Exception):
+ pass
+
+
+class DummyQueue(object):
+ """
+ Dummy queue thread that does nothing. Should only be used if imports fail.
+ """
+
+ def __init__(self, maxsize=0):
+ pass
+
+ def qsize(self):
+ return 0
+
+ def empty(self):
+ return True
+
+ def full(self):
+ return False
+
+ def put(self, obj, *args, **kwargs):
+ pass
+
+ def put_nowait(self, obj):
+ pass
+
+ def get(self, *args, **kwargs):
+ raise Empty
+
+ def get_nowait(self):
+ raise Empty
+
+ def task_done(self):
+ pass
+
+ def join(self):
+ pass
+
+
+def is_python3():
+ return version_info[0] == 3
+
+
+def is_windows():
+ return platform.system() == 'Windows'
+
+
+def is_linux():
+ return platform.system() == 'Linux'
+
+
+def is_mac_osx():
+ # this may not be accurate, just going off of what I find off the internet
+ return platform.system() == 'Darwin'
+
+
+def get_temp_path():
+ if is_windows():
+ return None
+ for val in ('XDG_RUNTIME_DIR', 'TMPDIR', 'TMP', 'TEMP'):
+ tmp = getenv(val)
+ if tmp is not None:
+ return tmp
+ return '/tmp'
+
+
+def get_process_id():
+ return getpid()
+
+
+def is_callable(obj):
+ try:
+ # for Python 2.x or Python 3.2+
+ return callable(obj)
+ except Exception:
+ # for Python version: 3 - 3.2
+ return hasattr(obj, '__call__')
+
+
+# python 2 + 3 compatibility
+if is_python3():
+ unicode = str
+ bytes = bytes
+else:
+ bytes = str
+ unicode = unicode
+
+
+def to_bytes(obj):
+ if isinstance(obj, type(b'')):
+ return obj
+ if hasattr(obj, 'encode') and is_callable(obj.encode):
+ return obj.encode('ascii', 'replace')
+ raise TypeError('Could not convert object type "{}" to bytes!'.format(type(obj)))
+
+
+def to_unicode(obj):
+ if isinstance(obj, type(u'')):
+ return obj
+ if hasattr(obj, 'decode') and is_callable(obj.decode):
+ return obj.decode(encoding='utf-8')
+ raise TypeError('Could not convert object type "{}" to unicode!'.format(type(obj)))
+
+
+def iter_keys(obj):
+ if not isinstance(obj, dict):
+ raise TypeError('Object must be of type dict!')
+ if is_python3():
+ return obj.keys()
+ return obj.iterkeys()
+
+
+def iter_items(obj):
+ if not isinstance(obj, dict):
+ raise TypeError('Object must be of type dict!')
+ if is_python3():
+ return obj.items()
+ return obj.iteritems()
+
+
+def iter_values(obj):
+ if not isinstance(obj, dict):
+ raise TypeError('Object must be of type dict!')
+ if is_python3():
+ return obj.values()
+ return obj.itervalues()
+
+
+def _py_dict(obj):
+ if not isinstance(obj, dict):
+ raise TypeError('Object must be of type dict!')
+ new_dict = dict()
+ for name, val in iter_items(obj):
+ if isinstance(name, type(b'')) and is_python3():
+ name = to_unicode(name)
+ elif isinstance(name, type(u'')) and not is_python3():
+ name = to_bytes(name)
+ if isinstance(val, dict):
+ val = _py_dict(val)
+ elif isinstance(val, type(b'')) and is_python3():
+ val = to_unicode(val)
+ elif isinstance(val, type(u'')) and not is_python3():
+ val = to_bytes(val)
+ new_dict[name] = val
+ return deepcopy(new_dict)
+
+
+def json2dict(obj):
+ if isinstance(obj, dict):
+ return deepcopy(_py_dict(obj))
+ if obj is None:
+ return dict()
+ if hasattr(obj, 'strip'):
+ if obj.strip() == '':
+ return dict()
+ else:
+ return deepcopy(_py_dict(deepcopy(json.loads(obj))))
+ raise TypeError('Object must be of string type!')
+
+
+if not is_python3():
+ range = xrange
+else:
+ range = range
+
+
+def get_executable_directory():
+ return path.abspath(path.dirname(argv[0]))
+
+
+def get_executable_path():
+ return path.join(get_executable_directory(), argv[0])