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 }