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 import ae.utils.array : asBytes; 29 30 debug(IRC) import std.stdio : stderr; 31 32 /// Types of a chat message. 33 enum IrcMessageType 34 { 35 NORMAL, /// PRIVMSG. 36 ACTION, /// PRIVMSG with an ACTION CTCP. (TODO remove this, as it is orthogonal to the IRC message type) 37 NOTICE, /// NOTICE message. 38 } 39 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 static assert(toLower("\\") == "\\" && toUpper("\\") == "\\"); 46 47 /// RFC1459 case mapping. 48 char rfc1459toLower(char c) pure 49 { 50 if (c >= 'A' && c <= ']') 51 c += ('a' - 'A'); 52 return c; 53 } 54 55 /// ditto 56 char rfc1459toUpper(char c) pure 57 { 58 if (c >= 'a' && c <= '}') 59 c -= ('a' - 'A'); 60 return c; 61 } 62 63 /// ditto 64 string rfc1459toLower(string name) pure 65 { 66 return name.byChar.map!rfc1459toLower.array; 67 } 68 69 70 /// ditto 71 string rfc1459toUpper(string name) pure 72 { 73 return name.byChar.map!rfc1459toUpper.array; 74 } 75 76 unittest 77 { 78 assert(rfc1459toLower("{}|[]\\") == "{}|{}|"); 79 assert(rfc1459toUpper("{}|[]\\") == "[]\\[]\\"); 80 } 81 82 /// Like `icmp`, but honoring RFC1459 case mapping rules. 83 int rfc1459cmp(in char[] a, in char[] b) 84 { 85 return cmp(a.byChar.map!rfc1459toUpper, b.byChar.map!rfc1459toUpper); 86 } 87 88 unittest 89 { 90 assert(rfc1459cmp("{}|[]\\", "[]\\[]\\") == 0); 91 assert(rfc1459cmp("a", "b") == -1); 92 } 93 94 /// Base class for an IRC client-server connection. 95 final class IrcConnection 96 { 97 private: 98 LineBufferedAdapter line; 99 TimeoutAdapter timer; 100 101 public: 102 IConnection conn; /// Underlying transport. 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.asBytes ~ "\r\n".asBytes)); 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 /// Forwards to the underlying transport. 138 @property void handleConnect(IConnection.ConnectHandler value) { conn.handleConnect = value; } 139 @property void handleDisconnect(IConnection.DisconnectHandler value) { conn.handleDisconnect = value; } /// ditto 140 void disconnect(string reason = IConnection.defaultDisconnectReason, DisconnectType type = DisconnectType.requested) { conn.disconnect(reason, type); } /// ditto 141 @property ConnectionState state() { return conn.state; } /// ditto 142 143 private: 144 void onNonIdle() 145 { 146 if (pingSent) 147 pingSent = false; 148 } 149 150 void onReadData(Data data) 151 { 152 string line = data.asDataOf!char.toGC().chomp("\r"); 153 debug(IRC) stderr.writeln("< ", line); 154 155 if (handleReadLine) 156 handleReadLine(line); 157 } 158 159 void onIdleTimeout() 160 { 161 if (pingSent || handleInactivity is null || conn.state != ConnectionState.connected) 162 { 163 if (handleTimeout) 164 handleTimeout(); 165 else 166 conn.disconnect("Time-out", DisconnectType.error); 167 } 168 else 169 { 170 handleInactivity(); 171 pingSent = true; 172 } 173 } 174 175 bool pingSent; 176 } 177 178 // TODO: this is server-specific 179 deprecated const string IRC_NICK_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-`"; 180 deprecated const string IRC_USER_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; 181 deprecated const string IRC_HOST_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-."; 182 183 /// Numeric IRC replies. 184 enum Reply 185 { 186 RPL_WELCOME = 001, /// 187 RPL_YOURHOST = 002, /// 188 RPL_CREATED = 003, /// 189 RPL_MYINFO = 004, /// 190 RPL_BOUNCE = 005, /// 191 RPL_TRACELINK = 200, /// 192 RPL_TRACECONNECTING = 201, /// 193 RPL_TRACEHANDSHAKE = 202, /// 194 RPL_TRACEUNKNOWN = 203, /// 195 RPL_TRACEOPERATOR = 204, /// 196 RPL_TRACEUSER = 205, /// 197 RPL_TRACESERVER = 206, /// 198 RPL_TRACESERVICE = 207, /// 199 RPL_TRACENEWTYPE = 208, /// 200 RPL_TRACECLASS = 209, /// 201 RPL_TRACERECONNECT = 210, /// 202 RPL_STATSLINKINFO = 211, /// 203 RPL_STATSCOMMANDS = 212, /// 204 RPL_STATSCLINE = 213, /// 205 RPL_STATSNLINE = 214, /// 206 RPL_STATSILINE = 215, /// 207 RPL_STATSKLINE = 216, /// 208 RPL_STATSQLINE = 217, /// 209 RPL_STATSYLINE = 218, /// 210 RPL_ENDOFSTATS = 219, /// 211 RPL_UMODEIS = 221, /// 212 RPL_SERVICEINFO = 231, /// 213 RPL_ENDOFSERVICES = 232, /// 214 RPL_SERVICE = 233, /// 215 RPL_SERVLIST = 234, /// 216 RPL_SERVLISTEND = 235, /// 217 RPL_STATSVLINE = 240, /// 218 RPL_STATSLLINE = 241, /// 219 RPL_STATSUPTIME = 242, /// 220 RPL_STATSOLINE = 243, /// 221 RPL_STATSHLINE = 244, /// 222 RPL_STATSSLINE = 244, /// 223 RPL_STATSPING = 246, /// 224 RPL_STATSBLINE = 247, /// 225 RPL_STATSDLINE = 250, /// 226 RPL_LUSERCLIENT = 251, /// 227 RPL_LUSEROP = 252, /// 228 RPL_LUSERUNKNOWN = 253, /// 229 RPL_LUSERCHANNELS = 254, /// 230 RPL_LUSERME = 255, /// 231 RPL_ADMINME = 256, /// 232 RPL_ADMINLOC1 = 257, /// 233 RPL_ADMINLOC2 = 258, /// 234 RPL_ADMINEMAIL = 259, /// 235 RPL_TRACELOG = 261, /// 236 RPL_TRACEEND = 262, /// 237 RPL_TRYAGAIN = 263, /// 238 RPL_NONE = 300, /// 239 RPL_AWAY = 301, /// 240 RPL_USERHOST = 302, /// 241 RPL_ISON = 303, /// 242 RPL_UNAWAY = 305, /// 243 RPL_NOWAWAY = 306, /// 244 RPL_WHOISUSER = 311, /// 245 RPL_WHOISSERVER = 312, /// 246 RPL_WHOISOPERATOR = 313, /// 247 RPL_WHOWASUSER = 314, /// 248 RPL_ENDOFWHO = 315, /// 249 RPL_WHOISCHANOP = 316, /// 250 RPL_WHOISIDLE = 317, /// 251 RPL_ENDOFWHOIS = 318, /// 252 RPL_WHOISCHANNELS = 319, /// 253 RPL_LISTSTART = 321, /// 254 RPL_LIST = 322, /// 255 RPL_LISTEND = 323, /// 256 RPL_CHANNELMODEIS = 324, /// 257 RPL_UNIQOPIS = 325, /// 258 RPL_NOTOPIC = 331, /// 259 RPL_TOPIC = 332, /// 260 RPL_INVITING = 341, /// 261 RPL_SUMMONING = 342, /// 262 RPL_INVITELIST = 346, /// 263 RPL_ENDOFINVITELIST = 347, /// 264 RPL_EXCEPTLIST = 348, /// 265 RPL_ENDOFEXCEPTLIST = 349, /// 266 RPL_VERSION = 351, /// 267 RPL_WHOREPLY = 352, /// 268 RPL_NAMREPLY = 353, /// 269 RPL_KILLDONE = 361, /// 270 RPL_CLOSING = 362, /// 271 RPL_CLOSEEND = 363, /// 272 RPL_LINKS = 364, /// 273 RPL_ENDOFLINKS = 365, /// 274 RPL_ENDOFNAMES = 366, /// 275 RPL_BANLIST = 367, /// 276 RPL_ENDOFBANLIST = 368, /// 277 RPL_ENDOFWHOWAS = 369, /// 278 RPL_INFO = 371, /// 279 RPL_MOTD = 372, /// 280 RPL_INFOSTART = 373, /// 281 RPL_ENDOFINFO = 374, /// 282 RPL_MOTDSTART = 375, /// 283 RPL_ENDOFMOTD = 376, /// 284 RPL_YOUREOPER = 381, /// 285 RPL_REHASHING = 382, /// 286 RPL_YOURESERVICE = 383, /// 287 RPL_MYPORTIS = 384, /// 288 RPL_TIME = 391, /// 289 RPL_USERSSTART = 392, /// 290 RPL_USERS = 393, /// 291 RPL_ENDOFUSERS = 394, /// 292 RPL_NOUSERS = 395, /// 293 ERR_NOSUCHNICK = 401, /// 294 ERR_NOSUCHSERVER = 402, /// 295 ERR_NOSUCHCHANNEL = 403, /// 296 ERR_CANNOTSENDTOCHAN = 404, /// 297 ERR_TOOMANYCHANNELS = 405, /// 298 ERR_WASNOSUCHNICK = 406, /// 299 ERR_TOOMANYTARGETS = 407, /// 300 ERR_NOSUCHSERVICE = 408, /// 301 ERR_NOORIGIN = 409, /// 302 ERR_NORECIPIENT = 411, /// 303 ERR_NOTEXTTOSEND = 412, /// 304 ERR_NOTOPLEVEL = 413, /// 305 ERR_WILDTOPLEVEL = 414, /// 306 ERR_BADMASK = 415, /// 307 ERR_UNKNOWNCOMMAND = 421, /// 308 ERR_NOMOTD = 422, /// 309 ERR_NOADMININFO = 423, /// 310 ERR_FILEERROR = 424, /// 311 ERR_NONICKNAMEGIVEN = 431, /// 312 ERR_ERRONEUSNICKNAME = 432, /// 313 ERR_NICKNAMEINUSE = 433, /// 314 ERR_NICKCOLLISION = 436, /// 315 ERR_UNAVAILRESOURCE = 437, /// 316 ERR_USERNOTINCHANNEL = 441, /// 317 ERR_NOTONCHANNEL = 442, /// 318 ERR_USERONCHANNEL = 443, /// 319 ERR_NOLOGIN = 444, /// 320 ERR_SUMMONDISABLED = 445, /// 321 ERR_USERSDISABLED = 446, /// 322 ERR_NOTREGISTERED = 451, /// 323 ERR_NEEDMOREPARAMS = 461, /// 324 ERR_ALREADYREGISTRED = 462, /// 325 ERR_NOPERMFORHOST = 463, /// 326 ERR_PASSWDMISMATCH = 464, /// 327 ERR_YOUREBANNEDCREEP = 465, /// 328 ERR_YOUWILLBEBANNED = 466, /// 329 ERR_KEYSET = 467, /// 330 ERR_CHANNELISFULL = 471, /// 331 ERR_UNKNOWNMODE = 472, /// 332 ERR_INVITEONLYCHAN = 473, /// 333 ERR_BANNEDFROMCHAN = 474, /// 334 ERR_BADCHANNELKEY = 475, /// 335 ERR_BADCHANMASK = 476, /// 336 ERR_NOCHANMODES = 477, /// 337 ERR_BANLISTFULL = 478, /// 338 ERR_NOPRIVILEGES = 481, /// 339 ERR_CHANOPRIVSNEEDED = 482, /// 340 ERR_CANTKILLSERVER = 483, /// 341 ERR_RESTRICTED = 484, /// 342 ERR_UNIQOPPRIVSNEEDED = 485, /// 343 ERR_NOOPERHOST = 491, /// 344 ERR_NOSERVICEHOST = 492, /// 345 ERR_UMODEUNKNOWNFLAG = 501, /// 346 ERR_USERSDONTMATCH = 502, /// 347 }