1 module irc.protocol; 2 3 import irc.exception; 4 5 import std.algorithm; 6 import std.array; 7 import std.exception; 8 import std..string; 9 import std.typetuple : staticIndexOf, TypeTuple; 10 11 @safe: 12 13 enum IRC_MAX_LEN = 512; 14 15 /* 16 * 63 is the maximum hostname length defined by the protocol. 10 is a common 17 * username limit on many networks. 1 is for the `@'. 18 * Shamelessly stolen from IRSSI, irc/core/irc-servers.c 19 */ 20 enum MAX_USERHOST_LEN = 63 + 10 + 1; 21 22 private enum AdditionalMsgLens 23 { 24 PRIVMSG = MAX_USERHOST_LEN, 25 NOTICE = MAX_USERHOST_LEN 26 } 27 28 /* 29 * Returns the additional length requirements of a method. 30 * 31 * Params: 32 * method = The method to query. 33 * Returns: 34 * The additional length requirements. 35 */ 36 template additionalMsgLen(string method) 37 { 38 static if(staticIndexOf!(method, __traits(allMembers, AdditionalMsgLens)) == -1) 39 enum additionalMsgLen = 0; 40 else 41 enum additionalMsgLen = __traits(getMember, AdditionalMsgLens, method); 42 } 43 44 unittest 45 { 46 static assert(additionalMsgLen!"PRIVMSG" == MAX_USERHOST_LEN); 47 static assert(additionalMsgLen!"NOTICE" == MAX_USERHOST_LEN); 48 static assert(additionalMsgLen!"JOIN" == 0); 49 } 50 51 enum IRC_MAX_COMMAND_PARAMETERS = 15; // RFC2812 52 53 /** 54 * Structure representing a parsed IRC message. 55 */ 56 struct IrcLine 57 { 58 /// Note: null when the message has no _prefix. 59 const(char)[] prefix; // Optional 60 /// 61 const(char)[] command; 62 /// 63 const(char)[][] arguments() @property pure nothrow @nogc 64 { 65 return argumentBuffer[0 .. numArguments]; 66 } 67 68 private const(char)[][IRC_MAX_COMMAND_PARAMETERS] argumentBuffer; 69 private size_t numArguments; 70 } 71 72 /// List of the four valid channel prefixes; 73 /// &, #, + and !. 74 alias channelPrefixes = TypeTuple!('&', '#', '+', '!'); 75 76 // [:prefix] <command> <parameters ...> [:long parameter] 77 // TODO: do something about the allocation of the argument array 78 bool parse(const(char)[] raw, out IrcLine line) pure @nogc 79 { 80 if(raw[0] == ':') 81 { 82 raw = raw[1 .. $]; 83 line.prefix = raw.munch("^ "); 84 raw.munch(" "); 85 } 86 87 line.command = raw.munch("^ "); 88 89 auto result = raw.findSplit(" :"); 90 91 const(char)[] args = result[0]; 92 args.munch(" "); 93 94 while(args.length) 95 { 96 assert(line.numArguments < line.argumentBuffer.length); 97 line.argumentBuffer[line.numArguments++] = args.munch("^ "); 98 args.munch(" "); 99 } 100 101 if(!result[2].empty) 102 { 103 assert(line.numArguments < line.argumentBuffer.length); 104 line.argumentBuffer[line.numArguments++] = result[2]; 105 } 106 107 return true; 108 } 109 110 version(unittest) 111 { 112 import std.stdio; 113 } 114 115 unittest 116 { 117 struct InputOutput 118 { 119 string input; 120 121 struct Output 122 { 123 string prefix, command; 124 string[] arguments; 125 } 126 Output output; 127 128 bool valid = true; 129 } 130 131 static InputOutput[] testData = [ 132 { 133 input: "PING 123456", 134 output: {command: "PING", arguments: ["123456"]} 135 }, 136 { 137 input: ":foo!bar@baz PRIVMSG #channel hi!", 138 output: {prefix: "foo!bar@baz", command: "PRIVMSG", arguments: ["#channel", "hi!"]} 139 }, 140 { 141 input: ":foo!bar@baz PRIVMSG #channel :hello, world!", 142 output: {prefix: "foo!bar@baz", command: "PRIVMSG", arguments: ["#channel", "hello, world!"]} 143 }, 144 { 145 input: ":foo!bar@baz 005 testnick CHANLIMIT=#:120 :are supported by this server", 146 output: {prefix: "foo!bar@baz", command: "005", arguments: ["testnick", "CHANLIMIT=#:120", "are supported by this server"]} 147 }, 148 { 149 input: ":nick!~ident@00:00:00:00::00 PRIVMSG #some.channel :some message", 150 output: {prefix: "nick!~ident@00:00:00:00::00", command: "PRIVMSG", arguments: ["#some.channel", "some message"]} 151 }, 152 { 153 input: ":foo!bar@baz JOIN :#channel", 154 output: {prefix: "foo!bar@baz", command: "JOIN", arguments: ["#channel"]} 155 } 156 ]; 157 158 foreach(i, test; testData) 159 { 160 IrcLine line; 161 bool succ = parse(test.input, line); 162 163 scope(failure) 164 { 165 writefln("irc.protocol.parse unittest failed, test #%s", i + 1); 166 writefln(`prefix: "%s"`, line.prefix); 167 writefln(`command: "%s"`, line.command); 168 writefln(`arguments: "%s"`, line.arguments); 169 } 170 171 if(test.valid) 172 { 173 assert(line.prefix == test.output.prefix); 174 assert(line.command == test.output.command); 175 assert(line.arguments == test.output.arguments); 176 } 177 else 178 assert(!succ); 179 } 180 } 181 182 /** 183 * Structure representing an IRC user. 184 */ 185 struct IrcUser 186 { 187 /// 188 const(char)[] nickName; 189 /// 190 const(char)[] userName; 191 /// 192 const(char)[] hostName; 193 194 deprecated alias nick = nickName; 195 196 // TODO: Change to use sink once formattedWrite supports them 197 version(none) string toString() const 198 { 199 return format("%s!%s@%s", nickName, userName, hostName); 200 } 201 202 void toString(scope void delegate(const(char)[]) @safe sink) const 203 { 204 if(nickName) 205 sink(nickName); 206 207 if(userName) 208 { 209 sink("!"); 210 sink(userName); 211 } 212 213 if(hostName) 214 { 215 sink("@"); 216 sink(hostName); 217 } 218 } 219 220 unittest 221 { 222 auto user = IrcUser("nick", "user", "host"); 223 assert(format("%s", user) == "nick!user@host"); 224 225 user.hostName = null; 226 assert(format("%s", user) == "nick!user"); 227 228 user.userName = null; 229 assert(format("%s", user) == "nick"); 230 231 user.hostName = "host"; 232 assert(format("%s", user) == "nick@host"); 233 } 234 235 static: 236 /** 237 * Create an IRC user from a message prefix. 238 */ 239 IrcUser fromPrefix(const(char)[] prefix) 240 { 241 IrcUser user; 242 243 if(prefix !is null) 244 { 245 user.nickName = prefix.munch("^!"); 246 if(prefix.length > 0) 247 { 248 prefix = prefix[1 .. $]; 249 user.userName = prefix.munch("^@"); 250 if(prefix.length > 0) 251 user.hostName = prefix[1 .. $]; 252 } 253 } 254 255 return user; 256 } 257 258 /** 259 * Create users from userhost reply. 260 */ 261 size_t parseUserhostReply(ref IrcUser[5] users, in char[] reply) 262 { 263 auto splitter = reply.splitter(" "); 264 foreach(i, ref user; users) 265 { 266 if(splitter.empty) 267 return i; 268 269 auto strUser = splitter.front; 270 271 if(strUser.strip.empty) // ??? 272 return i; 273 274 user.nickName = strUser.munch("^="); 275 strUser.popFront(); 276 277 user.userName = strUser.munch("^@"); 278 if(!strUser.empty) 279 strUser.popFront(); 280 281 if(user.userName[0] == '-' || user.userName[0] == '+') 282 { 283 // TODO: away stuff 284 user.userName.popFront(); 285 } 286 287 user.hostName = strUser; 288 289 splitter.popFront(); 290 } 291 292 return 5; 293 } 294 } 295 296 unittest 297 { 298 IrcUser user; 299 300 user = IrcUser.fromPrefix("foo!bar@baz"); 301 assert(user.nickName == "foo"); 302 assert(user.userName == "bar"); 303 assert(user.hostName == "baz"); 304 305 // TODO: figure out which field to fill with prefixes like "irc.server.net" 306 }