lynxpresence 4.4.0__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of lynxpresence might be problematic. Click here for more details.
- lynxpresence-4.4.0.dist-info/METADATA +82 -0
- lynxpresence-4.4.0.dist-info/RECORD +15 -0
- lynxpresence-4.4.0.dist-info/WHEEL +6 -0
- lynxpresence-4.4.0.dist-info/licenses/LICENSE +21 -0
- lynxpresence-4.4.0.dist-info/top_level.txt +1 -0
- lynxpresence-4.4.0.dist-info/zip-safe +1 -0
- pypresence/__init__.py +18 -0
- pypresence/baseclient.py +130 -0
- pypresence/client.py +385 -0
- pypresence/exceptions.py +68 -0
- pypresence/payloads.py +316 -0
- pypresence/presence.py +90 -0
- pypresence/py.typed +0 -0
- pypresence/types.py +15 -0
- pypresence/utils.py +64 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lynxpresence
|
|
3
|
+
Version: 4.4.0
|
|
4
|
+
Summary: Discord RPC client written in Python
|
|
5
|
+
Home-page: https://github.com/C0rn3j/pypresence
|
|
6
|
+
Author: C0rn3j
|
|
7
|
+
License: MIT
|
|
8
|
+
Keywords: discord rich presence pypresence rpc api wrapper gamers chat irc
|
|
9
|
+
Platform: Windows
|
|
10
|
+
Platform: Linux
|
|
11
|
+
Platform: OSX
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
15
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
16
|
+
Classifier: Operating System :: MacOS :: MacOS X
|
|
17
|
+
Classifier: Programming Language :: Python
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
24
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
25
|
+
Classifier: Typing :: Typed
|
|
26
|
+
Classifier: Intended Audience :: Developers
|
|
27
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
28
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
29
|
+
Classifier: Topic :: Communications :: Chat
|
|
30
|
+
Classifier: Framework :: AsyncIO
|
|
31
|
+
Requires-Python: >=3.9
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
License-File: LICENSE
|
|
34
|
+
Dynamic: author
|
|
35
|
+
Dynamic: classifier
|
|
36
|
+
Dynamic: description
|
|
37
|
+
Dynamic: description-content-type
|
|
38
|
+
Dynamic: home-page
|
|
39
|
+
Dynamic: keywords
|
|
40
|
+
Dynamic: license
|
|
41
|
+
Dynamic: license-file
|
|
42
|
+
Dynamic: platform
|
|
43
|
+
Dynamic: requires-python
|
|
44
|
+
Dynamic: summary
|
|
45
|
+
|
|
46
|
+
Forked as upstream did not make a release in quite a while, I hope to kill this fork off in the future.
|
|
47
|
+
|
|
48
|
+
> A Discord Rich Presence Client in Python? Looks like you've come to the right place.
|
|
49
|
+

