The recent-messages API now serializes empty IRCv3 tags as bare keys (e.g. `emotes`) instead of `emotes=`, which threw a RangeError in the tag parser and silently dropped every recent message. Parse tags per the IRCv3 spec: treat bare and empty values as equivalent, split on the first `=` so values may contain `=`, and unescape values in a single pass (\: \s \ \r \n). Also guard user extraction against prefixes without `!`, and isolate per-message parse failures in _handleIRCData so one bad message can no longer abort an entire batch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
197 lines
9.5 KiB
Dart
197 lines
9.5 KiB
Dart
/// Test fixtures containing real Twitch IRC message samples.
|
|
/// These are used to test the IRC message parsing logic.
|
|
library;
|
|
|
|
/// A basic chat message (PRIVMSG) with standard tags
|
|
const basicPrivmsg =
|
|
'@badge-info=;badges=;color=#FF0000;display-name=TestUser;emotes=;'
|
|
'id=abc123-def456;mod=0;room-id=12345;subscriber=0;'
|
|
'tmi-sent-ts=1234567890123;turbo=0;user-id=67890;user-type= '
|
|
':testuser!testuser@testuser.tmi.twitch.tv PRIVMSG #channel :Hello World';
|
|
|
|
/// Exercises the full IRCv3 tag escape set plus a value that itself contains
|
|
/// '='. Uses a raw string so the backslashes are literal wire bytes.
|
|
const messageWithEscapedTagChars =
|
|
r'@a-space=one\stwo;a-semi=left\:right;a-slash=a\\b;a-equals=k=v;'
|
|
r'a-double=x\\sy;display-name=EscapeAll;mod=0 '
|
|
r':escapeall!escapeall@escapeall.tmi.twitch.tv PRIVMSG #channel :hi';
|
|
|
|
/// A message whose prefix is not the usual `nick!user@host` form (no '!').
|
|
/// The parser should fall back to the login tag instead of throwing.
|
|
const messageNoBangPrefix =
|
|
'@login=fallbackuser;mod=0;id=nobang-1 '
|
|
':some.server.example PRIVMSG #channel :server style';
|
|
|
|
/// A chat message with emotes in the tags
|
|
/// Note: emote positions are 0-indexed byte positions in the message
|
|
/// "Kappa LUL" - Kappa is at 0-4, LUL is at 6-8
|
|
const privmsgWithEmotes =
|
|
'@badge-info=subscriber/12;badges=subscriber/12,premium/1;'
|
|
'color=#1E90FF;display-name=EmoteUser;'
|
|
'emotes=25:0-4/1902:6-8;id=msg-id-123;mod=0;room-id=12345;'
|
|
'subscriber=1;tmi-sent-ts=1234567890123;turbo=0;user-id=11111;user-type= '
|
|
':emoteuser!emoteuser@emoteuser.tmi.twitch.tv PRIVMSG #channel :Kappa LUL';
|
|
|
|
/// A chat message with multiple instances of the same emote
|
|
const privmsgWithMultipleEmotes =
|
|
'@badge-info=;badges=;color=#00FF00;display-name=MultiEmote;'
|
|
'emotes=25:0-4,11-15;id=msg-456;mod=0;room-id=12345;subscriber=0;'
|
|
'tmi-sent-ts=1234567890123;turbo=0;user-id=22222;user-type= '
|
|
':multiemote!multiemote@multiemote.tmi.twitch.tv PRIVMSG #channel :Kappa test Kappa';
|
|
|
|
/// An IRC /me action message
|
|
const actionMessage =
|
|
'@badge-info=;badges=broadcaster/1;color=#8A2BE2;display-name=Broadcaster;'
|
|
'emotes=;id=action-123;mod=0;room-id=12345;subscriber=0;'
|
|
'tmi-sent-ts=1234567890123;turbo=0;user-id=12345;user-type= '
|
|
':broadcaster!broadcaster@broadcaster.tmi.twitch.tv PRIVMSG #channel '
|
|
':\x01ACTION is testing\x01';
|
|
|
|
/// A message that mentions another user
|
|
const mentionMessage =
|
|
'@badge-info=;badges=;color=#FF6347;display-name=Mentioner;emotes=;'
|
|
'id=mention-123;mod=0;room-id=12345;subscriber=0;'
|
|
'tmi-sent-ts=1234567890123;turbo=0;user-id=33333;user-type= '
|
|
':mentioner!mentioner@mentioner.tmi.twitch.tv PRIVMSG #channel :@targetuser hello!';
|
|
|
|
/// A message with escaped spaces in tags (\\s represents a space)
|
|
const messageWithEscapedSpaces =
|
|
'@badge-info=subscriber/12\\smonths;badges=subscriber/12;color=#FF0000;'
|
|
'display-name=EscapeTest;emotes=;id=escape-123;mod=0;room-id=12345;'
|
|
'subscriber=1;tmi-sent-ts=1234567890123;turbo=0;user-id=44444;user-type= '
|
|
':escapetest!escapetest@escapetest.tmi.twitch.tv PRIVMSG #channel :test message';
|
|
|
|
/// A CLEARCHAT command that bans/times out a specific user
|
|
const clearChatUser =
|
|
'@ban-duration=600;room-id=12345;target-user-id=67890;tmi-sent-ts=1234567890123 '
|
|
':tmi.twitch.tv CLEARCHAT #channel :banneduser';
|
|
|
|
/// A CLEARCHAT command that clears the entire chat (no target user)
|
|
const clearChatAll =
|
|
'@room-id=12345;tmi-sent-ts=1234567890123 '
|
|
':tmi.twitch.tv CLEARCHAT #channel';
|
|
|
|
/// A CLEARMSG command that deletes a specific message
|
|
const clearMsg =
|
|
'@login=deleteduser;room-id=;target-msg-id=target-msg-abc123;'
|
|
'tmi-sent-ts=1234567890123 '
|
|
':tmi.twitch.tv CLEARMSG #channel :deleted message content';
|
|
|
|
/// A NOTICE command (system message)
|
|
const noticeMessage =
|
|
'@msg-id=slow_on;room-id=12345 '
|
|
':tmi.twitch.tv NOTICE #channel :This room is now in slow mode.';
|
|
|
|
/// A USERNOTICE command (subscription, raid, etc.)
|
|
const userNoticeSubscription =
|
|
'@badge-info=subscriber/1;badges=subscriber/0;color=#FF69B4;'
|
|
'display-name=NewSub;emotes=;id=sub-123;login=newsub;mod=0;'
|
|
'msg-id=sub;msg-param-cumulative-months=1;msg-param-months=0;'
|
|
'msg-param-multimonth-duration=0;msg-param-multimonth-tenure=0;'
|
|
'msg-param-should-share-streak=0;msg-param-sub-plan-name=Channel\\sSub;'
|
|
'msg-param-sub-plan=1000;msg-param-was-gifted=false;room-id=12345;'
|
|
'subscriber=1;system-msg=NewSub\\ssubscribed\\sat\\sTier\\s1.;'
|
|
'tmi-sent-ts=1234567890123;user-id=55555;user-type= '
|
|
':tmi.twitch.tv USERNOTICE #channel';
|
|
|
|
/// A ROOMSTATE command with room settings
|
|
const roomState =
|
|
'@emote-only=0;followers-only=-1;r9k=0;room-id=12345;slow=0;subs-only=0 '
|
|
':tmi.twitch.tv ROOMSTATE #channel';
|
|
|
|
/// A USERSTATE command with user state info
|
|
const userState =
|
|
'@badge-info=;badges=broadcaster/1;color=#FF0000;display-name=Broadcaster;'
|
|
'emote-sets=0,300374282;mod=0;subscriber=0;user-type= '
|
|
':tmi.twitch.tv USERSTATE #channel';
|
|
|
|
/// A GLOBALUSERSTATE command
|
|
const globalUserState =
|
|
'@badge-info=;badges=premium/1;color=#8A2BE2;display-name=GlobalUser;'
|
|
'emote-sets=0,300374282;user-id=12345;user-type= '
|
|
':tmi.twitch.tv GLOBALUSERSTATE';
|
|
|
|
/// A message with Unicode that should be filtered (invalid Unicode char)
|
|
const messageWithInvalidUnicode =
|
|
'@badge-info=;badges=;color=#00FF00;display-name=UnicodeTest;emotes=;'
|
|
'id=unicode-123;mod=0;room-id=12345;subscriber=0;'
|
|
'tmi-sent-ts=1234567890123;turbo=0;user-id=66666;user-type= '
|
|
':unicodetest!unicodetest@unicodetest.tmi.twitch.tv PRIVMSG #channel '
|
|
':Hello\u{E0000}World';
|
|
|
|
/// A message that is a reply to another message
|
|
const replyMessage =
|
|
'@badge-info=;badges=;color=#FF4500;display-name=Replier;emotes=;'
|
|
'id=reply-123;mod=0;reply-parent-display-name=OriginalUser;'
|
|
'reply-parent-msg-body=original\\smessage;reply-parent-msg-id=original-123;'
|
|
'reply-parent-user-id=77777;reply-parent-user-login=originaluser;'
|
|
'room-id=12345;subscriber=0;tmi-sent-ts=1234567890123;'
|
|
'turbo=0;user-id=88888;user-type= '
|
|
':replier!replier@replier.tmi.twitch.tv PRIVMSG #channel :@OriginalUser this is my reply';
|
|
|
|
/// A message from a moderator
|
|
const moderatorMessage =
|
|
'@badge-info=subscriber/24;badges=moderator/1,subscriber/24;'
|
|
'color=#2E8B57;display-name=ModUser;emotes=;id=mod-123;mod=1;'
|
|
'room-id=12345;subscriber=1;tmi-sent-ts=1234567890123;'
|
|
'turbo=0;user-id=99999;user-type=mod '
|
|
':moduser!moduser@moduser.tmi.twitch.tv PRIVMSG #channel :Mod message here';
|
|
|
|
/// A message with no color set (should default to grey)
|
|
const messageNoColor =
|
|
'@badge-info=;badges=;color=;display-name=NoColor;emotes=;'
|
|
'id=nocolor-123;mod=0;room-id=12345;subscriber=0;'
|
|
'tmi-sent-ts=1234567890123;turbo=0;user-id=11111;user-type= '
|
|
':nocolor!nocolor@nocolor.tmi.twitch.tv PRIVMSG #channel :No color message';
|
|
|
|
/// A historical message (from recent messages API)
|
|
const historicalMessage =
|
|
'@badge-info=;badges=;color=#FF0000;display-name=HistUser;emotes=;'
|
|
'historical=1;id=hist-123;mod=0;room-id=12345;subscriber=0;'
|
|
'tmi-sent-ts=1234567890123;turbo=0;user-id=12121;user-type= '
|
|
':histuser!histuser@histuser.tmi.twitch.tv PRIVMSG #channel :Historical message';
|
|
|
|
/// A historical message as serialized by the recent-messages API, which emits
|
|
/// empty tags as bare keys (e.g. `emotes`, `flags`) instead of the `key=` form
|
|
/// that live Twitch IRC uses.
|
|
const historicalMessageBareTags =
|
|
'@badges=raging-wolf-helm/1;emotes;first-msg=0;tmi-sent-ts=1234567890123;'
|
|
'flags;mod=0;returning-chatter=0;rm-received-ts=1234567890456;color=#1E90FF;'
|
|
'id=hist-bare-123;room-id=12345;historical=1;user-type;turbo=0;subscriber=0;'
|
|
'badge-info;display-name=BareUser;user-id=18181 '
|
|
':bareuser!bareuser@bareuser.tmi.twitch.tv PRIVMSG #channel :Bare tag message';
|
|
|
|
/// A message from a shared chat session (source room different from current)
|
|
const sharedChatMessage =
|
|
'@badge-info=;badges=;color=#00BFFF;display-name=SharedUser;emotes=;'
|
|
'id=shared-123;mod=0;room-id=12345;source-room-id=99999;subscriber=0;'
|
|
'tmi-sent-ts=1234567890123;turbo=0;user-id=13131;user-type= '
|
|
':shareduser!shareduser@shareduser.tmi.twitch.tv PRIVMSG #channel :From another channel';
|
|
|
|
/// A message with emoji characters
|
|
const messageWithEmoji =
|
|
'@badge-info=;badges=;color=#FFD700;display-name=EmojiUser;emotes=;'
|
|
'id=emoji-123;mod=0;room-id=12345;subscriber=0;'
|
|
'tmi-sent-ts=1234567890123;turbo=0;user-id=14141;user-type= '
|
|
':emojiuser!emojiuser@emojiuser.tmi.twitch.tv PRIVMSG #channel :Hello 😀🎉 World';
|
|
|
|
/// A message with empty body (edge case)
|
|
const messageEmptyBody =
|
|
'@badge-info=;badges=;color=#FF0000;display-name=EmptyMsg;emotes=;'
|
|
'id=empty-123;mod=0;room-id=12345;subscriber=0;'
|
|
'tmi-sent-ts=1234567890123;turbo=0;user-id=16161;user-type= '
|
|
':emptymsg!emptymsg@emptymsg.tmi.twitch.tv PRIVMSG #channel :';
|
|
|
|
/// A message containing only emotes (no text words)
|
|
const messageOnlyEmotes =
|
|
'@badge-info=;badges=;color=#FF0000;display-name=EmoteOnly;'
|
|
'emotes=25:0-4,6-10;id=emoteonly-123;mod=0;room-id=12345;subscriber=0;'
|
|
'tmi-sent-ts=1234567890123;turbo=0;user-id=17171;user-type= '
|
|
':emoteonly!emoteonly@emoteonly.tmi.twitch.tv PRIVMSG #channel :Kappa Kappa';
|
|
|
|
/// A message with a URL
|
|
const messageWithUrl =
|
|
'@badge-info=;badges=;color=#9400D3;display-name=LinkUser;emotes=;'
|
|
'id=url-123;mod=0;room-id=12345;subscriber=0;'
|
|
'tmi-sent-ts=1234567890123;turbo=0;user-id=15151;user-type= '
|
|
':linkuser!linkuser@linkuser.tmi.twitch.tv PRIVMSG #channel :Check out https://twitch.tv/channel';
|