1 /++ 2 + 3 +/ 4 module virc.numerics.rfc1459; 5 6 import virc.modes : ModeType; 7 import virc.numerics.definitions; 8 9 /++ 10 + 11 +/ 12 struct LUserClient { 13 /// 14 string message; 15 /// 16 this(string msg) pure @safe { 17 message = msg; 18 } 19 } 20 /++ 21 + 22 +/ 23 struct LUserMe { 24 /// 25 string message; 26 /// 27 this(string msg) pure @safe { 28 message = msg; 29 } 30 } 31 /++ 32 + 33 +/ 34 struct LUserChannels { 35 /// 36 ulong numChannels; 37 /// 38 string message; 39 /// 40 this(string chans, string msg) pure @safe { 41 import std.conv : to; 42 numChannels = chans.to!ulong; 43 message = msg; 44 } 45 } 46 /++ 47 + 48 +/ 49 struct LUserOp { 50 /// 51 ulong numOperators; 52 /// 53 string message; 54 /// 55 this(string ops, string msg) pure @safe { 56 import std.conv : to; 57 numOperators = ops.to!ulong; 58 message = msg; 59 } 60 } 61 /++ 62 + RPL_VERSION reply contents. 63 +/ 64 struct VersionReply { 65 import virc.common : User; 66 User me; 67 ///The responding server's version string. 68 string version_; 69 ///The server hostmask responding to the version query 70 string server; 71 ///Contents depend on server, but are usually related to version 72 string comments; 73 } 74 75 /++ 76 + RPL_REHASHING reply contents. 77 +/ 78 struct RehashingReply { 79 import virc.common : User; 80 User me; 81 ///The config file being rehashed. 82 string configFile; 83 ///Message displayed to user. 84 string message; 85 } 86 87 /++ 88 + 89 +/ 90 //251 :There are <users> users and <services> services on <servers> servers 91 //TODO: Find out if this is safe to parse 92 auto parseNumeric(Numeric numeric : Numeric.RPL_LUSERCLIENT, T)(T input) { 93 input.popFront(); 94 return LUserClient(input.front); 95 } 96 /// 97 @safe pure /+nothrow @nogc+/ unittest { //Numeric.RPL_LUSERCLIENT 98 import std.range : only; 99 { 100 immutable luser = parseNumeric!(Numeric.RPL_LUSERCLIENT)(only("someone", "There are 42 users and 43 services on 44 servers")); 101 assert(luser.message == "There are 42 users and 43 services on 44 servers"); 102 } 103 } 104 /++ 105 + 106 +/ 107 //252 <opers> :operator(s) online 108 auto parseNumeric(Numeric numeric : Numeric.RPL_LUSEROP, T)(T input) { 109 input.popFront(); 110 auto ops = input.front; 111 input.popFront(); 112 auto msg = input.front; 113 auto output = LUserOp(ops, msg); 114 return output; 115 } 116 /// 117 @safe pure /+nothrow @nogc+/ unittest { //Numeric.RPL_LUSEROP 118 import std.range : only; 119 { 120 immutable luser = parseNumeric!(Numeric.RPL_LUSEROP)(only("someone", "45", "operator(s) online")); 121 assert(luser.numOperators == 45); 122 assert(luser.message == "operator(s) online"); 123 } 124 } 125 126 /++ 127 + 128 +/ 129 //254 <channels> :channels formed 130 auto parseNumeric(Numeric numeric : Numeric.RPL_LUSERCHANNELS, T)(T input) { 131 input.popFront(); 132 auto chans = input.front; 133 input.popFront(); 134 auto msg = input.front; 135 auto output = LUserChannels(chans, msg); 136 return output; 137 } 138 /// 139 @safe pure /+nothrow @nogc+/ unittest { //Numeric.RPL_LUSERCHANNELS 140 import std.range : only; 141 { 142 immutable luser = parseNumeric!(Numeric.RPL_LUSERCHANNELS)(only("someone", "46", "channels formed")); 143 assert(luser.numChannels == 46); 144 assert(luser.message == "channels formed"); 145 } 146 } 147 /++ 148 + 149 +/ 150 //255 :I have <clients> clients and <servers> servers 151 //TODO: Find out if this is safe to parse 152 auto parseNumeric(Numeric numeric : Numeric.RPL_LUSERME, T)(T input) { 153 input.popFront(); 154 return LUserMe(input.front); 155 } 156 /// 157 @safe pure /+nothrow @nogc+/ unittest { //Numeric.RPL_LUSERME 158 import std.range : only; 159 { 160 immutable luser = parseNumeric!(Numeric.RPL_LUSERME)(only("someone", "I have 47 clients and 48 servers")); 161 assert(luser.message == "I have 47 clients and 48 servers"); 162 } 163 } 164 165 struct ChannelListResult { 166 import virc.common : Topic; 167 import virc.modes : Mode; 168 string name; 169 uint userCount; 170 Topic topic; 171 Mode[] modes; 172 } 173 174 /++ 175 + 176 +/ 177 //322 <username> <channel> <count> :[\[<modes\] ]<topic> 178 auto parseNumeric(Numeric numeric : Numeric.RPL_LIST, T)(T input, ModeType[char] channelModeTypes) { 179 import std.algorithm.iteration : filter, map; 180 import std.algorithm.searching : findSplitAfter, startsWith; 181 import std.array : array; 182 import std.conv : parse; 183 import virc.common : Channel, Topic; 184 import virc.modes : Change, parseModeString; 185 //Note: RFC2812 makes no mention of the modes being included. 186 //Seems to be a de-facto standard, supported by several softwares. 187 ChannelListResult channel; 188 //username doesn't really help us here. skip it 189 input.popFront(); 190 channel.name = input.front; 191 input.popFront(); 192 auto str = input.front; 193 channel.userCount = parse!uint(str); 194 input.popFront(); 195 if (input.front.startsWith("[+")) { 196 auto splitTopicStr = input.front.findSplitAfter("] "); 197 channel.topic = Topic(splitTopicStr[1]); 198 channel.modes = parseModeString(splitTopicStr[0][1..$], channelModeTypes).filter!(x => x.change == Change.set).map!(x => x.mode).array; 199 } else { 200 channel.topic = Topic(input.front); 201 } 202 return channel; 203 } 204 /// 205 @safe pure /+nothrow @nogc+/ unittest { //Numeric.RPL_LIST 206 import std.algorithm.searching : canFind; 207 import std.range : only; 208 import std.typecons : Nullable; 209 import virc.common : Topic; 210 import virc.modes : Mode; 211 { 212 immutable listEntry = parseNumeric!(Numeric.RPL_LIST)(only("someone", "#test", "4", "[+fnt 200:2] some words"), ['n': ModeType.d, 't': ModeType.d, 'f': ModeType.c]); 213 assert(listEntry.name == "#test"); 214 assert(listEntry.userCount == 4); 215 assert(listEntry.topic == Topic("some words")); 216 assert(listEntry.modes.canFind(Mode(ModeType.d, 'n'))); 217 assert(listEntry.modes.canFind(Mode(ModeType.d, 't'))); 218 assert(listEntry.modes.canFind(Mode(ModeType.c, 'f', Nullable!string("100:2")))); 219 } 220 { 221 immutable listEntry = parseNumeric!(Numeric.RPL_LIST)(only("someone", "#test2", "6", "[+fnst 100:2] some more words"), ['n': ModeType.d, 't': ModeType.d, 'f': ModeType.c]); 222 assert(listEntry.name == "#test2"); 223 assert(listEntry.userCount == 6); 224 assert(listEntry.topic == Topic("some more words")); 225 assert(listEntry.modes.canFind(Mode(ModeType.d, 'n'))); 226 assert(listEntry.modes.canFind(Mode(ModeType.d, 't'))); 227 assert(listEntry.modes.canFind(Mode(ModeType.d, 's'))); 228 assert(listEntry.modes.canFind(Mode(ModeType.c, 'f', Nullable!string("100:2")))); 229 } 230 { 231 immutable listEntry = parseNumeric!(Numeric.RPL_LIST)(only("someone", "#test3", "1", "no modes?"), ['n': ModeType.d, 't': ModeType.d, 'f': ModeType.c]); 232 assert(listEntry.name == "#test3"); 233 assert(listEntry.userCount == 1); 234 assert(listEntry.topic == Topic("no modes?")); 235 assert(listEntry.modes.length == 0); 236 } 237 } 238 struct TopicReply { 239 string channel; 240 string topic; 241 } 242 /++ 243 + Parser for RPL_TOPIC. 244 + 245 + Format is `332 <client><channel> :<topic>` 246 +/ 247 auto parseNumeric(Numeric numeric : Numeric.RPL_TOPIC, T)(T input) { 248 import std.typecons : Nullable; 249 Nullable!TopicReply output = TopicReply(); 250 if (input.empty) { 251 output.nullify; 252 return output; 253 } 254 input.popFront(); 255 if (input.empty) { 256 output.nullify; 257 return output; 258 } 259 output.get.channel = input.front; 260 input.popFront(); 261 if (input.empty) { 262 output.nullify; 263 return output; 264 } 265 output.get.topic = input.front; 266 return output; 267 } 268 /// 269 @safe pure nothrow @nogc unittest { 270 import std.range : only, takeNone; 271 { 272 immutable topic = parseNumeric!(Numeric.RPL_TOPIC)(only("someone", "#channel", "This is the topic!")); 273 assert(topic.get.channel == "#channel"); 274 assert(topic.get.topic == "This is the topic!"); 275 } 276 { 277 immutable topic = parseNumeric!(Numeric.RPL_TOPIC)(takeNone(only(""))); 278 assert(topic.isNull); 279 } 280 { 281 immutable topic = parseNumeric!(Numeric.RPL_TOPIC)(only("someone")); 282 assert(topic.isNull); 283 } 284 { 285 immutable topic = parseNumeric!(Numeric.RPL_TOPIC)(only("someone", "#channel")); 286 assert(topic.isNull); 287 } 288 } 289 struct NamesReply { 290 import std.algorithm : splitter; 291 import std.traits : ReturnType; 292 import std.typecons : No; 293 NamReplyFlag chanFlag; 294 string channel; 295 string _users; 296 static struct User { 297 string modes; 298 string name; 299 this(string str, char[char] prefixes) @safe pure nothrow { 300 size_t nameStart; 301 foreach (idx, char chr; str) { 302 bool found; 303 foreach (mode; prefixes.keys) { 304 const prefix = prefixes[mode]; 305 if (chr == prefix) { 306 found = true; 307 modes ~= mode; 308 break; 309 } 310 } 311 if (!found) { 312 nameStart = idx; 313 break; 314 } 315 } 316 name = str[nameStart .. $]; 317 } 318 } 319 auto users(char[char] prefixes) inout nothrow { 320 import std.algorithm.iteration : map; 321 return _users.splitter(" ").map!(x => User(x, prefixes)); 322 } 323 } 324 /++ 325 + 326 +/ 327 enum NamReplyFlag : string { 328 ///Channel will never be shown to users that aren't in it 329 secret = "@", 330 ///Channel will have its name replaced for users that aren't in it 331 private_ = "*", 332 ///Non-private and non-secret channel. 333 public_ = "=", 334 ///ditto 335 other = public_ 336 } 337 /++ 338 + Parser for RPL_VERSION 339 + 340 + Format is `351 <client> <version> <server> :<comments>` 341 +/ 342 auto parseNumeric(Numeric numeric : Numeric.RPL_VERSION, T)(T input) { 343 import virc.numerics.magicparser : autoParse; 344 return autoParse!VersionReply(input); 345 } 346 /// 347 @safe pure nothrow @nogc unittest { 348 import std.algorithm.searching : canFind; 349 import std.array : array; 350 import std.range : only, takeNone; 351 import virc.ircsplitter : IRCSplitter; 352 { 353 auto versionReply = parseNumeric!(Numeric.RPL_VERSION)(only("Someone", "ircd-seven-1.1.4(20170104-717fbca8dbac,charybdis-3.4-dev)", "localhost", "eHIKMpSZ6 TS6ow 7IZ")); 354 assert(versionReply.get.version_ == "ircd-seven-1.1.4(20170104-717fbca8dbac,charybdis-3.4-dev)"); 355 assert(versionReply.get.server == "localhost"); 356 assert(versionReply.get.comments == "eHIKMpSZ6 TS6ow 7IZ"); 357 } 358 { 359 immutable versionReply = parseNumeric!(Numeric.RPL_VERSION)(takeNone(only(""))); 360 assert(versionReply.isNull); 361 } 362 { 363 immutable versionReply = parseNumeric!(Numeric.RPL_VERSION)(only("Someone")); 364 assert(versionReply.isNull); 365 } 366 { 367 immutable versionReply = parseNumeric!(Numeric.RPL_VERSION)(only("Someone", "ircd-seven-1.1.4(20170104-717fbca8dbac,charybdis-3.4-dev)")); 368 assert(versionReply.isNull); 369 } 370 { 371 immutable versionReply = parseNumeric!(Numeric.RPL_VERSION)(only("Someone", "", "localhost")); 372 assert(versionReply.isNull); 373 } 374 } 375 376 /++ 377 + Parser for RPL_NAMREPLY 378 + 379 + Format is `353 <client> =/*/@ <channel> :<prefix[es]><usermask>[ <prefix[es]><usermask>...]` 380 +/ 381 auto parseNumeric(Numeric numeric : Numeric.RPL_NAMREPLY, T)(T input) { 382 import std.algorithm : splitter; 383 import std.typecons : Nullable; 384 Nullable!NamesReply output = NamesReply(); 385 if (input.empty) { 386 output.nullify(); 387 return output; 388 } 389 input.popFront(); 390 if (input.empty) { 391 output.nullify(); 392 return output; 393 } 394 output.get.chanFlag = cast(NamReplyFlag)input.front; 395 input.popFront(); 396 if (input.empty) { 397 output.nullify(); 398 return output; 399 } 400 output.get.channel = input.front; 401 input.popFront(); 402 if (input.empty) { 403 output.nullify(); 404 return output; 405 } 406 output.get._users = input.front; 407 return output; 408 } 409 /// 410 @safe pure nothrow unittest { 411 import std.algorithm.searching : canFind; 412 import std.algorithm.comparison : equal; 413 import std.array : array; 414 import std.range : only, takeNone; 415 import virc.ircsplitter : IRCSplitter; 416 { 417 auto namReply = parseNumeric!(Numeric.RPL_NAMREPLY)(IRCSplitter("someone = #channel :User1 User2 @User3 +User4")); 418 assert(namReply.get.chanFlag == NamReplyFlag.public_); 419 assert(namReply.get.channel == "#channel"); 420 auto users = namReply.get.users(['o': '@', 'v': '+']); 421 with(users.front) { 422 assert(modes.equal("")); 423 assert(name == "User1"); 424 } 425 users.popFront(); 426 with(users.front) { 427 assert(modes.equal("")); 428 assert(name == "User2"); 429 } 430 users.popFront(); 431 with(users.front) { 432 assert(modes.equal("o")); 433 assert(name == "User3"); 434 } 435 users.popFront(); 436 with(users.front) { 437 assert(modes.equal("v")); 438 assert(name == "User4"); 439 } 440 } 441 { 442 immutable namReply = parseNumeric!(Numeric.RPL_NAMREPLY)(IRCSplitter("someone = #channel")); 443 assert(namReply.isNull); 444 } 445 { 446 immutable namReply = parseNumeric!(Numeric.RPL_NAMREPLY)(IRCSplitter("someone =")); 447 assert(namReply.isNull); 448 } 449 { 450 immutable namReply = parseNumeric!(Numeric.RPL_NAMREPLY)(IRCSplitter("someone")); 451 assert(namReply.isNull); 452 } 453 { 454 immutable namReply = parseNumeric!(Numeric.RPL_NAMREPLY)(takeNone(only(""))); 455 assert(namReply.isNull); 456 } 457 } 458 /++ 459 + Parser for RPL_REHASHING 460 + 461 + Format is `382 <client> <config file> :Rehashing]` 462 +/ 463 auto parseNumeric(Numeric numeric : Numeric.RPL_REHASHING, T)(T input) { 464 import virc.numerics.magicparser : autoParse; 465 return autoParse!RehashingReply(input); 466 } 467 /// 468 @safe pure nothrow unittest { 469 import std.range : only, takeNone; 470 { 471 auto reply = parseNumeric!(Numeric.RPL_REHASHING)(only("someone", "ircd.conf", "Rehashing")); 472 assert(reply.get.configFile == "ircd.conf"); 473 assert(reply.get.message == "Rehashing"); 474 } 475 { 476 immutable reply = parseNumeric!(Numeric.RPL_REHASHING)(only("someone", "ircd.conf")); 477 assert(reply.isNull); 478 } 479 { 480 immutable reply = parseNumeric!(Numeric.RPL_REHASHING)(only("someone")); 481 assert(reply.isNull); 482 } 483 { 484 immutable reply = parseNumeric!(Numeric.RPL_REHASHING)(takeNone(only(""))); 485 assert(reply.isNull); 486 } 487 } 488 489 struct NoSuchServerError { 490 import virc.common : User; 491 User me; 492 ///Server mask that failed to match any servers. 493 string serverMask; 494 ///User-readable error message 495 string message; 496 } 497 /++ 498 + Parser for ERR_NOSUCHSERVER 499 + 500 + Format is `402 <client> <server> :No such server` 501 +/ 502 auto parseNumeric(Numeric numeric : Numeric.ERR_NOSUCHSERVER, T)(T input) { 503 import virc.numerics.magicparser : autoParse; 504 return autoParse!NoSuchServerError(input); 505 } 506 /// 507 @safe pure nothrow unittest { 508 import std.range : only, takeNone; 509 { 510 auto reply = parseNumeric!(Numeric.ERR_NOSUCHSERVER)(only("someone", "badserver.example.net", "No such server")); 511 assert(reply.get.serverMask == "badserver.example.net"); 512 assert(reply.get.message == "No such server"); 513 } 514 { 515 immutable reply = parseNumeric!(Numeric.ERR_NOSUCHSERVER)(only("someone", "badserver.example.net")); 516 assert(reply.isNull); 517 } 518 { 519 immutable reply = parseNumeric!(Numeric.ERR_NOSUCHSERVER)(only("someone")); 520 assert(reply.isNull); 521 } 522 { 523 immutable reply = parseNumeric!(Numeric.ERR_NOSUCHSERVER)(takeNone(only(""))); 524 assert(reply.isNull); 525 } 526 } 527 struct AwayReply { 528 import virc.common : User; 529 User me; 530 ///User that's away. 531 User user; 532 ///User's away message. 533 string message; 534 } 535 /++ 536 + Parser for RPL_AWAY 537 + 538 + Format is `301 <client> <nick> <message>` 539 +/ 540 auto parseNumeric(Numeric numeric : Numeric.RPL_AWAY, T)(T input) { 541 import virc.numerics.magicparser : autoParse; 542 return autoParse!AwayReply(input); 543 } 544 /// 545 @safe pure nothrow unittest { 546 import virc.common : User; 547 import std.range : only, takeNone; 548 { 549 auto reply = parseNumeric!(Numeric.RPL_AWAY)(only("someone", "awayuser", "On fire")); 550 assert(reply.get.user == User("awayuser")); 551 assert(reply.get.message == "On fire"); 552 } 553 { 554 immutable reply = parseNumeric!(Numeric.RPL_AWAY)(only("someone", "awayuser")); 555 assert(reply.isNull); 556 } 557 { 558 immutable reply = parseNumeric!(Numeric.RPL_AWAY)(only("someone")); 559 assert(reply.isNull); 560 } 561 { 562 immutable reply = parseNumeric!(Numeric.RPL_AWAY)(takeNone(only(""))); 563 assert(reply.isNull); 564 } 565 } 566 /++ 567 + WHOIS reply containing only a nickname and human-readable message. 568 +/ 569 struct InfolessWhoisReply { 570 import virc.common : User; 571 User me; 572 ///User whose query is complete. 573 User user; 574 ///Human-readable numeric message. 575 string message; 576 } 577 /++ 578 + Parser for RPL_ENDOFWHOIS 579 + 580 + Format is `318 <client> <nick> :End of /WHOIS list` 581 +/ 582 auto parseNumeric(Numeric numeric : Numeric.RPL_ENDOFWHOIS, T)(T input) { 583 import virc.numerics.magicparser : autoParse; 584 return autoParse!InfolessWhoisReply(input); 585 } 586 /// 587 @safe pure nothrow unittest { 588 import virc.common : User; 589 import std.range : only, takeNone; 590 { 591 auto reply = parseNumeric!(Numeric.RPL_ENDOFWHOIS)(only("someone", "whoisuser", "End of /WHOIS list")); 592 assert(reply.get.user == User("whoisuser")); 593 } 594 { 595 immutable reply = parseNumeric!(Numeric.RPL_ENDOFWHOIS)(only("someone", "whoisuser")); 596 assert(reply.isNull); 597 } 598 { 599 immutable reply = parseNumeric!(Numeric.RPL_ENDOFWHOIS)(only("someone")); 600 assert(reply.isNull); 601 } 602 { 603 immutable reply = parseNumeric!(Numeric.RPL_ENDOFWHOIS)(takeNone(only(""))); 604 assert(reply.isNull); 605 } 606 } 607 /++ 608 + Parser for RPL_WHOISOPERATOR 609 + 610 + Format is `313 <client> <nick> :is an IRC operator` 611 +/ 612 auto parseNumeric(Numeric numeric : Numeric.RPL_WHOISOPERATOR, T)(T input) { 613 import virc.numerics.magicparser : autoParse; 614 return autoParse!InfolessWhoisReply(input); 615 } 616 /// 617 @safe pure nothrow unittest { 618 import virc.common : User; 619 import std.range : only, takeNone; 620 { 621 auto reply = parseNumeric!(Numeric.RPL_WHOISOPERATOR)(only("someone", "whoisuser", "is an IRC operator")); 622 assert(reply.get.user == User("whoisuser")); 623 } 624 { 625 immutable reply = parseNumeric!(Numeric.RPL_WHOISOPERATOR)(only("someone", "whoisuser")); 626 assert(reply.isNull); 627 } 628 { 629 immutable reply = parseNumeric!(Numeric.RPL_WHOISOPERATOR)(only("someone")); 630 assert(reply.isNull); 631 } 632 { 633 immutable reply = parseNumeric!(Numeric.RPL_WHOISOPERATOR)(takeNone(only(""))); 634 assert(reply.isNull); 635 } 636 } 637 /++ 638 + 639 +/ 640 struct WhoisUserReply { 641 import virc.common : User; 642 User me; 643 ///User whose query is complete. 644 User user; 645 ///User's username. 646 string username; 647 ///User's hostname. 648 string hostname; 649 //Reserved. Just a *. 650 string reserved; 651 ///User's realname. 652 string realname; 653 } 654 /++ 655 + Parser for RPL_WHOISUSER 656 + 657 + Format is `311 <client> <nick> <user> <host> * :<real name>` 658 +/ 659 auto parseNumeric(Numeric numeric : Numeric.RPL_WHOISUSER, T)(T input) { 660 import virc.numerics.magicparser : autoParse; 661 return autoParse!WhoisUserReply(input); 662 } 663 /// 664 @safe pure nothrow unittest { 665 import virc.common : User; 666 import std.range : only, takeNone; 667 { 668 immutable reply = parseNumeric!(Numeric.RPL_WHOISUSER)(only("someone", "whoisuser", "someUsername", "someHostname", "*", "a real name")); 669 assert(reply.get.user == User("whoisuser")); 670 assert(reply.get.username == "someUsername"); 671 assert(reply.get.hostname == "someHostname"); 672 assert(reply.get.realname == "a real name"); 673 } 674 { 675 immutable reply = parseNumeric!(Numeric.RPL_WHOISUSER)(only("someone", "whoisuser", "someUsername", "someHostname", "*")); 676 assert(reply.isNull); 677 } 678 { 679 immutable reply = parseNumeric!(Numeric.RPL_WHOISUSER)(only("someone", "whoisuser", "someUsername", "someHostname")); 680 assert(reply.isNull); 681 } 682 { 683 immutable reply = parseNumeric!(Numeric.RPL_WHOISUSER)(only("someone", "whoisuser", "someUsername")); 684 assert(reply.isNull); 685 } 686 { 687 immutable reply = parseNumeric!(Numeric.RPL_WHOISUSER)(only("someone", "whoisuser")); 688 assert(reply.isNull); 689 } 690 { 691 immutable reply = parseNumeric!(Numeric.RPL_WHOISUSER)(only("someone")); 692 assert(reply.isNull); 693 } 694 { 695 immutable reply = parseNumeric!(Numeric.RPL_WHOISUSER)(takeNone(only(""))); 696 assert(reply.isNull); 697 } 698 } 699 /++ 700 + Newer form of RPL_WHOISIDLE. Also provides the time at which the user 701 + connected. 702 +/ 703 struct WhoisIdleReplyNew { 704 import core.time : Duration; 705 import std.datetime.systime : SysTime; 706 import virc.common : User; 707 User me; 708 ///User whose query is complete. 709 User user; 710 ///How long the user has been idle. 711 Duration idleTime; 712 ///Time at which the user connected. 713 SysTime connectedTime; 714 ///Human-readable numeric message. 715 string message; 716 } 717 /++ 718 + Older form of RPL_WHOISIDLE. Does not have user connection time. 719 +/ 720 struct WhoisIdleReplyOld { 721 import core.time : Duration; 722 import virc.common : User; 723 User me; 724 ///User whose query is complete. 725 User user; 726 ///How long the user has been idle. 727 Duration idleTime; 728 ///Human-readable numeric message. 729 string message; 730 } 731 /++ 732 + Parser for RPL_WHOISIDLE 733 + 734 + Format is `317 <client> <nick> <idle time in seconds> :seconds idle` or 735 + `317 <client> <nick> <idle time in seconds> <time connected> :seconds idle, signon time` 736 +/ 737 auto parseNumeric(Numeric numeric : Numeric.RPL_WHOISIDLE, T)(T input) { 738 import virc.numerics.magicparser : autoParse; 739 return autoParse!(WhoisIdleReplyNew, WhoisIdleReplyOld)(input); 740 } 741 /// 742 @safe pure nothrow unittest { 743 import core.time : seconds; 744 import std.datetime : DateTime, SysTime, UTC; 745 import std.range : only, takeNone; 746 import virc.common : User; 747 static immutable testTime = SysTime(DateTime(2017, 07, 14, 02, 40, 00), UTC()); 748 { 749 immutable reply = parseNumeric!(Numeric.RPL_WHOISIDLE)(only("someone", "whoisuser", "1500", "1500000000", "seconds idle, signon time")); 750 assert(reply.user == User("whoisuser")); 751 assert(reply.idleTime == 1500.seconds); 752 assert(reply.connectedTime.get == testTime); 753 } 754 { 755 immutable reply = parseNumeric!(Numeric.RPL_WHOISIDLE)(only("someone", "whoisuser", "1500", "seconds idle")); 756 assert(reply.user == User("whoisuser")); 757 assert(reply.idleTime == 1500.seconds); 758 assert(reply.connectedTime.isNull); 759 } 760 { 761 immutable reply = parseNumeric!(Numeric.RPL_WHOISIDLE)(only("someone", "whoisuser", "1500")); 762 assert(reply.isNull); 763 } 764 { 765 immutable reply = parseNumeric!(Numeric.RPL_WHOISIDLE)(only("someone", "whoisuser")); 766 assert(reply.isNull); 767 } 768 { 769 immutable reply = parseNumeric!(Numeric.RPL_WHOISIDLE)(only("someone")); 770 assert(reply.isNull); 771 } 772 { 773 immutable reply = parseNumeric!(Numeric.RPL_WHOISIDLE)(takeNone(only(""))); 774 assert(reply.isNull); 775 } 776 } 777 /++ 778 + 779 +/ 780 struct WhoisServerReply { 781 import virc.common : User; 782 User me; 783 ///User whose query is complete. 784 User user; 785 ///Hostmask of server the user is connected to. 786 string server; 787 ///Server description. 788 string serverDescription; 789 } 790 /++ 791 + Parser for RPL_WHOISSERVER 792 + 793 + Format is `312 <client> <nick> <server mask> :<server description>` 794 +/ 795 auto parseNumeric(Numeric numeric : Numeric.RPL_WHOISSERVER, T)(T input) { 796 import virc.numerics.magicparser : autoParse; 797 return autoParse!WhoisServerReply(input); 798 } 799 /// 800 @safe pure nothrow unittest { 801 import std.range : only, takeNone; 802 import virc.common : User; 803 { 804 immutable reply = parseNumeric!(Numeric.RPL_WHOISSERVER)(only("someone", "whoisuser", "example.net", "Mysterious example server")); 805 assert(reply.get.user == User("whoisuser")); 806 assert(reply.get.server == "example.net"); 807 assert(reply.get.serverDescription == "Mysterious example server"); 808 } 809 { 810 immutable reply = parseNumeric!(Numeric.RPL_WHOISSERVER)(only("someone", "whoisuser", "example.net")); 811 assert(reply.isNull); 812 } 813 { 814 immutable reply = parseNumeric!(Numeric.RPL_WHOISSERVER)(only("someone", "whoisuser")); 815 assert(reply.isNull); 816 } 817 { 818 immutable reply = parseNumeric!(Numeric.RPL_WHOISSERVER)(only("someone")); 819 assert(reply.isNull); 820 } 821 { 822 immutable reply = parseNumeric!(Numeric.RPL_WHOISSERVER)(takeNone(only(""))); 823 assert(reply.isNull); 824 } 825 } 826 /++ 827 + 828 +/ 829 struct WhoisChannelReply { 830 import virc.common : User; 831 User me; 832 ///User whose query is complete. 833 User user; 834 ///Channel details of this user, including name and prefix. 835 WhoisChannelReplyChannel[] channels; 836 } 837 /++ 838 + 839 +/ 840 struct WhoisChannelReplyChannel { 841 import std.typecons : Nullable; 842 import virc.common : Channel; 843 ///Mode prefix applied to this user on this channel. 844 Nullable!string prefix; 845 ///Name of the channel. 846 Channel channel; 847 } 848 /++ 849 + Parser for RPL_WHOISCHANNELS 850 + 851 + Format is `319 <client> <nick> <server mask> :<server description>` 852 +/ 853 auto parseNumeric(Numeric numeric : Numeric.RPL_WHOISCHANNELS, T)(T input, string prefixes, string channelTypes) { 854 import std.algorithm.iteration : splitter; 855 import std.typecons : Nullable; 856 import virc.target : Target; 857 import virc.numerics.magicparser : autoParse; 858 struct Reduced { 859 import virc.common : User; 860 User me; 861 User user; 862 string channels; 863 } 864 Nullable!WhoisChannelReply output; 865 auto parsed = autoParse!Reduced(input); 866 if (!parsed.isNull) { 867 output = WhoisChannelReply(); 868 output.get.me = parsed.get.me; 869 output.get.user = parsed.get.user; 870 auto split = parsed.get.channels.splitter(" "); 871 foreach (rawChan; split) { 872 auto parsedChannel = Target(rawChan, prefixes, channelTypes); 873 if (parsedChannel.isChannel) { 874 auto channel = WhoisChannelReplyChannel(); 875 channel.prefix = parsedChannel.prefixes; 876 channel.channel = parsedChannel.channel.get; 877 output.get.channels ~= channel; 878 } 879 } 880 } 881 return output; 882 } 883 /// 884 /+@safe pure nothrow +/unittest { 885 import std.range : only, takeNone; 886 import virc.common : User; 887 import virc.numerics.isupport : defaultModePrefixes; 888 { 889 auto reply = parseNumeric!(Numeric.RPL_WHOISCHANNELS)(only("someone", "whoisuser", "#test3 +#test"), defaultModePrefixes, "#"); 890 assert(reply.get.user == User("whoisuser")); 891 assert(reply.get.channels.length == 2); 892 with(reply.get.channels[0]) { 893 assert(channel == Channel("#test3")); 894 assert(prefix.isNull); 895 } 896 with(reply.get.channels[1]) { 897 assert(channel == Channel("#test")); 898 assert(prefix.get == "+"); 899 } 900 } 901 { 902 auto reply = parseNumeric!(Numeric.RPL_WHOISCHANNELS)(only("someone", "whoisuser"), defaultModePrefixes, "#"); 903 assert(reply.isNull); 904 } 905 { 906 auto reply = parseNumeric!(Numeric.RPL_WHOISCHANNELS)(only("someone"), defaultModePrefixes, "#"); 907 assert(reply.isNull); 908 } 909 { 910 auto reply = parseNumeric!(Numeric.RPL_WHOISCHANNELS)(takeNone(only("")), defaultModePrefixes, "#"); 911 assert(reply.isNull); 912 } 913 } 914 915 auto parseNumeric(Numeric numeric : Numeric.RPL_ISON, T)(T input) { 916 import std.algorithm.iteration : splitter; 917 import std.typecons : Nullable, Tuple; 918 import virc.common : User; 919 Nullable!(Tuple!(User, "user", typeof("".splitter(" ")), "online")) output = Tuple!(User, "user", typeof("".splitter(" ")), "online")(); 920 if (input.empty) { 921 return output.init; 922 } 923 output.get.user = User(input.front); 924 input.popFront(); 925 if (input.empty) { 926 return output.init; 927 } 928 output.get.online = input.front.splitter(" "); 929 return output; 930 } 931 unittest { 932 import std.array : array; 933 import std.range : only, takeNone; 934 import virc.common : User; 935 assert(parseNumeric!(Numeric.RPL_ISON)(takeNone(only(""))).isNull); 936 assert(parseNumeric!(Numeric.RPL_ISON)(only("someone")).isNull); 937 { 938 auto reply = parseNumeric!(Numeric.RPL_ISON)(only("someone", "user1 user2 user3")); 939 assert(reply.get.user == User("someone")); 940 assert(reply.get.online.array == ["user1", "user2", "user3"]); 941 } 942 }