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 }