A small asynchronous relay that reads chat messages from Kick and YouTube and posts them into a Twitch chat via the official Helix chat/messages endpoint.
This repo contains the bot itself and a minimal OAuth helper to obtain and refresh Twitch tokens.
Heads‑up: Keep your secrets out of Git. Use
.env(not committed) and the provided.env.exampleas a template.
- ✅ Relays Kick and YouTube Live Chat messages into Twitch.
- ✅ Uses Twitch Helix
POST /helix/chat/messages(no IRC). - ✅ Auto‑refresh of Twitch access token at runtime (after first OAuth).
- ✅ Optional prefixes (e.g.,
🟢[KICK],🔴[YT]) so you can tell sources apart. - ✅ Works fine under systemd for 24/7 operation.
- ✅ OAuth helper (
auth_server.py) to securely obtain tokens. - ✅ Pluggable moderation: word/phrase list (
banned_words.txt) with hot‑reload (mtime watcher) and whole‑word regex matching (no partial censoring inside larger words).
chatbot.py # The relay bot (Kick/YouTube -> Twitch)
auth_server.py # Tiny OAuth2 web app to obtain/refresh Twitch tokens
.env.example # Environment variable template (no secrets)
requirements.txt # Python dependencies
.gitignore # Ignores secrets and local files
- Python 3.10+ (tested on 3.12)
- A Twitch Developer Application (Client ID + Client Secret)
- A YouTube Data API v3 key
- (Production) An HTTPS endpoint for the OAuth callback (e.g., Cloudflare Tunnel)
# clone & enter the repo
git clone <your-repo-url>
cd <repo>
# (recommended) virtual environment
python3 -m venv .venv
source .venv/bin/activate
# dependencies
pip install -r requirements.txtCreate your env file from the template and fill it out:
cp .env.example .envTWITCH_CLIENT_ID/TWITCH_CLIENT_SECRET– from your Twitch Dev app.TWITCH_SCOPES– leave asuser:write:chat user:bot.TWITCH_TOKENS_FILE– path to token cache file (defaulttwitch_tokens.json).TWITCH_BOT_TOKEN/TWITCH_REFRESH_TOKEN/TWITCH_TOKEN_EXPIRES_AT– populated automatically after you authorize viaauth_server.py.TWITCH_BROADCASTER_ID– the channel user ID that receives messages.TWITCH_SENDER_ID– the bot account user ID that posts the messages (can be same as broadcaster).
After successful OAuth, the helper page shows the logged-in username and
user_id.
If you want the bot to post as the channel owner, set both IDs to that same user ID.
YOUTUBE_API_KEY– your Data API v3 key.YOUTUBE_CHANNEL_ID– the canonical channel ID (UC…), recommended.YOUTUBE_CHANNEL_HANDLE– optional fallback handle (e.g.,@yourhandle).YOUTUBE_LIVE_CHAT_ID– useAUTOso the bot resolves the current live chat automatically.YOUTUBE_VIDEO_ID– set only when testing unlisted streams (directly resolve that video’s chat).ENABLE_YT/ENABLE_KICK–true/falsetoggles.KICK_CHANNEL– Kick username.YT_MIN_POLL_MS– minimum wait between YouTube chat polls (e.g.,15000= 15s).
Add a plain text file (recommended name: banned_words.txt) and point the bot to it with:
BANNED_WORDS_FILE=banned_words.txt
Each non‑empty line = one word or multi‑word phrase to match. Lines starting with # are treated as comments and ignored. The bot builds a single compiled regex where every entry is wrapped in \b word boundaries:
\bWORD_OR_PHRASE\b
This means you don’t accidentally censor substrings inside longer words (e.g. ass won’t match pass, cat won’t match educate). Multi‑word phrases still work; the boundary applies only at the start of the first word and end of the last word. Matching is case‑insensitive by default.
Variables:
| Variable | Default | Description |
|---|---|---|
BANNED_WORDS_FILE |
(unset) | Path to the word/phrase list file. If unset, watcher is skipped. |
BAN_MODE |
censor |
censor = replace each hit with repeat of BAN_CHAR; drop = skip the entire message. |
BAN_CHAR |
* |
Replacement character when censoring. Repeated for the full length of the matched word/phrase. |
BAN_CASE_SENSITIVE |
false |
Set to true / 1 / yes to make matches case‑sensitive. |
BAN_WATCH_INTERVAL |
600 |
Seconds between modification‑time checks of the list file (hot‑reload). Set lower (e.g. 5) for faster reloads. |
Hot‑reload mechanism:
The tiny watcher coroutine simply checks the file’s modification time (stat().st_mtime) every BAN_WATCH_INTERVAL seconds. If it changed, it re‑reads and recompiles the regex—no extra dependencies (no inotify, watchdog, etc.). If the file is removed, the banned list is cleared until it reappears.
Edge cases / notes:
- Blank lines and comment lines are ignored during matching (but preserved if you manage the file manually).
- Because of
\bboundaries, punctuation immediately after a word (e.g.word!) still counts (word boundary is between the last alphanumeric and the punctuation). - To match something that is not word‑character delimited (e.g. an emoji sequence or symbols), you can surround it with spaces in the file, or add a variant without relying on boundaries (future enhancement: optional raw regex mode if needed).
- Long lists: A few hundred entries are fine; they compile once per change. If you go into thousands, consider benchmarking startup.
Example banned_words.txt snippet:
# Sexual content
anal
blow job
# Threats
kill yourself
# Spam
free crypto giveaway
Switching modes:
BAN_MODE=drop # silently ignore whole messages containing a banned phrase
BAN_MODE=censor # (default) replace each matched word/phrase with *****
BAN_CHAR=# # use # instead of *
BAN_WATCH_INTERVAL=30 # check for file updates every 30s
Disable moderation: simply unset / remove BANNED_WORDS_FILE (the watcher prints a notice and exits).
Maintenance helpers: Use the included script to clean and sort your list:
python check_duplicate_lines.py banned_words.txt --fix
--fix = dedupe (case‑insensitive, trimmed), sort A→Z, and write back (creates a one‑time .bak backup).
AUTH_BIND_HOST– usually127.0.0.1in production.AUTH_PUBLIC_BASE– the public base URL (HTTPS) that Twitch will redirect to, e.g.https://twitch-auth.example.com.
-
Set an OAuth Redirect URL in your Twitch Developer app:
- Local testing:
http://localhost:3750/callback - Production:
https://twitch-auth.example.com/callback(must be HTTPS)
- Local testing:
-
Run the OAuth helper:
python auth_server.py
-
Open:
- Local:
http://localhost:3750/login - Prod:
https://twitch-auth.example.com/login
Login & authorize → you should see “Bot authorized ✅”.
The helper writesTWITCH_BOT_TOKEN,TWITCH_REFRESH_TOKEN,TWITCH_TOKEN_EXPIRES_ATinto.envand also stores them intwitch_tokens.json. - Local:
-
Stop the helper (Ctrl+C). You only need it again if you need to re‑authorize.
The bot code automatically refreshes the access token while running using the refresh token.
python chatbot.pyCreate /etc/systemd/system/chatbot.service:
[Unit]
Description=Chat Relay Bot (Kick + YouTube -> Twitch)
After=network-online.target
Wants=network-online.target
[Service]
User=<your-linux-user>
Group=<your-linux-user>
WorkingDirectory=/full/path/to/repo
Environment="PYTHONUNBUFFERED=1"
ExecStart=/full/path/to/repo/.venv/bin/python /full/path/to/repo/chatbot.py
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.targetEnable & start:
sudo systemctl daemon-reload
sudo systemctl enable --now chatbot.service
journalctl -u chatbot.service -f- Kick → Twitch: Uses
kickpythonto read chat; forwards each message to Twitch (1 msg/sec to respect limits). - YouTube → Twitch: Resolves the active live chat (
activeLiveChatId) and pollsliveChatMessages.list; forwards messages to Twitch. - Twitch posting: Uses Helix
POST /helix/chat/messageswith your bot user token. Messages are trimmed to 500 chars and rate‑limited to 1 msg/sec per channel. - Moderation flow: Incoming message → regex whole‑word scan → if any match: either censored (character repeat per span) or dropped entirely based on
BAN_MODE.
For YouTube: public live discovery uses
search.list(quota‑heavy). For unlisted/private testing, setYOUTUBE_VIDEO_IDto skip the search and resolve the chat directly.
- Discovery (
search.list): expensive. Check infrequently (e.g., every 10–15 minutes) when idle. - Chat polling (
liveChatMessages.list): set a sensible floor viaYT_MIN_POLL_MS(e.g., 10–15 seconds) to stay within the default 10k daily quota.
If you hit quotaExceeded (403), increase intervals or wait until the daily reset.
- Twitch 401/403 – Re‑run
auth_server.pyand authorize again. - YouTube live not found – Ensure the stream is public and chat is enabled; for unlisted testing, define
YOUTUBE_VIDEO_ID. - Kick import error –
pip install curl_cffi websockets kickpython. - Slow relay – Twitch is limited to 1 message/sec; bursts are queued and delivered in order.
Logs:
journalctl -u chatbot.service -n 200 --no-pager- Never commit
.envortwitch_tokens.json(already ignored in.gitignore). - Regenerate Client Secret / API keys if they were exposed.
- Use HTTPS for the OAuth callback in production (Cloudflare Tunnel works great).
