1 module virc.ircmessage;
2 
3 import virc.common : User;
4 import virc.ircv3.batch;
5 import virc.ircv3.tags;
6 
7 import std.typecons : Nullable;
8 
9 struct IRCMessage {
10 	string raw;
11 	IRCTags tags;
12 	private string nonTaggedString;
13 	Nullable!User sourceUser;
14 	string verb;
15 	private string argString;
16 	BatchInformation batch;
17 
18 	invariant() {
19 		import std.algorithm.searching : canFind;
20 		assert(!verb.canFind(" "), "Verb cannot contain spaces");
21 	}
22 
23 	this(string msg) @safe pure {
24 		import std.algorithm.iteration : splitter;
25 		import std.algorithm.searching : findSplit;
26 		import std.string : join;
27 		raw = msg;
28 		if (raw[0] == '@') {
29 			auto split = msg.findSplit(" ");
30 			assert(split, "Found nothing following tags");
31 			tags = IRCTags(split[0][1..$]);
32 			nonTaggedString = split[2];
33 		} else {
34 			nonTaggedString = msg;
35 		}
36 		auto split = nonTaggedString.splitter(" ");
37 		if (split.front[0] == ':') {
38 			sourceUser = User(split.front[1..$]);
39 			do {
40 				split.popFront();
41 			} while((split.front == "") && !split.empty);
42 		}
43 		verb = split.front;
44 		do {
45 			split.popFront();
46 		} while(!split.empty && (split.front == ""));
47 		argString = split.join(" ");
48 	}
49 	static IRCMessage fromClient(string str) @safe {
50 		return IRCMessage(str);
51 	}
52 	static IRCMessage fromServer(string str) @safe {
53 		return IRCMessage(str);
54 	}
55 
56 	auto args() const @safe {
57 		import virc.ircsplitter : IRCSplitter;
58 		return IRCSplitter(argString);
59 	}
60 
61 	void args(string input) @safe {
62 		argString = ":"~input;
63 	}
64 
65 	void args(string[] input) @safe {
66 		import std.algorithm.searching : canFind;
67 		import std.format : format;
68 
69 		if (input.length == 0) {
70 			argString = "";
71 			return;
72 		}
73 
74 		foreach (earlyArg; input[0..$-1]) {
75 			assert(!earlyArg.canFind(" "));
76 		}
77 		argString = format!"%-(%s %|%):%s"(input[0..$-1], input[$-1]);
78 	}
79 	void toString(S)(ref S sink) const @safe {
80 		import std.conv : text;
81 		import std.format : formattedWrite;
82 		import std.range : put;
83 		auto tagString = tags.toString();
84 		if (tagString != "") {
85 			put(sink, "@");
86 			put(sink, tagString);
87 			put(sink, " ");
88 		}
89 		if (!sourceUser.isNull) {
90 			put(sink, ":");
91 			sink.formattedWrite!"%s"(sourceUser.get);
92 			put(sink, " ");
93 		}
94 		put(sink, verb);
95 		if (argString != "") {
96 			put(sink, " ");
97 			put(sink, argString);
98 		}
99 	}
100 	bool opEquals(const IRCMessage other) const @safe pure {
101 		import std.algorithm.comparison : equal;
102 		return ((this.tags == other.tags) && (this.sourceUser == other.sourceUser) && (this.verb == other.verb) && (this.args.equal(other.args)));
103 	}
104 }
105 
106 @safe unittest {
107 	import std.algorithm.comparison : equal;
108 	import std.conv : text;
109 	import std.range : only;
110 	{
111 		auto msg1 = IRCMessage("foo bar baz asdf");
112 		assert(msg1 == IRCMessage("foo bar baz :asdf"));
113 		with(msg1) {
114 			assert(verb == "foo");
115 			assert(args.equal(only("bar", "baz", "asdf")));
116 		}
117 	}
118 	with(IRCMessage(":src AWAY")) {
119 		assert(verb == "AWAY");
120 		assert(sourceUser.get == User("src"));
121 		assert(args.empty);
122 	}
123 	with(IRCMessage(":src AWAY :")) {
124 		assert(verb == "AWAY");
125 		assert(sourceUser.get == User("src"));
126 		assert(args.equal(only("")));
127 	}
128 	{
129 		auto msg1 = IRCMessage(":coolguy foo bar baz asdf");
130 		assert(msg1 == IRCMessage(":coolguy foo bar baz :asdf"));
131 		with(msg1) {
132 			assert(sourceUser.get == User("coolguy"));
133 			assert(verb == "foo");
134 			assert(args.equal(only("bar", "baz", "asdf")));
135 		}
136 	}
137 	with(IRCMessage("foo bar baz :asdf quux")) {
138 		assert(verb == "foo");
139 		assert(args.equal(only("bar", "baz", "asdf quux")));
140 	}
141 	with(IRCMessage("foo bar baz :")) {
142 		assert(verb == "foo");
143 		assert(args.equal(only("bar", "baz", "")));
144 	}
145 	with(IRCMessage("foo bar baz ::asdf")) {
146 		assert(verb == "foo");
147 		assert(args.equal(only("bar", "baz", ":asdf")));
148 	}
149 	with(IRCMessage(":coolguy foo bar baz :asdf quux")) {
150 		assert(sourceUser.get == User("coolguy"));
151 		assert(verb == "foo");
152 		assert(args.equal(only("bar", "baz", "asdf quux")));
153 	}
154 	with(IRCMessage(":coolguy foo bar baz :  asdf quux ")) {
155 		assert(sourceUser.get == User("coolguy"));
156 		assert(verb == "foo");
157 		assert(args.equal(only("bar", "baz", "  asdf quux ")));
158 	}
159 	with(IRCMessage(":coolguy PRIVMSG bar :lol :) ")) {
160 		assert(sourceUser.get == User("coolguy"));
161 		assert(verb == "PRIVMSG");
162 		assert(args.equal(only("bar", "lol :) ")));
163 	}
164 	with(IRCMessage(":coolguy foo bar baz :")) {
165 		assert(sourceUser.get == User("coolguy"));
166 		assert(verb == "foo");
167 		assert(args.equal(only("bar", "baz", "")));
168 	}
169 	with(IRCMessage(":coolguy foo bar baz :  ")) {
170 		assert(sourceUser.get == User("coolguy"));
171 		assert(verb == "foo");
172 		assert(args.equal(only("bar", "baz", "  ")));
173 	}
174 	{
175 		auto msg1 = IRCMessage(":coolguy foo b\tar baz");
176 		assert(msg1 == IRCMessage(":coolguy foo b\tar :baz"));
177 		with(msg1) {
178 			assert(sourceUser.get == User("coolguy"));
179 			assert(verb == "foo");
180 			assert(args.equal(only("b\tar", "baz")));
181 		}
182 	}
183 	with(IRCMessage("@asd :coolguy foo bar baz :  ")) {
184 		assert(sourceUser.get == User("coolguy"));
185 		assert(verb == "foo");
186 		assert(args.equal(["bar", "baz", "  "]));
187 		assert(tags == ["asd": ""]);
188 	}
189 	{
190 		auto msg1 = IRCMessage("@a=b\\\\and\\nk;d=gh\\:764 foo");
191 		assert(msg1 == IRCMessage("@d=gh\\:764;a=b\\\\and\\nk foo"));
192 		with(msg1) {
193 			assert(tags["a"] == "b\\and\nk");
194 			assert(tags["d"] == "gh;764");
195 			assert(verb == "foo");
196 		}
197 	}
198 	{
199 		auto msg1 = IRCMessage("@a=b\\\\and\\nk;d=gh\\:764 foo par1 par2");
200 		assert(msg1 == IRCMessage("@a=b\\\\and\\nk;d=gh\\:764 foo par1 :par2"));
201 		assert(msg1 == IRCMessage("@d=gh\\:764;a=b\\\\and\\nk foo par1 par2"));
202 		assert(msg1 == IRCMessage("@d=gh\\:764;a=b\\\\and\\nk foo par1 :par2"));
203 		with(msg1) {
204 			assert(tags["a"] == "b\\and\nk");
205 			assert(tags["d"] == "gh;764");
206 			assert(verb == "foo");
207 			assert(args.equal(only("par1", "par2")));
208 		}
209 	}
210 	with(IRCMessage("@c;h=;a=b :quux ab cd")) {
211 		assert(sourceUser.get == User("quux"));
212 		assert(verb == "ab");
213 		assert(args.equal(["cd"]));
214 		assert(tags["c"] == "");
215 		assert(tags["h"] == "");
216 		assert(tags["a"] == "b");
217 	}
218 	with(IRCMessage(":src JOIN #chan")) {
219 		assert(sourceUser.get == User("src"));
220 		assert(verb == "JOIN");
221 		assert(args.equal(["#chan"]));
222 	}
223 	with(IRCMessage(":src JOIN :#chan")) {
224 		assert(sourceUser.get == User("src"));
225 		assert(verb == "JOIN");
226 		assert(args.equal(["#chan"]));
227 	}
228 	with(IRCMessage(":src AWAY")) {
229 		assert(sourceUser.get == User("src"));
230 		assert(verb == "AWAY");
231 		assert(args.empty);
232 	}
233 	with(IRCMessage(":src AWAY ")) {
234 		assert(sourceUser.get == User("src"));
235 		assert(verb == "AWAY");
236 		assert(args.empty);
237 	}
238 	with(IRCMessage(":cool\tguy foo bar baz")) {
239 		assert(sourceUser.get == User("cool\tguy"));
240 		assert(verb == "foo");
241 		assert(args.equal(only("bar", "baz")));
242 	}
243 	with(IRCMessage(":coolguy!ag@net\x035w\x03ork.admin PRIVMSG foo :bar baz")) {
244 		assert(sourceUser.get == User("coolguy!ag@net\x035w\x03ork.admin"));
245 		assert(verb == "PRIVMSG");
246 		assert(args.equal(only("foo", "bar baz")));
247 	}
248 	with(IRCMessage(":coolguy!~ag@n\x02et\x0305w\x0fork.admin PRIVMSG foo :bar baz")) {
249 		assert(sourceUser.get == User("coolguy!~ag@n\x02et\x0305w\x0fork.admin"));
250 		assert(verb == "PRIVMSG");
251 		assert(args.equal(only("foo", "bar baz")));
252 	}
253 	with(IRCMessage("@tag1=value1;tag2;vendor1/tag3=value2;vendor2/tag4 :irc.example.com COMMAND param1 param2 :param3 param3")) {
254 		assert(sourceUser.get == User("irc.example.com"));
255 		assert(verb == "COMMAND");
256 		assert(args.equal(only("param1", "param2", "param3 param3")));
257 		assert(tags["tag1"] == "value1");
258 		assert(tags["tag2"] == "");
259 		assert(tags["vendor1/tag3"] == "value2");
260 		assert(tags["vendor2/tag4"] == "");
261 	}
262 	with(IRCMessage(":irc.example.com COMMAND param1 param2 :param3 param3")) {
263 		assert(sourceUser.get == User("irc.example.com"));
264 		assert(verb == "COMMAND");
265 		assert(args.equal(only("param1", "param2", "param3 param3")));
266 	}
267 	with(IRCMessage("@tag1=value1;tag2;vendor1/tag3=value2;vendor2/tag4 COMMAND param1 param2 :param3 param3")) {
268 		assert(verb == "COMMAND");
269 		assert(args.equal(only("param1", "param2", "param3 param3")));
270 		assert(tags["tag1"] == "value1");
271 		assert(tags["tag2"] == "");
272 		assert(tags["vendor1/tag3"] == "value2");
273 		assert(tags["vendor2/tag4"] == "");
274 	}
275 	with(IRCMessage("@foo=\\\\\\\\\\:\\\\s\\s\\r\\n COMMAND")) {
276 		assert(verb == "COMMAND");
277 		assert(args.empty);
278 		assert(tags["foo"] == "\\\\;\\s \r\n");
279 	}
280 	with(IRCMessage(":gravel.mozilla.org 432  #momo :Erroneous Nickname: Illegal characters")) {
281 		assert(sourceUser.get == User("gravel.mozilla.org"));
282 		assert(verb == "432");
283 		assert(args.equal(only("#momo", "Erroneous Nickname: Illegal characters")));
284 	}
285 	with(IRCMessage(":gravel.mozilla.org MODE #tckk +n ")) {
286 		assert(sourceUser.get == User("gravel.mozilla.org"));
287 		assert(verb == "MODE");
288 		assert(args.equal(only("#tckk", "+n")));
289 	}
290 	with(IRCMessage(":services.esper.net MODE #foo-bar +o foobar  ")) {
291 		assert(sourceUser.get == User("services.esper.net"));
292 		assert(verb == "MODE");
293 		assert(args.equal(only("#foo-bar", "+o", "foobar")));
294 	}
295 	with(IRCMessage("@tag1=value\\\\ntest COMMAND")) {
296 		assert(verb == "COMMAND");
297 		assert(args.empty);
298 		assert(tags["tag1"] == "value\\ntest");
299 	}
300 	with(IRCMessage("@tag1=value\\1 COMMAND")) {
301 		assert(verb == "COMMAND");
302 		assert(args.empty);
303 		assert(tags["tag1"] == "value1");
304 	}
305 	with(IRCMessage("@tag1=value1\\ COMMAND")) {
306 		assert(verb == "COMMAND");
307 		assert(args.empty);
308 		assert(tags["tag1"] == "value1");
309 	}
310 	with(IRCMessage("@tag1=value1\\\\ COMMAND")) {
311 		assert(verb == "COMMAND");
312 		assert(args.empty);
313 		assert(tags["tag1"] == "value1\\");
314 	}
315 	with(IRCMessage(":remote!foo@example.com PRIVMSG local :I like turtles.")) {
316 		assert(sourceUser.get == User("remote!foo@example.com"));
317 		assert(verb == "PRIVMSG");
318 		assert(args.equal(["local", "I like turtles."]));
319 		assert(tags.length == 0);
320 	}
321 	assert(IRCMessage(IRCMessage(":remote!foo@example.com PRIVMSG local :I like turtles.").text) == IRCMessage(":remote!foo@example.com PRIVMSG local :I like turtles."));
322 	with(IRCMessage("@aaa=bbb;ccc;example.com/ddd=eee :nick!ident@host.com PRIVMSG me :Hello")) {
323 		assert(sourceUser.get == User("nick!ident@host.com"));
324 		assert(verb == "PRIVMSG");
325 		assert(args.equal(["me", "Hello"]));
326 		assert(tags["aaa"] == "bbb");
327 		assert(tags["ccc"] == "");
328 		assert(tags["example.com/ddd"] == "eee");
329 	}
330 	assert(IRCMessage(IRCMessage("@aaa=bbb;ccc;example.com/ddd=eee :nick!ident@host.com PRIVMSG me :Hello").text) == IRCMessage("@aaa=bbb;ccc;example.com/ddd=eee :nick!ident@host.com PRIVMSG me :Hello"));
331 	{
332 		auto msg = IRCMessage();
333 		msg.sourceUser = User("server");
334 		msg.verb = "HELLO";
335 		msg.args = "WORLD";
336 		assert(msg.text == ":server HELLO :WORLD");
337 	}
338 	with(IRCMessage(":example.com                   PRIVMSG              local           :I like turtles.")) {
339 		assert(sourceUser.get == User("example.com"));
340 		assert(verb == "PRIVMSG");
341 		assert(args.equal(["local", "I like turtles."]));
342 		assert(tags.length == 0);
343 	}
344 	{
345 		auto msg = IRCMessage();
346 		msg.sourceUser = User("server");
347 		msg.verb = "HELLO";
348 		msg.args = string[].init;
349 		assert(msg.text == ":server HELLO");
350 	}
351 	{
352 		auto msg = IRCMessage();
353 		msg.sourceUser = User("server");
354 		msg.verb = "HELLO";
355 		msg.args = ["WORLD"];
356 		assert(msg.text == ":server HELLO :WORLD");
357 	}
358 	{
359 		auto msg = IRCMessage();
360 		msg.sourceUser = User("server");
361 		msg.verb = "HELLO";
362 		msg.args = ["WORLD", "!!!"];
363 		assert(msg.text == ":server HELLO WORLD :!!!");
364 	}
365 }