1 /++ 2 The Twitch postprocessor processes [dialect.defs.IRCEvent|IRCEvent]s after 3 they are parsed, and deals with Twitch-specifics. Those include extracting 4 the colour someone's name should be printed in, their alias/"display name" 5 (generally their nickname cased), converting the event to some event types 6 unique to Twitch, etc. 7 +/ 8 module dialect.postprocessors.twitch; 9 10 version(TwitchSupport): 11 12 //version = TwitchWarnings; 13 14 private: 15 16 import dialect.defs; 17 import dialect.parsing : IRCParser; 18 import dialect.postprocessors; 19 20 version(Postprocessors) {} 21 else 22 { 23 enum message = "Version `Postprocessors` must be enabled in `dub.sdl` for Twitch support."; 24 static assert(0, message); 25 } 26 27 28 /+ 29 Mix in [dialect.postprocessors.PostprocessorRegistration] to enable this 30 postprocessor and have it be automatically instantiated on library initialisation. 31 +/ 32 mixin PostprocessorRegistration!TwitchPostprocessor; 33 34 35 // parseTwitchTags 36 /++ 37 Parses a Twitch event's IRCv3 tags. 38 39 The event is passed by ref as many tags necessitate changes to it. 40 41 Params: 42 parser = Current [dialect.parsing.IRCParser|IRCParser]. 43 event = Reference to the [dialect.defs.IRCEvent|IRCEvent] whose tags 44 should be parsed. 45 +/ 46 auto parseTwitchTags(ref IRCParser parser, ref IRCEvent event) @safe 47 { 48 import dialect.common : decodeIRCv3String; 49 import std.algorithm.iteration : splitter; 50 import std.conv : to; 51 52 // https://dev.twitch.tv/docs/v5/guides/irc/#twitch-irc-capability-tags 53 54 if (!event.tags.length) return; 55 56 auto tagRange = event.tags.splitter(";"); // mutable 57 58 version(TwitchWarnings) 59 { 60 /// Whether or not an error occured and debug information should be printed 61 /// upon leaving the function. 62 bool printTagsOnExit; 63 64 static void appendToErrors(ref IRCEvent event, const string msg) 65 { 66 import std.conv : text; 67 immutable spacer = (event.errors.length ? " | " : string.init); 68 event.errors ~= text(spacer, msg); 69 } 70 71 static void printTags(typeof(tagRange) tagRange, const IRCEvent event) 72 { 73 import lu.string : advancePast; 74 import std.stdio : writefln, writeln; 75 76 writeln('@', event.tags, ' ', event.raw, '$'); 77 78 foreach (immutable tagline; tagRange) 79 { 80 string slice = tagline; // mutable 81 immutable key = slice.advancePast('='); 82 83 writefln(`%-35s"%s"`, key, slice); 84 } 85 } 86 87 void warnAboutOverwrittenCount( 88 const size_t i, 89 const string key, 90 const string type = "tag") 91 { 92 if (!event.count[i].isNull) 93 { 94 import std.conv : text; 95 import std.stdio : writeln; 96 97 immutable msg = text(type, ' ', key, " overwrote `count[", i, "]`: ", event.count[i].get); 98 appendToErrors(event, msg); 99 writeln(msg); 100 printTagsOnExit = true; 101 } 102 } 103 104 void warnAboutOverwrittenAuxString( 105 const size_t i, 106 const string key, 107 const string type = "tag") 108 { 109 if (event.aux[i].length) 110 { 111 import std.conv : text; 112 import std.stdio : writeln; 113 114 immutable msg = text(type, ' ', key, " overwrote `aux[", i, "]`: ", event.aux[i]); 115 appendToErrors(event, msg); 116 writeln(msg); 117 printTagsOnExit = true; 118 } 119 } 120 } 121 122 with (IRCEvent.Type) 123 foreach (tag; tagRange) 124 { 125 import lu.string : advancePast; 126 127 immutable key = tag.advancePast('='); 128 string value = tag; // mutable 129 130 switch (key) 131 { 132 case "msg-id": 133 // The type of notice (not the ID) / A message ID string. 134 // Can be used for i18ln. Valid values: see 135 // Msg-id Tags for the NOTICE Commands Capability. 136 // https://dev.twitch.tv/docs/irc#msg-id-tags-for-the-notice-commands-capability 137 // https://swiftyspiffy.com/TwitchLib/Client/_msg_ids_8cs_source.html 138 // https://dev.twitch.tv/docs/irc/msg-id/ 139 140 /* 141 sub 142 resub 143 charity 144 already_banned <user> is already banned in this room. 145 already_emote_only_off This room is not in emote-only mode. 146 already_emote_only_on This room is already in emote-only mode. 147 already_r9k_off This room is not in r9k mode. 148 already_r9k_on This room is already in r9k mode. 149 already_subs_off This room is not in subscribers-only mode. 150 already_subs_on This room is already in subscribers-only mode. 151 bad_unban_no_ban <user> is not banned from this room. 152 ban_success <user> is banned from this room. 153 emote_only_off This room is no longer in emote-only mode. 154 emote_only_on This room is now in emote-only mode. 155 hosts_remaining There are <number> host commands remaining this half hour. 156 msg_channel_suspended This channel is suspended. 157 r9k_off This room is no longer in r9k mode. 158 r9k_on This room is now in r9k mode. 159 slow_off This room is no longer in slow mode. 160 slow_on This room is now in slow mode. You may send messages every <slow seconds> seconds. 161 subs_off This room is no longer in subscribers-only mode. 162 subs_on This room is now in subscribers-only mode. 163 timeout_success <user> has been timed out for <duration> seconds. 164 unban_success <user> is no longer banned from this chat room. 165 unrecognized_cmd Unrecognized command: <command> 166 raid Raiders from <other channel> have joined!\n 167 */ 168 169 alias msgID = value; 170 if (!msgID.length) continue; // Rare occurence but happens 171 172 switch (msgID) 173 { 174 case "sub": 175 case "resub": 176 // Subscription. Disambiguate subs from resubs by other tags, set 177 // in count and altcount. 178 event.type = TWITCH_SUB; 179 break; 180 181 case "subgift": 182 // A gifted subscription. 183 // "X subscribed with Twitch Prime." 184 // "Y subscribed at Tier 1. They've subscribed for 11 months!" 185 // "We added the msg-id “anonsubgift” to the user-notice which 186 // defaults the sender to the channel owner" 187 /+ 188 For anything anonomous 189 The channel ID and Channel name are set as normal 190 The Recipienet is set as normal 191 The person giving the gift is anonomous 192 193 https://discuss.dev.twitch.tv/t/msg-id-purchase/22067/8 194 +/ 195 // In reality the sender is "ananonymousgifter". 196 event.type = TWITCH_SUBGIFT; 197 break; 198 199 case "submysterygift": 200 // Gifting several subs to random people in one event. 201 // "A is gifting 1 Tier 1 Subs to C's community! They've gifted a total of n in the channel!" 202 event.type = TWITCH_BULKGIFT; 203 break; 204 205 case "ritual": 206 // Oneliner upon joining chat. 207 // content: "HeyGuys" 208 event.type = TWITCH_RITUAL; 209 break; 210 211 case "rewardgift": 212 event.type = TWITCH_REWARDGIFT; 213 break; 214 215 case "raid": 216 // Raid start. Seen in target channel. 217 // "3322 raiders from A have joined!" 218 event.type = TWITCH_RAID; 219 break; 220 221 case "unraid": 222 // Manual raid abort. 223 // "The raid has been cancelled." 224 event.type = TWITCH_UNRAID; 225 break; 226 227 case "charity": 228 import std.algorithm.iteration : filter; 229 import std.algorithm.searching : startsWith; 230 import std.array : Appender; 231 import std.typecons : Flag, No, Yes; 232 233 event.type = TWITCH_CHARITY; 234 235 string[string] charityAA; 236 auto charityTags = tagRange 237 .filter!(tagline => tagline.startsWith("msg-param-charity")); 238 239 foreach (immutable tagline; charityTags) 240 { 241 string slice = tagline; // mutable 242 immutable charityKey = slice.advancePast('='); 243 charityAA[charityKey] = slice; 244 } 245 246 static immutable charityStringTags = 247 [ 248 "msg-param-charity-learn-more", 249 "msg-param-charity-hashtag", 250 ]; 251 252 static immutable charityCountTags = 253 [ 254 //"msg-param-total" 255 "msg-param-charity-hours-remaining", 256 "msg-param-charity-days-remaining", 257 ]; 258 259 if (const charityName = "msg-param-charity-name" in charityAA) 260 { 261 import lu.string : removeControlCharacters, strippedRight; 262 263 //msg-param-charity-name = Direct\sRelief 264 265 version(TwitchWarnings) warnAboutOverwrittenAuxString(0, "msg-param-charity-name"); 266 event.aux[0] = (*charityName) 267 .decodeIRCv3String 268 .strippedRight 269 .removeControlCharacters; 270 } 271 272 foreach (immutable i, charityKey; charityStringTags) 273 { 274 if (const charityString = charityKey in charityAA) 275 { 276 //msg-param-charity-learn-more = https://link.twitch.tv/blizzardofbits 277 //msg-param-charity-hashtag = #charity 278 // Pad count by 1 to allow for msg-param-charity-name 279 280 version(TwitchWarnings) warnAboutOverwrittenAuxString(i+1, charityKey); 281 event.aux[i+1] = *charityString; 282 } 283 } 284 285 // Doesn't start with msg-param-charity but it will be set later down 286 /*if (const charityTotal = "msg-param-total" in charityAA) 287 { 288 //msg-param-charity-hours-remaining = 286 289 event.count[0] = (*charityTotal).to!int; 290 }*/ 291 292 foreach (immutable i, charityKey; charityCountTags) 293 { 294 if (const charityCount = charityKey in charityAA) 295 { 296 //msg-param-charity-hours-remaining 297 //msg-param-charity-days-remaining = 11 298 // Pad count by 1 to allow for msg-param-total 299 300 version(TwitchWarnings) warnAboutOverwrittenCount(i+1, charityKey); 301 event.count[i+1] = (*charityCount).to!long; 302 } 303 } 304 305 // Remove once we have a recorded parse 306 version(TwitchWarnings) 307 { 308 appendToErrors(event, "RECORD TWITCH CHARITY"); 309 printTagsOnExit = true; 310 } 311 break; 312 313 case "giftpaidupgrade": 314 case "anongiftpaidupgrade": 315 // "Continuing a gift sub" by gifting a sub you were gifted (?) 316 // "A is continuing the Gift Sub they got from B!" 317 event.type = TWITCH_GIFTCHAIN; 318 break; 319 320 case "primepaidupgrade": 321 // User upgrading a prime sub to a normal paid one. 322 // "A converted from a Twitch Prime sub to a Tier 1 sub!" 323 event.type = TWITCH_SUBUPGRADE; 324 break; 325 326 case "bitsbadgetier": 327 // User just earned a badge for a tier of bits 328 // content is the message body, e.g. "GG" 329 event.type = TWITCH_BITSBADGETIER; 330 break; 331 332 case "extendsub": 333 // User extended their sub, always by a month? 334 // "A extended their Tier 1 subscription through April!" 335 event.type = TWITCH_EXTENDSUB; 336 break; 337 338 case "highlighted-message": 339 case "skip-subs-mode-message": 340 // These are PRIVMSGs 341 version(TwitchWarnings) warnAboutOverwrittenCount(0, msgID, "msg-id"); 342 event.aux[0] = msgID; 343 break; 344 345 case "primecommunitygiftreceived": 346 // "A viewer was gifted a World of Tanks: Care Package, courtesy of a Prime member!" 347 event.type = TWITCH_GIFTRECEIVED; 348 break; 349 350 case "standardpayforward": // has a target 351 case "communitypayforward": // toward community, no target 352 // "A is paying forward the Gift they got from B to #channel!" 353 event.type = TWITCH_PAYFORWARD; 354 break; 355 356 case "crowd-chant": 357 // PRIVMSG #fextralife :Clap Clap FeelsBirthdayMan 358 // Seemingly no other interesting tags 359 event.type = TWITCH_CROWDCHANT; 360 break; 361 362 case "announcement": 363 // USERNOTICE #zorael :test 364 // by /announcement test 365 // Unknown Twitch msg-id: announcement 366 // Unknown Twitch tag: msg-param-color = PRIMARY 367 event.type = TWITCH_ANNOUNCEMENT; 368 break; 369 370 case "user-intro": 371 // PRIVMSG #ginomachino :yo this is much coller with actual music 372 // Unknown Twitch msg-id: user-intro 373 event.type = TWITCH_INTRO; 374 break; 375 376 /*case "bad_ban_admin": 377 case "bad_ban_anon": 378 case "bad_ban_broadcaster": 379 case "bad_ban_global_mod": 380 case "bad_ban_mod": 381 case "bad_ban_self": 382 case "bad_ban_staff": 383 case "bad_commercial_error": 384 case "bad_delete_message_broadcaster": 385 case "bad_delete_message_mod": 386 case "bad_delete_message_error": 387 case "bad_marker_client": 388 case "bad_mod_banned": 389 case "bad_mod_mod": 390 case "bad_slow_duration": 391 case "bad_timeout_admin": 392 case "bad_timeout_broadcaster": 393 case "bad_timeout_duration": 394 case "bad_timeout_global_mod": 395 case "bad_timeout_mod": 396 case "bad_timeout_self": 397 case "bad_timeout_staff": 398 case "bad_unban_no_ban": 399 case "bad_unmod_mod":*/ 400 401 case "already_banned": 402 case "already_emote_only_on": 403 case "already_emote_only_off": 404 case "already_r9k_on": 405 case "already_r9k_off": 406 case "already_subs_on": 407 case "already_subs_off": 408 case "invalid_user": 409 case "msg_bad_characters": 410 case "msg_channel_blocked": 411 case "msg_r9k": 412 case "msg_ratelimit": 413 case "msg_rejected_mandatory": 414 case "msg_room_not_found": 415 case "msg_suspended": 416 case "msg_timedout": 417 case "no_help": 418 case "no_permission": 419 case "raid_already_raiding": 420 case "raid_error_forbidden": 421 case "raid_error_self": 422 case "raid_error_too_many_viewers": 423 case "raid_error_unexpected": 424 case "timeout_no_timeout": 425 case "unraid_error_no_active_raid": 426 case "unraid_error_unexpected": 427 case "unrecognized_cmd": 428 case "unsupported_chatrooms_cmd": 429 case "untimeout_banned": 430 case "whisper_banned": 431 case "whisper_banned_recipient": 432 case "whisper_restricted_recipient": 433 case "whisper_invalid_args": 434 case "whisper_invalid_login": 435 case "whisper_invalid_self": 436 case "whisper_limit_per_min": 437 case "whisper_limit_per_sec": 438 case "whisper_restricted": 439 case "msg_subsonly": 440 case "msg_verified_email": 441 case "msg_slowmode": 442 case "tos_ban": 443 case "msg_channel_suspended": 444 case "msg_banned": 445 case "msg_duplicate": 446 case "msg_facebook": 447 case "turbo_only_color": 448 case "unavailable_command": 449 // Generic Twitch error. 450 event.type = TWITCH_ERROR; 451 452 version(TwitchWarnings) warnAboutOverwrittenAuxString(0, key, "error"); 453 event.aux[0] = msgID; 454 break; 455 456 case "emote_only_on": 457 case "emote_only_off": 458 case "r9k_on": 459 case "r9k_off": 460 case "slow_on": 461 case "slow_off": 462 case "subs_on": 463 case "subs_off": 464 case "followers_on": 465 case "followers_off": 466 case "followers_on_zero": 467 468 /*case "usage_ban": 469 case "usage_clear": 470 case "usage_color": 471 case "usage_commercial": 472 case "usage_disconnect": 473 case "usage_emote_only_off": 474 case "usage_emote_only_on": 475 case "usage_followers_off": 476 case "usage_followers_on": 477 case "usage_help": 478 case "usage_marker": 479 case "usage_me": 480 case "usage_mod": 481 case "usage_mods": 482 case "usage_r9k_off": 483 case "usage_r9k_on": 484 case "usage_raid": 485 case "usage_slow_off": 486 case "usage_slow_on": 487 case "usage_subs_off": 488 case "usage_subs_on": 489 case "usage_timeout": 490 case "usage_unban": 491 case "usage_unmod": 492 case "usage_unraid": 493 case "usage_untimeout":*/ 494 495 case "mod_success": 496 case "msg_emotesonly": 497 case "msg_followersonly": 498 case "msg_followersonly_followed": 499 case "msg_followersonly_zero": 500 case "msg_rejected": // "being checked by mods" 501 case "raid_notice_mature": 502 case "raid_notice_restricted_chat": 503 case "room_mods": 504 case "timeout_success": 505 case "unban_success": 506 case "unmod_success": 507 case "unraid_success": 508 case "untimeout_success": 509 case "cmds_available": 510 case "color_changed": 511 case "commercial_success": 512 case "delete_message_success": 513 case "ban_success": 514 case "no_vips": 515 case "no_mods": 516 // Generic Twitch server reply. 517 event.type = TWITCH_NOTICE; 518 519 version(TwitchWarnings) warnAboutOverwrittenAuxString(0, key, "notice"); 520 event.aux[0] = msgID; 521 break; 522 523 case "midnightsquid": 524 // New direct cheer with real currency 525 event.type = TWITCH_DIRECTCHEER; 526 527 version(TwitchWarnings) warnAboutOverwrittenAuxString(1, key, "msg-id"); 528 event.aux[1] = msgID; 529 break; 530 531 default: 532 import std.algorithm.searching : startsWith; 533 534 version(TwitchWarnings) warnAboutOverwrittenAuxString(0, key, "msg-id"); 535 event.aux[0] = msgID; 536 537 if (msgID.startsWith("bad_")) 538 { 539 event.type = TWITCH_ERROR; 540 break; 541 } 542 else if (msgID.startsWith("usage_")) 543 { 544 event.type = TWITCH_NOTICE; 545 break; 546 } 547 548 version(TwitchWarnings) 549 { 550 import std.conv : text; 551 import std.stdio : writeln; 552 553 immutable msg = text("Unknown Twitch msg-id: ", msgID); 554 appendToErrors(event, msg); 555 writeln(msg); 556 printTagsOnExit = true; 557 } 558 break; 559 } 560 break; 561 562 //////////////////////////////////////////////////////////////////////// 563 564 case "display-name": 565 // The user’s display name, escaped as described in the IRCv3 spec. 566 // This is empty if it is never set. 567 import lu.string : strippedRight; 568 569 if (!value.length) break; 570 571 immutable displayName = decodeIRCv3String(value).strippedRight; 572 573 if ((event.type == USERSTATE) || (event.type == GLOBALUSERSTATE)) 574 { 575 // USERSTATE describes the bot in the context of a specific channel, 576 // such as what badges are available. It's *always* about the bot, 577 // so expose the display name in event.target and let Persistence store it. 578 event.target = event.sender; // get badges etc 579 event.target.nickname = parser.client.nickname; 580 event.target.displayName = displayName; 581 event.target.address = string.init; 582 event.sender.colour = string.init; 583 event.sender.badges = string.init; 584 585 if (!parser.client.displayName.length) 586 { 587 // Also store the alias in the IRCClient, for highlighting purposes 588 // *ASSUME* it never changes during runtime. 589 parser.client.displayName = displayName; 590 version(FlagAsUpdated) parser.updates |= typeof(parser).Update.client; 591 } 592 } 593 else 594 { 595 // The display name of the sender. 596 event.sender.displayName = displayName; 597 } 598 break; 599 600 case "badges": 601 // Comma-separated list of chat badges and the version of each 602 // badge (each in the format <badge>/<version>, such as admin/1). 603 // Valid badge values: admin, bits, broadcaster, global_mod, 604 // moderator, subscriber, staff, turbo. 605 // Save the whole list, let the printer deal with which to display 606 // Set an empty list to a placeholder asterisk 607 event.sender.badges = value.length ? value : "*"; 608 break; 609 610 case "system-msg": 611 case "ban-reason": 612 // @ban-duration=<ban-duration>;ban-reason=<ban-reason> :tmi.twitch.tv CLEARCHAT #<channel> :<user> 613 // The moderator’s reason for the timeout or ban. 614 // system-msg: The message printed in chat along with this notice. 615 import lu.string : removeControlCharacters, strippedRight; 616 import std.typecons : No, Yes; 617 618 if (!value.length) break; 619 620 immutable message = value 621 .decodeIRCv3String 622 .strippedRight 623 .removeControlCharacters; 624 625 if (event.type == TWITCH_RITUAL) 626 { 627 version(TwitchWarnings) warnAboutOverwrittenAuxString(0, key); 628 event.aux[0] = message; 629 } 630 else if (!event.content.length) 631 { 632 event.content = message; 633 } 634 else if (!event.aux[0].length) 635 { 636 // If event.content.length but no aux.length, store in aux 637 event.aux[0] = message; 638 } 639 break; 640 641 case "msg-param-recipient-display-name": 642 case "msg-param-sender-name": 643 // In a GIFTCHAIN the display name of the one who started the gift sub train? 644 event.target.displayName = value; 645 break; 646 647 case "msg-param-recipient-user-name": 648 case "msg-param-sender-login": 649 case "msg-param-recipient": // Prime community gift received 650 // In a GIFTCHAIN the one who started the gift sub train? 651 event.target.nickname = value; 652 break; 653 654 case "msg-param-displayName": 655 case "msg-param-sender": // Prime community gift received (apparently display name) 656 // RAID; sender alias and thus raiding channel cased 657 event.sender.displayName = value; 658 break; 659 660 case "msg-param-login": 661 case "login": 662 // RAID; real sender nickname and thus raiding channel lowercased 663 // CLEARMSG, SUBGIFT, lots 664 event.sender.nickname = value; 665 break; 666 667 case "color": 668 // Hexadecimal RGB colour code. This is empty if it is never set. 669 if (value.length) event.sender.colour = value[1..$]; 670 break; 671 672 case "bits": 673 /* (Optional) The amount of cheer/bits employed by the user. 674 All instances of these regular expressions: 675 676 /(^\|\s)<emote-name>\d+(\s\|$)/ 677 678 (where <emote-name> is an emote name returned by the Get 679 Cheermotes endpoint), should be replaced with the appropriate 680 emote: 681 682 static-cdn.jtvnw.net/bits/<theme>/<type>/<color>/<size> 683 684 * theme – light or dark 685 * type – animated or static 686 * color – red for 10000+ bits, blue for 5000-9999, green for 687 1000-4999, purple for 100-999, gray for 1-99 688 * size – A digit between 1 and 4 689 */ 690 event.type = TWITCH_CHEER; 691 goto case "ban-duration"; 692 693 case "msg-param-sub-plan": 694 // The type of subscription plan being used. 695 // Valid values: Prime, 1000, 2000, 3000. 696 // 1000, 2000, and 3000 refer to the first, second, and third 697 // levels of paid subscriptions, respectively (currently $4.99, 698 // $9.99, and $24.99). 699 case "msg-param-promo-name": 700 // Promotion name 701 // msg-param-promo-name = Subtember 702 case "msg-param-trigger-type": 703 // reward gift, what kind of event triggered a gifting 704 // example values CHEER, SUBGIFT 705 // We don't have anywhere to store this without adding altalt 706 case "msg-param-gift-name": 707 // msg-param-gift-name = "World\sof\sTanks:\sCare\sPackage" 708 // Prime community gift name 709 case "msg-param-prior-gifter-user-name": 710 // msg-param-prior-gifter-user-name = "coopamantv" 711 // Prior gifter when a user pays forward a gift 712 case "msg-param-color": 713 // msg-param-color = PRIMARY 714 // msg-param-color = PURPLE 715 // seen in a TWITCH_ANNOUNCEMENT 716 case "msg-param-currency": 717 // New midnightsquid direct cheer currency 718 case "message-id": 719 // message-id = 3 720 // WHISPER, rolling number enumerating messages 721 case "reply-parent-msg-body": 722 // The body of the message that is being replied to 723 // reply-parent-msg-body = she's\sgonna\swin\s2truths\sand\sa\slie\severytime 724 725 /+ 726 Aux 0 727 +/ 728 version(TwitchWarnings) warnAboutOverwrittenAuxString(0, key); 729 event.aux[0] = decodeIRCv3String(value); 730 break; 731 732 case "msg-param-fun-string": 733 // msg-param-fun-string = FunStringTwo 734 // [subgift] [#waifugate] AnAnonymousGifter (Asdf): "An anonymous user gifted a Tier 1 sub to Asdf!" (1000) {1} 735 // Unsure. Useless. 736 case "msg-param-ritual-name": 737 // msg-param-ritual-name = 'new_chatter' 738 case "msg-param-middle-man": 739 // msg-param-middle-man = gabepeixe 740 // Prime community gift "middle-man"? Name of the channel? 741 case "msg-param-domain": 742 // msg-param-domain = owl2018 743 // [rewardgift] [#overwatchleague] Asdf [bits]: "A Cheer shared Rewards to 35 others in Chat!" {35} 744 // Name of the context? 745 // Swapped places with msg-param-trigger-type 746 case "msg-param-prior-gifter-display-name": 747 // Prior gifter display name when a user pays forward a gift 748 case "pinned-chat-paid-currency": 749 // elevated message currency 750 751 /+ 752 Aux 1 753 +/ 754 version(TwitchWarnings) warnAboutOverwrittenAuxString(1, key); 755 event.aux[1] = decodeIRCv3String(value); 756 break; 757 758 case "msg-param-sub-plan-name": 759 // The display name of the subscription plan. This may be a default 760 // name or one created by the channel owner. 761 case "msg-param-exponent": 762 // something with new midnightsquid direct cheers 763 case "pinned-chat-paid-level": 764 // pinned-chat-paid-level = ONE 765 // Something about hype chat? 766 767 /+ 768 Aux 2 769 +/ 770 version(TwitchWarnings) warnAboutOverwrittenAuxString(2, key); 771 event.aux[2] = decodeIRCv3String(value); 772 break; 773 774 case "msg-param-goal-description": 775 // msg-param-goal-description = Lali-this\sis\sa\sgoal-ho 776 case "msg-param-pill-type": 777 // something with new midnightsquid direct cheers 778 case "msg-param-gift-theme": 779 // msg-param-gift-theme = party 780 // Theme of a bulkgift? 781 782 /+ 783 Aux 3 784 +/ 785 version(TwitchWarnings) warnAboutOverwrittenAuxString(3, key); 786 event.aux[3] = decodeIRCv3String(value); 787 break; 788 789 case "msg-param-goal-contribution-type": 790 // msg-param-goal-contribution-type = SUB_POINTS 791 case "msg-param-is-highlighted": 792 // something with new midnightsquid direct cheers 793 794 /+ 795 Aux 4 796 +/ 797 version(TwitchWarnings) warnAboutOverwrittenAuxString(4, key); 798 event.aux[4] = value; // no need to decode? 799 break; 800 801 case "first-msg": 802 // first-msg = 0 803 // Whether or not it's the user's first message after joining the channel 804 if (value == "0") break; 805 806 /+ 807 Aux $-2 808 809 Reserve this for first-msg. Set the key, not the 0/1 value. 810 +/ 811 version(TwitchWarnings) warnAboutOverwrittenAuxString(event.aux.length+(-2), key); 812 event.aux[$-2] = key; 813 break; 814 815 case "emotes": 816 /++ Information to replace text in the message with emote images. 817 This can be empty. Syntax: 818 819 <emote ID>:<first index>-<last index>, 820 <another first index>-<another last index>/ 821 <another emote ID>:<first index>-<last index>... 822 823 * emote ID – The number to use in this URL: 824 http://static-cdn.jtvnw.net/emoticons/v1/:<emote ID>/:<size> 825 (size is 1.0, 2.0 or 3.0.) 826 * first index, last index – Character indexes. \001ACTION does 827 not count. Indexing starts from the first character that is 828 part of the user’s actual message. See the example (normal 829 message) below. 830 +/ 831 event.emotes = value; 832 break; 833 834 case "msg-param-bits-amount": 835 //msg-param-bits-amount = '199' 836 case "msg-param-mass-gift-count": 837 // Number of subs being gifted 838 case "msg-param-total": 839 // Total amount donated to this charity 840 case "msg-param-threshold": 841 // (Sent only on bitsbadgetier) The tier of the bits badge the user just earned; e.g. 100, 1000, 10000. 842 843 // These events are generally present with value of 0, so in most case they're noise 844 if (value == "0") break; 845 goto case; 846 847 case "ban-duration": 848 // @ban-duration=<ban-duration>;ban-reason=<ban-reason> :tmi.twitch.tv CLEARCHAT #<channel> :<user> 849 // (Optional) Duration of the timeout, in seconds. If omitted, 850 // the ban is permanent. 851 case "msg-param-viewerCount": 852 // RAID; viewer count of raiding channel 853 // msg-param-viewerCount = '9' 854 //case "bits": // goto'ed here 855 case "msg-param-amount": 856 // New midnightsquid direct cheer 857 case "pinned-chat-paid-amount": 858 // elevated message amount 859 case "msg-param-gift-months": 860 // ... 861 case "msg-param-sub-benefit-end-month": 862 /// "...extended their Tier 1 sub to {month}" 863 864 /+ 865 Count 0 866 +/ 867 version(TwitchWarnings) warnAboutOverwrittenCount(0, key); 868 event.count[0] = (value == "0") ? 0 : value.to!long; 869 break; 870 871 case "msg-param-selected-count": 872 // REWARDGIFT; how many users "the Cheer shared Rewards" with 873 // "A Cheer shared Rewards to 20 others in Chat!" 874 case "msg-param-promo-gift-total": 875 // Number of total gifts this promotion 876 case "msg-param-sender-count": 877 // Number of gift subs a user has given in the channel, on a SUBGIFT event 878 case "pinned-chat-paid-canonical-amount": 879 // elevated message, amount in real currency) 880 // we can infer it from pinned-chat-paid-amount in event.count[0] 881 case "msg-param-cumulative-months": 882 // Total number of months subscribed, over time. Replaces msg-param-months 883 884 /+ 885 Count 1 886 +/ 887 version(TwitchWarnings) warnAboutOverwrittenCount(1, key); 888 889 if (value == "0") break; 890 event.count[1] = value.to!long; 891 break; 892 893 case "msg-param-gift-month-being-redeemed": 894 // Didn't save a description... 895 case "msg-param-goal-target-contributions": 896 // msg-param-goal-target-contributions = 600 897 case "msg-param-min-cheer-amount": 898 // REWARDGIFT; of interest? 899 // msg-param-min-cheer-amount = '150' 900 case "msg-param-charity-hours-remaining": 901 // Number of hours remaining in a charity 902 case "number-of-viewers": 903 // (Optional) Number of viewers watching the host. 904 case "msg-param-trigger-amount": 905 // reward gift, the "amount" of an event that triggered a gifting 906 // (eg "1000" for 1000 bits) 907 case "pinned-chat-paid-exponent": 908 // something with elevated messages 909 910 /+ 911 Count 2 912 +/ 913 version(TwitchWarnings) warnAboutOverwrittenCount(2, key); 914 915 if (value == "0") break; 916 event.count[2] = value.to!long; 917 break; 918 919 case "msg-param-goal-current-contributions": 920 // msg-param-goal-current-contributions = 90 921 case "msg-param-charity-days-remaining": 922 // Number of days remaining in a charity 923 case "msg-param-total-reward-count": 924 // reward gift, to how many users a reward was gifted 925 // alias of msg-param-selected-count? 926 case "msg-param-streak-months": 927 /// "...extended their Tier 1 sub to {month}" 928 929 /+ 930 Count 3 931 +/ 932 version(TwitchWarnings) warnAboutOverwrittenCount(3, key); 933 934 if (value == "0") break; 935 event.count[3] = value.to!long; 936 break; 937 938 case "msg-param-streak-tenure-months": 939 /// "...extended their Tier 1 sub to {month}" 940 case "msg-param-goal-user-contributions": 941 // msg-param-goal-user-contributions = 1 942 943 /+ 944 Count 4 945 +/ 946 version(TwitchWarnings) warnAboutOverwrittenCount(4, key); 947 948 if (value == "0") break; 949 event.count[4] = value.to!long; 950 break; 951 952 case "msg-param-cumulative-tenure-months": 953 // Ongoing number of subscriptions (in a row) 954 case "msg-param-multimonth-duration": 955 // msg-param-multimonth-duration = 0 956 // Seen in a sub event 957 958 /+ 959 Count 5 960 +/ 961 version(TwitchWarnings) warnAboutOverwrittenCount(5, key); 962 963 if (value == "0") break; 964 event.count[5] = value.to!long; 965 break; 966 967 case "msg-param-multimonth-tenure": 968 // msg-param-multimonth-tenure = 0 969 // Ditto 970 // Number of months in a gifted sub? 971 case "msg-param-should-share-streak-tenure": 972 // Streak resubs 973 974 /+ 975 Count 6 976 +/ 977 version(TwitchWarnings) warnAboutOverwrittenCount(6, key); 978 979 if (value == "0") break; 980 event.count[6] = value.to!long; 981 break; 982 983 case "msg-param-should-share-streak": 984 // Streak resubs 985 986 /+ 987 Count 7 988 +/ 989 version(TwitchWarnings) warnAboutOverwrittenCount(7, key); 990 991 if (value == "0") break; 992 event.count[7] = value.to!long; 993 break; 994 995 case "badge-info": 996 /+ 997 Metadata related to the chat badges in the badges tag. 998 999 Currently this is used only for subscriber, to indicate the exact 1000 number of months the user has been a subscriber. This number is 1001 finer grained than the version number in badges. For example, 1002 a user who has been a subscriber for 45 months would have a 1003 badge-info value of 45 but might have a badges version number 1004 for only 3 years. 1005 1006 https://dev.twitch.tv/docs/irc/tags/ 1007 +/ 1008 // As of yet we're not taking into consideration badge versions values. 1009 // When/if we do, we'll have to make sure this value overwrites the 1010 // subscriber/version value in the badges tag. 1011 // For now, ignore, as "subscriber/*" is repeated in badges. 1012 break; 1013 1014 case "id": 1015 // A unique ID for the message. 1016 event.id = value; 1017 break; 1018 1019 case "msg-param-userID": 1020 case "user-id": 1021 case "user-ID": 1022 // The sender's user ID. 1023 if (value.length) event.sender.id = value.to!uint; 1024 break; 1025 1026 case "target-user-id": 1027 case "reply-parent-user-id": 1028 case "msg-param-gifter-id": 1029 // The target's user ID 1030 // The user id of the author of the message that is being replied to 1031 // reply-parent-user-id = 50081302 1032 if (value.length) event.target.id = value.to!uint; 1033 break; 1034 1035 case "room-id": 1036 // The channel ID. 1037 if (event.type == ROOMSTATE) 1038 { 1039 version(TwitchWarnings) warnAboutOverwrittenAuxString(0, key); 1040 event.aux[0] = value; 1041 } 1042 break; 1043 1044 case "reply-parent-display-name": 1045 case "msg-param-gifter-name": 1046 // The display name of the user that is being replied to 1047 // reply-parent-display-name = zenArc 1048 event.target.displayName = value; 1049 break; 1050 1051 case "reply-parent-user-login": 1052 case "msg-param-gifter-login": 1053 // The account name of the author of the message that is being replied to 1054 // reply-parent-user-login = zenarc 1055 event.target.nickname = value; 1056 break; 1057 1058 // We only need set cases for every known tag if we want to be alerted 1059 // when we come across unknown ones, which is version TwitchWarnings. 1060 // As such, version away all the cases from normal builds, and just let 1061 // them fall to the default. 1062 version(TwitchWarnings) 1063 { 1064 case "emote-only": 1065 // We don't conflate ACTION emotes and emote-only messages anymore 1066 /*if (value == "0") break; 1067 if (event.type == CHAN) event.type = EMOTE; 1068 break;*/ 1069 case "broadcaster-lang": 1070 // The chat language when broadcaster language mode is enabled; 1071 // otherwise, empty. Examples: en (English), fi (Finnish), es-MX 1072 // (Mexican variant of Spanish). 1073 case "subs-only": 1074 // Subscribers-only mode. If enabled, only subscribers and 1075 // moderators can chat. Valid values: 0 (disabled) or 1 (enabled). 1076 case "r9k": 1077 // R9K mode. If enabled, messages with more than 9 characters must 1078 // be unique. Valid values: 0 (disabled) or 1 (enabled). 1079 case "emote-sets": 1080 // A comma-separated list of emotes, belonging to one or more emote 1081 // sets. This always contains at least 0. Get Chat Emoticons by Set 1082 // gets a subset of emoticons. 1083 case "mercury": 1084 // ? 1085 case "followers-only": 1086 // Probably followers only. 1087 case "slow": 1088 // The number of seconds chatters without moderator privileges must 1089 // wait between sending messages. 1090 case "sent-ts": 1091 // ? 1092 case "tmi-sent-ts": 1093 // ? 1094 case "user": 1095 // The name of the user who sent the notice. 1096 case "rituals": 1097 /++ 1098 "Rituals makes it easier for you to celebrate special moments 1099 that bring your community together. Say a viewer is checking out 1100 a new channel for the first time. After a minute, she’ll have 1101 the choice to signal to the rest of the community that she’s new 1102 to the channel. Twitch will break the ice for her in Chat, and 1103 maybe she’ll make some new friends. 1104 1105 Rituals will help you build a more vibrant community when it 1106 launches in November." 1107 1108 spotted in the wild as = 0 1109 +/ 1110 case "msg-param-recipient-id": 1111 // sub gifts 1112 case "target-msg-id": 1113 // banphrase 1114 case "msg-param-profileImageURL": 1115 // URL link to profile picture. 1116 case "flags": 1117 // Unsure. 1118 // flags = 1119 // flags = 4-11:P.5,40-46:P.6 1120 case "mod": 1121 case "subscriber": 1122 case "turbo": 1123 // 1 if the user has a (moderator|subscriber|turbo) badge; otherwise, 0. 1124 // Deprecated, use badges instead. 1125 case "user-type": 1126 // The user’s type. Valid values: empty, mod, global_mod, admin, staff. 1127 // Deprecated, use badges instead. 1128 case "msg-param-origin-id": 1129 // msg-param-origin-id = 6e\s15\s70\s6d\s34\s2a\s7e\s5b\sd9\s45\sd3\sd2\sce\s20\sd3\s4b\s9c\s07\s49\sc4 1130 // [subgift] [#savjz] sender [SP] (target): "sender gifted a Tier 1 sub to target! This is their first Gift Sub in the channel!" (1000) {1} 1131 case "thread-id": 1132 // thread-id = 22216721_404208264 1133 // WHISPER, private message session? 1134 case "msg-param-months": 1135 // DEPRECATED in favour of msg-param-cumulative-months. 1136 // The number of consecutive months the user has subscribed for, 1137 // in a resub notice. 1138 case "msg-param-charity-hashtag": 1139 //msg-param-charity-hashtag = #charity 1140 case "msg-param-charity-name": 1141 //msg-param-charity-name = Direct\sRelief 1142 case "msg-param-charity-learn-more": 1143 //msg-param-charity-learn-more = https://link.twitch.tv/blizzardofbits 1144 // Do nothing; everything is done at msg-id charity 1145 case "message": 1146 // The message. 1147 case "custom-reward-id": 1148 // custom-reward-id = f597fc7c-703e-42d8-98ed-f5ada6d19f4b 1149 // Unsure, was just part of an emote-only PRIVMSG 1150 case "msg-param-prior-gifter-anonymous": 1151 // Paying forward gifts, whether or not the prior gifter was anonymous 1152 case "msg-param-prior-gifter-id": 1153 // Numeric id of prior gifter when a user pays forward a gift 1154 case "client-nonce": 1155 // Opaque nonce ID for this message 1156 case "reply-parent-msg-id": 1157 // The msg-id of the message that is being replied to 1158 // reply-parent-msg-id = 81b6262b-7ce3-4686-be4f-1f5c548c9d16 1159 // Ignore. Let plugins who want it grep event.tags 1160 case "msg-param-was-gifted": 1161 // msg-param-was-gifted = false 1162 // On subscription events, whether or not the sub was from a gift. 1163 case "msg-param-anon-gift": 1164 // msg-param-anon-gift = false 1165 case "crowd-chant-parent-msg-id": 1166 // crowd-chant-parent-msg-id = <uuid> 1167 // Chant? Seems to be a reply/quote 1168 case "returning-chatter": 1169 // returning-chatter = 0 1170 // Unsure. 1171 case "vip": 1172 // vip = 1 1173 // Whether or not the sender is a VIP. Superfluous; we can tell from the badges 1174 case "msg-param-emote-id": 1175 // something with new midnightsquid direct cheers 1176 case "reply-thread-parent-msg-id": 1177 // Message ID of reply thread parent? 1178 case "pinned-chat-paid-is-system-message": 1179 // pinned-chat-paid-is-system-message = 1 1180 // Something about hype chat. ...what's a system message? 1181 case "reply-thread-parent-user-login": 1182 // Login of reply thread parent? 1183 1184 // Ignore these events. 1185 break; 1186 } 1187 1188 default: 1189 version(TwitchWarnings) 1190 { 1191 import std.conv : text; 1192 import std.stdio : writeln; 1193 1194 immutable msg = text("Unknown Twitch tag: ", key, " = ", value); 1195 appendToErrors(event, msg); 1196 writeln(msg); 1197 printTagsOnExit = true; 1198 } 1199 break; 1200 } 1201 } 1202 1203 version(TwitchWarnings) 1204 { 1205 if (printTagsOnExit) 1206 { 1207 import std.stdio : writefln, writeln; 1208 1209 void printStuffTrusted() @trusted 1210 { 1211 /+ 1212 write{,f}ln is @trusted, but event.aux now being a static string[n] 1213 causes it to output a deprecation warning anyway. 1214 1215 "Deprecation: `@safe` function `parseTwitchTags` calling `writefln`" 1216 +/ 1217 enum pattern = `%-35s%s`; 1218 writefln(pattern, "event.aux", event.aux); 1219 writefln(pattern, "event.count", event.count); 1220 writeln(); 1221 } 1222 1223 printTags(tagRange, event); 1224 printStuffTrusted(); 1225 writeln(); 1226 } 1227 } 1228 } 1229 1230 1231 package: 1232 1233 1234 // TwitchPostprocessor 1235 /++ 1236 Twitch-specific postprocessor. 1237 1238 Twitch events are initially very basic with only skeletal functionality, 1239 until you enable capabilities that unlock their IRCv3 tags, at which point 1240 events become a flood of information. 1241 +/ 1242 final class TwitchPostprocessor : Postprocessor 1243 { 1244 // postprocess 1245 /++ 1246 Handle Twitch specifics, modifying the [dialect.defs.IRCEvent|IRCEvent] 1247 to add things like [dialect.defs.IRCEvent.colour|IRCEvent.colour] and 1248 differentiate between temporary and permanent bans. 1249 1250 Params: 1251 parser = Current [dialect.parsing.IRCParser|IRCParser]. 1252 event = [dialect.defs.IRCEvent|IRCEvent] in flight. 1253 +/ 1254 void postprocess( 1255 ref IRCParser parser, 1256 ref IRCEvent event) @system 1257 { 1258 if (parser.server.daemon != IRCServer.Daemon.twitch) return; 1259 1260 parser.parseTwitchTags(event); 1261 1262 with (IRCEvent.Type) 1263 { 1264 if ((event.type == CLEARCHAT) && event.target.nickname.length) 1265 { 1266 // Stay CLEARCHAT if no target nickname 1267 event.type = (!event.count[0].isNull && (event.count[0].get > 0)) ? 1268 TWITCH_TIMEOUT : 1269 TWITCH_BAN; 1270 } 1271 } 1272 1273 if (event.sender.nickname.length) 1274 { 1275 // Twitch nicknames are always the same as the user account; the 1276 // displayed name/alias is sent separately as a "display-name" IRCv3 tag 1277 event.sender.account = event.sender.nickname; 1278 } 1279 1280 if (event.target.nickname.length) 1281 { 1282 // Likewise sync target nickname and account. 1283 event.target.account = event.target.nickname; 1284 } 1285 } 1286 }