Repository navigation
feat(chat): render Twitch subs, gifts, raids, announcements and cheers - #11
Merged
Merged
Conversation
Capture USERNOTICE in the recorder and show it in VOD replay and live chat close to the Twitch web chat. - irc_command() routes IRC lines on the real command token; message text such as "CLEARCHAT #x" is no longer misrouted (recorder and TTS bot). - parse_usernotice() maps sub/resub/subgift/mystery gifts/pay-forward/ raid/bits badge/announcement/milestone/charity into a strictly allowlisted "notice" event; sanitize_event() validates it. - parse_privmsg_meta()/sanitize_meta() keep bits, highlighted message, first message, reply parent and power-ups as optional "meta". - API adds a localized event.text (Flask-Babel, 11 catalogs), never sends Twitch's raw system-msg, passes meta through, and includes kind + text in the duplicate token so several gift subs stay separate. - VOD chat chunks now go through the same sanitizer as the range API. - chat_emotes.js: notice renderer with icons, bits pills in Twitch tier colours, meta line classes, decorateLine() for reply/first context; wired into vod, live, mobile, Twitch watch and the OBS overlay. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
WEBUI.webui runs init_db() only on its first import. When another test module imports it first inside a patched temp base, the tables land in that (later deleted) database and ChatApiTests hit an empty one: "sqlite3.OperationalError: no such table: users" in reversed order. Give each ChatApiTests test its own base directory and an initialized database, like StorageTestBase. No assertion changed (78 before/after). Verified: full suite 206 tests OK (skipped=1) in normal and reversed order; storage -> ChatApiTests and reliability -> ChatApiTests green. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Share button built its onclick handler with
shareClip('{{ request.args.get('clip_name') }}'). Jinja HTML-escapes the
quote to ', but the browser decodes attribute entities before the JS
runs, so a crafted ?clip_ok=1&clip_name= link could break out of the
string and run script when a signed-in user clicked Share.
Render the value with |tojson inside a single-quoted attribute, so the
argument is always one JS string literal. A double-quoted attribute
would be cut short by tojson's double quotes; the new test caught that.
ClipEditorShareXssTests requests the editor with five payloads, decodes
the onclick attribute as a browser does and requires the single argument
to equal the payload. Red before the fix (errors=5), green after; full
suite 207 tests OK (skipped=1).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Twitch colours become --sv-* tokens; text colours come from the theme. - Bits text tints lightened to >= 4.5:1 on all five themes and tints (tier 100 was 2.58:1, 10000 3.20:1, 5000 4.22:1, 1 4.34:1). - Reply context uses --tube-text-2 instead of --tube-muted (3.78:1 in cyber). - Notice lines hide the empty badge/name slots, so the header aligns with the timestamp instead of sitting below an empty line box. - Highlight, first message and notices share stripe and radius tokens. - Notice body omits an empty badge wrapper (stray indent). - OBS overlay: same tokens, plus first message, power-ups and the points/mod pills that rendered as plain text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts: - WEBUI/webui.py: keep master's BlueprintSetupState import and the new chat_items_from_chunk_file(); chat_vod now registers on vod_bp. - messages.mo (de): rebuilt from the merged messages.po. The catalogs were re-synced with the merged sources (pybabel update). Two login messages that master changed had stale translations in 10 locales; they are translated, check_translations reports 0 problems. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…elity Conflicts: - WEBUI/static/style.css: master reformatted the neighbouring chat rules; the Twitch chat block (--sv-* tokens) is kept unchanged. The tokens it uses (--tube-text, --tube-text-2) still exist on master. - Catalogs (.po/.pot/.mo, 11 locales): merged per message. Master's catalogs are the base (new UI strings from PR #13); entries missing there come from this branch (Twitch chat notices). Then extract, update --ignore-obsolete (master keeps no obsolete entries) and compile. check_translations: 1132/1132 in every locale, 0 problems. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
VOD replay and live chat now show Twitch chat events almost 1:1: subs, resubs, gifted subs, community gifts, pay-forwards, raids, announcements (with colour), bits/cheers, highlighted messages, first messages, replies, power-ups and channel point rewards.
Rendered from
chat_emotes.js+style.csswith one case each: resub, mystery gift, subgift, raid, BLUE announcement, cheer + first message, reply, highlight, reward, timeout, plus a normal line for comparison.Changes per file
recorder/chat_events.pyirc_command(line)skips tags and prefix and returns the real command.parse_usernotice(tags, message)maps themsg-param-*tags into a{"type": "notice", "kind": ...}event (sub/resub, subgift/anonsubgift, submysterygift, giftpaidupgrade, pay-forward, raid, bitsbadgetier, announcement, viewermilestone, charitydonation, sharedchatnotice → source-msg-id,anon,system).sanitize_eventvalidates notices with a strict allowlist: kind^[a-z0-9_-]{1,40}$, plan/color from fixed sets, ints > 0 and capped, text fields whitespace-normalised and cut to 80 characters (system-msg 300), unknown fields dropped.parse_privmsg_meta/sanitize_metahandlebits,highlight,first,reply{user, msg ≤ 140} andpower.recorder/Streamrecorder.pyirc_cmd == 'PING' / 'CLEARCHAT' / 'CLEARMSG' / 'USERNOTICE' / 'PRIVMSG'. A message text likeCLEARCHAT #xis no longer misrouted. USERNOTICE is written throughappend_chat_log(live log + session log withrel), with emotes, badges and colour taken fromprocess_twitch_message. PRIVMSG entries get an optionalmeta.core/tts_bot.pyparse_privmsgnow checks the command token instead of" PRIVMSG " in rest, so a USERNOTICE whose text contains "PRIVMSG" no longer triggers TTS.WEBUI/webui.pyformat_notice_text(event, user)builds Twitch-style headlines with Flask-Babel (gettext/ngettext) and falls back toevent.systemfor unknown kinds.chat_item_from_rowsetsevent.text, never sendssystem, and passesmetathrough. The duplicate token for notices includeskindand the notice text, so several gift subs from one gifter to different recipients stay separate. VOD chat chunks (?chunk=N) used to send the raw JSONL rows unfiltered. They now go throughchat_item_from_rowwith the same duplicate check, served asprivate, max-age=600withVary: Cookie, Accept-Languagebecause the text depends on the locale.WEBUI/static/chat_emotes.jsrenderNoticeHtml(header with an SVG icon per group sub/gift/raid/announcement/bits/other, then badges, name in the user's colour and the message with emotes).chatLineClassNameadds `chat-line-notice[-WEBUI/static/style.css#9147ffleft border on a light purple background. Announcementborder-imagegradients: BLUE#00d6d6→#9146ff, GREEN#00db84→#57bee6, ORANGE#ffb31a→#e0e000, PURPLE/PRIMARY#9146ff→#ff75e6. Highlight#755ebc, first message, reply context, cheer tiers, gigantified/animated power-ups.WEBUI/templates/vod.html,live.html,mobile_player.html,twitch_watch_chat.js.html,chat_obs.htmldecorateLineafter.chat-msgis filled and usechatEventKeyin the duplicate keys. The VOD text filter also searchesevent.text, and notices stay visible under badge filters.chat_obs.htmlgets the notice classes and inline styles. It also fixes two bugs on lines this PR already touches:renderBadgesHtmlgot the array instead of the item, anduserColorgot the user name, so the overlay showed no badges and no name colours.WEBUI/i18n.py,WEBUI/translations/*JS_MESSAGES. 26 new msgids are translated in all 11 catalogs (pl with 3 plural forms; ja, ko, zh_Hans and tr with 1)..poand.moare committed. Catalogs were updated with--no-wrapto match the existing unwrapped.pofiles; the.potuses the standard extract.tests/test_chat_events.pydocs/screenshots/chat_twitch_events.pngLimits
channel.channel_points_custom_reward_redemption.addwith broadcaster authorisation. This PR only documents that and does not implement it. Rewards with text input keep working as before (custom-reward-idon PRIVMSG).@parentmention in the body. Removing it would shift the native emote offsets.format_moderation_message) are still German strings from the recorder. That predates this PR and is unchanged here.Test plan
python -m unittest discover -s tests -p "test_*.py"→Ran 206 tests,OK (skipped=1), exit 0 (178 existing + 28 new)python -m compileall -q core recorder WEBUI bin run_transcribe.py tests→ exit 0uvx ruff@0.17.3 check .→All checks passed!node --check WEBUI/static/chat_emotes.js→ exit 0python -m py_compileon every changed.pyfile → exit 0python scripts/check_translations.py→ 11 locales, 1109/1109 translated, 0 problems eachchat_emotes.js+style.css; screenshot above, no console errorsNew tests cover:
irc_command(USERNOTICE; PRIVMSG with "CLEARCHAT #x" in the text; PING), resub with message and streak, subgift with sender_count, anonymous submysterygift, raid (displayName/viewerCount), announcement colour, unknown kind → system-msg, invalid kind → None,sanitize_eventdropping bad plan/color/negative numbers/foreign fields,parse_privmsg_meta(bits/first/highlight/reply; first-msg=0 → None), the TTS bot routing,process_twitch_messageon USERNOTICE, and API tests where two gift subs from one gifter return 2 items with the recipients inevent.text, nosystem, andmetapassed through (range API and chunk API).Design pass (commit 50f9ad4)
Checked in all five themes (vault, aurora, cyber, ember, ocean), the VOD/live grid at 360 px, mobile at 375 px and the OBS overlay.
--sv-*tokens; text colours come from the theme (--tube-*).--tube-text-2instead of--tube-muted(3.78:1 in cyber).Merge with master (commit 8083a46)
webui.py: keeps master's blueprint import, andchat_vodnow registers onvod_bpwith the sanitized chunk serving.de/messages.mois rebuilt.Ran 338 tests,OK (skipped=20), ruff and compileall exit 0, check_translations 0 problems.🤖 Generated with Claude Code