Upload to pypi
This commit is contained in:
parent
175448d390
commit
f6fc9ee7a2
4 changed files with 289 additions and 1 deletions
26
.gitignore
vendored
Normal file
26
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
# build output — generated by `python -m build`, uploaded via twine, not committed
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# packaging metadata — generated automatically, not edited by hand
|
||||||
|
*.egg-info/
|
||||||
|
|
||||||
|
# virtual environments
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
env/
|
||||||
|
|
||||||
|
# Python cache
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
|
||||||
|
# test / coverage output
|
||||||
|
.pytest_cache/
|
||||||
|
.coverage
|
||||||
|
htmlcov/
|
||||||
|
|
||||||
|
# editor
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 veretube contributors
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
221
README.md
Normal file
221
README.md
Normal file
|
|
@ -0,0 +1,221 @@
|
||||||
|
# veretube-bot
|
||||||
|
|
||||||
|
Python library for writing bots on sync 4.0 aka veretube channels.
|
||||||
|
|
||||||
|
Bots connect over **socket.io** for real-time events (chat, user list, playlist changes) and use a **REST API** for commands (queue media, kick/ban, manage emotes, read/write settings). Both surfaces are covered by this library.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install veretube-bot
|
||||||
|
```
|
||||||
|
|
||||||
|
## Getting a token
|
||||||
|
|
||||||
|
Tokens are issued in the channel settings modal on the **Bots** tab. You need at least moderator rank to see it. Fill in a name and rank, click **Issue Token**, and copy the token immediately — it starts with `cbt_` and is only shown once.
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
```python
|
||||||
|
from veretube_bot import Bot
|
||||||
|
|
||||||
|
bot = Bot(
|
||||||
|
token="cbt_...",
|
||||||
|
channel="mychannel",
|
||||||
|
socket_url="http://your-server:1337", # socket.io port (default 1337)
|
||||||
|
api_url="http://your-server:8080/api/v1", # HTTP port (default 8080)
|
||||||
|
)
|
||||||
|
|
||||||
|
@bot.on("chatMsg")
|
||||||
|
def on_chat(data):
|
||||||
|
if data["msg"] == "!hello":
|
||||||
|
bot.send_message("Hello!", to=data["username"])
|
||||||
|
|
||||||
|
if data["msg"] == "!np":
|
||||||
|
if bot.now_playing:
|
||||||
|
bot.send_message(f"Now playing: {bot.now_playing['title']}")
|
||||||
|
|
||||||
|
@bot.on("changeMedia")
|
||||||
|
def on_media(data):
|
||||||
|
print(f"Now playing: {data['title']}")
|
||||||
|
|
||||||
|
bot.run() # connect and block until disconnected
|
||||||
|
```
|
||||||
|
|
||||||
|
## Connection
|
||||||
|
|
||||||
|
```python
|
||||||
|
Bot(
|
||||||
|
token, # str — bot token starting with 'cbt_'
|
||||||
|
channel, # str — channel name (lowercase, no '#')
|
||||||
|
socket_url, # str — socket.io server URL (uses io.port, not the HTTP port)
|
||||||
|
api_url, # str — REST API base URL, e.g. http://host:8080/api/v1
|
||||||
|
reconnection=False, # set True to reconnect automatically on drop
|
||||||
|
reconnection_delay=3, # seconds between reconnect attempts
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
| Method | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `bot.run()` | Connect and block until disconnected |
|
||||||
|
| `bot.connect()` | Open the connection (returns immediately) |
|
||||||
|
| `bot.wait()` | Block until disconnected (call after `connect()`) |
|
||||||
|
| `bot.disconnect()` | Close the connection |
|
||||||
|
|
||||||
|
By default `reconnection=False` — the expectation is that an external process supervisor (systemd, supervisord, etc.) handles restarts. Pass `reconnection=True` for bots that should self-recover.
|
||||||
|
|
||||||
|
## Event handling
|
||||||
|
|
||||||
|
Register handlers with `@bot.on(event_name)`. Multiple handlers for the same event are all called. Exceptions in a handler are logged and skipped so one bad handler doesn't affect the others.
|
||||||
|
|
||||||
|
```python
|
||||||
|
@bot.on("chatMsg")
|
||||||
|
def on_chat(data):
|
||||||
|
print(data["username"], data["msg"])
|
||||||
|
|
||||||
|
@bot.on("userLeave")
|
||||||
|
def on_leave(data):
|
||||||
|
print(f"{data['name']} left")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Socket events
|
||||||
|
|
||||||
|
| Event | Payload | Description |
|
||||||
|
|-------|---------|-------------|
|
||||||
|
| `connect` | `None` | Socket connected |
|
||||||
|
| `disconnect` | reason string or `None` | Socket disconnected |
|
||||||
|
| `connect_error` | error object | Connection failed |
|
||||||
|
| `login` | `{ success, name, guest }` | Server accepted the token |
|
||||||
|
| `chatMsg` | `{ username, msg, meta, time }` | Chat message sent |
|
||||||
|
| `pm` | `{ username, msg, meta }` | Private message received |
|
||||||
|
| `userlist` | `[{ name, rank, meta }, ...]` | Full user list on join |
|
||||||
|
| `addUser` | `{ name, rank, meta }` | User joined |
|
||||||
|
| `userLeave` | `{ name }` | User left |
|
||||||
|
| `setUserMeta` | `{ name, meta }` | User AFK/muted state changed |
|
||||||
|
| `setUserRank` | `{ name, rank }` | User rank changed |
|
||||||
|
| `changeMedia` | `{ id, type, title, seconds, ... }` | New video started |
|
||||||
|
| `playlist` | `[{ uid, media, queueby, temp }, ...]` | Full playlist on join |
|
||||||
|
| `queue` | `{ item, after }` | Item added to playlist |
|
||||||
|
| `delete` | `{ uid }` | Playlist item removed |
|
||||||
|
| `channelOpts` | settings dict | Channel options updated |
|
||||||
|
| `clearchat` | `None` | Chat cleared by a moderator |
|
||||||
|
| `errorMsg` | `{ msg }` | Error from the server |
|
||||||
|
| `kick` | `{ reason }` | Bot was kicked |
|
||||||
|
| `announcement` | `{ title, text }` | Server-wide announcement |
|
||||||
|
| `updateEmote` | `{ name, image }` | Emote added or changed |
|
||||||
|
| `removeEmote` | `{ name }` | Emote removed |
|
||||||
|
|
||||||
|
`meta.is_bot` is `True` in `chatMsg` and userlist payloads for bots.
|
||||||
|
|
||||||
|
## In-memory state
|
||||||
|
|
||||||
|
These are updated automatically from socket events before your handlers are called:
|
||||||
|
|
||||||
|
```python
|
||||||
|
bot.users # list of { name, rank, meta } — current user list
|
||||||
|
bot.now_playing # { id, type, title, seconds, ... } or None
|
||||||
|
bot.playlist # list of playlist items
|
||||||
|
bot.channel_opts # channel options dict
|
||||||
|
```
|
||||||
|
|
||||||
|
Look up a specific user:
|
||||||
|
|
||||||
|
```python
|
||||||
|
user = bot.get_user("Alice") # returns dict or None
|
||||||
|
```
|
||||||
|
|
||||||
|
## Chat
|
||||||
|
|
||||||
|
```python
|
||||||
|
bot.send_message("Hello!")
|
||||||
|
bot.send_message("Hey!", to="Alice") # sends "Alice: Hey!"
|
||||||
|
bot.send_action("waves") # sends "/me waves"
|
||||||
|
bot.send_pm("Alice", "Hello privately")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Playlist
|
||||||
|
|
||||||
|
```python
|
||||||
|
bot.queue("dQw4w9WgXcQ", "yt") # add to end (rank >= MOD)
|
||||||
|
bot.queue("dQw4w9WgXcQ", "yt", "next") # add as next up
|
||||||
|
bot.skip() # skip to next item (rank >= MOD)
|
||||||
|
bot.skip_to(uid) # jump to specific uid (rank >= MOD)
|
||||||
|
bot.delete_item(uid) # remove by uid (rank >= ADMIN)
|
||||||
|
bot.shuffle_playlist() # shuffle (rank >= ADMIN)
|
||||||
|
bot.clear_playlist() # clear all (rank >= ADMIN)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Media types:** `yt` YouTube, `sc` SoundCloud, `tw` Twitch stream, `tc` Twitch clip, `vm` Vimeo, `dm` Dailymotion, `fi` direct file URL, `cu` custom embed.
|
||||||
|
|
||||||
|
## Moderation
|
||||||
|
|
||||||
|
```python
|
||||||
|
bot.kick("BadUser") # rank >= MOD
|
||||||
|
bot.kick("BadUser", reason="Spamming")
|
||||||
|
bot.ban("BadUser") # rank >= ADMIN
|
||||||
|
bot.ban("BadUser", reason="Evading")
|
||||||
|
bot.unban("BadUser") # rank >= ADMIN
|
||||||
|
bot.set_rank("Alice", Rank.MOD) # rank >= OWNER
|
||||||
|
```
|
||||||
|
|
||||||
|
## Emotes
|
||||||
|
|
||||||
|
Emote endpoints read/write the database directly and work even when the channel is offline.
|
||||||
|
|
||||||
|
```python
|
||||||
|
emotes = bot.get_emotes() # list of { name, image, source }
|
||||||
|
bot.add_emote("KEKW", "https://...") # rank >= OWNER
|
||||||
|
bot.update_emote("KEKW", image="https://...")
|
||||||
|
bot.update_emote("KEKW", new_name="KEKWait")
|
||||||
|
bot.delete_emote("KEKW") # rank >= OWNER
|
||||||
|
```
|
||||||
|
|
||||||
|
## Settings
|
||||||
|
|
||||||
|
```python
|
||||||
|
settings = bot.get_settings() # rank >= OWNER, channel must be active
|
||||||
|
bot.update_settings(pagetitle="Now Playing: Chill Beats")
|
||||||
|
bot.update_settings(allow_voteskip=False, afk_timeout=300)
|
||||||
|
```
|
||||||
|
|
||||||
|
Available setting keys: `allow_voteskip`, `allow_dupes`, `voteskip_ratio`, `maxlength`, `playlist_max_duration_per_user`, `afk_timeout`, `enable_link_regex`, `chat_antiflood`, `chat_antiflood_burst`, `chat_antiflood_sustained`, `new_user_chat_delay`, `new_user_chat_link_delay`, `pagetitle`, `password`, `externalcss`, `externaljs`, `show_public`, `torbanned`, `block_anonymous_users`, `allow_ascii_control`, `playlist_max_per_user`.
|
||||||
|
|
||||||
|
## Direct REST access
|
||||||
|
|
||||||
|
Every REST endpoint is also accessible on `bot.api` if you need something not covered by the shortcuts:
|
||||||
|
|
||||||
|
```python
|
||||||
|
playlist = bot.api.get_playlist() # { items, currentIndex, locked }
|
||||||
|
bot.api.skip_to(uid)
|
||||||
|
bot.api.set_user_rank("Alice", 2)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error handling
|
||||||
|
|
||||||
|
REST calls raise `BotAPIError` on failure:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from veretube_bot import Bot, BotAPIError
|
||||||
|
|
||||||
|
@bot.on("chatMsg")
|
||||||
|
def on_chat(data):
|
||||||
|
if data["msg"].startswith("!add "):
|
||||||
|
_, type, id = data["msg"].split(None, 2)
|
||||||
|
try:
|
||||||
|
bot.queue(id, type)
|
||||||
|
except BotAPIError as e:
|
||||||
|
bot.send_message(f"Error: {e}") # e.status_code has the HTTP code
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rank constants
|
||||||
|
|
||||||
|
```python
|
||||||
|
from veretube_bot import Rank
|
||||||
|
|
||||||
|
Rank.MOD # 2
|
||||||
|
Rank.ADMIN # 3
|
||||||
|
Rank.OWNER # 4
|
||||||
|
Rank.CREATOR # 5
|
||||||
|
```
|
||||||
|
|
||||||
|
A bot's effective rank is capped at the rank of the user who issued its token.
|
||||||
|
|
@ -6,14 +6,34 @@ build-backend = "setuptools.build_meta"
|
||||||
name = "veretube-bot"
|
name = "veretube-bot"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "Python bot library for veretube sync channels"
|
description = "Python bot library for veretube sync channels"
|
||||||
|
readme = "README.md"
|
||||||
|
license = "MIT"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
|
authors = [
|
||||||
|
{ name = "veretube", email = "faceeatingtumor@gmail.com" },
|
||||||
|
]
|
||||||
|
keywords = ["veretube", "cytube", "bot", "chat", "socketio"]
|
||||||
|
classifiers = [
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"Programming Language :: Python :: 3.10",
|
||||||
|
"Programming Language :: Python :: 3.11",
|
||||||
|
"Programming Language :: Python :: 3.12",
|
||||||
|
"Programming Language :: Python :: 3.13",
|
||||||
|
"Operating System :: OS Independent",
|
||||||
|
"Topic :: Communications :: Chat",
|
||||||
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||||
|
]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"python-socketio[client]>=5.0",
|
"python-socketio[client]>=5.0",
|
||||||
"requests>=2.28",
|
"requests>=2.28",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
dev = ["pytest"]
|
dev = ["pytest", "build", "twine"]
|
||||||
|
|
||||||
|
[project.urls]
|
||||||
|
Homepage = "https://veretium.com"
|
||||||
|
Source = "https://veretium.com/w0zard/veretube_bot_lib"
|
||||||
|
|
||||||
[tool.setuptools.packages.find]
|
[tool.setuptools.packages.find]
|
||||||
where = ["."]
|
where = ["."]
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue