elos 1.0.1__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.
- ELOS/__init__.py +15 -0
- ELOS/__main__.py +24 -0
- ELOS/_ast.py +86 -0
- ELOS/_bot.py +89 -0
- ELOS/_gateway.py +159 -0
- ELOS/_http.py +261 -0
- ELOS/_lexer.py +99 -0
- ELOS/_parser.py +344 -0
- ELOS/_runtime.py +1082 -0
- ELOS/runner.py +66 -0
- ELOS/transpiler.py +3262 -0
- elos-1.0.1.dist-info/METADATA +765 -0
- elos-1.0.1.dist-info/RECORD +17 -0
- elos-1.0.1.dist-info/WHEEL +5 -0
- elos-1.0.1.dist-info/entry_points.txt +2 -0
- elos-1.0.1.dist-info/licenses/LICENSE +101 -0
- elos-1.0.1.dist-info/top_level.txt +1 -0
ELOS/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import sys as _sys
|
|
2
|
+
from .runner import run, run_source
|
|
3
|
+
|
|
4
|
+
__all__ = ["run", "run_source"]
|
|
5
|
+
__version__ = "2.0.0"
|
|
6
|
+
|
|
7
|
+
_sys.modules.pop("ELOS.runner", None)
|
|
8
|
+
_sys.modules.pop("ELOS._bot", None)
|
|
9
|
+
_sys.modules.pop("ELOS._gateway", None)
|
|
10
|
+
_sys.modules.pop("ELOS._http", None)
|
|
11
|
+
_sys.modules.pop("ELOS._runtime", None)
|
|
12
|
+
_sys.modules.pop("ELOS._parser", None)
|
|
13
|
+
_sys.modules.pop("ELOS._lexer", None)
|
|
14
|
+
_sys.modules.pop("ELOS._ast", None)
|
|
15
|
+
del _sys
|
ELOS/__main__.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import argparse
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
def main():
|
|
6
|
+
parser = argparse.ArgumentParser(prog="python -m ELOS")
|
|
7
|
+
parser.add_argument("file", help=".elos source file to run")
|
|
8
|
+
parser.add_argument("--user-id", type=int, default=0, metavar="INT")
|
|
9
|
+
parser.add_argument("--env", action="append", default=[], metavar="KEY=VALUE")
|
|
10
|
+
args = parser.parse_args()
|
|
11
|
+
|
|
12
|
+
extra_env = {}
|
|
13
|
+
for item in args.env:
|
|
14
|
+
if "=" not in item:
|
|
15
|
+
print(f"[ELOS] Invalid --env: {item}", file=sys.stderr)
|
|
16
|
+
sys.exit(1)
|
|
17
|
+
k, v = item.split("=", 1)
|
|
18
|
+
extra_env[k] = v
|
|
19
|
+
|
|
20
|
+
from ELOS.runner import run
|
|
21
|
+
sys.exit(run(args.file, user_id=args.user_id, env=extra_env or None))
|
|
22
|
+
|
|
23
|
+
if __name__ == "__main__":
|
|
24
|
+
main()
|
ELOS/_ast.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
class Node:
|
|
4
|
+
line: int = None
|
|
5
|
+
|
|
6
|
+
class Program(Node):
|
|
7
|
+
def __init__(self, events, funcdefs=None, login_token=None):
|
|
8
|
+
self.events = events
|
|
9
|
+
self.funcdefs = funcdefs or []
|
|
10
|
+
self.login_token = login_token
|
|
11
|
+
|
|
12
|
+
class EventBlock(Node):
|
|
13
|
+
def __init__(self, event_type, params, body):
|
|
14
|
+
self.event_type = event_type; self.params = params; self.body = body
|
|
15
|
+
|
|
16
|
+
class FuncDef(Node):
|
|
17
|
+
def __init__(self, name, params, body):
|
|
18
|
+
self.name = name; self.params = params; self.body = body
|
|
19
|
+
|
|
20
|
+
class AssignStmt(Node):
|
|
21
|
+
def __init__(self, target, value): self.target = target; self.value = value
|
|
22
|
+
|
|
23
|
+
class IfStmt(Node):
|
|
24
|
+
def __init__(self, cond, body, elifs=None, else_body=None):
|
|
25
|
+
self.cond = cond; self.body = body
|
|
26
|
+
self.elifs = elifs or []; self.else_body = else_body or []
|
|
27
|
+
|
|
28
|
+
class ForStmt(Node):
|
|
29
|
+
def __init__(self, var, iterable, body):
|
|
30
|
+
self.var = var; self.iterable = iterable; self.body = body
|
|
31
|
+
|
|
32
|
+
class WhileStmt(Node):
|
|
33
|
+
def __init__(self, cond, body): self.cond = cond; self.body = body
|
|
34
|
+
|
|
35
|
+
class TryCatch(Node):
|
|
36
|
+
def __init__(self, try_body, catch_body, catch_var=None):
|
|
37
|
+
self.try_body = try_body; self.catch_body = catch_body; self.catch_var = catch_var
|
|
38
|
+
|
|
39
|
+
class ReturnStmt(Node):
|
|
40
|
+
def __init__(self, value=None): self.value = value
|
|
41
|
+
|
|
42
|
+
class BreakStmt(Node): pass
|
|
43
|
+
class ContinueStmt(Node): pass
|
|
44
|
+
class StopStmt(Node): pass
|
|
45
|
+
|
|
46
|
+
class ExprStmt(Node):
|
|
47
|
+
def __init__(self, expr): self.expr = expr
|
|
48
|
+
|
|
49
|
+
class CallExpr(Node):
|
|
50
|
+
def __init__(self, func, args): self.func = func; self.args = args
|
|
51
|
+
|
|
52
|
+
class IndexExpr(Node):
|
|
53
|
+
def __init__(self, obj, idx): self.obj = obj; self.idx = idx
|
|
54
|
+
|
|
55
|
+
class BinOp(Node):
|
|
56
|
+
def __init__(self, left, op, right): self.left = left; self.op = op; self.right = right
|
|
57
|
+
|
|
58
|
+
class UnaryOp(Node):
|
|
59
|
+
def __init__(self, op, operand): self.op = op; self.operand = operand
|
|
60
|
+
|
|
61
|
+
class Ident(Node):
|
|
62
|
+
def __init__(self, name): self.name = name
|
|
63
|
+
|
|
64
|
+
class StringLit(Node):
|
|
65
|
+
def __init__(self, value): self.value = value
|
|
66
|
+
|
|
67
|
+
class NumberLit(Node):
|
|
68
|
+
def __init__(self, value): self.value = value
|
|
69
|
+
|
|
70
|
+
class BoolLit(Node):
|
|
71
|
+
def __init__(self, value): self.value = value
|
|
72
|
+
|
|
73
|
+
class NullLit(Node): pass
|
|
74
|
+
|
|
75
|
+
class ListLit(Node):
|
|
76
|
+
def __init__(self, items): self.items = items
|
|
77
|
+
|
|
78
|
+
class FStringLit(Node):
|
|
79
|
+
def __init__(self, parts): self.parts = parts
|
|
80
|
+
|
|
81
|
+
class EmbedLit(Node):
|
|
82
|
+
def __init__(self): self.fields = []
|
|
83
|
+
|
|
84
|
+
class TernaryExpr(Node):
|
|
85
|
+
def __init__(self, true_val, condition, false_val):
|
|
86
|
+
self.true_val = true_val; self.condition = condition; self.false_val = false_val
|
ELOS/_bot.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from ._lexer import Lexer
|
|
7
|
+
from ._parser import Parser
|
|
8
|
+
from ._ast import Program, EventBlock, FuncDef
|
|
9
|
+
from ._http import HTTP
|
|
10
|
+
from ._gateway import Gateway
|
|
11
|
+
from ._runtime import Interpreter, StopSignal, _red, _yellow
|
|
12
|
+
|
|
13
|
+
class Bot:
|
|
14
|
+
def __init__(self, token: str, user_id: int = 0):
|
|
15
|
+
self.token = token
|
|
16
|
+
self.user_id = user_id
|
|
17
|
+
self._http = HTTP(token)
|
|
18
|
+
self._gw: Optional[Gateway] = None
|
|
19
|
+
self._interp: Optional[Interpreter] = None
|
|
20
|
+
self._handlers: list[EventBlock] = []
|
|
21
|
+
self._timer_handlers: list[EventBlock] = []
|
|
22
|
+
self._program: Optional[Program] = None
|
|
23
|
+
|
|
24
|
+
def load_source(self, source: str):
|
|
25
|
+
tokens = Lexer(source).tokenize()
|
|
26
|
+
program = Parser(tokens).parse()
|
|
27
|
+
self._program = program
|
|
28
|
+
|
|
29
|
+
self._interp = Interpreter(self._http)
|
|
30
|
+
if self.user_id:
|
|
31
|
+
var_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '.elos_vars')
|
|
32
|
+
os.makedirs(var_dir, exist_ok=True)
|
|
33
|
+
self._interp._var_file = os.path.join(var_dir, f'user_{self.user_id}.json')
|
|
34
|
+
|
|
35
|
+
for fd in program.funcdefs:
|
|
36
|
+
self._interp.user_funcs[fd.name] = fd
|
|
37
|
+
|
|
38
|
+
self._handlers = program.events
|
|
39
|
+
self._timer_handlers = [e for e in program.events if e.event_type in ('onTimerEnd', 'onTimerStart')]
|
|
40
|
+
|
|
41
|
+
async def _timer_dispatch(timer_id: str):
|
|
42
|
+
await self._dispatch_elos('onTimerEnd', {'timer_id': timer_id})
|
|
43
|
+
self._interp._timer_dispatch = _timer_dispatch
|
|
44
|
+
|
|
45
|
+
return program
|
|
46
|
+
|
|
47
|
+
async def _dispatch_elos(self, elos_event: str, ctx: dict):
|
|
48
|
+
for handler in self._handlers:
|
|
49
|
+
if handler.event_type != elos_event:
|
|
50
|
+
continue
|
|
51
|
+
env = dict(self._interp.globals)
|
|
52
|
+
for i, param in enumerate(handler.params):
|
|
53
|
+
env[param] = list(ctx.values())[i] if i < len(ctx) else None
|
|
54
|
+
try:
|
|
55
|
+
from ._runtime import ReturnSignal, BreakSignal
|
|
56
|
+
await self._interp._exec_block(handler.body, env)
|
|
57
|
+
except StopSignal:
|
|
58
|
+
pass
|
|
59
|
+
except Exception as e:
|
|
60
|
+
line = getattr(handler, 'line', '?')
|
|
61
|
+
_red(f"[ELOS] {type(e).__name__}: {e} [Event: {elos_event}, Line: {line}]")
|
|
62
|
+
|
|
63
|
+
async def _on_gateway_event(self, event: str, data: dict):
|
|
64
|
+
if not self._interp:
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
if event == "READY":
|
|
68
|
+
self._interp._bot_user = data.get("user", {})
|
|
69
|
+
self._interp.application_id = self._gw.application_id
|
|
70
|
+
await self._interp.run_event(self._handlers, event, data)
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
await self._interp.run_event(self._handlers, event, data)
|
|
74
|
+
|
|
75
|
+
async def start(self):
|
|
76
|
+
self._gw = Gateway(self.token, self._on_gateway_event)
|
|
77
|
+
try:
|
|
78
|
+
await self._gw.connect()
|
|
79
|
+
except asyncio.CancelledError:
|
|
80
|
+
pass
|
|
81
|
+
finally:
|
|
82
|
+
await self._http.close()
|
|
83
|
+
if self._gw:
|
|
84
|
+
await self._gw.close()
|
|
85
|
+
|
|
86
|
+
async def close(self):
|
|
87
|
+
if self._gw:
|
|
88
|
+
await self._gw.close()
|
|
89
|
+
await self._http.close()
|
ELOS/_gateway.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import sys
|
|
4
|
+
import time
|
|
5
|
+
import aiohttp
|
|
6
|
+
from typing import Callable, Optional
|
|
7
|
+
|
|
8
|
+
RED = "\033[91m"
|
|
9
|
+
YELLOW= "\033[93m"
|
|
10
|
+
RESET = "\033[0m"
|
|
11
|
+
|
|
12
|
+
GW_URL = "wss://gateway.discord.gg/?v=10&encoding=json"
|
|
13
|
+
|
|
14
|
+
OP_DISPATCH = 0
|
|
15
|
+
OP_HEARTBEAT = 1
|
|
16
|
+
OP_IDENTIFY = 2
|
|
17
|
+
OP_RESUME = 6
|
|
18
|
+
OP_RECONNECT = 7
|
|
19
|
+
OP_INVALID_SESSION = 9
|
|
20
|
+
OP_HELLO = 10
|
|
21
|
+
OP_HEARTBEAT_ACK = 11
|
|
22
|
+
|
|
23
|
+
INTENT_GUILDS = 1 << 0
|
|
24
|
+
INTENT_GUILD_MEMBERS = 1 << 1
|
|
25
|
+
INTENT_GUILD_MODERATION = 1 << 2
|
|
26
|
+
INTENT_GUILD_EMOJIS = 1 << 3
|
|
27
|
+
INTENT_GUILD_MESSAGES = 1 << 9
|
|
28
|
+
INTENT_GUILD_REACTIONS = 1 << 10
|
|
29
|
+
INTENT_GUILD_TYPING = 1 << 11
|
|
30
|
+
INTENT_DM_MESSAGES = 1 << 12
|
|
31
|
+
INTENT_MESSAGE_CONTENT = 1 << 15
|
|
32
|
+
INTENT_GUILD_VOICE = 1 << 7
|
|
33
|
+
|
|
34
|
+
ALL_INTENTS = (
|
|
35
|
+
INTENT_GUILDS | INTENT_GUILD_MEMBERS | INTENT_GUILD_MODERATION |
|
|
36
|
+
INTENT_GUILD_EMOJIS | INTENT_GUILD_MESSAGES | INTENT_GUILD_REACTIONS |
|
|
37
|
+
INTENT_GUILD_TYPING | INTENT_DM_MESSAGES | INTENT_MESSAGE_CONTENT |
|
|
38
|
+
INTENT_GUILD_VOICE
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
class Gateway:
|
|
42
|
+
def __init__(self, token: str, dispatch: Callable):
|
|
43
|
+
self.token = token
|
|
44
|
+
self.dispatch = dispatch
|
|
45
|
+
self._ws: Optional[aiohttp.ClientWebSocketResponse] = None
|
|
46
|
+
self._session: Optional[aiohttp.ClientSession] = None
|
|
47
|
+
self._seq: Optional[int] = None
|
|
48
|
+
self._session_id: Optional[str] = None
|
|
49
|
+
self._heartbeat_interval: float = 41.25
|
|
50
|
+
self._last_ack: float = time.monotonic()
|
|
51
|
+
self._running = False
|
|
52
|
+
self.application_id: Optional[str] = None
|
|
53
|
+
self.user: Optional[dict] = None
|
|
54
|
+
|
|
55
|
+
async def connect(self):
|
|
56
|
+
self._running = True
|
|
57
|
+
self._session = aiohttp.ClientSession()
|
|
58
|
+
while self._running:
|
|
59
|
+
try:
|
|
60
|
+
await self._run()
|
|
61
|
+
except asyncio.CancelledError:
|
|
62
|
+
break
|
|
63
|
+
except Exception as e:
|
|
64
|
+
print(f"{RED}[ELOS] Gateway error: {e}{RESET}", file=sys.stderr)
|
|
65
|
+
if self._running:
|
|
66
|
+
print(f"{YELLOW}[ELOS] Reconnecting in 5s...{RESET}", file=sys.stderr)
|
|
67
|
+
await asyncio.sleep(5)
|
|
68
|
+
|
|
69
|
+
async def _run(self):
|
|
70
|
+
async with self._session.ws_connect(GW_URL) as ws:
|
|
71
|
+
self._ws = ws
|
|
72
|
+
hello = await ws.receive_json()
|
|
73
|
+
self._heartbeat_interval = hello["d"]["heartbeat_interval"] / 1000.0
|
|
74
|
+
|
|
75
|
+
hb_task = asyncio.create_task(self._heartbeat_loop())
|
|
76
|
+
try:
|
|
77
|
+
if self._session_id:
|
|
78
|
+
await self._resume(ws)
|
|
79
|
+
else:
|
|
80
|
+
await self._identify(ws)
|
|
81
|
+
|
|
82
|
+
async for msg in ws:
|
|
83
|
+
if msg.type == aiohttp.WSMsgType.TEXT:
|
|
84
|
+
await self._handle(json.loads(msg.data))
|
|
85
|
+
elif msg.type in (aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.ERROR):
|
|
86
|
+
break
|
|
87
|
+
finally:
|
|
88
|
+
hb_task.cancel()
|
|
89
|
+
try: await hb_task
|
|
90
|
+
except asyncio.CancelledError: pass
|
|
91
|
+
|
|
92
|
+
async def _identify(self, ws):
|
|
93
|
+
await ws.send_json({
|
|
94
|
+
"op": OP_IDENTIFY,
|
|
95
|
+
"d": {
|
|
96
|
+
"token": self.token,
|
|
97
|
+
"intents": ALL_INTENTS,
|
|
98
|
+
"properties": {"os": "linux", "browser": "ELOS", "device": "ELOS"},
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
async def _resume(self, ws):
|
|
103
|
+
await ws.send_json({
|
|
104
|
+
"op": OP_RESUME,
|
|
105
|
+
"d": {
|
|
106
|
+
"token": self.token,
|
|
107
|
+
"session_id": self._session_id,
|
|
108
|
+
"seq": self._seq,
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
async def _heartbeat_loop(self):
|
|
113
|
+
await asyncio.sleep(self._heartbeat_interval * 0.5)
|
|
114
|
+
while True:
|
|
115
|
+
if self._ws and not self._ws.closed:
|
|
116
|
+
await self._ws.send_json({"op": OP_HEARTBEAT, "d": self._seq})
|
|
117
|
+
await asyncio.sleep(self._heartbeat_interval)
|
|
118
|
+
|
|
119
|
+
async def _handle(self, msg: dict):
|
|
120
|
+
op = msg.get("op")
|
|
121
|
+
data = msg.get("d")
|
|
122
|
+
seq = msg.get("s")
|
|
123
|
+
event = msg.get("t")
|
|
124
|
+
|
|
125
|
+
if seq is not None:
|
|
126
|
+
self._seq = seq
|
|
127
|
+
|
|
128
|
+
if op == OP_DISPATCH:
|
|
129
|
+
if event == "READY":
|
|
130
|
+
self._session_id = data["session_id"]
|
|
131
|
+
self.user = data["user"]
|
|
132
|
+
self.application_id = data.get("application", {}).get("id") or data["user"]["id"]
|
|
133
|
+
print(f"[ELOS] Bot online: {data['user']['username']}#{data['user']['discriminator']}")
|
|
134
|
+
await self.dispatch(event, data)
|
|
135
|
+
|
|
136
|
+
elif op == OP_HEARTBEAT:
|
|
137
|
+
if self._ws:
|
|
138
|
+
await self._ws.send_json({"op": OP_HEARTBEAT, "d": self._seq})
|
|
139
|
+
|
|
140
|
+
elif op == OP_HEARTBEAT_ACK:
|
|
141
|
+
self._last_ack = time.monotonic()
|
|
142
|
+
|
|
143
|
+
elif op == OP_RECONNECT:
|
|
144
|
+
if self._ws: await self._ws.close()
|
|
145
|
+
|
|
146
|
+
elif op == OP_INVALID_SESSION:
|
|
147
|
+
resumable = bool(data)
|
|
148
|
+
if not resumable:
|
|
149
|
+
self._session_id = None
|
|
150
|
+
self._seq = None
|
|
151
|
+
await asyncio.sleep(2)
|
|
152
|
+
if self._ws: await self._ws.close()
|
|
153
|
+
|
|
154
|
+
async def close(self):
|
|
155
|
+
self._running = False
|
|
156
|
+
if self._ws and not self._ws.closed:
|
|
157
|
+
await self._ws.close()
|
|
158
|
+
if self._session and not self._session.closed:
|
|
159
|
+
await self._session.close()
|
ELOS/_http.py
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import aiohttp
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
YELLOW = "\033[93m"
|
|
8
|
+
RESET = "\033[0m"
|
|
9
|
+
|
|
10
|
+
API = "https://discord.com/api/v10"
|
|
11
|
+
|
|
12
|
+
class DiscordHTTPError(Exception):
|
|
13
|
+
def __init__(self, status: int, message: str, code: int = 0):
|
|
14
|
+
self.status = status
|
|
15
|
+
self.code = code
|
|
16
|
+
super().__init__(f"HTTP {status}: {message} (code={code})")
|
|
17
|
+
|
|
18
|
+
class HTTP:
|
|
19
|
+
def __init__(self, token: str):
|
|
20
|
+
self.token = token
|
|
21
|
+
self._session: Optional[aiohttp.ClientSession] = None
|
|
22
|
+
self._global_rl = asyncio.Event()
|
|
23
|
+
self._global_rl.set()
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def headers(self):
|
|
27
|
+
return {
|
|
28
|
+
"Authorization": f"Bot {self.token}",
|
|
29
|
+
"Content-Type": "application/json",
|
|
30
|
+
"User-Agent": "ELOS/1.0",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async def _session_get(self) -> aiohttp.ClientSession:
|
|
34
|
+
if self._session is None or self._session.closed:
|
|
35
|
+
self._session = aiohttp.ClientSession()
|
|
36
|
+
return self._session
|
|
37
|
+
|
|
38
|
+
async def request(self, method: str, path: str, **kwargs) -> dict | list | None:
|
|
39
|
+
await self._global_rl.wait()
|
|
40
|
+
session = await self._session_get()
|
|
41
|
+
url = f"{API}{path}"
|
|
42
|
+
headers = self.headers.copy()
|
|
43
|
+
|
|
44
|
+
data = kwargs.pop("json", None)
|
|
45
|
+
if data is not None:
|
|
46
|
+
kwargs["data"] = json.dumps(data)
|
|
47
|
+
|
|
48
|
+
form = kwargs.pop("form", None)
|
|
49
|
+
|
|
50
|
+
for attempt in range(5):
|
|
51
|
+
try:
|
|
52
|
+
if form:
|
|
53
|
+
resp = await session.request(method, url, headers={k: v for k, v in headers.items() if k != "Content-Type"}, data=form)
|
|
54
|
+
else:
|
|
55
|
+
resp = await session.request(method, url, headers=headers, **kwargs)
|
|
56
|
+
|
|
57
|
+
if resp.status == 429:
|
|
58
|
+
data_rl = await resp.json()
|
|
59
|
+
retry_after = data_rl.get("retry_after", 1.0)
|
|
60
|
+
is_global = data_rl.get("global", False)
|
|
61
|
+
print(f"{YELLOW}[ELOS] Rate limited. Retry after {retry_after}s (global={is_global}){RESET}", file=sys.stderr)
|
|
62
|
+
if is_global:
|
|
63
|
+
self._global_rl.clear()
|
|
64
|
+
await asyncio.sleep(retry_after)
|
|
65
|
+
self._global_rl.set()
|
|
66
|
+
else:
|
|
67
|
+
await asyncio.sleep(retry_after)
|
|
68
|
+
continue
|
|
69
|
+
|
|
70
|
+
if resp.status == 204:
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
body = await resp.text()
|
|
74
|
+
try:
|
|
75
|
+
result = json.loads(body)
|
|
76
|
+
except Exception:
|
|
77
|
+
result = body
|
|
78
|
+
|
|
79
|
+
if resp.status >= 400:
|
|
80
|
+
msg = result.get("message", body) if isinstance(result, dict) else body
|
|
81
|
+
code = result.get("code", 0) if isinstance(result, dict) else 0
|
|
82
|
+
raise DiscordHTTPError(resp.status, msg, code)
|
|
83
|
+
|
|
84
|
+
remaining = resp.headers.get("X-RateLimit-Remaining")
|
|
85
|
+
reset_after = resp.headers.get("X-RateLimit-Reset-After")
|
|
86
|
+
if remaining == "0" and reset_after:
|
|
87
|
+
await asyncio.sleep(float(reset_after))
|
|
88
|
+
|
|
89
|
+
return result
|
|
90
|
+
|
|
91
|
+
except DiscordHTTPError:
|
|
92
|
+
raise
|
|
93
|
+
except aiohttp.ClientError as e:
|
|
94
|
+
if attempt == 4:
|
|
95
|
+
raise
|
|
96
|
+
await asyncio.sleep(1 + attempt)
|
|
97
|
+
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
async def close(self):
|
|
101
|
+
if self._session and not self._session.closed:
|
|
102
|
+
await self._session.close()
|
|
103
|
+
|
|
104
|
+
async def send_message(self, channel_id, content=None, embed=None, components=None, reference=None, ephemeral=False, files=None):
|
|
105
|
+
payload = {}
|
|
106
|
+
if content is not None:
|
|
107
|
+
payload["content"] = str(content)
|
|
108
|
+
if embed is not None:
|
|
109
|
+
payload["embeds"] = [embed if isinstance(embed, dict) else embed.to_dict()]
|
|
110
|
+
if components:
|
|
111
|
+
payload["components"] = components
|
|
112
|
+
if reference:
|
|
113
|
+
payload["message_reference"] = {"message_id": str(reference)}
|
|
114
|
+
if files:
|
|
115
|
+
form = aiohttp.FormData()
|
|
116
|
+
form.add_field("payload_json", json.dumps(payload), content_type="application/json")
|
|
117
|
+
for i, (name, data, ct) in enumerate(files):
|
|
118
|
+
form.add_field(f"files[{i}]", data, filename=name, content_type=ct)
|
|
119
|
+
return await self.request("POST", f"/channels/{channel_id}/messages", form=form)
|
|
120
|
+
return await self.request("POST", f"/channels/{channel_id}/messages", json=payload)
|
|
121
|
+
|
|
122
|
+
async def delete_message(self, channel_id, message_id):
|
|
123
|
+
return await self.request("DELETE", f"/channels/{channel_id}/messages/{message_id}")
|
|
124
|
+
|
|
125
|
+
async def edit_message(self, channel_id, message_id, content=None, embed=None):
|
|
126
|
+
payload = {}
|
|
127
|
+
if content is not None: payload["content"] = str(content)
|
|
128
|
+
if embed is not None: payload["embeds"] = [embed if isinstance(embed, dict) else embed.to_dict()]
|
|
129
|
+
return await self.request("PATCH", f"/channels/{channel_id}/messages/{message_id}", json=payload)
|
|
130
|
+
|
|
131
|
+
async def pin_message(self, channel_id, message_id):
|
|
132
|
+
return await self.request("PUT", f"/channels/{channel_id}/pins/{message_id}")
|
|
133
|
+
|
|
134
|
+
async def unpin_message(self, channel_id, message_id):
|
|
135
|
+
return await self.request("DELETE", f"/channels/{channel_id}/pins/{message_id}")
|
|
136
|
+
|
|
137
|
+
async def add_reaction(self, channel_id, message_id, emoji):
|
|
138
|
+
emoji_enc = emoji.replace("#", "%23")
|
|
139
|
+
return await self.request("PUT", f"/channels/{channel_id}/messages/{message_id}/reactions/{emoji_enc}/@me")
|
|
140
|
+
|
|
141
|
+
async def remove_reaction(self, channel_id, message_id, emoji, user_id=None):
|
|
142
|
+
emoji_enc = emoji.replace("#", "%23")
|
|
143
|
+
target = f"/{user_id}" if user_id else "/@me"
|
|
144
|
+
return await self.request("DELETE", f"/channels/{channel_id}/messages/{message_id}/reactions/{emoji_enc}{target}")
|
|
145
|
+
|
|
146
|
+
async def get_message(self, channel_id, message_id):
|
|
147
|
+
return await self.request("GET", f"/channels/{channel_id}/messages/{message_id}")
|
|
148
|
+
|
|
149
|
+
async def get_channel(self, channel_id):
|
|
150
|
+
return await self.request("GET", f"/channels/{channel_id}")
|
|
151
|
+
|
|
152
|
+
async def create_channel(self, guild_id, name, type=0, topic=None, parent_id=None):
|
|
153
|
+
payload = {"name": name, "type": type}
|
|
154
|
+
if topic: payload["topic"] = topic
|
|
155
|
+
if parent_id: payload["parent_id"] = str(parent_id)
|
|
156
|
+
return await self.request("POST", f"/guilds/{guild_id}/channels", json=payload)
|
|
157
|
+
|
|
158
|
+
async def delete_channel(self, channel_id):
|
|
159
|
+
return await self.request("DELETE", f"/channels/{channel_id}")
|
|
160
|
+
|
|
161
|
+
async def edit_channel(self, channel_id, **kwargs):
|
|
162
|
+
return await self.request("PATCH", f"/channels/{channel_id}", json=kwargs)
|
|
163
|
+
|
|
164
|
+
async def get_guild_channels(self, guild_id):
|
|
165
|
+
return await self.request("GET", f"/guilds/{guild_id}/channels")
|
|
166
|
+
|
|
167
|
+
async def create_invite(self, channel_id, max_age=86400, max_uses=0, temporary=False):
|
|
168
|
+
return await self.request("POST", f"/channels/{channel_id}/invites",
|
|
169
|
+
json={"max_age": max_age, "max_uses": max_uses, "temporary": temporary})
|
|
170
|
+
|
|
171
|
+
async def get_guild(self, guild_id):
|
|
172
|
+
return await self.request("GET", f"/guilds/{guild_id}")
|
|
173
|
+
|
|
174
|
+
async def get_guild_members(self, guild_id, limit=1000):
|
|
175
|
+
return await self.request("GET", f"/guilds/{guild_id}/members?limit={limit}")
|
|
176
|
+
|
|
177
|
+
async def get_member(self, guild_id, user_id):
|
|
178
|
+
return await self.request("GET", f"/guilds/{guild_id}/members/{user_id}")
|
|
179
|
+
|
|
180
|
+
async def ban_member(self, guild_id, user_id, reason=None, delete_days=0):
|
|
181
|
+
payload = {"delete_message_days": delete_days}
|
|
182
|
+
headers = {}
|
|
183
|
+
if reason: headers["X-Audit-Log-Reason"] = reason
|
|
184
|
+
return await self.request("PUT", f"/guilds/{guild_id}/bans/{user_id}", json=payload)
|
|
185
|
+
|
|
186
|
+
async def unban_member(self, guild_id, user_id):
|
|
187
|
+
return await self.request("DELETE", f"/guilds/{guild_id}/bans/{user_id}")
|
|
188
|
+
|
|
189
|
+
async def kick_member(self, guild_id, user_id, reason=None):
|
|
190
|
+
return await self.request("DELETE", f"/guilds/{guild_id}/members/{user_id}")
|
|
191
|
+
|
|
192
|
+
async def add_role(self, guild_id, user_id, role_id):
|
|
193
|
+
return await self.request("PUT", f"/guilds/{guild_id}/members/{user_id}/roles/{role_id}")
|
|
194
|
+
|
|
195
|
+
async def remove_role(self, guild_id, user_id, role_id):
|
|
196
|
+
return await self.request("DELETE", f"/guilds/{guild_id}/members/{user_id}/roles/{role_id}")
|
|
197
|
+
|
|
198
|
+
async def edit_member(self, guild_id, user_id, **kwargs):
|
|
199
|
+
return await self.request("PATCH", f"/guilds/{guild_id}/members/{user_id}", json=kwargs)
|
|
200
|
+
|
|
201
|
+
async def get_guild_roles(self, guild_id):
|
|
202
|
+
return await self.request("GET", f"/guilds/{guild_id}/roles")
|
|
203
|
+
|
|
204
|
+
async def create_role(self, guild_id, name, color=0, permissions="0", mentionable=False):
|
|
205
|
+
return await self.request("POST", f"/guilds/{guild_id}/roles",
|
|
206
|
+
json={"name": name, "color": color, "permissions": permissions, "mentionable": mentionable})
|
|
207
|
+
|
|
208
|
+
async def delete_role(self, guild_id, role_id):
|
|
209
|
+
return await self.request("DELETE", f"/guilds/{guild_id}/roles/{role_id}")
|
|
210
|
+
|
|
211
|
+
async def edit_role(self, guild_id, role_id, **kwargs):
|
|
212
|
+
return await self.request("PATCH", f"/guilds/{guild_id}/roles/{role_id}", json=kwargs)
|
|
213
|
+
|
|
214
|
+
async def get_user(self, user_id):
|
|
215
|
+
return await self.request("GET", f"/users/{user_id}")
|
|
216
|
+
|
|
217
|
+
async def get_me(self):
|
|
218
|
+
return await self.request("GET", "/users/@me")
|
|
219
|
+
|
|
220
|
+
async def create_webhook(self, channel_id, name):
|
|
221
|
+
return await self.request("POST", f"/channels/{channel_id}/webhooks", json={"name": name})
|
|
222
|
+
|
|
223
|
+
async def delete_webhook(self, webhook_id):
|
|
224
|
+
return await self.request("DELETE", f"/webhooks/{webhook_id}")
|
|
225
|
+
|
|
226
|
+
async def execute_webhook(self, webhook_id, webhook_token, content=None, username=None, avatar_url=None, embeds=None):
|
|
227
|
+
payload = {}
|
|
228
|
+
if content: payload["content"] = str(content)
|
|
229
|
+
if username: payload["username"] = username
|
|
230
|
+
if avatar_url: payload["avatar_url"] = avatar_url
|
|
231
|
+
if embeds: payload["embeds"] = embeds
|
|
232
|
+
return await self.request("POST", f"/webhooks/{webhook_id}/{webhook_token}", json=payload)
|
|
233
|
+
|
|
234
|
+
async def interaction_response(self, interaction_id, interaction_token, type: int, data: dict = None):
|
|
235
|
+
payload = {"type": type}
|
|
236
|
+
if data: payload["data"] = data
|
|
237
|
+
return await self.request("POST", f"/interactions/{interaction_id}/{interaction_token}/callback", json=payload)
|
|
238
|
+
|
|
239
|
+
async def edit_interaction_response(self, application_id, interaction_token, content=None, embeds=None):
|
|
240
|
+
payload = {}
|
|
241
|
+
if content is not None: payload["content"] = str(content)
|
|
242
|
+
if embeds: payload["embeds"] = embeds
|
|
243
|
+
return await self.request("PATCH", f"/webhooks/{application_id}/{interaction_token}/messages/@original", json=payload)
|
|
244
|
+
|
|
245
|
+
async def register_global_command(self, application_id, command: dict):
|
|
246
|
+
return await self.request("POST", f"/applications/{application_id}/commands", json=command)
|
|
247
|
+
|
|
248
|
+
async def register_guild_command(self, application_id, guild_id, command: dict):
|
|
249
|
+
return await self.request("POST", f"/applications/{application_id}/guilds/{guild_id}/commands", json=command)
|
|
250
|
+
|
|
251
|
+
async def get_global_commands(self, application_id):
|
|
252
|
+
return await self.request("GET", f"/applications/{application_id}/commands")
|
|
253
|
+
|
|
254
|
+
async def delete_global_command(self, application_id, command_id):
|
|
255
|
+
return await self.request("DELETE", f"/applications/{application_id}/commands/{command_id}")
|
|
256
|
+
|
|
257
|
+
async def trigger_typing(self, channel_id):
|
|
258
|
+
return await self.request("POST", f"/channels/{channel_id}/typing")
|
|
259
|
+
|
|
260
|
+
async def create_dm(self, user_id):
|
|
261
|
+
return await self.request("POST", "/users/@me/channels", json={"recipient_id": str(user_id)})
|