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 }