1 /++ 2 + Module containing IRC client guts. Parses and dispatches to appropriate 3 + handlers/ 4 +/ 5 module virc.client.skeleton; 6 import std.algorithm.comparison : among; 7 import std.algorithm.iteration : chunkBy, cumulativeFold, filter, map, splitter; 8 import std.algorithm.searching : canFind, endsWith, find, findSplit, findSplitAfter, findSplitBefore, skipOver, startsWith; 9 import std.array : array; 10 import std.ascii : isDigit; 11 import std.conv : parse, text; 12 import std.datetime; 13 import std.exception : enforce; 14 import std.format : format, formattedWrite; 15 import std.meta : AliasSeq; 16 import std.range.primitives : ElementType, isInputRange, isOutputRange; 17 import std.range : chain, empty, front, put, walkLength; 18 import std.traits : isCopyable, Parameters, Unqual; 19 import std.typecons : Nullable, RefCounted, refCounted; 20 import std.utf : byCodeUnit; 21 22 import virc.common; 23 import virc.encoding; 24 import virc.client.internaladdresslist; 25 import virc.ircsplitter; 26 import virc.ircv3.batch; 27 import virc.ircv3.sasl; 28 import virc.ircv3.tags; 29 import virc.ircmessage; 30 import virc.message; 31 import virc.modes; 32 import virc.numerics; 33 import virc.target; 34 import virc.usermask; 35 36 /++ 37 + 38 +/ 39 struct NickInfo { 40 /// 41 string nickname; 42 /// 43 string username; 44 /// 45 string realname; 46 } 47 48 /++ 49 + 50 +/ 51 enum supportedCaps = AliasSeq!( 52 "account-notify", // http://ircv3.net/specs/extensions/account-notify-3.1.html 53 "account-tag", // http://ircv3.net/specs/extensions/account-tag-3.2.html 54 "away-notify", // http://ircv3.net/specs/extensions/away-notify-3.1.html 55 "batch", // http://ircv3.net/specs/extensions/batch-3.2.html 56 "cap-notify", // http://ircv3.net/specs/extensions/cap-notify-3.2.html 57 "chghost", // http://ircv3.net/specs/extensions/chghost-3.2.html 58 "echo-message", // http://ircv3.net/specs/extensions/echo-message-3.2.html 59 "extended-join", // http://ircv3.net/specs/extensions/extended-join-3.1.html 60 "invite-notify", // http://ircv3.net/specs/extensions/invite-notify-3.2.html 61 "draft/metadata-2", // 62 "message-tags", //https://ircv3.net/specs/extensions/message-tags 63 "draft/metadata-notify-2", // 64 "draft/multiline", // https://ircv3.net/specs/extensions/multiline 65 //"monitor", // http://ircv3.net/specs/core/monitor-3.2.html 66 "multi-prefix", // http://ircv3.net/specs/extensions/multi-prefix-3.1.html 67 "sasl", // http://ircv3.net/specs/extensions/sasl-3.1.html and http://ircv3.net/specs/extensions/sasl-3.2.html 68 "server-time", // http://ircv3.net/specs/extensions/server-time-3.2.html 69 "userhost-in-names", // http://ircv3.net/specs/extensions/userhost-in-names-3.2.html 70 ); 71 72 /++ 73 + 74 +/ 75 auto ircClient(Output output, NickInfo info, SASLMechanism[] saslMechs = [], string password = string.init) { 76 auto client = IRCClient(output); 77 client.nickinfo.username = info.username; 78 client.nickinfo.realname = info.realname; 79 client.nickinfo.nickname = info.nickname; 80 if (password != string.init) { 81 client.password = password; 82 } 83 client.saslMechs = saslMechs; 84 client.initialize(); 85 return client; 86 } 87 88 /++ 89 + 90 +/ 91 struct Server { 92 /// 93 MyInfo myInfo; 94 /// 95 ISupport iSupport; 96 } 97 /++ 98 + 99 +/ 100 enum RFC1459Commands { 101 privmsg = "PRIVMSG", 102 notice = "NOTICE", 103 info = "INFO", 104 admin = "ADMIN", 105 trace = "TRACE", 106 connect = "CONNECT", 107 time = "TIME", 108 links = "LINKS", 109 stats = "STATS", 110 version_ = "VERSION", 111 kick = "KICK", 112 invite = "INVITE", 113 list = "LIST", 114 names = "NAMES", 115 topic = "TOPIC", 116 mode = "MODE", 117 part = "PART", 118 join = "JOIN", 119 squit = "SQUIT", 120 quit = "QUIT", 121 oper = "OPER", 122 server = "SERVER", 123 user = "USER", 124 nick = "NICK", 125 pass = "PASS", 126 who = "WHO", 127 whois = "WHOIS", 128 whowas = "WHOWAS", 129 kill = "KILL", 130 ping = "PING", 131 pong = "PONG", 132 error = "ERROR", 133 away = "AWAY", 134 rehash = "REHASH", 135 restart = "RESTART", 136 summon = "SUMMON", 137 users = "USERS", 138 wallops = "WALLOPS", 139 userhost = "USERHOST", 140 ison = "ISON", 141 } 142 /++ 143 + 144 +/ 145 enum RFC2812Commands { 146 service = "SERVICE" 147 } 148 149 import virc.ircv3 : IRCV3Commands; 150 alias ClientNoOpCommands = AliasSeq!( 151 RFC1459Commands.server, 152 RFC1459Commands.user, 153 RFC1459Commands.pass, 154 RFC1459Commands.whois, 155 RFC1459Commands.whowas, 156 RFC1459Commands.kill, 157 RFC1459Commands.who, 158 RFC1459Commands.oper, 159 RFC1459Commands.squit, 160 RFC1459Commands.summon, 161 RFC1459Commands.pong, //UNIMPLEMENTED 162 RFC1459Commands.error, //UNIMPLEMENTED 163 RFC1459Commands.userhost, 164 RFC1459Commands.version_, 165 RFC1459Commands.names, 166 RFC1459Commands.away, 167 RFC1459Commands.connect, 168 RFC1459Commands.trace, 169 RFC1459Commands.links, 170 RFC1459Commands.stats, 171 RFC1459Commands.ison, 172 RFC1459Commands.restart, 173 RFC1459Commands.users, 174 RFC1459Commands.list, 175 RFC1459Commands.admin, 176 RFC1459Commands.rehash, 177 RFC1459Commands.time, 178 RFC1459Commands.info, 179 RFC2812Commands.service, 180 IRCV3Commands.starttls, //DO NOT IMPLEMENT 181 IRCV3Commands.batch, //SPECIAL CASE 182 IRCV3Commands.monitor, 183 Numeric.RPL_HOSTHIDDEN, 184 Numeric.RPL_ENDOFNAMES, 185 Numeric.RPL_ENDOFMONLIST, 186 Numeric.RPL_ENDOFWHO, 187 Numeric.RPL_LOCALUSERS, 188 Numeric.RPL_GLOBALUSERS, 189 Numeric.RPL_YOURHOST, 190 Numeric.RPL_YOURID, 191 Numeric.RPL_CREATED, 192 Numeric.RPL_LISTSTART, 193 Numeric.RPL_LISTEND, 194 Numeric.RPL_TEXT, 195 Numeric.RPL_ADMINME, 196 Numeric.RPL_ADMINLOC1, 197 Numeric.RPL_ADMINLOC2, 198 Numeric.RPL_ADMINEMAIL, 199 Numeric.RPL_WHOISCERTFP, 200 Numeric.RPL_WHOISHOST, 201 Numeric.RPL_WHOISMODE 202 ); 203 204 /++ 205 + 206 +/ 207 struct ChannelState { 208 Channel channel; 209 string topic; 210 InternalAddressList users; 211 Mode[] modes; 212 void toString(T)(T sink) const if (isOutputRange!(T, const(char))) { 213 formattedWrite!"Channel: %s\n"(sink, channel); 214 formattedWrite!"\tTopic: %s\n"(sink, topic); 215 formattedWrite!"\tUsers:\n"(sink); 216 foreach (user; users.list) { 217 formattedWrite!"\t\t%s\n"(sink, user); 218 } 219 } 220 } 221 unittest { 222 import std.outbuffer; 223 ChannelState(Channel("#test"), "Words").toString(new OutBuffer); 224 } 225 /++ 226 + Types of errors. 227 +/ 228 enum ErrorType { 229 ///Insufficient privileges for command. See message for missing privilege. 230 noPrivs, 231 ///Monitor list is full. 232 monListFull, 233 ///Server has no MOTD. 234 noMOTD, 235 ///No server matches client-provided server mask. 236 noSuchServer, 237 ///User is not an IRC operator. 238 noPrivileges, 239 ///Malformed message received from server. 240 malformed, 241 ///Message received unexpectedly. 242 unexpected, 243 ///Unhandled command or numeric. 244 unrecognized, 245 ///Bad input from client. 246 badUserInput, 247 ///No key matched 248 keyNotSet, 249 ///Action could not be performed now, try again later 250 waitAndRetry, 251 ///Too many metadata subscriptions 252 tooManySubs, 253 ///Standard replies: FAIL 254 standardFail 255 } 256 /++ 257 + Struct holding data about non-fatal errors. 258 +/ 259 struct IRCError { 260 ErrorType type; 261 string message; 262 } 263 /++ 264 + Channels in a WHOIS response. 265 +/ 266 struct WhoisChannel { 267 Channel name; 268 string prefix; 269 } 270 271 /++ 272 + Full response to a WHOIS. 273 +/ 274 struct WhoisResponse { 275 bool isOper; 276 bool isSecure; 277 bool isRegistered; 278 Nullable!string username; 279 Nullable!string hostname; 280 Nullable!string realname; 281 Nullable!SysTime connectedTime; 282 Nullable!Duration idleTime; 283 Nullable!string connectedTo; 284 Nullable!string account; 285 WhoisChannel[string] channels; 286 } 287 288 /++ 289 + Metadata update. 290 +/ 291 struct MetadataValue { 292 ///Visibility of this value. Exact meaning is defined by the server implementation. 293 string visibility; 294 ///Main payload 295 string value; 296 297 alias value this; 298 } 299 300 interface Output { 301 void put(char) @safe; 302 } 303 304 enum ChannelListUpdateType { 305 added, 306 removed, 307 updated 308 } 309 310 /++ 311 + IRC client implementation. 312 +/ 313 struct IRCClient { 314 import virc.ircv3 : Capability, CapabilityServerSubcommands, IRCV3Commands; 315 Output output; 316 /// 317 Server server; 318 /// 319 Capability[] capsEnabled; 320 ///User metadata received so far 321 MetadataValue[string][User] userMetadata; 322 ///Channel metadata received so far 323 MetadataValue[string][Channel] channelMetadata; 324 325 NickInfo nickinfo; 326 Nullable!string password; 327 /// 328 ChannelState[string] channels; 329 330 ///SASL mechanisms available for usage 331 SASLMechanism[] saslMechs; 332 /// 333 InternalAddressList internalAddressList; 334 335 /// 336 void delegate(const Capability, const MessageMetadata) @safe onReceiveCapList; 337 /// 338 void delegate(const Capability, const MessageMetadata) @safe onReceiveCapLS; 339 /// 340 void delegate(const Capability, const MessageMetadata) @safe onReceiveCapAck; 341 /// 342 void delegate(const Capability, const MessageMetadata) @safe onReceiveCapNak; 343 /// 344 void delegate(const Capability, const MessageMetadata) @safe onReceiveCapDel; 345 /// 346 void delegate(const Capability, const MessageMetadata) @safe onReceiveCapNew; 347 /// 348 void delegate(const User, const SysTime, const MessageMetadata) @safe onUserOnline; 349 /// 350 void delegate(const User, const MessageMetadata) @safe onUserOffline; 351 /// 352 void delegate(const User, const MessageMetadata) @safe onLogin; 353 /// 354 void delegate(const User, const MessageMetadata) @safe onLogout; 355 /// 356 void delegate(const User, const string, const MessageMetadata) @safe onOtherUserAwayReply; 357 /// 358 void delegate(const User, const MessageMetadata) @safe onBack; 359 /// 360 void delegate(const User, const MessageMetadata) @safe onMonitorList; 361 /// 362 void delegate(const User, const User, const MessageMetadata) @safe onNick; 363 /// 364 void delegate(const User, const User, const Channel, const MessageMetadata) @safe onInvite; 365 /// 366 void delegate(const User, const Channel, const MessageMetadata) @safe onJoin; 367 /// 368 void delegate(const User, const Channel, const string, const MessageMetadata) @safe onPart; 369 /// 370 void delegate(const User, const Channel, const User, const string, const MessageMetadata) @safe onKick; 371 /// 372 void delegate(const User, const string, const MessageMetadata) @safe onQuit; 373 /// 374 void delegate(const User, const Target, const ModeChange, const MessageMetadata) @safe onMode; 375 /// 376 void delegate(const User, const Target, const Message, const MessageMetadata) @safe onMessage; 377 /// 378 void delegate(const User, const WhoisResponse) @safe onWhois; 379 /// 380 void delegate(const User, const string, const MessageMetadata) @safe onWallops; 381 /// 382 void delegate(const ChannelListResult, const MessageMetadata) @safe onList; 383 /// 384 void delegate(const User, const User, const MessageMetadata) @safe onChgHost; 385 /// 386 void delegate(const LUserClient, const MessageMetadata) @safe onLUserClient; 387 /// 388 void delegate(const LUserOp, const MessageMetadata) @safe onLUserOp; 389 /// 390 void delegate(const LUserChannels, const MessageMetadata) @safe onLUserChannels; 391 /// 392 void delegate(const LUserMe, const MessageMetadata) @safe onLUserMe; 393 /// 394 void delegate(const NamesReply, const MessageMetadata) @safe onNamesReply; 395 /// 396 void delegate(const WHOXReply, const MessageMetadata) @safe onWHOXReply; 397 /// 398 void delegate(const TopicReply, const MessageMetadata) @safe onTopicReply; 399 /// 400 void delegate(const User, const Channel, const string, const MessageMetadata) @safe onTopicChange; 401 /// 402 void delegate(const User, const MessageMetadata) @safe onUnAwayReply; 403 /// 404 void delegate(const User, const MessageMetadata) @safe onAwayReply; 405 /// 406 void delegate(const TopicWhoTime, const MessageMetadata) @safe onTopicWhoTimeReply; 407 /// 408 void delegate(const VersionReply, const MessageMetadata) @safe onVersionReply; 409 /// 410 void delegate(const RehashingReply, const MessageMetadata) @safe onServerRehashing; 411 /// 412 void delegate(const MessageMetadata) @safe onYoureOper; 413 ///Called when an RPL_ISON message is received 414 void delegate(const User, const MessageMetadata) @safe onIsOn; 415 ///Called when a metadata subscription list is received 416 void delegate(const string, const MessageMetadata) @safe onMetadataSubList; 417 /// 418 void delegate(const IRCError, const MessageMetadata) @safe onError; 419 /// 420 void delegate(const MessageMetadata) @safe onRaw; 421 /// 422 void delegate() @safe onConnect; 423 /// Called whenever a channel user list is updated 424 void delegate(const User, const User, const Channel, ChannelListUpdateType) @safe onChannelListUpdate; 425 /// 426 debug void delegate(const string) @safe onSend; 427 428 static struct ClientState { 429 bool invalid = true; 430 bool isRegistered; 431 ulong capReqCount = 0; 432 BatchProcessor batchProcessor; 433 bool isAuthenticating; 434 bool authenticationSucceeded; 435 string[] supportedSASLMechs; 436 SASLMechanism selectedSASLMech; 437 bool autoSelectSASLMech; 438 string receivedSASLAuthenticationText; 439 bool _isAway; 440 ulong maxMetadataSubscriptions; 441 ulong maxMetadataSelfKeys; 442 const(string)[] metadataSubscribedKeys; 443 Capability[string] availableCapabilities; 444 WhoisResponse[string] whoisCache; 445 size_t multilineMaxBytes = size_t.max; 446 size_t multilineMaxLines = size_t.max; 447 size_t batchCounter = 0; 448 } 449 private ClientState state; 450 451 bool isAuthenticated() @safe { 452 return state.authenticationSucceeded; 453 } 454 455 void initialize(NickInfo info) @safe { 456 nickinfo = info; 457 initialize(); 458 } 459 void initialize() @safe { 460 state = state.init; 461 state.invalid = false; 462 write("CAP LS 302"); 463 register(); 464 } 465 public void ping() @safe { 466 467 } 468 public void names() @safe { 469 write("NAMES"); 470 } 471 public void ping(const string nonce) @safe { 472 write!"PING :%s"(nonce); 473 } 474 public void lUsers() @safe { 475 write!"LUSERS"; 476 } 477 private void pong(const string nonce) @safe { 478 write!"PONG :%s"(nonce); 479 } 480 public void put(string line) @safe { 481 import std.conv : asOriginalType; 482 import std.meta : NoDuplicates; 483 import std.string : representation; 484 import std.traits : EnumMembers; 485 debug(verboseirc) import std.experimental.logger : trace; 486 //Chops off terminating \r\n. Everything after is ignored, according to spec. 487 line = findSplitBefore(line, "\r\n")[0]; 488 debug(verboseirc) trace("←: ", line); 489 assert(isValid, "Received data after invalidation"); 490 if (line.empty) { 491 return; 492 } 493 state.batchProcessor.put(line); 494 foreach (batch; state.batchProcessor) { 495 state.batchProcessor.popFront(); 496 foreach (parsed; batch.lines) { 497 auto metadata = MessageMetadata(); 498 metadata.batch = parsed.batch; 499 metadata.tags = parsed.tags; 500 if("time" in parsed.tags) { 501 metadata.time = parseTime(parsed.tags); 502 } else { 503 metadata.time = Clock.currTime(UTC()); 504 } 505 if ("account" in parsed.tags) { 506 if (!parsed.sourceUser.isNull) { 507 parsed.sourceUser.get.account = parsed.tags["account"]; 508 } 509 } 510 if (!parsed.sourceUser.isNull) { 511 internalAddressList.update(parsed.sourceUser.get); 512 if (parsed.sourceUser.get.nickname in internalAddressList) { 513 parsed.sourceUser = internalAddressList[parsed.sourceUser.get.nickname]; 514 } 515 } 516 517 if (parsed.verb.filter!(x => !isDigit(x)).empty) { 518 metadata.messageNumeric = cast(Numeric)parsed.verb; 519 } 520 metadata.original = parsed.raw; 521 tryCall!"onRaw"(metadata); 522 523 switchy: switch (parsed.verb) { 524 //TOO MANY TEMPLATE INSTANTIATIONS! uncomment when compiler fixes this! 525 //alias Numerics = NoDuplicates!(EnumMembers!Numeric); 526 alias Numerics = AliasSeq!(Numeric.RPL_WELCOME, Numeric.RPL_ISUPPORT, Numeric.RPL_LIST, Numeric.RPL_YOURHOST, Numeric.RPL_CREATED, Numeric.RPL_LISTSTART, Numeric.RPL_LISTEND, Numeric.RPL_ENDOFMONLIST, Numeric.RPL_ENDOFNAMES, Numeric.RPL_YOURID, Numeric.RPL_LOCALUSERS, Numeric.RPL_GLOBALUSERS, Numeric.RPL_HOSTHIDDEN, Numeric.RPL_TEXT, Numeric.RPL_MYINFO, Numeric.RPL_LOGON, Numeric.RPL_MONONLINE, Numeric.RPL_MONOFFLINE, Numeric.RPL_MONLIST, Numeric.RPL_LUSERCLIENT, Numeric.RPL_LUSEROP, Numeric.RPL_LUSERCHANNELS, Numeric.RPL_LUSERME, Numeric.RPL_TOPIC, Numeric.RPL_NAMREPLY, Numeric.RPL_TOPICWHOTIME, Numeric.RPL_SASLSUCCESS, Numeric.RPL_LOGGEDIN, Numeric.RPL_VERSION, Numeric.ERR_MONLISTFULL, Numeric.ERR_NOMOTD, Numeric.ERR_NICKLOCKED, Numeric.ERR_SASLFAIL, Numeric.ERR_SASLTOOLONG, Numeric.ERR_SASLABORTED, Numeric.RPL_REHASHING, Numeric.ERR_NOPRIVS, Numeric.RPL_YOUREOPER, Numeric.ERR_NOSUCHSERVER, Numeric.ERR_NOPRIVILEGES, Numeric.RPL_AWAY, Numeric.RPL_UNAWAY, Numeric.RPL_NOWAWAY, Numeric.RPL_ENDOFWHOIS, Numeric.RPL_WHOISUSER, Numeric.RPL_WHOISSECURE, Numeric.RPL_WHOISOPERATOR, Numeric.RPL_WHOISREGNICK, Numeric.RPL_WHOISIDLE, Numeric.RPL_WHOISSERVER, Numeric.RPL_WHOISACCOUNT, Numeric.RPL_ADMINEMAIL, Numeric.RPL_ADMINLOC1, Numeric.RPL_ADMINLOC2, Numeric.RPL_ADMINME, Numeric.RPL_WHOISHOST, Numeric.RPL_WHOISMODE, Numeric.RPL_WHOISCERTFP, Numeric.RPL_WHOISCHANNELS, Numeric.RPL_ISON, Numeric.RPL_WHOISKEYVALUE, Numeric.RPL_KEYVALUE, Numeric.RPL_KEYNOTSET, Numeric.ERR_METADATASYNCLATER, Numeric.RPL_METADATASUBOK, Numeric.RPL_METADATAUNSUBOK, Numeric.RPL_METADATASUBS, Numeric.RPL_WHOSPCRPL, Numeric.RPL_ENDOFWHO); 527 528 static foreach (cmd; AliasSeq!(NoDuplicates!(EnumMembers!IRCV3Commands), NoDuplicates!(EnumMembers!RFC1459Commands), NoDuplicates!(EnumMembers!RFC2812Commands), Numerics)) { 529 case cmd: 530 static if (!cmd.asOriginalType.among(ClientNoOpCommands)) { 531 rec!cmd(parsed, metadata); 532 } 533 break switchy; 534 } 535 default: recUnknownCommand(parsed.verb, metadata); break; 536 } 537 } 538 } 539 } 540 void put(const immutable(ubyte)[] rawString) @safe { 541 put(rawString.toUTF8String); 542 } 543 private void tryEndRegistration() @safe { 544 if (state.capReqCount == 0 && !state.isAuthenticating && !state.isRegistered) { 545 endRegistration(); 546 } 547 } 548 private void endAuthentication() @safe { 549 state.isAuthenticating = false; 550 tryEndRegistration(); 551 } 552 private void endRegistration() @safe { 553 write("CAP END"); 554 } 555 public void capList() @safe { 556 write("CAP LIST"); 557 } 558 public void list() @safe { 559 write("LIST"); 560 } 561 public void away(const string message) @safe { 562 write!"AWAY :%s"(message); 563 } 564 public void away() @safe { 565 write("AWAY"); 566 } 567 public void whois(const string nick) @safe { 568 write!"WHOIS %s"(nick); 569 } 570 public void monitorClear() @safe { 571 assert(monitorIsEnabled); 572 write("MONITOR C"); 573 } 574 public void monitorList() @safe { 575 assert(monitorIsEnabled); 576 write("MONITOR L"); 577 } 578 public void monitorStatus() @safe { 579 assert(monitorIsEnabled); 580 write("MONITOR S"); 581 } 582 public void monitorAdd(T)(T users) if (isInputRange!T && is(ElementType!T == User)) { 583 assert(monitorIsEnabled); 584 writeList!("MONITOR + ", ",")(users.map!(x => x.nickname)); 585 } 586 public void monitorRemove(T)(T users) if (isInputRange!T && is(ElementType!T == User)) { 587 assert(monitorIsEnabled); 588 writeList!("MONITOR - ", ",")(users.map!(x => x.nickname)); 589 } 590 public bool isAway() const @safe { 591 return state._isAway; 592 } 593 public bool monitorIsEnabled() @safe { 594 return capsEnabled.canFind("MONITOR"); 595 } 596 public void quit(const string msg) @safe { 597 write!"QUIT :%s"(msg); 598 state = state.init; 599 state.invalid = false; 600 } 601 public void changeNickname(const string nick) @safe { 602 write!"NICK %s"(nick); 603 } 604 public void join(T,U)(T chans, U keys) if (isInputRange!T && isInputRange!U) { 605 auto filteredKeys = keys.filter!(x => !x.empty); 606 if (!filteredKeys.empty) { 607 write!"JOIN %-(%s,%) %-(%s,%)"(chans, filteredKeys); 608 } else { 609 write!"JOIN %-(%s,%)"(chans); 610 } 611 } 612 public void join(const string chan, const string key = "") @safe { 613 import std.range : only; 614 join(only(chan), only(key)); 615 } 616 public void join(const Channel chan, const string key = "") @safe { 617 import std.range : only; 618 join(only(chan.text), only(key)); 619 } 620 public void msg(const string target, const string message, IRCTags tags = IRCTags.init) @safe { 621 import std.algorithm.comparison : min; 622 import std.format : sformat; 623 import virc.style : checkFormatting, ControlCharacters; 624 enum breathingRoom = 5; 625 const maxLength = 512 - 14 - breathingRoom - me.nickname.length - me.ident.get("").length - me.host.get("").length - target.length; 626 if (message.length < maxLength && !message.canFind("\n")) { 627 writeTags!"PRIVMSG %s :%s"(tags, target, message); 628 } else { 629 const batch = isEnabled(Capability("draft/multiline")); 630 auto batchRemainingLines = state.multilineMaxLines; 631 auto batchRemainingBytes = state.multilineMaxBytes; 632 auto id = batch ? startBatch!" %s"("draft/multiline", tags, target) : 0; 633 char[] formatting; 634 char[20] formattingBuf; 635 bool startNewBatch; 636 foreach (line; message.splitter("\n")) { 637 IRCTags batchTag; 638 do { 639 if (startNewBatch) { 640 endBatch(id); 641 id = startBatch!" %s"("draft/multiline", tags, target); 642 batchTag = batchTag.init; 643 startNewBatch = false; 644 } 645 if (batch) { 646 batchTag.batch(id.text); 647 } else { 648 batchTag = tags; 649 } 650 const printLine = line[0 .. min(formatting.length + line.length, maxLength, batchRemainingBytes) - formatting.length]; 651 const activeFormatting = checkFormatting(printLine); 652 writeTags!"PRIVMSG %s :%s%s"(batchTag, target, formatting, printLine); 653 line = line[min(line.length, maxLength, batchRemainingBytes) .. $]; 654 if (batch) { 655 batchTag.multilineConcat(); 656 } 657 if (--batchRemainingLines == 0) { 658 startNewBatch = true; 659 batchRemainingBytes = state.multilineMaxBytes; 660 batchRemainingLines = state.multilineMaxLines; 661 } 662 batchRemainingBytes -= formatting.length + printLine.length; 663 if (batchRemainingBytes == 0) { 664 startNewBatch = true; 665 batchRemainingBytes = state.multilineMaxBytes; 666 batchRemainingLines = state.multilineMaxLines; 667 } 668 formatting = sformat!"%s%s%s%s%s%s%s"(formattingBuf, 669 activeFormatting.bold ? [cast(char)ControlCharacters.bold] : "", 670 activeFormatting.underline ? [cast(char)ControlCharacters.underline] : "", 671 activeFormatting.strikethrough ? [cast(char)ControlCharacters.strikethrough] : "", 672 activeFormatting.italic ? [cast(char)ControlCharacters.italic] : "", 673 activeFormatting.reverse ? [cast(char)ControlCharacters.reverse] : "", 674 activeFormatting.monospace ? [cast(char)ControlCharacters.monospace] : "", 675 activeFormatting.colour, 676 ); 677 } while (line.length > 0); 678 } 679 if (batch) { 680 endBatch(id); 681 } 682 } 683 } 684 size_t startBatch(string fmt, T...)(string type, IRCTags tags, T args) { 685 writeTags!("BATCH +%s %s"~fmt)(tags, state.batchCounter, type, args); 686 return state.batchCounter++; 687 } 688 void endBatch(size_t id) @safe { 689 write!"BATCH -%s"(id); 690 } 691 public void tagMsg(const string target, IRCTags tags = IRCTags.init) @safe { 692 writeTags!"TAGMSG %s"(tags, target); 693 } 694 public void wallops(const string message) @safe { 695 write!"WALLOPS :%s"(message); 696 } 697 public void msg(const Target target, const Message message, IRCTags tags = IRCTags.init) @safe { 698 msg(target.targetText, message.text, tags); 699 } 700 public void tagMsg(const Target target, IRCTags tags = IRCTags.init) @safe { 701 tagMsg(target.targetText, tags); 702 } 703 public void ctcp(const Target target, const string command, const string args) @safe { 704 msg(target, Message("\x01"~command~" "~args~"\x01")); 705 } 706 public void ctcp(const Target target, const string command) @safe { 707 msg(target, Message("\x01"~command~"\x01")); 708 } 709 public void ctcpReply(const Target target, const string command, const string args) @safe { 710 notice(target, Message("\x01"~command~" "~args~"\x01")); 711 } 712 public void notice(const string target, const string message) @safe { 713 write!"NOTICE %s :%s"(target, message); 714 } 715 public void notice(const Target target, const Message message) @safe { 716 notice(target.targetText, message.text); 717 } 718 public void changeTopic(const Target target, const string topic) @safe { 719 write!"TOPIC %s :%s"(target, topic); 720 } 721 public void oper(const string name, const string pass) @safe { 722 assert(!name.canFind(" ") && !pass.canFind(" ")); 723 write!"OPER %s %s"(name, pass); 724 } 725 public void rehash() @safe { 726 write!"REHASH"; 727 } 728 public void restart() @safe { 729 write!"RESTART"; 730 } 731 public void squit(const string server, const string reason) @safe { 732 assert(!server.canFind(" ")); 733 write!"SQUIT %s :%s"(server, reason); 734 } 735 public void version_() @safe { 736 write!"VERSION"(); 737 } 738 public void version_(const string serverMask) @safe { 739 write!"VERSION %s"(serverMask); 740 } 741 public void kick(const Channel chan, const User nick, const string message = "") @safe { 742 assert(message.length < server.iSupport.kickLength, "Kick message length exceeded"); 743 write!"KICK %s %s :%s"(chan, nick, message); 744 } 745 public void isOn(const string[] nicknames...) @safe { 746 write!"ISON %-(%s %)"(nicknames); 747 } 748 public void isOn(const User[] users...) @safe { 749 write!"ISON %-(%s %)"(users.map!(x => x.nickname)); 750 } 751 public void admin(const string server = "") @safe { 752 if (server == "") { 753 write!"ADMIN"(); 754 } else { 755 write!"ADMIN %s"(server); 756 } 757 } 758 public auto ownMetadata() const @safe { 759 if (me !in userMetadata) { 760 return null; 761 } 762 return userMetadata[me]; 763 } 764 public void setMetadata(const User user, const string key, const string data) @safe { 765 write!"METADATA %s SET %s :%s"(user, key, data); 766 } 767 public void setMetadata(const Channel channel, const string key, const string data) @safe { 768 write!"METADATA %s SET %s :%s"(channel, key, data); 769 } 770 public void setMetadata(const string key, const string data) @safe { 771 write!"METADATA * SET %s :%s"(key, data); 772 } 773 public void getMetadata(const Channel channel, const string[] keys...) @safe { 774 write!"METADATA %s GET %-(%s %)"(channel, keys); 775 } 776 public void getMetadata(const User user, const string[] keys...) @safe { 777 write!"METADATA %s GET %-(%s %)"(user, keys); 778 } 779 public void listMetadata(const Channel channel) @safe { 780 write!"METADATA %s LIST"(channel); 781 } 782 public void listMetadata(const User user) @safe { 783 write!"METADATA %s LIST"(user); 784 } 785 public void subscribeMetadata(const string[] keys...) @safe { 786 write!"METADATA * SUB %-(%s %)"(keys); 787 } 788 public void unsubscribeMetadata(const string[] keys...) @safe { 789 write!"METADATA * UNSUB %-(%s %)"(keys); 790 } 791 public void listSubscribedMetadata() @safe { 792 write!"METADATA * SUBS"(); 793 } 794 public void syncMetadata(const User user) @safe { 795 write!"METADATA %s SYNC"(user); 796 } 797 public void syncMetadata(const Channel channel) @safe { 798 write!"METADATA %s SYNC"(channel); 799 } 800 public void syncMetadata() @safe { 801 write!"METADATA * SYNC"(); 802 } 803 public void clearMetadata(const User user) @safe { 804 write!"METADATA %s CLEAR"(user); 805 } 806 public void clearMetadata(const Channel channel) @safe { 807 write!"METADATA %s CLEAR"(channel); 808 } 809 public void clearMetadata() @safe { 810 write!"METADATA * CLEAR"(); 811 } 812 public bool isSubscribed(const string key) @safe { 813 return state.metadataSubscribedKeys.canFind(key); 814 } 815 private void sendAuthenticatePayload(const string payload) @safe { 816 import std.base64 : Base64; 817 import std.range : chunks; 818 import std.string : representation; 819 if (payload == "") { 820 write!"AUTHENTICATE +"(); 821 } else { 822 auto str = Base64.encode(payload.representation); 823 size_t lastChunkSize = 0; 824 foreach (chunk; str.byCodeUnit.chunks(400)) { 825 write!"AUTHENTICATE %s"(chunk); 826 lastChunkSize = chunk.length; 827 } 828 if (lastChunkSize == 400) { 829 write!"AUTHENTICATE +"(); 830 } 831 } 832 } 833 private void user(const string username_, const string realname_) @safe { 834 write!"USER %s 0 * :%s"(username_, realname_); 835 } 836 private void pass(const string pass) @safe { 837 write!"PASS :%s"(pass); 838 } 839 private void register() @safe { 840 assert(!state.isRegistered); 841 if (!password.isNull) { 842 pass(password.get); 843 } 844 changeNickname(nickinfo.nickname); 845 user(nickinfo.username, nickinfo.realname); 846 } 847 private void write(string fmt, T...)(scope T args) { 848 writeTags!(fmt, T)(IRCTags.init, args); 849 } 850 private void writeTags(string fmt, T...)(IRCTags tags, scope T args) { 851 import std.range : put; 852 debug(verboseirc) import std.experimental.logger : tracef; 853 const sendTags = !tags.empty && isEnabled(Capability("message-tags")); 854 debug(verboseirc) tracef("→: %s"~fmt, sendTags ? format!"@%s "(tags) : "", args); 855 if (sendTags) { 856 formattedWrite!"@%s "(output, tags); 857 } 858 formattedWrite!fmt(output, args); 859 put(output, "\r\n"); 860 debug { 861 tryCall!"onSend"(format!fmt(args)); 862 } 863 static if (is(typeof(output.flush()))) { 864 output.flush(); 865 } 866 } 867 private void write(const scope string text) @safe { 868 write!"%s"(text); 869 } 870 private void writeList(string prefix, string separator, T)(T range) if (isInputRange!T && is(Unqual!(ElementType!T) == string)) { 871 write!(prefix~"%-(%s"~separator~"%)")(range); 872 } 873 private bool isEnabled(const Capability cap) @safe { 874 return capsEnabled.canFind(cap); 875 } 876 private void tryCall(string func, T...)(const T params) { 877 if (__traits(getMember, this, func) !is null) { 878 __traits(getMember, this, func)(params); 879 } 880 } 881 auto me() const @safe { 882 assert(nickinfo.nickname in internalAddressList); 883 return internalAddressList[nickinfo.nickname]; 884 } 885 //Message parsing functions follow 886 private void rec(string cmd : IRCV3Commands.cap)(IRCMessage message, const MessageMetadata metadata) { 887 auto tokens = message.args; 888 immutable username = tokens.front; //Unused? 889 tokens.popFront(); 890 immutable subCommand = tokens.front; 891 tokens.popFront(); 892 immutable terminator = !tokens.skipOver("*"); 893 auto args = tokens 894 .front 895 .splitter(" ") 896 .filter!(x => x != "") 897 .map!(x => Capability(x)); 898 final switch (cast(CapabilityServerSubcommands) subCommand) { 899 case CapabilityServerSubcommands.ls: 900 recCapLS(args, metadata); 901 break; 902 case CapabilityServerSubcommands.list: 903 recCapList(args, metadata); 904 break; 905 case CapabilityServerSubcommands.acknowledge: 906 recCapAck(args, metadata); 907 break; 908 case CapabilityServerSubcommands.notAcknowledge: 909 recCapNak(args, metadata); 910 break; 911 case CapabilityServerSubcommands.new_: 912 recCapNew(args, metadata); 913 break; 914 case CapabilityServerSubcommands.delete_: 915 recCapDel(args, metadata); 916 break; 917 } 918 } 919 private void recCapLS(T)(T caps, const MessageMetadata metadata) if (is(ElementType!T == Capability)) { 920 auto requestCaps = caps.filter!(among!supportedCaps).map!(x => Capability(x.name)); 921 state.capReqCount += requestCaps.save().walkLength; 922 if (!requestCaps.empty) { 923 write!"CAP REQ :%-(%s %)"(requestCaps); 924 } 925 foreach (ref cap; caps) { 926 state.availableCapabilities[cap.name] = cap; 927 tryCall!"onReceiveCapLS"(cap, metadata); 928 } 929 } 930 private void recCapList(T)(T caps, const MessageMetadata metadata) if (is(ElementType!T == Capability)) { 931 foreach (ref cap; caps) { 932 state.availableCapabilities[cap.name] = cap; 933 tryCall!"onReceiveCapList"(cap, metadata); 934 } 935 } 936 private void recCapAck(T)(T caps, const MessageMetadata metadata) if (is(ElementType!T == Capability)) { 937 import std.range : hasLength; 938 capsEnabled ~= caps.save().array; 939 foreach (ref cap; caps) { 940 enableCapability(cap); 941 tryCall!"onReceiveCapAck"(state.availableCapabilities[cap.name], metadata); 942 static if (!hasLength!T) { 943 capAcknowledgementCommon(1); 944 } 945 } 946 static if (hasLength!T) { 947 capAcknowledgementCommon(caps.length); 948 } 949 } 950 private void recCapNak(T)(T caps, const MessageMetadata metadata) if (is(ElementType!T == Capability)) { 951 import std.range : hasLength; 952 foreach (ref cap; caps) { 953 tryCall!"onReceiveCapNak"(state.availableCapabilities[cap.name], metadata); 954 static if (!hasLength!T) { 955 capAcknowledgementCommon(1); 956 } 957 } 958 static if (hasLength!T) { 959 capAcknowledgementCommon(caps.length); 960 } 961 } 962 private void capAcknowledgementCommon(const size_t count) @safe { 963 state.capReqCount -= count; 964 tryEndRegistration(); 965 } 966 private void recCapNew(T)(T caps, const MessageMetadata metadata) if (is(ElementType!T == Capability)) { 967 auto requestCaps = caps.filter!(among!supportedCaps).map!(x => Capability(x.name)); 968 state.capReqCount += requestCaps.save().walkLength; 969 if (!requestCaps.empty) { 970 write!"CAP REQ :%-(%s %)"(requestCaps); 971 } 972 foreach (ref cap; caps) { 973 state.availableCapabilities[cap.name] = cap; 974 tryCall!"onReceiveCapNew"(cap, metadata); 975 } 976 } 977 private void recCapDel(T)(T caps, const MessageMetadata metadata) if (is(ElementType!T == Capability)) { 978 import std.algorithm.mutation : remove; 979 import std.algorithm.searching : countUntil; 980 foreach (ref cap; caps) { 981 state.availableCapabilities.remove(cap.name); 982 auto findCap = countUntil(capsEnabled, cap); 983 if (findCap > -1) { 984 capsEnabled = capsEnabled.remove(findCap); 985 } 986 tryCall!"onReceiveCapDel"(cap, metadata); 987 } 988 } 989 private void enableCapability(const Capability cap) @safe { 990 import virc.keyvaluesplitter : splitKeyValues; 991 import std.conv : to; 992 const capDetails = state.availableCapabilities[cap.name]; 993 switch (cap.name) { 994 case "sasl": 995 state.supportedSASLMechs = capDetails.value.splitter(",").array; 996 startSASL(); 997 break; 998 case "draft/metadata-2": 999 state.maxMetadataSubscriptions = ulong.max; 1000 state.maxMetadataSelfKeys = ulong.max; 1001 foreach (kv; capDetails.value.splitKeyValues) { 1002 switch (kv.key) { 1003 case "maxsub": 1004 if (!kv.value.isNull) { 1005 state.maxMetadataSubscriptions = kv.value.get.to!ulong; 1006 } 1007 break; 1008 case "maxkey": 1009 if (!kv.value.isNull) { 1010 state.maxMetadataSelfKeys = kv.value.get.to!ulong; 1011 } 1012 break; 1013 default: break; 1014 } 1015 } 1016 break; 1017 case "draft/multiline": 1018 state.multilineMaxLines = size_t.max; 1019 foreach (kv; capDetails.value.splitKeyValues) { 1020 switch (kv.key) { 1021 case "max-lines": 1022 if (!kv.value.isNull) { 1023 state.multilineMaxLines = kv.value.get.to!size_t; 1024 } 1025 break; 1026 case "max-bytes": 1027 if (!kv.value.isNull) { 1028 state.multilineMaxBytes = kv.value.get.to!size_t; 1029 } 1030 break; 1031 default: break; 1032 } 1033 } 1034 break; 1035 default: break; 1036 } 1037 } 1038 private void startSASL() @safe { 1039 if (state.supportedSASLMechs.empty && !saslMechs.empty) { 1040 state.autoSelectSASLMech = true; 1041 saslAuth(saslMechs.front); 1042 } else if (!state.supportedSASLMechs.empty && !saslMechs.empty) { 1043 foreach (id, mech; saslMechs) { 1044 if (state.supportedSASLMechs.canFind(mech.name)) { 1045 saslAuth(mech); 1046 } 1047 } 1048 } 1049 } 1050 private void saslAuth(SASLMechanism mech) @safe { 1051 state.selectedSASLMech = mech; 1052 write!"AUTHENTICATE %s"(mech.name); 1053 state.isAuthenticating = true; 1054 } 1055 private void rec(string cmd : RFC1459Commands.kick)(IRCMessage message, const MessageMetadata metadata) { 1056 auto split = message.args; 1057 auto source = message.sourceUser.get; 1058 if (split.empty) { 1059 return; 1060 } 1061 Channel channel = Channel(split.front); 1062 split.popFront(); 1063 if (split.empty) { 1064 return; 1065 } 1066 User victim = User(split.front); 1067 split.popFront(); 1068 string msg; 1069 1070 if (!split.empty) { 1071 msg = split.front; 1072 } 1073 1074 tryCall!"onChannelListUpdate"(victim, victim, channel, ChannelListUpdateType.removed); 1075 tryCall!"onKick"(source, channel, victim, msg, metadata); 1076 } 1077 private void rec(string cmd : RFC1459Commands.wallops)(IRCMessage message, const MessageMetadata metadata) { 1078 tryCall!"onWallops"(message.sourceUser.get, message.args.front, metadata); 1079 } 1080 private void rec(string cmd : RFC1459Commands.mode)(IRCMessage message, const MessageMetadata metadata) { 1081 auto split = message.args; 1082 auto source = message.sourceUser.get; 1083 auto target = Target(split.front, server.iSupport.statusMessage, server.iSupport.channelTypes); 1084 split.popFront(); 1085 ModeType[char] modeTypes; 1086 if (target.isChannel) { 1087 modeTypes = server.iSupport.channelModeTypes; 1088 } else { 1089 //there are no user mode types. 1090 } 1091 auto modes = parseModeString(split, modeTypes); 1092 foreach (mode; modes) { 1093 tryCall!"onMode"(source, target, mode, metadata); 1094 } 1095 } 1096 private void rec(string cmd : RFC1459Commands.join)(IRCMessage message, const MessageMetadata metadata) { 1097 auto split = message.args; 1098 auto channel = Channel(split.front); 1099 auto source = message.sourceUser.get; 1100 split.popFront(); 1101 if (isEnabled(Capability("extended-join"))) { 1102 if (split.front != "*") { 1103 source.account = split.front; 1104 } 1105 split.popFront(); 1106 source.realName = split.front; 1107 split.popFront(); 1108 } 1109 if (server.iSupport.whoX) { 1110 write!"WHO %s %%uihsnflar"(channel); 1111 } 1112 if (channel.name !in channels) { 1113 channels[channel.name] = ChannelState(Channel(channel.name)); 1114 } 1115 internalAddressList.update(source); 1116 if (source.nickname in internalAddressList) { 1117 channels[channel.name].users.update(internalAddressList[source.nickname]); 1118 } 1119 tryCall!"onChannelListUpdate"(source, source, channel, ChannelListUpdateType.added); 1120 tryCall!"onJoin"(source, channel, metadata); 1121 } 1122 private void rec(string cmd : RFC1459Commands.part)(IRCMessage message, const MessageMetadata metadata) { 1123 import std.algorithm.mutation : remove; 1124 import std.algorithm.searching : countUntil; 1125 auto split = message.args; 1126 auto user = message.sourceUser.get; 1127 auto channel = Channel(split.front); 1128 split.popFront(); 1129 string msg; 1130 if (!split.empty) { 1131 msg = split.front; 1132 } 1133 if ((channel.name in channels) && (user.nickname in channels[channel.name].users)) { 1134 channels[channel.name].users.invalidate(user.nickname); 1135 } 1136 if ((user == me) && (channel.name in channels)) { 1137 channels.remove(channel.name); 1138 } 1139 tryCall!"onChannelListUpdate"(user, user, channel, ChannelListUpdateType.removed); 1140 tryCall!"onPart"(user, channel, msg, metadata); 1141 } 1142 private void rec(string cmd : RFC1459Commands.notice)(IRCMessage message, const MessageMetadata metadata) { 1143 auto split = message.args; 1144 auto user = message.sourceUser.get; 1145 auto target = Target(split.front, server.iSupport.statusMessage, server.iSupport.channelTypes); 1146 split.popFront(); 1147 auto msg = Message(split.front, MessageType.notice); 1148 recMessageCommon(user, target, msg, metadata); 1149 } 1150 private void rec(string cmd : RFC1459Commands.privmsg)(IRCMessage message, const MessageMetadata metadata) { 1151 auto split = message.args; 1152 auto user = message.sourceUser.get; 1153 auto target = Target(split.front, server.iSupport.statusMessage, server.iSupport.channelTypes); 1154 split.popFront(); 1155 if (split.empty) { 1156 return; 1157 } 1158 auto msg = Message(split.front, MessageType.privmsg); 1159 recMessageCommon(user, target, msg, metadata); 1160 } 1161 private void recMessageCommon(const User user, const Target target, Message msg, const MessageMetadata metadata) @safe { 1162 if (user.nickname == nickinfo.nickname) { 1163 msg.isEcho = true; 1164 } 1165 tryCall!"onMessage"(user, target, msg, metadata); 1166 } 1167 private void rec(string cmd : Numeric.RPL_ISUPPORT)(IRCMessage message, const MessageMetadata metadata) { 1168 auto split = message.args; 1169 switch (split.save().canFind("UHNAMES", "NAMESX")) { 1170 case 1: 1171 if (!isEnabled(Capability("userhost-in-names"))) { 1172 write("PROTOCTL UHNAMES"); 1173 } 1174 break; 1175 case 2: 1176 if (!isEnabled(Capability("multi-prefix"))) { 1177 write("PROTOCTL NAMESX"); 1178 } 1179 break; 1180 default: break; 1181 } 1182 parseNumeric!(Numeric.RPL_ISUPPORT)(split, server.iSupport); 1183 } 1184 private void rec(string cmd : Numeric.RPL_WELCOME)(IRCMessage message, const MessageMetadata metadata) { 1185 state.isRegistered = true; 1186 if (!message.args.empty && (message.args.front != "*") && (User(message.args.front).nickname != nickinfo.nickname)) { 1187 nickinfo.nickname = User(message.args.front).nickname; 1188 } 1189 auto meUser = User(); 1190 meUser.mask.nickname = nickinfo.nickname; 1191 meUser.mask.ident = nickinfo.username; 1192 meUser.mask.host = "127.0.0.1"; 1193 internalAddressList.update(meUser); 1194 tryCall!"onConnect"(); 1195 } 1196 private void rec(string cmd : Numeric.RPL_LOGGEDIN)(IRCMessage message, const MessageMetadata metadata) { 1197 import virc.numerics.sasl : parseNumeric; 1198 if (state.isAuthenticating || isAuthenticated) { 1199 auto parsed = parseNumeric!(Numeric.RPL_LOGGEDIN)(message.args); 1200 auto user = User(parsed.get.mask); 1201 user.account = parsed.get.account; 1202 internalAddressList.update(user); 1203 } 1204 } 1205 private void rec(string cmd)(IRCMessage message, const MessageMetadata metadata) if (cmd.among(Numeric.ERR_NICKLOCKED, Numeric.ERR_SASLFAIL, Numeric.ERR_SASLTOOLONG, Numeric.ERR_SASLABORTED)) { 1206 endAuthentication(); 1207 } 1208 private void rec(string cmd : Numeric.RPL_MYINFO)(IRCMessage message, const MessageMetadata metadata) { 1209 server.myInfo = parseNumeric!(Numeric.RPL_MYINFO)(message.args).get; 1210 } 1211 private void rec(string cmd : Numeric.RPL_LUSERCLIENT)(IRCMessage message, const MessageMetadata metadata) { 1212 tryCall!"onLUserClient"(parseNumeric!(Numeric.RPL_LUSERCLIENT)(message.args), metadata); 1213 } 1214 private void rec(string cmd : Numeric.RPL_LUSEROP)(IRCMessage message, const MessageMetadata metadata) { 1215 tryCall!"onLUserOp"(parseNumeric!(Numeric.RPL_LUSEROP)(message.args), metadata); 1216 } 1217 private void rec(string cmd : Numeric.RPL_LUSERCHANNELS)(IRCMessage message, const MessageMetadata metadata) { 1218 tryCall!"onLUserChannels"(parseNumeric!(Numeric.RPL_LUSERCHANNELS)(message.args), metadata); 1219 } 1220 private void rec(string cmd : Numeric.RPL_LUSERME)(IRCMessage message, const MessageMetadata metadata) { 1221 tryCall!"onLUserMe"(parseNumeric!(Numeric.RPL_LUSERME)(message.args), metadata); 1222 } 1223 private void rec(string cmd : Numeric.RPL_YOUREOPER)(IRCMessage message, const MessageMetadata metadata) { 1224 tryCall!"onYoureOper"(metadata); 1225 } 1226 private void rec(string cmd : Numeric.ERR_NOMOTD)(IRCMessage message, const MessageMetadata metadata) { 1227 tryCall!"onError"(IRCError(ErrorType.noMOTD), metadata); 1228 } 1229 private void rec(string cmd : Numeric.RPL_SASLSUCCESS)(IRCMessage message, const MessageMetadata metadata) { 1230 if (state.selectedSASLMech) { 1231 state.authenticationSucceeded = true; 1232 } 1233 endAuthentication(); 1234 } 1235 private void rec(string cmd : Numeric.RPL_LIST)(IRCMessage message, const MessageMetadata metadata) { 1236 auto channel = parseNumeric!(Numeric.RPL_LIST)(message.args, server.iSupport.channelModeTypes); 1237 tryCall!"onList"(channel, metadata); 1238 } 1239 private void rec(string cmd : RFC1459Commands.ping)(IRCMessage message, const MessageMetadata) { 1240 pong(message.args.front); 1241 } 1242 private void rec(string cmd : Numeric.RPL_ISON)(IRCMessage message, const MessageMetadata metadata) { 1243 auto reply = parseNumeric!(Numeric.RPL_ISON)(message.args); 1244 if (!reply.isNull) { 1245 foreach (online; reply.get.online) { 1246 internalAddressList.update(User(online)); 1247 tryCall!"onIsOn"(internalAddressList[online], metadata); 1248 } 1249 } else { 1250 tryCall!"onError"(IRCError(ErrorType.malformed), metadata); 1251 } 1252 } 1253 private void rec(string cmd : Numeric.RPL_MONONLINE)(IRCMessage message, const MessageMetadata metadata) { 1254 auto users = parseNumeric!(Numeric.RPL_MONONLINE)(message.args); 1255 foreach (user; users) { 1256 tryCall!"onUserOnline"(user, SysTime.init, metadata); 1257 } 1258 } 1259 private void rec(string cmd : Numeric.RPL_MONOFFLINE)(IRCMessage message, const MessageMetadata metadata) { 1260 auto users = parseNumeric!(Numeric.RPL_MONOFFLINE)(message.args); 1261 foreach (user; users) { 1262 tryCall!"onUserOffline"(user, metadata); 1263 } 1264 } 1265 private void rec(string cmd : Numeric.RPL_MONLIST)(IRCMessage message, const MessageMetadata metadata) { 1266 auto users = parseNumeric!(Numeric.RPL_MONLIST)(message.args); 1267 foreach (user; users) { 1268 tryCall!"onMonitorList"(user, metadata); 1269 } 1270 } 1271 private void rec(string cmd : Numeric.ERR_MONLISTFULL)(IRCMessage message, const MessageMetadata metadata) { 1272 auto err = parseNumeric!(Numeric.ERR_MONLISTFULL)(message.args); 1273 tryCall!"onError"(IRCError(ErrorType.monListFull), metadata); 1274 } 1275 private void rec(string cmd : Numeric.RPL_VERSION)(IRCMessage message, const MessageMetadata metadata) { 1276 auto versionReply = parseNumeric!(Numeric.RPL_VERSION)(message.args); 1277 tryCall!"onVersionReply"(versionReply.get, metadata); 1278 } 1279 private void rec(string cmd : Numeric.RPL_LOGON)(IRCMessage message, const MessageMetadata metadata) { 1280 auto reply = parseNumeric!(Numeric.RPL_LOGON)(message.args); 1281 tryCall!"onUserOnline"(reply.user, reply.timeOccurred, metadata); 1282 } 1283 private void rec(string cmd : IRCV3Commands.chghost)(IRCMessage message, const MessageMetadata metadata) { 1284 User target; 1285 auto split = message.args; 1286 auto user = message.sourceUser.get; 1287 target.mask.nickname = user.nickname; 1288 target.mask.ident = split.front; 1289 split.popFront(); 1290 target.mask.host = split.front; 1291 internalAddressList.update(target); 1292 tryCall!"onChgHost"(user, target, metadata); 1293 } 1294 private void rec(string cmd : Numeric.RPL_TOPICWHOTIME)(IRCMessage message, const MessageMetadata metadata) { 1295 auto reply = parseNumeric!(Numeric.RPL_TOPICWHOTIME)(message.args); 1296 if (!reply.isNull) { 1297 tryCall!"onTopicWhoTimeReply"(reply.get, metadata); 1298 } 1299 } 1300 private void rec(string cmd : Numeric.RPL_AWAY)(IRCMessage message, const MessageMetadata metadata) { 1301 auto reply = parseNumeric!(Numeric.RPL_AWAY)(message.args); 1302 if (!reply.isNull) { 1303 tryCall!"onOtherUserAwayReply"(reply.get.user, reply.get.message, metadata); 1304 } 1305 } 1306 private void rec(string cmd : Numeric.RPL_UNAWAY)(IRCMessage message, const MessageMetadata metadata) { 1307 tryCall!"onUnAwayReply"(message.sourceUser.get, metadata); 1308 state._isAway = false; 1309 } 1310 private void rec(string cmd : Numeric.RPL_NOWAWAY)(IRCMessage message, const MessageMetadata metadata) { 1311 tryCall!"onAwayReply"(message.sourceUser.get, metadata); 1312 state._isAway = true; 1313 } 1314 private void rec(string cmd : Numeric.RPL_TOPIC)(IRCMessage message, const MessageMetadata metadata) { 1315 auto reply = parseNumeric!(Numeric.RPL_TOPIC)(message.args); 1316 if (!reply.isNull) { 1317 tryCall!"onTopicReply"(reply.get, metadata); 1318 } 1319 } 1320 private void rec(string cmd : RFC1459Commands.topic)(IRCMessage message, const MessageMetadata metadata) { 1321 auto split = message.args; 1322 auto user = message.sourceUser.get; 1323 if (split.empty) { 1324 tryCall!"onError"(IRCError(ErrorType.malformed), metadata); 1325 return; 1326 } 1327 auto target = Channel(split.front); 1328 split.popFront(); 1329 if (split.empty) { 1330 tryCall!"onError"(IRCError(ErrorType.malformed), metadata); 1331 return; 1332 } 1333 auto msg = split.front; 1334 tryCall!"onTopicChange"(user, target, msg, metadata); 1335 } 1336 private void rec(string cmd : RFC1459Commands.nick)(IRCMessage message, const MessageMetadata metadata) { 1337 auto split = message.args; 1338 if (!split.empty) { 1339 auto old = message.sourceUser.get; 1340 auto newNick = split.front; 1341 internalAddressList.renameTo(old, newNick); 1342 foreach (ref channel; channels) { 1343 if (old.nickname in channel.users) { 1344 channel.users.renameTo(old, newNick); 1345 } 1346 } 1347 auto new_ = internalAddressList[newNick]; 1348 if (old.nickname == nickinfo.nickname) { 1349 nickinfo.nickname = new_.nickname; 1350 } 1351 tryCall!"onNick"(old, new_, metadata); 1352 } 1353 } 1354 private void rec(string cmd : RFC1459Commands.invite)(IRCMessage message, const MessageMetadata metadata) { 1355 auto split = message.args; 1356 auto inviter = message.sourceUser.get; 1357 if (!split.empty) { 1358 User invited; 1359 if (split.front in internalAddressList) { 1360 invited = internalAddressList[split.front]; 1361 } else { 1362 invited = User(split.front); 1363 } 1364 split.popFront(); 1365 if (!split.empty) { 1366 auto channel = Channel(split.front); 1367 tryCall!"onInvite"(inviter, invited, channel, metadata); 1368 } 1369 } 1370 } 1371 private void rec(string cmd : RFC1459Commands.quit)(IRCMessage message, const MessageMetadata metadata) { 1372 auto split = message.args; 1373 auto user = message.sourceUser.get; 1374 string msg; 1375 if (!split.empty) { 1376 msg = split.front; 1377 } 1378 foreach (ref channel; channels) { 1379 if (user.nickname in channel.users) { 1380 tryCall!"onChannelListUpdate"(user, user, channel.channel, ChannelListUpdateType.added); 1381 } 1382 } 1383 if (isMe(user)) { 1384 state.invalid = true; 1385 } 1386 tryCall!"onQuit"(user, msg, metadata); 1387 internalAddressList.invalidate(user.nickname); 1388 } 1389 private void recUnknownCommand(const string cmd, const MessageMetadata metadata) @safe { 1390 if (cmd.filter!(x => !x.isDigit).empty) { 1391 recUnknownNumeric(cmd, metadata); 1392 } else { 1393 tryCall!"onError"(IRCError(ErrorType.unrecognized, cmd), metadata); 1394 debug(verboseirc) import std.experimental.logger : trace; 1395 debug(verboseirc) trace(" Unknown command: ", metadata.original); 1396 } 1397 } 1398 private void rec(string cmd : Numeric.RPL_NAMREPLY)(IRCMessage message, const MessageMetadata metadata) { 1399 auto reply = parseNumeric!(Numeric.RPL_NAMREPLY)(message.args); 1400 foreach (user; reply.get.users(server.iSupport.prefixes)) { 1401 internalAddressList.update(User(user.name)); 1402 } 1403 if (reply.get.channel in channels) { 1404 foreach (user; reply.get.users(server.iSupport.prefixes)) { 1405 const newUser = User(user.name); 1406 channels[reply.get.channel].users.update(newUser); 1407 if (newUser != me) { 1408 tryCall!"onChannelListUpdate"(newUser, newUser, Channel(reply.get.channel), ChannelListUpdateType.added); 1409 } 1410 } 1411 } 1412 if (!reply.isNull) { 1413 tryCall!"onNamesReply"(reply.get, metadata); 1414 } 1415 } 1416 private void rec(string cmd : Numeric.RPL_WHOSPCRPL)(IRCMessage message, const MessageMetadata metadata) { 1417 auto reply = parseNumeric!(Numeric.RPL_WHOSPCRPL)(message.args, "uihsnflar"); 1418 if (!reply.isNull) { 1419 User user; 1420 user.account = reply.get.account; 1421 user.realName = reply.get.realname; 1422 user.mask.ident = reply.get.ident; 1423 user.mask.host = reply.get.host; 1424 user.mask.nickname = reply.get.nick.get; 1425 auto oldUser = internalAddressList[user.mask.nickname]; 1426 internalAddressList.update(user); 1427 foreach (ref channel; channels) { 1428 if (user.nickname in channel.users) { 1429 tryCall!"onChannelListUpdate"(user, oldUser, channel.channel, ChannelListUpdateType.updated); 1430 } 1431 } 1432 tryCall!"onWHOXReply"(reply.get, metadata); 1433 } 1434 } 1435 private void rec(string cmd : Numeric.RPL_REHASHING)(IRCMessage message, const MessageMetadata metadata) { 1436 auto reply = parseNumeric!(Numeric.RPL_REHASHING)(message.args); 1437 if (!reply.isNull) { 1438 tryCall!"onServerRehashing"(reply.get, metadata); 1439 } 1440 } 1441 private void rec(string cmd : Numeric.ERR_NOPRIVS)(IRCMessage message, const MessageMetadata metadata) { 1442 auto reply = parseNumeric!(Numeric.ERR_NOPRIVS)(message.args); 1443 if (!reply.isNull) { 1444 tryCall!"onError"(IRCError(ErrorType.noPrivs, reply.get.priv), metadata); 1445 } else { 1446 tryCall!"onError"(IRCError(ErrorType.malformed), metadata); 1447 } 1448 } 1449 private void rec(string cmd : Numeric.ERR_NOPRIVILEGES)(IRCMessage message, const MessageMetadata metadata) { 1450 tryCall!"onError"(IRCError(ErrorType.noPrivileges), metadata); 1451 } 1452 private void rec(string cmd : Numeric.ERR_NOSUCHSERVER)(IRCMessage message, const MessageMetadata metadata) { 1453 auto reply = parseNumeric!(Numeric.ERR_NOSUCHSERVER)(message.args); 1454 if (!reply.isNull) { 1455 tryCall!"onError"(IRCError(ErrorType.noSuchServer, reply.get.serverMask), metadata); 1456 } else { 1457 tryCall!"onError"(IRCError(ErrorType.malformed), metadata); 1458 } 1459 } 1460 private void rec(string cmd : Numeric.RPL_ENDOFWHOIS)(IRCMessage message, const MessageMetadata metadata) { 1461 auto reply = parseNumeric!(Numeric.RPL_ENDOFWHOIS)(message.args); 1462 if (!reply.isNull) { 1463 if (reply.get.user.nickname in state.whoisCache) { 1464 tryCall!"onWhois"(reply.get.user, state.whoisCache[reply.get.user.nickname]); 1465 state.whoisCache.remove(reply.get.user.nickname); 1466 } else { 1467 tryCall!"onError"(IRCError(ErrorType.unexpected, "empty WHOIS data returned"), metadata); 1468 } 1469 } else { 1470 tryCall!"onError"(IRCError(ErrorType.malformed), metadata); 1471 } 1472 } 1473 private void rec(string cmd : Numeric.RPL_WHOISUSER)(IRCMessage message, const MessageMetadata metadata) { 1474 auto reply = parseNumeric!(Numeric.RPL_WHOISUSER)(message.args); 1475 if (!reply.isNull) { 1476 if (reply.get.user.nickname !in state.whoisCache) { 1477 state.whoisCache[reply.get.user.nickname] = WhoisResponse(); 1478 } 1479 state.whoisCache[reply.get.user.nickname].username = reply.get.username; 1480 state.whoisCache[reply.get.user.nickname].hostname = reply.get.hostname; 1481 state.whoisCache[reply.get.user.nickname].realname = reply.get.realname; 1482 } else { 1483 tryCall!"onError"(IRCError(ErrorType.malformed), metadata); 1484 } 1485 } 1486 private void rec(string cmd : Numeric.RPL_WHOISSECURE)(IRCMessage message, const MessageMetadata metadata) { 1487 auto reply = parseNumeric!(Numeric.RPL_WHOISSECURE)(message.args); 1488 if (!reply.isNull) { 1489 if (reply.get.user.nickname !in state.whoisCache) { 1490 state.whoisCache[reply.get.user.nickname] = WhoisResponse(); 1491 } 1492 state.whoisCache[reply.get.user.nickname].isSecure = true; 1493 } else { 1494 tryCall!"onError"(IRCError(ErrorType.malformed), metadata); 1495 } 1496 } 1497 private void rec(string cmd : Numeric.RPL_WHOISOPERATOR)(IRCMessage message, const MessageMetadata metadata) { 1498 auto reply = parseNumeric!(Numeric.RPL_WHOISOPERATOR)(message.args); 1499 if (!reply.isNull) { 1500 if (reply.get.user.nickname !in state.whoisCache) { 1501 state.whoisCache[reply.get.user.nickname] = WhoisResponse(); 1502 } 1503 state.whoisCache[reply.get.user.nickname].isOper = true; 1504 } else { 1505 tryCall!"onError"(IRCError(ErrorType.malformed), metadata); 1506 } 1507 } 1508 private void rec(string cmd : Numeric.RPL_WHOISREGNICK)(IRCMessage message, const MessageMetadata metadata) { 1509 auto reply = parseNumeric!(Numeric.RPL_WHOISREGNICK)(message.args); 1510 if (!reply.isNull) { 1511 if (reply.get.user.nickname !in state.whoisCache) { 1512 state.whoisCache[reply.get.user.nickname] = WhoisResponse(); 1513 } 1514 state.whoisCache[reply.get.user.nickname].isRegistered = true; 1515 } else { 1516 tryCall!"onError"(IRCError(ErrorType.malformed), metadata); 1517 } 1518 } 1519 private void rec(string cmd : Numeric.RPL_WHOISIDLE)(IRCMessage message, const MessageMetadata metadata) { 1520 auto reply = parseNumeric!(Numeric.RPL_WHOISIDLE)(message.args); 1521 if (!reply.isNull) { 1522 if (reply.user.get.nickname !in state.whoisCache) { 1523 state.whoisCache[reply.user.get.nickname] = WhoisResponse(); 1524 } 1525 state.whoisCache[reply.user.get.nickname].idleTime = reply.idleTime; 1526 state.whoisCache[reply.user.get.nickname].connectedTime = reply.connectedTime; 1527 } else { 1528 tryCall!"onError"(IRCError(ErrorType.malformed), metadata); 1529 } 1530 } 1531 private void rec(string cmd : Numeric.RPL_WHOISSERVER)(IRCMessage message, const MessageMetadata metadata) { 1532 auto reply = parseNumeric!(Numeric.RPL_WHOISSERVER)(message.args); 1533 if (!reply.isNull) { 1534 if (reply.get.user.nickname !in state.whoisCache) { 1535 state.whoisCache[reply.get.user.nickname] = WhoisResponse(); 1536 } 1537 state.whoisCache[reply.get.user.nickname].connectedTo = reply.get.server; 1538 } else { 1539 tryCall!"onError"(IRCError(ErrorType.malformed), metadata); 1540 } 1541 } 1542 private void rec(string cmd : Numeric.RPL_WHOISACCOUNT)(IRCMessage message, const MessageMetadata metadata) { 1543 auto reply = parseNumeric!(Numeric.RPL_WHOISACCOUNT)(message.args); 1544 if (!reply.isNull) { 1545 if (reply.get.user.nickname !in state.whoisCache) { 1546 state.whoisCache[reply.get.user.nickname] = WhoisResponse(); 1547 } 1548 state.whoisCache[reply.get.user.nickname].account = reply.get.account; 1549 } else { 1550 tryCall!"onError"(IRCError(ErrorType.malformed), metadata); 1551 } 1552 } 1553 private void rec(string cmd : IRCV3Commands.metadata)(IRCMessage message, const MessageMetadata metadata) { 1554 auto split = message.args; 1555 auto target = Target(split.front, server.iSupport.statusMessage, server.iSupport.channelTypes); 1556 split.popFront(); 1557 auto key = split.front; 1558 split.popFront(); 1559 auto visibility = split.front; 1560 split.popFront(); 1561 if (split.empty) { 1562 deleteMetadataCommon(target, key); 1563 } else { 1564 setMetadataCommon(target, visibility, key, split.front); 1565 } 1566 } 1567 private void rec(string cmd : Numeric.RPL_WHOISKEYVALUE)(IRCMessage message, const MessageMetadata metadata) { 1568 auto split = message.args; 1569 string prefixes; 1570 foreach (k,v; server.iSupport.prefixes) { 1571 prefixes ~= v; 1572 } 1573 auto reply = parseNumeric!(Numeric.RPL_WHOISKEYVALUE)(split, prefixes, server.iSupport.channelTypes); 1574 if (!reply.isNull) { 1575 setMetadataCommon(reply.get.target, reply.get.visibility, reply.get.key, reply.get.value); 1576 } else { 1577 tryCall!"onError"(IRCError(ErrorType.malformed), metadata); 1578 } 1579 } 1580 private void rec(string cmd : Numeric.RPL_KEYVALUE)(IRCMessage message, const MessageMetadata metadata) { 1581 auto split = message.args; 1582 string prefixes; 1583 foreach (k,v; server.iSupport.prefixes) { 1584 prefixes ~= v; 1585 } 1586 auto reply = parseNumeric!(Numeric.RPL_KEYVALUE)(split, prefixes, server.iSupport.channelTypes); 1587 if (!reply.isNull) { 1588 if (reply.get.value.isNull) { 1589 deleteMetadataCommon(reply.get.target, reply.get.key); 1590 } else { 1591 setMetadataCommon(reply.get.target, reply.get.visibility, reply.get.key, reply.get.value.get); 1592 } 1593 } else { 1594 tryCall!"onError"(IRCError(ErrorType.malformed), metadata); 1595 } 1596 } 1597 private void setMetadataCommon(Target target, string visibility, string key, string value) @safe pure { 1598 if (target.isUser && target.user == User("*")) { 1599 userMetadata[me][key] = MetadataValue(visibility, value); 1600 } else if (target.isChannel) { 1601 channelMetadata[target.channel.get][key] = MetadataValue(visibility, value); 1602 } else if (target.isUser) { 1603 userMetadata[target.user.get][key] = MetadataValue(visibility, value); 1604 } 1605 } 1606 private void deleteMetadataCommon(Target target, string key) @safe pure { 1607 if (target.isUser && target.user == User("*")) { 1608 userMetadata[me].remove(key); 1609 } else if (target.isChannel) { 1610 channelMetadata[target.channel.get].remove(key); 1611 } else if (target.isUser) { 1612 userMetadata[target.user.get].remove(key); 1613 } 1614 } 1615 private void rec(string cmd : Numeric.RPL_METADATASUBOK)(IRCMessage message, const MessageMetadata metadata) { 1616 auto parsed = parseNumeric!(Numeric.RPL_METADATASUBOK)(message.args); 1617 if (!parsed.isNull) { 1618 foreach (sub; parsed.get.subs) { 1619 if (!state.metadataSubscribedKeys.canFind(sub)) { 1620 state.metadataSubscribedKeys ~= sub; 1621 } 1622 } 1623 } else { 1624 tryCall!"onError"(IRCError(ErrorType.malformed), metadata); 1625 } 1626 } 1627 private void rec(string cmd : Numeric.RPL_METADATAUNSUBOK)(IRCMessage message, const MessageMetadata metadata) { 1628 auto parsed = parseNumeric!(Numeric.RPL_METADATAUNSUBOK)(message.args); 1629 if (!parsed.isNull) { 1630 state.metadataSubscribedKeys = state.metadataSubscribedKeys.filter!(x => !parsed.get.subs.canFind(x)).array; 1631 } else { 1632 tryCall!"onError"(IRCError(ErrorType.malformed), metadata); 1633 } 1634 } 1635 private void rec(string cmd : Numeric.RPL_METADATASUBS)(IRCMessage message, const MessageMetadata metadata) { 1636 auto reply = parseNumeric!(Numeric.RPL_METADATASUBS)(message.args); 1637 if (!reply.isNull) { 1638 foreach (sub; reply.get.subs) { 1639 tryCall!"onMetadataSubList"(sub, metadata); 1640 } 1641 } else { 1642 tryCall!"onError"(IRCError(ErrorType.malformed), metadata); 1643 } 1644 } 1645 private void rec(string cmd : Numeric.RPL_KEYNOTSET)(IRCMessage message, const MessageMetadata metadata) { 1646 auto split = message.args; 1647 string prefixes; 1648 foreach (k,v; server.iSupport.prefixes) { 1649 prefixes ~= v; 1650 } 1651 auto err = parseNumeric!(Numeric.RPL_KEYNOTSET)(split, prefixes, server.iSupport.channelTypes); 1652 tryCall!"onError"(IRCError(ErrorType.keyNotSet, err.get.humanReadable), metadata); 1653 } 1654 private void rec(string cmd : Numeric.ERR_METADATASYNCLATER)(IRCMessage message, const MessageMetadata metadata) { 1655 auto split = message.args; 1656 string prefixes; 1657 foreach (k,v; server.iSupport.prefixes) { 1658 prefixes ~= v; 1659 } 1660 auto err = parseNumeric!(Numeric.ERR_METADATASYNCLATER)(split, prefixes, server.iSupport.channelTypes); 1661 tryCall!"onError"(IRCError(ErrorType.waitAndRetry), metadata); 1662 } 1663 private void rec(string cmd : Numeric.RPL_WHOISCHANNELS)(IRCMessage message, const MessageMetadata metadata) { 1664 string prefixes; 1665 foreach (k,v; server.iSupport.prefixes) { 1666 prefixes ~= v; 1667 } 1668 auto reply = parseNumeric!(Numeric.RPL_WHOISCHANNELS)(message.args, prefixes, server.iSupport.channelTypes); 1669 if (!reply.isNull) { 1670 if (reply.get.user.nickname !in state.whoisCache) { 1671 state.whoisCache[reply.get.user.nickname] = WhoisResponse(); 1672 } 1673 foreach (channel; reply.get.channels) { 1674 auto whoisChannel = WhoisChannel(); 1675 whoisChannel.name = channel.channel; 1676 if (!channel.prefix.isNull) { 1677 whoisChannel.prefix = channel.prefix.get; 1678 } 1679 state.whoisCache[reply.get.user.nickname].channels[channel.channel.name] = whoisChannel; 1680 } 1681 } else { 1682 tryCall!"onError"(IRCError(ErrorType.malformed), metadata); 1683 } 1684 } 1685 private void recUnknownNumeric(const string cmd, const MessageMetadata metadata) @safe { 1686 tryCall!"onError"(IRCError(ErrorType.unrecognized, cmd), metadata); 1687 debug(verboseirc) import std.experimental.logger : trace; 1688 debug(verboseirc) trace("Unhandled numeric: ", cast(Numeric)cmd, " ", metadata.original); 1689 } 1690 private void rec(string cmd : IRCV3Commands.account)(IRCMessage message, const MessageMetadata metadata) { 1691 auto split = message.args; 1692 if (!split.empty) { 1693 auto user = message.sourceUser.get; 1694 auto newAccount = split.front; 1695 internalAddressList.update(user); 1696 auto newUser = internalAddressList[user.nickname]; 1697 if (newAccount == "*") { 1698 newUser.account.nullify(); 1699 } else { 1700 newUser.account = newAccount; 1701 } 1702 internalAddressList.updateExact(newUser); 1703 } 1704 } 1705 private void rec(string cmd : IRCV3Commands.authenticate)(IRCMessage message, const MessageMetadata metadata) { 1706 import std.base64 : Base64; 1707 auto split = message.args; 1708 if (split.front != "+") { 1709 state.receivedSASLAuthenticationText ~= Base64.decode(split.front); 1710 } 1711 if ((state.selectedSASLMech) && (split.front == "+" || (split.front.length < 400))) { 1712 state.selectedSASLMech.put(state.receivedSASLAuthenticationText); 1713 if (state.selectedSASLMech.empty) { 1714 sendAuthenticatePayload(""); 1715 } else { 1716 sendAuthenticatePayload(state.selectedSASLMech.front); 1717 state.selectedSASLMech.popFront(); 1718 } 1719 state.receivedSASLAuthenticationText = []; 1720 } 1721 } 1722 private void rec(string cmd : IRCV3Commands.note)(IRCMessage message, const MessageMetadata metadata) { 1723 } 1724 private void rec(string cmd : IRCV3Commands.warn)(IRCMessage message, const MessageMetadata metadata) { 1725 } 1726 private void rec(string cmd : IRCV3Commands.fail)(IRCMessage message, const MessageMetadata metadata) { 1727 tryCall!"onError"(IRCError(ErrorType.standardFail, cmd), metadata); 1728 } 1729 bool isMe(const User user) const pure @safe nothrow { 1730 return user == me; 1731 } 1732 bool isValid() const pure @safe nothrow { 1733 return !state.invalid; 1734 } 1735 bool isRegistered() const pure @safe nothrow { 1736 return state.isRegistered; 1737 } 1738 } 1739 version(unittest) { 1740 import std.algorithm : equal, sort, until; 1741 import std.array : appender, array; 1742 import std.range: drop, empty, tail; 1743 import std.stdio : writeln; 1744 import std.string : lineSplitter, representation; 1745 import std.typecons : Tuple, tuple; 1746 import virc.ircv3 : Capability; 1747 static immutable testClientInfo = NickInfo("someone", "ident", "real name!"); 1748 static immutable testUser = User(testClientInfo.nickname, testClientInfo.username, "example.org"); 1749 mixin template Test() { 1750 bool lineReceived; 1751 void onRaw(const MessageMetadata) @safe pure { 1752 lineReceived = true; 1753 } 1754 } 1755 void setupFakeConnection(T)(ref T client) { 1756 if (!client.onError) { 1757 client.onError = (const IRCError error, const MessageMetadata metadata) { 1758 writeln(metadata.time, " - ", error.type, " - ", metadata.original); 1759 }; 1760 } 1761 client.put(":localhost 001 someone :Welcome to the TestNet IRC Network "~testUser.text); 1762 client.put(":localhost 002 someone :Your host is localhost, running version IRCd-2.0"); 1763 client.put(":localhost 003 someone :This server was created 20:21:33 Oct 21 2016"); 1764 client.put(":localhost 004 someone localhost IRCd-2.0 BGHIRSWcdgikorswx ABCDFGIJKLMNOPQRSTYabcefghijklmnopqrstuvz FIJLYabefghjkloqv"); 1765 client.put(":localhost 005 someone AWAYLEN=200 CALLERID=g CASEMAPPING=rfc1459 CHANMODES=IYbeg,k,FJLfjl,ABCDGKMNOPQRSTcimnprstuz CHANNELLEN=31 CHANTYPES=# CHARSET=ascii ELIST=MU ESILENCE EXCEPTS=e EXTBAN=,ABCNOQRSTUcjmprsz FNC INVEX=I :are supported by this server"); 1766 client.put(":localhost 005 someone KICKLEN=255 MAP MAXBANS=60 MAXCHANNELS=25 MAXPARA=32 MAXTARGETS=20 MODES=10 NAMESX NETWORK=TestNet NICKLEN=31 OPERLOG OVERRIDE PREFIX=(qaohv)~&@%+ :are supported by this server"); 1767 client.put(":localhost 005 someone REMOVE SECURELIST SILENCE=32 SSL=[::]:6697 STARTTLS STATUSMSG=~&@%+ TOPICLEN=307 UHNAMES USERIP VBANLIST WALLCHOPS WALLVOICES WATCH=1000 WHOX :are supported by this server"); 1768 assert(client.isRegistered); 1769 assert(client.server.iSupport.userhostsInNames == true); 1770 } 1771 void initializeCaps(T)(ref T client) { 1772 initializeWithCaps(client, [Capability("multi-prefix"), Capability("server-time"), Capability("sasl", "EXTERNAL")]); 1773 } 1774 void initializeWithCaps(T)(ref T client, Capability[] caps) { 1775 foreach (i, cap; caps) { 1776 client.put(":localhost CAP * LS " ~ ((i+1 == caps.length) ? "" : "* ")~ ":" ~ cap.toString); 1777 client.put(":localhost CAP * ACK :" ~ cap.name); 1778 } 1779 setupFakeConnection(client); 1780 } 1781 class Wrapper : Output { 1782 import std.array : Appender; 1783 Appender!string buffer; 1784 override void put(char c) @safe { 1785 buffer.put(c); 1786 } 1787 this(Appender!string buf) @safe { 1788 buffer = buf; 1789 } 1790 } 1791 auto data(Output o) @safe { 1792 return (cast(Wrapper)o).buffer.data; 1793 } 1794 auto spawnNoBufferClient(string password = string.init) { 1795 auto buffer = appender!(string); 1796 return ircClient(new Wrapper(buffer), testClientInfo, [], password); 1797 } 1798 } 1799 //Test the basics 1800 @safe unittest { 1801 auto client = spawnNoBufferClient(); 1802 bool lineReceived; 1803 client.onRaw = (_) { 1804 lineReceived = true; 1805 }; 1806 client.put(""); 1807 assert(lineReceived == false); 1808 client.put("\r\n"); 1809 assert(lineReceived == false); 1810 client.put("hello"); 1811 assert(lineReceived == true); 1812 assert(!client.isRegistered); 1813 client.put(":localhost 001 someone :words"); 1814 assert(client.isRegistered); 1815 client.put(":localhost 001 someone :words"); 1816 assert(client.isRegistered); 1817 } 1818 //Auto-decoding test 1819 @safe unittest { 1820 auto client = spawnNoBufferClient(); 1821 bool lineReceived; 1822 client.onRaw = (_) { 1823 lineReceived = true; 1824 }; 1825 client.put("\r\n".representation); 1826 assert(lineReceived == false); 1827 } 1828 @safe unittest { 1829 import virc.ircv3 : Capability; 1830 { //password test 1831 auto client = spawnNoBufferClient("Example"); 1832 1833 assert(client.output.data.lineSplitter.until!(x => x.startsWith("USER")).canFind("PASS :Example")); 1834 } 1835 //Request capabilities (IRC v3.2) 1836 { 1837 auto client = spawnNoBufferClient(); 1838 client.put(":localhost CAP * LS :multi-prefix sasl"); 1839 client.put(":localhost CAP * ACK :multi-prefix sasl"); 1840 1841 auto lineByLine = client.output.data.lineSplitter; 1842 1843 assert(lineByLine.front == "CAP LS 302"); 1844 lineByLine.popFront(); 1845 lineByLine.popFront(); 1846 lineByLine.popFront(); 1847 //sasl not yet supported 1848 assert(lineByLine.front == "CAP REQ :multi-prefix sasl"); 1849 lineByLine.popFront(); 1850 assert(!lineByLine.empty); 1851 assert(lineByLine.front == "CAP END"); 1852 } 1853 //Request capabilities NAK (IRC v3.2) 1854 { 1855 auto client = spawnNoBufferClient(); 1856 Capability[] capabilities; 1857 client.onReceiveCapNak = (const Capability cap, const MessageMetadata) { 1858 capabilities ~= cap; 1859 }; 1860 client.put(":localhost CAP * LS :multi-prefix"); 1861 client.put(":localhost CAP * NAK :multi-prefix"); 1862 1863 1864 auto lineByLine = client.output.data.lineSplitter; 1865 1866 assert(lineByLine.front == "CAP LS 302"); 1867 lineByLine.popFront(); 1868 lineByLine.popFront(); 1869 lineByLine.popFront(); 1870 //sasl not yet supported 1871 assert(lineByLine.front == "CAP REQ :multi-prefix"); 1872 lineByLine.popFront(); 1873 assert(!lineByLine.empty); 1874 assert(lineByLine.front == "CAP END"); 1875 1876 assert(!client.capsEnabled.canFind("multi-prefix")); 1877 assert(capabilities.length == 1); 1878 assert(capabilities[0] == Capability("multi-prefix")); 1879 } 1880 //Request capabilities, multiline (IRC v3.2) 1881 { 1882 auto client = spawnNoBufferClient(); 1883 auto lineByLine = client.output.data.lineSplitter(); 1884 1885 Capability[] capabilities; 1886 client.onReceiveCapLS = (const Capability cap, const MessageMetadata) { 1887 capabilities ~= cap; 1888 }; 1889 1890 assert(lineByLine.front == "CAP LS 302"); 1891 1892 put(client, ":localhost CAP * LS * :multi-prefix extended-join account-notify batch invite-notify tls"); 1893 put(client, ":localhost CAP * LS * :cap-notify server-time example.org/dummy-cap=dummyvalue example.org/second-dummy-cap"); 1894 put(client, ":localhost CAP * LS :userhost-in-names sasl=EXTERNAL,DH-AES,DH-BLOWFISH,ECDSA-NIST256P-CHALLENGE,PLAIN"); 1895 auto lines = client.output.data.lineSplitter().array; 1896 assert(lines[5] == "CAP REQ :userhost-in-names sasl"); 1897 assert(capabilities.length == 12); 1898 setupFakeConnection(client); 1899 } 1900 //CAP LIST multiline (IRC v3.2) 1901 { 1902 auto client = spawnNoBufferClient(); 1903 Capability[] capabilities; 1904 client.onReceiveCapList = (const Capability cap, const MessageMetadata) { 1905 capabilities ~= cap; 1906 }; 1907 setupFakeConnection(client); 1908 client.capList(); 1909 client.put(":localhost CAP modernclient LIST * :example.org/example-cap example.org/second-example-cap account-notify"); 1910 client.put(":localhost CAP modernclient LIST :invite-notify batch example.org/third-example-cap"); 1911 assert( 1912 capabilities.array.sort().equal( 1913 [ 1914 Capability("account-notify"), 1915 Capability("batch"), 1916 Capability("example.org/example-cap"), 1917 Capability("example.org/second-example-cap"), 1918 Capability("example.org/third-example-cap"), 1919 Capability("invite-notify") 1920 ] 1921 )); 1922 } 1923 //CAP NEW, DEL (IRCv3.2 - cap-notify) 1924 { 1925 auto client = spawnNoBufferClient(); 1926 Capability[] capabilitiesNew; 1927 Capability[] capabilitiesDeleted; 1928 client.onReceiveCapNew = (const Capability cap, const MessageMetadata) { 1929 capabilitiesNew ~= cap; 1930 }; 1931 client.onReceiveCapDel = (const Capability cap, const MessageMetadata) { 1932 capabilitiesDeleted ~= cap; 1933 }; 1934 initializeWithCaps(client, [Capability("cap-notify"), Capability("userhost-in-names"), Capability("multi-prefix"), Capability("away-notify")]); 1935 1936 assert(client.capsEnabled.length == 4); 1937 1938 client.put(":irc.example.com CAP modernclient NEW :batch"); 1939 assert(capabilitiesNew == [Capability("batch")]); 1940 client.put(":irc.example.com CAP modernclient ACK :batch"); 1941 assert( 1942 client.capsEnabled.sort().equal( 1943 [ 1944 Capability("away-notify"), 1945 Capability("batch"), 1946 Capability("cap-notify"), 1947 Capability("multi-prefix"), 1948 Capability("userhost-in-names") 1949 ] 1950 )); 1951 1952 client.put(":irc.example.com CAP modernclient DEL :userhost-in-names multi-prefix away-notify"); 1953 assert( 1954 capabilitiesDeleted.array.sort().equal( 1955 [ 1956 Capability("away-notify"), 1957 Capability("multi-prefix"), 1958 Capability("userhost-in-names") 1959 ] 1960 )); 1961 assert( 1962 client.capsEnabled.sort().equal( 1963 [ 1964 Capability("batch"), 1965 Capability("cap-notify") 1966 ] 1967 )); 1968 client.put(":irc.example.com CAP modernclient NEW :account-notify"); 1969 auto lineByLine = client.output.data.lineSplitter(); 1970 assert(lineByLine.array[$-1] == "CAP REQ :account-notify"); 1971 client.put(":irc.example.com CAP modernclient NEW :sasl=new"); 1972 auto lineByLine2 = client.output.data.lineSplitter(); 1973 assert(lineByLine2.array[$-1] == "CAP REQ :sasl"); 1974 } 1975 { //JOIN 1976 auto client = spawnNoBufferClient(); 1977 Tuple!(const User, "user", const Channel, "channel")[] joins; 1978 client.onJoin = (const User user, const Channel chan, const MessageMetadata) { 1979 joins ~= tuple!("user", "channel")(user, chan); 1980 }; 1981 TopicWhoTime topicWhoTime; 1982 bool topicWhoTimeReceived; 1983 client.onTopicWhoTimeReply = (const TopicWhoTime twt, const MessageMetadata) { 1984 assert(!topicWhoTimeReceived); 1985 topicWhoTimeReceived = true; 1986 topicWhoTime = twt; 1987 }; 1988 Tuple!(const User, "user", const Channel, "channel", ChannelListUpdateType, "type")[] updates; 1989 client.onChannelListUpdate = (const User user, const User old, const Channel chan, ChannelListUpdateType type) { 1990 updates ~= tuple!("user", "channel", "type")(user, chan, type); 1991 }; 1992 NamesReply[] namesReplies; 1993 client.onNamesReply = (const NamesReply reply, const MessageMetadata) { 1994 namesReplies ~= reply; 1995 }; 1996 TopicReply topicReply; 1997 bool topicReplyReceived; 1998 client.onTopicReply = (const TopicReply tr, const MessageMetadata) { 1999 assert(!topicReplyReceived); 2000 topicReplyReceived = true; 2001 topicReply = tr; 2002 }; 2003 setupFakeConnection(client); 2004 client.join("#test"); 2005 client.put(":someone!ident@hostmask JOIN :#test"); 2006 client.put(":localhost 332 someone #test :a topic"); 2007 client.put(":localhost 333 someone #test someoneElse :1496821983"); 2008 client.put(":localhost 353 someone = #test :someone!ident@hostmask +@another!user@somewhere"); 2009 client.put(":localhost 366 someone #test :End of /NAMES list."); 2010 client.put(":localhost 324 someone #test :+nt"); 2011 client.put(":localhost 329 someone #test :1496821983"); 2012 assert("someone" in client.internalAddressList); 2013 assert(client.internalAddressList["someone"] == User("someone!ident@hostmask")); 2014 assert(client.internalAddressList["someone"].account.isNull); 2015 client.put(":localhost 354 someone ident 127.0.0.1 hostmask localhost someone H@r 0 SomeoneAccount * :a real name"); 2016 client.put(":localhost 354 someone ident 127.0.0.2 somewhere localhost another H@r 66 SomeoneElseAccount * :a different real name"); 2017 client.put(":localhost 315 someone #test :End of WHO list"); 2018 2019 assert(joins.length == 1); 2020 with(joins[0]) { 2021 assert(user.nickname == "someone"); 2022 assert(channel.name == "#test"); 2023 } 2024 assert("someone" in client.channels["#test"].users); 2025 assert(client.channels["#test"].channel.name == "#test"); 2026 assert(client.channels["#test"].users["someone"] == User("someone!ident@hostmask")); 2027 assert("someone" in client.internalAddressList); 2028 assert(client.internalAddressList["someone"] == User("someone!ident@hostmask")); 2029 assert(client.internalAddressList["someone"].account.get == "SomeoneAccount"); 2030 2031 assert(topicWhoTimeReceived); 2032 assert(topicReplyReceived); 2033 2034 with(topicReply) { 2035 assert(channel == "#test"); 2036 assert(topic == "a topic"); 2037 } 2038 2039 with (topicWhoTime) { 2040 //TODO: remove when lack of these imports no longer produces warnings 2041 import std.datetime : SysTime; 2042 import virc.common : User; 2043 assert(channel == "#test"); 2044 assert(setter == User("someoneElse")); 2045 assert(timestamp == SysTime(DateTime(2017, 6, 7, 7, 53, 3), UTC())); 2046 } 2047 //TODO: Add 366, 324, 329 tests 2048 auto lineByLine = client.output.data.lineSplitter(); 2049 assert(lineByLine.array[$-2] == "JOIN #test"); 2050 assert(namesReplies.length == 1); 2051 assert(namesReplies[0].users(['o': '@', 'v': '+']).array.length == 2); 2052 assert(updates.length == 4); 2053 with(updates[0]) { 2054 assert(user.nickname == "someone"); 2055 assert(user.account.isNull); 2056 assert(channel.name == "#test"); 2057 } 2058 with(updates[1]) { 2059 assert(user.nickname == "another"); 2060 assert(user.account.isNull); 2061 assert(channel.name == "#test"); 2062 } 2063 with(updates[2]) { 2064 assert(user.nickname == "someone"); 2065 assert(!user.account.isNull); 2066 assert(user.account.get == "SomeoneAccount"); 2067 assert(channel.name == "#test"); 2068 } 2069 with(updates[3]) { 2070 assert(user.nickname == "another"); 2071 assert(!user.account.isNull); 2072 assert(user.account.get == "SomeoneElseAccount"); 2073 assert(channel.name == "#test"); 2074 } 2075 } 2076 { //Channel list example 2077 auto client = spawnNoBufferClient(); 2078 const(ChannelListResult)[] channels; 2079 client.onList = (const ChannelListResult chan, const MessageMetadata) { 2080 channels ~= chan; 2081 }; 2082 setupFakeConnection(client); 2083 client.list(); 2084 client.put("321 someone Channel :Users Name"); 2085 client.put("322 someone #test 4 :[+fnt 200:2] some words"); 2086 client.put("322 someone #test2 6 :[+fnst 100:2] some more words"); 2087 client.put("322 someone #test3 1 :no modes?"); 2088 client.put("323 someone :End of channel list."); 2089 assert(channels.length == 3); 2090 with(channels[0]) { 2091 //TODO: remove when lack of import no longer produces a warning 2092 import virc.common : Topic; 2093 assert(name == "#test"); 2094 assert(userCount == 4); 2095 assert(topic == Topic("some words")); 2096 } 2097 with(channels[1]) { 2098 //TODO: remove when lack of import no longer produces a warning 2099 import virc.common : Topic; 2100 assert(name == "#test2"); 2101 assert(userCount == 6); 2102 assert(topic == Topic("some more words")); 2103 } 2104 with(channels[2]) { 2105 //TODO: remove when lack of import no longer produces a warning 2106 import virc.common : Topic; 2107 assert(name == "#test3"); 2108 assert(userCount == 1); 2109 assert(topic == Topic("no modes?")); 2110 } 2111 } 2112 { //server-time http://ircv3.net/specs/extensions/server-time-3.2.html 2113 auto client = spawnNoBufferClient(); 2114 User[] users; 2115 const(Channel)[] channels; 2116 client.onJoin = (const User user, const Channel chan, const MessageMetadata metadata) { 2117 users ~= user; 2118 channels ~= chan; 2119 assert(metadata.time == SysTime(DateTime(2012, 6, 30, 23, 59, 59), 419.msecs, UTC())); 2120 }; 2121 setupFakeConnection(client); 2122 client.put("@time=2012-06-30T23:59:59.419Z :John!~john@1.2.3.4 JOIN #chan"); 2123 assert(users.length == 1); 2124 assert(users[0].nickname == "John"); 2125 assert(channels.length == 1); 2126 assert(channels[0].name == "#chan"); 2127 } 2128 { //monitor 2129 auto client = spawnNoBufferClient(); 2130 User[] users; 2131 const(MessageMetadata)[] metadata; 2132 client.onUserOnline = (const User user, const SysTime, const MessageMetadata) { 2133 users ~= user; 2134 }; 2135 client.onUserOffline = (const User user, const MessageMetadata) { 2136 users ~= user; 2137 }; 2138 client.onMonitorList = (const User user, const MessageMetadata) { 2139 users ~= user; 2140 }; 2141 client.onError = (const IRCError error, const MessageMetadata received) { 2142 assert(error.type == ErrorType.monListFull); 2143 metadata ~= received; 2144 }; 2145 setupFakeConnection(client); 2146 client.put(":localhost 730 someone :John!test@example.net,Bob!test2@example.com"); 2147 assert(users.length == 2); 2148 with (users[0]) { 2149 assert(nickname == "John"); 2150 assert(ident == "test"); 2151 assert(host == "example.net"); 2152 } 2153 with (users[1]) { 2154 assert(nickname == "Bob"); 2155 assert(ident == "test2"); 2156 assert(host == "example.com"); 2157 } 2158 2159 users.length = 0; 2160 2161 client.put(":localhost 731 someone :John"); 2162 assert(users.length == 1); 2163 assert(users[0].nickname == "John"); 2164 2165 users.length = 0; 2166 2167 client.put(":localhost 732 someone :John,Bob"); 2168 client.put(":localhost 733 someone :End of MONITOR list"); 2169 assert(users.length == 2); 2170 assert(users[0].nickname == "John"); 2171 assert(users[1].nickname == "Bob"); 2172 2173 client.put(":localhost 734 someone 5 Earl :Monitor list is full."); 2174 assert(metadata.length == 1); 2175 assert(metadata[0].messageNumeric.get == Numeric.ERR_MONLISTFULL); 2176 } 2177 { //extended-join http://ircv3.net/specs/extensions/extended-join-3.1.html 2178 auto client = spawnNoBufferClient(); 2179 2180 User[] users; 2181 client.onJoin = (const User user, const Channel, const MessageMetadata) { 2182 users ~= user; 2183 }; 2184 2185 initializeWithCaps(client, [Capability("extended-join")]); 2186 2187 client.put(":nick!user@host JOIN #channelname accountname :Real Name"); 2188 auto user = User("nick!user@host"); 2189 user.account = "accountname"; 2190 user.realName = "Real Name"; 2191 assert(users.front == user); 2192 2193 user.account.nullify(); 2194 users = []; 2195 client.put(":nick!user@host JOIN #channelname * :Real Name"); 2196 assert(users.front == user); 2197 } 2198 { //test for blank caps 2199 auto client = spawnNoBufferClient(); 2200 put(client, ":localhost CAP * LS * : "); 2201 setupFakeConnection(client); 2202 assert(client.isRegistered); 2203 } 2204 { //example taken from RFC2812, section 3.2.2 2205 auto client = spawnNoBufferClient(); 2206 2207 User[] users; 2208 const(Channel)[] channels; 2209 string lastMsg; 2210 client.onPart = (const User user, const Channel chan, const string msg, const MessageMetadata) { 2211 users ~= user; 2212 channels ~= chan; 2213 lastMsg = msg; 2214 }; 2215 2216 setupFakeConnection(client); 2217 2218 client.put(":WiZ!jto@tolsun.oulu.fi PART #playzone :I lost"); 2219 immutable user = User("WiZ!jto@tolsun.oulu.fi"); 2220 assert(users.front == user); 2221 assert(channels.front == Channel("#playzone")); 2222 assert(lastMsg == "I lost"); 2223 } 2224 { //PART tests 2225 auto client = spawnNoBufferClient(); 2226 2227 Tuple!(const User, "user", const Channel, "channel", string, "message")[] parts; 2228 client.onPart = (const User user, const Channel chan, const string msg, const MessageMetadata) { 2229 parts ~= tuple!("user", "channel", "message")(user, chan, msg); 2230 }; 2231 2232 setupFakeConnection(client); 2233 2234 client.put(":"~testUser.text~" JOIN #example"); 2235 client.put(":SomeoneElse JOIN #example"); 2236 assert("#example" in client.channels); 2237 assert("SomeoneElse" in client.channels["#example"].users); 2238 client.put(":SomeoneElse PART #example :bye forever"); 2239 assert("SomeoneElse" !in client.channels["#example"].users); 2240 client.put(":"~testUser.text~" PART #example :see ya"); 2241 assert("#example" !in client.channels); 2242 2243 client.put(":"~testUser.text~" JOIN #example"); 2244 client.put(":SomeoneElse JOIN #example"); 2245 assert("#example" in client.channels); 2246 assert("SomeoneElse" in client.channels["#example"].users); 2247 client.put(":SomeoneElse PART #example"); 2248 assert("SomeoneElse" !in client.channels["#example"].users); 2249 client.put(":"~testUser.text~" PART #example"); 2250 assert("#example" !in client.channels); 2251 2252 assert(parts.length == 4); 2253 with (parts[0]) { 2254 assert(user == User("SomeoneElse")); 2255 assert(channel == Channel("#example")); 2256 assert(message == "bye forever"); 2257 } 2258 with (parts[1]) { 2259 assert(user == client.me); 2260 assert(channel == Channel("#example")); 2261 assert(message == "see ya"); 2262 } 2263 with (parts[2]) { 2264 assert(user == User("SomeoneElse")); 2265 assert(channel == Channel("#example")); 2266 } 2267 with (parts[3]) { 2268 assert(user == client.me); 2269 assert(channel == Channel("#example")); 2270 } 2271 } 2272 { //http://ircv3.net/specs/extensions/chghost-3.2.html 2273 auto client = spawnNoBufferClient(); 2274 2275 User[] users; 2276 client.onChgHost = (const User user, const User newUser, const MessageMetadata) { 2277 users ~= user; 2278 users ~= newUser; 2279 }; 2280 2281 setupFakeConnection(client); 2282 client.put(":nick!user@host JOIN #test"); 2283 assert("nick" in client.internalAddressList); 2284 assert(client.internalAddressList["nick"] == User("nick!user@host")); 2285 client.put(":nick!user@host CHGHOST user new.host.goes.here"); 2286 assert(users[0] == User("nick!user@host")); 2287 assert(users[1] == User("nick!user@new.host.goes.here")); 2288 assert(client.internalAddressList["nick"] == User("nick!user@new.host.goes.here")); 2289 client.put(":nick!user@host CHGHOST newuser host"); 2290 assert(users[2] == User("nick!user@host")); 2291 assert(users[3] == User("nick!newuser@host")); 2292 assert(client.internalAddressList["nick"] == User("nick!newuser@host")); 2293 client.put(":nick!user@host CHGHOST newuser new.host.goes.here"); 2294 assert(users[4] == User("nick!user@host")); 2295 assert(users[5] == User("nick!newuser@new.host.goes.here")); 2296 assert(client.internalAddressList["nick"] == User("nick!newuser@new.host.goes.here")); 2297 client.put(":tim!~toolshed@backyard CHGHOST b ckyard"); 2298 assert(users[6] == User("tim!~toolshed@backyard")); 2299 assert(users[7] == User("tim!b@ckyard")); 2300 assert(client.internalAddressList["tim"] == User("tim!b@ckyard")); 2301 client.put(":tim!b@ckyard CHGHOST ~toolshed backyard"); 2302 assert(users[8] == User("tim!b@ckyard")); 2303 assert(users[9] == User("tim!~toolshed@backyard")); 2304 assert(client.internalAddressList["tim"] == User("tim!~toolshed@backyard")); 2305 } 2306 { //PING? PONG! 2307 auto client = spawnNoBufferClient(); 2308 2309 setupFakeConnection(client); 2310 client.put("PING :words"); 2311 auto lineByLine = client.output.data.lineSplitter(); 2312 assert(lineByLine.array[$-1] == "PONG :words"); 2313 } 2314 { //echo-message http://ircv3.net/specs/extensions/echo-message-3.2.html 2315 auto client = spawnNoBufferClient(); 2316 Message[] messages; 2317 client.onMessage = (const User, const Target, const Message msg, const MessageMetadata) { 2318 messages ~= msg; 2319 }; 2320 setupFakeConnection(client); 2321 client.msg("Attila", "hi"); 2322 client.put(":"~testUser.text~" PRIVMSG Attila :hi"); 2323 assert(messages.length > 0); 2324 assert(messages[0].isEcho); 2325 2326 client.msg("#ircv3", "back from \x02lunch\x0F"); 2327 client.put(":"~testUser.text~" PRIVMSG #ircv3 :back from lunch"); 2328 assert(messages.length > 1); 2329 assert(messages[1].isEcho); 2330 } 2331 { //Test self-tracking 2332 auto client = spawnNoBufferClient(); 2333 setupFakeConnection(client); 2334 assert(client.me.nickname == testUser.nickname); 2335 client.changeNickname("Testface"); 2336 client.put(":"~testUser.nickname~" NICK Testface"); 2337 assert(client.me.nickname == "Testface"); 2338 } 2339 } 2340 @system unittest { 2341 { //QUIT and invalidation check 2342 import core.exception : AssertError; 2343 import std.exception : assertThrown; 2344 auto client = spawnNoBufferClient(); 2345 2346 setupFakeConnection(client); 2347 client.quit("I'm out"); 2348 auto lineByLine = client.output.data.lineSplitter(); 2349 assert(lineByLine.array[$-1] == "QUIT :I'm out"); 2350 client.put(":"~testUser.nickname~" QUIT"); 2351 assert(!client.isValid); 2352 assertThrown!AssertError(client.put("PING :hahahaha")); 2353 } 2354 } 2355 @safe unittest { 2356 { //NAMES 2357 auto client = spawnNoBufferClient(); 2358 NamesReply[] replies; 2359 client.onNamesReply = (const NamesReply reply, const MessageMetadata) { 2360 replies ~= reply; 2361 }; 2362 2363 setupFakeConnection(client); 2364 2365 client.names(); 2366 client.put(":localhost 353 someone = #channel :User1 User2 @User3 +User4"); 2367 client.put(":localhost 353 someone @ #channel2 :User5 User2 @User6 +User7"); 2368 client.put(":localhost 353 someone * #channel3 :User1 User2 @User3 +User4"); 2369 client.put(":localhost 366 someone :End of NAMES list"); 2370 assert(replies.length == 3); 2371 assert(replies[0].chanFlag == NamReplyFlag.public_); 2372 assert(replies[1].chanFlag == NamReplyFlag.secret); 2373 assert(replies[2].chanFlag == NamReplyFlag.private_); 2374 } 2375 { //WATCH stuff 2376 auto client = spawnNoBufferClient(); 2377 User[] users; 2378 SysTime[] times; 2379 client.onUserOnline = (const User user, const SysTime time, const MessageMetadata) { 2380 users ~= user; 2381 times ~= time; 2382 }; 2383 setupFakeConnection(client); 2384 client.put(":localhost 600 someone someoneElse someIdent example.net 911248013 :logged on"); 2385 2386 assert(users.length == 1); 2387 assert(users[0] == User("someoneElse!someIdent@example.net")); 2388 assert(times.length == 1); 2389 assert(times[0] == SysTime(DateTime(1998, 11, 16, 20, 26, 53), UTC())); 2390 } 2391 { //LUSER stuff 2392 auto client = spawnNoBufferClient(); 2393 bool lUserMeReceived; 2394 bool lUserChannelsReceived; 2395 bool lUserOpReceived; 2396 bool lUserClientReceived; 2397 LUserMe lUserMe; 2398 LUserClient lUserClient; 2399 LUserOp lUserOp; 2400 LUserChannels lUserChannels; 2401 client.onLUserMe = (const LUserMe param, const MessageMetadata) { 2402 assert(!lUserMeReceived); 2403 lUserMeReceived = true; 2404 lUserMe = param; 2405 }; 2406 client.onLUserChannels = (const LUserChannels param, const MessageMetadata) { 2407 assert(!lUserChannelsReceived); 2408 lUserChannelsReceived = true; 2409 lUserChannels = param; 2410 }; 2411 client.onLUserOp = (const LUserOp param, const MessageMetadata) { 2412 assert(!lUserOpReceived); 2413 lUserOpReceived = true; 2414 lUserOp = param; 2415 }; 2416 client.onLUserClient = (const LUserClient param, const MessageMetadata) { 2417 assert(!lUserClientReceived); 2418 lUserClientReceived = true; 2419 lUserClient = param; 2420 }; 2421 setupFakeConnection(client); 2422 client.lUsers(); 2423 client.put(":localhost 251 someone :There are 8 users and 0 invisible on 2 servers"); 2424 client.put(":localhost 252 someone 1 :operator(s) online"); 2425 client.put(":localhost 254 someone 1 :channels formed"); 2426 client.put(":localhost 255 someone :I have 1 clients and 1 servers"); 2427 2428 assert(lUserMeReceived); 2429 assert(lUserChannelsReceived); 2430 assert(lUserOpReceived); 2431 assert(lUserClientReceived); 2432 2433 assert(lUserMe.message == "I have 1 clients and 1 servers"); 2434 assert(lUserClient.message == "There are 8 users and 0 invisible on 2 servers"); 2435 assert(lUserOp.numOperators == 1); 2436 assert(lUserOp.message == "operator(s) online"); 2437 assert(lUserChannels.numChannels == 1); 2438 assert(lUserChannels.message == "channels formed"); 2439 } 2440 { //PRIVMSG and NOTICE stuff 2441 auto client = spawnNoBufferClient(); 2442 Tuple!(const User, "user", const Target, "target", const Message, "message")[] messages; 2443 client.onMessage = (const User user, const Target target, const Message msg, const MessageMetadata) { 2444 messages ~= tuple!("user", "target", "message")(user, target, msg); 2445 }; 2446 2447 setupFakeConnection(client); 2448 2449 client.put(":someoneElse!somebody@somewhere PRIVMSG someone :words go here"); 2450 assert(messages.length == 1); 2451 with (messages[0]) { 2452 assert(user == User("someoneElse!somebody@somewhere")); 2453 assert(!target.isChannel); 2454 assert(target.isNickname); 2455 assert(target == User("someone")); 2456 assert(message == "words go here"); 2457 assert(message.isReplyable); 2458 assert(!message.isEcho); 2459 } 2460 client.put(":ohno!it's@me PRIVMSG #someplace :more words go here"); 2461 assert(messages.length == 2); 2462 with (messages[1]) { 2463 assert(user == User("ohno!it's@me")); 2464 assert(target.isChannel); 2465 assert(!target.isNickname); 2466 assert(target == Channel("#someplace")); 2467 assert(message == "more words go here"); 2468 assert(message.isReplyable); 2469 assert(!message.isEcho); 2470 } 2471 2472 client.put(":someoneElse2!somebody2@somewhere2 NOTICE someone :words don't go here"); 2473 assert(messages.length == 3); 2474 with(messages[2]) { 2475 assert(user == User("someoneElse2!somebody2@somewhere2")); 2476 assert(!target.isChannel); 2477 assert(target.isNickname); 2478 assert(target == User("someone")); 2479 assert(message == "words don't go here"); 2480 assert(!message.isReplyable); 2481 assert(!message.isEcho); 2482 } 2483 2484 client.put(":ohno2!it's2@me4 NOTICE #someplaceelse :more words might go here"); 2485 assert(messages.length == 4); 2486 with(messages[3]) { 2487 assert(user == User("ohno2!it's2@me4")); 2488 assert(target.isChannel); 2489 assert(!target.isNickname); 2490 assert(target == Channel("#someplaceelse")); 2491 assert(message == "more words might go here"); 2492 assert(!message.isReplyable); 2493 assert(!message.isEcho); 2494 } 2495 2496 client.put(":someoneElse2!somebody2@somewhere2 NOTICE someone :\x01ACTION did the thing\x01"); 2497 assert(messages.length == 5); 2498 with(messages[4]) { 2499 assert(user == User("someoneElse2!somebody2@somewhere2")); 2500 assert(!target.isChannel); 2501 assert(target.isNickname); 2502 assert(target == User("someone")); 2503 assert(message.isCTCP); 2504 assert(message.ctcpArgs == "did the thing"); 2505 assert(message.ctcpCommand == "ACTION"); 2506 assert(!message.isReplyable); 2507 assert(!message.isEcho); 2508 } 2509 2510 client.put(":ohno2!it's2@me4 NOTICE #someplaceelse :\x01ACTION did not do the thing\x01"); 2511 assert(messages.length == 6); 2512 with(messages[5]) { 2513 assert(user == User("ohno2!it's2@me4")); 2514 assert(target.isChannel); 2515 assert(!target.isNickname); 2516 assert(target == Channel("#someplaceelse")); 2517 assert(message.isCTCP); 2518 assert(message.ctcpArgs == "did not do the thing"); 2519 assert(message.ctcpCommand == "ACTION"); 2520 assert(!message.isReplyable); 2521 assert(!message.isEcho); 2522 } 2523 2524 client.msg("#channel", "ohai"); 2525 client.notice("#channel", "ohi"); 2526 client.msg("someoneElse", "ohay"); 2527 client.notice("someoneElse", "ohello"); 2528 Target channelTarget; 2529 channelTarget.channel = Channel("#channel"); 2530 Target userTarget; 2531 userTarget.user = User("someoneElse"); 2532 client.msg(channelTarget, Message("ohai")); 2533 client.notice(channelTarget, Message("ohi")); 2534 client.msg(userTarget, Message("ohay")); 2535 client.notice(userTarget, Message("ohello")); 2536 auto lineByLine = client.output.data.lineSplitter(); 2537 foreach (i; 0..5) //skip the initial handshake 2538 lineByLine.popFront(); 2539 assert(lineByLine.array == ["PRIVMSG #channel :ohai", "NOTICE #channel :ohi", "PRIVMSG someoneElse :ohay", "NOTICE someoneElse :ohello", "PRIVMSG #channel :ohai", "NOTICE #channel :ohi", "PRIVMSG someoneElse :ohay", "NOTICE someoneElse :ohello"]); 2540 } 2541 { //PING? PONG! 2542 auto client = spawnNoBufferClient(); 2543 2544 setupFakeConnection(client); 2545 client.ping("hooray"); 2546 client.put(":localhost PONG localhost :hooray"); 2547 2548 client.put(":localhost PING :hoorah"); 2549 2550 auto lineByLine = client.output.data.lineSplitter(); 2551 assert(lineByLine.array[$-2] == "PING :hooray"); 2552 assert(lineByLine.array[$-1] == "PONG :hoorah"); 2553 } 2554 { //Mode change test 2555 auto client = spawnNoBufferClient(); 2556 Tuple!(const User, "user", const Target, "target", const ModeChange, "change")[] changes; 2557 2558 client.onMode = (const User user, const Target target, const ModeChange mode, const MessageMetadata) { 2559 changes ~= tuple!("user", "target", "change")(user, target, mode); 2560 }; 2561 2562 setupFakeConnection(client); 2563 client.join("#test"); 2564 client.put(":"~testUser.text~" JOIN #test "~testUser.nickname); 2565 client.put(":someone!ident@host JOIN #test"); 2566 client.put(":someoneElse!user@host2 MODE #test +s"); 2567 client.put(":someoneElse!user@host2 MODE #test -s"); 2568 client.put(":someoneElse!user@host2 MODE #test +kp 2"); 2569 client.put(":someoneElse!user@host2 MODE someone +r"); 2570 client.put(":someoneElse!user@host2 MODE someone +k"); 2571 2572 assert(changes.length == 6); 2573 with (changes[0]) { 2574 assert(target == Channel("#test")); 2575 assert(user == User("someoneElse!user@host2")); 2576 } 2577 with (changes[1]) { 2578 assert(target == Channel("#test")); 2579 assert(user == User("someoneElse!user@host2")); 2580 } 2581 with (changes[2]) { 2582 assert(target == Channel("#test")); 2583 assert(user == User("someoneElse!user@host2")); 2584 } 2585 with (changes[3]) { 2586 assert(target == Channel("#test")); 2587 assert(user == User("someoneElse!user@host2")); 2588 } 2589 with (changes[4]) { 2590 assert(target == User("someone")); 2591 assert(user == User("someoneElse!user@host2")); 2592 } 2593 with (changes[5]) { 2594 assert(target == User("someone")); 2595 assert(user == User("someoneElse!user@host2")); 2596 } 2597 } 2598 { //client join stuff 2599 auto client = spawnNoBufferClient(); 2600 client.join("#test"); 2601 assert(client.output.data.lineSplitter.array[$-1] == "JOIN #test"); 2602 client.join(Channel("#test2")); 2603 assert(client.output.data.lineSplitter.array[$-1] == "JOIN #test2"); 2604 client.join("#test3", "key"); 2605 assert(client.output.data.lineSplitter.array[$-1] == "JOIN #test3 key"); 2606 client.join("#test4", "key2"); 2607 assert(client.output.data.lineSplitter.array[$-1] == "JOIN #test4 key2"); 2608 } 2609 { //account-tag examples from http://ircv3.net/specs/extensions/account-tag-3.2.html 2610 auto client = spawnNoBufferClient(); 2611 User[] privmsgUsers; 2612 client.onMessage = (const User user, const Target, const Message, const MessageMetadata) { 2613 privmsgUsers ~= user; 2614 }; 2615 setupFakeConnection(client); 2616 2617 client.put(":user PRIVMSG #atheme :Hello everyone."); 2618 client.put(":user ACCOUNT hax0r"); 2619 client.put("@account=hax0r :user PRIVMSG #atheme :Now I'm logged in."); 2620 client.put("@account=hax0r :user ACCOUNT bob"); 2621 client.put("@account=bob :user PRIVMSG #atheme :I switched accounts."); 2622 with(privmsgUsers[0]) { 2623 assert(account.isNull); 2624 } 2625 with(privmsgUsers[1]) { 2626 assert(account.get == "hax0r"); 2627 } 2628 with(privmsgUsers[2]) { 2629 assert(account.get == "bob"); 2630 } 2631 assert(client.internalAddressList["user"].account == "bob"); 2632 } 2633 { //account-notify - http://ircv3.net/specs/extensions/account-notify-3.1.html 2634 auto client = spawnNoBufferClient(); 2635 setupFakeConnection(client); 2636 client.put(":nick!user@host ACCOUNT accountname"); 2637 assert(client.internalAddressList["nick"].account.get == "accountname"); 2638 client.put(":nick!user@host ACCOUNT *"); 2639 assert(client.internalAddressList["nick"].account.isNull); 2640 } 2641 { //monitor - http://ircv3.net/specs/core/monitor-3.2.html 2642 auto client = spawnNoBufferClient(); 2643 initializeWithCaps(client, [Capability("MONITOR")]); 2644 2645 assert(client.monitorIsEnabled); 2646 2647 client.monitorAdd([User("Someone")]); 2648 client.monitorRemove([User("Someone")]); 2649 client.monitorClear(); 2650 client.monitorList(); 2651 client.monitorStatus(); 2652 2653 const lineByLine = client.output.data.lineSplitter().drop(5).array; 2654 assert(lineByLine == ["MONITOR + Someone", "MONITOR - Someone", "MONITOR C", "MONITOR L", "MONITOR S"]); 2655 } 2656 { //No MOTD test 2657 auto client = spawnNoBufferClient(); 2658 bool errorReceived; 2659 client.onError = (const IRCError error, const MessageMetadata) { 2660 assert(!errorReceived); 2661 errorReceived = true; 2662 assert(error.type == ErrorType.noMOTD); 2663 }; 2664 setupFakeConnection(client); 2665 client.put("422 someone :MOTD File is missing"); 2666 assert(errorReceived); 2667 } 2668 { //NICK tests 2669 auto client = spawnNoBufferClient(); 2670 Tuple!(const User, "old", const User, "new_")[] nickChanges; 2671 client.onNick = (const User old, const User new_, const MessageMetadata) { 2672 nickChanges ~= tuple!("old", "new_")(old, new_); 2673 }; 2674 2675 setupFakeConnection(client); 2676 client.put(":WiZ JOIN #testchan"); 2677 client.put(":dan- JOIN #testchan"); 2678 2679 2680 client.put(":WiZ NICK Kilroy"); 2681 2682 assert(nickChanges.length == 1); 2683 with(nickChanges[0]) { 2684 assert(old.nickname == "WiZ"); 2685 assert(new_.nickname == "Kilroy"); 2686 } 2687 2688 assert("Kilroy" in client.internalAddressList); 2689 assert("Kilroy" in client.channels["#testchan"].users); 2690 assert("WiZ" !in client.channels["#testchan"].users); 2691 2692 client.put(":dan-!d@localhost NICK Mamoped"); 2693 2694 assert(nickChanges.length == 2); 2695 with(nickChanges[1]) { 2696 assert(old.nickname == "dan-"); 2697 assert(new_.nickname == "Mamoped"); 2698 } 2699 2700 assert("Mamoped" in client.internalAddressList); 2701 assert("Mamoped" in client.channels["#testchan"].users); 2702 assert("dan-" !in client.channels["#testchan"].users); 2703 2704 //invalid, so we shouldn't see anything 2705 client.put(":a NICK"); 2706 assert(nickChanges.length == 2); 2707 } 2708 { //QUIT tests 2709 auto client = spawnNoBufferClient(); 2710 2711 Tuple!(const User, "user", string, "message")[] quits; 2712 client.onQuit = (const User user, const string msg, const MessageMetadata) { 2713 quits ~= tuple!("user", "message")(user, msg); 2714 }; 2715 2716 setupFakeConnection(client); 2717 2718 client.put(":dan-!d@localhost QUIT :Quit: Bye for now!"); 2719 assert(quits.length == 1); 2720 with (quits[0]) { 2721 assert(user == User("dan-!d@localhost")); 2722 assert(message == "Quit: Bye for now!"); 2723 } 2724 client.put(":nomessage QUIT"); 2725 assert(quits.length == 2); 2726 with(quits[1]) { 2727 assert(user == User("nomessage")); 2728 assert(message == ""); 2729 } 2730 } 2731 { //Batch stuff 2732 auto client = spawnNoBufferClient(); 2733 2734 Tuple!(const User, "user", const MessageMetadata, "metadata")[] quits; 2735 client.onQuit = (const User user, const string, const MessageMetadata metadata) { 2736 quits ~= tuple!("user", "metadata")(user, metadata); 2737 }; 2738 2739 setupFakeConnection(client); 2740 2741 client.put(`:irc.host BATCH +yXNAbvnRHTRBv netsplit irc.hub other.host`); 2742 client.put(`@batch=yXNAbvnRHTRBv :aji!a@a QUIT :irc.hub other.host`); 2743 client.put(`@batch=yXNAbvnRHTRBv :nenolod!a@a QUIT :irc.hub other.host`); 2744 client.put(`:nick!user@host PRIVMSG #channel :This is not in batch, so processed immediately`); 2745 client.put(`@batch=yXNAbvnRHTRBv :jilles!a@a QUIT :irc.hub other.host`); 2746 2747 assert(quits.length == 0); 2748 2749 client.put(`:irc.host BATCH -yXNAbvnRHTRBv`); 2750 2751 2752 assert(quits.length == 3); 2753 with(quits[0]) { 2754 assert(metadata.batch.type == "netsplit"); 2755 } 2756 client.put(`:SomeUser!Someplace@Somewhere BATCH +2 something`); 2757 client.put(`@batch=2 :SomeUser!Someplace@Somewhere PRIVMSG #channel :hello`); 2758 client.put(`@batch=2 :SomeUser!Someplace@Somewhere PRIVMSG #channel :hello again`); 2759 client.put(`:SomeUser!Someplace@Somewhere BATCH -2`); 2760 } 2761 { //INVITE tests 2762 auto client = spawnNoBufferClient(); 2763 2764 Tuple!(const User, "inviter", const User, "invited", const Channel, "channel")[] invites; 2765 client.onInvite = (const User inviter, const User invited, const Channel channel, const MessageMetadata) { 2766 invites ~= tuple!("inviter", "invited", "channel")(inviter, invited, channel); 2767 }; 2768 2769 setupFakeConnection(client); 2770 2771 //Ensure the internal address list gets used for invited users as well 2772 client.internalAddressList.update(User("Wiz!ident@host")); 2773 2774 client.put(":Angel INVITE Wiz #Dust"); 2775 assert(invites.length == 1); 2776 with(invites[0]) { 2777 assert(inviter.nickname == "Angel"); 2778 assert(invited.nickname == "Wiz"); 2779 assert(invited.host == "host"); 2780 assert(channel == Channel("#Dust")); 2781 } 2782 2783 client.put(":ChanServ!ChanServ@example.com INVITE Attila #channel"); 2784 assert(invites.length == 2); 2785 with(invites[1]) { 2786 assert(inviter.nickname == "ChanServ"); 2787 assert(invited.nickname == "Attila"); 2788 assert(channel == Channel("#channel")); 2789 } 2790 } 2791 { //VERSION tests 2792 auto client = spawnNoBufferClient(); 2793 2794 VersionReply[] replies; 2795 client.onVersionReply = (const VersionReply reply, const MessageMetadata) { 2796 replies ~= reply; 2797 }; 2798 2799 setupFakeConnection(client); 2800 2801 client.version_(); 2802 client.put(format!":localhost 351 %s example-1.0 localhost :not a beta"(testUser.nickname)); 2803 with (replies[0]) { 2804 assert(version_ == "example-1.0"); 2805 assert(server == "localhost"); 2806 assert(comments == "not a beta"); 2807 } 2808 client.version_("*.example"); 2809 client.put(format!":localhost 351 %s example-1.0 test.example :not a beta"(testUser.nickname)); 2810 with (replies[1]) { 2811 assert(version_ == "example-1.0"); 2812 assert(server == "test.example"); 2813 assert(comments == "not a beta"); 2814 } 2815 } 2816 { //SASL test 2817 auto client = spawnNoBufferClient(); 2818 client.saslMechs = [new SASLPlain("jilles", "jilles", "sesame")]; 2819 client.put(":localhost CAP * LS :sasl"); 2820 client.put(":localhost CAP whoever ACK :sasl"); 2821 client.put("AUTHENTICATE +"); 2822 client.put(":localhost 900 "~testUser.nickname~" "~testUser.text~" "~testUser.nickname~" :You are now logged in as "~testUser.nickname); 2823 client.put(":localhost 903 "~testUser.nickname~" :SASL authentication successful"); 2824 2825 assert(client.output.data.canFind("AUTHENTICATE amlsbGVzAGppbGxlcwBzZXNhbWU=")); 2826 assert(client.isAuthenticated == true); 2827 assert(client.me.account.get == testUser.nickname); 2828 } 2829 { //SASL 3.2 test 2830 auto client = spawnNoBufferClient(); 2831 client.saslMechs = [new SASLPlain("jilles", "jilles", "sesame")]; 2832 client.put(":localhost CAP * LS :sasl=UNKNOWN,PLAIN,EXTERNAL"); 2833 client.put(":localhost CAP whoever ACK :sasl"); 2834 client.put("AUTHENTICATE +"); 2835 client.put(":localhost 900 "~testUser.nickname~" "~testUser.text~" "~testUser.nickname~" :You are now logged in as "~testUser.nickname); 2836 client.put(":localhost 903 "~testUser.nickname~" :SASL authentication successful"); 2837 2838 assert(client.output.data.canFind("AUTHENTICATE amlsbGVzAGppbGxlcwBzZXNhbWU=")); 2839 assert(client.isAuthenticated == true); 2840 assert(client.me.account.get == testUser.nickname); 2841 } 2842 { //SASL 3.2 test 2843 auto client = spawnNoBufferClient(); 2844 client.saslMechs = [new SASLExternal]; 2845 client.put(":localhost CAP * LS :sasl=UNKNOWN,EXTERNAL"); 2846 client.put(":localhost CAP whoever ACK :sasl"); 2847 client.put("AUTHENTICATE +"); 2848 client.put(":localhost 900 "~testUser.nickname~" "~testUser.text~" "~testUser.nickname~" :You are now logged in as "~testUser.nickname); 2849 client.put(":localhost 903 "~testUser.nickname~" :SASL authentication successful"); 2850 2851 assert(client.output.data.canFind("AUTHENTICATE +")); 2852 assert(client.isAuthenticated == true); 2853 assert(client.me.account.get == testUser.nickname); 2854 } 2855 { //SASL 3.2 test (bogus) 2856 auto client = spawnNoBufferClient(); 2857 client.saslMechs = [new SASLPlain("jilles", "jilles", "sesame")]; 2858 client.put(":localhost CAP * LS :sasl=UNKNOWN,EXTERNAL"); 2859 client.put(":localhost CAP whoever ACK :sasl"); 2860 client.put("AUTHENTICATE +"); 2861 client.put(":localhost 900 "~testUser.nickname~" "~testUser.text~" "~testUser.nickname~" :You are now logged in as "~testUser.nickname); 2862 client.put(":localhost 903 "~testUser.nickname~" :SASL authentication successful"); 2863 2864 assert(!client.output.data.canFind("AUTHENTICATE amlsbGVzAGppbGxlcwBzZXNhbWU=")); 2865 assert(client.isAuthenticated == false); 2866 //assert(client.me.account.get.isNull); 2867 } 2868 { //SASL post-registration test 2869 auto client = spawnNoBufferClient(); 2870 client.saslMechs = [new SASLExternal()]; 2871 setupFakeConnection(client); 2872 client.capList(); 2873 client.put(":localhost CAP * LIST :sasl=UNKNOWN,PLAIN,EXTERNAL"); 2874 } 2875 { //KICK tests 2876 auto client = spawnNoBufferClient(); 2877 Tuple!(const User, "kickedBy", const User, "kicked", const Channel, "channel", string, "message")[] kicks; 2878 client.onKick = (const User kickedBy, const Channel channel, const User kicked, const string message, const MessageMetadata) { 2879 kicks ~= tuple!("kickedBy", "kicked", "channel", "message")(kickedBy, kicked, channel, message); 2880 }; 2881 setupFakeConnection(client); 2882 client.kick(Channel("#test"), User("Example"), "message"); 2883 auto lineByLine = client.output.data.lineSplitter(); 2884 assert(lineByLine.array[$-1] == "KICK #test Example :message"); 2885 2886 client.put(":WiZ KICK #Finnish John"); 2887 2888 assert(kicks.length == 1); 2889 with(kicks[0]) { 2890 assert(kickedBy == User("WiZ")); 2891 assert(channel == Channel("#Finnish")); 2892 assert(kicked == User("John")); 2893 assert(message == ""); 2894 } 2895 2896 client.put(":Testo KICK #example User :Now with kick message!"); 2897 2898 assert(kicks.length == 2); 2899 with(kicks[1]) { 2900 assert(kickedBy == User("Testo")); 2901 assert(channel == Channel("#example")); 2902 assert(kicked == User("User")); 2903 assert(message == "Now with kick message!"); 2904 } 2905 2906 client.put(":WiZ!jto@tolsun.oulu.fi KICK #Finnish John"); 2907 2908 assert(kicks.length == 3); 2909 with(kicks[2]) { 2910 assert(kickedBy == User("WiZ!jto@tolsun.oulu.fi")); 2911 assert(channel == Channel("#Finnish")); 2912 assert(kicked == User("John")); 2913 assert(message == ""); 2914 } 2915 2916 client.put(":User KICK"); 2917 assert(kicks.length == 3); 2918 2919 client.put(":User KICK #channel"); 2920 assert(kicks.length == 3); 2921 } 2922 { //REHASH tests 2923 auto client = spawnNoBufferClient(); 2924 RehashingReply[] replies; 2925 client.onServerRehashing = (const RehashingReply reply, const MessageMetadata) { 2926 replies ~= reply; 2927 }; 2928 IRCError[] errors; 2929 client.onError = (const IRCError error, const MessageMetadata) { 2930 errors ~= error; 2931 }; 2932 2933 setupFakeConnection(client); 2934 client.rehash(); 2935 auto lineByLine = client.output.data.lineSplitter(); 2936 assert(lineByLine.array[$-1] == "REHASH"); 2937 client.put(":localhost 382 Someone ircd.conf :Rehashing config"); 2938 2939 assert(replies.length == 1); 2940 with (replies[0]) { 2941 import virc.common : User; 2942 assert(me == User("Someone")); 2943 assert(configFile == "ircd.conf"); 2944 assert(message == "Rehashing config"); 2945 } 2946 2947 client.put(":localhost 382 Nope"); 2948 2949 assert(replies.length == 1); 2950 2951 client.put(":localhost 723 Someone rehash :Insufficient oper privileges"); 2952 client.put(":localhost 723 Someone"); 2953 assert(errors.length == 2); 2954 with(errors[0]) { 2955 assert(type == ErrorType.noPrivs); 2956 } 2957 with(errors[1]) { 2958 assert(type == ErrorType.malformed); 2959 } 2960 } 2961 { //ISON tests 2962 auto client = spawnNoBufferClient(); 2963 const(User)[] users; 2964 client.onIsOn = (const User user, const MessageMetadata) { 2965 users ~= user; 2966 }; 2967 setupFakeConnection(client); 2968 2969 client.isOn("phone", "trillian", "WiZ", "jarlek", "Avalon", "Angel", "Monstah"); 2970 2971 client.put(":localhost 303 Someone :trillian"); 2972 client.put(":localhost 303 Someone :WiZ"); 2973 client.put(":localhost 303 Someone :jarlek"); 2974 client.put(":localhost 303 Someone :Angel"); 2975 client.put(":localhost 303 Someone :Monstah"); 2976 2977 assert(users.length == 5); 2978 assert(users[0].nickname == "trillian"); 2979 assert(users[1].nickname == "WiZ"); 2980 assert(users[2].nickname == "jarlek"); 2981 assert(users[3].nickname == "Angel"); 2982 assert(users[4].nickname == "Monstah"); 2983 } 2984 { //OPER tests 2985 auto client = spawnNoBufferClient(); 2986 bool received; 2987 client.onYoureOper = (const MessageMetadata) { 2988 received = true; 2989 }; 2990 setupFakeConnection(client); 2991 2992 client.oper("foo", "bar"); 2993 auto lineByLine = client.output.data.lineSplitter(); 2994 assert(lineByLine.array[$-1] == "OPER foo bar"); 2995 client.put(":localhost 381 Someone :You are now an IRC operator"); 2996 assert(received); 2997 } 2998 { //SQUIT tests 2999 auto client = spawnNoBufferClient(); 3000 IRCError[] errors; 3001 client.onError = (const IRCError error, const MessageMetadata) { 3002 errors ~= error; 3003 }; 3004 setupFakeConnection(client); 3005 3006 client.squit("badserver.example.net", "Bad link"); 3007 auto lineByLine = client.output.data.lineSplitter(); 3008 assert(lineByLine.array[$-1] == "SQUIT badserver.example.net :Bad link"); 3009 client.put(":localhost 481 Someone :Permission Denied- You're not an IRC operator"); 3010 client.put(":localhost 402 Someone badserver.example.net :No such server"); 3011 client.put(":localhost 402 Someone"); 3012 assert(errors.length == 3); 3013 with(errors[0]) { 3014 assert(type == ErrorType.noPrivileges); 3015 } 3016 with(errors[1]) { 3017 assert(type == ErrorType.noSuchServer); 3018 } 3019 with(errors[2]) { 3020 assert(type == ErrorType.malformed); 3021 } 3022 } 3023 { //AWAY tests 3024 auto client = spawnNoBufferClient(); 3025 Tuple!(const User, "user", string, "message")[] aways; 3026 client.onOtherUserAwayReply = (const User awayUser, const string msg, const MessageMetadata) { 3027 aways ~= tuple!("user", "message")(awayUser, msg); 3028 }; 3029 bool unAwayReceived; 3030 client.onUnAwayReply = (const User, const MessageMetadata) { 3031 unAwayReceived = true; 3032 }; 3033 bool awayReceived; 3034 client.onAwayReply = (const User, const MessageMetadata) { 3035 awayReceived = true; 3036 }; 3037 setupFakeConnection(client); 3038 3039 client.away("Laughing at salads"); 3040 client.put(":localhost 306 Someone :You have been marked as being away"); 3041 assert(client.isAway); 3042 assert(awayReceived); 3043 3044 client.away(); 3045 client.put(":localhost 305 Someone :You are no longer marked as being away"); 3046 assert(!client.isAway); 3047 assert(unAwayReceived); 3048 3049 client.put(":localhost 301 Someone AwayUser :User on fire"); 3050 3051 assert(aways.length == 1); 3052 with (aways[0]) { 3053 assert(user == User("AwayUser")); 3054 assert(message == "User on fire"); 3055 } 3056 } 3057 { //ADMIN tests 3058 auto client = spawnNoBufferClient(); 3059 3060 setupFakeConnection(client); 3061 3062 client.admin("localhost"); 3063 client.admin(); 3064 auto lineByLine = client.output.data.lineSplitter(); 3065 assert(lineByLine.array[$-2] == "ADMIN localhost"); 3066 assert(lineByLine.array[$-1] == "ADMIN"); 3067 client.put(":localhost 256 Someone :Administrative info for localhost"); 3068 client.put(":localhost 257 Someone :Name - Admin"); 3069 client.put(":localhost 258 Someone :Nickname - Admin"); 3070 client.put(":localhost 259 Someone :E-Mail - Admin@localhost"); 3071 } 3072 { //WHOIS tests 3073 auto client = spawnNoBufferClient(); 3074 const(WhoisResponse)[] responses; 3075 client.onWhois = (const User, const WhoisResponse whois) { 3076 responses ~= whois; 3077 }; 3078 setupFakeConnection(client); 3079 client.whois("someoneElse"); 3080 3081 client.put(":localhost 276 Someone someoneElse :has client certificate 0"); 3082 client.put(":localhost 311 Someone someoneElse someUsername someHostname * :Some Real Name"); 3083 client.put(":localhost 312 Someone someoneElse example.net :The real world is out there"); 3084 client.put(":localhost 313 Someone someoneElse :is an IRC operator"); 3085 client.put(":localhost 317 Someone someoneElse 1000 1500000000 :seconds idle, signon time"); 3086 client.put(":localhost 319 Someone someoneElse :+#test #test2"); 3087 client.put(":localhost 330 Someone someoneElse someoneElseAccount :is logged in as"); 3088 client.put(":localhost 378 Someone someoneElse :is connecting from someoneElse@127.0.0.5 127.0.0.5"); 3089 client.put(":localhost 671 Someone someoneElse :is using a secure connection"); 3090 client.put(":localhost 379 Someone someoneElse :is using modes +w"); 3091 client.put(":localhost 307 Someone someoneElse :is a registered nick"); 3092 3093 assert(responses.length == 0); 3094 client.put(":localhost 318 Someone someoneElse :End of /WHOIS list"); 3095 3096 assert(responses.length == 1); 3097 with(responses[0]) { 3098 assert(isSecure); 3099 assert(isOper); 3100 assert(isRegistered); 3101 assert(username.get == "someUsername"); 3102 assert(hostname.get == "someHostname"); 3103 assert(realname.get == "Some Real Name"); 3104 assert(connectedTime.get == SysTime(DateTime(2017, 7, 14, 2, 40, 0), UTC())); 3105 assert(idleTime.get == 1000.seconds); 3106 assert(connectedTo.get == "example.net"); 3107 assert(account.get == "someoneElseAccount"); 3108 assert(channels.length == 2); 3109 assert("#test" in channels); 3110 assert(channels["#test"].prefix == "+"); 3111 assert("#test2" in channels); 3112 assert(channels["#test2"].prefix == ""); 3113 } 3114 } 3115 { //RESTART tests 3116 auto client = spawnNoBufferClient(); 3117 setupFakeConnection(client); 3118 3119 client.restart(); 3120 auto lineByLine = client.output.data.lineSplitter(); 3121 assert(lineByLine.array[$-1] == "RESTART"); 3122 } 3123 { //WALLOPS tests 3124 auto client = spawnNoBufferClient(); 3125 string[] messages; 3126 client.onWallops = (const User, const string msg, const MessageMetadata) { 3127 messages ~= msg; 3128 }; 3129 setupFakeConnection(client); 3130 3131 client.wallops("Test message!"); 3132 auto lineByLine = client.output.data.lineSplitter(); 3133 assert(lineByLine.array[$-1] == "WALLOPS :Test message!"); 3134 3135 client.put(":OtherUser!someone@somewhere WALLOPS :Test message reply!"); 3136 assert(messages.length == 1); 3137 assert(messages[0] == "Test message reply!"); 3138 3139 } 3140 { //CTCP tests 3141 auto client = spawnNoBufferClient(); 3142 setupFakeConnection(client); 3143 client.ctcp(Target(User("test")), "ping"); 3144 client.ctcp(Target(User("test")), "action", "does the thing."); 3145 client.ctcpReply(Target(User("test")), "ping", "1000000000"); 3146 3147 auto lineByLine = client.output.data.lineSplitter(); 3148 assert(lineByLine.array[$-3] == "PRIVMSG test :\x01ping\x01"); 3149 assert(lineByLine.array[$-2] == "PRIVMSG test :\x01action does the thing.\x01"); 3150 assert(lineByLine.array[$-1] == "NOTICE test :\x01ping 1000000000\x01"); 3151 } 3152 { //TOPIC tests 3153 auto client = spawnNoBufferClient(); 3154 Tuple!(const User, "user", const Channel, "channel", string, "message")[] topics; 3155 IRCError[] errors; 3156 client.onTopicChange = (const User user, const Channel channel, const string msg, const MessageMetadata) { 3157 topics ~= tuple!("user", "channel", "message")(user, channel, msg); 3158 }; 3159 client.onError = (const IRCError error, const MessageMetadata) { 3160 errors ~= error; 3161 }; 3162 3163 setupFakeConnection(client); 3164 client.changeTopic(Target(Channel("#test")), "This is a new topic"); 3165 client.put(":"~testUser.text~" TOPIC #test :This is a new topic"); 3166 client.put(":"~testUser.text~" TOPIC #test"); //Malformed 3167 client.put(":"~testUser.text~" TOPIC"); //Malformed 3168 3169 auto lineByLine = client.output.data.lineSplitter(); 3170 assert(lineByLine.array[$-1] == "TOPIC #test :This is a new topic"); 3171 assert(topics.length == 1); 3172 with(topics[0]) { 3173 assert(channel == Channel("#test")); 3174 assert(message == "This is a new topic"); 3175 } 3176 assert(errors.length == 2); 3177 assert(errors[0].type == ErrorType.malformed); 3178 assert(errors[1].type == ErrorType.malformed); 3179 } 3180 //Request capabilities (IRC v3.2) - Missing prefix 3181 { 3182 auto client = spawnNoBufferClient(); 3183 client.put("CAP * LS :multi-prefix sasl"); 3184 client.put("CAP * ACK :multi-prefix sasl"); 3185 3186 auto lineByLine = client.output.data.lineSplitter; 3187 lineByLine.popFront(); 3188 lineByLine.popFront(); 3189 lineByLine.popFront(); 3190 //sasl not yet supported 3191 assert(lineByLine.front == "CAP REQ :multi-prefix sasl"); 3192 lineByLine.popFront(); 3193 assert(!lineByLine.empty); 3194 assert(lineByLine.front == "CAP END"); 3195 } 3196 { //METADATA tests 3197 auto client = spawnNoBufferClient(); 3198 IRCError[] errors; 3199 client.onError = (const IRCError error, const MessageMetadata md) { 3200 errors ~= error; 3201 }; 3202 string[] subs; 3203 client.onMetadataSubList = (const string str, const MessageMetadata) { 3204 subs ~= str; 3205 }; 3206 initializeWithCaps(client, [Capability("draft/metadata-2", "foo,maxsub=50,maxkey=25,bar"), Capability("draft/metadata-notify-2")]); 3207 3208 3209 assert(client.state.maxMetadataSubscriptions == 50); 3210 assert(client.state.maxMetadataSelfKeys == 25); 3211 3212 client.setMetadata("url", "http://www.example.com"); 3213 assert(client.output.data.lineSplitter().array[$-1] == "METADATA * SET url :http://www.example.com"); 3214 client.put(":irc.example.com 761 * url * :http://www.example.com"); 3215 assert(client.ownMetadata["url"] == "http://www.example.com"); 3216 3217 client.setMetadata("url", "http://www.example.com"); 3218 client.put("FAIL METADATA LIMIT_REACHED :Metadata limit reached"); 3219 assert(errors.length == 1); 3220 with(errors[0]) { 3221 assert(type == ErrorType.standardFail); 3222 } 3223 3224 client.setMetadata(User("user1"), "url", "http://www.example.com"); 3225 assert(client.output.data.lineSplitter().array[$ - 1] == "METADATA user1 SET url :http://www.example.com"); 3226 client.put("FAIL METADATA KEY_NO_PERMISSION url user1 :You do not have permission to set 'url' on 'user1'"); 3227 assert(errors.length == 2); 3228 with(errors[1]) { 3229 assert(type == ErrorType.standardFail); 3230 } 3231 3232 client.setMetadata(Channel("#example"), "url", "http://www.example.com"); 3233 assert(client.output.data.lineSplitter().array[$ - 1] == "METADATA #example SET url :http://www.example.com"); 3234 client.put(":irc.example.com 761 #example url * :http://www.example.com"); 3235 assert(client.channelMetadata[Channel("#example")]["url"] == "http://www.example.com"); 3236 3237 client.setMetadata(User("$a:user"), "url", "http://www.example.com"); 3238 client.put("FAIL METADATA INVALID_TARGET $a:user :Invalid target."); 3239 assert(errors.length == 3); 3240 with(errors[2]) { 3241 assert(type == ErrorType.standardFail); 3242 } 3243 3244 client.setMetadata(User("user1"), "$url$", "http://www.example.com"); 3245 client.put("FAIL METADATA INVALID_KEY $url$ user1 :Invalid key."); 3246 assert(errors.length == 4); 3247 with(errors[3]) { 3248 assert(type == ErrorType.standardFail); 3249 } 3250 3251 client.setMetadata("url", "http://www.example.com"); 3252 client.put("FAIL METADATA RATE_LIMIT url 5 :Rate-limit reached. You're going too fast! Try again in 5 seconds."); 3253 assert(errors.length == 5); 3254 with(errors[4]) { 3255 assert(type == ErrorType.standardFail); 3256 } 3257 3258 client.setMetadata("url", "http://www.example.com"); 3259 client.put("FAIL METADATA RATE_LIMIT url * :Rate-limit reached. You're going too fast!"); 3260 assert(errors.length == 6); 3261 with(errors[5]) { 3262 assert(type == ErrorType.standardFail); 3263 } 3264 3265 client.put(":irc.example.com METADATA user1 account * :user1"); 3266 assert(client.userMetadata[User("user1")]["account"] == "user1"); 3267 3268 client.put(":user1!~user@somewhere.example.com METADATA #example url * :http://www.example.com"); 3269 assert(client.channelMetadata[Channel("#example")]["url"] == "http://www.example.com"); 3270 3271 client.put(":irc.example.com METADATA #example wiki-url * :http://wiki.example.com"); 3272 assert(client.channelMetadata[Channel("#example")]["wiki-url"] == "http://wiki.example.com"); 3273 3274 client.listMetadata(User("user1")); 3275 client.put(":irc.example.com BATCH +VUN2ot metadata"); 3276 client.put("@batch=VUN2ot :irc.example.com 761 user1 url * :http://www.example.com"); 3277 client.put("@batch=VUN2ot :irc.example.com 761 user1 im.xmpp * :user1@xmpp.example.com"); 3278 client.put("@batch=VUN2ot :irc.example.com 761 user1 bot-likeliness-score visible-only-for-admin :42"); 3279 client.put(":irc.example.com BATCH -VUN2ot"); 3280 assert(client.userMetadata[User("user1")]["url"] == "http://www.example.com"); 3281 assert(client.userMetadata[User("user1")]["im.xmpp"] == "user1@xmpp.example.com"); 3282 assert(client.userMetadata[User("user1")]["bot-likeliness-score"] == "42"); 3283 3284 client.userMetadata.remove(User("user1")); 3285 3286 client.getMetadata(User("user1"), "blargh", "splot", "im.xmpp"); 3287 client.put(":irc.example.com BATCH +gWkCiV metadata"); 3288 client.put("@batch=gWkCiV 766 user1 blargh :No matching key"); 3289 client.put("@batch=gWkCiV 766 user1 splot :No matching key"); 3290 client.put("@batch=gWkCiV :irc.example.com 761 user1 im.xmpp * :user1@xmpp.example.com"); 3291 client.put(":irc.example.com BATCH -gWkCiV"); 3292 assert("blargh" !in client.userMetadata[User("user1")]); 3293 assert("splot" !in client.userMetadata[User("user1")]); 3294 assert(client.userMetadata[User("user1")]["im.xmpp"] == "user1@xmpp.example.com"); 3295 assert(errors.length == 8); 3296 with(errors[6]) { 3297 assert(type == ErrorType.keyNotSet); 3298 } 3299 with(errors[7]) { 3300 assert(type == ErrorType.keyNotSet); 3301 } 3302 3303 client.join(Channel("#smallchan")); 3304 client.put(":modernclient!modernclient@example.com JOIN #smallchan"); 3305 client.put(":irc.example.com 353 modernclient @ #smallchan :user1 user2 user3 user4 user5 ..."); 3306 client.put(":irc.example.com 353 modernclient @ #smallchan :user51 user52 user53 user54 ..."); 3307 client.put(":irc.example.com 353 modernclient @ #smallchan :user101 user102 user103 user104 ..."); 3308 client.put(":irc.example.com 353 modernclient @ #smallchan :user151 user152 user153 user154 ..."); 3309 client.put(":irc.example.com 366 modernclient #smallchan :End of /NAMES list."); 3310 client.put(":irc.example.com BATCH +UwZ67M metadata"); 3311 client.put("@batch=UwZ67M :irc.example.com METADATA user2 bar * :second example value "); 3312 client.put("@batch=UwZ67M :irc.example.com METADATA user1 foo * :third example value"); 3313 client.put("@batch=UwZ67M :irc.example.com METADATA user1 bar * :this is another example value"); 3314 client.put("@batch=UwZ67M :irc.example.com METADATA user3 website * :www.example.com"); 3315 client.put(":irc.example.com BATCH -UwZ67M"); 3316 assert(client.userMetadata[User("user1")]["foo"] == "third example value"); 3317 assert(client.userMetadata[User("user1")]["bar"] == "this is another example value"); 3318 assert(client.userMetadata[User("user2")]["bar"] == "second example value "); 3319 assert(client.userMetadata[User("user3")]["website"] == "www.example.com"); 3320 3321 client.join(Channel("#bigchan")); 3322 client.put(":modernclient!modernclient@example.com JOIN #bigchan"); 3323 client.put(":irc.example.com 353 modernclient @ #bigchan :user1 user2 user3 user4 user5 ..."); 3324 client.put(":irc.example.com 353 modernclient @ #bigchan :user51 user52 user53 user54 ..."); 3325 client.put(":irc.example.com 353 modernclient @ #bigchan :user101 user102 user103 user104 ..."); 3326 client.put(":irc.example.com 353 modernclient @ #bigchan :user151 user152 user153 user154 ..."); 3327 client.put(":irc.example.com 366 modernclient #bigchan :End of /NAMES list."); 3328 client.put(":irc.example.com 774 modernclient #bigchan 4"); 3329 assert(errors.length == 9); 3330 with(errors[8]) { 3331 assert(type == ErrorType.waitAndRetry); 3332 } 3333 3334 client.syncMetadata(Channel("#bigchan")); 3335 client.put(":irc.example.com 774 modernclient #bigchan 6"); 3336 assert(errors.length == 10); 3337 with(errors[9]) { 3338 assert(type == ErrorType.waitAndRetry); 3339 } 3340 3341 client.syncMetadata(Channel("#bigchan")); 3342 client.put(":irc.example.com BATCH +O5J6rk metadata"); 3343 client.put("@batch=O5J6rk :irc.example.com METADATA user52 foo * :example value 1"); 3344 client.put("@batch=O5J6rk :irc.example.com METADATA user2 bar * :second example value "); 3345 client.put("@batch=O5J6rk :irc.example.com METADATA user1 foo * :third example value"); 3346 client.put("@batch=O5J6rk :irc.example.com METADATA user1 bar * :this is another example value"); 3347 client.put("@batch=O5J6rk :irc.example.com METADATA user152 baz * :Lorem ipsum"); 3348 client.put("@batch=O5J6rk :irc.example.com METADATA user3 website * :www.example.com"); 3349 client.put("@batch=O5J6rk :irc.example.com METADATA user152 bar * :dolor sit amet"); 3350 client.put(":irc.example.com BATCH -O5J6rk"); 3351 assert(client.userMetadata[User("user1")]["foo"] == "third example value"); 3352 assert(client.userMetadata[User("user1")]["bar"] == "this is another example value"); 3353 assert(client.userMetadata[User("user2")]["bar"] == "second example value "); 3354 assert(client.userMetadata[User("user3")]["website"] == "www.example.com"); 3355 assert(client.userMetadata[User("user52")]["foo"] == "example value 1"); 3356 assert(client.userMetadata[User("user152")]["baz"] == "Lorem ipsum"); 3357 assert(client.userMetadata[User("user152")]["bar"] == "dolor sit amet"); 3358 3359 client.subscribeMetadata("avatar", "website", "foo", "bar"); 3360 client.put(":irc.example.com 770 modernclient :avatar website foo bar"); 3361 assert(client.isSubscribed("avatar")); 3362 assert(client.isSubscribed("website")); 3363 assert(client.isSubscribed("foo")); 3364 assert(client.isSubscribed("bar")); 3365 client.unsubscribeMetadata("foo", "bar"); 3366 client.put(":irc.example.com 771 modernclient :bar foo"); 3367 assert(!client.isSubscribed("foo")); 3368 assert(!client.isSubscribed("bar")); 3369 3370 client.subscribeMetadata("avatar", "website", "foo", "bar", "baz"); 3371 client.put(":irc.example.com 770 modernclient :avatar website"); 3372 client.put(":irc.example.com 770 modernclient :foo"); 3373 client.put(":irc.example.com 770 modernclient :bar baz"); 3374 assert(client.isSubscribed("avatar")); 3375 assert(client.isSubscribed("website")); 3376 assert(client.isSubscribed("foo")); 3377 assert(client.isSubscribed("bar")); 3378 assert(client.isSubscribed("baz")); 3379 3380 client.subscribeMetadata("foo", "$url", "bar"); 3381 client.put(":irc.example.com 770 modernclient :foo bar"); 3382 client.put("FAIL METADATA INVALID_KEY $url :Invalid key"); 3383 assert(errors.length == 11); 3384 with(errors[10]) { 3385 assert(type == ErrorType.standardFail); 3386 } 3387 3388 // uh oh zone 3389 client.state.metadataSubscribedKeys = []; 3390 client.subscribeMetadata("website", "avatar", "foo", "bar", "baz"); 3391 client.put(":irc.example.com 770 modernclient :website avatar foo bar baz"); 3392 client.subscribeMetadata("email", "city"); 3393 client.put("FAIL METADATA TOO_MANY_SUBS email :Too many subscriptions!"); 3394 client.listSubscribedMetadata(); 3395 client.put(":irc.example.com 772 modernclient :website avatar foo bar baz"); 3396 assert(errors.length == 12); 3397 with(errors[11]) { 3398 assert(type == ErrorType.standardFail); 3399 } 3400 assert(client.isSubscribed("website")); 3401 assert(client.isSubscribed("avatar")); 3402 assert(client.isSubscribed("foo")); 3403 assert(client.isSubscribed("bar")); 3404 assert(client.isSubscribed("baz")); 3405 assert(!client.isSubscribed("email")); 3406 assert(!client.isSubscribed("city")); 3407 3408 client.state.metadataSubscribedKeys = []; 3409 client.subscribeMetadata("website", "avatar", "foo"); 3410 client.put(":irc.example.com 770 modernclient :website avatar foo"); 3411 client.subscribeMetadata("email", "city", "country", "bar", "baz"); 3412 client.put("FAIL METADATA TOO_MANY_SUBS country :Too many subscriptions!"); 3413 client.put(":irc.example.com 770 modernclient :email city"); 3414 client.listSubscribedMetadata(); 3415 client.put(":irc.example.com 772 modernclient :website avatar city foo email"); 3416 assert(errors.length == 13); 3417 with(errors[12]) { 3418 assert(type == ErrorType.standardFail); 3419 } 3420 assert(client.isSubscribed("website")); 3421 assert(client.isSubscribed("avatar")); 3422 assert(client.isSubscribed("foo")); 3423 assert(client.isSubscribed("email")); 3424 assert(client.isSubscribed("city")); 3425 assert(!client.isSubscribed("country")); 3426 assert(!client.isSubscribed("bar")); 3427 assert(!client.isSubscribed("baz")); 3428 3429 client.state.metadataSubscribedKeys = []; 3430 client.subscribeMetadata("avatar", "website"); 3431 client.put(":irc.example.com 770 modernclient :avatar website"); 3432 client.subscribeMetadata("foo", "avatar", "website"); 3433 client.put("FAIL METADATA TOO_MANY_SUBS website :Too many subscriptions!"); 3434 client.put(":irc.example.com 770 modernclient :foo"); 3435 client.listSubscribedMetadata(); 3436 client.put(":irc.example.com 772 modernclient :avatar foo website"); 3437 assert(errors.length == 14); 3438 with(errors[13]) { 3439 assert(type == ErrorType.standardFail); 3440 } 3441 assert(client.isSubscribed("avatar")); 3442 assert(client.isSubscribed("foo")); 3443 assert(client.isSubscribed("website")); 3444 assert(!client.isSubscribed("country")); 3445 assert(!client.isSubscribed("bar")); 3446 assert(!client.isSubscribed("baz")); 3447 3448 client.state.metadataSubscribedKeys = []; 3449 client.subscribeMetadata("website", "avatar", "foo", "bar", "baz"); 3450 client.put(":irc.example.com 770 modernclient :website avatar foo bar baz"); 3451 client.listSubscribedMetadata(); 3452 client.put(":irc.example.com 772 modernclient :avatar bar baz foo website"); 3453 assert(client.isSubscribed("avatar")); 3454 assert(client.isSubscribed("foo")); 3455 assert(client.isSubscribed("website")); 3456 assert(client.isSubscribed("bar")); 3457 assert(client.isSubscribed("baz")); 3458 3459 client.state.metadataSubscribedKeys = []; 3460 client.subscribeMetadata("website", "avatar", "foo", "bar", "baz"); 3461 client.put(":irc.example.com 770 modernclient :website avatar foo bar baz"); 3462 client.listSubscribedMetadata(); 3463 client.put(":irc.example.com 772 modernclient :avatar"); 3464 client.put(":irc.example.com 772 modernclient :bar baz"); 3465 client.put(":irc.example.com 772 modernclient :foo website"); 3466 assert(client.isSubscribed("avatar")); 3467 assert(client.isSubscribed("foo")); 3468 assert(client.isSubscribed("website")); 3469 assert(client.isSubscribed("bar")); 3470 assert(client.isSubscribed("baz")); 3471 3472 client.state.metadataSubscribedKeys = []; 3473 client.listSubscribedMetadata(); 3474 3475 client.state.metadataSubscribedKeys = []; 3476 client.subscribeMetadata("website", "avatar", "foo", "bar", "baz"); 3477 client.put(":irc.example.com 770 modernclient :website avatar foo bar baz"); 3478 client.listSubscribedMetadata(); 3479 client.put(":irc.example.com 772 modernclient :avatar bar baz foo website"); 3480 client.unsubscribeMetadata("bar", "foo", "baz"); 3481 client.put(":irc.example.com 771 modernclient :baz foo bar"); 3482 client.listSubscribedMetadata(); 3483 client.put(":irc.example.com 772 modernclient :avatar website"); 3484 assert(client.isSubscribed("avatar")); 3485 assert(client.isSubscribed("website")); 3486 3487 client.state.metadataSubscribedKeys = []; 3488 client.subscribeMetadata("website", "avatar", "foo", "bar", "baz"); 3489 client.put(":irc.example.com 770 modernclient :website avatar foo bar baz"); 3490 client.listSubscribedMetadata(); 3491 client.put(":irc.example.com 772 modernclient :avatar bar baz foo website"); 3492 client.subscribeMetadata("avatar", "website"); 3493 client.put(":irc.example.com 770 modernclient :avatar website"); 3494 client.listSubscribedMetadata(); 3495 client.put(":irc.example.com 772 modernclient :avatar bar baz foo website"); 3496 assert(client.isSubscribed("avatar")); 3497 assert(client.isSubscribed("website")); 3498 assert(client.isSubscribed("foo")); 3499 assert(client.isSubscribed("bar")); 3500 assert(client.isSubscribed("baz")); 3501 3502 client.state.metadataSubscribedKeys = []; 3503 client.subscribeMetadata("avatar", "avatar"); 3504 client.put(":irc.example.com 770 modernclient :avatar"); 3505 client.listSubscribedMetadata(); 3506 client.put(":irc.example.com 772 modernclient :avatar"); 3507 assert(client.isSubscribed("avatar")); 3508 3509 client.state.metadataSubscribedKeys = []; 3510 client.subscribeMetadata("avatar", "avatar"); 3511 client.put(":irc.example.com 770 modernclient :avatar avatar"); 3512 client.listSubscribedMetadata(); 3513 client.put(":irc.example.com 772 modernclient :avatar"); 3514 assert(client.isSubscribed("avatar")); 3515 3516 client.state.metadataSubscribedKeys = []; 3517 client.listSubscribedMetadata(); 3518 client.unsubscribeMetadata("website"); 3519 client.put(":irc.example.com 771 modernclient :website"); 3520 assert(!client.isSubscribed("website")); 3521 client.listSubscribedMetadata(); 3522 client.subscribeMetadata("website"); 3523 client.put(":irc.example.com 770 modernclient :website"); 3524 client.listSubscribedMetadata(); 3525 client.put(":irc.example.com 772 modernclient :website"); 3526 assert(client.isSubscribed("website")); 3527 3528 client.state.metadataSubscribedKeys = []; 3529 client.listSubscribedMetadata(); 3530 client.put(":irc.example.com 772 modernclient :website"); 3531 client.unsubscribeMetadata("website", "website"); 3532 client.put(":irc.example.com 771 modernclient :website"); 3533 assert(!client.isSubscribed("website")); 3534 3535 client.state.metadataSubscribedKeys = []; 3536 client.listSubscribedMetadata(); 3537 client.put(":irc.example.com 772 modernclient :website"); 3538 client.unsubscribeMetadata("website", "website"); 3539 client.put(":irc.example.com 771 modernclient :website website"); 3540 assert(!client.isSubscribed("website")); 3541 3542 client.state.metadataSubscribedKeys = []; 3543 client.subscribeMetadata("avatar", "secretkey", "website"); 3544 client.put("FAIL METADATA KEY_NO_PERMISSION secretkey modernclient :You do not have permission to do that."); 3545 client.put(":irc.example.com 770 modernclient :avatar website"); 3546 client.listSubscribedMetadata(); 3547 client.put(":irc.example.com 772 modernclient :avatar website"); 3548 assert(!client.isSubscribed("secretkey")); 3549 assert(client.isSubscribed("website")); 3550 assert(client.isSubscribed("avatar")); 3551 assert(errors.length == 15); 3552 with(errors[14]) { 3553 assert(type == ErrorType.standardFail); 3554 } 3555 3556 client.state.metadataSubscribedKeys = []; 3557 client.subscribeMetadata("$invalid1", "secretkey1", "$invalid2", "secretkey2", "website"); 3558 client.put("FAIL METADATA KEY_NO_PERMISSION secretkey1 modernclient :You do not have permission to do that."); 3559 client.put("FAIL METADATA KEY_INVALID $invalid1 modernclient :Invalid key"); 3560 client.put("FAIL METADATA KEY_NO_PERMISSION secretkey2 modernclient :You do not have permission to do that."); 3561 client.put("FAIL METADATA KEY_INVALID $invalid2 modernclient :Invalid key"); 3562 client.put(":irc.example.com 770 modernclient :website"); 3563 client.listSubscribedMetadata(); 3564 client.put(":irc.example.com 772 modernclient :website"); 3565 assert(!client.isSubscribed("$invalid1")); 3566 assert(!client.isSubscribed("secretkey1")); 3567 assert(!client.isSubscribed("$invalid2")); 3568 assert(!client.isSubscribed("secretkey2")); 3569 assert(client.isSubscribed("website")); 3570 assert(errors.length == 19); 3571 with(errors[15]) { 3572 assert(type == ErrorType.standardFail); 3573 } 3574 with(errors[16]) { 3575 assert(type == ErrorType.standardFail); 3576 } 3577 with(errors[17]) { 3578 assert(type == ErrorType.standardFail); 3579 } 3580 with(errors[18]) { 3581 assert(type == ErrorType.standardFail); 3582 } 3583 3584 // end of uh oh zone 3585 3586 } 3587 { //Multiline messages 3588 auto client = spawnNoBufferClient(); 3589 initializeWithCaps(client, [Capability("draft/multiline", "max-bytes=1000,max-lines=100"), Capability("message-tags")]); 3590 { 3591 client.msg("someoneElse", "This is a message\nwith newlines in it.\nDo not be alarmed."); 3592 auto lines = client.output.data.lineSplitter.array; 3593 assert(lines[$ - 5] == "BATCH +0 draft/multiline someoneElse"); 3594 assert(lines[$ - 4] == "@batch=0 PRIVMSG someoneElse :This is a message"); 3595 assert(lines[$ - 3] == "@batch=0 PRIVMSG someoneElse :with newlines in it."); 3596 assert(lines[$ - 2] == "@batch=0 PRIVMSG someoneElse :Do not be alarmed."); 3597 assert(lines[$ - 1] == "BATCH -0"); 3598 } 3599 { 3600 client.msg("someoneElse", "This is a very long message. a very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very long message."); 3601 auto lines = client.output.data.lineSplitter.array; 3602 assert(lines[$ - 4] == "BATCH +1 draft/multiline someoneElse"); 3603 assert(lines[$ - 3] == "@batch=1 PRIVMSG someoneElse :This is a very long message. a very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very "); 3604 assert(lines[$ - 2] == "@batch=1;draft/multiline-concat PRIVMSG someoneElse :very very very very very very very very very very very very very very long message."); 3605 assert(lines[$ - 1] == "BATCH -1"); 3606 } 3607 { 3608 client.msg("someoneElse", "\x1FThis is a simple message\nwith newlines and formatting."); 3609 auto lines = client.output.data.lineSplitter.array; 3610 assert(lines[$ - 4] == "BATCH +2 draft/multiline someoneElse"); 3611 assert(lines[$ - 3] == "@batch=2 PRIVMSG someoneElse :\x1FThis is a simple message"); 3612 assert(lines[$ - 2] == "@batch=2 PRIVMSG someoneElse :\x1Fwith newlines and formatting."); 3613 assert(lines[$ - 1] == "BATCH -2"); 3614 } 3615 { 3616 IRCTags reply; 3617 reply.reply("replyid"); 3618 client.msg("someoneElse", "\x1FThis is a simple message\nwith newlines and formatting.", reply); 3619 auto lines = client.output.data.lineSplitter.array; 3620 assert(lines[$ - 4] == "@+draft/reply=replyid BATCH +3 draft/multiline someoneElse"); 3621 assert(lines[$ - 3] == "@batch=3 PRIVMSG someoneElse :\x1FThis is a simple message"); 3622 assert(lines[$ - 2] == "@batch=3 PRIVMSG someoneElse :\x1Fwith newlines and formatting."); 3623 assert(lines[$ - 1] == "BATCH -3"); 3624 } 3625 } 3626 { //Multiline messages, low line limit 3627 auto client = spawnNoBufferClient(); 3628 initializeWithCaps(client, [Capability("draft/multiline", "max-bytes=1000,max-lines=2"), Capability("message-tags")]); 3629 { 3630 client.msg("someoneElse", "This is a message\nwith newlines in it.\nDo not be alarmed.\nIt's fine."); 3631 auto lines = client.output.data.lineSplitter.array; 3632 assert(lines[$ - 8] == "BATCH +0 draft/multiline someoneElse"); 3633 assert(lines[$ - 7] == "@batch=0 PRIVMSG someoneElse :This is a message"); 3634 assert(lines[$ - 6] == "@batch=0 PRIVMSG someoneElse :with newlines in it."); 3635 assert(lines[$ - 5] == "BATCH -0"); 3636 assert(lines[$ - 4] == "BATCH +1 draft/multiline someoneElse"); 3637 assert(lines[$ - 3] == "@batch=1 PRIVMSG someoneElse :Do not be alarmed."); 3638 assert(lines[$ - 2] == "@batch=1 PRIVMSG someoneElse :It's fine."); 3639 assert(lines[$ - 1] == "BATCH -1"); 3640 } 3641 } 3642 { //Multiline messages, low byte limit 3643 auto client = spawnNoBufferClient(); 3644 initializeWithCaps(client, [Capability("draft/multiline", "max-bytes=600,max-lines=100"), Capability("message-tags")]); 3645 { 3646 client.msg("someoneElse", "This is a very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very long message"); 3647 auto lines = client.output.data.lineSplitter.array; 3648 assert(lines[$ - 7] == "BATCH +0 draft/multiline someoneElse"); 3649 assert(lines[$ - 6] == "@batch=0 PRIVMSG someoneElse :This is a very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very v"); 3650 assert(lines[$ - 5] == "@batch=0;draft/multiline-concat PRIVMSG someoneElse :ery very very very very very very very very very very very very very very very very very very very very very very very very very very very "); 3651 assert(lines[$ - 4] == "BATCH -0"); 3652 assert(lines[$ - 3] == "BATCH +1 draft/multiline someoneElse"); 3653 assert(lines[$ - 2] == "@batch=1 PRIVMSG someoneElse :very very very very very very very very very very long message"); 3654 assert(lines[$ - 1] == "BATCH -1"); 3655 } 3656 } 3657 { //Multiline messages (no cap) 3658 auto client = spawnNoBufferClient(); 3659 initializeWithCaps(client, [Capability("message-tags")]); 3660 { 3661 client.msg("someoneElse", "This is a message\nwith newlines in it.\nDo not be alarmed."); 3662 auto lines = client.output.data.lineSplitter.array; 3663 assert(lines[$ - 3] == "PRIVMSG someoneElse :This is a message"); 3664 assert(lines[$ - 2] == "PRIVMSG someoneElse :with newlines in it."); 3665 assert(lines[$ - 1] == "PRIVMSG someoneElse :Do not be alarmed."); 3666 } 3667 { 3668 client.msg("someoneElse", "This is a very long message. a very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very long message."); 3669 auto lines = client.output.data.lineSplitter.array; 3670 assert(lines[$ - 2] == "PRIVMSG someoneElse :This is a very long message. a very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very "); 3671 assert(lines[$ - 1] == "PRIVMSG someoneElse :very very very very very very very very very very very very very very long message."); 3672 } 3673 { 3674 client.msg("someoneElse", "\x1FThis is a simple message\nwith newlines and formatting."); 3675 auto lines = client.output.data.lineSplitter.array; 3676 assert(lines[$ - 2] == "PRIVMSG someoneElse :\x1FThis is a simple message"); 3677 assert(lines[$ - 1] == "PRIVMSG someoneElse :\x1Fwith newlines and formatting."); 3678 } 3679 { 3680 IRCTags reply; 3681 reply.reply("replyid"); 3682 client.msg("someoneElse", "\x1FThis is a simple message\nwith newlines and formatting.", reply); 3683 auto lines = client.output.data.lineSplitter.array; 3684 assert(lines[$ - 2] == "@+draft/reply=replyid PRIVMSG someoneElse :\x1FThis is a simple message"); 3685 assert(lines[$ - 1] == "@+draft/reply=replyid PRIVMSG someoneElse :\x1Fwith newlines and formatting."); 3686 } 3687 } 3688 }