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