diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2ec254c --- /dev/null +++ b/.gitignore @@ -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 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a2466d7 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..defd2c3 --- /dev/null +++ b/README.md @@ -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. diff --git a/pyproject.toml b/pyproject.toml index 4ec4de1..9517da3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,14 +6,34 @@ build-backend = "setuptools.build_meta" name = "veretube-bot" version = "0.1.0" description = "Python bot library for veretube sync channels" +readme = "README.md" +license = "MIT" 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 = [ "python-socketio[client]>=5.0", "requests>=2.28", ] [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] where = ["."]