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; 28 } 29 30 private enum CTCP_ENCAPSULATOR = '\x01'; 31 32 /++ 33 A struct containing details about a connection. 34 +/ 35 struct ConnectionParameters 36 { 37 string hostname; ///The _hostname or address of the IRC server. 38 ushort port; ///The _port of the server. 39 string password = null; ///The _password for the server, if any. 40 string username = "vibeIRC"; ///The _username, later used by the server in this connection's hostmask. 41 string realname = null; ///The _realname as returned by the WHOIS command. 42 } 43 44 /++ 45 A struct containing details about a user. 46 +/ 47 struct User 48 { 49 string nickname; ///The _nickname of this user. 50 string username; ///The _username portion of this user's hostmask. 51 string hostname; ///The _hostname portion of this user's hostmask. 52 } 53 54 /++ 55 A struct containing details about an incoming message. 56 +/ 57 struct Message 58 { 59 User sender; ///The user who sent the message. 60 string receiver; ///The destination of the message, either a user or a channel. 61 string ctcpCommand; ///The CTCP command, if any. 62 string message; ///The _message body. 63 64 /++ 65 Returns whether this message uses CTCP. 66 +/ 67 @property bool isCTCP() 68 { 69 return ctcpCommand != null; 70 } 71 } 72 73 //Thrown from line_received, handle_numeric or handle_command in case of an error 74 private class GracelessDisconnect: Exception 75 { 76 this(string msg) 77 { 78 super(msg); 79 } 80 } 81 82 /++ 83 The base class for IRC connections. 84 +/ 85 class IRCConnection 86 { 87 import vibe.core.net: TCPConnection; 88 import vibe.core.log: logDebug, logError, logInfo; //FIXME: logInfo 89 90 ConnectionParameters connectionParameters; ///The connection parameters passed to $(SYMBOL_LINK irc_connect). 91 TCPConnection transport; ///The vibe socket underlying this connection. 92 private string _nickname; 93 94 /++ 95 Default constructor. Should not be called from user code. 96 97 See_Also: 98 $(SYMBOL_LINK irc_connect) 99 +/ 100 protected this() {} 101 102 private void protocol_loop() 103 in { assert(transport && transport.connected); } 104 body 105 { 106 import vibe.stream.operations: readLine; 107 108 string disconnectReason = "Connection terminated gracefully"; 109 110 logDebug("irc connected"); 111 112 if(connectionParameters.password != null) 113 send_line("PASS %s", connectionParameters.password); 114 115 send_line("NICK %s", nickname); 116 send_line("USER %s 0 * :%s", connectionParameters.username, connectionParameters.realname); 117 118 while(transport.connected) 119 { 120 string line; 121 122 try 123 line = cast(string)transport.readLine; 124 catch(Exception err) 125 { 126 logError(err.toString); 127 128 break; 129 } 130 131 version(IrcDebugLogging) logDebug("irc recv: %s", line); 132 133 try 134 line_received(line); 135 catch(GracelessDisconnect err) 136 { 137 disconnectReason = err.msg; 138 139 transport.close; 140 } 141 } 142 143 disconnected(disconnectReason); 144 logDebug("irc disconnected"); 145 } 146 147 private void line_received(string line) 148 { 149 import std.conv: ConvException, to; 150 151 string[] parts = line.split(" "); 152 153 switch(parts[0]) 154 { 155 case "PING": 156 send_line("PONG %s", parts[1]); 157 158 break; 159 case "ERROR": 160 throw new GracelessDisconnect(parts.drop_first.join.drop_first); 161 default: 162 parts[0] = parts[0].drop_first; 163 164 try 165 handle_numeric(parts[0], parts[1].to!int, parts[2 .. $]); 166 catch(ConvException err) 167 handle_command(parts[0], parts[1], parts[2 .. $]); 168 } 169 } 170 171 private void handle_command(string prefix, string command, string[] parts) 172 { 173 logDebug("handle_command(%s, %s, %s)", prefix, command, parts); 174 175 switch(command) 176 { 177 case "NOTICE": 178 case "PRIVMSG": 179 Message msg; 180 string message = parts.drop_first.join; 181 msg.sender = prefix.split_userinfo; 182 msg.receiver = parts[0]; 183 msg.message = message != null ? message.drop_first : ""; 184 185 if(message.is_ctcp) 186 { 187 auto parsedCtcp = message.parse_ctcp; 188 msg.ctcpCommand = parsedCtcp.command; 189 msg.message = parsedCtcp.message; 190 } 191 192 if(command == "NOTICE") 193 notice(msg); 194 else 195 privmsg(msg); 196 197 break; 198 case "JOIN": 199 user_joined(prefix.split_userinfo, parts[0].drop_first); 200 201 break; 202 case "PART": 203 user_left(prefix.split_userinfo, parts[0], parts.drop_first.join.drop_first); 204 205 break; 206 case "QUIT": 207 user_quit(prefix.split_userinfo, parts.join.drop_first); 208 209 break; 210 case "NICK": 211 user_renamed(prefix.split_userinfo, parts[0].drop_first); 212 213 break; 214 case "KICK": 215 user_kicked(prefix.split_userinfo, parts[1], parts[0], parts[2 .. $].join.drop_first); 216 217 break; 218 default: 219 unknown_command(prefix, command, parts); 220 } 221 } 222 223 private void handle_numeric(string prefix, int id, string[] parts) 224 { 225 logDebug("handle_numeric(%s, %s, %s)", prefix, id, parts); 226 227 switch(id) 228 { 229 case 1: //RPL_WELCOME (connection success) 230 signed_on; 231 232 break; 233 case 432: //ERR_ERRONEUSNICKNAME 234 throw new GracelessDisconnect("Erroneus nickname"); //TODO: handle gracefully? 235 case 433: //ERR_NICKNAMEINUSE 236 throw new GracelessDisconnect("Nickname already in use"); //TODO: handle gracefully? 237 default: 238 unknown_numeric(prefix, id, parts); 239 } 240 } 241 242 /++ 243 Get this connection's _nickname. 244 +/ 245 final @property string nickname() 246 { 247 return _nickname; 248 } 249 250 /++ 251 Set this connection's _nickname. 252 +/ 253 final @property string nickname(string newNick) 254 { 255 if(transport && transport.connected) 256 send_line("NICK %s", newNick); 257 258 return _nickname = newNick; 259 } 260 261 final void connect() 262 in { assert(transport is null ? true : !transport.connected); } 263 body 264 { 265 import vibe.core.net: connectTCP; 266 import vibe.core.core: runTask; 267 268 transport = connectTCP(connectionParameters.hostname, connectionParameters.port); 269 270 runTask(&protocol_loop); 271 } 272 273 final void disconnect(string reason) 274 in { assert(transport && transport.connected); } 275 body 276 { 277 send_line("QUIT :%s", reason); 278 279 transport.close; 280 } 281 282 /++ 283 Send a formatted line. 284 285 Params: 286 contents = format string for the line 287 args = formatting arguments 288 +/ 289 final void send_line(Args...)(string contents, Args args) 290 in { assert(transport && transport.connected); } 291 body 292 { 293 import std.string: format; 294 295 //TODO: buffering 296 contents = contents.format(args); 297 298 version(IrcDebugLogging) logDebug("irc send: %s", contents); 299 transport.write(contents ~ "\r\n"); 300 } 301 302 /++ 303 Send a _message. 304 305 Params: 306 destination = _destination of the message, either a #channel or a nickname 307 notice = send a NOTICE instead of a PRIVMSG 308 +/ 309 final void send_message(string destination, string message, bool notice = false) 310 { 311 send_line("%s %s :%s", notice ? "NOTICE" : "PRIVMSG", destination, message); 312 } 313 314 /++ 315 Join a channel. 316 +/ 317 final void join_channel(string name) 318 { 319 send_line("JOIN %s", name); 320 } 321 322 /++ 323 Called when an unknown command is received. 324 325 Params: 326 prefix = origin of the _command, either a server or a user 327 command = the name of the _command 328 arguments = the body of the _command 329 +/ 330 void unknown_command(string prefix, string command, string[] arguments) {} 331 332 /++ 333 Called when an unknown numeric command is received. 334 335 Params: 336 prefix = origin of the command, either a server or a user 337 id = the number of the command 338 arguments = the body of the command 339 +/ 340 void unknown_numeric(string prefix, int id, string[] arguments) {} 341 342 /++ 343 Called after succesfully logging in to the network. 344 +/ 345 void signed_on() {} 346 347 /++ 348 Called after being _disconnected from the network. 349 +/ 350 void disconnected(string reason) {} 351 352 /++ 353 Called upon reception of an incoming private message. 354 +/ 355 void privmsg(Message message) {} 356 357 /++ 358 Called upon reception of an incoming _notice. 359 360 A _notice is similar to a privmsg, except it is expected to not generate automatic replies. 361 +/ 362 void notice(Message message) {} 363 364 /++ 365 Called when a _user joins a _channel. 366 +/ 367 void user_joined(User user, string channel) {} 368 369 /++ 370 Called when a _user leaves a _channel. 371 +/ 372 void user_left(User user, string channel, string reason) {} 373 374 /++ 375 Called when a _user disconnects from the network. 376 +/ 377 void user_quit(User user, string reason) {} 378 379 /++ 380 Called when a _user is kicked from a _channel. 381 382 Params: 383 kicker = the _user that performed the kick 384 user = the _user that was kicked 385 +/ 386 void user_kicked(User kicker, string user, string channel, string reason) {} 387 388 /++ 389 Called when a _user changes their nickname. 390 +/ 391 void user_renamed(User user, string oldNick) {} 392 } 393 394 /++ 395 Establish a connection to a network and construct an instance of ConnectionClass 396 to handle events from that connection. 397 +/ 398 ConnectionClass irc_connect(ConnectionClass)(ConnectionParameters parameters) 399 if(is(ConnectionClass: IRCConnection)) 400 { 401 auto connection = new ConnectionClass; 402 connection.connectionParameters = parameters; 403 404 connection.connect; 405 406 return connection; 407 } 408 409 private User split_userinfo(string info) 410 { 411 import std.regex: ctRegex, matchFirst; 412 413 auto expression = ctRegex!(r"^(.+)!(.+)@(.+)$"); 414 auto matches = info.matchFirst(expression); 415 416 if(matches.empty) 417 throw new Exception("Invalid userinfo: " ~ info); 418 419 return User(matches[1], matches[2], matches[3]); 420 } 421 422 unittest 423 { 424 void assert_fails(string test) 425 { 426 try 427 { 428 test.split_userinfo; 429 assert(false, test); 430 } 431 catch(Exception) {} 432 } 433 434 assert("abc!def@ghi".split_userinfo == User("abc", "def", "ghi")); 435 assert_fails("abc!@"); 436 assert_fails("!def@"); 437 assert_fails("!@ghi"); 438 assert_fails("abc!def"); 439 assert_fails("def@ghi"); 440 assert_fails("!def@ghi"); 441 assert_fails("abc!def@"); 442 } 443 444 private bool is_ctcp(string message) 445 { 446 return message[0] == CTCP_ENCAPSULATOR && message[$ - 1] == CTCP_ENCAPSULATOR; 447 } 448 449 private auto parse_ctcp(string message) 450 { 451 struct Result 452 { 453 string command; 454 string message; 455 } 456 457 if(!message.is_ctcp) 458 throw new Exception("Message is not CTCP"); 459 460 string command; 461 message = message.drop_first[0 .. $ - 1]; 462 463 foreach(index, character; message) 464 { 465 if(character == ' ') 466 { 467 message = message[index + 1 .. $]; 468 469 break; 470 } 471 472 if(index == message.length - 1) 473 { 474 command ~= character; 475 message = ""; 476 477 break; 478 } 479 480 command ~= character; 481 } 482 483 return Result(command, message); 484 } 485 486 unittest 487 { 488 assert(is_ctcp(CTCP_ENCAPSULATOR ~ "abc def" ~ CTCP_ENCAPSULATOR)); 489 490 auto one = (CTCP_ENCAPSULATOR ~ "abc def" ~ CTCP_ENCAPSULATOR).parse_ctcp; 491 auto two = (CTCP_ENCAPSULATOR ~ "abc" ~ CTCP_ENCAPSULATOR).parse_ctcp; 492 auto three = [CTCP_ENCAPSULATOR, CTCP_ENCAPSULATOR].parse_ctcp; 493 494 assert(one.command == "abc"); 495 assert(one.message == "def"); 496 assert(two.command == "abc"); 497 assert(two.message == null); 498 assert(three.command == null); 499 assert(three.message == null); 500 501 try 502 { 503 "abc".parse_ctcp; 504 assert(false); 505 } 506 catch(Exception err) {} 507 } 508 509 private Array drop_first(Array)(Array array) 510 { 511 return array[1 .. $]; 512 } 513 514 private auto join(Array)(Array array) 515 { 516 static import std.string; 517 518 return std..string.join(array, " "); 519 }