bab64de321639658c1479e486016322c177c6615
[reactos.git] / irc / TechBot / TechBot.IRCLibrary / IrcMessage.cs
1 using System;
2
3 namespace TechBot.IRCLibrary
4 {
5 /*
6 <message> ::= [':' <prefix> <SPACE> ] <command> <params> <crlf>
7 <prefix> ::= <servername> | <nick> [ '!' <user> ] [ '@' <host> ]
8 <command> ::= <letter> { <letter> } | <number> <number> <number>
9 <SPACE> ::= ' ' { ' ' }
10 <params> ::= <SPACE> [ ':' <trailing> | <middle> <params> ]
11
12 <middle> ::= <Any *non-empty* sequence of octets not including SPACE
13 or NUL or CR or LF, the first of which may not be ':'>
14 <trailing> ::= <Any, possibly *empty*, sequence of octets not including
15 NUL or CR or LF>
16
17 <crlf> ::= CR LF
18
19 NOTES:
20
21 1) <SPACE> is consists only of SPACE character(s) (0x20).
22 Specially notice that TABULATION, and all other control
23 characters are considered NON-WHITE-SPACE.
24
25 2) After extracting the parameter list, all parameters are equal,
26 whether matched by <middle> or <trailing>. <Trailing> is just
27 a syntactic trick to allow SPACE within parameter.
28
29 3) The fact that CR and LF cannot appear in parameter strings is
30 just artifact of the message framing. This might change later.
31
32 4) The NUL character is not special in message framing, and
33 basically could end up inside a parameter, but as it would
34 cause extra complexities in normal C string handling. Therefore
35 NUL is not allowed within messages.
36
37 5) The last parameter may be an empty string.
38
39 6) Use of the extended prefix (['!' <user> ] ['@' <host> ]) must
40 not be used in server to server communications and is only
41 intended for server to client messages in order to provide
42 clients with more useful information about who a message is
43 from without the need for additional queries.
44 */
45 /*
46 NOTICE AUTH :*** Looking up your hostname
47 NOTICE AUTH :*** Checking Ident
48 NOTICE AUTH :*** Found your hostname
49 NOTICE AUTH :*** No ident response
50 */
51
52 /// <summary>
53 /// IRC message.
54 /// </summary>
55 public class IrcMessage
56 {
57 #region Private fields
58 private string line;
59 private string prefix;
60 private string prefixServername;
61 private string prefixNickname;
62 private string prefixUser;
63 private string prefixHost;
64 private string command;
65 private string parameters;
66 #endregion
67
68 /// <summary>
69 /// Line of text that is to be parsed as an IRC message.
70 /// </summary>
71 public string Line
72 {
73 get
74 {
75 return line;
76 }
77 }
78
79 /// <summary>
80 /// Does the message have a prefix?
81 /// </summary>
82 public bool HasPrefix
83 {
84 get
85 {
86 return prefix != null;
87 }
88 }
89
90 /// <summary>
91 /// Prefix or null if none exists.
92 /// </summary>
93 public string Prefix
94 {
95 get
96 {
97 return prefix;
98 }
99 }
100
101 /// <summary>
102 /// Servername part of prefix or null if no prefix or servername exists.
103 /// </summary>
104 public string PrefixServername
105 {
106 get
107 {
108 return prefixServername;
109 }
110 }
111
112 /// <summary>
113 /// Nickname part of prefix or null if no prefix or nick exists.
114 /// </summary>
115 public string PrefixNickname
116 {
117 get
118 {
119 return prefixNickname;
120 }
121 }
122
123 /// <summary>
124 /// User part of (extended) prefix or null if no (extended) prefix exists.
125 /// </summary>
126 public string PrefixUser
127 {
128 get
129 {
130 return prefixUser;
131 }
132 }
133
134 /// <summary>
135 /// Host part of (extended) prefix or null if no (extended) prefix exists.
136 /// </summary>
137 public string PrefixHost
138 {
139 get
140 {
141 return prefixHost;
142 }
143 }
144
145 /// <summary>
146 /// Command part of message.
147 /// </summary>
148 public string Command
149 {
150 get
151 {
152 return command;
153 }
154 }
155
156 /// <summary>
157 /// Is command numeric?
158 /// </summary>
159 public bool IsCommandNumeric
160 {
161 get
162 {
163 if (command == null || command.Length != 3)
164 {
165 return false;
166 }
167 try
168 {
169 Int32.Parse(command);
170 return true;
171 }
172 catch (Exception)
173 {
174 return false;
175 }
176 }
177 }
178
179 /// <summary>
180 /// Command part of message as text.
181 /// </summary>
182 public string CommandText
183 {
184 get
185 {
186 return command;
187 }
188 }
189
190 /// <summary>
191 /// Command part of message as a number.
192 /// </summary>
193 /// <exception cref="InvalidOperationException">Thrown if IsCommandNumeric returns false.</exception>
194 public int CommandNumber
195 {
196 get
197 {
198 if (IsCommandNumeric)
199 {
200 return Int32.Parse(command);
201 }
202 else
203 {
204 throw new InvalidOperationException();
205 }
206 }
207 }
208
209 /// <summary>
210 /// Parameters part of message.
211 /// </summary>
212 public string Parameters
213 {
214 get
215 {
216 return parameters;
217 }
218 }
219
220 /// <summary>
221 /// Constructor.
222 /// </summary>
223 /// <param name="line">Line of text that is to be parsed as an IRC message.</param>
224 public IrcMessage(string line)
225 {
226 /*
227 * <message> ::= [':' <prefix> <SPACE> ] <command> <params> <crlf>
228 * <prefix> ::= <servername> | <nick> [ '!' <user> ] [ '@' <host> ]
229 * :Oslo1.NO.EU.undernet.org 461 MYNICK USER :Not enough parameters
230 */
231 try
232 {
233 this.line = line;
234 int i = 0;
235
236 #region Prefix
237 if (line[i].Equals(':'))
238 {
239 i++;
240 prefix = "";
241 /* This message has a prefix */
242 string s = "";
243 while (i < line.Length && line[i] != ' ' && line[i] != '!' && line[i] != '@')
244 {
245 s += line[i++];
246 }
247 if (IsValidIrcNickname(s))
248 {
249 prefixNickname = s;
250 prefix += prefixNickname;
251 if (line[i] == '!')
252 {
253 /* This message has an extended prefix */
254 i++;
255 s = "";
256 while (i < line.Length && line[i] != ' ' && line[i] != '@')
257 {
258 s += line[i];
259 i++;
260 }
261 prefixUser = s;
262 prefix += "!" + prefixUser;
263 }
264 if (line[i] == '@')
265 {
266 /* This message has a host prefix */
267 s = "";
268 do
269 {
270 s += line[++i];
271 }
272 while (i < line.Length && line[i] != ' ');
273 prefixHost = s;
274 prefix += "@" + prefixHost;
275 }
276 }
277 else /* Assume it is a servername */
278 {
279 prefixServername = s;
280 prefix += prefixServername;
281 }
282
283 /* Skip spaces */
284 while (i < line.Length && line[i] == ' ')
285 {
286 i++;
287 }
288 }
289 else
290 {
291 prefix = null;
292 }
293 #endregion
294
295 #region Command
296 if (Char.IsDigit(line[i]))
297 {
298 if (!Char.IsDigit(line, i + 1) || !Char.IsDigit(line, i + 2))
299 {
300 throw new Exception();
301 }
302 command = String.Format("{0}{1}{2}", line[i++], line[i++], line[i++]);
303 }
304 else
305 {
306 command = "";
307 while (i < line.Length && Char.IsLetter(line[i]))
308 {
309 command += line[i];
310 i++;
311 }
312 }
313 #endregion
314
315 #region Parameters
316 while (true)
317 {
318 /* Skip spaces */
319 while (i < line.Length && line[i] == ' ')
320 {
321 i++;
322 }
323 if (i < line.Length && line[i].Equals(':'))
324 {
325 i++;
326
327 /* Trailing */
328 while (i < line.Length && line[i] != ' ' && line[i] != '\r' && line[i] != '\n' && line[i] != 0)
329 {
330 if (parameters == null)
331 {
332 parameters = "";
333 }
334 parameters += line[i];
335 i++;
336 }
337 }
338 else
339 {
340 /* Middle */
341 while (i < line.Length && line[i] != '\r' && line[i] != '\n' && line[i] != 0)
342 {
343 if (parameters == null)
344 {
345 parameters = "";
346 }
347 parameters += line[i];
348 i++;
349 }
350 }
351 if (i >= line.Length)
352 {
353 break;
354 }
355 }
356 #endregion
357 }
358 catch (Exception ex)
359 {
360 throw new MalformedMessageException("The message is malformed.", ex);
361 }
362 }
363
364 /// <summary>
365 /// Constructor.
366 /// </summary>
367 /// <param name="prefixServername"></param>
368 /// <param name="prefixNickname"></param>
369 /// <param name="prefixUser"></param>
370 /// <param name="prefixHost"></param>
371 /// <param name="command"></param>
372 /// <param name="parameters"></param>
373 public IrcMessage(string prefixServername,
374 string prefixNickname,
375 string prefixUser,
376 string prefixHost,
377 string command,
378 string parameters)
379 {
380 throw new NotImplementedException();
381 }
382
383 /// <summary>
384 /// Constructor.
385 /// </summary>
386 /// <param name="command">IRC command.</param>
387 /// <param name="parameters">IRC command parameters. May be null if there are no parameters.</param>
388 public IrcMessage(string command,
389 string parameters)
390 {
391 if (command == null || !IsValidIrcCommand(command))
392 {
393 throw new ArgumentException("Command is not a valid IRC command.", "command");
394 }
395 /* An IRC message must not be longer than 512 characters (including terminating CRLF) */
396 int parametersLength = (parameters != null) ? 1 + parameters.Length : 0;
397 if (command.Length + parametersLength > 510)
398 {
399 throw new MalformedMessageException("IRC message cannot be longer than 512 characters.");
400 }
401 this.command = command;
402 this.parameters = parameters;
403 if (parameters != null)
404 {
405 this.line = String.Format("{0} {1}\r\n", command, parameters);
406 }
407 else
408 {
409 this.line = String.Format("{0}\r\n", command);
410 }
411 }
412
413 /// <summary>
414 /// Returns wether a string of text is a valid IRC command.
415 /// </summary>
416 /// <param name="command">The IRC command.</param>
417 /// <returns>True, if <c ref="command">command</c> is a valid IRC command, false if not.</returns>
418 private static bool IsValidIrcCommand(string command)
419 {
420 foreach (char c in command)
421 {
422 if (!Char.IsLetter(c))
423 {
424 return false;
425 }
426 }
427 return true;
428 }
429
430 private const string IrcSpecial = @"-[]\`^{}";
431 private const string IrcSpecialNonSpecs = @"_";
432
433 /// <summary>
434 /// Returns wether a character is an IRC special character.
435 /// </summary>
436 /// <param name="c">Character to test.</param>
437 /// <returns>True if the character is an IRC special character, false if not.</returns>
438 private static bool IsSpecial(char c)
439 {
440 foreach (char validCharacter in IrcSpecial)
441 {
442 if (c.Equals(validCharacter))
443 {
444 return true;
445 }
446 }
447 foreach (char validCharacter in IrcSpecialNonSpecs)
448 {
449 if (c.Equals(validCharacter))
450 {
451 return true;
452 }
453 }
454 return false;
455 }
456
457 /// <summary>
458 /// Returns wether a string of text is a valid IRC nickname.
459 /// </summary>
460 /// <param name="nickname">The IRC nickname.</param>
461 /// <returns>True, if <c ref="nickname">nickname</c> is a valid IRC nickname, false if not.</returns>
462 private static bool IsValidIrcNickname(string nickname)
463 {
464 /*
465 * <nick> ::= <letter> { <letter> | <number> | <special> }
466 * <letter> ::= 'a' ... 'z' | 'A' ... 'Z'
467 * <number> ::= '0' ... '9'
468 * <special> ::= '-' | '[' | ']' | '\' | '`' | '^' | '{' | '}'
469 */
470 /* An IRC nicknmame must be 1 - 9 characters in length. We don't care so much if it is larger */
471 if ((nickname.Length < 1) || (nickname.Length > 30))
472 {
473 return false;
474 }
475 /* First character must be a letter. */
476 if (!Char.IsLetter(nickname[0]))
477 {
478 return false;
479 }
480 /* Check the other valid characters for validity. */
481 foreach (char c in nickname)
482 {
483 if (!Char.IsLetter(c) && !Char.IsDigit(c) && !IsSpecial(c))
484 {
485 return false;
486 }
487 }
488 return true;
489 }
490
491 /// <summary>
492 /// Write contents to a string.
493 /// </summary>
494 /// <returns>Contents as a string.</returns>
495 public override string ToString()
496 {
497 return String.Format("Line({0})Prefix({1})Command({2})Parameters({3})",
498 line, prefix != null ? prefix : "(null)",
499 command != null ? command : "(null)",
500 parameters != null ? parameters : "(null)");
501 }
502 }
503 }