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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: python-yappy
3
- Version: 0.1.0
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
- Create a file named `bot.py`:
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
- @bot.event("message")
47
+ @bot.event(
48
+ "message"
49
+ )
67
50
  async def on_message(ctx):
68
- if ctx.is_bot:
69
- return
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
- name="ping",
76
- description="replies with 'Pong!'",
55
+ name="ping",
56
+ description="replies with 'Pong!'"
77
57
  )
78
58
  async def ping(ctx):
79
- await ctx.reply("Pong!")
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.reply("Hello!")
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
- Create a file named `bot.py`:
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
- @bot.event("message")
36
+ @bot.event(
37
+ "message"
38
+ )
56
39
  async def on_message(ctx):
57
- if ctx.is_bot:
58
- return
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
- name="ping",
65
- description="replies with 'Pong!'",
44
+ name="ping",
45
+ description="replies with 'Pong!'"
66
46
  )
67
47
  async def ping(ctx):
68
- await ctx.reply("Pong!")
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.reply("Hello!")
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,6 +1,6 @@
1
1
  [project]
2
2
  name = "python-yappy"
3
- version = "0.1.0"
3
+ version = "0.1.1"
4
4
  description = "API Bindings for Yapper!"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.14"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "python-yappy"
3
- version = "0.1.0"
3
+ version = "0.1.1"
4
4
  description = "API Bindings for Yapper!"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -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 api(session, method, path, body, tries - 1)
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 api:
33
+
34
+ class API:
33
35
  @staticmethod
34
- async def put(session, path="", body=None, tries=3):
35
- await api_push(session, "PUT", path, body, tries)
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 api_push(session, "GET", path, body, tries)
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 api_push(session, "POST", path, body, tries)
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 send(session, channel_id=0, data=None, tries=3):
44
- await api_push(session, "POST", f"/channels/{channel_id}/messages", data, tries)
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()
@@ -0,0 +1,4 @@
1
+ def get_asset_url(type: str, path: str):
2
+ if not path: return None
3
+
4
+ return f"https://files.yapper.website/{type}s/{path}"
@@ -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
- )