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 }