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 raw.munch(" "); 42 43 const(char)[] params = raw.munch("^:"); 44 while(params.length > 0) 45 { 46 line.arguments ~= params.munch("^ "); 47 params.munch(" "); 48 } 49 50 if(raw.length > 0) 51 line.arguments ~= raw[1..$]; 52 53 return true; 54 } 55 56 version(unittest) 57 { 58 import std.stdio; 59 } 60 61 unittest 62 { 63 struct InputOutput 64 { 65 char[] input; 66 IrcLine output; 67 bool valid = true; 68 } 69 70 static InputOutput[] testData = [ 71 { 72 input: "PING 123456".dup, 73 output: {command: "PING", arguments: ["123456"]} 74 }, 75 { 76 input: ":foo!bar@baz PRIVMSG #channel hi!".dup, 77 output: {prefix: "foo!bar@baz", command: "PRIVMSG", arguments: ["#channel", "hi!"]} 78 }, 79 { 80 input: ":foo!bar@baz PRIVMSG #channel :hello, world!".dup, 81 output: {prefix: "foo!bar@baz", command: "PRIVMSG", arguments: ["#channel", "hello, world!"]} 82 } 83 ]; 84 85 foreach(i, test; testData) 86 { 87 IrcLine line; 88 bool succ = parse(test.input, line); 89 90 scope(failure) 91 { 92 writefln("irc.protocol.parse unittest failed, test #%s", i + 1); 93 writefln(`prefix: "%s"`, line.prefix); 94 writefln(`command: "%s"`, line.command); 95 writefln(`arguments: "%s"`, line.arguments); 96 } 97 98 if(test.valid) 99 { 100 assert(line.prefix == test.output.prefix); 101 assert(line.command == test.output.command); 102 assert(line.arguments == test.output.arguments); 103 } 104 else 105 assert(!succ); 106 } 107 } 108 109 /** 110 * Structure representing an IRC user. 111 */ 112 struct IrcUser 113 { 114 /// 115 const(char)[] nickName; 116 /// 117 const(char)[] userName; 118 /// 119 const(char)[] hostName; 120 121 deprecated alias nick = nickName; 122 123 // TODO: Change to use sink once formattedWrite supports them 124 version(none) string toString() const 125 { 126 return format("%s!%s@%s", nickName, userName, hostName); 127 } 128 129 void toString(scope void delegate(const(char)[]) sink) const 130 { 131 if(nickName) 132 sink(nickName); 133 134 if(userName) 135 { 136 sink("!"); 137 sink(userName); 138 } 139 140 if(hostName) 141 { 142 sink("@"); 143 sink(hostName); 144 } 145 } 146 147 unittest 148 { 149 auto user = IrcUser("nick", "user", "host"); 150 assert(format("%s", user) == "nick!user@host"); 151 152 user.hostName = null; 153 assert(format("%s", user) == "nick!user"); 154 155 user.userName = null; 156 assert(format("%s", user) == "nick"); 157 158 user.hostName = "host"; 159 assert(format("%s", user) == "nick@host"); 160 } 161 162 static: 163 /** 164 * Create an IRC user from a message prefix. 165 */ 166 IrcUser fromPrefix(const(char)[] prefix) 167 { 168 IrcUser user; 169 170 if(prefix !is null) 171 { 172 user.nickName = prefix.munch("^!"); 173 if(prefix.length > 0) 174 { 175 prefix = prefix[1 .. $]; 176 user.userName = prefix.munch("^@"); 177 if(prefix.length > 0) 178 user.hostName = prefix[1 .. $]; 179 } 180 } 181 182 return user; 183 } 184 185 /** 186 * Create users from userhost reply. 187 */ 188 size_t parseUserhostReply(ref IrcUser[5] users, in char[] reply) 189 { 190 auto splitter = reply.splitter(" "); 191 foreach(i, ref user; users) 192 { 193 if(splitter.empty) 194 return i; 195 196 auto strUser = splitter.front; 197 198 if(strUser.strip.empty) // ??? 199 return i; 200 201 user.nickName = strUser.munch("^="); 202 strUser.popFront(); 203 204 user.userName = strUser.munch("^@"); 205 if(!strUser.empty) 206 strUser.popFront(); 207 208 if(user.userName[0] == '-' || user.userName[0] == '+') 209 { 210 // TODO: away stuff 211 user.userName.popFront(); 212 } 213 214 user.hostName = strUser; 215 216 splitter.popFront(); 217 } 218 219 return 5; 220 } 221 } 222 223 unittest 224 { 225 IrcUser user; 226 227 user = IrcUser.fromPrefix("foo!bar@baz"); 228 assert(user.nickName == "foo"); 229 assert(user.userName == "bar"); 230 assert(user.hostName == "baz"); 231 232 // TODO: figure out which field to fill with prefixes like "irc.server.net" 233 }