1 /++ 2 + Module handling various IRC extensions used by Twitch. 3 +/ 4 module virc.ircv3.twitch; 5 6 import std.algorithm; 7 import std.conv : to; 8 import std.range : chain, choose, drop, only, takeNone; 9 import std.typecons : Nullable; 10 11 import virc.ircmessage; 12 import virc.ircv3.tags; 13 14 /// 15 alias banReason = stringTag!"ban-reason"; 16 /// 17 alias banDuration = secondDurationTag!"ban-duration"; 18 /// 19 alias bits = typeTag!("slow", ulong); 20 /// 21 alias broadcasterLang = stringTag!"broadcaster-lang"; 22 /// 23 alias color = stringTag!"color"; 24 /// 25 alias displayName = stringTag!"display-name"; 26 //alias emoteSets = arrayTag!("emote-sets", string); 27 alias mod = booleanTag!"mod"; 28 /// 29 alias login = stringTag!"login"; 30 /// 31 alias msgID = stringTag!"msg-id"; 32 /// 33 alias r9k = booleanTag!"r9k"; 34 /// 35 alias roomID = stringTag!"room-id"; 36 /// 37 alias slow = typeTag!("slow", ulong); 38 /// 39 alias subsOnly = booleanTag!"subs-only"; 40 /// 41 alias subscriber = booleanTag!"subscriber"; 42 /// 43 alias systemMsg = stringTag!"system-msg"; 44 /// 45 alias turbo = booleanTag!"turbo"; 46 /// 47 alias userID = stringTag!"user-id"; 48 /// 49 alias userType = stringTag!"user-type"; 50 /// 51 auto emotes(IRCTags tags) { 52 auto parseTag(string str) { 53 return str.splitter("/") 54 .map!(x => 55 x 56 .splitter(",") 57 .map!(y => y.findSplitAfter(":")[1]) 58 .map!(y => 59 TwitchEmote( 60 x.splitter(":").front.to!ulong, 61 y.findSplitBefore("-")[0].to!ulong, 62 y.findSplitAfter("-")[1].to!ulong 63 ) 64 ) 65 ) 66 .joiner; 67 } 68 if ("emotes" in tags) 69 return Nullable!(typeof(parseTag("")))(parseTag(tags["emotes"])); 70 return Nullable!(typeof(parseTag(""))).init; 71 } 72 /++ 73 + 74 +/ 75 struct TwitchEmote { 76 /// 77 ulong id; 78 /// 79 ulong beginPosition; 80 /// 81 ulong endPosition; 82 } 83 84 @safe pure /+nothrow+/ unittest { //Source: https://github.com/justintv/Twitch-API/blob/master/IRC.md 85 { 86 import std.algorithm.comparison : equal; 87 auto parsed = IRCMessage("@badges=global_mod/1,turbo/1;color=#0D4200;display-name=TWITCH_UserNaME;emotes=25:0-4,12-16/1902:6-10;mod=0;room-id=1337;subscriber=0;turbo=1;user-id=1337;user-type=global_mod :twitch_username!twitch_username@twitch_username.tmi.twitch.tv PRIVMSG #channel :Kappa Keepo Kappa"); 88 assert(parsed.tags.mod == false); 89 assert(parsed.tags.displayName == "TWITCH_UserNaME"); 90 //assert(parsed.tags.badges.equal(only("global_mod", "turbo"))); 91 assert(parsed.tags.color == "#0D4200"); 92 assert(parsed.tags.emotes.get.equal(only(TwitchEmote(25, 0, 4), TwitchEmote(25, 12, 16), TwitchEmote(1902, 6, 10)))); 93 assert(parsed.tags.subscriber == false); 94 assert(parsed.tags.turbo == true); 95 assert(parsed.tags.roomID == "1337"); 96 assert(parsed.tags.userType == "global_mod"); 97 } 98 { 99 auto parsed = IRCMessage("@color=#0D4200;display-name=TWITCH_UserNaME;emote-sets=0,33,50,237,793,2126,3517,4578,5569,9400,10337,12239;mod=1;subscriber=1;turbo=1;user-type=staff :tmi.twitch.tv USERSTATE #channel"); 100 assert(parsed.tags.emotes.isNull); 101 assert(parsed.tags.color == "#0D4200"); 102 assert(parsed.tags.turbo == true); 103 assert(parsed.tags.subscriber == true); 104 assert(parsed.tags.mod == true); 105 assert(parsed.tags.displayName == "TWITCH_UserNaME"); 106 assert(parsed.tags.userType == "staff"); 107 } 108 { 109 auto parsed = IRCMessage("@color=#0D4200;display-name=TWITCH_UserNaME;emote-sets=0,33,50,237,793,2126,3517,4578,5569,9400,10337,12239;turbo=0;user-id=1337;user-type=admin :tmi.twitch.tv GLOBALUSERSTATE"); 110 //assert(parsed.tags.emoteSets.equal(only("0", "33", "50", "237", "793", "2126", "3517", "4578", "5569", "9400", "10337", "12239"))); 111 assert(parsed.tags.color == "#0D4200"); 112 assert(parsed.tags.mod.isNull); 113 assert(parsed.tags.displayName == "TWITCH_UserNaME"); 114 assert(parsed.tags.userID == "1337"); 115 assert(parsed.tags.turbo == false); 116 assert(parsed.tags.slow.isNull); 117 assert(parsed.tags.userType == "admin"); 118 } 119 { 120 auto parsed = IRCMessage("@broadcaster-lang=;r9k=0;slow=0;subs-only=0 :tmi.twitch.tv ROOMSTATE #channel"); 121 assert(parsed.tags.r9k == false); 122 assert(parsed.tags.broadcasterLang == ""); 123 assert(parsed.tags.displayName.isNull); 124 assert(parsed.tags.slow == 0); 125 assert(parsed.tags.subsOnly == false); 126 } 127 { 128 auto parsed = IRCMessage("@slow=10 :tmi.twitch.tv ROOMSTATE #channel"); 129 assert(parsed.tags.slow == 10); 130 } 131 { 132 auto parsed = IRCMessage("@badges=staff/1,broadcaster/1,turbo/1;color=#008000;display-name=TWITCH_UserName;emotes=;mod=0;msg-id=resub;msg-param-months=6;room-id=1337;subscriber=1;system-msg=TWITCH_UserName\\shas\\ssubscribed\\sfor\\s6\\smonths!;login=twitch_username;turbo=1;user-id=1337;user-type=staff :tmi.twitch.tv USERNOTICE #channel :Great stream -- keep it up!"); 133 assert(parsed.tags.emotes.get.empty); 134 assert(parsed.tags.color == "#008000"); 135 assert(parsed.tags.displayName == "TWITCH_UserName"); 136 assert(parsed.tags.systemMsg == "TWITCH_UserName has subscribed for 6 months!"); 137 assert(parsed.tags.roomID == "1337"); 138 assert(parsed.tags.userID == "1337"); 139 assert(parsed.tags.userType == "staff"); 140 assert(parsed.tags.subscriber == true); 141 assert(parsed.tags.turbo == true); 142 assert(parsed.tags.login == "twitch_username"); 143 assert(parsed.tags.msgID == "resub"); 144 } 145 { 146 auto parsed = IRCMessage("@badges=staff/1,broadcaster/1,turbo/1;color=#008000;display-name=TWITCH_UserName;emotes=;mod=0;msg-id=resub;msg-param-months=6;room-id=1337;subscriber=1;system-msg=TWITCH_UserName\\shas\\ssubscribed\\sfor\\s6\\smonths!;login=twitch_username;turbo=1;user-id=1337;user-type=staff :tmi.twitch.tv USERNOTICE #channel"); 147 assert(parsed.tags.emotes.get.empty); 148 assert(parsed.tags.color == "#008000"); 149 assert(parsed.tags.displayName == "TWITCH_UserName"); 150 assert(parsed.tags.systemMsg == "TWITCH_UserName has subscribed for 6 months!"); 151 assert(parsed.tags.roomID == "1337"); 152 assert(parsed.tags.userID == "1337"); 153 assert(parsed.tags.userType == "staff"); 154 assert(parsed.tags.subscriber == true); 155 assert(parsed.tags.turbo == true); 156 assert(parsed.tags.login == "twitch_username"); 157 assert(parsed.tags.msgID == "resub"); 158 } 159 { 160 import core.time : seconds; 161 auto parsed = IRCMessage("@ban-duration=1;ban-reason=Follow\\sthe\\srules :tmi.twitch.tv CLEARCHAT #channel :target_username"); 162 assert(parsed.tags.banDuration == 1.seconds); 163 assert(parsed.tags.banReason == "Follow the rules"); 164 } 165 { 166 auto parsed = IRCMessage("@ban-reason=Follow\\sthe\\srules :tmi.twitch.tv CLEARCHAT #channel :target_username"); 167 assert(parsed.tags.banDuration.isNull); 168 assert(parsed.tags.banReason == "Follow the rules"); 169 } 170 }