[ Index ] |
PHP Cross Reference of Phabricator |
[Summary view] [Print] [Text view]
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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Sun Nov 30 09:20:46 2014 | Cross-referenced by PHPXref 0.7.1 |