python-yappy 0.1.0__tar.gz → 0.1.1__tar.gz
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.
- {python_yappy-0.1.0 → python_yappy-0.1.1}/PKG-INFO +28 -35
- {python_yappy-0.1.0 → python_yappy-0.1.1}/README.md +27 -34
- {python_yappy-0.1.0 → python_yappy-0.1.1}/pyproject.toml +1 -1
- {python_yappy-0.1.0 → python_yappy-0.1.1}/pyproject.toml.orig +1 -1
- {python_yappy-0.1.0 → python_yappy-0.1.1}/src/yappy/api.py +27 -9
- python_yappy-0.1.1/src/yappy/app.py +401 -0
- python_yappy-0.1.1/src/yappy/core.py +272 -0
- python_yappy-0.1.1/src/yappy/parser.py +69 -0
- python_yappy-0.1.1/src/yappy/utils.py +4 -0
- python_yappy-0.1.0/src/yappy/app.py +0 -204
- python_yappy-0.1.0/src/yappy/core.py +0 -105
- {python_yappy-0.1.0 → python_yappy-0.1.1}/src/yappy/__init__.py +0 -0
- {python_yappy-0.1.0 → python_yappy-0.1.1}/src/yappy/consts.py +0 -0
- {python_yappy-0.1.0 → python_yappy-0.1.1}/src/yappy/errors.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: python-yappy
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: API Bindings for Yapper!
|
|
5
5
|
Author: H1387Lmao
|
|
6
6
|
Requires-Dist: aiohttp>=3.14.5
|
|
@@ -28,56 +28,47 @@ YapPy brings a Python-friendly interface to [Yapper](https://yapper.website), a
|
|
|
28
28
|
|
|
29
29
|
## Installation
|
|
30
30
|
|
|
31
|
-
Install YapPy using your project's preferred method.
|
|
32
|
-
|
|
33
|
-
If the package is available on PyPI:
|
|
34
|
-
|
|
35
|
-
```bash
|
|
36
|
-
pip install yappy
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
If you are working from a local checkout, install the project from its repository directory:
|
|
40
31
|
|
|
41
32
|
```bash
|
|
42
|
-
pip install
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
The example below uses [`python-dotenv`](https://pypi.org/project/python-dotenv/) to load the bot token from a `.env` file:
|
|
46
|
-
|
|
47
|
-
```bash
|
|
48
|
-
pip install python-dotenv
|
|
33
|
+
pip install python-yappy
|
|
49
34
|
```
|
|
50
35
|
|
|
51
36
|
## Quick start
|
|
52
37
|
|
|
53
|
-
|
|
38
|
+
This example uses the dotenv module from `pip install load_dotenv`
|
|
39
|
+
Create a file named `main.py`:
|
|
54
40
|
|
|
55
41
|
```python
|
|
42
|
+
from yappy import App
|
|
56
43
|
import os
|
|
57
|
-
|
|
58
44
|
from dotenv import load_dotenv
|
|
59
|
-
from yappy import App
|
|
60
|
-
|
|
61
45
|
load_dotenv()
|
|
62
|
-
|
|
63
46
|
bot = App(verbose=True)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
47
|
+
@bot.event(
|
|
48
|
+
"message"
|
|
49
|
+
)
|
|
67
50
|
async def on_message(ctx):
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
await ctx.reply("Hi!")
|
|
72
|
-
|
|
73
|
-
|
|
51
|
+
if ctx.is_bot:
|
|
52
|
+
return
|
|
53
|
+
await bot.process_command(ctx)
|
|
74
54
|
@bot.command(
|
|
75
|
-
|
|
76
|
-
description="replies with 'Pong!'"
|
|
55
|
+
name="ping",
|
|
56
|
+
description="replies with 'Pong!'"
|
|
77
57
|
)
|
|
78
58
|
async def ping(ctx):
|
|
79
|
-
|
|
59
|
+
await ctx.reply("Pong!")
|
|
80
60
|
|
|
61
|
+
@bot.command(
|
|
62
|
+
name="react",
|
|
63
|
+
description="React your message with a certain emoji!"
|
|
64
|
+
)
|
|
65
|
+
async def react(ctx, emoji: str):
|
|
66
|
+
await ctx.message.react(emoji)
|
|
67
|
+
@bot.event(
|
|
68
|
+
"ready"
|
|
69
|
+
)
|
|
70
|
+
async def on_ready():
|
|
71
|
+
print("Logged as", bot.user.name)
|
|
81
72
|
|
|
82
73
|
bot.run(os.getenv("TOKEN"))
|
|
83
74
|
```
|
|
@@ -123,7 +114,7 @@ async def on_message(ctx):
|
|
|
123
114
|
if ctx.is_bot:
|
|
124
115
|
return
|
|
125
116
|
|
|
126
|
-
await ctx.
|
|
117
|
+
await ctx.process.command(ctx)
|
|
127
118
|
```
|
|
128
119
|
|
|
129
120
|
The event name and handler arguments should match the event interface supported by the installed YapPy version.
|
|
@@ -153,6 +144,8 @@ YapPy currently provides these core models:
|
|
|
153
144
|
| `Message` | Represents a message. |
|
|
154
145
|
| `Channel` | Represents a channel. |
|
|
155
146
|
| `User` | Represents a user. |
|
|
147
|
+
| `Guild` | Represents a guild / server |
|
|
148
|
+
| `Role` | Represents a role |
|
|
156
149
|
|
|
157
150
|
The example uses the context object (`ctx`) to check `ctx.is_bot` and send a reply with `ctx.reply(...)`. Consult the source code for the currently available attributes and methods of each model.
|
|
158
151
|
|
|
@@ -17,56 +17,47 @@ YapPy brings a Python-friendly interface to [Yapper](https://yapper.website), a
|
|
|
17
17
|
|
|
18
18
|
## Installation
|
|
19
19
|
|
|
20
|
-
Install YapPy using your project's preferred method.
|
|
21
|
-
|
|
22
|
-
If the package is available on PyPI:
|
|
23
|
-
|
|
24
|
-
```bash
|
|
25
|
-
pip install yappy
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
If you are working from a local checkout, install the project from its repository directory:
|
|
29
20
|
|
|
30
21
|
```bash
|
|
31
|
-
pip install
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
The example below uses [`python-dotenv`](https://pypi.org/project/python-dotenv/) to load the bot token from a `.env` file:
|
|
35
|
-
|
|
36
|
-
```bash
|
|
37
|
-
pip install python-dotenv
|
|
22
|
+
pip install python-yappy
|
|
38
23
|
```
|
|
39
24
|
|
|
40
25
|
## Quick start
|
|
41
26
|
|
|
42
|
-
|
|
27
|
+
This example uses the dotenv module from `pip install load_dotenv`
|
|
28
|
+
Create a file named `main.py`:
|
|
43
29
|
|
|
44
30
|
```python
|
|
31
|
+
from yappy import App
|
|
45
32
|
import os
|
|
46
|
-
|
|
47
33
|
from dotenv import load_dotenv
|
|
48
|
-
from yappy import App
|
|
49
|
-
|
|
50
34
|
load_dotenv()
|
|
51
|
-
|
|
52
35
|
bot = App(verbose=True)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
36
|
+
@bot.event(
|
|
37
|
+
"message"
|
|
38
|
+
)
|
|
56
39
|
async def on_message(ctx):
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
await ctx.reply("Hi!")
|
|
61
|
-
|
|
62
|
-
|
|
40
|
+
if ctx.is_bot:
|
|
41
|
+
return
|
|
42
|
+
await bot.process_command(ctx)
|
|
63
43
|
@bot.command(
|
|
64
|
-
|
|
65
|
-
description="replies with 'Pong!'"
|
|
44
|
+
name="ping",
|
|
45
|
+
description="replies with 'Pong!'"
|
|
66
46
|
)
|
|
67
47
|
async def ping(ctx):
|
|
68
|
-
|
|
48
|
+
await ctx.reply("Pong!")
|
|
69
49
|
|
|
50
|
+
@bot.command(
|
|
51
|
+
name="react",
|
|
52
|
+
description="React your message with a certain emoji!"
|
|
53
|
+
)
|
|
54
|
+
async def react(ctx, emoji: str):
|
|
55
|
+
await ctx.message.react(emoji)
|
|
56
|
+
@bot.event(
|
|
57
|
+
"ready"
|
|
58
|
+
)
|
|
59
|
+
async def on_ready():
|
|
60
|
+
print("Logged as", bot.user.name)
|
|
70
61
|
|
|
71
62
|
bot.run(os.getenv("TOKEN"))
|
|
72
63
|
```
|
|
@@ -112,7 +103,7 @@ async def on_message(ctx):
|
|
|
112
103
|
if ctx.is_bot:
|
|
113
104
|
return
|
|
114
105
|
|
|
115
|
-
await ctx.
|
|
106
|
+
await ctx.process.command(ctx)
|
|
116
107
|
```
|
|
117
108
|
|
|
118
109
|
The event name and handler arguments should match the event interface supported by the installed YapPy version.
|
|
@@ -142,6 +133,8 @@ YapPy currently provides these core models:
|
|
|
142
133
|
| `Message` | Represents a message. |
|
|
143
134
|
| `Channel` | Represents a channel. |
|
|
144
135
|
| `User` | Represents a user. |
|
|
136
|
+
| `Guild` | Represents a guild / server |
|
|
137
|
+
| `Role` | Represents a role |
|
|
145
138
|
|
|
146
139
|
The example uses the context object (`ctx`) to check `ctx.is_bot` and send a reply with `ctx.reply(...)`. Consult the source code for the currently available attributes and methods of each model.
|
|
147
140
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from .consts import BASE_URL
|
|
2
2
|
import aiohttp
|
|
3
|
+
import random
|
|
4
|
+
import asyncio
|
|
3
5
|
|
|
4
6
|
async def api_push(session: aiohttp.ClientSession, method: str, path: str, body=None, tries=3):
|
|
5
7
|
headers = {
|
|
@@ -25,20 +27,36 @@ async def api_push(session: aiohttp.ClientSession, method: str, path: str, body=
|
|
|
25
27
|
if 0 < wait <= 60 and tries > 1:
|
|
26
28
|
print(f"{'busy' if busy else 'rate limited'}, retrying in {wait}s")
|
|
27
29
|
await asyncio.sleep(wait * (1 + random.random() * 0.5))
|
|
28
|
-
return await
|
|
30
|
+
return await api_push(session, method, path, body, tries - 1)
|
|
29
31
|
|
|
30
32
|
raise RuntimeError(f"{method} {path} failed ({e.status}): {msg or e.message}") from None
|
|
31
|
-
|
|
32
|
-
class
|
|
33
|
+
|
|
34
|
+
class API:
|
|
33
35
|
@staticmethod
|
|
34
|
-
async def
|
|
35
|
-
await api_push(session,
|
|
36
|
+
async def _request(session, method, path="", body=None, tries=3):
|
|
37
|
+
return await api_push(session, method, path, body, tries)
|
|
38
|
+
|
|
36
39
|
@staticmethod
|
|
37
40
|
async def get(session, path="", body=None, tries=3):
|
|
38
|
-
return await
|
|
41
|
+
return await API._request(session, "GET", path, body, tries)
|
|
42
|
+
|
|
39
43
|
@staticmethod
|
|
40
44
|
async def post(session, path="", body=None, tries=3):
|
|
41
|
-
await
|
|
45
|
+
return await API._request(session, "POST", path, body, tries)
|
|
46
|
+
|
|
47
|
+
@staticmethod
|
|
48
|
+
async def put(session, path="", body=None, tries=3):
|
|
49
|
+
return await API._request(session, "PUT", path, body, tries)
|
|
50
|
+
|
|
51
|
+
@staticmethod
|
|
52
|
+
async def patch(session, path="", body=None, tries=3):
|
|
53
|
+
return await API._request(session, "PATCH", path, body, tries)
|
|
54
|
+
|
|
42
55
|
@staticmethod
|
|
43
|
-
async def
|
|
44
|
-
await
|
|
56
|
+
async def delete(session, path="", body=None, tries=3):
|
|
57
|
+
return await API._request(session, "DELETE", path, body, tries)
|
|
58
|
+
|
|
59
|
+
@staticmethod
|
|
60
|
+
async def send(session, channel_id=0, body=None, tries=3):
|
|
61
|
+
return await API._request(session, "POST", f"/channels/{channel_id}/messages", body, tries)
|
|
62
|
+
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
from .core import *
|
|
2
|
+
from .consts import BASE_URL
|
|
3
|
+
from .api import *
|
|
4
|
+
from .errors import ImproperToken, InvalidSignature, UnknownCommand
|
|
5
|
+
from .parser import parser
|
|
6
|
+
|
|
7
|
+
import inspect
|
|
8
|
+
import asyncio
|
|
9
|
+
import aiohttp
|
|
10
|
+
import traceback
|
|
11
|
+
from functools import wraps
|
|
12
|
+
from typing import Callable
|
|
13
|
+
|
|
14
|
+
class App:
|
|
15
|
+
def __init__(
|
|
16
|
+
self, verbose=False,
|
|
17
|
+
prefix = "!"
|
|
18
|
+
):
|
|
19
|
+
self.__commands = {}
|
|
20
|
+
self.__event_handlers = {
|
|
21
|
+
"message": self.on_message
|
|
22
|
+
}
|
|
23
|
+
self.__aliases = {}
|
|
24
|
+
|
|
25
|
+
self.user = None
|
|
26
|
+
self.prefix = prefix
|
|
27
|
+
|
|
28
|
+
self._token = None
|
|
29
|
+
|
|
30
|
+
self._verbose = verbose
|
|
31
|
+
|
|
32
|
+
self.__cache = {
|
|
33
|
+
"users":{}, "channels":{},
|
|
34
|
+
"guilds": {}, "roles": {}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async def _load_bot_info(self):
|
|
38
|
+
my_info = await API.get(self.session, "/me")
|
|
39
|
+
|
|
40
|
+
return Bot(
|
|
41
|
+
name=my_info["username"],
|
|
42
|
+
display_name=my_info["display_name"],
|
|
43
|
+
id=my_info["id"],
|
|
44
|
+
assets=Assets(
|
|
45
|
+
avatar=my_info["avatar_url"],
|
|
46
|
+
banner=my_info["banner_url"],
|
|
47
|
+
),
|
|
48
|
+
bio=my_info["bio"],
|
|
49
|
+
created_at=my_info["created_at"],
|
|
50
|
+
|
|
51
|
+
owner_id=my_info["bot"]["owner_id"],
|
|
52
|
+
is_bot=True
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
async def _cache_members(
|
|
56
|
+
self, id
|
|
57
|
+
):
|
|
58
|
+
_members_data = (await API.get(
|
|
59
|
+
self.session, f"/servers/{id}/members"
|
|
60
|
+
))["members"]
|
|
61
|
+
|
|
62
|
+
members = []
|
|
63
|
+
|
|
64
|
+
for _user_data in _members_data:
|
|
65
|
+
roles = [
|
|
66
|
+
self.__cache["roles"][role_id]
|
|
67
|
+
for role_id in _user_data["roles"]
|
|
68
|
+
if role_id in self.__cache["roles"]
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
members.append(
|
|
72
|
+
Member(
|
|
73
|
+
bot=self,
|
|
74
|
+
guild=None, roles=roles,
|
|
75
|
+
id=id,
|
|
76
|
+
name = _user_data["username"],
|
|
77
|
+
display_name = _user_data["display_name"],
|
|
78
|
+
badges = _user_data["badges"],
|
|
79
|
+
joined_at = _user_data["joined_at"],
|
|
80
|
+
nickname = _user_data["nickname"],
|
|
81
|
+
assets = Assets(
|
|
82
|
+
avatar = "https://files.yapper.website/avatars/"+_user_data.get("avatar_path",""),
|
|
83
|
+
banner = "https://files.yapper.website/banners/"+_user_data.get("banner_path", "")
|
|
84
|
+
)
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
return members
|
|
89
|
+
|
|
90
|
+
async def _cache_roles(
|
|
91
|
+
self, id
|
|
92
|
+
):
|
|
93
|
+
_roles_data = (await API.get(
|
|
94
|
+
self.session, f"/servers/{id}/roles"
|
|
95
|
+
))
|
|
96
|
+
|
|
97
|
+
roles = []
|
|
98
|
+
|
|
99
|
+
for role in _roles_data["roles"]:
|
|
100
|
+
roles.append(
|
|
101
|
+
r:= Role(
|
|
102
|
+
id = role["id"],
|
|
103
|
+
name = role["name"],
|
|
104
|
+
color = role["color"],
|
|
105
|
+
permissions = Permissions(
|
|
106
|
+
manage_server=role["manage_server"],
|
|
107
|
+
manage_messages=role["manage_messages"],
|
|
108
|
+
**role["perms"]
|
|
109
|
+
),
|
|
110
|
+
auto = role["auto"],
|
|
111
|
+
position = role["position"]
|
|
112
|
+
)
|
|
113
|
+
)
|
|
114
|
+
self.__cache["roles"][id]=r
|
|
115
|
+
return roles
|
|
116
|
+
|
|
117
|
+
def get_role(self, rid):
|
|
118
|
+
return self.__cache["roles"].get(id)
|
|
119
|
+
|
|
120
|
+
async def fetch_guild(
|
|
121
|
+
self, id
|
|
122
|
+
) -> Guild:
|
|
123
|
+
_guild_data = (await API.get(
|
|
124
|
+
self.session, f"/servers/{id}"
|
|
125
|
+
))["server"]
|
|
126
|
+
|
|
127
|
+
roles = await self._cache_roles(id)
|
|
128
|
+
members = await self._cache_members(id)
|
|
129
|
+
|
|
130
|
+
return Guild(
|
|
131
|
+
self,
|
|
132
|
+
id = id,
|
|
133
|
+
name=_guild_data["name"],
|
|
134
|
+
owner=await self.find_user(
|
|
135
|
+
_guild_data["owner_id"]
|
|
136
|
+
),
|
|
137
|
+
member_count = _guild_data["member_count"],
|
|
138
|
+
discoverable = _guild_data["discoverable"],
|
|
139
|
+
created_at = _guild_data["created_at"],
|
|
140
|
+
assets = Assets(
|
|
141
|
+
icon = _guild_data["icon_path"],
|
|
142
|
+
banner = _guild_data["banner_path"]
|
|
143
|
+
),
|
|
144
|
+
roles = roles,
|
|
145
|
+
members = members
|
|
146
|
+
)
|
|
147
|
+
async def find_guild(
|
|
148
|
+
self, id
|
|
149
|
+
):
|
|
150
|
+
guild = self.get_guild(id)
|
|
151
|
+
if guild is None:
|
|
152
|
+
guild = await self.fetch_guild(id)
|
|
153
|
+
self.__cache["guilds"]=guild
|
|
154
|
+
return guild
|
|
155
|
+
|
|
156
|
+
def get_guild(
|
|
157
|
+
self, id
|
|
158
|
+
):
|
|
159
|
+
return self.__cache["guilds"].get(id)
|
|
160
|
+
|
|
161
|
+
async def fetch_channel(
|
|
162
|
+
self, id
|
|
163
|
+
) -> Channel:
|
|
164
|
+
_channel_data = (await API.get(
|
|
165
|
+
self.session, f"/channels/{id}"
|
|
166
|
+
))["channel"]
|
|
167
|
+
|
|
168
|
+
return Channel(
|
|
169
|
+
self,
|
|
170
|
+
id=_channel_data["id"],
|
|
171
|
+
kind=_channel_data["kind"],
|
|
172
|
+
name=_channel_data["name"],
|
|
173
|
+
slowmode=_channel_data["slowmode"],
|
|
174
|
+
private=_channel_data["private"],
|
|
175
|
+
guild=await self.find_guild(
|
|
176
|
+
_channel_data["server_id"]
|
|
177
|
+
),
|
|
178
|
+
read_only=_channel_data["read_only"],
|
|
179
|
+
category_id=_channel_data["category_id"]
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
async def find_channel(
|
|
183
|
+
self, id
|
|
184
|
+
):
|
|
185
|
+
channel = self.get_channel(id)
|
|
186
|
+
if channel is None:
|
|
187
|
+
channel = await self.fetch_channel(id)
|
|
188
|
+
self.__cache["channels"][id] = channel
|
|
189
|
+
return channel
|
|
190
|
+
|
|
191
|
+
def get_channel(
|
|
192
|
+
self, id
|
|
193
|
+
):
|
|
194
|
+
return self.__cache["channels"].get(id)
|
|
195
|
+
|
|
196
|
+
async def fetch_user(
|
|
197
|
+
self,
|
|
198
|
+
id
|
|
199
|
+
) -> User:
|
|
200
|
+
_user_data = (await API.get(
|
|
201
|
+
self.session, f"/users/{id}"
|
|
202
|
+
))["user"]
|
|
203
|
+
|
|
204
|
+
return User(
|
|
205
|
+
bot=self,
|
|
206
|
+
id=id,
|
|
207
|
+
name = _user_data["username"],
|
|
208
|
+
display_name = _user_data["display_name"],
|
|
209
|
+
badges = _user_data["badges"],
|
|
210
|
+
created_at = _user_data["created_at"],
|
|
211
|
+
bio = _user_data["bio"],
|
|
212
|
+
pronouns = _user_data["pronouns"],
|
|
213
|
+
assets = Assets(
|
|
214
|
+
avatar = _user_data["avatar_path"],
|
|
215
|
+
banner = _user_data["banner_path"]
|
|
216
|
+
)
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
async def find_user(
|
|
220
|
+
self,
|
|
221
|
+
id,
|
|
222
|
+
) -> User:
|
|
223
|
+
user = self.get_user(id)
|
|
224
|
+
|
|
225
|
+
if user is None:
|
|
226
|
+
user = await self.fetch_user(
|
|
227
|
+
id
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
self.__cache["users"][id]=user
|
|
231
|
+
return user
|
|
232
|
+
|
|
233
|
+
def get_user(
|
|
234
|
+
self, id
|
|
235
|
+
):
|
|
236
|
+
return self.__cache["users"].get(id)
|
|
237
|
+
|
|
238
|
+
async def _build_context(self, ev):
|
|
239
|
+
d = ev["data"]
|
|
240
|
+
|
|
241
|
+
channel = await self.find_channel(d["channel_id"])
|
|
242
|
+
|
|
243
|
+
return Context(
|
|
244
|
+
self,
|
|
245
|
+
is_bot=ev.get("author_bot", False),
|
|
246
|
+
message=Message(
|
|
247
|
+
self,
|
|
248
|
+
id=d["id"],
|
|
249
|
+
author=await channel.guild.find_member(
|
|
250
|
+
d["author_id"]
|
|
251
|
+
),
|
|
252
|
+
channel=channel,
|
|
253
|
+
created_at=d["created_at"],
|
|
254
|
+
content=d["content"]
|
|
255
|
+
),
|
|
256
|
+
channel=channel
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
async def _parse_arguments(self, content):
|
|
260
|
+
return await parser.parse_args(self, content)
|
|
261
|
+
|
|
262
|
+
async def _handle_prefix_command(self, ctx):
|
|
263
|
+
if ctx.message.content.startswith(self.prefix):
|
|
264
|
+
if " " in ctx.message.content:
|
|
265
|
+
name, raw = ctx.message.content.removeprefix(self.prefix).split(" ", maxsplit=1)
|
|
266
|
+
else:
|
|
267
|
+
name = ctx.message.content.removeprefix(self.prefix)
|
|
268
|
+
raw = ""
|
|
269
|
+
arguments = await self._parse_arguments(
|
|
270
|
+
raw
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
cmd = None
|
|
274
|
+
if name in self.__commands:
|
|
275
|
+
cmd = self.__commands[name.lower()]
|
|
276
|
+
elif name in self.__aliases:
|
|
277
|
+
cmd = self.__aliases[name.lower()]
|
|
278
|
+
else:
|
|
279
|
+
raise UnknownCommand(
|
|
280
|
+
f"Unhandled command '{name.lower()}'!"
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
await cmd.func(
|
|
284
|
+
ctx, *arguments
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
return True
|
|
288
|
+
return False
|
|
289
|
+
|
|
290
|
+
async def process_command(self, ctx):
|
|
291
|
+
await self._handle_prefix_command(
|
|
292
|
+
ctx
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
async def on_message(self, ctx):
|
|
296
|
+
if ctx.is_bot: return
|
|
297
|
+
await process_command(ctx)
|
|
298
|
+
|
|
299
|
+
async def _handle_event(self, session: aiohttp.ClientSession, ev):
|
|
300
|
+
d = ev["data"]
|
|
301
|
+
|
|
302
|
+
if ev["type"] in self.__event_handlers:
|
|
303
|
+
await self.__event_handlers[ev["type"]](
|
|
304
|
+
await self._build_context(
|
|
305
|
+
ev
|
|
306
|
+
)
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
async def _init_commands(self):
|
|
310
|
+
_commands = []
|
|
311
|
+
for name, func in self.__commands.items():
|
|
312
|
+
_commands.append(
|
|
313
|
+
{"name": name, "description": func.description}
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
await API.put(
|
|
317
|
+
self.session,
|
|
318
|
+
"/commands",
|
|
319
|
+
{"commands": _commands}
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
async def _event_loop(self):
|
|
323
|
+
async with aiohttp.ClientSession() as self.session:
|
|
324
|
+
self.session._bearer_token=self._token
|
|
325
|
+
|
|
326
|
+
self.user = await self._load_bot_info()
|
|
327
|
+
await self._init_commands()
|
|
328
|
+
|
|
329
|
+
events_init = await API.get(self.session, "/events?after=0&wait=0")
|
|
330
|
+
last = events_init["last"]
|
|
331
|
+
fails = 0
|
|
332
|
+
|
|
333
|
+
if "ready" in self.__event_handlers:
|
|
334
|
+
await self.__event_handlers["ready"]()
|
|
335
|
+
|
|
336
|
+
while True:
|
|
337
|
+
try:
|
|
338
|
+
out = await API.get(self.session, f"/events?after={last}&wait=25")
|
|
339
|
+
fails = 0
|
|
340
|
+
except Exception as e:
|
|
341
|
+
fails += 1
|
|
342
|
+
if "(401)" in str(e):
|
|
343
|
+
raise ImproperToken("Please check if your token is correctly used!")
|
|
344
|
+
wait = min(30, 2 ** fails)
|
|
345
|
+
|
|
346
|
+
print(f"retrying in {wait}s:", e)
|
|
347
|
+
|
|
348
|
+
await asyncio.sleep(wait)
|
|
349
|
+
continue
|
|
350
|
+
for ev in out["events"]:
|
|
351
|
+
if ev["id"] <= last:
|
|
352
|
+
continue
|
|
353
|
+
last = ev["id"]
|
|
354
|
+
try:
|
|
355
|
+
await self._handle_event(self.session, ev)
|
|
356
|
+
except Exception as e:
|
|
357
|
+
traceback.print_exc()
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def event(self, event_type: str):
|
|
361
|
+
def wrapper(func):
|
|
362
|
+
if not inspect.iscoroutinefunction(func):
|
|
363
|
+
raise InvalidSignature(f"Expected event handler for '{event_type}' to be a coroutine!")
|
|
364
|
+
self.__event_handlers[
|
|
365
|
+
event_type
|
|
366
|
+
] = func
|
|
367
|
+
return wrapper
|
|
368
|
+
|
|
369
|
+
def command(
|
|
370
|
+
self,
|
|
371
|
+
name, *,
|
|
372
|
+
aliases=[],
|
|
373
|
+
description=""
|
|
374
|
+
):
|
|
375
|
+
def wrapper(
|
|
376
|
+
func
|
|
377
|
+
):
|
|
378
|
+
if not inspect.iscoroutinefunction(func):
|
|
379
|
+
raise InvalidSignature(f"Expected command handler for '{name}' to be a coroutine!")
|
|
380
|
+
self.__commands[name.lower()]=Command(
|
|
381
|
+
name=name,
|
|
382
|
+
aliases=aliases,
|
|
383
|
+
description=description,
|
|
384
|
+
func=func
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
for alias in aliases:
|
|
388
|
+
self.__aliases[alias.lower()] = self.__commands[name.lower()]
|
|
389
|
+
return wrapper
|
|
390
|
+
|
|
391
|
+
def run(self, token: str):
|
|
392
|
+
self._token = token
|
|
393
|
+
|
|
394
|
+
self._yappy_loop = asyncio.new_event_loop()
|
|
395
|
+
asyncio.set_event_loop(self._yappy_loop)
|
|
396
|
+
|
|
397
|
+
try:
|
|
398
|
+
self._yappy_loop.run_until_complete(self._event_loop())
|
|
399
|
+
finally:
|
|
400
|
+
self._yappy_loop.close()
|
|
401
|
+
__all__=["App"]
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
from .api import *
|
|
2
|
+
from typing import Callable
|
|
3
|
+
import urllib.parse
|
|
4
|
+
from .utils import get_asset_url
|
|
5
|
+
|
|
6
|
+
class Assets:
|
|
7
|
+
def __init__(self,
|
|
8
|
+
avatar: str = "",
|
|
9
|
+
banner: str = "",
|
|
10
|
+
icon: str = ""
|
|
11
|
+
):
|
|
12
|
+
self.avatar = get_asset_url("avatar", avatar)
|
|
13
|
+
self.banner = get_asset_url("banner", banner)
|
|
14
|
+
self.icon = get_asset_url("icon", icon)
|
|
15
|
+
|
|
16
|
+
class User:
|
|
17
|
+
def __init__(self,
|
|
18
|
+
bot = None,
|
|
19
|
+
name: str ="",
|
|
20
|
+
display_name: str = "",
|
|
21
|
+
id=0,
|
|
22
|
+
assets: Assets | None = None,
|
|
23
|
+
created_at: str = "",
|
|
24
|
+
bio: str = "",
|
|
25
|
+
pronouns: str = "",
|
|
26
|
+
badges: list = [],
|
|
27
|
+
is_bot: bool = True
|
|
28
|
+
):
|
|
29
|
+
self.bot = bot
|
|
30
|
+
self.name = name
|
|
31
|
+
self.id = id
|
|
32
|
+
self.assets = assets
|
|
33
|
+
self.display_name = display_name or name
|
|
34
|
+
self.is_bot = is_bot
|
|
35
|
+
self.bio = bio
|
|
36
|
+
|
|
37
|
+
class Bot(User):
|
|
38
|
+
def __init__(self, owner_id=None, **kwargs):
|
|
39
|
+
super().__init__(**kwargs)
|
|
40
|
+
self.owner_id = owner_id
|
|
41
|
+
|
|
42
|
+
class Command:
|
|
43
|
+
def __init__(self,
|
|
44
|
+
name: str,
|
|
45
|
+
aliases: list[str] = [],
|
|
46
|
+
description: str = "",
|
|
47
|
+
func: Callable = None,
|
|
48
|
+
):
|
|
49
|
+
self.name = name
|
|
50
|
+
self.aliases = aliases
|
|
51
|
+
self.description = description
|
|
52
|
+
self.func = func
|
|
53
|
+
|
|
54
|
+
class Permissions:
|
|
55
|
+
def __init__(self, **perms):
|
|
56
|
+
self._perms = perms
|
|
57
|
+
def has_perm(self, perm):
|
|
58
|
+
return self._perms.get(perm, False)
|
|
59
|
+
|
|
60
|
+
class Role:
|
|
61
|
+
def __init__(self,
|
|
62
|
+
id: str = "",
|
|
63
|
+
name: str = "",
|
|
64
|
+
color: str = "",
|
|
65
|
+
permissions: Permissions | None = None,
|
|
66
|
+
auto: None = None, # unused
|
|
67
|
+
position: int = 0,
|
|
68
|
+
|
|
69
|
+
):
|
|
70
|
+
self.id = id
|
|
71
|
+
self.name = name
|
|
72
|
+
self.color = color
|
|
73
|
+
self.permissions = permissions
|
|
74
|
+
self.auto = auto
|
|
75
|
+
self.position = position
|
|
76
|
+
|
|
77
|
+
class Guild:
|
|
78
|
+
def __init__(self,
|
|
79
|
+
bot,
|
|
80
|
+
id: str = "",
|
|
81
|
+
name: str = "",
|
|
82
|
+
created_at: str = "",
|
|
83
|
+
discoverable: bool = True,
|
|
84
|
+
member_count: int = 1,
|
|
85
|
+
owner: User | None = None,
|
|
86
|
+
assets: Assets | None = None,
|
|
87
|
+
roles: list[Role] = [],
|
|
88
|
+
members: list[Member] = []
|
|
89
|
+
):
|
|
90
|
+
self.id = id
|
|
91
|
+
self.name = name
|
|
92
|
+
self.created_at = created_at
|
|
93
|
+
self.member_count = member_count
|
|
94
|
+
self.owner = owner
|
|
95
|
+
self.assets = assets
|
|
96
|
+
self.discoverable = discoverable
|
|
97
|
+
self.roles = roles
|
|
98
|
+
self.bot = bot
|
|
99
|
+
|
|
100
|
+
self.members = members
|
|
101
|
+
|
|
102
|
+
self._cache={"members": {}, "roles": {
|
|
103
|
+
r.id: r for r in roles
|
|
104
|
+
}}
|
|
105
|
+
|
|
106
|
+
for m in members:
|
|
107
|
+
self._cache[m.id] = m
|
|
108
|
+
m. guild = self
|
|
109
|
+
async def find_member(self, mid):
|
|
110
|
+
member = self.get_member(mid)
|
|
111
|
+
if member is None:
|
|
112
|
+
_user_data = (await API.get(
|
|
113
|
+
self.bot.session,
|
|
114
|
+
f"/servers/{self.id}/members/{mid}"
|
|
115
|
+
))["member"]
|
|
116
|
+
|
|
117
|
+
roles = [
|
|
118
|
+
self._cache["roles"][role_id]
|
|
119
|
+
for role_id in _user_data["roles"]
|
|
120
|
+
if role_id in self._cache["roles"]
|
|
121
|
+
]
|
|
122
|
+
|
|
123
|
+
member = Member(
|
|
124
|
+
bot=self.bot,
|
|
125
|
+
guild=self, roles=roles,
|
|
126
|
+
id=id,
|
|
127
|
+
name = _user_data["username"],
|
|
128
|
+
display_name = _user_data["display_name"],
|
|
129
|
+
badges = _user_data["badges"],
|
|
130
|
+
joined_at = _user_data["joined_at"],
|
|
131
|
+
nickname = _user_data["nickname"],
|
|
132
|
+
assets = Assets(
|
|
133
|
+
avatar = "https://files.yapper.website/avatars/"+_user_data.get("avatar_path",""),
|
|
134
|
+
banner = "https://files.yapper.website/banners/"+_user_data.get("banner_path", "")
|
|
135
|
+
),
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
self._cache["members"][mid] = member
|
|
139
|
+
|
|
140
|
+
return member
|
|
141
|
+
def get_member(self, mid):
|
|
142
|
+
return self._cache.get(mid, None)
|
|
143
|
+
|
|
144
|
+
class Channel:
|
|
145
|
+
def __init__(self,
|
|
146
|
+
bot,
|
|
147
|
+
id=0,
|
|
148
|
+
name: str = "",
|
|
149
|
+
kind: str = "",
|
|
150
|
+
private: bool = False,
|
|
151
|
+
slowmode: int = 0,
|
|
152
|
+
category_id: str = "",
|
|
153
|
+
guild: Guild | None = None,
|
|
154
|
+
read_only: bool = False
|
|
155
|
+
):
|
|
156
|
+
self.id = id
|
|
157
|
+
self.bot = bot
|
|
158
|
+
self.name = name
|
|
159
|
+
self.kind = kind
|
|
160
|
+
self.private = private
|
|
161
|
+
self.slowmode = slowmode
|
|
162
|
+
self.category_id = category_id
|
|
163
|
+
self.guild = guild
|
|
164
|
+
self.read_only = read_only
|
|
165
|
+
|
|
166
|
+
async def send(self, content, embeds=[], components=[]):
|
|
167
|
+
await API.send(
|
|
168
|
+
self.bot.session, self.id, {
|
|
169
|
+
"content": content,
|
|
170
|
+
"embeds": embeds,
|
|
171
|
+
"components": components,
|
|
172
|
+
"reply_to": None
|
|
173
|
+
}
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
class Member(User):
|
|
177
|
+
def __init__(
|
|
178
|
+
self,
|
|
179
|
+
guild: guild | None = None,
|
|
180
|
+
roles: list[Role] = [],
|
|
181
|
+
joined_at: str = "",
|
|
182
|
+
nickname: str = "",
|
|
183
|
+
*args, **kwargs
|
|
184
|
+
):
|
|
185
|
+
super().__init__(*args, **kwargs)
|
|
186
|
+
self.guild = guild
|
|
187
|
+
self.roles = roles
|
|
188
|
+
self.joined_at = joined_at
|
|
189
|
+
self.nickname = nickname or self.display_name
|
|
190
|
+
|
|
191
|
+
async def add_role(self, role):
|
|
192
|
+
role_id = role if isinstance(role, str) else role.id
|
|
193
|
+
return API.put(self.bot.session, f"/servers/{self.guild.id}/members/{self.id}/roles/{role_id}")
|
|
194
|
+
|
|
195
|
+
async def remove_role(self, role):
|
|
196
|
+
role_id = role if isinstance(role, str) else role.id
|
|
197
|
+
return API.delete(self.bot.session, f"/servers/{self.guild.id}/members/{self.id}/roles/{role_id}")
|
|
198
|
+
|
|
199
|
+
async def kick(self):
|
|
200
|
+
return API.post(self.bot.session, f"/servers/{self.guild.id}/", {"user_id": self.id})
|
|
201
|
+
|
|
202
|
+
async def ban(self):
|
|
203
|
+
return API.post(self.bot.session, f"/servers/{self.guild.id}/bans", {"user_id": self.id})
|
|
204
|
+
|
|
205
|
+
async def unban(self):
|
|
206
|
+
return API.delete(self.bot.session, f"/servers/{self.guild.id}/bans/{self.id}")
|
|
207
|
+
|
|
208
|
+
async def timeout(self, minutes, reason=""):
|
|
209
|
+
return API.post(self.bot.session, f"/servers/{self.guild.id}/timeouts", {"user_id": self._id, "minutes": minutes, "reason": reason})
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class Message:
|
|
213
|
+
def __init__(self,
|
|
214
|
+
bot,
|
|
215
|
+
id=0,
|
|
216
|
+
channel: Channel | None = None,
|
|
217
|
+
author: User | None = None,
|
|
218
|
+
created_at: str = "",
|
|
219
|
+
content: str = ""
|
|
220
|
+
):
|
|
221
|
+
self.id = id
|
|
222
|
+
self.channel = channel
|
|
223
|
+
self.bot = bot
|
|
224
|
+
self.created_at = created_at
|
|
225
|
+
self.content = content
|
|
226
|
+
|
|
227
|
+
self.author = author
|
|
228
|
+
async def reply(self, content, embeds=[], components=[]):
|
|
229
|
+
await API.send(
|
|
230
|
+
self.bot.session, self.channel.id, {
|
|
231
|
+
"content": content,
|
|
232
|
+
"embeds": embeds,
|
|
233
|
+
"components": components,
|
|
234
|
+
"reply_to": self.id
|
|
235
|
+
}
|
|
236
|
+
)
|
|
237
|
+
async def delete(self):
|
|
238
|
+
await API.delete(self.bot.session, f"/channels/{self.channel.id}/messages/{self.id}")
|
|
239
|
+
async def edit(self, content, embeds=[], components=[]):
|
|
240
|
+
await API.patch(self.bot.session, f"/channels/{self.channels.id}/messages/{self.id}", {
|
|
241
|
+
"content": content,
|
|
242
|
+
"embeds": embeds,
|
|
243
|
+
"components": components
|
|
244
|
+
})
|
|
245
|
+
async def send(self, content, embeds=[], components=[]):
|
|
246
|
+
await self.channel.send(*args, **kwargs)
|
|
247
|
+
async def react(self, emoji):
|
|
248
|
+
return await API.put(
|
|
249
|
+
self.bot.session,
|
|
250
|
+
f"/channels/{self.channel.id}/messages/{self.id}/reactions/{urllib.parse.quote(emoji)}"
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class Context:
|
|
255
|
+
def __init__(self,
|
|
256
|
+
bot,
|
|
257
|
+
is_bot: bool = False,
|
|
258
|
+
message: Message | None = None,
|
|
259
|
+
channel: Channel | None = None,
|
|
260
|
+
author: Member | None = None
|
|
261
|
+
):
|
|
262
|
+
self.bot = bot
|
|
263
|
+
self.is_bot = is_bot
|
|
264
|
+
self.message = message
|
|
265
|
+
self.channel = channel
|
|
266
|
+
self.author = author
|
|
267
|
+
|
|
268
|
+
self.reply = self.message.reply
|
|
269
|
+
self.delete = self.message.delete
|
|
270
|
+
self.edit = self.message.edit
|
|
271
|
+
self.react = self.message.react
|
|
272
|
+
self.send = self.message.send
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Any, List, Union
|
|
3
|
+
|
|
4
|
+
class CommandParser:
|
|
5
|
+
def __init__(self):
|
|
6
|
+
self.token_regex = re.compile(
|
|
7
|
+
r'"([^"\\]*(?:\\.[^"\\]*)*)"|'
|
|
8
|
+
r"'([^'\\]*(?:\\.[^'\\]*)*)'|"
|
|
9
|
+
r'(<[#@&!]+[^\s>]+>)|'
|
|
10
|
+
r'([^\s"\']+)'
|
|
11
|
+
)
|
|
12
|
+
self.user_regex = re.compile(r'^<@!?([0-9\-]+)>$|^<@!(.+)>$')
|
|
13
|
+
self.channel_regex = re.compile(r'^<#([0-9\-]+)>$|^<#(.+)>$')
|
|
14
|
+
self.role_regex = re.compile(r'^<@&([0-9\-]+)>$|^<@&(.+)>$')
|
|
15
|
+
|
|
16
|
+
async def parse_args(self, bot, text: str) -> List[Any]:
|
|
17
|
+
parsed_arguments = []
|
|
18
|
+
|
|
19
|
+
for match in self.token_regex.finditer(text):
|
|
20
|
+
dq_str, sq_str, _raw, unquoted = match.groups()
|
|
21
|
+
|
|
22
|
+
if dq_str is not None:
|
|
23
|
+
parsed_arguments.append(dq_str.encode().decode('unicode_escape'))
|
|
24
|
+
continue
|
|
25
|
+
if sq_str is not None:
|
|
26
|
+
parsed_arguments.append(sq_str.encode().decode('unicode_escape'))
|
|
27
|
+
continue
|
|
28
|
+
|
|
29
|
+
if _raw is not None:
|
|
30
|
+
parsed_arguments.append(await self._process_mentions(bot, _raw))
|
|
31
|
+
continue
|
|
32
|
+
|
|
33
|
+
if unquoted is not None:
|
|
34
|
+
parsed_arguments.append(self._parse_primitive(unquoted))
|
|
35
|
+
|
|
36
|
+
return parsed_arguments
|
|
37
|
+
|
|
38
|
+
async def _process_mentions(self, bot, token: str) -> Any:
|
|
39
|
+
if user_match := self.user_regex.match(token):
|
|
40
|
+
user_id = user_match.group(1) or user_match.group(2)
|
|
41
|
+
return await self._parse_user(bot, user_id)
|
|
42
|
+
|
|
43
|
+
if channel_match := self.channel_regex.match(token):
|
|
44
|
+
channel_id = channel_match.group(1) or channel_match.group(2)
|
|
45
|
+
return await self._parse_channel(bot, channel_id)
|
|
46
|
+
|
|
47
|
+
if role_match := self.role_regex.match(token):
|
|
48
|
+
role_id = role_match.group(1) or role_match.group(2)
|
|
49
|
+
return await self._parse_role(bot, role_id)
|
|
50
|
+
|
|
51
|
+
return token
|
|
52
|
+
|
|
53
|
+
def _parse_primitive(self, token: str) -> Union[int, float, str]:
|
|
54
|
+
if re.match(r'^-?\d+$', token):
|
|
55
|
+
return int(token)
|
|
56
|
+
if re.match(r'^-?\d*\.\d+$', token):
|
|
57
|
+
return float(token)
|
|
58
|
+
return token
|
|
59
|
+
|
|
60
|
+
async def _parse_user(self, bot, user_id: str):
|
|
61
|
+
return await bot.find_user(user_id)
|
|
62
|
+
|
|
63
|
+
async def _parse_channel(self, bot, channel_id: str):
|
|
64
|
+
return await bot.find_channel(channel_id)
|
|
65
|
+
|
|
66
|
+
async def _parse_role(self, bot, role_id: str):
|
|
67
|
+
return await bot.find_role(role_id)
|
|
68
|
+
|
|
69
|
+
parser = CommandParser()
|
|
@@ -1,204 +0,0 @@
|
|
|
1
|
-
from .core import *
|
|
2
|
-
from .consts import BASE_URL
|
|
3
|
-
from .api import *
|
|
4
|
-
from .errors import ImproperToken, InvalidSignature
|
|
5
|
-
|
|
6
|
-
import inspect
|
|
7
|
-
import asyncio
|
|
8
|
-
import aiohttp
|
|
9
|
-
from functools import wraps
|
|
10
|
-
from typing import Callable
|
|
11
|
-
|
|
12
|
-
class App:
|
|
13
|
-
def __init__(self, verbose=False):
|
|
14
|
-
self.__commands = {}
|
|
15
|
-
self.__event_handlers = {}
|
|
16
|
-
|
|
17
|
-
self.user = None
|
|
18
|
-
self._token = None
|
|
19
|
-
|
|
20
|
-
self._verbose = verbose
|
|
21
|
-
|
|
22
|
-
self.__cache = {"users":{}}
|
|
23
|
-
|
|
24
|
-
async def _load_bot_info(self):
|
|
25
|
-
my_info = await api.get(self.session, "/me")
|
|
26
|
-
|
|
27
|
-
if self._verbose:
|
|
28
|
-
print(my_info)
|
|
29
|
-
|
|
30
|
-
return Bot(
|
|
31
|
-
name=my_info["username"],
|
|
32
|
-
display_name=my_info["display_name"],
|
|
33
|
-
id=my_info["id"],
|
|
34
|
-
profile=Profile(
|
|
35
|
-
avatar=my_info["avatar_url"],
|
|
36
|
-
banner=my_info["banner_url"],
|
|
37
|
-
),
|
|
38
|
-
bio=my_info["bio"],
|
|
39
|
-
created_at=my_info["created_at"],
|
|
40
|
-
|
|
41
|
-
owner_id=my_info["bot"]["owner_id"],
|
|
42
|
-
is_bot=True
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
def get_user(
|
|
46
|
-
self,
|
|
47
|
-
id,
|
|
48
|
-
_data
|
|
49
|
-
):
|
|
50
|
-
if id in self.__cache["users"]:
|
|
51
|
-
return self.__cache["users"]
|
|
52
|
-
user = User(
|
|
53
|
-
name=_data["author"],
|
|
54
|
-
id=id
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
self.__cache[id]=user
|
|
58
|
-
return user
|
|
59
|
-
|
|
60
|
-
def _build_context(self, ev):
|
|
61
|
-
d = ev["data"]
|
|
62
|
-
|
|
63
|
-
channel=Channel(
|
|
64
|
-
self,
|
|
65
|
-
id=d["channel_id"],
|
|
66
|
-
name=d["channel_name"]
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
return Context(
|
|
70
|
-
self,
|
|
71
|
-
is_bot=ev.get("author_bot", False),
|
|
72
|
-
message=Message(
|
|
73
|
-
self,
|
|
74
|
-
id=d["id"],
|
|
75
|
-
author=self.get_user(
|
|
76
|
-
d["author_id"], d
|
|
77
|
-
),
|
|
78
|
-
channel=channel,
|
|
79
|
-
created_at=d["created_at"],
|
|
80
|
-
content=d["content"]
|
|
81
|
-
),
|
|
82
|
-
channel=channel
|
|
83
|
-
)
|
|
84
|
-
|
|
85
|
-
async def _handle_event(self, session: aiohttp.ClientSession, ev):
|
|
86
|
-
if self._verbose:
|
|
87
|
-
print(ev)
|
|
88
|
-
d = ev["data"]
|
|
89
|
-
|
|
90
|
-
if ev["type"] in self.__event_handlers:
|
|
91
|
-
if (cmd_name:=d.get("command")):
|
|
92
|
-
if cmd_name in self.__commands:
|
|
93
|
-
await self.__commands[cmd_name].func(
|
|
94
|
-
self._build_context(
|
|
95
|
-
ev
|
|
96
|
-
)
|
|
97
|
-
)
|
|
98
|
-
elif cmd_name in self.__aliases:
|
|
99
|
-
await self.__aliases[cmd_name](
|
|
100
|
-
self._build_context(
|
|
101
|
-
ev
|
|
102
|
-
)
|
|
103
|
-
)
|
|
104
|
-
else:
|
|
105
|
-
raise UnknownCommand(
|
|
106
|
-
f"Unhandled command '{cmd_name}'!"
|
|
107
|
-
)
|
|
108
|
-
else:
|
|
109
|
-
await self.__event_handlers[ev["type"]](
|
|
110
|
-
self._build_context(
|
|
111
|
-
ev
|
|
112
|
-
)
|
|
113
|
-
)
|
|
114
|
-
|
|
115
|
-
async def _init_commands(self):
|
|
116
|
-
_commands = []
|
|
117
|
-
for name, func in self.__commands.items():
|
|
118
|
-
_commands.append(
|
|
119
|
-
{"name": name, "description": func.description}
|
|
120
|
-
)
|
|
121
|
-
|
|
122
|
-
await api.put(
|
|
123
|
-
self.session,
|
|
124
|
-
"/commands",
|
|
125
|
-
{"commands": _commands}
|
|
126
|
-
)
|
|
127
|
-
|
|
128
|
-
async def _event_loop(self):
|
|
129
|
-
async with aiohttp.ClientSession() as self.session:
|
|
130
|
-
self.session._bearer_token=self._token
|
|
131
|
-
|
|
132
|
-
self.user = await self._load_bot_info()
|
|
133
|
-
await self._init_commands()
|
|
134
|
-
|
|
135
|
-
events_init = await api.get(self.session, "/events?after=0&wait=0")
|
|
136
|
-
last = events_init["last"]
|
|
137
|
-
fails = 0
|
|
138
|
-
|
|
139
|
-
while True:
|
|
140
|
-
try:
|
|
141
|
-
out = await api.get(self.session, f"/events?after={last}&wait=25")
|
|
142
|
-
fails = 0
|
|
143
|
-
except Exception as e:
|
|
144
|
-
fails += 1
|
|
145
|
-
if "(401)" in str(e):
|
|
146
|
-
raise ImproperToken("Please check if your token is correctly used!")
|
|
147
|
-
wait = min(30, 2 ** fails)
|
|
148
|
-
|
|
149
|
-
print(f"retrying in {wait}s:", e)
|
|
150
|
-
|
|
151
|
-
await asyncio.sleep(wait)
|
|
152
|
-
continue
|
|
153
|
-
for ev in out["events"]:
|
|
154
|
-
if ev["id"] <= last:
|
|
155
|
-
continue
|
|
156
|
-
last = ev["id"]
|
|
157
|
-
try:
|
|
158
|
-
await self._handle_event(self.session, ev)
|
|
159
|
-
except Exception as e:
|
|
160
|
-
print("error handling", ev["type"], "event:", e)
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
def event(self, event_type: str):
|
|
164
|
-
def wrapper(func):
|
|
165
|
-
if not inspect.iscoroutinefunction(func):
|
|
166
|
-
raise InvalidSignature(f"Expected event handler for '{event_type}' to be a coroutine!")
|
|
167
|
-
self.__event_handlers[
|
|
168
|
-
event_type
|
|
169
|
-
] = func
|
|
170
|
-
return wrapper
|
|
171
|
-
|
|
172
|
-
def command(
|
|
173
|
-
self,
|
|
174
|
-
name, *,
|
|
175
|
-
aliases=[],
|
|
176
|
-
description=""
|
|
177
|
-
):
|
|
178
|
-
def wrapper(
|
|
179
|
-
func
|
|
180
|
-
):
|
|
181
|
-
if not inspect.iscoroutinefunction(func):
|
|
182
|
-
raise InvalidSignature(f"Expected command handler for '{name}' to be a coroutine!")
|
|
183
|
-
self.__commands[name]=Command(
|
|
184
|
-
name=name,
|
|
185
|
-
aliases=aliases,
|
|
186
|
-
description=description,
|
|
187
|
-
func=func
|
|
188
|
-
)
|
|
189
|
-
|
|
190
|
-
for alias in aliases:
|
|
191
|
-
self.__aliases[alias] = self.__commands[name]
|
|
192
|
-
|
|
193
|
-
return wrapper
|
|
194
|
-
|
|
195
|
-
def run(self, token: str):
|
|
196
|
-
self._token = token
|
|
197
|
-
|
|
198
|
-
self._yappy_loop = asyncio.new_event_loop()
|
|
199
|
-
asyncio.set_event_loop(self._yappy_loop)
|
|
200
|
-
|
|
201
|
-
try:
|
|
202
|
-
self._yappy_loop.run_until_complete(self._event_loop())
|
|
203
|
-
finally:
|
|
204
|
-
self._yappy_loop.close()
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
from .api import *
|
|
2
|
-
from typing import Callable
|
|
3
|
-
|
|
4
|
-
class Profile:
|
|
5
|
-
def __init__(self,
|
|
6
|
-
avatar: str = "",
|
|
7
|
-
banner: str = ""
|
|
8
|
-
):
|
|
9
|
-
self.avatar = avatar
|
|
10
|
-
self.banner = banner
|
|
11
|
-
|
|
12
|
-
class User:
|
|
13
|
-
def __init__(self,
|
|
14
|
-
name: str ="",
|
|
15
|
-
display_name: str = "",
|
|
16
|
-
id=0,
|
|
17
|
-
profile: Profile | None = None,
|
|
18
|
-
created_at: str = "",
|
|
19
|
-
bio: str = "",
|
|
20
|
-
is_bot: bool = True
|
|
21
|
-
):
|
|
22
|
-
self.name = name
|
|
23
|
-
self.id = id
|
|
24
|
-
self.profile = profile
|
|
25
|
-
self.display_name = display_name
|
|
26
|
-
self.is_bot = is_bot
|
|
27
|
-
self.bio = bio
|
|
28
|
-
|
|
29
|
-
class Bot(User):
|
|
30
|
-
def __init__(self, owner_id=None, **kwargs):
|
|
31
|
-
super().__init__(**kwargs)
|
|
32
|
-
self.owner_id = owner_id
|
|
33
|
-
|
|
34
|
-
class Command:
|
|
35
|
-
def __init__(self,
|
|
36
|
-
name: str,
|
|
37
|
-
aliases: list[str] = [],
|
|
38
|
-
description: str = "",
|
|
39
|
-
func: Callable = None,
|
|
40
|
-
):
|
|
41
|
-
self.name = name
|
|
42
|
-
self.aliases = aliases
|
|
43
|
-
self.description = description
|
|
44
|
-
self.func = func
|
|
45
|
-
|
|
46
|
-
class Channel:
|
|
47
|
-
def __init__(self,
|
|
48
|
-
bot,
|
|
49
|
-
id=0,
|
|
50
|
-
name: str = ""
|
|
51
|
-
):
|
|
52
|
-
self.id = id
|
|
53
|
-
self.bot = bot
|
|
54
|
-
self.name = name
|
|
55
|
-
|
|
56
|
-
class Message:
|
|
57
|
-
def __init__(self,
|
|
58
|
-
bot,
|
|
59
|
-
id=0,
|
|
60
|
-
channel: Channel | None = None,
|
|
61
|
-
author: User | None = None,
|
|
62
|
-
created_at: str = "",
|
|
63
|
-
content: str = ""
|
|
64
|
-
):
|
|
65
|
-
self.id = id
|
|
66
|
-
self.channel = channel
|
|
67
|
-
self.bot = bot
|
|
68
|
-
self.created_at = created_at
|
|
69
|
-
self.content = content
|
|
70
|
-
|
|
71
|
-
self.author = author
|
|
72
|
-
|
|
73
|
-
class Context:
|
|
74
|
-
def __init__(self,
|
|
75
|
-
bot,
|
|
76
|
-
is_bot: bool = False,
|
|
77
|
-
message: Message | None = None,
|
|
78
|
-
channel: Channel | None = None
|
|
79
|
-
):
|
|
80
|
-
self.bot = bot
|
|
81
|
-
self.is_bot = is_bot
|
|
82
|
-
self.message = message
|
|
83
|
-
self.channel = channel
|
|
84
|
-
|
|
85
|
-
async def reply(
|
|
86
|
-
self,
|
|
87
|
-
content: str = "Hi"
|
|
88
|
-
):
|
|
89
|
-
await api.send(
|
|
90
|
-
self.bot.session, self.channel.id, data={
|
|
91
|
-
"content": content,
|
|
92
|
-
"reply_to": self.message.id
|
|
93
|
-
}
|
|
94
|
-
)
|
|
95
|
-
|
|
96
|
-
async def send(
|
|
97
|
-
self,
|
|
98
|
-
content: str = "Hi"
|
|
99
|
-
):
|
|
100
|
-
await api.send(
|
|
101
|
-
self.bot.session, self.channel.id, data={
|
|
102
|
-
"content": content,
|
|
103
|
-
"reply_to": None
|
|
104
|
-
}
|
|
105
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|