Erebot  latest
A modular IRC bot for PHP 7.0+
IrcParser.php
1 <?php
2 /*
3  This file is part of Erebot, a modular IRC bot written in PHP.
4 
5  Copyright © 2010 François Poirotte
6 
7  Erebot is free software: you can redistribute it and/or modify
8  it under the terms of the GNU General Public License as published by
9  the Free Software Foundation, either version 3 of the License, or
10  (at your option) any later version.
11 
12  Erebot is distributed in the hope that it will be useful,
13  but WITHOUT ANY WARRANTY; without even the implied warranty of
14  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15  GNU General Public License for more details.
16 
17  You should have received a copy of the GNU General Public License
18  along with Erebot. If not, see <http://www.gnu.org/licenses/>.
19 */
20 
21 namespace Erebot;
22 
29 class IrcParser implements \Erebot\Interfaces\IrcParser
30 {
32  const STRIP_NONE = 0x00;
34  const STRIP_COLORS = 0x01;
36  const STRIP_BOLD = 0x02;
38  const STRIP_UNDERLINE = 0x04;
40  const STRIP_REVERSE = 0x08;
42  const STRIP_RESET = 0x10;
44  const STRIP_ALL = 0xFF;
45 
46 
48  protected $eventsMapping;
49 
51  protected $connection;
52 
53 
68  public function __construct(\Erebot\Interfaces\Connection $connection)
69  {
70  $this->connection = $connection;
71  $this->eventsMapping = array();
72  }
73 
88  public static function stripCodes($text, $strip = self::STRIP_ALL)
89  {
90  if (!is_int($strip)) {
91  throw new \Erebot\InvalidValueException("Invalid stripping flags");
92  }
93 
94  if ($strip & self::STRIP_BOLD) {
95  $text = str_replace("\002", '', $text);
96  }
97 
98  if ($strip & self::STRIP_COLORS) {
99  $text = preg_replace("/\003(?:[0-9]{1,2}(?:,[0-9]{1,2})?)?/", '', $text);
100  }
101 
102  if ($strip & self::STRIP_RESET) {
103  $text = str_replace("\017", '', $text);
104  }
105 
106  if ($strip & self::STRIP_REVERSE) {
107  $text = str_replace("\026", '', $text);
108  }
109 
110  if ($strip & self::STRIP_UNDERLINE) {
111  $text = str_replace("\037", '', $text);
112  }
113 
114  return $text;
115  }
116 
127  public function makeEvent($iface /* , ... */)
128  {
129  $args = func_get_args();
130 
131  // Shortcuts.
132  $iface = str_replace('!', '\\Erebot\\Interfaces\\Event\\', $iface);
133  $iface = strtolower($iface);
134 
135  if (!isset($this->eventsMapping[$iface])) {
136  throw new \Erebot\NotFoundException('No such declared interface');
137  }
138 
139  // Replace the first argument (interface) with a reference
140  // to the connection, since all events require it anyway.
141  // This simplifies calls to this method a bit.
142  $args[0] = $this->connection;
143  $cls = new \ReflectionClass($this->eventsMapping[$iface]);
144  $instance = $cls->newInstanceArgs($args);
145  return $instance;
146  }
147 
160  protected static function ctcpUnquote($msg)
161  {
162  // CTCP-level unquoting
163  $quoting = array(
164  "\\a" => "\001",
165  "\\\\" => "\\",
166  "\\" => "", // Ignore quoting character
167  // for invalid sequences.
168  );
169  $msg = strtr($msg, $quoting);
170 
171  // Low-level unquoting
172  $quoting = array(
173  "\0200" => "\000",
174  "\020n" => "\n",
175  "\020r" => "\r",
176  "\020\020" => "\020",
177  "\020" => "", // Ignore quoting character
178  // for invalid sequences.
179  );
180  $msg = strtr($msg, $quoting);
181  return $msg;
182  }
183 
196  public function getEventClass($iface)
197  {
198  // Shortcuts.
199  $iface = str_replace('!', '\\Erebot\\Interfaces\\Event\\', $iface);
200  $iface = strtolower($iface);
201 
202  return isset($this->eventsMapping[$iface])
203  ? $this->eventsMapping[$iface]
204  : null;
205  }
206 
215  public function getEventClasses()
216  {
217  return $this->eventsMapping;
218  }
219 
228  public function setEventClasses($events)
229  {
230  foreach ($events as $iface => $cls) {
231  $this->setEventClass($iface, $cls);
232  }
233  }
234 
247  public function setEventClass($iface, $cls)
248  {
249  // Shortcuts.
250  $iface = str_replace('!', '\\Erebot\\Interfaces\\Event\\', $iface);
251  if (!interface_exists($iface)) {
252  throw new \Erebot\InvalidValueException(
253  'The given interface ('.$iface.') does not exist'
254  );
255  }
256 
257  $iface = strtolower($iface);
258  $reflector = new \ReflectionClass($cls);
259  if (!$reflector->implementsInterface($iface)) {
260  throw new \Erebot\InvalidValueException(
261  'The given class does not implement that interface'
262  );
263  }
264  $this->eventsMapping[$iface] = $cls;
265  }
266 
276  public function parseLine($msg)
277  {
278  if (!strncmp($msg, ':', 1)) {
279  $pos = strcspn($msg, ' ');
280  $origin = (string) substr($msg, 1, $pos - 1);
281  $msg = new \Erebot\IrcTextWrapper((string) substr($msg, $pos + 1));
282  } else {
284  $origin = '';
285  $msg = new \Erebot\IrcTextWrapper($msg);
286  }
287 
288  $type = $msg[0];
289  $type = strtoupper($type);
290  unset($msg[0]);
291 
292  $method = 'handle'.$type;
293  $exists = method_exists($this, $method);
294  // We need a backup for numeric events
295  // as the method may alter the message.
296  $backup = clone $msg;
297 
298  if ($exists) {
299  $res = $this->$method($origin, $msg);
300  }
301 
302  if (ctype_digit($type)) {
303  // For numeric events, the first token is always the target.
304  $target = $backup[0];
305  unset($backup[0]);
306  return $this->connection->dispatch(
307  $this->makeEvent(
308  '!Numeric',
309  intval($type, 10),
310  $origin,
311  $target,
312  $backup
313  )
314  );
315  }
316 
317  if ($exists) {
318  return $res;
319  }
320 
322  return false;
323  }
324 
347  protected function noticeOrPrivmsg($origin, $msg, $mapping)
348  {
349  // :nick1!ident@host NOTICE <nick2/#chan> :Message
350  // :nick1!ident@host PRIVMSG <nick2/#chan> :Message
351  $target = $msg[0];
352  $msg = $msg[1];
353  $isChan = (int) $this->connection->isChannel($target);
354  $len = strlen($msg);
355  if ($len > 1 && $msg[$len-1] == "\x01" && $msg[0] == "\x01") {
356  // Remove the markers.
357  $msg = (string) substr($msg, 1, -1);
358  // Unquote the message.
359  $msg = self::ctcpUnquote($msg);
360  // Extract the tag from the rest of the message.
361  $pos = strcspn($msg, " ");
362  $ctcp = substr($msg, 0, $pos);
363  $msg = (string) substr($msg, $pos + 1);
364 
365  if ($ctcp == "ACTION") {
366  if ($isChan) {
367  return $this->connection->dispatch(
368  $this->makeEvent($mapping['action'][$isChan], $target, $origin, $msg)
369  );
370  }
371 
372  return $this->connection->dispatch(
373  $this->makeEvent($mapping['action'][$isChan], $origin, $msg)
374  );
375  }
376 
377  if ($isChan) {
378  return $this->connection->dispatch(
379  $this->makeEvent($mapping['ctcp'][$isChan], $target, $origin, $ctcp, $msg)
380  );
381  }
382 
383  return $this->connection->dispatch(
384  $this->makeEvent($mapping['ctcp'][$isChan], $origin, $ctcp, $msg)
385  );
386  }
387 
388  if ($isChan) {
389  return $this->connection->dispatch(
390  $this->makeEvent(
391  $mapping['normal'][$isChan],
392  $target,
393  $origin,
394  $msg
395  )
396  );
397  }
398 
399  return $this->connection->dispatch(
400  $this->makeEvent($mapping['normal'][$isChan], $origin, $msg)
401  );
402  }
403 
415  protected function handleINVITE($origin, $msg)
416  {
417  // :nick1!ident@host INVITE nick2 :#chan
418  return $this->connection->dispatch(
419  $this->makeEvent('!Invite', $msg[1], $origin, $msg[0])
420  );
421  }
422 
434  protected function handleJOIN($origin, $msg)
435  {
436  // :nick1!ident@host JOIN :#chan
437  return $this->connection->dispatch(
438  $this->makeEvent('!Join', $msg[0], $origin)
439  );
440  }
441 
453  protected function handleKICK($origin, $msg)
454  {
455  // :nick1!ident@host KICK #chan nick2 :Reason
456  return $this->connection->dispatch(
457  $this->makeEvent('!Kick', $msg[0], $origin, $msg[1], $msg[2])
458  );
459  }
460 
473  protected function handleMODE($origin, $msg)
474  {
475  // :nick1!ident@host MODE <nick2/#chan> <modes> [args]
476  $target = $msg[0];
477  unset($msg[0]);
478  if (!$this->connection->isChannel($target)) {
479  return $this->connection->dispatch(
480  $this->makeEvent('!UserMode', $origin, $target, $msg)
481  );
482  }
483 
484  $event = $this->makeEvent('!RawMode', $target, $origin, $msg);
485  $this->connection->dispatch($event);
486  if ($event->preventDefault(true)) {
487  return;
488  }
489 
490  $modes = $msg[0];
491  unset($msg[0]);
492  $len = strlen($modes);
493  $mode = 'add';
494  $k = 0;
495 
496  $priv = array(
497  'add' => array(
498  'o' => '!Op',
499  'h' => '!Halfop',
500  'v' => '!Voice',
501  'a' => '!Protect',
502  'q' => '!Owner',
503  'b' => '!Ban',
504  'e' => '!Except',
505  ),
506  'remove' => array(
507  'o' => '!DeOp',
508  'h' => '!DeHalfop',
509  'v' => '!DeVoice',
510  'a' => '!DeProtect',
511  'q' => '!DeOwner',
512  'b' => '!UnBan',
513  'e' => '!UnExcept',
514  ),
515  );
516 
517  $remains = array();
518  for ($i = 0; $i < $len; $i++) {
519  switch ($modes[$i]) {
520  case '+':
521  $mode = 'add';
522  break;
523  case '-':
524  $mode = 'remove';
525  break;
526 
527  case 'o':
528  case 'v':
529  case 'h':
530  case 'a':
531  case 'q':
532  case 'b':
533  case 'e':
534  $tnick = $msg[$k++];
535  $cls = $priv[$mode][$modes[$i]];
536  $this->connection->dispatch(
537  $this->makeEvent($cls, $target, $origin, $tnick)
538  );
539  break;
540 
541  default:
542  }
543  } // for each mode in $modes
545  }
546 
558  protected function handleNICK($origin, $msg)
559  {
560  // :oldnick!ident@host NICK newnick
561  return $this->connection->dispatch(
562  $this->makeEvent('!Nick', $origin, $msg[0])
563  );
564  }
565 
578  protected function handleNOTICE($origin, $msg)
579  {
580  // :nick1!ident@host NOTICE <nick2/#chan> :Message
581  $mapping = array(
582  'action' => array('!PrivateCtcpReply', '!ChanCtcpReply'),
583  'ctcp' => array('!PrivateCtcpReply', '!ChanCtcpReply'),
584  'normal' => array('!PrivateNotice', '!ChanNotice'),
585  );
586  return $this->noticeOrPrivmsg($origin, $msg, $mapping);
587  }
588 
600  protected function handlePART($origin, $msg)
601  {
602  // :nick1!ident@host PART #chan [reason]
603  return $this->connection->dispatch(
604  $this->makeEvent(
605  '!Part',
606  $msg[0],
607  $origin,
608  isset($msg[1]) ? $msg[1] : ''
609  )
610  );
611  }
612 
624  protected function handlePING($origin, $msg)
625  {
626  // PING origin
627  return $this->connection->dispatch(
628  $this->makeEvent('!Ping', $msg)
629  );
630  }
631 
643  protected function handlePONG($origin, $msg)
644  {
645  // :origin PONG origin target
646  return $this->connection->dispatch(
647  $this->makeEvent('!Pong', $origin, $msg[1])
648  );
649  }
650 
663  protected function handlePRIVMSG($origin, $msg)
664  {
665  // :nick1!ident@host PRIVMSG <nick2/#chan> :Message
666  $mapping = array(
667  'action' => array('!PrivateAction', '!ChanAction'),
668  'ctcp' => array('!PrivateCtcp', '!ChanCtcp'),
669  'normal' => array('!PrivateText', '!ChanText'),
670  );
671  return $this->noticeOrPrivmsg($origin, $msg, $mapping);
672  }
673 
685  protected function handleQUIT($origin, $msg)
686  {
687  // :nick1!ident@host QUIT :Reason
688  return $this->connection->dispatch(
689  $this->makeEvent('!Quit', $origin, $msg[0])
690  );
691  }
692 
704  protected function handleTOPIC($origin, $msg)
705  {
706  // :nick1!ident@host TOPIC #chan :New topic
707  return $this->connection->dispatch(
708  $this->makeEvent('!Topic', $msg[0], $origin, $msg[1])
709  );
710  }
711 
726  protected function handle255($origin, $msg)
727  {
728  // \Erebot\Interfaces\Numerics::RPL_LUSERME
729  /* We can't rely on RPL_WELCOME because we may need
730  * to detect the server's capabilities first.
731  * So, we delay detection of the connection for as
732  * long as we can (while retaining portability). */
733  if (!$this->connection->isConnected()) {
734  return $this->connection->dispatch($this->makeEvent('!Connect'));
735  }
736  }
737 
752  protected function handle600($origin, $msg)
753  {
754  // \Erebot\Interfaces\Numerics::RPL_LOGON
755  return $this->watchList('!Notify', $msg);
756  }
757 
772  protected function handle601($origin, $msg)
773  {
774  // \Erebot\Interfaces\Numerics::RPL_LOGOFF
775  return $this->watchList('!UnNotify', $msg);
776  }
777 
792  protected function handle604($origin, $msg)
793  {
794  // \Erebot\Interfaces\Numerics::RPL_NOWON
795  return $this->watchList('!Notify', $msg);
796  }
797 
812  protected function handle605($origin, $msg)
813  {
814  // \Erebot\Interfaces\Numerics::RPL_NOWOFF
815  return $this->watchList('!UnNotify', $msg);
816  }
817 
829  protected function watchList($event, $msg)
830  {
831  // <bot> <nick> <ident> <host> <timestamp> :<msg>
832  unset($msg[0]);
833  $nick = $msg[0];
834  $ident = $msg[1];
835  $host = $msg[2];
836  $timestamp = intval($msg[3], 10);
837  $timestamp = new \DateTime('@'.$timestamp);
838  $text = $msg[4];
839 
840  return $this->connection->dispatch(
841  $this->makeEvent(
842  $event,
843  $nick,
844  ($ident == '*' ? null : $ident),
845  ($host == '*' ? null : $host),
846  $timestamp,
847  $text
848  )
849  );
850  }
851 }
handle255($origin, $msg)
Definition: IrcParser.php:726
getEventClass($iface)
Definition: IrcParser.php:196
handleQUIT($origin, $msg)
Definition: IrcParser.php:685
makeEvent($iface)
Definition: IrcParser.php:127
handleKICK($origin, $msg)
Definition: IrcParser.php:453
handleMODE($origin, $msg)
Definition: IrcParser.php:473
handlePART($origin, $msg)
Definition: IrcParser.php:600
handleNICK($origin, $msg)
Definition: IrcParser.php:558
handlePING($origin, $msg)
Definition: IrcParser.php:624
handleNOTICE($origin, $msg)
Definition: IrcParser.php:578
static ctcpUnquote($msg)
Definition: IrcParser.php:160
handle605($origin, $msg)
Definition: IrcParser.php:812
handleJOIN($origin, $msg)
Definition: IrcParser.php:434
setEventClass($iface, $cls)
Definition: IrcParser.php:247
handlePONG($origin, $msg)
Definition: IrcParser.php:643
handle601($origin, $msg)
Definition: IrcParser.php:772
setEventClasses($events)
Definition: IrcParser.php:228
A class that can parse IRC messages and produce events to match the commands in those messages...
Definition: IrcParser.php:29
watchList($event, $msg)
Definition: IrcParser.php:829
$connection
IRC connection that will send us some messages to parse.
Definition: IrcParser.php:51
handle604($origin, $msg)
Definition: IrcParser.php:792
handle600($origin, $msg)
Definition: IrcParser.php:752
handleTOPIC($origin, $msg)
Definition: IrcParser.php:704
static stripCodes($text, $strip=self::STRIP_ALL)
Definition: IrcParser.php:88
$eventsMapping
Mappings from (lowercase) interface names to actual classes.
Definition: IrcParser.php:48
__construct(\Erebot\Interfaces\Connection $connection)
Definition: IrcParser.php:68
handlePRIVMSG($origin, $msg)
Definition: IrcParser.php:663
handleINVITE($origin, $msg)
Definition: IrcParser.php:415
noticeOrPrivmsg($origin, $msg, $mapping)
Definition: IrcParser.php:347