xmppdaemon.php 12.4 KB
Newer Older
1
#!/usr/bin/env php
2 3
<?php
/*
4
 * StatusNet - the distributed open-source microblogging tool
5
 * Copyright (C) 2008, 2009, StatusNet, Inc.
6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

21
define('INSTALLDIR', realpath(dirname(__FILE__) . '/..'));
22

23 24
$shortoptions = 'fi::';
$longoptions = array('id::', 'foreground');
25 26 27

$helptext = <<<END_OF_XMPP_HELP
Daemon script for receiving new notices from Jabber users.
28

29
    -i --id           Identity (default none)
30
    -f --foreground   Stay in the foreground (default background)
31

32
END_OF_XMPP_HELP;
33

34 35 36 37 38
require_once INSTALLDIR.'/scripts/commandline.inc';

require_once INSTALLDIR . '/lib/common.php';
require_once INSTALLDIR . '/lib/jabber.php';
require_once INSTALLDIR . '/lib/daemon.php';
39

Evan Prodromou's avatar
Evan Prodromou committed
40 41 42 43
# This is kind of clunky; we create a class to call the global functions
# in jabber.php, which create a new XMPP class. A more elegant (?) solution
# might be to use make this a subclass of XMPP.

44 45
class XMPPDaemon extends Daemon
{
46
    function __construct($resource=null, $daemonize=true)
47
    {
48 49
        parent::__construct($daemonize);

50 51 52 53 54 55 56 57
        static $attrs = array('server', 'port', 'user', 'password', 'host');

        foreach ($attrs as $attr)
        {
            $this->$attr = common_config('xmpp', $attr);
        }

        if ($resource) {
58
            $this->resource = $resource . 'daemon';
59 60 61 62
        } else {
            $this->resource = common_config('xmpp', 'resource') . 'daemon';
        }

63 64 65
        $this->jid = $this->user.'@'.$this->server.'/'.$this->resource;

        $this->log(LOG_INFO, "INITIALIZE XMPPDaemon {$this->jid}");
66 67
    }

68 69
    function connect()
    {
70 71 72 73 74 75 76 77 78 79
        $connect_to = ($this->host) ? $this->host : $this->server;

        $this->log(LOG_INFO, "Connecting to $connect_to on port $this->port");

        $this->conn = jabber_connect($this->resource);

        if (!$this->conn) {
            return false;
        }

80 81
        $this->log(LOG_INFO, "Connected");

82 83
        $this->conn->setReconnectTimeout(600);

84 85
        $this->log(LOG_INFO, "Sending initial presence.");

86
        jabber_send_presence("Send me a message to post a notice", 'available',
Evan Prodromou's avatar
Evan Prodromou committed
87
                             null, 'available', 100);
88 89 90

        $this->log(LOG_INFO, "Done connecting.");

91 92 93
        return !$this->conn->isDisconnected();
    }

94 95
    function name()
    {
96 97 98
        return strtolower('xmppdaemon.'.$this->resource);
    }

99 100
    function run()
    {
101 102
        if ($this->connect()) {

103 104
            $this->log(LOG_DEBUG, "Initializing stanza handlers.");

105 106 107 108
            $this->conn->addEventHandler('message', 'handle_message', $this);
            $this->conn->addEventHandler('presence', 'handle_presence', $this);
            $this->conn->addEventHandler('reconnect', 'handle_reconnect', $this);

109 110
            $this->log(LOG_DEBUG, "Beginning processing loop.");

111 112 113 114 115 116 117 118 119 120 121 122
            while ($this->conn->processTime(60)) {
                $this->sendPing();
            }
        }
    }

    function sendPing()
    {
        if (!isset($this->pingid)) {
            $this->pingid = 0;
        } else {
            $this->pingid++;
123
        }
124 125 126 127

        $this->log(LOG_DEBUG, "Sending ping #{$this->pingid}");

		$this->conn->send("<iq from='{$this->jid}' to='{$this->server}' id='ping_{$this->pingid}' type='get'><ping xmlns='urn:xmpp:ping'/></iq>");
128 129
    }

130 131
    function handle_reconnect(&$pl)
    {
132
        $this->log(LOG_DEBUG, "Got reconnection callback.");
133
        $this->conn->processUntil('session_start');
134
        $this->log(LOG_DEBUG, "Sending reconnection presence.");
Evan Prodromou's avatar
Evan Prodromou committed
135
        $this->conn->presence('Send me a message to post a notice', 'available', null, 'available', 100);
Evan Prodromou's avatar
Evan Prodromou committed
136 137 138 139 140
        unset($pl['xml']);
        $pl['xml'] = null;

        $pl = null;
        unset($pl);
141 142
    }

143 144
    function get_user($from)
    {
145 146 147 148
        $user = User::staticGet('jabber', jabber_normalize_jid($from));
        return $user;
    }

149 150
    function handle_message(&$pl)
    {
151 152
        $from = jabber_normalize_jid($pl['from']);

153
        if ($pl['type'] != 'chat') {
154
            $this->log(LOG_WARNING, "Ignoring message of type ".$pl['type']." from $from.");
155 156
            return;
        }
157

158
        if (mb_strlen($pl['body']) == 0) {
159
            $this->log(LOG_WARNING, "Ignoring message with empty body from $from.");
160 161 162 163 164 165 166
            return;
        }

        # Forwarded from another daemon (probably a broadcaster) for
        # us to handle

        if ($this->is_self($from)) {
167
            $this->log(LOG_INFO, "Got forwarded notice from self ($from).");
168
            $from = $this->get_ofrom($pl);
169
            $this->log(LOG_INFO, "Originally sent by $from.");
170
            if (is_null($from) || $this->is_self($from)) {
171
                $this->log(LOG_INFO, "Ignoring notice originally sent by $from.");
172 173 174 175 176 177
                return;
            }
        }

        $user = $this->get_user($from);

178 179 180 181
        // For common_current_user to work
        global $_cur;
        $_cur = $user;

182 183 184 185 186 187 188 189
        if (!$user) {
            $this->from_site($from, 'Unknown user; go to ' .
                             common_local_url('imsettings') .
                             ' to add your address to your account');
            $this->log(LOG_WARNING, 'Message from unknown user ' . $from);
            return;
        }
        if ($this->handle_command($user, $pl['body'])) {
190
            $this->log(LOG_INFO, "Command messag by $from handled.");
191 192 193 194 195 196 197 198
            return;
        } else if ($this->is_autoreply($pl['body'])) {
            $this->log(LOG_INFO, 'Ignoring auto reply from ' . $from);
            return;
        } else if ($this->is_otr($pl['body'])) {
            $this->log(LOG_INFO, 'Ignoring OTR from ' . $from);
            return;
        } else if ($this->is_direct($pl['body'])) {
199 200
            $this->log(LOG_INFO, 'Got a direct message ' . $from);

201 202 203 204
            preg_match_all('/d[\ ]*([a-z0-9]{1,64})/', $pl['body'], $to);

            $to = preg_replace('/^d([\ ])*/', '', $to[0][0]);
            $body = preg_replace('/d[\ ]*('. $to .')[\ ]*/', '', $pl['body']);
