1 /++ 2 Macros: 3 SYMBOL_LINK = $(LINK2 #$1, $1) 4 DDOC = <!DOCTYPE html> 5 <html lang="en"> 6 <head> 7 <title>$(TITLE)</title> 8 <meta charset="utf-8" /> 9 <style type="text/css"> 10 table, tr, td 11 { 12 /* putting these on their own line caused most of the document to be left out (dmd bug?) */ 13 border-collapse: collapse; border: 1px solid black; padding: 5px; 14 } 15 </style> 16 </head> 17 <body> 18 <h1>$(TITLE)</h1> 19 $(BODY) 20 </body> 21 </html> 22 +/ 23 module vibeirc; 24 25 private 26 { 27 import std.string: split, format; 28 } 29 30 private enum CTCP_ENCAPSULATOR = '\x01'; 31 32 enum 33 { 34 /++ 35 The server sends RPL_WELCOME through RPL_MYINFO to a user upon successful registration. 36 +/ 37 RPL_WELCOME = 001, 38 RPL_YOURHOST = 002, ///ditto 39 RPL_CREATED = 003, ///ditto 40 RPL_MYINFO = 004, ///ditto 41 42 /++ 43 Sent by the server to a user to suggest an alternative server. 44 This is often used when the connection is refused because the server is already full. 45 +/ 46 RPL_BOUNCE = 005, 47 48 /++ 49 Reply format used by USERHOST to list replies to the query list. 50 +/ 51 RPL_USERHOST = 302, 52 53 /++ 54 Reply format used by ISON to list replies to the query list. 55 +/ 56 RPL_ISON = 303, 57 58 /++ 59 These replies are used with the AWAY command (if allowed). 60 RPL_AWAY is sent to any client sending a PRIVMSG to a client which is away. 61 RPL_AWAY is only sent by the server to which the client is connected. 62 Replies RPL_UNAWAY and RPL_NOWAWAY are sent when the client removes and sets an AWAY message. 63 +/ 64 RPL_AWAY = 301, 65 RPL_UNAWAY = 305, ///ditto 66 RPL_NOWAWAY = 306, ///ditto 67 68 /++ 69 Replies RPL_WHOISUSER through RPL_WHOIS, RPL_WHOISIDLE through RPL_WHOISCHANNELS are all replies generated in response to a WHOIS message. 70 Given that there are enough parameters present, 71 the answering server MUST either formulate a reply out of the above numerics 72 (if the query nick is found) or return an error reply. 73 The '*' in RPL_WHOISUSER is there as the literal character and not as a wild card. 74 For each reply set, only RPL_WHOISCHANNELS may appear more than once 75 (for long lists of channel names). 76 The '@' and '+' characters next to the channel name indicate whether 77 a client is a channel operator or has been granted permission to speak on a moderated channel. 78 The RPL_ENDOFWHOIS reply is used to mark the end of processing a WHOIS message. 79 +/ 80 RPL_WHOISUSER = 311, 81 RPL_WHOISSERVER = 312, ///ditto 82 RPL_WHOISOPERATOR = 313, ///ditto 83 RPL_WHOISIDLE = 317, ///ditto 84 RPL_ENDOFWHOIS = 318, ///ditto 85 RPL_WHOISCHANNELS = 319, ///ditto 86 87 /++ 88 When replying to a WHOWAS message, a server MUST use the replies 89 RPL_WHOWASUSER, RPL_WHOISSERVER or ERR_WASNOSUCHNICK for each nickname in the presented list. 90 At the end of all reply batches, there MUST be RPL_ENDOFWHOWAS 91 (even if there was only one reply and it was an error). 92 +/ 93 RPL_WHOWASUSER = 314, 94 RPL_ENDOFWHOWAS = 369, ///ditto 95 96 /++ 97 Replies RPL_LIST, RPL_LISTEND mark the actual replies with data 98 and end of the server's responseto a LIST command. 99 If there are no channels available to return, only the end reply MUST be sent. 100 +/ 101 RPL_LIST = 322, ///ditto 102 RPL_LISTEND = 323, ///ditto 103 RPL_LISTSTART = 321, ///Obsolete. Not used. 104 105 RPL_UNIQOPIS = 325, /// 106 RPL_CHANNELMODEIS = 324, /// 107 108 /++ 109 When sending a TOPIC message to determine the channel topic, one of two replies is sent. 110 If the topic is set, RPL_TOPIC is sent back else RPL_NOTOPIC. 111 +/ 112 RPL_NOTOPIC = 331, ///ditto 113 RPL_TOPIC = 332, ///ditto 114 115 /++ 116 Returned by the server to indicate that the attempted INVITE message 117 was successful and is being passed onto the end client. 118 +/ 119 RPL_INVITING = 341, 120 121 /++ 122 Returned by a server answering a SUMMON message to indicate that it is summoning that user. 123 +/ 124 RPL_SUMMONING = 342, 125 126 /++ 127 When listing the 'invitations masks' for a given channel, 128 a server is required to send the list back using the RPL_INVITELIST 129 and RPL_ENDOFINVITELIST messages. 130 A separate RPL_INVITELIST is sent for each active mask. 131 After the masks have been listed (or if none present) 132 a RPL_ENDOFINVITELIST MUST be sent. 133 +/ 134 RPL_INVITELIST = 346, 135 RPL_ENDOFINVITELIST = 347, ///ditto 136 137 /++ 138 When listing the 'exception masks' for a given channel, 139 a server is required to send the list back using the RPL_EXCEPTLIST 140 and RPL_ENDOFEXCEPTLIST messages. 141 A separate RPL_EXCEPTLIST is sent for each active mask. 142 After the masks have been listed (or if none present) 143 a RPL_ENDOFEXCEPTLIST MUST be sent. 144 +/ 145 RPL_EXCEPTLIST = 348, 146 RPL_ENDOFEXCEPTLIST = 349, ///ditto 147 148 /++ 149 Reply by the server showing its version details. 150 +/ 151 RPL_VERSION = 351, 152 153 /++ 154 The RPL_WHOREPLY and RPL_ENDOFWHO pair are used to answer a WHO message. 155 The RPL_WHOREPLY is only sent if there is an appropriate match to the WHO query. 156 +/ 157 RPL_WHOREPLY = 352, 158 RPL_ENDOFWHO = 315, ///ditto 159 160 /++ 161 To reply to a NAMES message, a reply pair consisting of RPL_NAMREPLY 162 and RPL_ENDOFNAMES is sent by the server back to the client. 163 If there is no channel found as in the query, then only RPL_ENDOFNAMES is returned. 164 The exception to this is when a NAMES message is sent with no parameters 165 and all visible channels and contents are sent back in a series of 166 RPL_NAMEREPLY messages with a RPL_ENDOFNAMES to mark the end. 167 +/ 168 RPL_NAMREPLY = 353, 169 RPL_ENDOFNAMES = 366, ///ditto 170 171 /++ 172 In replying to the LINKS message, a server MUST send replies back using the RPL_LINKS 173 numeric and mark the end of the list using an RPL_ENDOFLINKS reply. 174 +/ 175 RPL_LINKS = 364, 176 RPL_ENDOFLINKS = 365, ///ditto 177 178 /++ 179 When listing the active 'bans' for a given channel, a server is required 180 to send the list back using the RPL_BANLIST and RPL_ENDOFBANLIST messages. 181 A separate RPL_BANLIST is sent for each active banmask. 182 After the banmasks have been listed (or if none present) a RPL_ENDOFBANLIST MUST be sent. 183 +/ 184 RPL_BANLIST = 367, 185 RPL_ENDOFBANLIST = 368, ///ditto 186 187 /++ 188 A server responding to an INFO message is required to send all its 'info' in a series of 189 RPL_INFO messages with a RPL_ENDOFINFO reply to indicate the end of the replies. 190 +/ 191 RPL_INFO = 371, 192 RPL_ENDOFINFO = 374, ///ditto 193 194 /++ 195 When responding to the MOTD message and the MOTD file is found, 196 the file is displayed line by line, with each line no longer than 80 characters, 197 using RPL_MOTD format replies. 198 These MUST be surrounded by a RPL_MOTDSTART (before the RPL_MOTDs) 199 and an RPL_ENDOFMOTD (after). 200 +/ 201 RPL_MOTDSTART = 375, 202 RPL_MOTD = 372, ///ditto 203 RPL_ENDOFMOTD = 376, ///ditto 204 205 /++ 206 RPL_YOUREOPER is sent back to a client which has just successfully issued 207 an OPER message and gained operator status. 208 +/ 209 RPL_YOUREOPER = 381, 210 211 /++ 212 If the REHASH option is used and an operator sends a REHASH message, 213 an RPL_REHASHING is sent back to the operator. 214 +/ 215 RPL_REHASHING = 382, 216 217 /++ 218 Sent by the server to a service upon successful registration. 219 +/ 220 RPL_YOURESERVICE = 383, 221 222 /++ 223 When replying to the TIME message, a server MUST send the reply using the RPL_TIME format above. 224 The string showing the time need only contain the correct day and time there. 225 There is no further requirement for the time string. 226 +/ 227 RPL_TIME = 391, 228 229 /++ 230 If the USERS message is handled by a server, the replies RPL_USERSTART, 231 RPL_USERS, RPL_ENDOFUSERS and RPL_NOUSERS are used. 232 RPL_USERSSTART MUST be sent first, following by either a sequence of RPL_USERS 233 or a single RPL_NOUSER. 234 Following this is RPL_ENDOFUSERS. 235 +/ 236 RPL_USERSSTART = 392, 237 RPL_USERS = 393, ///ditto 238 RPL_ENDOFUSERS = 394, ///ditto 239 RPL_NOUSERS = 395, ///ditto 240 241 /++ 242 The RPL_TRACE* are all returned by the server in response to the TRACE message. 243 How many are returned is dependent on the TRACE message and whether it was sent by an operator or not. 244 There is no predefined order for which occurs first. 245 Replies RPL_TRACEUNKNOWN, RPL_TRACECONNECTING and RPL_TRACEHANDSHAKE are all used for connections 246 which have not been fully established and are either unknown, 247 still attempting to connect or in the process of completing the 'server handshake'. 248 RPL_TRACELINK is sent by any server which handlesa TRACE message and has to pass it on to another server. 249 The list of RPL_TRACELINKs sent in response to a TRACE command traversing the IRC network 250 should reflect the actual connectivity ofthe servers themselves along that path. 251 RPL_TRACENEWTYPE is to be used for any connection which does not fit in the other categories 252 but is being displayed anyway. 253 RPL_TRACEEND is sent to indicate the end of the list. 254 +/ 255 RPL_TRACELINK = 200, 256 RPL_TRACECONNECTING = 201, ///ditto 257 RPL_TRACEHANDSHAKE = 202, ///ditto 258 RPL_TRACEUNKNOWN = 203, ///ditto 259 RPL_TRACEOPERATOR = 204, ///ditto 260 RPL_TRACEUSER = 205, ///ditto 261 RPL_TRACESERVER = 206, ///ditto 262 RPL_TRACESERVICE = 207, ///ditto 263 RPL_TRACENEWTYPE = 208, ///ditto 264 RPL_TRACECLASS = 209, ///ditto 265 RPL_TRACELOG = 261, ///ditto 266 RPL_TRACEEND = 262, ///ditto 267 RPL_TRACERECONNECT = 210, ///Unused. 268 269 /++ 270 Returned from the server in response to the STATS message. 271 +/ 272 RPL_STATSLINKINFO = 211, 273 RPL_STATSCOMMANDS = 212, ///ditto 274 RPL_ENDOFSTATS = 219, ///ditto 275 RPL_STATSUPTIME = 242, ///ditto 276 RPL_STATSOLINE = 243, ///ditto 277 278 RPL_UMODEIS = 221, /// 279 280 RPL_SERVLIST = 234, /// 281 282 /++ 283 When listing services in reply to a SERVLIST message, 284 a server is required to send the list back using the RPL_SERVLIST 285 and RPL_SERVLISTEND messages. 286 A separate RPL_SERVLIST is sent for each service. 287 After the services have been listed (or if none present) a RPL_SERVLISTEND MUST be sent. 288 +/ 289 RPL_SERVLISTEND = 235, 290 291 /++ 292 In processing an LUSERS message, the server sends a set of replies from 293 RPL_LUSERCLIENT, RPL_LUSEROP, RPL_USERUNKNOWN, RPL_LUSERCHANNELS and RPL_LUSERME. 294 When replying, a server MUST send back RPL_LUSERCLIENT and RPL_LUSERME. 295 The other replies are only sent back if a non-zero count is found for them. 296 +/ 297 RPL_LUSERCLIENT = 251, 298 RPL_LUSEROP = 252, ///ditto 299 RPL_LUSERUNKNOWN = 253, ///ditto 300 RPL_LUSERCHANNELS = 254, ///ditto 301 RPL_LUSERME = 255, ///ditto 302 303 /++ 304 When replying to an ADMIN message, a server is expected to use replies RPL_ADMINME 305 through to RPL_ADMINEMAIL and provide a text message with each. 306 For RPL_ADMINLOC1 a description of what city, state and country the server is in is expected, 307 followed by details of the institution (RPL_ADMINLOC2) 308 and finally the administrative contact for the server (an email address here is REQUIRED) 309 in RPL_ADMINEMAIL. 310 +/ 311 RPL_ADMINME = 256, 312 RPL_ADMINLOC1 = 257, ///ditto 313 RPL_ADMINLOC2 = 258, ///ditto 314 RPL_ADMINEMAIL = 259, ///ditto 315 316 /++ 317 When a server drops a command without processing it, 318 it MUST use the reply RPL_TRYAGAIN to inform the originating client. 319 +/ 320 RPL_TRYAGAIN = 263, 321 322 /++ 323 Used to indicate the nickname parameter supplied to a command is currently unused. 324 +/ 325 ERR_NOSUCHNICK = 401, 326 327 /++ 328 Used to indicate the server name given currently does not exist. 329 +/ 330 ERR_NOSUCHSERVER = 402, 331 332 /++ 333 Used to indicate the given channel name is invalid. 334 +/ 335 ERR_NOSUCHCHANNEL = 403, 336 337 /++ 338 Sent to a user who is either (a) not on a channel which is mode +n 339 or (b) not a chanop (or mode +v) on a channel which has mode +m set 340 or where the user is banned and is trying to send a PRIVMSG message to that channel. 341 +/ 342 ERR_CANNOTSENDTOCHAN = 404, 343 344 /++ 345 Sent to a user when they have joined the maximum number 346 of allowed channels and they try to join another channel. 347 +/ 348 ERR_TOOMANYCHANNELS = 405, 349 350 /++ 351 Returned by WHOWAS to indicate there is no history information for that nickname. 352 +/ 353 ERR_WASNOSUCHNICK = 406, 354 355 /++ 356 Returned to a client which is attempting to send a PRIVMSG/NOTICE 357 using the user@host destination format and for a user@host which has several occurrences. 358 Returned to a client which trying to send a PRIVMSG/NOTICE to too many recipients. 359 Returned to a client which is attempting to JOIN a safe channel 360 using the shortname when there are more than one such channel. 361 +/ 362 ERR_TOOMANYTARGETS = 407, 363 364 /++ 365 Returned to a client which is attempting to send a SQUERY to a service which does not exist. 366 +/ 367 ERR_NOSUCHSERVICE = 408, 368 369 /++ 370 PING or PONG message missing the originator parameter. 371 +/ 372 ERR_NOORIGIN = 409, 373 374 /++ 375 ERR_NOTEXTTOSEND through ERR_BADMASK are returned by PRIVMSG to indicate that the message 376 wasn't delivered for some reason. 377 ERR_NOTOPLEVEL and ERR_WILDTOPLEVEL are errors that are returned 378 when an invalid use of "PRIVMSG $<server>" or "PRIVMSG #<host>" is attempted. 379 +/ 380 ERR_NORECIPIENT = 411, 381 ERR_NOTEXTTOSEND = 412, ///ditto 382 ERR_NOTOPLEVEL = 413, ///ditto 383 ERR_WILDTOPLEVEL = 414, ///ditto 384 ERR_BADMASK = 415, ///ditto 385 386 /++ 387 Returned to a registered client to indicate 388 that the command sent is unknown by the server. 389 +/ 390 ERR_UNKNOWNCOMMAND = 421, 391 392 /++ 393 Server's MOTD file could not be opened by the server. 394 +/ 395 ERR_NOMOTD = 422, 396 397 /++ 398 Returned by a server in response to an ADMIN message 399 when there is an error in finding the appropriate information. 400 +/ 401 ERR_NOADMININFO = 423, 402 403 /++ 404 Generic error message used to report a failed file 405 operation during the processing of a message. 406 +/ 407 ERR_FILEERROR = 424, 408 409 /++ 410 Returned when a nickname parameter expected for a command and isn't found. 411 +/ 412 ERR_NONICKNAMEGIVEN = 431, 413 414 /++ 415 Returned after receiving a NICK message 416 which contains characters which do not fall in the defined set. 417 +/ 418 ERR_ERRONEUSNICKNAME = 432, 419 420 /++ 421 Returned when a NICK message is processed that results 422 in an attempt to change to a currently existing nickname. 423 +/ 424 ERR_NICKNAMEINUSE = 433, 425 426 /++ 427 Returned by a server to a client when it detects a nickname collision 428 (registered of a NICK that already exists by another server). 429 +/ 430 ERR_NICKCOLLISION = 436, 431 } 432 433 enum 434 { 435 /++ 436 Color codes for use with $(SYMBOL_LINK color). 437 +/ 438 WHITE = "00", 439 BLACK = "01", ///ditto 440 BLUE = "02", ///ditto 441 GREEN = "03", ///ditto 442 RED = "04", ///ditto 443 BROWN = "05", ///ditto 444 PURPLE = "06", ///ditto 445 ORANGE = "07", ///ditto 446 YELLOW = "08", ///ditto 447 LIGHTGREEN = "09", ///ditto 448 TEAL = "10", ///ditto 449 LIGHTCYAN = "11", ///ditto 450 LIGHTBLUE = "12", ///ditto 451 PINK = "13", ///ditto 452 GREY = "14", ///ditto 453 LIGHTGREY = "15", ///ditto 454 TRANSPARENT = "99", ///ditto 455 } 456 457 /++ 458 A struct containing details about a connection. 459 +/ 460 struct ConnectionParameters 461 { 462 string hostname; ///The _hostname or address of the IRC server. 463 ushort port; ///The _port of the server. 464 string password = null; ///The _password for the server, if any. 465 string username = "vibeIRC"; ///The _username, later used by the server in this connection's hostmask. 466 string realname = null; ///The _realname as returned by the WHOIS command. 467 } 468 469 /++ 470 A struct containing details about a user. 471 +/ 472 struct User 473 { 474 string nickname; ///The _nickname of this user. 475 string username; ///The _username portion of this user's hostmask. 476 string hostname; ///The _hostname portion of this user's hostmask. 477 } 478 479 /++ 480 A struct containing details about an incoming message. 481 +/ 482 struct Message 483 { 484 User sender; ///The user who sent the message. 485 string receiver; ///The destination of the message, either a user or a channel. 486 string ctcpCommand; ///The CTCP command, if any. 487 string message; ///The _message body. 488 489 /++ 490 Returns whether this message uses CTCP. 491 +/ 492 @property bool isCTCP() 493 { 494 return ctcpCommand != null; 495 } 496 } 497 498 //Thrown from line_received, handle_numeric or handle_command in case of an error 499 private class GracelessDisconnect: Exception 500 { 501 this(string msg) 502 { 503 super(msg); 504 } 505 } 506 507 /++ 508 The base class for IRC connections. 509 +/ 510 class IRCConnection 511 { 512 import vibe.core.net: TCPConnection; 513 import vibe.core.log: logDebug, logError; 514 import vibe.core.task: Task; 515 516 private string _nickname; 517 private Task protocolTask; 518 ConnectionParameters connectionParameters; ///The connection parameters passed to $(SYMBOL_LINK irc_connect). 519 TCPConnection transport; ///The vibe socket underlying this connection. 520 521 /++ 522 Default constructor. Should not be called from user code. 523 524 See_Also: 525 $(SYMBOL_LINK irc_connect) 526 +/ 527 protected this() {} 528 529 private void protocol_loop() 530 in { assert(transport && transport.connected); } 531 body 532 { 533 import vibe.stream.operations: readLine; 534 535 string disconnectReason = "Connection terminated gracefully"; 536 537 version(IrcDebugLogging) logDebug("irc connected"); 538 539 if(connectionParameters.password != null) 540 send_line("PASS %s", connectionParameters.password); 541 542 send_line("NICK %s", nickname); 543 send_line("USER %s 0 * :%s", connectionParameters.username, connectionParameters.realname); 544 545 while(transport.connected) 546 { 547 string line; 548 549 try 550 line = cast(string)transport.readLine; 551 catch(Exception err) 552 { 553 logError(err.toString); 554 555 break; 556 } 557 558 version(IrcDebugLogging) logDebug("irc recv: %s", line); 559 560 try 561 line_received(line); 562 catch(GracelessDisconnect err) 563 { 564 disconnectReason = err.msg; 565 566 transport.close; 567 } 568 } 569 570 disconnected(disconnectReason); 571 version(IrcDebugLogging) logDebug("irc disconnected"); 572 } 573 574 private void line_received(string line) 575 { 576 import std.conv: ConvException, to; 577 578 string[] parts = line.split(" "); 579 580 switch(parts[0]) 581 { 582 case "PING": 583 send_line("PONG %s", parts[1]); 584 585 break; 586 case "ERROR": 587 throw new GracelessDisconnect(parts.drop_first.join.drop_first); 588 default: 589 parts[0] = parts[0].drop_first; 590 591 try 592 handle_numeric(parts[0], parts[1].to!int, parts[2 .. $]); 593 catch(ConvException err) 594 handle_command(parts[0], parts[1], parts[2 .. $]); 595 } 596 } 597 598 private void handle_command(string prefix, string command, string[] parts) 599 { 600 version(IrcDebugLogging) logDebug("handle_command(%s, %s, %s)", prefix, command, parts); 601 602 switch(command) 603 { 604 case "NOTICE": 605 case "PRIVMSG": 606 Message msg; 607 string message = parts.drop_first.join; 608 msg.sender = prefix.split_userinfo; 609 msg.receiver = parts[0]; 610 msg.message = message != null ? message.drop_first : ""; 611 612 if(message.is_ctcp) 613 { 614 auto parsedCtcp = message.parse_ctcp; 615 msg.ctcpCommand = parsedCtcp.command; 616 msg.message = parsedCtcp.message; 617 } 618 619 if(command == "NOTICE") 620 notice(msg); 621 else 622 privmsg(msg); 623 624 break; 625 case "JOIN": 626 user_joined(prefix.split_userinfo, parts[0].drop_first); 627 628 break; 629 case "PART": 630 user_left(prefix.split_userinfo, parts[0], parts.drop_first.join.drop_first); 631 632 break; 633 case "QUIT": 634 user_quit(prefix.split_userinfo, parts.join.drop_first); 635 636 break; 637 case "NICK": 638 user_renamed(prefix.split_userinfo, parts[0].drop_first); 639 640 break; 641 case "KICK": 642 user_kicked(prefix.split_userinfo, parts[1], parts[0], parts[2 .. $].join.drop_first); 643 644 break; 645 default: 646 unknown_command(prefix, command, parts); 647 } 648 } 649 650 private void handle_numeric(string prefix, int id, string[] parts) 651 { 652 version(IrcDebugLogging) logDebug("handle_numeric(%s, %s, %s)", prefix, id, parts); 653 654 switch(id) 655 { 656 case RPL_WELCOME: 657 signed_on; 658 659 break; 660 case ERR_ERRONEUSNICKNAME: 661 throw new GracelessDisconnect("Erroneus nickname"); //TODO: handle gracefully? 662 case ERR_NICKNAMEINUSE: 663 throw new GracelessDisconnect("Nickname already in use"); //TODO: handle gracefully? 664 default: 665 unknown_numeric(prefix, id, parts); 666 } 667 } 668 669 /++ 670 Get this connection's _nickname. 671 +/ 672 final @property string nickname() 673 { 674 return _nickname; 675 } 676 677 /++ 678 Set this connection's _nickname. 679 +/ 680 final @property string nickname(string newNick) 681 { 682 if(transport && transport.connected) 683 send_line("NICK %s", newNick); 684 685 return _nickname = newNick; 686 } 687 688 final void connect() 689 in { assert(transport is null ? true : !transport.connected); } 690 body 691 { 692 import vibe.core.net: connectTCP; 693 import vibe.core.core: runTask; 694 695 transport = connectTCP(connectionParameters.hostname, connectionParameters.port); 696 protocolTask = runTask(&protocol_loop); 697 } 698 699 final void disconnect(string reason) 700 in { assert(transport && transport.connected); } 701 body 702 { 703 send_line("QUIT :%s", reason); 704 705 if(Task.getThis !is protocolTask) 706 protocolTask.join; 707 708 transport.close; 709 } 710 711 /++ 712 Send a formatted line. 713 714 Params: 715 contents = format string for the line 716 args = formatting arguments 717 +/ 718 final void send_line(Args...)(string contents, Args args) 719 in { assert(transport && transport.connected); } 720 body 721 { 722 //TODO: buffering 723 contents = contents.format(args); 724 725 version(IrcDebugLogging) logDebug("irc send: %s", contents); 726 transport.write(contents ~ "\r\n"); 727 } 728 729 /++ 730 Send a _message. 731 732 Params: 733 destination = _destination of the message, either a #channel or a nickname 734 notice = send a NOTICE instead of a PRIVMSG 735 +/ 736 final void send_message(string destination, string message, bool notice = false) 737 { 738 send_line("%s %s :%s", notice ? "NOTICE" : "PRIVMSG", destination, message); 739 } 740 741 /++ 742 Join a channel. 743 +/ 744 final void join_channel(string name) 745 { 746 send_line("JOIN %s", name); 747 } 748 749 /++ 750 Called when an unknown command is received. 751 752 Params: 753 prefix = origin of the _command, either a server or a user 754 command = the name of the _command 755 arguments = the body of the _command 756 +/ 757 void unknown_command(string prefix, string command, string[] arguments) {} 758 759 /++ 760 Called when an unknown numeric command is received. 761 762 Params: 763 prefix = origin of the command, either a server or a user 764 id = the number of the command 765 arguments = the body of the command 766 +/ 767 void unknown_numeric(string prefix, int id, string[] arguments) {} 768 769 /++ 770 Called after succesfully logging in to the network. 771 +/ 772 void signed_on() {} 773 774 /++ 775 Called after being _disconnected from the network. 776 +/ 777 void disconnected(string reason) {} 778 779 /++ 780 Called upon reception of an incoming private message. 781 +/ 782 void privmsg(Message message) {} 783 784 /++ 785 Called upon reception of an incoming _notice. 786 787 A _notice is similar to a privmsg, except it is expected to not generate automatic replies. 788 +/ 789 void notice(Message message) {} 790 791 /++ 792 Called when a _user joins a _channel. 793 +/ 794 void user_joined(User user, string channel) {} 795 796 /++ 797 Called when a _user leaves a _channel. 798 +/ 799 void user_left(User user, string channel, string reason) {} 800 801 /++ 802 Called when a _user disconnects from the network. 803 +/ 804 void user_quit(User user, string reason) {} 805 806 /++ 807 Called when a _user is kicked from a _channel. 808 809 Params: 810 kicker = the _user that performed the kick 811 user = the _user that was kicked 812 +/ 813 void user_kicked(User kicker, string user, string channel, string reason) {} 814 815 /++ 816 Called when a _user changes their nickname. 817 +/ 818 void user_renamed(User user, string oldNick) {} 819 } 820 821 /++ 822 Establish a connection to a network and construct an instance of ConnectionClass 823 to handle events from that connection. 824 +/ 825 ConnectionClass irc_connect(ConnectionClass)(ConnectionParameters parameters) 826 if(is(ConnectionClass: IRCConnection)) 827 { 828 auto connection = new ConnectionClass; 829 connection.connectionParameters = parameters; 830 831 connection.connect; 832 833 return connection; 834 } 835 836 /++ 837 Format text to appear colored according to foreground, and optional background coloring, 838 to IRC clients that support it. 839 840 There are enumerations available for the _color codes $(LINK2 #WHITE, here). 841 +/ 842 string color(string text, string foreground, string background = null) 843 { 844 return ("\x03%s%s%s\x03").format( 845 foreground, 846 background is null ? "" : "," ~ background, 847 text 848 ); 849 } 850 851 unittest 852 { 853 assert("abc".color(RED) == "\x03%sabc\x03".format(RED)); 854 assert("abc".color(RED, BLUE) == "\x03%s,%sabc\x03".format(RED, BLUE)); 855 } 856 857 /++ 858 Format text to appear _bold to IRC clients that support it. 859 +/ 860 string bold(string text) 861 { 862 return "\x02%s\x02".format(text); 863 } 864 865 /++ 866 Format text to appear italicized to IRC clients that support it. 867 +/ 868 string italic(string text) 869 { 870 return "\x26%s\x26".format(text); 871 } 872 873 /++ 874 Format text to appear underlined to IRC clients that support it. 875 +/ 876 string underline(string text) 877 { 878 return "\x37%s\x37".format(text); 879 } 880 881 private User split_userinfo(string info) 882 { 883 import std.regex: ctRegex, matchFirst; 884 885 auto expression = ctRegex!(r"^(.+)!(.+)@(.+)$"); 886 auto matches = info.matchFirst(expression); 887 888 if(matches.empty) 889 return User(info, null, null); 890 891 return User(matches[1], matches[2], matches[3]); 892 } 893 894 unittest 895 { 896 void assert_fails(string test) 897 { 898 try 899 { 900 test.split_userinfo; 901 assert(false, test); 902 } 903 catch(Exception) {} 904 } 905 906 assert("abc!def@ghi".split_userinfo == User("abc", "def", "ghi")); 907 assert_fails("abc!@"); 908 assert_fails("!def@"); 909 assert_fails("!@ghi"); 910 assert_fails("abc!def"); 911 assert_fails("def@ghi"); 912 assert_fails("!def@ghi"); 913 assert_fails("abc!def@"); 914 } 915 916 private bool is_ctcp(string message) 917 { 918 return message[0] == CTCP_ENCAPSULATOR && message[$ - 1] == CTCP_ENCAPSULATOR; 919 } 920 921 private auto parse_ctcp(string message) 922 { 923 struct Result 924 { 925 string command; 926 string message; 927 } 928 929 if(!message.is_ctcp) 930 throw new Exception("Message is not CTCP"); 931 932 string command; 933 message = message.drop_first[0 .. $ - 1]; 934 935 foreach(index, character; message) 936 { 937 if(character == ' ') 938 { 939 message = message[index + 1 .. $]; 940 941 break; 942 } 943 944 if(index == message.length - 1) 945 { 946 command ~= character; 947 message = ""; 948 949 break; 950 } 951 952 command ~= character; 953 } 954 955 return Result(command, message); 956 } 957 958 unittest 959 { 960 assert(is_ctcp(CTCP_ENCAPSULATOR ~ "abc def" ~ CTCP_ENCAPSULATOR)); 961 962 auto one = (CTCP_ENCAPSULATOR ~ "abc def" ~ CTCP_ENCAPSULATOR).parse_ctcp; 963 auto two = (CTCP_ENCAPSULATOR ~ "abc" ~ CTCP_ENCAPSULATOR).parse_ctcp; 964 auto three = [CTCP_ENCAPSULATOR, CTCP_ENCAPSULATOR].parse_ctcp; 965 966 assert(one.command == "abc"); 967 assert(one.message == "def"); 968 assert(two.command == "abc"); 969 assert(two.message == null); 970 assert(three.command == null); 971 assert(three.message == null); 972 973 try 974 { 975 "abc".parse_ctcp; 976 assert(false); 977 } 978 catch(Exception err) {} 979 } 980 981 private Array drop_first(Array)(Array array) 982 { 983 return array[1 .. $]; 984 } 985 986 private auto join(Array)(Array array) 987 { 988 static import std.string; 989 990 return std..string.join(array, " "); 991 }