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