205 206 207

            $this->log(LOG_INFO, 'Direct message from '. $user->nickname . ' to ' . $to);

208 209
            $this->add_direct($user, $body, $to, $from);
        } else {
210 211 212

            $this->log(LOG_INFO, 'Posting a notice from ' . $user->nickname);

213 214 215 216 217
            $this->add_notice($user, $pl);
        }

        $user->free();
        unset($user);
218
        unset($_cur);
Evan Prodromou's avatar
Evan Prodromou committed
219 220 221 222 223 224

        unset($pl['xml']);
        $pl['xml'] = null;

        $pl = null;
        unset($pl);
225 226
    }

227 228
    function is_self($from)
    {
229 230 231
        return preg_match('/^'.strtolower(jabber_daemon_address()).'/', strtolower($from));
    }

232 233
    function get_ofrom($pl)
    {
234 235 236 237
        $xml = $pl['xml'];
        $addresses = $xml->sub('addresses');
        if (!$addresses) {
            $this->log(LOG_WARNING, 'Forwarded message without addresses');
Evan Prodromou's avatar
Evan Prodromou committed
238
            return null;
239 240 241 242
        }
        $address = $addresses->sub('address');
        if (!$address) {
            $this->log(LOG_WARNING, 'Forwarded message without address');
Evan Prodromou's avatar
Evan Prodromou committed
243
            return null;
244 245 246
        }
        if (!array_key_exists('type', $address->attrs)) {
            $this->log(LOG_WARNING, 'No type for forwarded message');
Evan Prodromou's avatar
Evan Prodromou committed
247
            return null;
248 249 250 251
        }
        $type = $address->attrs['type'];
        if ($type != 'ofrom') {
            $this->log(LOG_WARNING, 'Type of forwarded message is not ofrom');
Evan Prodromou's avatar
Evan Prodromou committed
252
            return null;
253 254 255
        }
        if (!array_key_exists('jid', $address->attrs)) {
            $this->log(LOG_WARNING, 'No jid for forwarded message');
Evan Prodromou's avatar
Evan Prodromou committed
256
            return null;
257 258 259 260
        }
        $jid = $address->attrs['jid'];
        if (!$jid) {
            $this->log(LOG_WARNING, 'Could not get jid from address');
Evan Prodromou's avatar
Evan Prodromou committed
261
            return null;
262 263 264 265 266
        }
        $this->log(LOG_DEBUG, 'Got message forwarded from jid ' . $jid);
        return $jid;
    }

