[ Index ]

PHP Cross Reference of Phabricator

title

Body

[close]

/src/infrastructure/daemon/bot/adapter/ -> PhabricatorIRCProtocolAdapter.php (source)

   1  <?php
   2  
   3  final class PhabricatorIRCProtocolAdapter
   4    extends PhabricatorBaseProtocolAdapter {
   5  
   6    private $socket;
   7  
   8    private $writeBuffer;
   9    private $readBuffer;
  10  
  11    private $nickIncrement = 0;
  12  
  13    public function getServiceType() {
  14      return 'IRC';
  15    }
  16  
  17    public function getServiceName() {
  18      return $this->getConfig('network', $this->getConfig('server'));
  19    }
  20  
  21    // Hash map of command translations
  22    public static $commandTranslations = array(
  23      'PRIVMSG' => 'MESSAGE',
  24    );
  25  
  26    public function connect() {
  27      $nick = $this->getConfig('nick', 'phabot');
  28      $server = $this->getConfig('server');
  29      $port = $this->getConfig('port', 6667);
  30      $pass = $this->getConfig('pass');
  31      $ssl = $this->getConfig('ssl', false);
  32      $user = $this->getConfig('user', $nick);
  33  
  34      if (!preg_match('/^[A-Za-z0-9_`[{}^|\]\\-]+$/', $nick)) {
  35        throw new Exception(
  36          "Nickname '{$nick}' is invalid!");
  37      }
  38  
  39      $errno = null;
  40      $error = null;
  41      if (!$ssl) {
  42        $socket = fsockopen($server, $port, $errno, $error);
  43      } else {
  44        $socket = fsockopen('ssl://'.$server, $port, $errno, $error);
  45      }
  46      if (!$socket) {
  47        throw new Exception("Failed to connect, #{$errno}: {$error}");
  48      }
  49      $ok = stream_set_blocking($socket, false);
  50      if (!$ok) {
  51        throw new Exception('Failed to set stream nonblocking.');
  52      }
  53  
  54      $this->socket = $socket;
  55      if ($pass) {
  56        $this->write("PASS {$pass}");
  57      }
  58      $this->write("NICK {$nick}");
  59      $this->write("USER {$user} 0 * :{$user}");
  60    }
  61  
  62    public function getNextMessages($poll_frequency) {
  63      $messages = array();
  64  
  65      $read = array($this->socket);
  66      if (strlen($this->writeBuffer)) {
  67        $write = array($this->socket);
  68      } else {
  69        $write = array();
  70      }
  71      $except = array();
  72  
  73      $ok = @stream_select($read, $write, $except, $timeout_sec = 1);
  74      if ($ok === false) {
  75        // We may have been interrupted by a signal, like a SIGINT. Try
  76        // selecting again. If the second select works, conclude that the failure
  77        // was most likely because we were signaled.
  78        $ok = @stream_select($read, $write, $except, $timeout_sec = 0);
  79        if ($ok === false) {
  80          throw new Exception('stream_select() failed!');
  81        }
  82      }
  83  
  84      if ($read) {
  85        // Test for connection termination; in PHP, fread() off a nonblocking,
  86        // closed socket is empty string.
  87        if (feof($this->socket)) {
  88          // This indicates the connection was terminated on the other side,
  89          // just exit via exception and let the overseer restart us after a
  90          // delay so we can reconnect.
  91          throw new Exception('Remote host closed connection.');
  92        }
  93        do {
  94          $data = fread($this->socket, 4096);
  95          if ($data === false) {
  96            throw new Exception('fread() failed!');
  97          } else {
  98            $messages[] = id(new PhabricatorBotMessage())
  99              ->setCommand('LOG')
 100              ->setBody('>>> '.$data);
 101            $this->readBuffer .= $data;
 102          }
 103        } while (strlen($data));
 104      }
 105  
 106      if ($write) {
 107        do {
 108          $len = fwrite($this->socket, $this->writeBuffer);
 109          if ($len === false) {
 110            throw new Exception('fwrite() failed!');
 111          } else if ($len === 0) {
 112            break;
 113          } else {
 114            $messages[] = id(new PhabricatorBotMessage())
 115              ->setCommand('LOG')
 116              ->setBody('>>> '.substr($this->writeBuffer, 0, $len));
 117            $this->writeBuffer = substr($this->writeBuffer, $len);
 118          }
 119        } while (strlen($this->writeBuffer));
 120      }
 121  
 122      while (($m = $this->processReadBuffer()) !== false) {
 123        if ($m !== null) {
 124          $messages[] = $m;
 125        }
 126      }
 127  
 128      return $messages;
 129    }
 130  
 131    private function write($message) {
 132      $this->writeBuffer .= $message."\r\n";
 133      return $this;
 134    }
 135  
 136    public function writeMessage(PhabricatorBotMessage $message) {
 137      switch ($message->getCommand()) {
 138        case 'MESSAGE':
 139        case 'PASTE':
 140          $name = $message->getTarget()->getName();
 141          $body = $message->getBody();
 142          $this->write("PRIVMSG {$name} :{$body}");
 143          return true;
 144        default:
 145          return false;
 146      }
 147    }
 148  
 149    private function processReadBuffer() {
 150      $until = strpos($this->readBuffer, "\r\n");
 151      if ($until === false) {
 152        return false;
 153      }
 154  
 155      $message = substr($this->readBuffer, 0, $until);
 156      $this->readBuffer = substr($this->readBuffer, $until + 2);
 157  
 158      $pattern =
 159        '/^'.
 160        '(?::(?P<sender>(\S+?))(?:!\S*)? )?'. // This may not be present.
 161        '(?P<command>[A-Z0-9]+) '.
 162        '(?P<data>.*)'.
 163        '$/';
 164  
 165      $matches = null;
 166      if (!preg_match($pattern, $message, $matches)) {
 167        throw new Exception("Unexpected message from server: {$message}");
 168      }
 169  
 170      if ($this->handleIRCProtocol($matches)) {
 171        return null;
 172      }
 173  
 174      $command = $this->getBotCommand($matches['command']);
 175      list($target, $body) = $this->parseMessageData($command, $matches['data']);
 176  
 177      if (!strlen($matches['sender'])) {
 178        $sender = null;
 179      } else {
 180        $sender = id(new PhabricatorBotUser())
 181         ->setName($matches['sender']);
 182      }
 183  
 184      $bot_message = id(new PhabricatorBotMessage())
 185        ->setSender($sender)
 186        ->setCommand($command)
 187        ->setTarget($target)
 188        ->setBody($body);
 189  
 190      return $bot_message;
 191    }
 192  
 193    private function handleIRCProtocol(array $matches) {
 194      $data = $matches['data'];
 195      switch ($matches['command']) {
 196        case '433': // Nickname already in use
 197          // If we receive this error, try appending "-1", "-2", etc. to the nick
 198          $this->nickIncrement++;
 199          $nick = $this->getConfig('nick', 'phabot').'-'.$this->nickIncrement;
 200          $this->write("NICK {$nick}");
 201          return true;
 202        case '422': // Error - no MOTD
 203        case '376': // End of MOTD
 204          $nickpass = $this->getConfig('nickpass');
 205          if ($nickpass) {
 206            $this->write("PRIVMSG nickserv :IDENTIFY {$nickpass}");
 207          }
 208          $join = $this->getConfig('join');
 209          if (!$join) {
 210            throw new Exception('Not configured to join any channels!');
 211          }
 212          foreach ($join as $channel) {
 213            $this->write("JOIN {$channel}");
 214          }
 215          return true;
 216        case 'PING':
 217          $this->write("PONG {$data}");
 218          return true;
 219      }
 220  
 221      return false;
 222    }
 223  
 224    private function getBotCommand($irc_command) {
 225      if (isset(self::$commandTranslations[$irc_command])) {
 226        return self::$commandTranslations[$irc_command];
 227      }
 228  
 229      // We have no translation for this command, use as-is
 230      return $irc_command;
 231    }
 232  
 233    private function parseMessageData($command, $data) {
 234      switch ($command) {
 235        case 'MESSAGE':
 236          $matches = null;
 237          if (preg_match('/^(\S+)\s+:?(.*)$/', $data, $matches)) {
 238  
 239            $target_name = $matches[1];
 240            if (strncmp($target_name, '#', 1) === 0) {
 241              $target = id(new PhabricatorBotChannel())
 242                ->setName($target_name);
 243            } else {
 244              $target = id(new PhabricatorBotUser())
 245                ->setName($target_name);
 246            }
 247  
 248            return array(
 249              $target,
 250              rtrim($matches[2], "\r\n"),
 251            );
 252          }
 253          break;
 254      }
 255  
 256      // By default we assume there is no target, only a body
 257      return array(
 258        null,
 259        $data,
 260      );
 261    }
 262  
 263    public function disconnect() {
 264      // NOTE: FreeNode doesn't show quit messages if you've recently joined a
 265      // channel, presumably to prevent some kind of abuse. If you're testing
 266      // this, you may need to stay connected to the network for a few minutes
 267      // before it works. If you disconnect too quickly, the server will replace
 268      // your message with a "Client Quit" message.
 269  
 270      $quit = $this->getConfig('quit', pht('Shutting down.'));
 271      $this->write("QUIT :{$quit}");
 272  
 273      // Flush the write buffer.
 274      while (strlen($this->writeBuffer)) {
 275        $this->getNextMessages(0);
 276      }
 277  
 278      @fclose($this->socket);
 279      $this->socket = null;
 280    }
 281  }


Generated: Sun Nov 30 09:20:46 2014 Cross-referenced by PHPXref 0.7.1