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 }