1 /++ 2 + Module for supporting IRCv3's BATCH capability. 3 +/ 4 module virc.ircv3.batch; 5 import virc.ircv3.tags; 6 import virc.ircmessage; 7 /++ 8 + 9 +/ 10 struct BatchProcessor { 11 /// 12 IRCMessage[] batchless; 13 private Batch[string] batchCache; 14 /// 15 Batch[] batches; 16 /// 17 bool[] consumeBatch; 18 /// 19 void put(string line) @safe pure { 20 put(IRCMessage(line)); 21 } 22 /// 23 void put(IRCMessage msg) @safe pure { 24 auto processed = BatchCommand(msg); 25 Batch newBatch; 26 if (processed.isValid && processed.isNew) { 27 newBatch.info.referenceTag = processed.referenceTag; 28 newBatch.info.type = processed.type; 29 newBatch.info.parameters = processed.parameters; 30 newBatch.info.tags = processed.tags; 31 } 32 if ("batch" !in msg.tags) { 33 if (processed.isValid) { 34 if (processed.isNew) { 35 batchCache[newBatch.info.referenceTag] = newBatch; 36 } else if (processed.isClosed) { 37 batches ~= batchCache[processed.referenceTag]; 38 consumeBatch ~= true; 39 batchCache.remove(processed.referenceTag); 40 } 41 } else { 42 batchless ~= msg; 43 consumeBatch ~= false; 44 } 45 } else { 46 void findBatch(ref Batch[string] searchBatches, string identifier) @safe pure { 47 foreach (ref batch; searchBatches) { 48 if (batch.info.referenceTag == identifier) { 49 if (processed.isValid) { 50 if (processed.isNew) 51 batch.nestedBatches[newBatch.info.referenceTag] = newBatch; 52 } else { 53 batch.put(msg); 54 } 55 return; 56 } 57 else 58 findBatch(batch.nestedBatches, identifier); 59 } 60 } 61 findBatch(batchCache, msg.tags["batch"]); 62 } 63 } 64 /// 65 auto empty() { 66 import std.range : empty; 67 return (batches.empty && batchless.empty); 68 } 69 /// 70 void popFront() @safe pure { 71 if (consumeBatch[0]) { 72 batches = batches[1..$]; 73 } else 74 batchless = batchless[1..$]; 75 consumeBatch = consumeBatch[1..$]; 76 } 77 /// 78 auto front() { 79 if (consumeBatch[0]) 80 return batches[0]; 81 else 82 return Batch(BatchInformation(false, "", "NOT A BATCH", []), [batchless[0]]); 83 } 84 } 85 private struct BatchCommand { 86 import std.typecons : Nullable; 87 import virc.common : User; 88 Nullable!User source; 89 string referenceTag; 90 string type; 91 string[] parameters; 92 bool isNew; 93 bool isValid = false; 94 IRCTags tags; 95 this(IRCMessage msg) @safe pure nothrow { 96 import std.array : array; 97 if (msg.verb != "BATCH") { 98 return; 99 } 100 isValid = true; 101 source = msg.sourceUser; 102 auto args = msg.args; 103 referenceTag = args.front[1..$]; 104 tags = msg.tags; 105 isNew = (args.front[0] == '+'); 106 if (isNew) { 107 args.popFront(); 108 type = args.front; 109 args.popFront(); 110 if (!args.empty) { 111 parameters = args.array; 112 } 113 } 114 } 115 auto isClosed() { return !isNew; } 116 } 117 /++ 118 + 119 +/ 120 struct Batch { 121 ///Metadata attached to this batch 122 BatchInformation info; 123 ///Lines captured minus the batch tag and starting/ending commands. 124 IRCMessage[] lines; 125 ///Any batches nested inside this one. 126 Batch[string] nestedBatches; 127 /// 128 void put(IRCMessage line) @safe pure { 129 line.batch = info; 130 lines ~= line; 131 } 132 } 133 /++ 134 + 135 +/ 136 struct BatchInformation { 137 /// 138 bool isValidBatch = false; 139 ///A simple string identifying the batch. Uniqueness is not guaranteed? 140 string referenceTag; 141 ///Indicates how the batch is to be processed. Examples include netsplit, netjoin, chathistory 142 string type; 143 ///Miscellaneous details associated with the batch. Meanings vary based on type. 144 string[] parameters; 145 ///Tags attached to the opening BATCH verb 146 IRCTags tags; 147 } 148 @safe pure /+nothrow+/ unittest { 149 import std.algorithm : copy; 150 import std.range : isInputRange, isOutputRange, takeOne; 151 static assert(isOutputRange!(BatchProcessor, string), "BatchProcessor failed outputrange test"); 152 static assert(isInputRange!BatchProcessor, "BatchProcessor failed inputrange test"); 153 //Example from http://ircv3.net/specs/extensions/batch-3.2.html 154 { 155 auto batchProcessor = new BatchProcessor; 156 auto lines = [`:irc.host BATCH +yXNAbvnRHTRBv netsplit irc.hub other.host`, 157 `@batch=yXNAbvnRHTRBv :aji!a@a QUIT :irc.hub other.host`, 158 `@batch=yXNAbvnRHTRBv :nenolod!a@a QUIT :irc.hub other.host`, 159 `:nick!user@host PRIVMSG #channel :This is not in batch, so processed immediately`, 160 `@batch=yXNAbvnRHTRBv :jilles!a@a QUIT :irc.hub other.host`, 161 `:irc.host BATCH -yXNAbvnRHTRBv`]; 162 copy(lines, batchProcessor); 163 { 164 const batch = takeOne(batchProcessor).front; 165 assert(batch.lines == [ 166 IRCMessage(":nick!user@host PRIVMSG #channel :This is not in batch, so processed immediately") 167 ]); 168 } 169 batchProcessor.popFront(); 170 { 171 const batch = takeOne(batchProcessor).front; 172 assert(batch.info.referenceTag == `yXNAbvnRHTRBv`); 173 assert(batch.info.type == `netsplit`); 174 assert(batch.info.parameters == [`irc.hub`, `other.host`]); 175 assert(batch.lines == [ 176 IRCMessage(`@batch=yXNAbvnRHTRBv :aji!a@a QUIT :irc.hub other.host`), 177 IRCMessage(`@batch=yXNAbvnRHTRBv :nenolod!a@a QUIT :irc.hub other.host`), 178 IRCMessage(`@batch=yXNAbvnRHTRBv :jilles!a@a QUIT :irc.hub other.host`) 179 ]); 180 } 181 batchProcessor.popFront(); 182 assert(batchProcessor.empty); 183 } 184 //ditto 185 { 186 auto batchProcessor = new BatchProcessor; 187 auto lines = [`:irc.host BATCH +1 example.com/foo`, 188 `@batch=1 :nick!user@host PRIVMSG #channel :Message 1`, 189 `:irc.host BATCH +2 example.com/foo`, 190 `@batch=1 :nick!user@host PRIVMSG #channel :Message 2`, 191 `@batch=2 :nick!user@host PRIVMSG #channel :Message 4`, 192 `@batch=1 :nick!user@host PRIVMSG #channel :Message 3`, 193 `:irc.host BATCH -1`, 194 `@batch=2 :nick!user@host PRIVMSG #channel :Message 5`, 195 `:irc.host BATCH -2`]; 196 copy(lines, batchProcessor); 197 { 198 const batch = takeOne(batchProcessor).front; 199 assert(batch.info.type == "example.com/foo"); 200 assert(batch.info.referenceTag == "1"); 201 assert(batch.lines == [ 202 IRCMessage("@batch=1 :nick!user@host PRIVMSG #channel :Message 1"), 203 IRCMessage("@batch=1 :nick!user@host PRIVMSG #channel :Message 2"), 204 IRCMessage("@batch=1 :nick!user@host PRIVMSG #channel :Message 3") 205 ]); 206 } 207 batchProcessor.popFront(); 208 { 209 const batch = takeOne(batchProcessor).front; 210 assert(batch.info.type == "example.com/foo"); 211 assert(batch.info.referenceTag == "2"); 212 assert(batch.lines == [ 213 IRCMessage("@batch=2 :nick!user@host PRIVMSG #channel :Message 4"), 214 IRCMessage("@batch=2 :nick!user@host PRIVMSG #channel :Message 5") 215 ]); 216 } 217 batchProcessor.popFront(); 218 assert(batchProcessor.empty); 219 } 220 //ditto 221 { 222 auto batchProcessor = new BatchProcessor; 223 auto lines = [`:irc.host BATCH +outer example.com/foo`, 224 `@batch=outer :irc.host BATCH +inner example.com/bar`, 225 `@batch=inner :nick!user@host PRIVMSG #channel :Hi`, 226 `@batch=outer :irc.host BATCH -inner`, 227 `:irc.host BATCH -outer`]; 228 copy(lines, batchProcessor); 229 { 230 auto batch = takeOne(batchProcessor).front; 231 assert(batch.info.type == "example.com/foo"); 232 assert(batch.info.referenceTag == "outer"); 233 assert(batch.info.parameters == []); 234 assert(batch.lines == []); 235 assert("inner" in batch.nestedBatches); 236 assert(batch.nestedBatches["inner"].info.type == "example.com/bar"); 237 assert(batch.nestedBatches["inner"].info.referenceTag == "inner"); 238 assert(batch.nestedBatches["inner"].info.parameters == []); 239 assert(batch.nestedBatches["inner"].lines == [ 240 IRCMessage("@batch=inner :nick!user@host PRIVMSG #channel :Hi") 241 ]); 242 } 243 batchProcessor.popFront(); 244 assert(batchProcessor.empty); 245 } 246 //Example from http://ircv3.net/specs/extensions/batch/netsplit-3.2.html 247 { 248 auto batchProcessor = new BatchProcessor; 249 auto lines = [`:irc.host BATCH +yXNAbvnRHTRBv netsplit irc.hub other.host`, 250 `@batch=yXNAbvnRHTRBv :aji!a@a QUIT :irc.hub other.host`, 251 `@batch=yXNAbvnRHTRBv :nenolod!a@a QUIT :irc.hub other.host`, 252 `@batch=yXNAbvnRHTRBv :jilles!a@a QUIT :irc.hub other.host`, 253 `:irc.host BATCH -yXNAbvnRHTRBv`]; 254 copy(lines, batchProcessor); 255 { 256 const batch = takeOne(batchProcessor).front; 257 assert(batch.info.type == "netsplit"); 258 assert(batch.info.referenceTag == "yXNAbvnRHTRBv"); 259 assert(batch.info.parameters == ["irc.hub", "other.host"]); 260 assert(batch.lines == [ 261 IRCMessage("@batch=yXNAbvnRHTRBv :aji!a@a QUIT :irc.hub other.host"), 262 IRCMessage("@batch=yXNAbvnRHTRBv :nenolod!a@a QUIT :irc.hub other.host"), 263 IRCMessage(`@batch=yXNAbvnRHTRBv :jilles!a@a QUIT :irc.hub other.host`) 264 ]); 265 } 266 batchProcessor.popFront(); 267 assert(batchProcessor.empty); 268 } 269 //ditto 270 { 271 auto batchProcessor = new BatchProcessor; 272 auto lines = [`:irc.host BATCH +4lMeQwsaOMs6s netjoin irc.hub other.host`, 273 `@batch=4lMeQwsaOMs6s :aji!a@a JOIN #atheme`, 274 `@batch=4lMeQwsaOMs6s :nenolod!a@a JOIN #atheme`, 275 `@batch=4lMeQwsaOMs6s :jilles!a@a JOIN #atheme`, 276 `@batch=4lMeQwsaOMs6s :nenolod!a@a JOIN #ircv3`, 277 `@batch=4lMeQwsaOMs6s :jilles!a@a JOIN #ircv3`, 278 `@batch=4lMeQwsaOMs6s :Elizacat!a@a JOIN #ircv3`, 279 `:irc.host BATCH -4lMeQwsaOMs6s`]; 280 copy(lines, batchProcessor); 281 { 282 const batch = takeOne(batchProcessor).front; 283 assert(batch.info.type == "netjoin"); 284 assert(batch.info.referenceTag == "4lMeQwsaOMs6s"); 285 assert(batch.info.parameters == ["irc.hub", "other.host"]); 286 assert(batch.lines == [ 287 IRCMessage("@batch=4lMeQwsaOMs6s :aji!a@a JOIN #atheme"), 288 IRCMessage("@batch=4lMeQwsaOMs6s :nenolod!a@a JOIN #atheme"), 289 IRCMessage(`@batch=4lMeQwsaOMs6s :jilles!a@a JOIN #atheme`), 290 IRCMessage(`@batch=4lMeQwsaOMs6s :nenolod!a@a JOIN #ircv3`), 291 IRCMessage(`@batch=4lMeQwsaOMs6s :jilles!a@a JOIN #ircv3`), 292 IRCMessage(`@batch=4lMeQwsaOMs6s :Elizacat!a@a JOIN #ircv3`) 293 ]); 294 } 295 batchProcessor.popFront(); 296 assert(batchProcessor.empty); 297 } 298 //Upcoming chathistory batch, subject to change 299 { 300 auto batchProcessor = new BatchProcessor; 301 auto lines = [`:irc.host BATCH +sxtUfAeXBgNoD chathistory #channel`, 302 `@batch=sxtUfAeXBgNoD;time=2015-06-26T19:40:31.230Z :foo!foo@example.com PRIVMSG #channel :I like turtles.`, 303 `@batch=sxtUfAeXBgNoD;time=2015-06-26T19:43:53.410Z :bar!bar@example.com NOTICE #channel :Tortoises are better.`, 304 `@batch=sxtUfAeXBgNoD;time=2015-06-26T19:48:18.140Z :irc.host PRIVMSG #channel :Squishy animals are inferior to computers.`, 305 `:irc.host BATCH -sxtUfAeXBgNoD`]; 306 copy(lines, batchProcessor); 307 { 308 const batch = takeOne(batchProcessor).front; 309 assert(batch.info.type == "chathistory"); 310 assert(batch.info.referenceTag == "sxtUfAeXBgNoD"); 311 assert(batch.info.parameters == ["#channel"]); 312 assert(batch.lines == [ 313 IRCMessage("@batch=sxtUfAeXBgNoD;time=2015-06-26T19:40:31.230Z :foo!foo@example.com PRIVMSG #channel :I like turtles."), 314 IRCMessage("@batch=sxtUfAeXBgNoD;time=2015-06-26T19:43:53.410Z :bar!bar@example.com NOTICE #channel :Tortoises are better."), 315 IRCMessage(`@batch=sxtUfAeXBgNoD;time=2015-06-26T19:48:18.140Z :irc.host PRIVMSG #channel :Squishy animals are inferior to computers.`) 316 ]); 317 } 318 batchProcessor.popFront(); 319 assert(batchProcessor.empty); 320 } 321 //ditto 322 { 323 auto batchProcessor = new BatchProcessor; 324 auto lines = [`:irc.host BATCH +sxtUfAeXBgNoD chathistory remote`, 325 `@batch=sxtUfAeXBgNoD;time=2015-06-26T19:40:31.230Z :remote!foo@example.com PRIVMSG local :I like turtles.`, 326 `@batch=sxtUfAeXBgNoD;time=2015-06-26T19:43:53.410Z :local!bar@example.com PRIVMSG remote :Tortoises are better.`, 327 `:irc.host BATCH -sxtUfAeXBgNoD`]; 328 copy(lines, batchProcessor); 329 { 330 const batch = takeOne(batchProcessor).front; 331 assert(batch.info.type == "chathistory"); 332 assert(batch.info.referenceTag == "sxtUfAeXBgNoD"); 333 assert(batch.info.parameters == ["remote"]); 334 assert(batch.lines == [ 335 IRCMessage("@batch=sxtUfAeXBgNoD;time=2015-06-26T19:40:31.230Z :remote!foo@example.com PRIVMSG local :I like turtles."), 336 IRCMessage("@batch=sxtUfAeXBgNoD;time=2015-06-26T19:43:53.410Z :local!bar@example.com PRIVMSG remote :Tortoises are better.") 337 ]); 338 } 339 batchProcessor.popFront(); 340 assert(batchProcessor.empty); 341 } 342 { //Non-batch 343 auto batchProcessor = new BatchProcessor; 344 auto lines = [`@time=2015-06-26T19:40:31.230Z :remote!foo@example.com PRIVMSG local :I like turtles.`]; 345 copy(lines, batchProcessor); 346 { 347 const batch = takeOne(batchProcessor).front; 348 assert(batch.lines == [IRCMessage("@time=2015-06-26T19:40:31.230Z :remote!foo@example.com PRIVMSG local :I like turtles.")]); 349 } 350 batchProcessor.popFront(); 351 assert(batchProcessor.empty); 352 } 353 { //Non-batch 354 auto batchProcessor = new BatchProcessor; 355 batchProcessor.put(`@time=2015-06-26T19:40:31.230Z :remote!foo@example.com PRIVMSG local :I like turtles.`); 356 { 357 const batch = takeOne(batchProcessor).front; 358 assert(batch.lines == [IRCMessage("@time=2015-06-26T19:40:31.230Z :remote!foo@example.com PRIVMSG local :I like turtles.")]); 359 } 360 batchProcessor.popFront(); 361 batchProcessor.put(`@time=2015-06-26T19:40:31.230Z :remote!foo@example.com PRIVMSG local :I like turtles.`); 362 { 363 const batch = takeOne(batchProcessor).front; 364 assert(batch.lines == [IRCMessage("@time=2015-06-26T19:40:31.230Z :remote!foo@example.com PRIVMSG local :I like turtles.")]); 365 } 366 batchProcessor.popFront(); 367 assert(batchProcessor.empty); 368 } 369 //Tagged batch 370 { 371 auto batchProcessor = new BatchProcessor; 372 auto lines = [`@tag=value;another=something :irc.host BATCH +yXNAbvnRHTRBv netsplit irc.hub other.host`, 373 `@batch=yXNAbvnRHTRBv :aji!a@a QUIT :irc.hub other.host`, 374 `@batch=yXNAbvnRHTRBv :nenolod!a@a QUIT :irc.hub other.host`, 375 `@batch=yXNAbvnRHTRBv :jilles!a@a QUIT :irc.hub other.host`, 376 `:irc.host BATCH -yXNAbvnRHTRBv`]; 377 copy(lines, batchProcessor); 378 { 379 const batch = takeOne(batchProcessor).front; 380 assert(batch.info.type == "netsplit"); 381 assert(batch.info.tags["tag"] == "value"); 382 assert(batch.info.tags["another"] == "something"); 383 assert(batch.info.referenceTag == "yXNAbvnRHTRBv"); 384 assert(batch.info.parameters == ["irc.hub", "other.host"]); 385 assert(batch.lines == [ 386 IRCMessage("@batch=yXNAbvnRHTRBv :aji!a@a QUIT :irc.hub other.host"), 387 IRCMessage("@batch=yXNAbvnRHTRBv :nenolod!a@a QUIT :irc.hub other.host"), 388 IRCMessage(`@batch=yXNAbvnRHTRBv :jilles!a@a QUIT :irc.hub other.host`) 389 ]); 390 } 391 batchProcessor.popFront(); 392 assert(batchProcessor.empty); 393 } 394 }