1 /** 2 * Common IRC code. 3 * 4 * License: 5 * This Source Code Form is subject to the terms of 6 * the Mozilla Public License, v. 2.0. If a copy of 7 * the MPL was not distributed with this file, You 8 * can obtain one at http://mozilla.org/MPL/2.0/. 9 * 10 * Authors: 11 * Stéphan Kochen <stephan@kochen.nl> 12 * Vladimir Panteleev <ae@cy.md> 13 * Vincent Povirk <madewokherd@gmail.com> 14 * Simon Arlott 15 */ 16 17 module ae.net.irc.common; 18 19 import core.time; 20 import std.algorithm.comparison; 21 import std.algorithm.iteration; 22 import std.array; 23 import std.exception; 24 import std.string; 25 import std.utf; 26 27 import ae.net.asockets; 28 29 debug(IRC) import std.stdio : stderr; 30 31 /// Types of a chat message. 32 enum IrcMessageType 33 { 34 NORMAL, /// PRIVMSG. 35 ACTION, /// PRIVMSG with an ACTION CTCP. (TODO remove this, as it is orthogonal to the IRC message type) 36 NOTICE, /// NOTICE message. 37 } 38 39 static assert(toLower("[") == "[" && toUpper("[") == "["); 40 static assert(toLower("]") == "]" && toUpper("]") == "]"); 41 static assert(toLower("{") == "{" && toUpper("{") == "{"); 42 static assert(toLower("}") == "}" && toUpper("}") == "}"); 43 static assert(toLower("|") == "|" && toUpper("|") == "|"); 44 static assert(toLower("\\") == "\\" && toUpper("\\") == "\\"); 45 46 /// RFC1459 case mapping. 47 char rfc1459toLower(char c) pure 48 { 49 if (c >= 'A' && c <= ']') 50 c += ('a' - 'A'); 51 return c; 52 } 53 54 /// ditto 55 char rfc1459toUpper(char c) pure 56 { 57 if (c >= 'a' && c <= '}') 58 c -= ('a' - 'A'); 59 return c; 60 } 61 62 /// ditto 63 string rfc1459toLower(string name) pure 64 { 65 return name.byChar.map!rfc1459toLower.array; 66 } 67 68 69 /// ditto 70 string rfc1459toUpper(string name) pure 71 { 72 return name.byChar.map!rfc1459toUpper.array; 73 } 74 75 unittest 76 { 77 assert(rfc1459toLower("{}|[]\\") == "{}|{}|"); 78 assert(rfc1459toUpper("{}|[]\\") == "[]\\[]\\"); 79 } 80 81 /// Like `icmp`, but honoring RFC1459 case mapping rules. 82 int rfc1459cmp(in char[] a, in char[] b) 83 { 84 return cmp(a.byChar.map!rfc1459toUpper, b.byChar.map!rfc1459toUpper); 85 } 86 87 unittest 88 { 89 assert(rfc1459cmp("{}|[]\\", "[]\\[]\\") == 0); 90 assert(rfc1459cmp("a", "b") == -1); 91 } 92 93 /// Base class for an IRC client-server connection. 94 final class IrcConnection 95 { 96 private: 97 LineBufferedAdapter line; 98 TimeoutAdapter timer; 99 100 public: 101 IConnection conn; /// Underlying transport. 102 alias conn this; 103 104 this(IConnection c, size_t maxLineLength = 512) 105 { 106 c = line = new LineBufferedAdapter(c); 107 line.delimiter = "\n"; 108 line.maxLength = maxLineLength; 109 110 c = timer = new TimeoutAdapter(c); 111 timer.setIdleTimeout(90.seconds); 112 timer.handleIdleTimeout = &onIdleTimeout; 113 timer.handleNonIdle = &onNonIdle; 114 115 conn = c; 116 conn.handleReadData = &onReadData; 117 } /// 118 119 /// Send `line`, plus a newline. 120 void send(string line) 121 { 122 debug(IRC) stderr.writeln("> ", line); 123 // Send with \r\n, but support receiving with \n 124 import ae.sys.data; 125 conn.send(Data(line ~ "\r\n")); 126 } 127 128 /// Inactivity handler (for sending a `PING` request). 129 void delegate() handleInactivity; 130 131 /// Timeout handler - called if `handleInactivity` was null or did not result in activity. 132 void delegate() handleTimeout; 133 134 /// Data handler. 135 void delegate(string line) handleReadLine; 136 137 private: 138 void onNonIdle() 139 { 140 if (pingSent) 141 pingSent = false; 142 } 143 144 void onReadData(Data data) 145 { 146 string line = (cast(string)data.toHeap()).chomp("\r"); 147 debug(IRC) stderr.writeln("< ", line); 148 149 if (handleReadLine) 150 handleReadLine(line); 151 } 152 153 void onIdleTimeout() 154 { 155 if (pingSent || handleInactivity is null || conn.state != ConnectionState.connected) 156 { 157 if (handleTimeout) 158 handleTimeout(); 159 else 160 conn.disconnect("Time-out", DisconnectType.error); 161 } 162 else 163 { 164 handleInactivity(); 165 pingSent = true; 166 } 167 } 168 169 bool pingSent; 170 } 171 172 // TODO: this is server-specific 173 deprecated const string IRC_NICK_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-`"; 174 deprecated const string IRC_USER_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; 175 deprecated const string IRC_HOST_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-."; 176 177 /// Numeric IRC replies. 178 enum Reply 179 { 180 RPL_WELCOME = 001, /// 181 RPL_YOURHOST = 002, /// 182 RPL_CREATED = 003, /// 183 RPL_MYINFO = 004, /// 184 RPL_BOUNCE = 005, /// 185 RPL_TRACELINK = 200, /// 186 RPL_TRACECONNECTING = 201, /// 187 RPL_TRACEHANDSHAKE = 202, /// 188 RPL_TRACEUNKNOWN = 203, /// 189 RPL_TRACEOPERATOR = 204, /// 190 RPL_TRACEUSER = 205, /// 191 RPL_TRACESERVER = 206, /// 192 RPL_TRACESERVICE = 207, /// 193 RPL_TRACENEWTYPE = 208, /// 194 RPL_TRACECLASS = 209, /// 195 RPL_TRACERECONNECT = 210, /// 196 RPL_STATSLINKINFO = 211, /// 197 RPL_STATSCOMMANDS = 212, /// 198 RPL_STATSCLINE = 213, /// 199 RPL_STATSNLINE = 214, /// 200 RPL_STATSILINE = 215, /// 201 RPL_STATSKLINE = 216, /// 202 RPL_STATSQLINE = 217, /// 203 RPL_STATSYLINE = 218, /// 204 RPL_ENDOFSTATS = 219, /// 205 RPL_UMODEIS = 221, /// 206 RPL_SERVICEINFO = 231, /// 207 RPL_ENDOFSERVICES = 232, /// 208 RPL_SERVICE = 233, /// 209 RPL_SERVLIST = 234, /// 210 RPL_SERVLISTEND = 235, /// 211 RPL_STATSVLINE = 240, /// 212 RPL_STATSLLINE = 241, /// 213 RPL_STATSUPTIME = 242, /// 214 RPL_STATSOLINE = 243, /// 215 RPL_STATSHLINE = 244, /// 216 RPL_STATSSLINE = 244, /// 217 RPL_STATSPING = 246, /// 218 RPL_STATSBLINE = 247, /// 219 RPL_STATSDLINE = 250, /// 220 RPL_LUSERCLIENT = 251, /// 221 RPL_LUSEROP = 252, /// 222 RPL_LUSERUNKNOWN = 253, /// 223 RPL_LUSERCHANNELS = 254, /// 224 RPL_LUSERME = 255, /// 225 RPL_ADMINME = 256, /// 226 RPL_ADMINLOC1 = 257, /// 227 RPL_ADMINLOC2 = 258, /// 228 RPL_ADMINEMAIL = 259, /// 229 RPL_TRACELOG = 261, /// 230 RPL_TRACEEND = 262, /// 231 RPL_TRYAGAIN = 263, /// 232 RPL_NONE = 300, /// 233 RPL_AWAY = 301, /// 234 RPL_USERHOST = 302, /// 235 RPL_ISON = 303, /// 236 RPL_UNAWAY = 305, /// 237 RPL_NOWAWAY = 306, /// 238 RPL_WHOISUSER = 311, /// 239 RPL_WHOISSERVER = 312, /// 240 RPL_WHOISOPERATOR = 313, /// 241 RPL_WHOWASUSER = 314, /// 242 RPL_ENDOFWHO = 315, /// 243 RPL_WHOISCHANOP = 316, /// 244 RPL_WHOISIDLE = 317, /// 245 RPL_ENDOFWHOIS = 318, /// 246 RPL_WHOISCHANNELS = 319, /// 247 RPL_LISTSTART = 321, /// 248 RPL_LIST = 322, /// 249 RPL_LISTEND = 323, /// 250 RPL_CHANNELMODEIS = 324, /// 251 RPL_UNIQOPIS = 325, /// 252 RPL_NOTOPIC = 331, /// 253 RPL_TOPIC = 332, /// 254 RPL_INVITING = 341, /// 255 RPL_SUMMONING = 342, /// 256 RPL_INVITELIST = 346, /// 257 RPL_ENDOFINVITELIST = 347, /// 258 RPL_EXCEPTLIST = 348, /// 259 RPL_ENDOFEXCEPTLIST = 349, /// 260 RPL_VERSION = 351, /// 261 RPL_WHOREPLY = 352, /// 262 RPL_NAMREPLY = 353, /// 263 RPL_KILLDONE = 361, /// 264 RPL_CLOSING = 362, /// 265 RPL_CLOSEEND = 363, /// 266 RPL_LINKS = 364, /// 267 RPL_ENDOFLINKS = 365, /// 268 RPL_ENDOFNAMES = 366, /// 269 RPL_BANLIST = 367, /// 270 RPL_ENDOFBANLIST = 368, /// 271 RPL_ENDOFWHOWAS = 369, /// 272 RPL_INFO = 371, /// 273 RPL_MOTD = 372, /// 274 RPL_INFOSTART = 373, /// 275 RPL_ENDOFINFO = 374, /// 276 RPL_MOTDSTART = 375, /// 277 RPL_ENDOFMOTD = 376, /// 278 RPL_YOUREOPER = 381, /// 279 RPL_REHASHING = 382, /// 280 RPL_YOURESERVICE = 383, /// 281 RPL_MYPORTIS = 384, /// 282 RPL_TIME = 391, /// 283 RPL_USERSSTART = 392, /// 284 RPL_USERS = 393, /// 285 RPL_ENDOFUSERS = 394, /// 286 RPL_NOUSERS = 395, /// 287 ERR_NOSUCHNICK = 401, /// 288 ERR_NOSUCHSERVER = 402, /// 289 ERR_NOSUCHCHANNEL = 403, /// 290 ERR_CANNOTSENDTOCHAN = 404, /// 291 ERR_TOOMANYCHANNELS = 405, /// 292 ERR_WASNOSUCHNICK = 406, /// 293 ERR_TOOMANYTARGETS = 407, /// 294 ERR_NOSUCHSERVICE = 408, /// 295 ERR_NOORIGIN = 409, /// 296 ERR_NORECIPIENT = 411, /// 297 ERR_NOTEXTTOSEND = 412, /// 298 ERR_NOTOPLEVEL = 413, /// 299 ERR_WILDTOPLEVEL = 414, /// 300 ERR_BADMASK = 415, /// 301 ERR_UNKNOWNCOMMAND = 421, /// 302 ERR_NOMOTD = 422, /// 303 ERR_NOADMININFO = 423, /// 304 ERR_FILEERROR = 424, /// 305 ERR_NONICKNAMEGIVEN = 431, /// 306 ERR_ERRONEUSNICKNAME = 432, /// 307 ERR_NICKNAMEINUSE = 433, /// 308 ERR_NICKCOLLISION = 436, /// 309 ERR_UNAVAILRESOURCE = 437, /// 310 ERR_USERNOTINCHANNEL = 441, /// 311 ERR_NOTONCHANNEL = 442, /// 312 ERR_USERONCHANNEL = 443, /// 313 ERR_NOLOGIN = 444, /// 314 ERR_SUMMONDISABLED = 445, /// 315 ERR_USERSDISABLED = 446, /// 316 ERR_NOTREGISTERED = 451, /// 317 ERR_NEEDMOREPARAMS = 461, /// 318 ERR_ALREADYREGISTRED = 462, /// 319 ERR_NOPERMFORHOST = 463, /// 320 ERR_PASSWDMISMATCH = 464, /// 321 ERR_YOUREBANNEDCREEP = 465, /// 322 ERR_YOUWILLBEBANNED = 466, /// 323 ERR_KEYSET = 467, /// 324 ERR_CHANNELISFULL = 471, /// 325 ERR_UNKNOWNMODE = 472, /// 326 ERR_INVITEONLYCHAN = 473, /// 327 ERR_BANNEDFROMCHAN = 474, /// 328 ERR_BADCHANNELKEY = 475, /// 329 ERR_BADCHANMASK = 476, /// 330 ERR_NOCHANMODES = 477, /// 331 ERR_BANLISTFULL = 478, /// 332 ERR_NOPRIVILEGES = 481, /// 333 ERR_CHANOPRIVSNEEDED = 482, /// 334 ERR_CANTKILLSERVER = 483, /// 335 ERR_RESTRICTED = 484, /// 336 ERR_UNIQOPPRIVSNEEDED = 485, /// 337 ERR_NOOPERHOST = 491, /// 338 ERR_NOSERVICEHOST = 492, /// 339 ERR_UMODEUNKNOWNFLAG = 501, /// 340 ERR_USERSDONTMATCH = 502, /// 341 }