vibes-client 0.1.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.
- vibes_client-0.1.0/.gitignore +42 -0
- vibes_client-0.1.0/LICENSE +21 -0
- vibes_client-0.1.0/PKG-INFO +442 -0
- vibes_client-0.1.0/README.md +415 -0
- vibes_client-0.1.0/examples/__init__.py +1 -0
- vibes_client-0.1.0/examples/app_commands.py +87 -0
- vibes_client-0.1.0/examples/events.py +130 -0
- vibes_client-0.1.0/examples/example_extension.py +72 -0
- vibes_client-0.1.0/examples/extensions.py +84 -0
- vibes_client-0.1.0/examples/quickstart.py +52 -0
- vibes_client-0.1.0/examples/rest_api.py +91 -0
- vibes_client-0.1.0/examples/text_commands.py +142 -0
- vibes_client-0.1.0/pyproject.toml +41 -0
- vibes_client-0.1.0/vibes/__init__.py +30 -0
- vibes_client-0.1.0/vibes/cache.py +5 -0
- vibes_client-0.1.0/vibes/clients.py +741 -0
- vibes_client-0.1.0/vibes/commands/__init__.py +49 -0
- vibes_client-0.1.0/vibes/commands/app_commands.py +187 -0
- vibes_client-0.1.0/vibes/commands/commands.py +149 -0
- vibes_client-0.1.0/vibes/commands/context.py +35 -0
- vibes_client-0.1.0/vibes/commands/cooldown.py +32 -0
- vibes_client-0.1.0/vibes/commands/errors.py +65 -0
- vibes_client-0.1.0/vibes/commands/params.py +22 -0
- vibes_client-0.1.0/vibes/donations.py +10 -0
- vibes_client-0.1.0/vibes/errors.py +15 -0
- vibes_client-0.1.0/vibes/events.py +139 -0
- vibes_client-0.1.0/vibes/ext/__init__.py +16 -0
- vibes_client-0.1.0/vibes/ext/errors.py +25 -0
- vibes_client-0.1.0/vibes/ext/listeners.py +51 -0
- vibes_client-0.1.0/vibes/messaging.py +62 -0
- vibes_client-0.1.0/vibes/models.py +10 -0
- vibes_client-0.1.0/vibes/streams.py +65 -0
- vibes_client-0.1.0/vibes/users.py +112 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Bytecompiled / optimized
|
|
2
|
+
*__pycache__*
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# Distribution / packaging
|
|
7
|
+
build/
|
|
8
|
+
dist/
|
|
9
|
+
*.egg-info/
|
|
10
|
+
*.egg
|
|
11
|
+
|
|
12
|
+
# Virtual environments
|
|
13
|
+
.venv/
|
|
14
|
+
venv/
|
|
15
|
+
env/
|
|
16
|
+
.env
|
|
17
|
+
|
|
18
|
+
# IDE
|
|
19
|
+
tempCodeRunnerFile.*
|
|
20
|
+
.vscode/
|
|
21
|
+
.idea/
|
|
22
|
+
*.swp
|
|
23
|
+
*.swo
|
|
24
|
+
|
|
25
|
+
# Testing / coverage
|
|
26
|
+
tests/
|
|
27
|
+
out.log
|
|
28
|
+
run.bat
|
|
29
|
+
test.py
|
|
30
|
+
.pytest_cache/
|
|
31
|
+
htmlcov/
|
|
32
|
+
.coverage
|
|
33
|
+
.coverage.*
|
|
34
|
+
|
|
35
|
+
# mypy / ruff
|
|
36
|
+
.mypy_cache/
|
|
37
|
+
.ruff_cache/
|
|
38
|
+
|
|
39
|
+
# OS
|
|
40
|
+
.DS_Store
|
|
41
|
+
Thumbs.db
|
|
42
|
+
build.bat
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 drek124
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: vibes-client
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: The VIBES Streaming API Wrapper
|
|
5
|
+
Project-URL: Homepage, https://github.com/drek124/vibes
|
|
6
|
+
Project-URL: Repository, https://github.com/drek124/vibes
|
|
7
|
+
Project-URL: Issues, https://github.com/drek124/vibes/issues
|
|
8
|
+
Author-email: drek124 <drek.dev124@gmail.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: api,async,streaming,vibes,wrapper
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Communications :: Chat
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Requires-Python: >=3.11
|
|
24
|
+
Requires-Dist: httpx
|
|
25
|
+
Requires-Dist: websockets
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# VIBES Client
|
|
29
|
+
|
|
30
|
+
An async Python wrapper for the [VIBES](https://vibes.stream/) streaming API — REST endpoints, a realtime
|
|
31
|
+
WebSocket client, a typed command framework, and a plugin (extension) system.
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
import os
|
|
35
|
+
import asyncio
|
|
36
|
+
import vibes
|
|
37
|
+
from vibes import Client
|
|
38
|
+
|
|
39
|
+
client = Client(token=os.environ["VIBES_TOKEN"], prefix="!")
|
|
40
|
+
|
|
41
|
+
@client.event()
|
|
42
|
+
async def on_message(message: vibes.Message):
|
|
43
|
+
if message.content == "!ping":
|
|
44
|
+
await message.reply("Pong!")
|
|
45
|
+
|
|
46
|
+
asyncio.run(client.start())
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Table of contents
|
|
52
|
+
|
|
53
|
+
- [Features](#features)
|
|
54
|
+
- [Installation](#installation)
|
|
55
|
+
- [Quickstart](#quickstart)
|
|
56
|
+
- [Clients](#clients)
|
|
57
|
+
- [PassiveClient (REST only)](#passiveclient-rest-only)
|
|
58
|
+
- [Client (WebSocket)](#client-websocket)
|
|
59
|
+
- [Events](#events)
|
|
60
|
+
- [Commands](#commands)
|
|
61
|
+
- [Text commands](#text-commands)
|
|
62
|
+
- [Parameters](#parameters)
|
|
63
|
+
- [Roles](#roles)
|
|
64
|
+
- [Cooldowns](#cooldowns)
|
|
65
|
+
- [App commands](#app-commands)
|
|
66
|
+
- [Error handling](#error-handling)
|
|
67
|
+
- [Extensions](#extensions)
|
|
68
|
+
- [Models reference](#models-reference)
|
|
69
|
+
- [Examples](#examples)
|
|
70
|
+
- [License](#license)
|
|
71
|
+
|
|
72
|
+
## Features
|
|
73
|
+
|
|
74
|
+
- **REST API** — fetch streams, users and your own application with `PassiveClient`.
|
|
75
|
+
- **Realtime events** — a WebSocket connection with automatic reconnects and 30s heartbeats.
|
|
76
|
+
- **Typed commands** — both classic chat (`!command`) and native VIBES *app commands*, with automatic
|
|
77
|
+
argument parsing, role gating and per-viewer/user/chat cooldowns.
|
|
78
|
+
- **Extensions** — load and unload self-contained modules of commands and listeners at runtime.
|
|
79
|
+
- **Caching** — users and messages are cached to cut down on API calls.
|
|
80
|
+
|
|
81
|
+
## Installation
|
|
82
|
+
|
|
83
|
+
Python 3.11+ is required. Dependencies are `httpx` and `websockets`.
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
pip install vibes-api
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Quickstart
|
|
90
|
+
|
|
91
|
+
Grab an API token from your [VIBES dashboard](https://vibes.stream/dashboard/plugins/api) and export it
|
|
92
|
+
as `VIBES_TOKEN` (the examples read it from the environment, e.g. with a `.env` file and `python-dotenv`).
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
import os
|
|
96
|
+
import asyncio
|
|
97
|
+
import vibes
|
|
98
|
+
from vibes import Client
|
|
99
|
+
|
|
100
|
+
client = Client(token=os.environ["VIBES_TOKEN"], prefix="!")
|
|
101
|
+
|
|
102
|
+
@client.event()
|
|
103
|
+
async def on_connect():
|
|
104
|
+
print(f"Logged in as {client.user}") # ClientUser
|
|
105
|
+
for stream in await client.fetch_streams(limit=10):
|
|
106
|
+
await client.subscribe_to(stream.channel) # listen to this stream
|
|
107
|
+
|
|
108
|
+
@client.event()
|
|
109
|
+
async def on_message(message: vibes.Message):
|
|
110
|
+
if message.content.startswith("!"):
|
|
111
|
+
await client.process_commands(message) # let the command framework run
|
|
112
|
+
|
|
113
|
+
asyncio.run(client.start())
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`Client.start()` opens the WebSocket and runs the event loop until the connection closes — it is the
|
|
117
|
+
last thing you await, and it blocks for the lifetime of the bot.
|
|
118
|
+
|
|
119
|
+
## Clients
|
|
120
|
+
|
|
121
|
+
### PassiveClient (REST only)
|
|
122
|
+
|
|
123
|
+
No WebSocket, no events — just authenticated API calls. Use it for scripts, cron jobs or dashboards.
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
import vibes
|
|
127
|
+
|
|
128
|
+
api = vibes.PassiveClient(token=TOKEN)
|
|
129
|
+
try:
|
|
130
|
+
app = await api.fetch_client()
|
|
131
|
+
print(app.owner, app.rate_limit)
|
|
132
|
+
|
|
133
|
+
streams = await api.fetch_streams(limit=50, category="chit-chat")
|
|
134
|
+
user = await api.fetch_user("some-streamer") # None if not found
|
|
135
|
+
finally:
|
|
136
|
+
await api.close()
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
| Method | Returns | Notes |
|
|
140
|
+
| --- | --- | --- |
|
|
141
|
+
| `fetch_client()` | `Application` | Your own app: owner, key, rate limit, webhook. |
|
|
142
|
+
| `fetch_streams(*, limit=100, category=None, mature=True)` | `list[Stream]` | `category` accepts a slug or a `StreamCategory`. `mature=True` requests 18+ streams (403 if your account isn't 18+). |
|
|
143
|
+
| `fetch_user(slug)` | `User \| None` | Accepts a username or ID. Results are cached. |
|
|
144
|
+
| `send_request(coro, *args, raise_for_status=True, **kwargs)` | `httpx.Response` | The low-level call every method is built on. |
|
|
145
|
+
| `close()` | `None` | Closes the underlying `httpx.AsyncClient`. |
|
|
146
|
+
| `clear_cache()` | `None` | Empties the user and message caches. |
|
|
147
|
+
|
|
148
|
+
`send_request` gives you rate-limit handling and typed errors for free:
|
|
149
|
+
|
|
150
|
+
- `429` responses are retried automatically after the `Retry-After` header (default 60s).
|
|
151
|
+
- `404` raises `vibes.errors.NotFound`.
|
|
152
|
+
- `403` raises `vibes.errors.Forbidden`.
|
|
153
|
+
- Any other non-OK status raises `httpx.HTTPStatusError`.
|
|
154
|
+
|
|
155
|
+
Pass `raise_for_status=False` to receive the raw response instead.
|
|
156
|
+
|
|
157
|
+
### Client (WebSocket)
|
|
158
|
+
|
|
159
|
+
`Client` extends `PassiveClient` with the realtime connection, event listeners and the command
|
|
160
|
+
framework.
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
client = Client(token=TOKEN, prefix="!")
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
- `prefix` is required if you use text commands; it can't be empty or contain spaces.
|
|
167
|
+
- `client.user` is a `ClientUser`, available after `on_connect`.
|
|
168
|
+
- `client.is_running` tells you whether the socket is open.
|
|
169
|
+
|
|
170
|
+
| Member | Description |
|
|
171
|
+
| --- | --- |
|
|
172
|
+
| `start()` | Connect and run forever. |
|
|
173
|
+
| `close()` | Disconnect once started (replaces the passive `close()`). |
|
|
174
|
+
| `event(name=None)` | Decorator registering an event listener. |
|
|
175
|
+
| `subscribe_to(user)` / `unsubscribe_from(user)` | Follow a streamer's events. Requires a running client and a live user. |
|
|
176
|
+
| `process_commands(message)` | Parse a `Message` as a command. |
|
|
177
|
+
| `sync_commands()` | Push your app commands to VIBES. Call it in `on_connect`. |
|
|
178
|
+
| `load_extension(name)` / `unload_extension(name)` | See [Extensions](#extensions). |
|
|
179
|
+
| `commands` / `app_commands` | The registered `Command` / `AppCommand` objects. |
|
|
180
|
+
|
|
181
|
+
## Events
|
|
182
|
+
|
|
183
|
+
Register listeners with `@client.event()`. The event name is taken from the function name
|
|
184
|
+
(`on_connect` → `connected`), or you can pass one explicitly: `@client.event("chat_message")`.
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
@client.event()
|
|
188
|
+
async def on_stream_online(stream: vibes.Stream):
|
|
189
|
+
print(f"{stream.channel} is live: {stream.title}")
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
| Listener | Arguments | Fires when |
|
|
193
|
+
| --- | --- | --- |
|
|
194
|
+
| `on_connect` | — | The socket is connected and `client.user` is populated. |
|
|
195
|
+
| `on_message` | `message: Message` | A chat message is sent. |
|
|
196
|
+
| `on_message_delete` | `message: Message` | A *cached* message is deleted. |
|
|
197
|
+
| `on_viewer_join` | `stream: Stream, viewer: Viewer` | Someone starts watching a subscribed stream. |
|
|
198
|
+
| `on_viewer_leave` | `stream: Stream, viewer: Viewer` | Someone stops watching. |
|
|
199
|
+
| `on_new_follower` | `streamer: User, viewer: User` | A streamer you subscribe to gains a follower. |
|
|
200
|
+
| `on_stream_like` | `stream: Stream, viewer: Viewer` | Someone likes a stream. |
|
|
201
|
+
| `on_stream_online` | `stream: Stream` | A subscribed stream goes live. |
|
|
202
|
+
| `on_stream_offline` | `stream: Stream` | A subscribed stream ends. |
|
|
203
|
+
| `on_donation` | `donation: Donation` | A donation arrives. |
|
|
204
|
+
| `on_command_error` | `error: CommandError \| Exception` | A command fails (see [Error handling](#error-handling)). |
|
|
205
|
+
|
|
206
|
+
Listeners run as independent asyncio tasks — a slow listener doesn't block the others. Exceptions
|
|
207
|
+
raised inside a listener are logged, not propagated.
|
|
208
|
+
|
|
209
|
+
There is also an internal `_debug` event that receives every raw WebSocket frame:
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
@client.event()
|
|
213
|
+
async def _debug(raw: dict):
|
|
214
|
+
...
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Commands
|
|
218
|
+
|
|
219
|
+
VIBES supports two kinds of commands. Both accept `role` and `cooldown` options.
|
|
220
|
+
|
|
221
|
+
### Text commands
|
|
222
|
+
|
|
223
|
+
Plain chat messages starting with your prefix. You must wire `process_commands` into `on_message`:
|
|
224
|
+
|
|
225
|
+
```python
|
|
226
|
+
from vibes import commands
|
|
227
|
+
from vibes.commands import Context
|
|
228
|
+
|
|
229
|
+
@commands.command() # invoked as "!hello Ada"
|
|
230
|
+
async def hello(ctx: Context, name: str = "world"):
|
|
231
|
+
await ctx.message.reply(f"Hello, {name}!")
|
|
232
|
+
|
|
233
|
+
@client.event()
|
|
234
|
+
async def on_message(message: vibes.Message):
|
|
235
|
+
if message.author and not message.author.bot and message.content.startswith(client.prefix):
|
|
236
|
+
await client.process_commands(message)
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
`ctx` is always the first parameter and is a `Context`:
|
|
240
|
+
|
|
241
|
+
| Attribute | Description |
|
|
242
|
+
| --- | --- |
|
|
243
|
+
| `ctx.message` | The triggering `Message`. Use `await ctx.message.reply(...)` to respond. |
|
|
244
|
+
| `ctx.author` | The `Viewer` who sent the message. |
|
|
245
|
+
| `ctx.stream` / `ctx.chat` | The `Stream` and its `StreamChat` (`await ctx.chat.send(...)`). |
|
|
246
|
+
| `ctx.command` | The `Command` being run. |
|
|
247
|
+
| `ctx.client` | The `Client`. |
|
|
248
|
+
| `ctx.invocation_id` | The invocation ID (message ID for text commands). |
|
|
249
|
+
|
|
250
|
+
### Parameters
|
|
251
|
+
|
|
252
|
+
Parameter types come from annotations. Text commands accept `str`, `int` and `float`; app commands
|
|
253
|
+
also accept `Viewer`. A parameter with a default is optional, and when a message contains more words
|
|
254
|
+
than the command takes parameters, the leftovers are merged into the last one — so `!hello Ada
|
|
255
|
+
Lovelace` passes the full name as a single string, and `!donate 10 Have a good one` passes
|
|
256
|
+
`message="Have a good one"`.
|
|
257
|
+
|
|
258
|
+
```python
|
|
259
|
+
@commands.command()
|
|
260
|
+
async def donate(ctx: Context, amount: int, message: str = "No message"):
|
|
261
|
+
...
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
If the user writes something that can't be converted (`!donate abc`), the command is skipped and a
|
|
265
|
+
`BadArgument` error is raised.
|
|
266
|
+
|
|
267
|
+
### Roles
|
|
268
|
+
|
|
269
|
+
Gate a command behind a minimum role — `Role.VIEWER` (default), `Role.MOD` or `Role.STREAMER`.
|
|
270
|
+
Roles are comparable, so the framework checks `ctx.author.role < command.required_role`.
|
|
271
|
+
|
|
272
|
+
```python
|
|
273
|
+
@commands.command(role=vibes.Role.MOD)
|
|
274
|
+
async def purge(ctx: Context):
|
|
275
|
+
...
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### Cooldowns
|
|
279
|
+
|
|
280
|
+
`Cooldown(period, max_uses=1, target=Cooldown.VIEWER)` limits how often a command can run.
|
|
281
|
+
|
|
282
|
+
| Target | Cooldown applies to |
|
|
283
|
+
| --- | --- |
|
|
284
|
+
| `Cooldown.VIEWER` | One viewer in one stream chat (default). |
|
|
285
|
+
| `Cooldown.USER` | One user across all streams. |
|
|
286
|
+
| `Cooldown.CHAT` | The entire chat, shared by everybody. |
|
|
287
|
+
|
|
288
|
+
```python
|
|
289
|
+
@commands.command(cooldown=commands.Cooldown(period=10, max_uses=2, target=commands.Cooldown.USER))
|
|
290
|
+
async def coinflip(ctx: Context):
|
|
291
|
+
...
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### App commands
|
|
295
|
+
|
|
296
|
+
Native VIBES commands, registered with the API instead of parsed from chat. Use `describe` to give
|
|
297
|
+
each option a description shown in the VIBES UI, and put the command description in the docstring.
|
|
298
|
+
Call `client.sync_commands()` in `on_connect` to publish them.
|
|
299
|
+
|
|
300
|
+
```python
|
|
301
|
+
@commands.app_command()
|
|
302
|
+
@commands.describe(name="Your name", age="Your age")
|
|
303
|
+
async def intro(ctx: Context, name: str, age: int):
|
|
304
|
+
"""Tells everyone who you are."""
|
|
305
|
+
await ctx.message.reply(f"{ctx.author} is {name}, {age} years old!")
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
App command handlers receive parsed options as **keyword arguments**, and `ctx.invocation_id` is the
|
|
309
|
+
invocation ID supplied by the server.
|
|
310
|
+
|
|
311
|
+
### Error handling
|
|
312
|
+
|
|
313
|
+
Command errors are never raised out of `process_commands` — they're dispatched to a single
|
|
314
|
+
`on_command_error` listener, which receives either a `CommandError` subclass or an unexpected
|
|
315
|
+
`Exception` from your callback.
|
|
316
|
+
|
|
317
|
+
| Error | Extra attributes |
|
|
318
|
+
| --- | --- |
|
|
319
|
+
| `CommandNotFound` | — |
|
|
320
|
+
| `NoPermission` | — |
|
|
321
|
+
| `BadArgument` | `.parameter` (`CommandParameter`) |
|
|
322
|
+
| `MissingArguments` | — |
|
|
323
|
+
| `OnCooldown` | `.cooldown`, `.available_after` (seconds) |
|
|
324
|
+
|
|
325
|
+
Every `CommandError` carries `.context` (the `Context`).
|
|
326
|
+
|
|
327
|
+
```python
|
|
328
|
+
from vibes.commands import CommandError, BadArgument, MissingArguments, NoPermission, OnCooldown
|
|
329
|
+
|
|
330
|
+
@client.event()
|
|
331
|
+
async def on_command_error(error: CommandError):
|
|
332
|
+
if not isinstance(error, CommandError):
|
|
333
|
+
logging.exception("Unexpected command error", exc_info=error)
|
|
334
|
+
return
|
|
335
|
+
|
|
336
|
+
ctx = error.context
|
|
337
|
+
if isinstance(error, BadArgument):
|
|
338
|
+
await ctx.message.reply(f"Invalid value for `{error.parameter.name}`.")
|
|
339
|
+
elif isinstance(error, MissingArguments):
|
|
340
|
+
await ctx.message.reply("Missing required argument(s).")
|
|
341
|
+
elif isinstance(error, NoPermission):
|
|
342
|
+
await ctx.message.reply("You don't have permission to do that.")
|
|
343
|
+
elif isinstance(error, OnCooldown):
|
|
344
|
+
await ctx.message.reply(f"Try again in {error.available_after:.1f}s.")
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
If no `on_command_error` listener is registered, user-input errors are swallowed silently and
|
|
348
|
+
unexpected exceptions are logged.
|
|
349
|
+
|
|
350
|
+
## Extensions
|
|
351
|
+
|
|
352
|
+
An extension is a module that exposes a `setup(client)` coroutine. Commands and listeners defined
|
|
353
|
+
inside it can be removed cleanly at runtime.
|
|
354
|
+
|
|
355
|
+
`my_extension.py`:
|
|
356
|
+
|
|
357
|
+
```python
|
|
358
|
+
import vibes
|
|
359
|
+
from vibes import commands
|
|
360
|
+
from vibes.commands import Context
|
|
361
|
+
from vibes.ext import Extension, event_listener
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
class MyExtension(Extension):
|
|
365
|
+
def __init__(self, client):
|
|
366
|
+
self.client: vibes.Client = client
|
|
367
|
+
|
|
368
|
+
async def ext_load(self):
|
|
369
|
+
print("Extension loaded")
|
|
370
|
+
|
|
371
|
+
async def ext_unload(self):
|
|
372
|
+
print("Extension unloaded")
|
|
373
|
+
|
|
374
|
+
@event_listener() # events inside an extension use this decorator
|
|
375
|
+
async def on_connect(self):
|
|
376
|
+
print("Connected!")
|
|
377
|
+
|
|
378
|
+
@commands.command(role=vibes.Role.MOD)
|
|
379
|
+
async def ping(self, ctx: Context):
|
|
380
|
+
await ctx.message.reply("Pong!")
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
async def setup(client): # the entry point
|
|
384
|
+
await client.add_ext(MyExtension(client))
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
`main.py`:
|
|
388
|
+
|
|
389
|
+
```python
|
|
390
|
+
await client.load_extension("my_extension") # without ".py"
|
|
391
|
+
...
|
|
392
|
+
await client.unload_extension("my_extension")
|
|
393
|
+
await client.sync_commands() # push removed app commands to VIBES
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
The extension class must live in an importable module — it can't be defined in `__main__`.
|
|
397
|
+
|
|
398
|
+
`load_extension` raises one of `ExtensionNotFound`, `ExtensionAlreadyLoaded`, `NoEntryPointError`
|
|
399
|
+
(from `vibes.ext.errors`) or `ExtensionFailed`.
|
|
400
|
+
|
|
401
|
+
## Models reference
|
|
402
|
+
|
|
403
|
+
| Type | Key attributes |
|
|
404
|
+
| --- | --- |
|
|
405
|
+
| `User` | `id`, `username`, `mention`, `avatar_url`, `bio`, `followers`, `created_at`, `url`, `is_live`, `stream`, `premium` |
|
|
406
|
+
| `ClientUser` | Everything on `User`, plus `name`, `visibility`, `scopes`, `channels` |
|
|
407
|
+
| `Viewer` | Everything on `User`, plus `display_name`, `role`, `bot`, `viewer_id` |
|
|
408
|
+
| `Role` | `name`, `level`; predefined as `Role.VIEWER`, `Role.MOD`, `Role.STREAMER` |
|
|
409
|
+
| `Stream` | `id`, `title`, `channel`, `category`, `viewers`, `likes`, `mature`, `thumbnail_url`, `url`, `started_at`, `duration`, `chat` |
|
|
410
|
+
| `StreamChannel` | `username`, `avatar_url`, `url`, `premium` |
|
|
411
|
+
| `StreamCategory` | `slug`, `name` |
|
|
412
|
+
| `StreamChat` | `await chat.send(content)` |
|
|
413
|
+
| `Message` | `id`, `content`, `author`, `streamer`, `stream`, `reference`, `created_at`, `system`, `await message.reply(content)` |
|
|
414
|
+
| `MessageReference` | `id`, `author`, `content` |
|
|
415
|
+
| `Donation` | `streamer`, `user`, `amount`, `amount_usd`, `currency`, `message` |
|
|
416
|
+
| `Application` | `url`, `key`, `owner`, `rate_limit`, `utm_source`, `mature_allowed`, `webhook` |
|
|
417
|
+
| `APIKey` | `name`, `created_at` |
|
|
418
|
+
| `ApplicationOwner` | `username`, `url` |
|
|
419
|
+
| `ClientWebhook` | `url`, `scope`, `active` |
|
|
420
|
+
| `PremiumSubscription` | `name`, `level`; reached via `user.premium` / `channel.premium` |
|
|
421
|
+
|
|
422
|
+
All datetime attributes (`created_at`, `started_at`) are timezone-aware `datetime.datetime` objects.
|
|
423
|
+
|
|
424
|
+
`User` and `Viewer` compare by ID, and `str(user)` gives the username.
|
|
425
|
+
|
|
426
|
+
## Examples
|
|
427
|
+
|
|
428
|
+
Runnable examples live in [`examples/`](examples/):
|
|
429
|
+
|
|
430
|
+
| File | Shows |
|
|
431
|
+
| --- | --- |
|
|
432
|
+
| [`quickstart.py`](examples/quickstart.py) | The smallest useful bot: connect, subscribe, reply. |
|
|
433
|
+
| [`events.py`](examples/events.py) | Every event listener and the arguments it receives. |
|
|
434
|
+
| [`rest_api.py`](examples/rest_api.py) | `PassiveClient` for REST-only scripts. |
|
|
435
|
+
| [`text_commands.py`](examples/text_commands.py) | Text commands, typed parameters, roles and cooldowns. |
|
|
436
|
+
| [`app_commands.py`](examples/app_commands.py) | Native app commands, `describe` and `sync_commands`. |
|
|
437
|
+
| [`extensions.py`](examples/extensions.py) | Loading and unloading an extension. |
|
|
438
|
+
| [`example_extension.py`](examples/example_extension.py) | The extension itself. |
|
|
439
|
+
|
|
440
|
+
## License
|
|
441
|
+
|
|
442
|
+
MIT — see [LICENSE](LICENSE).
|