yapper-bot 0.1.0__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.
- yapper_bot-0.1.0.dist-info/METADATA +280 -0
- yapper_bot-0.1.0.dist-info/RECORD +30 -0
- yapper_bot-0.1.0.dist-info/WHEEL +4 -0
- yapper_bot-0.1.0.dist-info/licenses/LICENSE +21 -0
- yapperpy/__init__.py +153 -0
- yapperpy/__main__.py +58 -0
- yapperpy/_core.py +58 -0
- yapperpy/_http/__init__.py +27 -0
- yapperpy/_http/aio.py +175 -0
- yapperpy/_http/base.py +97 -0
- yapperpy/_http/sync.py +83 -0
- yapperpy/_version.py +1 -0
- yapperpy/aio_bot.py +170 -0
- yapperpy/aio_client.py +423 -0
- yapperpy/bot.py +163 -0
- yapperpy/client.py +421 -0
- yapperpy/cogs.py +285 -0
- yapperpy/components.py +129 -0
- yapperpy/context.py +95 -0
- yapperpy/embeds.py +86 -0
- yapperpy/endpoints.py +730 -0
- yapperpy/enums.py +49 -0
- yapperpy/errors.py +79 -0
- yapperpy/event_models.py +257 -0
- yapperpy/events.py +80 -0
- yapperpy/framework.py +165 -0
- yapperpy/models.py +795 -0
- yapperpy/py.typed +0 -0
- yapperpy/registry.py +160 -0
- yapperpy/retry.py +96 -0
yapperpy/_http/aio.py
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import contextlib
|
|
5
|
+
import ssl
|
|
6
|
+
from urllib.parse import urlsplit
|
|
7
|
+
|
|
8
|
+
from .base import Headers, RawRequest, RawResponse, TransportError, TransportTimeout
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class _AsyncConnection:
|
|
12
|
+
def __init__(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
|
|
13
|
+
self.reader = reader
|
|
14
|
+
self.writer = writer
|
|
15
|
+
|
|
16
|
+
def close(self) -> None:
|
|
17
|
+
with contextlib.suppress(Exception):
|
|
18
|
+
self.writer.close()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class HTTPAsyncTransport:
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
*,
|
|
25
|
+
timeout: float = 40.0,
|
|
26
|
+
ssl_context: ssl.SSLContext | None = None,
|
|
27
|
+
max_idle: int = 4,
|
|
28
|
+
) -> None:
|
|
29
|
+
self.timeout = timeout
|
|
30
|
+
self.ssl_context = ssl_context or ssl.create_default_context()
|
|
31
|
+
self.max_idle = max(1, max_idle)
|
|
32
|
+
self._pool: dict[tuple[str, int], list[_AsyncConnection]] = {}
|
|
33
|
+
self._lock = asyncio.Lock()
|
|
34
|
+
|
|
35
|
+
async def _checkout(self, host: str, port: int, secure: bool) -> _AsyncConnection:
|
|
36
|
+
async with self._lock:
|
|
37
|
+
idle = self._pool.get((host, port))
|
|
38
|
+
if idle:
|
|
39
|
+
return idle.pop()
|
|
40
|
+
if secure:
|
|
41
|
+
reader, writer = await asyncio.open_connection(
|
|
42
|
+
host, port, ssl=self.ssl_context, server_hostname=host
|
|
43
|
+
)
|
|
44
|
+
else:
|
|
45
|
+
reader, writer = await asyncio.open_connection(host, port)
|
|
46
|
+
return _AsyncConnection(reader, writer)
|
|
47
|
+
|
|
48
|
+
async def _checkin(self, host: str, port: int, connection: _AsyncConnection) -> None:
|
|
49
|
+
async with self._lock:
|
|
50
|
+
idle = self._pool.setdefault((host, port), [])
|
|
51
|
+
if len(idle) < self.max_idle:
|
|
52
|
+
idle.append(connection)
|
|
53
|
+
return
|
|
54
|
+
connection.close()
|
|
55
|
+
|
|
56
|
+
async def send(self, request: RawRequest) -> RawResponse:
|
|
57
|
+
try:
|
|
58
|
+
return await asyncio.wait_for(self._exchange(request), request.timeout or self.timeout)
|
|
59
|
+
except asyncio.TimeoutError as exc:
|
|
60
|
+
raise TransportTimeout("request timed out") from exc
|
|
61
|
+
except TransportError:
|
|
62
|
+
raise
|
|
63
|
+
except OSError as exc:
|
|
64
|
+
raise TransportError(str(exc) or type(exc).__name__) from exc
|
|
65
|
+
|
|
66
|
+
async def _exchange(self, request: RawRequest) -> RawResponse:
|
|
67
|
+
parts = urlsplit(request.url)
|
|
68
|
+
host = parts.hostname or ""
|
|
69
|
+
secure = parts.scheme != "http"
|
|
70
|
+
port = parts.port or (443 if secure else 80)
|
|
71
|
+
path = parts.path or "/"
|
|
72
|
+
if parts.query:
|
|
73
|
+
path = f"{path}?{parts.query}"
|
|
74
|
+
headers = dict(request.headers)
|
|
75
|
+
headers.setdefault("Accept", "application/json")
|
|
76
|
+
headers.setdefault("Connection", "keep-alive")
|
|
77
|
+
if request.body is not None:
|
|
78
|
+
headers.setdefault("Content-Length", str(len(request.body)))
|
|
79
|
+
connection = await self._checkout(host, port, secure)
|
|
80
|
+
try:
|
|
81
|
+
head = f"{request.method} {path} HTTP/1.1\r\n".encode("latin-1")
|
|
82
|
+
head += b"".join(
|
|
83
|
+
f"{name}: {value}\r\n".encode("latin-1") for name, value in headers.items()
|
|
84
|
+
)
|
|
85
|
+
head += b"\r\n"
|
|
86
|
+
connection.writer.write(head)
|
|
87
|
+
if request.body:
|
|
88
|
+
connection.writer.write(request.body)
|
|
89
|
+
await connection.writer.drain()
|
|
90
|
+
status, reason, response_headers, body = await self._read_response(
|
|
91
|
+
connection.reader, request.method
|
|
92
|
+
)
|
|
93
|
+
except (TransportError, OSError, asyncio.IncompleteReadError):
|
|
94
|
+
connection.close()
|
|
95
|
+
raise
|
|
96
|
+
if response_headers.get("connection", "").lower() == "close":
|
|
97
|
+
connection.close()
|
|
98
|
+
else:
|
|
99
|
+
await self._checkin(host, port, connection)
|
|
100
|
+
return RawResponse(status=status, reason=reason, headers=response_headers, body=body)
|
|
101
|
+
|
|
102
|
+
async def _read_response(
|
|
103
|
+
self, reader: asyncio.StreamReader, method: str = "GET"
|
|
104
|
+
) -> tuple[int, str, Headers, bytes]:
|
|
105
|
+
status_line = await reader.readline()
|
|
106
|
+
if not status_line:
|
|
107
|
+
raise TransportError("the connection closed before a response arrived")
|
|
108
|
+
parts = status_line.decode("latin-1").rstrip("\r\n").split(" ", 2)
|
|
109
|
+
if len(parts) < 2:
|
|
110
|
+
raise TransportError("the server sent a malformed status line")
|
|
111
|
+
try:
|
|
112
|
+
status = int(parts[1])
|
|
113
|
+
except ValueError as exc:
|
|
114
|
+
raise TransportError("the server sent a malformed status line") from exc
|
|
115
|
+
reason = parts[2] if len(parts) > 2 else ""
|
|
116
|
+
headers: Headers = Headers()
|
|
117
|
+
while True:
|
|
118
|
+
line = await reader.readline()
|
|
119
|
+
if not line:
|
|
120
|
+
raise TransportError("the connection closed while reading headers")
|
|
121
|
+
if line in (b"\r\n", b"\n"):
|
|
122
|
+
break
|
|
123
|
+
name, _, value = line.decode("latin-1").partition(":")
|
|
124
|
+
headers[name.strip()] = value.strip()
|
|
125
|
+
body = await self._read_body(reader, headers, status, method)
|
|
126
|
+
return status, reason, headers, body
|
|
127
|
+
|
|
128
|
+
async def _read_body(
|
|
129
|
+
self,
|
|
130
|
+
reader: asyncio.StreamReader,
|
|
131
|
+
headers: Headers,
|
|
132
|
+
status: int = 200,
|
|
133
|
+
method: str = "GET",
|
|
134
|
+
) -> bytes:
|
|
135
|
+
if status in (204, 304) or method.upper() == "HEAD":
|
|
136
|
+
return b""
|
|
137
|
+
if headers.get("transfer-encoding", "").lower() == "chunked":
|
|
138
|
+
return await self._read_chunked(reader)
|
|
139
|
+
raw_length = headers.get("content-length")
|
|
140
|
+
if raw_length is None:
|
|
141
|
+
return await reader.read()
|
|
142
|
+
try:
|
|
143
|
+
length = int(raw_length)
|
|
144
|
+
except ValueError as exc:
|
|
145
|
+
raise TransportError("the server sent a malformed content length") from exc
|
|
146
|
+
if length <= 0:
|
|
147
|
+
return b""
|
|
148
|
+
return await reader.readexactly(length)
|
|
149
|
+
|
|
150
|
+
async def _read_chunked(self, reader: asyncio.StreamReader) -> bytes:
|
|
151
|
+
chunks: list[bytes] = []
|
|
152
|
+
while True:
|
|
153
|
+
size_line = await reader.readline()
|
|
154
|
+
if not size_line:
|
|
155
|
+
raise TransportError("the connection closed while reading the body")
|
|
156
|
+
try:
|
|
157
|
+
size = int(size_line.split(b";")[0].strip(), 16)
|
|
158
|
+
except ValueError as exc:
|
|
159
|
+
raise TransportError("the server sent a malformed chunk size") from exc
|
|
160
|
+
if size == 0:
|
|
161
|
+
while True:
|
|
162
|
+
trailer = await reader.readline()
|
|
163
|
+
if trailer in (b"\r\n", b"\n", b""):
|
|
164
|
+
break
|
|
165
|
+
break
|
|
166
|
+
chunks.append(await reader.readexactly(size))
|
|
167
|
+
await reader.readexactly(2)
|
|
168
|
+
return b"".join(chunks)
|
|
169
|
+
|
|
170
|
+
async def close(self) -> None:
|
|
171
|
+
async with self._lock:
|
|
172
|
+
for idle in self._pool.values():
|
|
173
|
+
for connection in idle:
|
|
174
|
+
connection.close()
|
|
175
|
+
self._pool.clear()
|
yapperpy/_http/base.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from collections.abc import Iterable, Mapping
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any, Protocol
|
|
7
|
+
from urllib.parse import parse_qsl, quote, urlencode, urlsplit
|
|
8
|
+
|
|
9
|
+
from .._version import __version__
|
|
10
|
+
|
|
11
|
+
DEFAULT_BASE_URL = "https://www.yapper.website/api/bot/v1"
|
|
12
|
+
DEFAULT_TIMEOUT = 40.0
|
|
13
|
+
USER_AGENT = f"yapperpy/{__version__} (+https://www.yapper.website/developers)"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def segment(value: object) -> str:
|
|
17
|
+
return quote(str(value), safe="")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def build_url(base: str, path: str, query: Mapping[str, Any] | None = None) -> str:
|
|
21
|
+
url = base.rstrip("/") + path
|
|
22
|
+
if query:
|
|
23
|
+
pairs = [(k, str(v)) for k, v in query.items() if v is not None]
|
|
24
|
+
if pairs:
|
|
25
|
+
url += "?" + urlencode(pairs)
|
|
26
|
+
return url
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class TransportError(Exception):
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class TransportTimeout(TransportError):
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Headers(dict[str, str]):
|
|
38
|
+
def __init__(self, data: Mapping[str, str] | Iterable[tuple[str, str]] | None = None) -> None:
|
|
39
|
+
super().__init__()
|
|
40
|
+
if data is None:
|
|
41
|
+
return
|
|
42
|
+
pairs = data.items() if isinstance(data, Mapping) else data
|
|
43
|
+
for name, value in pairs:
|
|
44
|
+
self[name] = value
|
|
45
|
+
|
|
46
|
+
def __setitem__(self, name: str, value: str) -> None:
|
|
47
|
+
super().__setitem__(name.lower(), value)
|
|
48
|
+
|
|
49
|
+
def __getitem__(self, name: str) -> str:
|
|
50
|
+
return super().__getitem__(name.lower())
|
|
51
|
+
|
|
52
|
+
def __contains__(self, name: object) -> bool:
|
|
53
|
+
if isinstance(name, str):
|
|
54
|
+
return super().__contains__(name.lower())
|
|
55
|
+
return super().__contains__(name)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(slots=True)
|
|
59
|
+
class RawRequest:
|
|
60
|
+
method: str
|
|
61
|
+
url: str
|
|
62
|
+
headers: dict[str, str] = field(default_factory=dict)
|
|
63
|
+
body: bytes | None = None
|
|
64
|
+
timeout: float = DEFAULT_TIMEOUT
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def path(self) -> str:
|
|
68
|
+
return urlsplit(self.url).path
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def query(self) -> dict[str, str]:
|
|
72
|
+
return dict(parse_qsl(urlsplit(self.url).query))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass(slots=True)
|
|
76
|
+
class RawResponse:
|
|
77
|
+
status: int
|
|
78
|
+
reason: str = ""
|
|
79
|
+
headers: Headers = field(default_factory=Headers)
|
|
80
|
+
body: bytes = b""
|
|
81
|
+
|
|
82
|
+
def json(self) -> Any:
|
|
83
|
+
if not self.body:
|
|
84
|
+
return None
|
|
85
|
+
return json.loads(self.body.decode("utf-8"))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class SyncTransport(Protocol):
|
|
89
|
+
def send(self, request: RawRequest) -> RawResponse: ...
|
|
90
|
+
|
|
91
|
+
def close(self) -> None: ...
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class AsyncTransport(Protocol):
|
|
95
|
+
async def send(self, request: RawRequest) -> RawResponse: ...
|
|
96
|
+
|
|
97
|
+
async def close(self) -> None: ...
|
yapperpy/_http/sync.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import http.client
|
|
4
|
+
import ssl
|
|
5
|
+
import threading
|
|
6
|
+
from urllib.parse import urlsplit
|
|
7
|
+
|
|
8
|
+
from .base import Headers, RawRequest, RawResponse, TransportError, TransportTimeout
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class HTTPSyncTransport:
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
*,
|
|
15
|
+
timeout: float = 40.0,
|
|
16
|
+
ssl_context: ssl.SSLContext | None = None,
|
|
17
|
+
max_idle: int = 4,
|
|
18
|
+
) -> None:
|
|
19
|
+
self.timeout = timeout
|
|
20
|
+
self.ssl_context = ssl_context or ssl.create_default_context()
|
|
21
|
+
self.max_idle = max(1, max_idle)
|
|
22
|
+
self._pool: dict[tuple[str, int], list[http.client.HTTPConnection]] = {}
|
|
23
|
+
self._lock = threading.Lock()
|
|
24
|
+
|
|
25
|
+
def _checkout(self, host: str, port: int, secure: bool) -> http.client.HTTPConnection:
|
|
26
|
+
with self._lock:
|
|
27
|
+
idle = self._pool.get((host, port))
|
|
28
|
+
if idle:
|
|
29
|
+
return idle.pop()
|
|
30
|
+
if secure:
|
|
31
|
+
return http.client.HTTPSConnection(
|
|
32
|
+
host, port, timeout=self.timeout, context=self.ssl_context
|
|
33
|
+
)
|
|
34
|
+
return http.client.HTTPConnection(host, port, timeout=self.timeout)
|
|
35
|
+
|
|
36
|
+
def _checkin(self, host: str, port: int, connection: http.client.HTTPConnection) -> None:
|
|
37
|
+
with self._lock:
|
|
38
|
+
idle = self._pool.setdefault((host, port), [])
|
|
39
|
+
if len(idle) < self.max_idle:
|
|
40
|
+
idle.append(connection)
|
|
41
|
+
return
|
|
42
|
+
connection.close()
|
|
43
|
+
|
|
44
|
+
def send(self, request: RawRequest) -> RawResponse:
|
|
45
|
+
parts = urlsplit(request.url)
|
|
46
|
+
host = parts.hostname or ""
|
|
47
|
+
secure = parts.scheme != "http"
|
|
48
|
+
port = parts.port or (443 if secure else 80)
|
|
49
|
+
path = parts.path or "/"
|
|
50
|
+
if parts.query:
|
|
51
|
+
path = f"{path}?{parts.query}"
|
|
52
|
+
headers = dict(request.headers)
|
|
53
|
+
headers.setdefault("Accept", "application/json")
|
|
54
|
+
headers.setdefault("Connection", "keep-alive")
|
|
55
|
+
connection = self._checkout(host, port, secure)
|
|
56
|
+
try:
|
|
57
|
+
connection.request(request.method, path, body=request.body, headers=headers)
|
|
58
|
+
response = connection.getresponse()
|
|
59
|
+
body = response.read()
|
|
60
|
+
except TimeoutError as exc:
|
|
61
|
+
connection.close()
|
|
62
|
+
raise TransportTimeout(str(exc) or "request timed out") from exc
|
|
63
|
+
except (http.client.HTTPException, OSError) as exc:
|
|
64
|
+
connection.close()
|
|
65
|
+
raise TransportError(str(exc) or type(exc).__name__) from exc
|
|
66
|
+
result = RawResponse(
|
|
67
|
+
status=response.status,
|
|
68
|
+
reason=response.reason,
|
|
69
|
+
headers=Headers(response.getheaders()),
|
|
70
|
+
body=body,
|
|
71
|
+
)
|
|
72
|
+
if response.will_close:
|
|
73
|
+
connection.close()
|
|
74
|
+
else:
|
|
75
|
+
self._checkin(host, port, connection)
|
|
76
|
+
return result
|
|
77
|
+
|
|
78
|
+
def close(self) -> None:
|
|
79
|
+
with self._lock:
|
|
80
|
+
for idle in self._pool.values():
|
|
81
|
+
for connection in idle:
|
|
82
|
+
connection.close()
|
|
83
|
+
self._pool.clear()
|
yapperpy/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
yapperpy/aio_bot.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import inspect
|
|
5
|
+
import traceback
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from .aio_client import AsyncYapper
|
|
10
|
+
from .context import MessageContext
|
|
11
|
+
from .event_models import InteractionPayload, MessagePayload
|
|
12
|
+
from .framework import BotBase
|
|
13
|
+
from .models import Command, Event
|
|
14
|
+
from .registry import Command as CommandSpec
|
|
15
|
+
|
|
16
|
+
WAIT_SECONDS = 25
|
|
17
|
+
MAX_BACKOFF = 30.0
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AsyncBot(BotBase):
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
token: str | None = None,
|
|
24
|
+
*,
|
|
25
|
+
client: AsyncYapper | None = None,
|
|
26
|
+
prefixes: tuple[str, ...] = ("/", "!"),
|
|
27
|
+
ignore_bots: bool = True,
|
|
28
|
+
log: bool = True,
|
|
29
|
+
command_limit: int = 100,
|
|
30
|
+
**client_kwargs: Any,
|
|
31
|
+
) -> None:
|
|
32
|
+
resolved = client if client is not None else AsyncYapper(token, **client_kwargs)
|
|
33
|
+
super().__init__(
|
|
34
|
+
resolved,
|
|
35
|
+
prefixes=prefixes,
|
|
36
|
+
ignore_bots=ignore_bots,
|
|
37
|
+
log=log,
|
|
38
|
+
command_limit=command_limit,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
def _log(self, message: str) -> None:
|
|
42
|
+
if self.log:
|
|
43
|
+
print(f"[yapperpy] {message}")
|
|
44
|
+
|
|
45
|
+
async def sync_commands(self) -> list[Command]:
|
|
46
|
+
payload = self.command_payload()
|
|
47
|
+
await self.client.bot.set_commands(payload)
|
|
48
|
+
self.registry.dirty = False
|
|
49
|
+
return [Command(name=item["name"], description=item["description"]) for item in payload]
|
|
50
|
+
|
|
51
|
+
async def invoke(self, handler: Callable[..., Any], *args: Any) -> Any:
|
|
52
|
+
result = handler(*args)
|
|
53
|
+
if inspect.isawaitable(result):
|
|
54
|
+
return await result
|
|
55
|
+
return result
|
|
56
|
+
|
|
57
|
+
async def emit(self, event: str, payload: Any) -> None:
|
|
58
|
+
for handler in self.listeners(event):
|
|
59
|
+
try:
|
|
60
|
+
await self.invoke(handler, payload)
|
|
61
|
+
except Exception as exc:
|
|
62
|
+
await self.handle_error(exc, None, event)
|
|
63
|
+
|
|
64
|
+
async def handle_error(
|
|
65
|
+
self, exc: Exception, owner: str | None = None, where: str | None = None
|
|
66
|
+
) -> None:
|
|
67
|
+
if owner is not None and getattr(exc, "cog", None) is None:
|
|
68
|
+
setattr(exc, "cog", owner)
|
|
69
|
+
setattr(exc, "where", where)
|
|
70
|
+
handlers = self.listeners("error")
|
|
71
|
+
if handlers:
|
|
72
|
+
for handler in handlers:
|
|
73
|
+
try:
|
|
74
|
+
await self.invoke(handler, exc)
|
|
75
|
+
except Exception as inner:
|
|
76
|
+
self._log(f"error listener failed: {inner}")
|
|
77
|
+
return
|
|
78
|
+
if self.log:
|
|
79
|
+
tag = f"[{owner}]" if owner else "[yapperpy]"
|
|
80
|
+
if where:
|
|
81
|
+
tag = f"{tag} {where}"
|
|
82
|
+
print(f"{tag} {type(exc).__name__}: {exc}")
|
|
83
|
+
traceback.print_exception(type(exc), exc, exc.__traceback__, limit=-3)
|
|
84
|
+
|
|
85
|
+
async def handle(self, event: Event) -> None:
|
|
86
|
+
kind, payload = self.prepare(event)
|
|
87
|
+
if kind == "message" and isinstance(payload, MessagePayload):
|
|
88
|
+
await self.handle_message(payload)
|
|
89
|
+
elif kind == "interaction" and isinstance(payload, InteractionPayload):
|
|
90
|
+
await self.handle_interaction(payload)
|
|
91
|
+
else:
|
|
92
|
+
await self.emit(kind, self.build_context(payload))
|
|
93
|
+
|
|
94
|
+
async def handle_message(self, payload: MessagePayload) -> None:
|
|
95
|
+
if self.ignore_bots and payload.author_bot:
|
|
96
|
+
return
|
|
97
|
+
context = self.build_context(payload)
|
|
98
|
+
match = self.resolve_command(payload)
|
|
99
|
+
if match is not None:
|
|
100
|
+
command, args = match
|
|
101
|
+
context.command = command.name
|
|
102
|
+
context.args = args
|
|
103
|
+
if await self.run_check(command, context):
|
|
104
|
+
try:
|
|
105
|
+
await self.invoke(command.handler, context)
|
|
106
|
+
except Exception as exc:
|
|
107
|
+
await self.handle_error(exc, command.owner, f"/{command.name}")
|
|
108
|
+
await self.emit("command", context)
|
|
109
|
+
await self.emit("message", context)
|
|
110
|
+
|
|
111
|
+
async def run_check(self, command: CommandSpec, context: MessageContext) -> bool:
|
|
112
|
+
if command.check is None:
|
|
113
|
+
return True
|
|
114
|
+
try:
|
|
115
|
+
result = command.check(context)
|
|
116
|
+
if inspect.isawaitable(result):
|
|
117
|
+
result = await result
|
|
118
|
+
return result is not False
|
|
119
|
+
except Exception as exc:
|
|
120
|
+
await self.handle_error(exc, command.owner, "check")
|
|
121
|
+
return False
|
|
122
|
+
|
|
123
|
+
async def handle_interaction(self, payload: InteractionPayload) -> None:
|
|
124
|
+
context = self.build_context(payload)
|
|
125
|
+
for entry in self.interaction_handlers(payload.custom_id):
|
|
126
|
+
try:
|
|
127
|
+
await self.invoke(entry.handler, context)
|
|
128
|
+
except Exception as exc:
|
|
129
|
+
await self.handle_error(exc, entry.owner, f"button {entry.matcher}")
|
|
130
|
+
await self.emit("interaction", context)
|
|
131
|
+
|
|
132
|
+
async def run(self) -> None:
|
|
133
|
+
try:
|
|
134
|
+
await self._run()
|
|
135
|
+
except KeyboardInterrupt:
|
|
136
|
+
self._log("stopped")
|
|
137
|
+
finally:
|
|
138
|
+
self.running = False
|
|
139
|
+
|
|
140
|
+
start = run
|
|
141
|
+
|
|
142
|
+
async def _run(self) -> None:
|
|
143
|
+
self.running = True
|
|
144
|
+
self.me = await self.client.bot.get()
|
|
145
|
+
await self.sync_commands()
|
|
146
|
+
await self.emit("ready", self.me)
|
|
147
|
+
if self.me is not None:
|
|
148
|
+
self._log(f"online as {self.me.username}")
|
|
149
|
+
failures = 0
|
|
150
|
+
while self.running:
|
|
151
|
+
try:
|
|
152
|
+
page = await self.client.events.fetch(after=self.last_event_id, wait=WAIT_SECONDS)
|
|
153
|
+
except Exception as exc:
|
|
154
|
+
failures += 1
|
|
155
|
+
await self.handle_error(exc, None, "events")
|
|
156
|
+
await asyncio.sleep(min(MAX_BACKOFF, 2.0**failures))
|
|
157
|
+
continue
|
|
158
|
+
failures = 0
|
|
159
|
+
for event in page.events:
|
|
160
|
+
if event.id <= self.last_event_id:
|
|
161
|
+
continue
|
|
162
|
+
self.last_event_id = event.id
|
|
163
|
+
await self.handle(event)
|
|
164
|
+
if self.registry.dirty:
|
|
165
|
+
await self.sync_commands()
|
|
166
|
+
if page.last > self.last_event_id:
|
|
167
|
+
self.last_event_id = page.last
|
|
168
|
+
|
|
169
|
+
def stop(self) -> None:
|
|
170
|
+
self.running = False
|