|
1 <?php |
|
2 |
|
3 /** |
|
4 * PHP IRC Client library |
|
5 * Copyright (C) 2008 Dan Fuhry. All rights reserved. |
|
6 */ |
|
7 |
|
8 /** |
|
9 * Version number |
|
10 * @const string |
|
11 */ |
|
12 |
|
13 define('REQUEST_IRC_VERSION', '0.1'); |
|
14 |
|
15 /** |
|
16 * The base class for an IRC session. |
|
17 */ |
|
18 |
|
19 class Request_IRC |
|
20 { |
|
21 |
|
22 /** |
|
23 * Hostname |
|
24 * @var string |
|
25 */ |
|
26 |
|
27 private $host = ''; |
|
28 |
|
29 /** |
|
30 * Port number |
|
31 * @var int |
|
32 */ |
|
33 |
|
34 private $port = 6667; |
|
35 |
|
36 /** |
|
37 * The socket for the connection. |
|
38 * @var resource |
|
39 */ |
|
40 |
|
41 public $sock = false; |
|
42 |
|
43 /** |
|
44 * Channel objects, associative array |
|
45 * @var array |
|
46 */ |
|
47 |
|
48 public $channels = array(); |
|
49 |
|
50 /** |
|
51 * The function called when a private message is received. |
|
52 * @var string |
|
53 */ |
|
54 |
|
55 private $privmsg_handler = false; |
|
56 |
|
57 /** |
|
58 * Switch to track if quitted or not. Helps avoid quitting the connection twice thus causing write errors. |
|
59 * @var bool |
|
60 * @access private |
|
61 */ |
|
62 |
|
63 protected $quitted = false; |
|
64 |
|
65 /** |
|
66 * The nickname we're connected as. Not modified once connected. |
|
67 * @var string |
|
68 */ |
|
69 |
|
70 public $nick = ''; |
|
71 |
|
72 /** |
|
73 * The username we're connected as. Not modified once connected. |
|
74 * @var string |
|
75 */ |
|
76 |
|
77 public $user = ''; |
|
78 |
|
79 /** |
|
80 * Constructor. |
|
81 * @param string Hostname |
|
82 * @param int Port number, defaults to 6667 |
|
83 */ |
|
84 |
|
85 public function __construct($host, $port = 6667) |
|
86 { |
|
87 // Check hostname |
|
88 if ( !preg_match('/^(([a-z0-9-]+\.)*?)([a-z0-9-]+)$/', $host) ) |
|
89 die(__CLASS__ . ': Invalid hostname'); |
|
90 $this->host = $host; |
|
91 |
|
92 // Check port |
|
93 if ( is_int($port) && $port >= 1 && $port <= 65535 ) |
|
94 $this->port = $port; |
|
95 else |
|
96 die(__CLASS__ . ': Invalid port'); |
|
97 } |
|
98 |
|
99 /** |
|
100 * Sets parameters and opens the connection. |
|
101 * @param string Nick |
|
102 * @param string User |
|
103 * @param string Real name |
|
104 * @param string NickServ password |
|
105 * @param int Flags, defaults to 0. |
|
106 */ |
|
107 |
|
108 public function connect($nick, $username, $realname, $pass, $flags = 0) |
|
109 { |
|
110 // Init connection |
|
111 $this->sock = fsockopen($this->host, $this->port); |
|
112 if ( !$this->sock ) |
|
113 throw new Exception('Could not make socket connection to host.'); |
|
114 |
|
115 stream_set_timeout($this->sock, 5); |
|
116 |
|
117 // Wait for initial ident messages |
|
118 while ( $msg = $this->get() ) |
|
119 { |
|
120 } |
|
121 |
|
122 // Send nick and username |
|
123 $this->put("NICK $nick\r\n"); |
|
124 $this->put("USER $username 0 * :$realname\r\n"); |
|
125 |
|
126 // Wait for response and end of motd |
|
127 $motd = ''; |
|
128 while ( $msg = $this->get() ) |
|
129 { |
|
130 // Match particles |
|
131 $msg = trim($msg); |
|
132 $mc = preg_match('/^:([A-z0-9\.-]+) ([0-9]+) [A-z0-9_-]+ :(.+)$/', $msg, $match); |
|
133 if ( !$mc ) |
|
134 { |
|
135 $mc = preg_match('/^:([A-z0-9_-]+)!([A-z0-9_-]+)@([A-z0-9_\.-]+) NOTICE [A-z0-9_-]+ :(.+)$/', $msg, $match); |
|
136 if ( !$mc ) |
|
137 continue; |
|
138 // Look for a response from NickServ |
|
139 if ( $match[1] == 'NickServ' ) |
|
140 { |
|
141 // Asking for auth? |
|
142 if ( strpos($match[4], 'IDENTIFY') ) |
|
143 { |
|
144 // Yes, send password |
|
145 $this->privmsg('NickServ', "IDENTIFY $pass"); |
|
146 } |
|
147 } |
|
148 } |
|
149 list(, $host, $stat, $msg) = $match; |
|
150 $motd .= "$msg"; |
|
151 } |
|
152 |
|
153 $this->nick = $nick; |
|
154 $this->user = $username; |
|
155 } |
|
156 |
|
157 /** |
|
158 * Writes some data to the socket, abstracted for debugging purposes. |
|
159 * @param string Message to send, this should include a CRLF. |
|
160 */ |
|
161 |
|
162 public function put($message) |
|
163 { |
|
164 if ( !$this->sock ) |
|
165 { |
|
166 if ( defined('LIBIRC_DEBUG') ) |
|
167 echo ">>> WRITE FAILED: $message"; |
|
168 return false; |
|
169 } |
|
170 if ( defined('LIBIRC_DEBUG') ) |
|
171 echo ">>> $message"; |
|
172 fwrite($this->sock, $message); |
|
173 } |
|
174 |
|
175 /** |
|
176 * Reads from the socket... |
|
177 * @return string |
|
178 */ |
|
179 |
|
180 public function get() |
|
181 { |
|
182 if ( !$this->sock ) |
|
183 { |
|
184 if ( defined('LIBIRC_DEBUG') ) |
|
185 echo "<<< READ FAILED\n"; |
|
186 return false; |
|
187 } |
|
188 $out = fgets($this->sock, 4096); |
|
189 if ( defined('LIBIRC_DEBUG') ) |
|
190 if ( !empty($out) ) |
|
191 echo "<<< $out"; |
|
192 return $out; |
|
193 } |
|
194 |
|
195 /** |
|
196 * Sends a message to a nick or channel. |
|
197 * @param string Nick or channel |
|
198 * @param string Message |
|
199 */ |
|
200 |
|
201 public function privmsg($nick, $message) |
|
202 { |
|
203 $message = str_replace("\r\n", "\n", $message); |
|
204 $message = explode("\n", $message); |
|
205 foreach ( $message as $line ) |
|
206 { |
|
207 $this->put("PRIVMSG $nick :$line\r\n"); |
|
208 } |
|
209 } |
|
210 |
|
211 /** |
|
212 * The main event loop. |
|
213 */ |
|
214 |
|
215 public function event_loop() |
|
216 { |
|
217 stream_set_timeout($this->sock, 0xFFFFFFFE); |
|
218 while ( $data = $this->get() ) |
|
219 { |
|
220 $data_trim = trim($data); |
|
221 $match = self::parse_message($data_trim); |
|
222 if ( preg_match('/^PING :(.+?)$/', $data_trim, $pmatch) ) |
|
223 { |
|
224 $this->put("PONG :{$pmatch[1]}\r\n"); |
|
225 } |
|
226 else if ( $match ) |
|
227 { |
|
228 // Received PRIVMSG or other mainstream action |
|
229 if ( $match['action'] == 'JOIN' ) |
|
230 $channel =& $match['message']; |
|
231 else |
|
232 $channel =& $match['target']; |
|
233 |
|
234 if ( !preg_match('/^[#!&\+]/', $channel) ) |
|
235 { |
|
236 // Private message from user |
|
237 $result = $this->handle_privmsg($data); |
|
238 stream_set_timeout($this->sock, 0xFFFFFFFE); |
|
239 } |
|
240 else if ( isset($this->channels[strtolower($channel)]) ) |
|
241 { |
|
242 // Message into channel |
|
243 $chan =& $this->channels[strtolower($channel)]; |
|
244 $func = $chan->get_handler(); |
|
245 $result = @call_user_func($func, $data, $chan); |
|
246 stream_set_timeout($this->sock, 0xFFFFFFFE); |
|
247 } |
|
248 if ( $result == 'BREAK' ) |
|
249 { |
|
250 break; |
|
251 } |
|
252 } |
|
253 } |
|
254 } |
|
255 |
|
256 /** |
|
257 * Processor for when a private message is received. |
|
258 * @access private |
|
259 */ |
|
260 |
|
261 private function handle_privmsg($message) |
|
262 { |
|
263 $message = self::parse_message($message); |
|
264 $ph = $this->privmsg_handler; |
|
265 if ( @function_exists($ph) ) |
|
266 return @call_user_func($ph, $message); |
|
267 } |
|
268 |
|
269 /** |
|
270 * Changes the function called upon receipt of a private message. |
|
271 * @param string Function to call, will be passed a parsed message. |
|
272 */ |
|
273 |
|
274 function set_privmsg_handler($func) |
|
275 { |
|
276 if ( !function_exists($func) ) |
|
277 return false; |
|
278 $this->privmsg_handler = $func; |
|
279 return true; |
|
280 } |
|
281 |
|
282 /** |
|
283 * Parses a message line. |
|
284 * @param string Message text |
|
285 * @return array Associative with keys: nick, user, host, action, target, message |
|
286 */ |
|
287 |
|
288 public static function parse_message($message) |
|
289 { |
|
290 // Indices: 12 3 4 5 67 8 |
|
291 $mc = preg_match('/^:(([^ ]+)!([^ ]+)@([^ ]+)) ([A-Z]+) (([#!&\+]*[A-z0-9_-]+) )?:?(.*?)$/', $message, $match); |
|
292 if ( !$mc ) |
|
293 { |
|
294 return false; |
|
295 } |
|
296 // Indices: 0 1 2 3 4 5 6 7 8 |
|
297 list( , , $nick, $user, $host, $action, , $target, $message) = $match; |
|
298 return array( |
|
299 'nick' => $nick, |
|
300 'user' => $user, |
|
301 'host' => $host, |
|
302 'action' => $action, |
|
303 'target' => $target, |
|
304 'message' => trim($message) |
|
305 ); |
|
306 } |
|
307 |
|
308 /** |
|
309 * Joins a channel, and returns a Request_IRC_Channel object. |
|
310 * @param string Channel name (remember # prefix) |
|
311 * @param string Event handler function, will be called with param 0 = socket output and param 1 = channel object |
|
312 * @return object |
|
313 */ |
|
314 |
|
315 function join($channel, $handler) |
|
316 { |
|
317 $chan = new Request_IRC_Channel(strtolower($channel), $handler, $this); |
|
318 $this->channels[strtolower($channel)] = $chan; |
|
319 return $chan; |
|
320 } |
|
321 |
|
322 /** |
|
323 * Closes the connection and quits. |
|
324 * @param string Optional part message |
|
325 */ |
|
326 |
|
327 public function close($partmsg = false) |
|
328 { |
|
329 if ( $this->quitted ) |
|
330 return true; |
|
331 |
|
332 $this->quitted = true; |
|
333 // Part all channels |
|
334 if ( !$partmsg ) |
|
335 $partmsg = 'IRC bot powered by PHP/' . PHP_VERSION . ' libirc/' . REQUEST_IRC_VERSION; |
|
336 |
|
337 foreach ( $this->channels as $channel ) |
|
338 { |
|
339 $channel->part($partmsg); |
|
340 } |
|
341 |
|
342 $this->put("QUIT\r\n"); |
|
343 |
|
344 while ( $msg = $this->get() ) |
|
345 { |
|
346 // Do nothing. |
|
347 } |
|
348 |
|
349 fclose($this->sock); |
|
350 } |
|
351 |
|
352 } |
|
353 |
|
354 /** |
|
355 * Wrapper for channels. |
|
356 */ |
|
357 |
|
358 class Request_IRC_Channel extends Request_IRC |
|
359 { |
|
360 |
|
361 /** |
|
362 * The name of the channel |
|
363 * @var string |
|
364 */ |
|
365 |
|
366 private $channel_name = ''; |
|
367 |
|
368 /** |
|
369 * The event handler function. |
|
370 * @var string |
|
371 */ |
|
372 |
|
373 private $handler = ''; |
|
374 |
|
375 /** |
|
376 * The parent connection. |
|
377 * @var object |
|
378 */ |
|
379 |
|
380 public $parent = false; |
|
381 |
|
382 /** |
|
383 * Whether the channel has been parted or not, used to kill the destructor. |
|
384 * @var bool |
|
385 */ |
|
386 |
|
387 protected $parted = false; |
|
388 |
|
389 /** |
|
390 * Constructor. |
|
391 * @param string Channel name |
|
392 * @param string Handler function |
|
393 * @param object IRC connection (Request_IRC object) |
|
394 */ |
|
395 |
|
396 function __construct($channel, $handler, $parent) |
|
397 { |
|
398 $this->parent = $parent; |
|
399 $this->parent->put("JOIN $channel\r\n"); |
|
400 stream_set_timeout($this->parent->sock, 3); |
|
401 while ( $msg = $this->parent->get() ) |
|
402 { |
|
403 // Do nothing |
|
404 } |
|
405 $this->channel_name = $channel; |
|
406 $this->handler = $handler; |
|
407 } |
|
408 |
|
409 /** |
|
410 * Returns the channel name |
|
411 * @return string |
|
412 */ |
|
413 |
|
414 function get_channel_name() |
|
415 { |
|
416 return $this->channel_name; |
|
417 } |
|
418 |
|
419 /** |
|
420 * Returns the handler function |
|
421 * @return string |
|
422 */ |
|
423 |
|
424 function get_handler() |
|
425 { |
|
426 return $this->handler; |
|
427 } |
|
428 |
|
429 /** |
|
430 * Sends a message. |
|
431 * @param string message |
|
432 * @param bool If true, will fire a message event when the message is sent. |
|
433 */ |
|
434 |
|
435 function msg($msg, $fire_event = false) |
|
436 { |
|
437 $this->parent->privmsg($this->channel_name, $msg); |
|
438 if ( $fire_event ) |
|
439 { |
|
440 $func = $this->get_handler(); |
|
441 // format: :nick!user@host PRIVMSG #channel :msg. |
|
442 $lines = explode("\n", $msg); |
|
443 foreach ( $lines as $line ) |
|
444 { |
|
445 $data = ":{$this->parent->nick}!{$this->parent->user}@localhost PRIVMSG {$this->channel_name} :$line"; |
|
446 $result = @call_user_func($func, $data, $this); |
|
447 stream_set_timeout($this->parent->sock, 0xFFFFFFFE); |
|
448 } |
|
449 } |
|
450 } |
|
451 |
|
452 /** |
|
453 * Destructor, automatically parts the channel. |
|
454 */ |
|
455 |
|
456 function __destruct() |
|
457 { |
|
458 if ( !$this->parted ) |
|
459 $this->part('IRC bot powered by PHP/' . PHP_VERSION . ' libirc/' . REQUEST_IRC_VERSION); |
|
460 } |
|
461 |
|
462 /** |
|
463 * Parts the channel. |
|
464 * @param string Optional message |
|
465 */ |
|
466 |
|
467 function part($msg = '') |
|
468 { |
|
469 $this->parent->put("PART {$this->channel_name} :$msg\r\n"); |
|
470 $this->parted = true; |
|
471 unset($this->parent->channels[$this->channel_name]); |
|
472 } |
|
473 |
|
474 } |
|
475 |
|
476 ?> |