ircmanager.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  1. <?php
  2. /*
  3. * StatusNet - the distributed open-source microblogging tool
  4. * Copyright (C) 2008, 2009, StatusNet, Inc.
  5. *
  6. * This program is free software: you can redistribute it and/or modify
  7. * it under the terms of the GNU Affero General Public License as published by
  8. * the Free Software Foundation, either version 3 of the License, or
  9. * (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. * GNU Affero General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Affero General Public License
  17. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  18. */
  19. if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
  20. /**
  21. * IRC background connection manager for IRC-using queue handlers,
  22. * allowing them to send outgoing messages on the right connection.
  23. *
  24. * Input is handled during socket select loop, Any incoming messages will be handled.
  25. *
  26. * In a multi-site queuedaemon.php run, one connection will be instantiated
  27. * for each site being handled by the current process that has IRC enabled.
  28. */
  29. class IrcManager extends ImManager {
  30. protected $conn = null;
  31. protected $lastPing = null;
  32. protected $messageWaiting = true;
  33. protected $lastMessage = null;
  34. protected $regChecks = array();
  35. protected $regChecksLookup = array();
  36. protected $connected = false;
  37. /**
  38. * Initialize connection to server.
  39. *
  40. * @return boolean true on success
  41. */
  42. public function start($master) {
  43. if (parent::start($master)) {
  44. $this->connect();
  45. return true;
  46. } else {
  47. return false;
  48. }
  49. }
  50. /**
  51. * Return any open sockets that the run loop should listen
  52. * for input on.
  53. *
  54. * @return array Array of socket resources
  55. */
  56. public function getSockets() {
  57. $this->connect();
  58. if ($this->conn) {
  59. return $this->conn->getSockets();
  60. } else {
  61. return array();
  62. }
  63. }
  64. /**
  65. * Request a maximum timeout for listeners before the next idle period.
  66. *
  67. * @return integer Maximum timeout
  68. */
  69. public function timeout() {
  70. if ($this->messageWaiting) {
  71. return 1;
  72. } else {
  73. return $this->plugin->pinginterval;
  74. }
  75. }
  76. /**
  77. * Idle processing for io manager's execution loop.
  78. *
  79. * @return void
  80. */
  81. public function idle() {
  82. // Send a ping if necessary
  83. if (empty($this->lastPing) || time() - $this->lastPing > $this->plugin->pinginterval) {
  84. $this->sendPing();
  85. }
  86. if ($this->connected) {
  87. // Send a waiting message if appropriate
  88. if ($this->messageWaiting && time() - $this->lastMessage > 1) {
  89. $wm = Irc_waiting_message::top();
  90. if ($wm === NULL) {
  91. $this->messageWaiting = false;
  92. return;
  93. }
  94. $data = unserialize($wm->data);
  95. $wm->incAttempts();
  96. if ($this->send_raw_message($data)) {
  97. $wm->delete();
  98. } else {
  99. if ($wm->attempts <= common_config('queue', 'max_retries')) {
  100. // Try again next idle
  101. $wm->releaseClaim();
  102. } else {
  103. // Exceeded the maximum number of retries
  104. $wm->delete();
  105. }
  106. }
  107. }
  108. }
  109. }
  110. /**
  111. * Process IRC events that have come in over the wire.
  112. *
  113. * @param resource $socket Socket to handle input on
  114. * @return void
  115. */
  116. public function handleInput($socket) {
  117. common_log(LOG_DEBUG, 'Servicing the IRC queue.');
  118. $this->stats('irc_process');
  119. try {
  120. $this->conn->handleEvents();
  121. } catch (Phergie_Driver_Exception $e) {
  122. $this->connected = false;
  123. $this->conn->reconnect();
  124. }
  125. }
  126. /**
  127. * Initiate connection
  128. *
  129. * @return void
  130. */
  131. public function connect() {
  132. if (!$this->conn) {
  133. $this->conn = new Phergie_StatusnetBot;
  134. $config = new Phergie_Config;
  135. $config->readArray(
  136. array(
  137. 'connections' => array(
  138. array(
  139. 'host' => $this->plugin->host,
  140. 'port' => $this->plugin->port,
  141. 'username' => $this->plugin->username,
  142. 'realname' => $this->plugin->realname,
  143. 'nick' => $this->plugin->nick,
  144. 'password' => $this->plugin->password,
  145. 'transport' => $this->plugin->transporttype,
  146. 'encoding' => $this->plugin->encoding
  147. )
  148. ),
  149. 'driver' => 'statusnet',
  150. 'processor' => 'async',
  151. 'processor.options' => array('sec' => 0, 'usec' => 0),
  152. 'plugins' => array(
  153. 'Pong',
  154. 'NickServ',
  155. 'AutoJoin',
  156. 'Statusnet',
  157. ),
  158. 'plugins.autoload' => true,
  159. // Uncomment to enable debugging output
  160. //'ui.enabled' => true,
  161. 'nickserv.password' => $this->plugin->nickservpassword,
  162. 'nickserv.identify_message' => $this->plugin->nickservidentifyregexp,
  163. 'autojoin.channels' => $this->plugin->channels,
  164. 'statusnet.messagecallback' => array($this, 'handle_irc_message'),
  165. 'statusnet.regcallback' => array($this, 'handle_reg_response'),
  166. 'statusnet.connectedcallback' => array($this, 'handle_connected'),
  167. 'statusnet.unregregexp' => $this->plugin->unregregexp,
  168. 'statusnet.regregexp' => $this->plugin->regregexp
  169. )
  170. );
  171. $this->conn->setConfig($config);
  172. $this->conn->connect();
  173. $this->lastPing = time();
  174. $this->lastMessage = time();
  175. }
  176. return $this->conn;
  177. }
  178. /**
  179. * Called via a callback when a message is received
  180. * Passes it back to the queuing system
  181. *
  182. * @param array $data Data
  183. * @return boolean
  184. */
  185. public function handle_irc_message($data) {
  186. $this->plugin->enqueueIncomingRaw($data);
  187. return true;
  188. }
  189. /**
  190. * Called via a callback when NickServ responds to
  191. * the bots query asking if a nick is registered
  192. *
  193. * @param array $data Data
  194. * @return void
  195. */
  196. public function handle_reg_response($data) {
  197. // Retrieve data
  198. $screenname = $data['screenname'];
  199. $nickdata = $this->regChecks[$screenname];
  200. $usernick = $nickdata['user']->nickname;
  201. if (isset($this->regChecksLookup[$usernick])) {
  202. if ($data['registered']) {
  203. // Send message
  204. $this->plugin->sendConfirmationCode($screenname, $nickdata['code'], $nickdata['user'], true);
  205. } else {
  206. // TRANS: Message given when using an unregistered IRC nickname.
  207. $this->plugin->sendMessage($screenname, _m('Your nickname is not registered so IRC connectivity cannot be enabled.'));
  208. $confirm = new Confirm_address();
  209. $confirm->user_id = $user->id;
  210. $confirm->address_type = $this->plugin->transport;
  211. if ($confirm->find(true)) {
  212. $result = $confirm->delete();
  213. if (!$result) {
  214. common_log_db_error($confirm, 'DELETE', __FILE__);
  215. // TRANS: Server error thrown on database error when deleting IRC nickname confirmation.
  216. throw new ServerException(_m('Could not delete confirmation.'));
  217. }
  218. }
  219. }
  220. // Unset lookup value
  221. unset($this->regChecksLookup[$usernick]);
  222. // Unset data
  223. unset($this->regChecks[$screename]);
  224. }
  225. }
  226. /**
  227. * Called when the connection is established
  228. *
  229. * @return void
  230. */
  231. public function handle_connected() {
  232. $this->connected = true;
  233. }
  234. /**
  235. * Enters a message into the database for sending when ready
  236. *
  237. * @param string $command Command
  238. * @param array $args Arguments
  239. * @return boolean
  240. */
  241. protected function enqueue_waiting_message($data) {
  242. $wm = new Irc_waiting_message();
  243. $wm->data = serialize($data);
  244. $wm->prioritise = $data['prioritise'];
  245. $wm->attempts = 0;
  246. $wm->created = common_sql_now();
  247. $result = $wm->insert();
  248. if (!$result) {
  249. common_log_db_error($wm, 'INSERT', __FILE__);
  250. // TRANS: Server exception thrown when an IRC waiting queue item could not be added to the database.
  251. throw new ServerException(_m('Database error inserting IRC waiting queue item.'));
  252. }
  253. return true;
  254. }
  255. /**
  256. * Send a message using the daemon
  257. *
  258. * @param $data Message data
  259. * @return boolean true on success
  260. */
  261. public function send_raw_message($data) {
  262. $this->connect();
  263. if (!$this->conn) {
  264. return false;
  265. }
  266. if ($data['type'] != 'delayedmessage') {
  267. if ($data['type'] != 'message') {
  268. // Nick checking
  269. $nickdata = $data['nickdata'];
  270. $usernick = $nickdata['user']->nickname;
  271. $screenname = $nickdata['screenname'];
  272. // Cancel any existing checks for this user
  273. if (isset($this->regChecksLookup[$usernick])) {
  274. unset($this->regChecks[$this->regChecksLookup[$usernick]]);
  275. }
  276. $this->regChecks[$screenname] = $nickdata;
  277. $this->regChecksLookup[$usernick] = $screenname;
  278. }
  279. // If there is a backlog or we need to wait, queue the message
  280. if ($this->messageWaiting || time() - $this->lastMessage < 1) {
  281. $this->enqueue_waiting_message(
  282. array(
  283. 'type' => 'delayedmessage',
  284. 'prioritise' => $data['prioritise'],
  285. 'data' => $data['data']
  286. )
  287. );
  288. $this->messageWaiting = true;
  289. return true;
  290. }
  291. }
  292. try {
  293. $this->conn->send($data['data']['command'], $data['data']['args']);
  294. } catch (Phergie_Driver_Exception $e) {
  295. $this->connected = false;
  296. $this->conn->reconnect();
  297. return false;
  298. }
  299. $this->lastMessage = time();
  300. return true;
  301. }
  302. /**
  303. * Sends a ping
  304. *
  305. * @return void
  306. */
  307. protected function sendPing() {
  308. $this->lastPing = time();
  309. $this->conn->send('PING', $this->lastPing);
  310. }
  311. }