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