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 }