267 268
    function is_autoreply($txt)
    {
269 270
        if (preg_match('/[\[\(]?[Aa]uto[-\s]?[Rr]e(ply|sponse)[\]\)]/', $txt)) {
            return true;
Evan Prodromou's avatar
Evan Prodromou committed
271 272
        } else if (preg_match('/^System: Message wasn\'t delivered. Offline storage size was exceeded.$/', $txt)) {
            return true;
273 274 275 276 277
        } else {
            return false;
        }
    }

278 279
    function is_otr($txt)
    {
280 281 282 283 284 285 286
        if (preg_match('/^\?OTR/', $txt)) {
            return true;
        } else {
            return false;
        }
    }

287 288
    function is_direct($txt)
    {
289 290 291 292 293 294 295
        if (strtolower(substr($txt, 0, 2))=='d ') {
            return true;
        } else {
            return false;
        }
    }

296 297
    function from_site($address, $msg)
    {
298 299 300 301
        $text = '['.common_config('site', 'name') . '] ' . $msg;
        jabber_send_message($address, $text);
    }

302 303
    function handle_command($user, $body)
    {
304 305 306 307 308 309 310 311 312 313 314
        $inter = new CommandInterpreter();
        $cmd = $inter->handle_command($user, $body);
        if ($cmd) {
            $chan = new XMPPChannel($this->conn);
            $cmd->execute($chan);
            return true;
        } else {
            return false;
        }
    }

315 316
    function add_notice(&$user, &$pl)
    {
317
        $body = trim($pl['body']);
318
        $content_shortened = common_shorten_links($body);
319
        if (Notice::contentTooLong($content_shortened)) {
320
          $from = jabber_normalize_jid($pl['from']);
321 322 323
          $this->from_site($from, sprintf(_("Message too long - maximum is %d characters, you sent %d"),
                                          Notice::maxContent(),
                                          mb_strlen($content_shortened)));
324
          return;
325
        }
326 327 328 329 330 331

        try {
            $notice = Notice::saveNew($user->id, $content_shortened, 'xmpp');
        } catch (Exception $e) {
            $this->log(LOG_ERR, $e->getMessage());
            $this->from_site($user->jabber, $e->getMessage());
332 333
            return;
        }
334

335 336 337 338 339 340 341
        common_broadcast_notice($notice);
        $this->log(LOG_INFO,
                   'Added notice ' . $notice->id . ' from user ' . $user->nickname);
        $notice->free();
        unset($notice);
    }

342 343
    function handle_presence(&$pl)
    {
344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374
        $from = jabber_normalize_jid($pl['from']);
        switch ($pl['type']) {
         case 'subscribe':
            # We let anyone subscribe
            $this->subscribed($from);
            $this->log(LOG_INFO,
                       'Accepted subscription from ' . $from);
            break;
         case 'subscribed':
         case 'unsubscribed':
         case 'unsubscribe':
            $this->log(LOG_INFO,
                       'Ignoring  "' . $pl['type'] . '" from ' . $from);
            break;
         default:
            if (!$pl['type']) {
                $user = User::staticGet('jabber', $from);
                if (!$user) {
                    $this->log(LOG_WARNING, 'Presence from unknown user ' . $from);
                    return;
                }
                if ($user->updatefrompresence) {
                    $this->log(LOG_INFO, 'Updating ' . $user->nickname .
                               ' status from presence.');
                    $this->add_notice($user, $pl);
                }
                $user->free();
                unset($user);
            }
            break;
        }
Evan Prodromou's avatar
Evan Prodromou committed
375 376 377 378 379
        unset($pl['xml']);
        $pl['xml'] = null;

        $pl = null;
        unset($pl);
380 381
    }

382 383
    function log($level, $msg)
    {
384 385 386 387 388 389 390 391
        $text = 'XMPPDaemon('.$this->resource.'): '.$msg;
        common_log($level, $text);
        if (!$this->daemonize)
        {
            $line = common_log_line($level, $text);
            echo $line;
            echo "\n";
        }
392 393
    }

394 395
    function subscribed($to)
    {
396 397
        jabber_special_presence('subscribed', $to);
    }
398 399
}

400 401 402 403 404 405 406
// Abort immediately if xmpp is not enabled, otherwise the daemon chews up
// lots of CPU trying to connect to unconfigured servers
if (common_config('xmpp','enabled')==false) {
    print "Aborting daemon - xmpp is disabled\n";
    exit();
}

407 408
if (have_option('i', 'id')) {
    $id = get_option_value('i', 'id');
409
} else if (count($args) > 0) {
410
    $id = $args[0];
411
} else {
412
    $id = null;
413
}
414

415 416
$foreground = have_option('f', 'foreground');

417
$daemon = new XMPPDaemon($id, !$foreground);
418

419
$daemon->runOnce();