|
|
50
|
+
|
|
51
|
+
----------
|
|
52
|
+
|
|
53
|
+
# Installation
|
|
54
|
+
|
|
55
|
+
Install pypresence with **`pip`**
|
|
56
|
+
|
|
57
|
+
For the latest development version:
|
|
58
|
+
|
|
59
|
+
### `pip install https://github.com/C0rn3j/pypresence/archive/master.zip`
|
|
60
|
+
|
|
61
|
+
For the latest stable version:
|
|
62
|
+
|
|
63
|
+
### `pip install lynxpresence`
|
|
64
|
+
|
|
65
|
+
----------
|
|
66
|
+
|
|
67
|
+
# Documentation
|
|
68
|
+
|
|
69
|
+
> [!Note]
|
|
70
|
+
> You need an **authorized app** to do anything besides rich presence!
|
|
71
|
+
|
|
72
|
+
#### [pypresence Documentation](https://qwertyquerty.github.io/pypresence/html/index.html)
|
|
73
|
+
#### [Discord Rich Presence Documentation](https://discord.com/developers/docs/rich-presence/how-to)
|
|
74
|
+
#### [Discord RPC Documentation](https://discord.com/developers/docs/topics/rpc)
|
|
75
|
+
#### [pyresence Discord Support Server](https://discord.gg/JF3kg77)
|
|
76
|
+
#### [Discord API Support Server](https://discord.gg/discord-api)
|
|
77
|
+
|
|
78
|
+
----------
|
|
79
|
+
|
|
80
|
+
# Examples
|
|
81
|
+
|
|
82
|
+
Examples can be found in the [examples](https://github.com/C0rn3j/pypresence/tree/master/examples) directory, and you can contribute your own examples if you wish, just read [examples.md](https://github.com/C0rn3j/pypresence/blob/master/examples/examples.md)!
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
lynxpresence-4.4.0.dist-info/licenses/LICENSE,sha256=xHIb8Oev4PgFc1SNQc69cOh4-s8nd0EL6S-DhjEw2tA,1069
|
|
2
|
+
pypresence/__init__.py,sha256=j08h4vFQRSztxkUkOxbsBE5esBGD_isXp63m7rO0V5g,421
|
|
3
|
+
pypresence/baseclient.py,sha256=dPtxH2VfE5pbO5cMflZzhOpH9-6RV4gsGxQBaLaJ6hs,4997
|
|
4
|
+
pypresence/client.py,sha256=mGCRU5LPBhiRtSNvC1MyY8emSbpoNX0R2HAQAcp3iKo,16106
|
|
5
|
+
pypresence/exceptions.py,sha256=bj7P6lllBpxjsr6Y6HbmFgP1caA8QIDNjuBJ90CG364,2280
|
|
6
|
+
pypresence/payloads.py,sha256=j2U6xpkrVaHvjh6RFLdfARgW5DX008Bj2r-xPvOc5vQ,9097
|
|
7
|
+
pypresence/presence.py,sha256=1eSxnEPj56-242P98HdI1J85q_8gVBwSVbOBzywkTrk,4186
|
|
8
|
+
pypresence/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
+
pypresence/types.py,sha256=DuXV8zdKdZHmwOwEX9KcDGcvh7qRhHe9cVECexsMvdQ,334
|
|
10
|
+
pypresence/utils.py,sha256=sa4zpFIb8IHj03yzd3HUWq7KFK566MPUIXFkZAeBT10,1959
|
|
11
|
+
lynxpresence-4.4.0.dist-info/METADATA,sha256=Z0AktccWp7hNtw-WxgMrix8fAe2jRPB3ylRtKoIrOoo,2888
|
|
12
|
+
lynxpresence-4.4.0.dist-info/WHEEL,sha256=9bhjOwO--Rs91xaPcBdlYFUmIudhuXqFlPriQeYQITw,109
|
|
13
|
+
lynxpresence-4.4.0.dist-info/top_level.txt,sha256=HgPRAKZXYeILcC2btf3X4WYdVa6CdfiT2Kl_O42RSuY,11
|
|
14
|
+
lynxpresence-4.4.0.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
15
|
+
lynxpresence-4.4.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 qwertyquerty
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pypresence
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
pypresence/__init__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Python RPC Client for Discord
|
|
3
|
+
-----------------------------
|
|
4
|
+
By: qwertyquerty and LewdNeko
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .baseclient import BaseClient
|
|
8
|
+
from .client import Client, AioClient
|
|
9
|
+
from .exceptions import *
|
|
10
|
+
from .types import ActivityType
|
|
11
|
+
from .presence import Presence, AioPresence
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
__title__ = 'pypresence'
|
|
15
|
+
__author__ = 'C0rn3j'
|
|
16
|
+
__copyright__ = 'Copyright 2018 - Current qwertyquerty'
|
|
17
|
+
__license__ = 'MIT'
|
|
18
|
+
__version__ = '4.4.0'
|
pypresence/baseclient.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import inspect
|
|
5
|
+
import json
|
|
6
|
+
import struct
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
# TODO: Get rid of this import * lol
|
|
10
|
+
from .exceptions import *
|
|
11
|
+
from .payloads import Payload
|
|
12
|
+
from .utils import get_ipc_path, get_event_loop
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BaseClient:
|
|
16
|
+
|
|
17
|
+
def __init__(self, client_id: str, **kwargs):
|
|
18
|
+
loop = kwargs.get('loop', None)
|
|
19
|
+
handler = kwargs.get('handler', None)
|
|
20
|
+
self.pipe = kwargs.get('pipe', None)
|
|
21
|
+
self.isasync = kwargs.get('isasync', False)
|
|
22
|
+
self.connection_timeout = kwargs.get('connection_timeout', 30)
|
|
23
|
+
self.response_timeout = kwargs.get('response_timeout', 10)
|
|
24
|
+
|
|
25
|
+
client_id = str(client_id)
|
|
26
|
+
|
|
27
|
+
if loop is not None:
|
|
28
|
+
self.update_event_loop(loop)
|
|
29
|
+
else:
|
|
30
|
+
self.update_event_loop(get_event_loop())
|
|
31
|
+
|
|
32
|
+
self.sock_reader: asyncio.StreamReader | None = None
|
|
33
|
+
self.sock_writer: asyncio.StreamWriter | None = None
|
|
34
|
+
|
|
35
|
+
self.client_id = client_id
|
|
36
|
+
|
|
37
|
+
if handler is not None:
|
|
38
|
+
if not inspect.isfunction(handler):
|
|
39
|
+
raise PyPresenceException('Error handler must be a function.')
|
|
40
|
+
args = inspect.getfullargspec(handler).args
|
|
41
|
+
if args[0] == 'self':
|
|
42
|
+
args = args[1:]
|
|
43
|
+
if len(args) != 2:
|
|
44
|
+
raise PyPresenceException('Error handler should only accept two arguments.')
|
|
45
|
+
|
|
46
|
+
if self.isasync:
|
|
47
|
+
if not inspect.iscoroutinefunction(handler):
|
|
48
|
+
raise InvalidArgument('Coroutine', 'Subroutine', 'You are running async mode - '
|
|
49
|
+
'your error handler should be awaitable.')
|
|
50
|
+
err_handler = self._async_err_handle
|
|
51
|
+
else:
|
|
52
|
+
err_handler = self._err_handle
|
|
53
|
+
|
|
54
|
+
self.loop.set_exception_handler(err_handler)
|
|
55
|
+
self.handler = handler
|
|
56
|
+
|
|
57
|
+
if getattr(self, "on_event", None): # Tasty bad code ;^)
|
|
58
|
+
self._events_on = True
|
|
59
|
+
else:
|
|
60
|
+
self._events_on = False
|
|
61
|
+
|
|
62
|
+
def update_event_loop(self, loop):
|
|
63
|
+
# noinspection PyAttributeOutsideInit
|
|
64
|
+
self.loop = loop
|
|
65
|
+
asyncio.set_event_loop(self.loop)
|
|
66
|
+
|
|
67
|
+
def _err_handle(self, loop, context: dict):
|
|
68
|
+
result = self.handler(context['exception'], context['future'])
|
|
69
|
+
if inspect.iscoroutinefunction(self.handler):
|
|
70
|
+
loop.run_until_complete(result)
|
|
71
|
+
|
|
72
|
+
# noinspection PyUnusedLocal
|
|
73
|
+
async def _async_err_handle(self, loop, context: dict):
|
|
74
|
+
await self.handler(context['exception'], context['future'])
|
|
75
|
+
|
|
76
|
+
async def read_output(self):
|
|
77
|
+
try:
|
|
78
|
+
preamble = await asyncio.wait_for(self.sock_reader.read(8), self.response_timeout)
|
|
79
|
+
status_code, length = struct.unpack('<II', preamble[:8])
|
|
80
|
+
data = await asyncio.wait_for(self.sock_reader.read(length), self.response_timeout)
|
|
81
|
+
except (BrokenPipeError, struct.error):
|
|
82
|
+
raise PipeClosed
|
|
83
|
+
except asyncio.TimeoutError:
|
|
84
|
+
raise ResponseTimeout
|
|
85
|
+
payload = json.loads(data.decode('utf-8'))
|
|
86
|
+
if payload["evt"] == "ERROR":
|
|
87
|
+
raise ServerError(payload["data"]["message"])
|
|
88
|
+
return payload
|
|
89
|
+
|
|
90
|
+
def send_data(self, op: int, payload: dict | Payload):
|
|
91
|
+
if isinstance(payload, Payload):
|
|
92
|
+
payload = payload.data
|
|
93
|
+
payload = json.dumps(payload)
|
|
94
|
+
|
|
95
|
+
assert self.sock_writer is not None, "You must connect your client before sending events!"
|
|
96
|
+
|
|
97
|
+
self.sock_writer.write(
|
|
98
|
+
struct.pack(
|
|
99
|
+
'<II',
|
|
100
|
+
op,
|
|
101
|
+
len(payload)) +
|
|
102
|
+
payload.encode('utf-8'))
|
|
103
|
+
|
|
104
|
+
async def handshake(self):
|
|
105
|
+
ipc_path = get_ipc_path(self.pipe)
|
|
106
|
+
if not ipc_path:
|
|
107
|
+
raise DiscordNotFound
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
if sys.platform == 'linux' or sys.platform == 'darwin':
|
|
111
|
+
self.sock_reader, self.sock_writer = await asyncio.wait_for(asyncio.open_unix_connection(ipc_path), self.connection_timeout)
|
|
112
|
+
elif sys.platform == 'win32':
|
|
113
|
+
self.sock_reader = asyncio.StreamReader(loop=self.loop)
|
|
114
|
+
reader_protocol = asyncio.StreamReaderProtocol(self.sock_reader, loop=self.loop)
|
|
115
|
+
self.sock_writer, _ = await asyncio.wait_for(self.loop.create_pipe_connection(lambda: reader_protocol, ipc_path), self.connection_timeout)
|
|
116
|
+
except FileNotFoundError:
|
|
117
|
+
raise InvalidPipe
|
|
118
|
+
except asyncio.TimeoutError:
|
|
119
|
+
raise ConnectionTimeout
|
|
120
|
+
|
|
121
|
+
self.send_data(0, {'v': 1, 'client_id': self.client_id})
|
|
122
|
+
preamble = await self.sock_reader.read(8)
|
|
123
|
+
code, length = struct.unpack('<ii', preamble)
|
|
124
|
+
data = json.loads(await self.sock_reader.read(length))
|
|
125
|
+
if 'code' in data:
|
|
126
|
+
if data['message'] == 'Invalid Client ID':
|
|
127
|
+
raise InvalidID
|
|
128
|
+
raise DiscordError(data['code'], data['message'])
|
|
129
|
+
if self._events_on:
|
|
130
|
+
self.sock_reader.feed_data = self.on_event
|
pypresence/client.py
ADDED
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import asyncio
|
|
3
|
+
import inspect
|
|
4
|
+
import struct
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from typing import List
|
|
8
|
+
|
|
9
|
+
from .baseclient import BaseClient
|
|
10
|
+
from .exceptions import *
|
|
11
|
+
from .payloads import Payload
|
|
12
|
+
from .types import ActivityType
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Client(BaseClient):
|
|
16
|
+
def __init__(self, *args, **kwargs):
|
|
17
|
+
super().__init__(*args, **kwargs)
|
|
18
|
+
self._closed = False
|
|
19
|
+
self._events = {}
|
|
20
|
+
|
|
21
|
+
def register_event(self, event: str, func: callable, args=None):
|
|
22
|
+
if args is None:
|
|
23
|
+
args = {}
|
|
24
|
+
if inspect.iscoroutinefunction(func):
|
|
25
|
+
raise NotImplementedError
|
|
26
|
+
elif len(inspect.signature(func).parameters) != 1:
|
|
27
|
+
raise ArgumentError
|
|
28
|
+
self.subscribe(event, args)
|
|
29
|
+
self._events[event.lower()] = func
|
|
30
|
+
|
|
31
|
+
def unregister_event(self, event: str, args=None):
|
|
32
|
+
if args is None:
|
|
33
|
+
args = {}
|
|
34
|
+
event = event.lower()
|
|
35
|
+
if event not in self._events:
|
|
36
|
+
raise EventNotFound
|
|
37
|
+
self.unsubscribe(event, args)
|
|
38
|
+
del self._events[event]
|
|
39
|
+
|
|
40
|
+
# noinspection PyProtectedMember
|
|
41
|
+
def on_event(self, data):
|
|
42
|
+
if self.sock_reader._eof:
|
|
43
|
+
raise PyPresenceException('feed_data after feed_eof')
|
|
44
|
+
if not data:
|
|
45
|
+
return
|
|
46
|
+
self.sock_reader._buffer.extend(data)
|
|
47
|
+
self.sock_reader._wakeup_waiter()
|
|
48
|
+
if (self.sock_reader._transport is not None and
|
|
49
|
+
not self.sock_reader._paused and
|
|
50
|
+
len(self.sock_reader._buffer) > 2 * self.sock_reader._limit):
|
|
51
|
+
try:
|
|
52
|
+
self.sock_reader._transport.pause_reading()
|
|
53
|
+
except NotImplementedError:
|
|
54
|
+
self.sock_reader._transport = None
|
|
55
|
+
else:
|
|
56
|
+
self.sock_reader._paused = True
|
|
57
|
+
|
|
58
|
+
end = 0
|
|
59
|
+
while end < len(data):
|
|
60
|
+
# While chunks are available in data
|
|
61
|
+
start = end + 8
|
|
62
|
+
status_code, length = struct.unpack('<II', data[end:start])
|
|
63
|
+
end = length + start
|
|
64
|
+
payload = json.loads(data[start:end].decode('utf-8'))
|
|
65
|
+
|
|
66
|
+
if payload["evt"] is not None:
|
|
67
|
+
evt = payload["evt"].lower()
|
|
68
|
+
if evt in self._events:
|
|
69
|
+
self._events[evt](payload["data"])
|
|
70
|
+
elif evt == 'error':
|
|
71
|
+
raise DiscordError(payload["data"]["code"], payload["data"]["message"])
|
|
72
|
+
|
|
73
|
+
def authorize(self, client_id: str, scopes: List[str]):
|
|
74
|
+
payload = Payload.authorize(client_id, scopes)
|
|
75
|
+
self.send_data(1, payload)
|
|
76
|
+
return self.loop.run_until_complete(self.read_output())
|
|
77
|
+
|
|
78
|
+
def authenticate(self, token: str):
|
|
79
|
+
payload = Payload.authenticate(token)
|
|
80
|
+
self.send_data(1, payload)
|
|
81
|
+
return self.loop.run_until_complete(self.read_output())
|
|
82
|
+
|
|
83
|
+
def get_guilds(self):
|
|
84
|
+
payload = Payload.get_guilds()
|
|
85
|
+
self.send_data(1, payload)
|
|
86
|
+
return self.loop.run_until_complete(self.read_output())
|
|
87
|
+
|
|
88
|
+
def get_guild(self, guild_id: str):
|
|
89
|
+
payload = Payload.get_guild(guild_id)
|
|
90
|
+
self.send_data(1, payload)
|
|
91
|
+
return self.loop.run_until_complete(self.read_output())
|
|
92
|
+
|
|
93
|
+
def get_channel(self, channel_id: str):
|
|
94
|
+
payload = Payload.get_channel(channel_id)
|
|
95
|
+
self.send_data(1, payload)
|
|
96
|
+
return self.loop.run_until_complete(self.read_output())
|
|
97
|
+
|
|
98
|
+
def get_channels(self, guild_id: str):
|
|
99
|
+
payload = Payload.get_channels(guild_id)
|
|
100
|
+
self.send_data(1, payload)
|
|
101
|
+
return self.loop.run_until_complete(self.read_output())
|
|
102
|
+
|
|
103
|
+
def set_user_voice_settings(self, user_id: str, pan_left: float | None = None,
|
|
104
|
+
pan_right: float | None = None, volume: int | None = None,
|
|
105
|
+
mute: bool | None = None):
|
|
106
|
+
payload = Payload.set_user_voice_settings(user_id, pan_left, pan_right, volume, mute)
|
|
107
|
+
self.send_data(1, payload)
|
|
108
|
+
return self.loop.run_until_complete(self.read_output())
|
|
109
|
+
|
|
110
|
+
def select_voice_channel(self, channel_id: str):
|
|
111
|
+
payload = Payload.select_voice_channel(channel_id)
|
|
112
|
+
self.send_data(1, payload)
|
|
113
|
+
return self.loop.run_until_complete(self.read_output())
|
|
114
|
+
|
|
115
|
+
def get_selected_voice_channel(self):
|
|
116
|
+
payload = Payload.get_selected_voice_channel()
|
|
117
|
+
self.send_data(1, payload)
|
|
118
|
+
return self.loop.run_until_complete(self.read_output())
|
|
119
|
+
|
|
120
|
+
def select_text_channel(self, channel_id: str):
|
|
121
|
+
payload = Payload.select_text_channel(channel_id)
|
|
122
|
+
self.send_data(1, payload)
|
|
123
|
+
return self.loop.run_until_complete(self.read_output())
|
|
124
|
+
|
|
125
|
+
def set_activity(self, pid: int = os.getpid(),
|
|
126
|
+
activity_type: ActivityType | None = None,
|
|
127
|
+
state: str | None = None, details: str | None = None,
|
|
128
|
+
start: int | None = None, end: int | None = None,
|
|
129
|
+
large_image: str | None = None, large_text: str | None = None,
|
|
130
|
+
small_image: str | None = None, small_text: str | None = None,
|
|
131
|
+
party_id: str | None = None, party_size: list | None = None,
|
|
132
|
+
join: str | None = None, spectate: str | None = None,
|
|
133
|
+
match: str | None = None, buttons: list | None = None,
|
|
134
|
+
instance: bool = True):
|
|
135
|
+
payload = Payload.set_activity(pid=pid, activity_type=activity_type, state=state, details=details, start=start,
|
|
136
|
+
end=end, large_image=large_image, large_text=large_text, small_image=small_image,
|
|
137
|
+
small_text=small_text, party_id=party_id, party_size=party_size, join=join,
|
|
138
|
+
spectate=spectate, match=match, buttons=buttons, instance=instance,
|
|
139
|
+
activity=True)
|
|
140
|
+
|
|
141
|
+
self.send_data(1, payload)
|
|
142
|
+
return self.loop.run_until_complete(self.read_output())
|
|
143
|
+
|
|
144
|
+
def clear_activity(self, pid: int = os.getpid()):
|
|
145
|
+
payload = Payload.set_activity(pid, activity=None)
|
|
146
|
+
self.send_data(1, payload)
|
|
147
|
+
return self.loop.run_until_complete(self.read_output())
|
|
148
|
+
|
|
149
|
+
def subscribe(self, event: str, args=None):
|
|
150
|
+
if args is None:
|
|
151
|
+
args = {}
|
|
152
|
+
payload = Payload.subscribe(event, args)
|
|
153
|
+
self.send_data(1, payload)
|
|
154
|
+
return self.loop.run_until_complete(self.read_output())
|
|
155
|
+
|
|
156
|
+
def unsubscribe(self, event: str, args=None):
|
|
157
|
+
if args is None:
|
|
158
|
+
args = {}
|
|
159
|
+
payload = Payload.unsubscribe(event, args)
|
|
160
|
+
self.send_data(1, payload)
|
|
161
|
+
return self.loop.run_until_complete(self.read_output())
|
|
162
|
+
|
|
163
|
+
def get_voice_settings(self):
|
|
164
|
+
payload = Payload.get_voice_settings()
|
|
165
|
+
self.send_data(1, payload)
|
|
166
|
+
return self.loop.run_until_complete(self.read_output())
|
|
167
|
+
|
|
168
|
+
def set_voice_settings(self, _input: dict | None = None, output: dict | None = None,
|
|
169
|
+
mode: dict | None = None, automatic_gain_control: bool | None = None,
|
|
170
|
+
echo_cancellation: bool | None = None, noise_suppression: bool | None = None,
|
|
171
|
+
qos: bool | None = None, silence_warning: bool | None = None,
|
|
172
|
+
deaf: bool | None = None, mute: bool | None = None):
|
|
173
|
+
payload = Payload.set_voice_settings(_input, output, mode, automatic_gain_control, echo_cancellation,
|
|
174
|
+
noise_suppression, qos, silence_warning, deaf, mute)
|
|
175
|
+
self.send_data(1, payload)
|
|
176
|
+
return self.loop.run_until_complete(self.read_output())
|
|
177
|
+
|
|
178
|
+
def capture_shortcut(self, action: str):
|
|
179
|
+
payload = Payload.capture_shortcut(action)
|
|
180
|
+
self.send_data(1, payload)
|
|
181
|
+
return self.loop.run_until_complete(self.read_output())
|
|
182
|
+
|
|
183
|
+
def send_activity_join_invite(self, user_id: str):
|
|
184
|
+
payload = Payload.send_activity_join_invite(user_id)
|
|
185
|
+
self.send_data(1, payload)
|
|
186
|
+
return self.loop.run_until_complete(self.read_output())
|
|
187
|
+
|
|
188
|
+
def close_activity_request(self, user_id: str):
|
|
189
|
+
payload = Payload.close_activity_request(user_id)
|
|
190
|
+
self.send_data(1, payload)
|
|
191
|
+
return self.loop.run_until_complete(self.read_output())
|
|
192
|
+
|
|
193
|
+
def close(self):
|
|
194
|
+
self.send_data(2, {'v': 1, 'client_id': self.client_id})
|
|
195
|
+
self.sock_writer.close()
|
|
196
|
+
self._closed = True
|
|
197
|
+
self.loop.close()
|
|
198
|
+
|
|
199
|
+
def start(self):
|
|
200
|
+
self.loop.run_until_complete(self.handshake())
|
|
201
|
+
|
|
202
|
+
def read(self):
|
|
203
|
+
return self.loop.run_until_complete(self.read_output())
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class AioClient(BaseClient):
|
|
207
|
+
def __init__(self, *args, **kwargs):
|
|
208
|
+
super().__init__(*args, **kwargs, isasync=True)
|
|
209
|
+
self._closed = False
|
|
210
|
+
self._events = {}
|
|
211
|
+
|
|
212
|
+
async def register_event(self, event: str, func: callable, args=None):
|
|
213
|
+
if args is None:
|
|
214
|
+
args = {}
|
|
215
|
+
if not inspect.iscoroutinefunction(func):
|
|
216
|
+
raise InvalidArgument('Coroutine', 'Subroutine', 'Event function must be a coroutine')
|
|
217
|
+
elif len(inspect.signature(func).parameters) != 1:
|
|
218
|
+
raise ArgumentError
|
|
219
|
+
await self.subscribe(event, args)
|
|
220
|
+
self._events[event.lower()] = func
|
|
221
|
+
|
|
222
|
+
async def unregister_event(self, event: str, args=None):
|
|
223
|
+
if args is None:
|
|
224
|
+
args = {}
|
|
225
|
+
event = event.lower()
|
|
226
|
+
if event not in self._events:
|
|
227
|
+
raise EventNotFound
|
|
228
|
+
await self.unsubscribe(event, args)
|
|
229
|
+
del self._events[event]
|
|
230
|
+
|
|
231
|
+
# noinspection PyProtectedMember
|
|
232
|
+
def on_event(self, data):
|
|
233
|
+
if self.sock_reader._eof:
|
|
234
|
+
raise PyPresenceException('feed_data after feed_eof')
|
|
235
|
+
if not data:
|
|
236
|
+
return
|
|
237
|
+
self.sock_reader._buffer.extend(data)
|
|
238
|
+
self.sock_reader._wakeup_waiter()
|
|
239
|
+
if (self.sock_reader._transport is not None and
|
|
240
|
+
not self.sock_reader._paused and
|
|
241
|
+
len(self.sock_reader._buffer) > 2 * self.sock_reader._limit):
|
|
242
|
+
try:
|
|
243
|
+
self.sock_reader._transport.pause_reading()
|
|
244
|
+
except NotImplementedError:
|
|
245
|
+
self.sock_reader._transport = None
|
|
246
|
+
else:
|
|
247
|
+
self.sock_reader._paused = True
|
|
248
|
+
|
|
249
|
+
payload = json.loads(data[8:].decode('utf-8'))
|
|
250
|
+
|
|
251
|
+
if payload["evt"] is not None:
|
|
252
|
+
evt = payload["evt"].lower()
|
|
253
|
+
if evt in self._events:
|
|
254
|
+
asyncio.create_task(self._events[evt](payload["data"]))
|
|
255
|
+
elif evt == 'error':
|
|
256
|
+
raise DiscordError(payload["data"]["code"], payload["data"]["message"])
|
|
257
|
+
|
|
258
|
+
async def authorize(self, client_id: str, scopes: List[str]):
|
|
259
|
+
payload = Payload.authorize(client_id, scopes)
|
|
260
|
+
self.send_data(1, payload)
|
|
261
|
+
return await self.read_output()
|
|
262
|
+
|
|
263
|
+
async def authenticate(self, token: str):
|
|
264
|
+
payload = Payload.authenticate(token)
|
|
265
|
+
self.send_data(1, payload)
|
|
266
|
+
return await self.read_output()
|
|
267
|
+
|
|
268
|
+
async def get_guilds(self):
|
|
269
|
+
payload = Payload.get_guilds()
|
|
270
|
+
self.send_data(1, payload)
|
|
271
|
+
return await self.read_output()
|
|
272
|
+
|
|
273
|
+
async def get_guild(self, guild_id: str):
|
|
274
|
+
payload = Payload.get_guild(guild_id)
|
|
275
|
+
self.send_data(1, payload)
|
|
276
|
+
return await self.read_output()
|
|
277
|
+
|
|
278
|
+
async def get_channel(self, channel_id: str):
|
|
279
|
+
payload = Payload.get_channel(channel_id)
|
|
280
|
+
self.send_data(1, payload)
|
|
281
|
+
return await self.read_output()
|
|
282
|
+
|
|
283
|
+
async def get_channels(self, guild_id: str):
|
|
284
|
+
payload = Payload.get_channels(guild_id)
|
|
285
|
+
self.send_data(1, payload)
|
|
286
|
+
return await self.read_output()
|
|
287
|
+
|
|
288
|
+
async def set_user_voice_settings(self, user_id: str, pan_left: float | None = None,
|
|
289
|
+
pan_right: float | None = None, volume: int | None = None,
|
|
290
|
+
mute: bool | None = None):
|
|
291
|
+
payload = Payload.set_user_voice_settings(user_id, pan_left, pan_right, volume, mute)
|
|
292
|
+
self.send_data(1, payload)
|
|
293
|
+
return await self.read_output()
|
|
294
|
+
|
|
295
|
+
async def select_voice_channel(self, channel_id: str):
|
|
296
|
+
payload = Payload.select_voice_channel(channel_id)
|
|
297
|
+
self.send_data(1, payload)
|
|
298
|
+
return await self.read_output()
|
|
299
|
+
|
|
300
|
+
async def get_selected_voice_channel(self):
|
|
301
|
+
payload = Payload.get_selected_voice_channel()
|
|
302
|
+
self.send_data(1, payload)
|
|
303
|
+
return await self.read_output()
|
|
304
|
+
|
|
305
|
+
async def select_text_channel(self, channel_id: str):
|
|
306
|
+
payload = Payload.select_text_channel(channel_id)
|
|
307
|
+
self.send_data(1, payload)
|
|
308
|
+
return await self.read_output()
|
|
309
|
+
|
|
310
|
+
async def set_activity(self, pid: int = os.getpid(),
|
|
311
|
+
activity_type: ActivityType | None = None,
|
|
312
|
+
state: str | None = None, details: str | None = None,
|
|
313
|
+
start: int | None = None, end: int | None = None,
|
|
314
|
+
large_image: str | None = None, large_text: str | None = None,
|
|
315
|
+
small_image: str | None = None, small_text: str | None = None,
|
|
316
|
+
party_id: str | None = None, party_size: list | None = None,
|
|
317
|
+
join: str | None = None, spectate: str | None = None,
|
|
318
|
+
buttons: list | None = None,
|
|
319
|
+
match: str | None = None, instance: bool = True):
|
|
320
|
+
payload = Payload.set_activity(pid, activity_type, state, details, start, end, large_image,
|
|
321
|
+
large_text, small_image, small_text, party_id, party_size,
|
|
322
|
+
join, spectate, match, buttons, instance, activity=True)
|
|
323
|
+
self.send_data(1, payload)
|
|
324
|
+
return await self.read_output()
|
|
325
|
+
|
|
326
|
+
async def clear_activity(self, pid: int = os.getpid()):
|
|
327
|
+
payload = Payload.set_activity(pid, activity=None)
|
|
328
|
+
self.send_data(1, payload)
|
|
329
|
+
return await self.read_output()
|
|
330
|
+
|
|
331
|
+
async def subscribe(self, event: str, args=None):
|
|
332
|
+
if args is None:
|
|
333
|
+
args = {}
|
|
334
|
+
payload = Payload.subscribe(event, args)
|
|
335
|
+
self.send_data(1, payload)
|
|
336
|
+
return await self.read_output()
|
|
337
|
+
|
|
338
|
+
async def unsubscribe(self, event: str, args=None):
|
|
339
|
+
if args is None:
|
|
340
|
+
args = {}
|
|
341
|
+
payload = Payload.unsubscribe(event, args)
|
|
342
|
+
self.send_data(1, payload)
|
|
343
|
+
return await self.read_output()
|
|
344
|
+
|
|
345
|
+
async def get_voice_settings(self):
|
|
346
|
+
payload = Payload.get_voice_settings()
|
|
347
|
+
self.send_data(1, payload)
|
|
348
|
+
return await self.read_output()
|
|
349
|
+
|
|
350
|
+
async def set_voice_settings(self, _input: dict | None = None, output: dict | None = None,
|
|
351
|
+
mode: dict | None = None, automatic_gain_control: bool | None = None,
|
|
352
|
+
echo_cancellation: bool | None = None, noise_suppression: bool | None = None,
|
|
353
|
+
qos: bool | None = None, silence_warning: bool | None = None,
|
|
354
|
+
deaf: bool | None = None, mute: bool | None = None):
|
|
355
|
+
payload = Payload.set_voice_settings(_input, output, mode, automatic_gain_control, echo_cancellation,
|
|
356
|
+
noise_suppression, qos, silence_warning, deaf, mute)
|
|
357
|
+
self.send_data(1, payload)
|
|
358
|
+
return await self.read_output()
|
|
359
|
+
|
|
360
|
+
async def capture_shortcut(self, action: str):
|
|
361
|
+
payload = Payload.capture_shortcut(action)
|
|
362
|
+
self.send_data(1, payload)
|
|
363
|
+
return await self.read_output()
|
|
364
|
+
|
|
365
|
+
async def send_activity_join_invite(self, user_id: str):
|
|
366
|
+
payload = Payload.send_activity_join_invite(user_id)
|
|
367
|
+
self.send_data(1, payload)
|
|
368
|
+
return await self.read_output()
|
|
369
|
+
|
|
370
|
+
async def close_activity_request(self, user_id: str):
|
|
371
|
+
payload = Payload.close_activity_request(user_id)
|
|
372
|
+
self.send_data(1, payload)
|
|
373
|
+
return await self.read_output()
|
|
374
|
+
|
|
375
|
+
def close(self):
|
|
376
|
+
self.send_data(2, {'v': 1, 'client_id': self.client_id})
|
|
377
|
+
self.sock_writer.close()
|
|
378
|
+
self._closed = True
|
|
379
|
+
self.loop.close()
|
|
380
|
+
|
|
381
|
+
async def start(self):
|
|
382
|
+
await self.handshake()
|
|
383
|
+
|
|
384
|
+
async def read(self):
|
|
385
|
+
return await self.read_output()
|
pypresence/exceptions.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class PyPresenceException(Exception):
|
|
5
|
+
def __init__(self, message: str | None = None):
|
|
6
|
+
if message is None:
|
|
7
|
+
message = 'An error has occurred within PyPresence'
|
|
8
|
+
super().__init__(message)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DiscordNotFound(PyPresenceException):
|
|
12
|
+
def __init__(self):
|
|
13
|
+
super().__init__('Could not find Discord installed and running on this machine.')
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class InvalidPipe(PyPresenceException):
|
|
17
|
+
def __init__(self):
|
|
18
|
+
super().__init__('Pipe Not Found - Is Discord Running?')
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class InvalidArgument(PyPresenceException):
|
|
22
|
+
def __init__(self, expected, received, description: str | None = None):
|
|
23
|
+
description = '\n{0}'.format(description) if description else ''
|
|
24
|
+
super().__init__('Bad argument passed. Expected {0} but got {1} instead{2}'.format(expected, received,
|
|
25
|
+
description)
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ServerError(PyPresenceException):
|
|
30
|
+
def __init__(self, message: str):
|
|
31
|
+
super().__init__(message.replace(']', '').replace('[', '').capitalize())
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class DiscordError(PyPresenceException):
|
|
35
|
+
def __init__(self, code: int, message: str, override: bool = False):
|
|
36
|
+
self.code = code
|
|
37
|
+
self.message = message
|
|
38
|
+
super().__init__('Error Code: {0} Message: {1}'.format(code, message) if not override else message)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class InvalidID(DiscordError):
|
|
42
|
+
def __init__(self):
|
|
43
|
+
super().__init__(4000, 'Client ID is Invalid')
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ArgumentError(PyPresenceException):
|
|
47
|
+
def __init__(self):
|
|
48
|
+
super().__init__('Supplied function must have one argument.')
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class EventNotFound(PyPresenceException):
|
|
52
|
+
def __init__(self, event):
|
|
53
|
+
super().__init__('No event with name {0} exists.'.format(event))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class PipeClosed(PyPresenceException):
|
|
57
|
+
def __init__(self):
|
|
58
|
+
super().__init__('The pipe was closed. Catch this exception and re-connect your instance.')
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ResponseTimeout(PyPresenceException):
|
|
62
|
+
def __init__(self):
|
|
63
|
+
super().__init__('No response was received from the pipe in time')
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class ConnectionTimeout(PyPresenceException):
|
|
67
|
+
def __init__(self):
|
|
68
|
+
super().__init__('Unable to create a connection to the pipe in time')
|
pypresence/payloads.py
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
from .utils import remove_none
|
|
8
|
+
from .types import ActivityType
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Payload:
|
|
12
|
+
|
|
13
|
+
def __init__(self, data, clear_none=True):
|
|
14
|
+
if clear_none:
|
|
15
|
+
data = remove_none(data)
|
|
16
|
+
self.data = data
|
|
17
|
+
|
|
18
|
+
def __str__(self):
|
|
19
|
+
return json.dumps(self.data, indent=2)
|
|
20
|
+
|
|
21
|
+
@staticmethod
|
|
22
|
+
def time():
|
|
23
|
+
return time.time()
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def set_activity(cls, pid: int = os.getpid(),
|
|
27
|
+
activity_type: ActivityType | None = None,
|
|
28
|
+
state: str | None = None, details: str | None = None,
|
|
29
|
+
start: int | None = None, end: int | None = None,
|
|
30
|
+
large_image: str | None = None, large_text: str | None = None,
|
|
31
|
+
small_image: str | None = None, small_text: str | None = None,
|
|
32
|
+
party_id: str | None = None, party_size: list | None = None,
|
|
33
|
+
join: str | None = None, spectate: str | None = None,
|
|
34
|
+
match: str | None = None, buttons: list | None = None,
|
|
35
|
+
instance: bool = True, activity: bool | None = True,
|
|
36
|
+
_rn: bool = True):
|
|
37
|
+
|
|
38
|
+
# They should already be an int because we give typehints, but some people are fucking stupid and use
|
|
39
|
+
# IDLE or some other stupid shit.
|
|
40
|
+
if start:
|
|
41
|
+
start = int(start)
|
|
42
|
+
if end:
|
|
43
|
+
end = int(end)
|
|
44
|
+
if activity_type:
|
|
45
|
+
if isinstance(activity_type, ActivityType):
|
|
46
|
+
activity_type = activity_type.value
|
|
47
|
+
else:
|
|
48
|
+
activity_type = int(activity_type)
|
|
49
|
+
|
|
50
|
+
if activity is None:
|
|
51
|
+
act_details = None
|
|
52
|
+
clear = True
|
|
53
|
+
else:
|
|
54
|
+
act_details = {
|
|
55
|
+
"type": activity_type,
|
|
56
|
+
"state": state,
|
|
57
|
+
"details": details,
|
|
58
|
+
"timestamps": {
|
|
59
|
+
"start": start,
|
|
60
|
+
"end": end
|
|
61
|
+
},
|
|
62
|
+
"assets": {
|
|
63
|
+
"large_image": large_image,
|
|
64
|
+
"large_text": large_text,
|
|
65
|
+
"small_image": small_image,
|
|
66
|
+
"small_text": small_text
|
|
67
|
+
},
|
|
68
|
+
"party": {
|
|
69
|
+
"id": party_id,
|
|
70
|
+
"size": party_size
|
|
71
|
+
},
|
|
72
|
+
"secrets": {
|
|
73
|
+
"join": join,
|
|
74
|
+
"spectate": spectate,
|
|
75
|
+
"match": match
|
|
76
|
+
},
|
|
77
|
+
"buttons": buttons,
|
|
78
|
+
"instance": instance
|
|
79
|
+
}
|
|
80
|
+
clear = False
|
|
81
|
+
|
|
82
|
+
payload = {
|
|
83
|
+
"cmd": "SET_ACTIVITY",
|
|
84
|
+
"args": {
|
|
85
|
+
"pid": pid,
|
|
86
|
+
"activity": act_details
|
|
87
|
+
},
|
|
88
|
+
"nonce": '{:.20f}'.format(cls.time())
|
|
89
|
+
}
|
|
90
|
+
if _rn:
|
|
91
|
+
clear = _rn
|
|
92
|
+
return cls(payload, clear)
|
|
93
|
+
|
|
94
|
+
@classmethod
|
|
95
|
+
def authorize(cls, client_id: str, scopes: list[str]):
|
|
96
|
+
payload = {
|
|
97
|
+
"cmd": "AUTHORIZE",
|
|
98
|
+
"args": {
|
|
99
|
+
"client_id": str(client_id),
|
|
100
|
+
"scopes": scopes
|
|
101
|
+
},
|
|
102
|
+
"nonce": '{:.20f}'.format(cls.time())
|
|
103
|
+
}
|
|
104
|
+
return cls(payload)
|
|
105
|
+
|
|
106
|
+
@classmethod
|
|
107
|
+
def authenticate(cls, token: str):
|
|
108
|
+
payload = {
|
|
109
|
+
"cmd": "AUTHENTICATE",
|
|
110
|
+
"args": {
|
|
111
|
+
"access_token": token
|
|
112
|
+
},
|
|
113
|
+
"nonce": '{:.20f}'.format(cls.time())
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return cls(payload)
|
|
117
|
+
|
|
118
|
+
@classmethod
|
|
119
|
+
def get_guilds(cls):
|
|
120
|
+
payload = {
|
|
121
|
+
"cmd": "GET_GUILDS",
|
|
122
|
+
"args": {
|
|
123
|
+
},
|
|
124
|
+
"nonce": '{:.20f}'.format(cls.time())
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return cls(payload)
|
|
128
|
+
|
|
129
|
+
@classmethod
|
|
130
|
+
def get_guild(cls, guild_id: str):
|
|
131
|
+
payload = {
|
|
132
|
+
"cmd": "GET_GUILD",
|
|
133
|
+
"args": {
|
|
134
|
+
"guild_id": str(guild_id),
|
|
135
|
+
},
|
|
136
|
+
"nonce": '{:.20f}'.format(cls.time())
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return cls(payload)
|
|
140
|
+
|
|
141
|
+
@classmethod
|
|
142
|
+
def get_channels(cls, guild_id: str):
|
|
143
|
+
payload = {
|
|
144
|
+
"cmd": "GET_CHANNELS",
|
|
145
|
+
"args": {
|
|
146
|
+
"guild_id": str(guild_id),
|
|
147
|
+
},
|
|
148
|
+
"nonce": '{:.20f}'.format(cls.time())
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return cls(payload)
|
|
152
|
+
|
|
153
|
+
@classmethod
|
|
154
|
+
def get_channel(cls, channel_id: str):
|
|
155
|
+
payload = {
|
|
156
|
+
"cmd": "GET_CHANNEL",
|
|
157
|
+
"args": {
|
|
158
|
+
"channel_id": str(channel_id),
|
|
159
|
+
},
|
|
160
|
+
"nonce": '{:.20f}'.format(cls.time())
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return cls(payload)
|
|
164
|
+
|
|
165
|
+
@classmethod
|
|
166
|
+
def set_user_voice_settings(cls, user_id: str, pan_left: float | None = None,
|
|
167
|
+
pan_right: float | None = None, volume: int | None = None,
|
|
168
|
+
mute: bool | None = None):
|
|
169
|
+
payload = {
|
|
170
|
+
"cmd": "SET_USER_VOICE_SETTINGS",
|
|
171
|
+
"args": {
|
|
172
|
+
"user_id": str(user_id),
|
|
173
|
+
"pan": {
|
|
174
|
+
"left": pan_left,
|
|
175
|
+
"right": pan_right
|
|
176
|
+
},
|
|
177
|
+
"volume": volume,
|
|
178
|
+
"mute": mute
|
|
179
|
+
},
|
|
180
|
+
"nonce": '{:.20f}'.format(cls.time())
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return cls(payload, True)
|
|
184
|
+
|
|
185
|
+
@classmethod
|
|
186
|
+
def select_voice_channel(cls, channel_id: str):
|
|
187
|
+
payload = {
|
|
188
|
+
"cmd": "SELECT_VOICE_CHANNEL",
|
|
189
|
+
"args": {
|
|
190
|
+
"channel_id": str(channel_id),
|
|
191
|
+
},
|
|
192
|
+
"nonce": '{:.20f}'.format(cls.time())
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return cls(payload)
|
|
196
|
+
|
|
197
|
+
@classmethod
|
|
198
|
+
def get_selected_voice_channel(cls):
|
|
199
|
+
payload = {
|
|
200
|
+
"cmd": "GET_SELECTED_VOICE_CHANNEL",
|
|
201
|
+
"args": {
|
|
202
|
+
},
|
|
203
|
+
"nonce": '{:.20f}'.format(cls.time())
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return cls(payload)
|
|
207
|
+
|
|
208
|
+
@classmethod
|
|
209
|
+
def select_text_channel(cls, channel_id: str):
|
|
210
|
+
payload = {
|
|
211
|
+
"cmd": "SELECT_TEXT_CHANNEL",
|
|
212
|
+
"args": {
|
|
213
|
+
"channel_id": str(channel_id),
|
|
214
|
+
},
|
|
215
|
+
"nonce": '{:.20f}'.format(cls.time())
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return cls(payload)
|
|
219
|
+
|
|
220
|
+
@classmethod
|
|
221
|
+
def subscribe(cls, event: str, args=None):
|
|
222
|
+
if args is None:
|
|
223
|
+
args = {}
|
|
224
|
+
payload = {
|
|
225
|
+
"cmd": "SUBSCRIBE",
|
|
226
|
+
"args": args,
|
|
227
|
+
"evt": event.upper(),
|
|
228
|
+
"nonce": '{:.20f}'.format(cls.time())
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return cls(payload)
|
|
232
|
+
|
|
233
|
+
@classmethod
|
|
234
|
+
def unsubscribe(cls, event: str, args=None):
|
|
235
|
+
if args is None:
|
|
236
|
+
args = {}
|
|
237
|
+
payload = {
|
|
238
|
+
"cmd": "UNSUBSCRIBE",
|
|
239
|
+
"args": args,
|
|
240
|
+
"evt": event.upper(),
|
|
241
|
+
"nonce": '{:.20f}'.format(cls.time())
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return cls(payload)
|
|
245
|
+
|
|
246
|
+
@classmethod
|
|
247
|
+
def get_voice_settings(cls):
|
|
248
|
+
payload = {
|
|
249
|
+
"cmd": "GET_VOICE_SETTINGS",
|
|
250
|
+
"args": {
|
|
251
|
+
},
|
|
252
|
+
"nonce": '{:.20f}'.format(cls.time())
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return cls(payload)
|
|
256
|
+
|
|
257
|
+
@classmethod
|
|
258
|
+
def set_voice_settings(cls, _input: dict | None = None, output: dict | None = None,
|
|
259
|
+
mode: dict | None = None, automatic_gain_control: bool | None = None,
|
|
260
|
+
echo_cancellation: bool | None = None, noise_suppression: bool | None = None,
|
|
261
|
+
qos: bool | None = None, silence_warning: bool | None = None,
|
|
262
|
+
deaf: bool | None = None, mute: bool | None = None):
|
|
263
|
+
payload = {
|
|
264
|
+
"cmd": "SET_VOICE_SETTINGS",
|
|
265
|
+
"args": {
|
|
266
|
+
"input": _input,
|
|
267
|
+
"output": output,
|
|
268
|
+
"mode": mode,
|
|
269
|
+
"automatic_gain_control": automatic_gain_control,
|
|
270
|
+
"echo_cancellation": echo_cancellation,
|
|
271
|
+
"noise_suppression": noise_suppression,
|
|
272
|
+
"qos": qos,
|
|
273
|
+
"silence_warning": silence_warning,
|
|
274
|
+
"deaf": deaf,
|
|
275
|
+
"mute": mute
|
|
276
|
+
},
|
|
277
|
+
"nonce": '{:.20f}'.format(cls.time())
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return cls(payload, True)
|
|
281
|
+
|
|
282
|
+
@classmethod
|
|
283
|
+
def capture_shortcut(cls, action: str):
|
|
284
|
+
payload = {
|
|
285
|
+
"cmd": "CAPTURE_SHORTCUT",
|
|
286
|
+
"args": {
|
|
287
|
+
"action": action.upper()
|
|
288
|
+
},
|
|
289
|
+
"nonce": '{:.20f}'.format(cls.time())
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return cls(payload)
|
|
293
|
+
|
|
294
|
+
@classmethod
|
|
295
|
+
def send_activity_join_invite(cls, user_id: str):
|
|
296
|
+
payload = {
|
|
297
|
+
"cmd": "SEND_ACTIVITY_JOIN_INVITE",
|
|
298
|
+
"args": {
|
|
299
|
+
"user_id": str(user_id)
|
|
300
|
+
},
|
|
301
|
+
"nonce": '{:.20f}'.format(cls.time())
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return cls(payload)
|
|
305
|
+
|
|
306
|
+
@classmethod
|
|
307
|
+
def close_activity_request(cls, user_id: str):
|
|
308
|
+
payload = {
|
|
309
|
+
"cmd": "CLOSE_ACTIVITY_REQUEST",
|
|
310
|
+
"args": {
|
|
311
|
+
"user_id": str(user_id)
|
|
312
|
+
},
|
|
313
|
+
"nonce": '{:.20f}'.format(cls.time())
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return cls(payload)
|
pypresence/presence.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from .baseclient import BaseClient
|
|
6
|
+
from .payloads import Payload
|
|
7
|
+
from .utils import get_event_loop
|
|
8
|
+
from .types import ActivityType
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Presence(BaseClient):
|
|
12
|
+
|
|
13
|
+
def __init__(self, *args, **kwargs):
|
|
14
|
+
super().__init__(*args, **kwargs)
|
|
15
|
+
|
|
16
|
+
def update(self, pid: int = os.getpid(),
|
|
17
|
+
activity_type: ActivityType | None = None,
|
|
18
|
+
state: str | None = None, details: str | None = None,
|
|
19
|
+
start: int | None = None, end: int | None = None,
|
|
20
|
+
large_image: str | None = None, large_text: str | None = None,
|
|
21
|
+
small_image: str | None = None, small_text: str | None = None,
|
|
22
|
+
party_id: str | None = None, party_size: list | None = None,
|
|
23
|
+
join: str | None = None, spectate: str | None = None,
|
|
24
|
+
match: str | None = None, buttons: list | None = None,
|
|
25
|
+
instance: bool = True, payload_override: dict | None = None):
|
|
26
|
+
|
|
27
|
+
if payload_override is None:
|
|
28
|
+
payload = Payload.set_activity(pid=pid, activity_type=activity_type, state=state, details=details,
|
|
29
|
+
start=start, end=end, large_image=large_image, large_text=large_text,
|
|
30
|
+
small_image=small_image, small_text=small_text, party_id=party_id,
|
|
31
|
+
party_size=party_size, join=join, spectate=spectate,
|
|
32
|
+
match=match, buttons=buttons, instance=instance, activity=True)
|
|
33
|
+
else:
|
|
34
|
+
payload = payload_override
|
|
35
|
+
self.send_data(1, payload)
|
|
36
|
+
return self.loop.run_until_complete(self.read_output())
|
|
37
|
+
|
|
38
|
+
def clear(self, pid: int = os.getpid()):
|
|
39
|
+
payload = Payload.set_activity(pid, activity=None)
|
|
40
|
+
self.send_data(1, payload)
|
|
41
|
+
return self.loop.run_until_complete(self.read_output())
|
|
42
|
+
|
|
43
|
+
def connect(self):
|
|
44
|
+
self.update_event_loop(get_event_loop())
|
|
45
|
+
self.loop.run_until_complete(self.handshake())
|
|
46
|
+
|
|
47
|
+
def close(self):
|
|
48
|
+
self.send_data(2, {'v': 1, 'client_id': self.client_id})
|
|
49
|
+
self.loop.close()
|
|
50
|
+
if sys.platform == 'win32':
|
|
51
|
+
self.sock_writer._call_connection_lost(None)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class AioPresence(BaseClient):
|
|
55
|
+
|
|
56
|
+
def __init__(self, *args, **kwargs):
|
|
57
|
+
super().__init__(*args, **kwargs, isasync=True)
|
|
58
|
+
|
|
59
|
+
async def update(self, pid: int = os.getpid(),
|
|
60
|
+
activity_type: ActivityType | None = None,
|
|
61
|
+
state: str | None = None, details: str | None = None,
|
|
62
|
+
start: int | None = None, end: int | None = None,
|
|
63
|
+
large_image: str | None = None, large_text: str | None = None,
|
|
64
|
+
small_image: str | None = None, small_text: str | None = None,
|
|
65
|
+
party_id: str | None = None, party_size: list | None = None,
|
|
66
|
+
join: str | None = None, spectate: str | None = None,
|
|
67
|
+
match: str | None = None, buttons: list | None = None,
|
|
68
|
+
instance: bool = True):
|
|
69
|
+
payload = Payload.set_activity(pid=pid, activity_type=activity_type, state=state, details=details,
|
|
70
|
+
start=start, end=end, large_image=large_image, large_text=large_text,
|
|
71
|
+
small_image=small_image, small_text=small_text, party_id=party_id,
|
|
72
|
+
party_size=party_size, join=join, spectate=spectate,
|
|
73
|
+
match=match, buttons=buttons, instance=instance, activity=True)
|
|
74
|
+
self.send_data(1, payload)
|
|
75
|
+
return await self.read_output()
|
|
76
|
+
|
|
77
|
+
async def clear(self, pid: int = os.getpid()):
|
|
78
|
+
payload = Payload.set_activity(pid, activity=None)
|
|
79
|
+
self.send_data(1, payload)
|
|
80
|
+
return await self.read_output()
|
|
81
|
+
|
|
82
|
+
async def connect(self):
|
|
83
|
+
self.update_event_loop(get_event_loop())
|
|
84
|
+
await self.handshake()
|
|
85
|
+
|
|
86
|
+
def close(self):
|
|
87
|
+
self.send_data(2, {'v': 1, 'client_id': self.client_id})
|
|
88
|
+
self.loop.close()
|
|
89
|
+
if sys.platform == 'win32':
|
|
90
|
+
self.sock_writer._call_connection_lost(None)
|
pypresence/py.typed
ADDED
|
File without changes
|
pypresence/types.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import enum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ActivityType(enum.Enum):
|
|
5
|
+
"""
|
|
6
|
+
https://discord.com/developers/docs/game-sdk/activities#data-models-activitytype-enum
|
|
7
|
+
"type" must be one of 0, 2, 3, 5 -- Discord only implemented these four
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
PLAYING = 0
|
|
11
|
+
# STREAMING = 1
|
|
12
|
+
LISTENING = 2
|
|
13
|
+
WATCHING = 3
|
|
14
|
+
# CUSTOM = 4
|
|
15
|
+
COMPETING = 5
|
pypresence/utils.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Util functions that are needed but messy."""
|
|
2
|
+
import asyncio
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import tempfile
|
|
6
|
+
import socket
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def remove_none(d: dict):
|
|
10
|
+
for item in d.copy():
|
|
11
|
+
if isinstance(d[item], dict):
|
|
12
|
+
if len(d[item]):
|
|
13
|
+
d[item] = remove_none(d[item])
|
|
14
|
+
if not len(d[item]):
|
|
15
|
+
del d[item]
|
|
16
|
+
elif d[item] is None:
|
|
17
|
+
del d[item]
|
|
18
|
+
return d
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_ipc_path(path) -> bool:
|
|
22
|
+
'''Tests an IPC pipe to ensure that it actually works'''
|
|
23
|
+
if sys.platform == 'win32':
|
|
24
|
+
with open(path):
|
|
25
|
+
return True
|
|
26
|
+
else:
|
|
27
|
+
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as client:
|
|
28
|
+
client.connect(path)
|
|
29
|
+
return True
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# Returns on first IPC pipe matching Discord's
|
|
33
|
+
def get_ipc_path(pipe=None):
|
|
34
|
+
ipc = 'discord-ipc-'
|
|
35
|
+
if pipe is not None:
|
|
36
|
+
ipc = f"{ipc}{pipe}"
|
|
37
|
+
|
|
38
|
+
if sys.platform in ('linux', 'darwin'):
|
|
39
|
+
tempdir = os.environ.get('XDG_RUNTIME_DIR') or (f"/run/user/{os.getuid()}" if os.path.exists(f"/run/user/{os.getuid()}") else tempfile.gettempdir())
|
|
40
|
+
paths = ['.', 'snap.discord', 'app/com.discordapp.Discord', 'app/com.discordapp.DiscordCanary']
|
|
41
|
+
elif sys.platform == 'win32':
|
|
42
|
+
tempdir = r'\\?\pipe'
|
|
43
|
+
paths = ['.']
|
|
44
|
+
else:
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
for path in paths:
|
|
48
|
+
full_path = os.path.abspath(os.path.join(tempdir, path))
|
|
49
|
+
if sys.platform == 'win32' or os.path.isdir(full_path):
|
|
50
|
+
for entry in os.scandir(full_path):
|
|
51
|
+
if entry.name.startswith(ipc) and os.path.exists(entry) and test_ipc_path(entry.path):
|
|
52
|
+
return entry.path
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def get_event_loop(force_fresh: bool = False):
|
|
56
|
+
if force_fresh:
|
|
57
|
+
return asyncio.new_event_loop()
|
|
58
|
+
try:
|
|
59
|
+
running = asyncio.get_running_loop()
|
|
60
|
+
except RuntimeError:
|
|
61
|
+
return asyncio.new_event_loop()
|
|
62
|
+
if running.is_closed():
|
|
63
|
+
return asyncio.new_event_loop()
|
|
64
|
+
return running
|