yapper-bot 0.1.0__tar.gz → 0.2.0__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.
- {yapper_bot-0.1.0 → yapper_bot-0.2.0}/PKG-INFO +10 -4
- {yapper_bot-0.1.0 → yapper_bot-0.2.0}/PLAN.md +11 -7
- {yapper_bot-0.1.0 → yapper_bot-0.2.0}/README.md +9 -3
- {yapper_bot-0.1.0 → yapper_bot-0.2.0}/src/yapperpy/__init__.py +2 -0
- {yapper_bot-0.1.0 → yapper_bot-0.2.0}/src/yapperpy/_http/base.py +1 -1
- yapper_bot-0.2.0/src/yapperpy/_version.py +1 -0
- {yapper_bot-0.1.0 → yapper_bot-0.2.0}/src/yapperpy/aio_bot.py +2 -0
- {yapper_bot-0.1.0 → yapper_bot-0.2.0}/src/yapperpy/aio_client.py +7 -0
- {yapper_bot-0.1.0 → yapper_bot-0.2.0}/src/yapperpy/bot.py +2 -0
- {yapper_bot-0.1.0 → yapper_bot-0.2.0}/src/yapperpy/client.py +7 -0
- {yapper_bot-0.1.0 → yapper_bot-0.2.0}/src/yapperpy/endpoints.py +14 -0
- {yapper_bot-0.1.0 → yapper_bot-0.2.0}/src/yapperpy/event_models.py +7 -0
- {yapper_bot-0.1.0 → yapper_bot-0.2.0}/src/yapperpy/framework.py +8 -0
- {yapper_bot-0.1.0 → yapper_bot-0.2.0}/src/yapperpy/models.py +23 -0
- {yapper_bot-0.1.0 → yapper_bot-0.2.0}/tests/test_async.py +47 -1
- {yapper_bot-0.1.0 → yapper_bot-0.2.0}/tests/test_bot.py +28 -1
- {yapper_bot-0.1.0 → yapper_bot-0.2.0}/tests/test_client.py +29 -1
- {yapper_bot-0.1.0 → yapper_bot-0.2.0}/tests/test_endpoints.py +25 -1
- {yapper_bot-0.1.0 → yapper_bot-0.2.0}/tests/test_models.py +21 -0
- yapper_bot-0.1.0/src/yapperpy/_version.py +0 -1
- {yapper_bot-0.1.0 → yapper_bot-0.2.0}/.gitignore +0 -0
- {yapper_bot-0.1.0 → yapper_bot-0.2.0}/LICENSE +0 -0
- {yapper_bot-0.1.0 → yapper_bot-0.2.0}/examples/async_bot.py +0 -0
- {yapper_bot-0.1.0 → yapper_bot-0.2.0}/examples/cogs/fun.py +0 -0
- {yapper_bot-0.1.0 → yapper_bot-0.2.0}/examples/cogs/moderation.py +0 -0
- {yapper_bot-0.1.0 → yapper_bot-0.2.0}/examples/cogs_bot.py +0 -0
- {yapper_bot-0.1.0 → yapper_bot-0.2.0}/examples/echo_bot.py +0 -0
- {yapper_bot-0.1.0 → yapper_bot-0.2.0}/pyproject.toml +0 -0
- {yapper_bot-0.1.0 → yapper_bot-0.2.0}/src/yapperpy/__main__.py +0 -0
- {yapper_bot-0.1.0 → yapper_bot-0.2.0}/src/yapperpy/_core.py +0 -0
- {yapper_bot-0.1.0 → yapper_bot-0.2.0}/src/yapperpy/_http/__init__.py +0 -0
- {yapper_bot-0.1.0 → yapper_bot-0.2.0}/src/yapperpy/_http/aio.py +0 -0
- {yapper_bot-0.1.0 → yapper_bot-0.2.0}/src/yapperpy/_http/sync.py +0 -0
- {yapper_bot-0.1.0 → yapper_bot-0.2.0}/src/yapperpy/cogs.py +0 -0
- {yapper_bot-0.1.0 → yapper_bot-0.2.0}/src/yapperpy/components.py +0 -0
- {yapper_bot-0.1.0 → yapper_bot-0.2.0}/src/yapperpy/context.py +0 -0
- {yapper_bot-0.1.0 → yapper_bot-0.2.0}/src/yapperpy/embeds.py +0 -0
- {yapper_bot-0.1.0 → yapper_bot-0.2.0}/src/yapperpy/enums.py +0 -0
- {yapper_bot-0.1.0 → yapper_bot-0.2.0}/src/yapperpy/errors.py +0 -0
- {yapper_bot-0.1.0 → yapper_bot-0.2.0}/src/yapperpy/events.py +0 -0
- {yapper_bot-0.1.0 → yapper_bot-0.2.0}/src/yapperpy/py.typed +0 -0
- {yapper_bot-0.1.0 → yapper_bot-0.2.0}/src/yapperpy/registry.py +0 -0
- {yapper_bot-0.1.0 → yapper_bot-0.2.0}/src/yapperpy/retry.py +0 -0
- {yapper_bot-0.1.0 → yapper_bot-0.2.0}/tests/conftest.py +0 -0
- {yapper_bot-0.1.0 → yapper_bot-0.2.0}/tests/test_components.py +0 -0
- {yapper_bot-0.1.0 → yapper_bot-0.2.0}/tests/test_transport.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: yapper-bot
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Python bot framework and typed client for the Yapper Bot API
|
|
5
5
|
Project-URL: Homepage, https://www.yapper.website/developers
|
|
6
6
|
Project-URL: Documentation, https://www.yapper.website/developers/api
|
|
@@ -88,6 +88,8 @@ gets, so one cog works in both.
|
|
|
88
88
|
| `bot.add_cog(Cog)` | Groups commands, listeners and buttons in one class. |
|
|
89
89
|
| `bot.load_extensions("cogs")` | Loads every `.py` file in a folder. `bot.unload_extension` and `bot.reload_extension` work while the bot runs. |
|
|
90
90
|
| `bot.send(channel_id, text)` | Posts a message. |
|
|
91
|
+
| `client.voice.join(server_id, channel_id)` | Gets a short-lived LiveKit ticket for joining a voice channel. |
|
|
92
|
+
| `client.voice.leave(server_id)` | Takes the bot out of voice in a server. |
|
|
91
93
|
|
|
92
94
|
## Cogs
|
|
93
95
|
|
|
@@ -139,7 +141,7 @@ with yapperpy.Yapper("yb_...") as client:
|
|
|
139
141
|
|
|
140
142
|
Every call returns a typed model: `BotProfile`, `User`, `Server`, `Channel`, `Category`, `Message`,
|
|
141
143
|
`Member`, `MemberDetail`, `Role`, `Ban`, `Reaction`, `Command`, `Event`, `VoiceChannel`, `Image`,
|
|
142
|
-
`Embed`, `ActionRow`, `Button`, `Ok`.
|
|
144
|
+
`VoiceTicket`, `Embed`, `ActionRow`, `Button`, `Ok`.
|
|
143
145
|
|
|
144
146
|
Namespaces: `client.bot`, `client.events`, `client.servers`, `client.channels`, `client.messages`,
|
|
145
147
|
`client.interactions`, `client.voice`, `client.users`.
|
|
@@ -158,8 +160,12 @@ message event is a `MessageContext`, which has every field of the message plus `
|
|
|
158
160
|
`author_mention`. Other events give you a typed payload: `MemberJoinPayload`, `MemberLeavePayload`,
|
|
159
161
|
`ReactionPayload`, `InteractionPayload`, `VoicePayload`, `TestPayload`.
|
|
160
162
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
+
When the API routes a slash command to another bot, `command_for` prevents this bot from running
|
|
164
|
+
it. On first start, the bot reads the current event cursor before listening, so it does not replay
|
|
165
|
+
old events. Set `bot.last_event_id` before `run()` to resume from a previously saved cursor.
|
|
166
|
+
|
|
167
|
+
The current cursor is exposed as `bot.last_event_id`; persist and restore it to resume after a
|
|
168
|
+
restart. Events are kept by Yapper for one day.
|
|
163
169
|
|
|
164
170
|
## Buttons and embeds
|
|
165
171
|
|
|
@@ -119,14 +119,14 @@ Namespaces on both clients:
|
|
|
119
119
|
| `client.channels` | `list`, `get`, `create`, `edit`, `delete`, `categories`, `create_category`, `rename_category`, `delete_category` |
|
|
120
120
|
| `client.messages` | `send`, `get`, `edit`, `delete`, `pin`, `unpin`, `react`, `unreact`, `reactions`, `bulk_delete`, `history`, `send_layout`, `edit_layout` |
|
|
121
121
|
| `client.interactions` | `callback(id, token, type, ...)` |
|
|
122
|
-
| `client.voice` | `state`, `move`, `disconnect` |
|
|
122
|
+
| `client.voice` | `state`, `move`, `disconnect`, `join`, `leave` |
|
|
123
123
|
| `client.users` | `get`, `dm` |
|
|
124
124
|
|
|
125
125
|
Escape hatch for anything undocumented: `client.api("GET", "/servers")` / `await client.api(...)`.
|
|
126
126
|
|
|
127
127
|
Every method returns a typed model: `BotProfile`, `User`, `Server`, `Channel`, `Category`,
|
|
128
128
|
`Message`, `Member`, `MemberDetail`, `Role`, `Ban`, `Reaction`, `Command`, `Event`,
|
|
129
|
-
`VoiceChannel`, `Image`, `Embed`, `ActionRow`, `Button`, `LayoutComponent`, `Ok`.
|
|
129
|
+
`VoiceChannel`, `VoiceTicket`, `Image`, `Embed`, `ActionRow`, `Button`, `LayoutComponent`, `Ok`.
|
|
130
130
|
|
|
131
131
|
### Bot framework
|
|
132
132
|
|
|
@@ -229,14 +229,18 @@ All seven are done.
|
|
|
229
229
|
|
|
230
230
|
Built and verified on 21 October 2026.
|
|
231
231
|
|
|
232
|
-
- 149 tests pass, against the source tree and against the
|
|
233
|
-
|
|
232
|
+
- 149 tests pass, against the source tree, against the local wheel, and against the copy
|
|
233
|
+
installed from PyPI.
|
|
234
|
+
- `mypy --strict` is clean over the 27 source files.
|
|
234
235
|
- `ruff check` and `ruff format --check` are clean.
|
|
235
236
|
- The wheel installs into an empty virtualenv with no third-party package beside it, and the
|
|
236
237
|
whole suite passes there too.
|
|
237
|
-
- Every operation in the OpenAPI file has a builder and a client method:
|
|
238
|
-
|
|
238
|
+
- Every operation in the OpenAPI file has a builder and a client method: 54 builders cover the
|
|
239
|
+
51 documented operations, because two responses are split for convenience (channels and
|
|
239
240
|
categories come from one call) and two Components V2 helpers were added on top.
|
|
241
|
+
- Published to PyPI as `yapper-bot` 0.1.0 on 21 October 2026. PyPI rejects `yapperpy` and
|
|
242
|
+
`py-yapper` as too similar to the existing `yapper` project, hence the distribution name.
|
|
243
|
+
The import name stays `yapperpy`.
|
|
240
244
|
|
|
241
245
|
Two design points changed while building:
|
|
242
246
|
|
|
@@ -261,4 +265,4 @@ Two design points changed while building:
|
|
|
261
265
|
- OAuth2 "Login with Yapper" (a separate flow with its own client id and secret).
|
|
262
266
|
- Channel webhooks (a different URL scheme and error format).
|
|
263
267
|
- Bot hosting: that runs Node, so it cannot host this package.
|
|
264
|
-
-
|
|
268
|
+
- LiveKit audio streaming: the API provides voice tickets, but this package does not wrap a LiveKit client.
|
|
@@ -60,6 +60,8 @@ gets, so one cog works in both.
|
|
|
60
60
|
| `bot.add_cog(Cog)` | Groups commands, listeners and buttons in one class. |
|
|
61
61
|
| `bot.load_extensions("cogs")` | Loads every `.py` file in a folder. `bot.unload_extension` and `bot.reload_extension` work while the bot runs. |
|
|
62
62
|
| `bot.send(channel_id, text)` | Posts a message. |
|
|
63
|
+
| `client.voice.join(server_id, channel_id)` | Gets a short-lived LiveKit ticket for joining a voice channel. |
|
|
64
|
+
| `client.voice.leave(server_id)` | Takes the bot out of voice in a server. |
|
|
63
65
|
|
|
64
66
|
## Cogs
|
|
65
67
|
|
|
@@ -111,7 +113,7 @@ with yapperpy.Yapper("yb_...") as client:
|
|
|
111
113
|
|
|
112
114
|
Every call returns a typed model: `BotProfile`, `User`, `Server`, `Channel`, `Category`, `Message`,
|
|
113
115
|
`Member`, `MemberDetail`, `Role`, `Ban`, `Reaction`, `Command`, `Event`, `VoiceChannel`, `Image`,
|
|
114
|
-
`Embed`, `ActionRow`, `Button`, `Ok`.
|
|
116
|
+
`VoiceTicket`, `Embed`, `ActionRow`, `Button`, `Ok`.
|
|
115
117
|
|
|
116
118
|
Namespaces: `client.bot`, `client.events`, `client.servers`, `client.channels`, `client.messages`,
|
|
117
119
|
`client.interactions`, `client.voice`, `client.users`.
|
|
@@ -130,8 +132,12 @@ message event is a `MessageContext`, which has every field of the message plus `
|
|
|
130
132
|
`author_mention`. Other events give you a typed payload: `MemberJoinPayload`, `MemberLeavePayload`,
|
|
131
133
|
`ReactionPayload`, `InteractionPayload`, `VoicePayload`, `TestPayload`.
|
|
132
134
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
+
When the API routes a slash command to another bot, `command_for` prevents this bot from running
|
|
136
|
+
it. On first start, the bot reads the current event cursor before listening, so it does not replay
|
|
137
|
+
old events. Set `bot.last_event_id` before `run()` to resume from a previously saved cursor.
|
|
138
|
+
|
|
139
|
+
The current cursor is exposed as `bot.last_event_id`; persist and restore it to resume after a
|
|
140
|
+
restart. Events are kept by Yapper for one day.
|
|
135
141
|
|
|
136
142
|
## Buttons and embeds
|
|
137
143
|
|
|
@@ -70,6 +70,7 @@ from .models import (
|
|
|
70
70
|
VoiceChannel,
|
|
71
71
|
VoiceDisconnectResult,
|
|
72
72
|
VoiceMoveResult,
|
|
73
|
+
VoiceTicket,
|
|
73
74
|
)
|
|
74
75
|
from .models import (
|
|
75
76
|
Embed as EmbedModel,
|
|
@@ -124,6 +125,7 @@ __all__ = [
|
|
|
124
125
|
"VoiceDisconnectResult",
|
|
125
126
|
"VoiceMoveResult",
|
|
126
127
|
"VoicePayload",
|
|
128
|
+
"VoiceTicket",
|
|
127
129
|
"Yapper",
|
|
128
130
|
"YapperAuthError",
|
|
129
131
|
"YapperConflictError",
|
|
@@ -8,7 +8,7 @@ from urllib.parse import parse_qsl, quote, urlencode, urlsplit
|
|
|
8
8
|
|
|
9
9
|
from .._version import __version__
|
|
10
10
|
|
|
11
|
-
DEFAULT_BASE_URL = "https://
|
|
11
|
+
DEFAULT_BASE_URL = "https://bots.yapper.website/api/bot/v1"
|
|
12
12
|
DEFAULT_TIMEOUT = 40.0
|
|
13
13
|
USER_AGENT = f"yapperpy/{__version__} (+https://www.yapper.website/developers)"
|
|
14
14
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.0"
|
|
@@ -143,6 +143,8 @@ class AsyncBot(BotBase):
|
|
|
143
143
|
self.running = True
|
|
144
144
|
self.me = await self.client.bot.get()
|
|
145
145
|
await self.sync_commands()
|
|
146
|
+
if self.last_event_id == 0:
|
|
147
|
+
self.last_event_id = (await self.client.events.fetch(after=0, wait=0)).last
|
|
146
148
|
await self.emit("ready", self.me)
|
|
147
149
|
if self.me is not None:
|
|
148
150
|
self._log(f"online as {self.me.username}")
|
|
@@ -42,6 +42,7 @@ from .models import (
|
|
|
42
42
|
VoiceChannelList,
|
|
43
43
|
VoiceDisconnectResult,
|
|
44
44
|
VoiceMoveResult,
|
|
45
|
+
VoiceTicket,
|
|
45
46
|
)
|
|
46
47
|
from .retry import RetryPolicy
|
|
47
48
|
|
|
@@ -317,6 +318,12 @@ class VoiceResource(Resource["AsyncYapper"]):
|
|
|
317
318
|
async def disconnect(self, server_id: str, user_id: str) -> VoiceDisconnectResult:
|
|
318
319
|
return await self._client._run(ep.disconnect_voice_member(server_id, user_id))
|
|
319
320
|
|
|
321
|
+
async def join(self, server_id: str, channel_id: str) -> VoiceTicket:
|
|
322
|
+
return await self._client._run(ep.join_voice(server_id, channel_id))
|
|
323
|
+
|
|
324
|
+
async def leave(self, server_id: str) -> Ok:
|
|
325
|
+
return await self._client._run(ep.leave_voice(server_id))
|
|
326
|
+
|
|
320
327
|
|
|
321
328
|
class UsersResource(Resource["AsyncYapper"]):
|
|
322
329
|
async def get(self, user_id: str) -> User:
|
|
@@ -136,6 +136,8 @@ class Bot(BotBase):
|
|
|
136
136
|
self.running = True
|
|
137
137
|
self.me = self.client.bot.get()
|
|
138
138
|
self.sync_commands()
|
|
139
|
+
if self.last_event_id == 0:
|
|
140
|
+
self.last_event_id = self.client.events.fetch(after=0, wait=0).last
|
|
139
141
|
self.emit("ready", self.me)
|
|
140
142
|
if self.me is not None:
|
|
141
143
|
self._log(f"online as {self.me.username}")
|
|
@@ -42,6 +42,7 @@ from .models import (
|
|
|
42
42
|
VoiceChannelList,
|
|
43
43
|
VoiceDisconnectResult,
|
|
44
44
|
VoiceMoveResult,
|
|
45
|
+
VoiceTicket,
|
|
45
46
|
)
|
|
46
47
|
from .retry import RetryPolicy
|
|
47
48
|
|
|
@@ -315,6 +316,12 @@ class VoiceResource(Resource["Yapper"]):
|
|
|
315
316
|
def disconnect(self, server_id: str, user_id: str) -> VoiceDisconnectResult:
|
|
316
317
|
return self._client._run(ep.disconnect_voice_member(server_id, user_id))
|
|
317
318
|
|
|
319
|
+
def join(self, server_id: str, channel_id: str) -> VoiceTicket:
|
|
320
|
+
return self._client._run(ep.join_voice(server_id, channel_id))
|
|
321
|
+
|
|
322
|
+
def leave(self, server_id: str) -> Ok:
|
|
323
|
+
return self._client._run(ep.leave_voice(server_id))
|
|
324
|
+
|
|
318
325
|
|
|
319
326
|
class UsersResource(Resource["Yapper"]):
|
|
320
327
|
def get(self, user_id: str) -> User:
|
|
@@ -46,6 +46,7 @@ from .models import (
|
|
|
46
46
|
VoiceChannelList,
|
|
47
47
|
VoiceDisconnectResult,
|
|
48
48
|
VoiceMoveResult,
|
|
49
|
+
VoiceTicket,
|
|
49
50
|
command_list,
|
|
50
51
|
component_list,
|
|
51
52
|
embed_list,
|
|
@@ -719,6 +720,19 @@ def disconnect_voice_member(server_id: str, user_id: str) -> Request[VoiceDiscon
|
|
|
719
720
|
)
|
|
720
721
|
|
|
721
722
|
|
|
723
|
+
def join_voice(server_id: str, channel_id: str) -> Request[VoiceTicket]:
|
|
724
|
+
return Request(
|
|
725
|
+
"POST",
|
|
726
|
+
f"/servers/{segment(server_id)}/voice/join",
|
|
727
|
+
json_body={"channel_id": channel_id},
|
|
728
|
+
parse=_model(VoiceTicket),
|
|
729
|
+
)
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
def leave_voice(server_id: str) -> Request[Ok]:
|
|
733
|
+
return Request("POST", f"/servers/{segment(server_id)}/voice/leave", parse=_parse_ok)
|
|
734
|
+
|
|
735
|
+
|
|
722
736
|
def get_user(user_id: str) -> Request[User]:
|
|
723
737
|
return Request("GET", f"/users/{segment(user_id)}", parse=_wrapped("user", User))
|
|
724
738
|
|
|
@@ -69,9 +69,15 @@ class MessagePayload(EventPayload):
|
|
|
69
69
|
author_bot: bool = False
|
|
70
70
|
command: str | None = None
|
|
71
71
|
args: str | None = None
|
|
72
|
+
command_for: str | list[str] | None = None
|
|
72
73
|
|
|
73
74
|
@classmethod
|
|
74
75
|
def from_dict(cls, data: Mapping[str, Any]) -> MessagePayload:
|
|
76
|
+
command_for = data.get("command_for")
|
|
77
|
+
if isinstance(command_for, list):
|
|
78
|
+
command_for = [str(bot_id) for bot_id in command_for]
|
|
79
|
+
elif not isinstance(command_for, str):
|
|
80
|
+
command_for = None
|
|
75
81
|
return cls(
|
|
76
82
|
id=str(data.get("id") or ""),
|
|
77
83
|
channel_id=str(data.get("channel_id") or ""),
|
|
@@ -87,6 +93,7 @@ class MessagePayload(EventPayload):
|
|
|
87
93
|
author_bot=_flag(data, "author_bot"),
|
|
88
94
|
command=_text(data, "command"),
|
|
89
95
|
args=_text(data, "args"),
|
|
96
|
+
command_for=command_for,
|
|
90
97
|
)
|
|
91
98
|
|
|
92
99
|
@property
|
|
@@ -104,6 +104,14 @@ class BotBase:
|
|
|
104
104
|
return self.registry.command_payload()
|
|
105
105
|
|
|
106
106
|
def resolve_command(self, payload: MessagePayload) -> tuple[Command, str] | None:
|
|
107
|
+
if payload.command_for is not None:
|
|
108
|
+
targets = (
|
|
109
|
+
payload.command_for
|
|
110
|
+
if isinstance(payload.command_for, list)
|
|
111
|
+
else [payload.command_for]
|
|
112
|
+
)
|
|
113
|
+
if self.me is None or self.me.id not in targets:
|
|
114
|
+
return None
|
|
107
115
|
match = self.registry.resolve(payload.content, self.prefixes, payload.command, payload.args)
|
|
108
116
|
if match is None:
|
|
109
117
|
return None
|
|
@@ -714,6 +714,29 @@ class VoiceDisconnectResult:
|
|
|
714
714
|
return cls(ok=_flag(data, "ok", True), channel_id=_text(data, "channel_id"))
|
|
715
715
|
|
|
716
716
|
|
|
717
|
+
@dataclass(slots=True)
|
|
718
|
+
class VoiceTicket:
|
|
719
|
+
url: str
|
|
720
|
+
token: str
|
|
721
|
+
e2ee: bool = False
|
|
722
|
+
voice_muted: bool = False
|
|
723
|
+
channel_id: str | None = None
|
|
724
|
+
server_id: str | None = None
|
|
725
|
+
expires_in: int | None = None
|
|
726
|
+
|
|
727
|
+
@classmethod
|
|
728
|
+
def from_dict(cls, data: Mapping[str, Any]) -> VoiceTicket:
|
|
729
|
+
return cls(
|
|
730
|
+
url=str(data.get("url") or ""),
|
|
731
|
+
token=str(data.get("token") or ""),
|
|
732
|
+
e2ee=_flag(data, "e2ee"),
|
|
733
|
+
voice_muted=_flag(data, "voice_muted"),
|
|
734
|
+
channel_id=_text(data, "channel_id"),
|
|
735
|
+
server_id=_text(data, "server_id"),
|
|
736
|
+
expires_in=_number(data, "expires_in"),
|
|
737
|
+
)
|
|
738
|
+
|
|
739
|
+
|
|
717
740
|
@dataclass(slots=True)
|
|
718
741
|
class BulkDeleteResult:
|
|
719
742
|
deleted: list[str] = field(default_factory=list)
|
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import asyncio
|
|
4
4
|
|
|
5
5
|
from conftest import AsyncFakeTransport, response
|
|
6
|
-
from yapperpy import AsyncBot, AsyncYapper
|
|
6
|
+
from yapperpy import AsyncBot, AsyncYapper, BotProfile
|
|
7
7
|
from yapperpy.errors import YapperRateLimitError
|
|
8
8
|
from yapperpy.models import Event
|
|
9
9
|
from yapperpy.retry import RetryPolicy
|
|
@@ -97,6 +97,36 @@ def test_the_async_bot_runs_a_command() -> None:
|
|
|
97
97
|
asyncio.run(scenario())
|
|
98
98
|
|
|
99
99
|
|
|
100
|
+
def test_the_async_bot_skips_a_command_routed_to_another_bot() -> None:
|
|
101
|
+
async def scenario() -> None:
|
|
102
|
+
bot = AsyncBot(client=AsyncYapper("yb_test", transport=AsyncFakeTransport()), log=False)
|
|
103
|
+
bot.me = BotProfile(id="b1", username="bot")
|
|
104
|
+
seen: list[object] = []
|
|
105
|
+
|
|
106
|
+
@bot.command("ping", "Is the bot alive?")
|
|
107
|
+
async def ping(msg: object) -> None:
|
|
108
|
+
seen.append(msg)
|
|
109
|
+
|
|
110
|
+
await bot.handle(
|
|
111
|
+
Event(
|
|
112
|
+
id=1,
|
|
113
|
+
type="message",
|
|
114
|
+
data={
|
|
115
|
+
"id": "m1",
|
|
116
|
+
"content": "/ping now",
|
|
117
|
+
"command": "ping",
|
|
118
|
+
"args": "now",
|
|
119
|
+
"command_for": ["b2"],
|
|
120
|
+
"channel_id": "c1",
|
|
121
|
+
"author_id": "u1",
|
|
122
|
+
},
|
|
123
|
+
)
|
|
124
|
+
)
|
|
125
|
+
assert seen == []
|
|
126
|
+
|
|
127
|
+
asyncio.run(scenario())
|
|
128
|
+
|
|
129
|
+
|
|
100
130
|
def test_the_async_bot_awaits_listeners() -> None:
|
|
101
131
|
async def scenario() -> None:
|
|
102
132
|
transport = AsyncFakeTransport()
|
|
@@ -172,3 +202,19 @@ def test_the_async_bot_answers_a_button() -> None:
|
|
|
172
202
|
assert transport.body == {"type": "ephemeral", "content": "counted"}
|
|
173
203
|
|
|
174
204
|
asyncio.run(scenario())
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def test_the_async_voice_resource_joins_and_leaves() -> None:
|
|
208
|
+
async def scenario() -> None:
|
|
209
|
+
transport = AsyncFakeTransport(
|
|
210
|
+
response({"url": "wss://voice.example", "token": "ticket", "expires_in": 60}),
|
|
211
|
+
response({"ok": True}),
|
|
212
|
+
)
|
|
213
|
+
client = AsyncYapper("yb_test", transport=transport)
|
|
214
|
+
ticket = await client.voice.join("s1", "v1")
|
|
215
|
+
assert ticket.token == "ticket"
|
|
216
|
+
assert transport.body == {"channel_id": "v1"}
|
|
217
|
+
assert await client.voice.leave("s1")
|
|
218
|
+
assert transport.last.url.endswith("/servers/s1/voice/leave")
|
|
219
|
+
|
|
220
|
+
asyncio.run(scenario())
|
|
@@ -7,7 +7,7 @@ import pytest
|
|
|
7
7
|
import yapperpy
|
|
8
8
|
from conftest import FakeTransport, response
|
|
9
9
|
from yapperpy import Bot, Cog, Yapper
|
|
10
|
-
from yapperpy.models import Event
|
|
10
|
+
from yapperpy.models import BotProfile, Event
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
def make_bot(*responses: object) -> tuple[Bot, FakeTransport]:
|
|
@@ -133,6 +133,32 @@ def test_the_api_can_name_the_command() -> None:
|
|
|
133
133
|
assert seen[0].args == "two"
|
|
134
134
|
|
|
135
135
|
|
|
136
|
+
def test_a_command_routed_to_another_bot_is_not_run() -> None:
|
|
137
|
+
bot, _ = make_bot()
|
|
138
|
+
bot.me = BotProfile(id="b1", username="bot")
|
|
139
|
+
seen: list[object] = []
|
|
140
|
+
|
|
141
|
+
@bot.command("ping", "Is the bot alive?")
|
|
142
|
+
def ping(msg: object) -> None:
|
|
143
|
+
seen.append(msg)
|
|
144
|
+
|
|
145
|
+
bot.handle(message_event("/ping now", command="ping", args="now", command_for=["b2"]))
|
|
146
|
+
assert seen == []
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def test_a_command_routed_to_this_bot_runs() -> None:
|
|
150
|
+
bot, _ = make_bot()
|
|
151
|
+
bot.me = BotProfile(id="b1", username="bot")
|
|
152
|
+
seen: list[object] = []
|
|
153
|
+
|
|
154
|
+
@bot.command("ping", "Is the bot alive?")
|
|
155
|
+
def ping(msg: object) -> None:
|
|
156
|
+
seen.append(msg)
|
|
157
|
+
|
|
158
|
+
bot.handle(message_event("/ping now", command="ping", args="now", command_for="b1"))
|
|
159
|
+
assert len(seen) == 1
|
|
160
|
+
|
|
161
|
+
|
|
136
162
|
def test_messages_from_bots_are_skipped() -> None:
|
|
137
163
|
bot, _ = make_bot()
|
|
138
164
|
seen: list[object] = []
|
|
@@ -384,4 +410,5 @@ def test_running_registers_the_commands_and_handles_events() -> None:
|
|
|
384
410
|
assert len(seen) == 1
|
|
385
411
|
assert transport.requests[1].url.endswith("/commands")
|
|
386
412
|
assert json.loads(transport.requests[1].body or b"null") == {"commands": []}
|
|
413
|
+
assert transport.requests[2].query == {"after": "0", "wait": "0"}
|
|
387
414
|
assert bot.last_event_id == 6
|
|
@@ -4,7 +4,7 @@ import pytest
|
|
|
4
4
|
|
|
5
5
|
from conftest import FakeTransport, response
|
|
6
6
|
from yapperpy import Yapper
|
|
7
|
-
from yapperpy._http.base import TransportError, TransportTimeout
|
|
7
|
+
from yapperpy._http.base import DEFAULT_BASE_URL, TransportError, TransportTimeout
|
|
8
8
|
from yapperpy.errors import (
|
|
9
9
|
YapperAuthError,
|
|
10
10
|
YapperConnectionError,
|
|
@@ -59,6 +59,10 @@ def test_the_base_url_can_be_changed() -> None:
|
|
|
59
59
|
assert transport.last.url == "http://127.0.0.1:1234/api/bot/v1/servers"
|
|
60
60
|
|
|
61
61
|
|
|
62
|
+
def test_the_default_base_url_is_the_bot_api_host() -> None:
|
|
63
|
+
assert DEFAULT_BASE_URL == "https://bots.yapper.website/api/bot/v1"
|
|
64
|
+
|
|
65
|
+
|
|
62
66
|
def test_lists_are_parsed() -> None:
|
|
63
67
|
client, _ = make_client(
|
|
64
68
|
response({"servers": [{"id": "s1", "name": "Home", "member_count": 2}]})
|
|
@@ -248,6 +252,30 @@ def test_status_is_sent() -> None:
|
|
|
248
252
|
assert transport.body == {"status": "dnd", "activity": "Moderating"}
|
|
249
253
|
|
|
250
254
|
|
|
255
|
+
def test_voice_ticket_can_be_requested_and_left() -> None:
|
|
256
|
+
client, transport = make_client(
|
|
257
|
+
response(
|
|
258
|
+
{
|
|
259
|
+
"url": "wss://voice.example",
|
|
260
|
+
"token": "ticket",
|
|
261
|
+
"e2ee": True,
|
|
262
|
+
"voice_muted": False,
|
|
263
|
+
"channel_id": "v1",
|
|
264
|
+
"server_id": "s1",
|
|
265
|
+
"expires_in": 60,
|
|
266
|
+
}
|
|
267
|
+
),
|
|
268
|
+
response({"ok": True}),
|
|
269
|
+
)
|
|
270
|
+
ticket = client.voice.join("s1", "v1")
|
|
271
|
+
assert ticket.token == "ticket"
|
|
272
|
+
assert ticket.e2ee is True
|
|
273
|
+
assert transport.body == {"channel_id": "v1"}
|
|
274
|
+
assert client.voice.leave("s1")
|
|
275
|
+
assert transport.last.method == "POST"
|
|
276
|
+
assert transport.last.url.endswith("/servers/s1/voice/leave")
|
|
277
|
+
|
|
278
|
+
|
|
251
279
|
def test_commands_are_replaced_as_a_list() -> None:
|
|
252
280
|
client, transport = make_client(response({"ok": True, "commands": []}))
|
|
253
281
|
client.bot.set_commands([{"name": "ping", "description": "d"}])
|
|
@@ -132,6 +132,28 @@ def test_voice_move_body() -> None:
|
|
|
132
132
|
assert request.json_body == {"user_id": "u1", "channel_id": "v2"}
|
|
133
133
|
|
|
134
134
|
|
|
135
|
+
def test_voice_join_returns_a_ticket() -> None:
|
|
136
|
+
request = ep.join_voice("s1", "v1")
|
|
137
|
+
assert request.method == "POST"
|
|
138
|
+
assert request.path == "/servers/s1/voice/join"
|
|
139
|
+
assert request.json_body == {"channel_id": "v1"}
|
|
140
|
+
ticket = request.parse(
|
|
141
|
+
b'{"url":"wss://voice.example","token":"secret","e2ee":true,'
|
|
142
|
+
b'"voice_muted":false,"channel_id":"v1","server_id":"s1","expires_in":60}'
|
|
143
|
+
)
|
|
144
|
+
assert ticket.url == "wss://voice.example"
|
|
145
|
+
assert ticket.token == "secret"
|
|
146
|
+
assert ticket.e2ee is True
|
|
147
|
+
assert ticket.expires_in == 60
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def test_voice_leave_has_no_request_body() -> None:
|
|
151
|
+
request = ep.leave_voice("s1")
|
|
152
|
+
assert request.method == "POST"
|
|
153
|
+
assert request.path == "/servers/s1/voice/leave"
|
|
154
|
+
assert request.json_body is None
|
|
155
|
+
|
|
156
|
+
|
|
135
157
|
def test_list_messages_keeps_the_timestamp_query() -> None:
|
|
136
158
|
request = ep.list_messages("c1", limit=10, before="2026-10-21T00:00:00Z")
|
|
137
159
|
assert request.query["before"] == "2026-10-21T00:00:00Z"
|
|
@@ -193,12 +215,14 @@ def test_every_operation_has_a_parse_function() -> None:
|
|
|
193
215
|
ep.get_voice("s1"),
|
|
194
216
|
ep.move_voice_member("s1", "u1", "v1"),
|
|
195
217
|
ep.disconnect_voice_member("s1", "u1"),
|
|
218
|
+
ep.join_voice("s1", "v1"),
|
|
219
|
+
ep.leave_voice("s1"),
|
|
196
220
|
ep.get_user("u1"),
|
|
197
221
|
ep.open_dm("u1"),
|
|
198
222
|
ep.send_layout("c1", [{"type": 10, "content": "hi"}]),
|
|
199
223
|
ep.edit_layout("c1", "m1", [{"type": 10, "content": "hi"}]),
|
|
200
224
|
]
|
|
201
|
-
assert len(requests) ==
|
|
225
|
+
assert len(requests) == 54
|
|
202
226
|
for request in requests:
|
|
203
227
|
assert request.method
|
|
204
228
|
assert request.path.startswith("/")
|
|
@@ -193,6 +193,27 @@ def test_voice_channel() -> None:
|
|
|
193
193
|
assert voice.user_ids == ["u1", "u2"]
|
|
194
194
|
|
|
195
195
|
|
|
196
|
+
def test_voice_ticket_parsing() -> None:
|
|
197
|
+
from yapperpy import VoiceTicket
|
|
198
|
+
|
|
199
|
+
ticket = VoiceTicket.from_dict(
|
|
200
|
+
{
|
|
201
|
+
"url": "wss://voice.example",
|
|
202
|
+
"token": "ticket",
|
|
203
|
+
"e2ee": True,
|
|
204
|
+
"voice_muted": True,
|
|
205
|
+
"channel_id": "v1",
|
|
206
|
+
"server_id": "s1",
|
|
207
|
+
"expires_in": 60,
|
|
208
|
+
}
|
|
209
|
+
)
|
|
210
|
+
assert ticket.url == "wss://voice.example"
|
|
211
|
+
assert ticket.token == "ticket"
|
|
212
|
+
assert ticket.e2ee is True
|
|
213
|
+
assert ticket.voice_muted is True
|
|
214
|
+
assert ticket.expires_in == 60
|
|
215
|
+
|
|
216
|
+
|
|
196
217
|
def test_command_round_trip() -> None:
|
|
197
218
|
command = Command.from_dict({"name": "ping", "description": "Is the bot alive?"})
|
|
198
219
|
assert command.to_dict() == {"name": "ping", "description": "Is the bot alive?"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|