# 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 ``` ## Async quick start ```python import asyncio from veretube_bot import AsyncBot async def main(): bot = AsyncBot( token="cbt_...", channel="mychannel", socket_url="http://your-server:1337", api_url="http://your-server:8080/api/v1", transports=["websocket"], # optional ) @bot.on("login") async def on_login(data): if data.get("success"): await bot.send_message("hello from async bot") await bot.connect(timeout=10) await asyncio.sleep(1) await bot.disconnect() asyncio.run(main()) ``` ## 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 transports=None, # e.g. ["websocket"] to bypass polling/upgrade issues 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 and wait until `/` namespace is ready | | `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. ## Shows Show endpoints manage scheduled playlist runs. They support bot Bearer auth and session auth. ```python shows = bot.list_shows() # rank >= MOD public_shows = bot.list_public_shows() # read-only public schedule show = bot.get_show(show_id) # rank >= MOD payload = { "name": "Friday Prime", "scheduled_for": "2026-05-22T19:00:00.000Z", "timezone": "America/New_York", "recurrence": "weekly", "fill_mode": "replace", "conflict_mode": "force", "start_playback": True, "playlist": [ {"type": "yt", "id": "dQw4w9WgXcQ", "pos": "end"} ], "status": "scheduled", "notes": "

Special guest this week.

", "color": "#22AAEE", } created = bot.create_show(payload) # rank >= MOD updated = bot.update_show(show_id, payload) # rank >= MOD bot.delete_show(show_id) # rank >= ADMIN bot.show_action(show_id, "pause") # rank >= MOD bot.show_action(show_id, "resume") # rank >= MOD bot.show_action(show_id, "schedule") # rank >= MOD bot.show_action(show_id, "run") # rank >= ADMIN bot.show_action(show_id, "cancel") # rank >= ADMIN ``` Create/update payload constraints: - `timezone`: required non-empty IANA time zone region string from the IANA Time Zone Database (for example `Europe/Berlin`, `America/New_York`) - `recurrence`: `none` | `daily` | `weekly` - `fill_mode`: `append` | `replace` - `conflict_mode`: `force` | `skip` - `playlist`: required non-empty array of media entries with `type`, `id`, and optional `pos` (`next` | `end`) - `status`: `draft` | `scheduled` | `paused` | `completed` | `failed` | `canceled` (`running` is internal) - `notes`: optional `string | null` (rich HTML allowed; expected max input length 20000 chars before sanitize) - `color`: optional `string | null`, must match `^#[0-9A-Fa-f]{6}$` when provided Action payload schema: ```json { "action": "run" } ``` ## 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) bot.api.list_shows() bot.api.show_action(show_id, "run") ``` ## 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.