jabber.php 14.4 KB
Newer Older
Evan Prodromou's avatar
Evan Prodromou committed
1
<?php
2
/**
3
 * StatusNet, the distributed open-source microblogging tool
Evan Prodromou's avatar
Evan Prodromou committed
4
 *
5 6 7 8 9
 * utility functions for Jabber/GTalk/XMPP messages
 *
 * PHP version 5
 *
 * LICENCE: This program is free software: you can redistribute it and/or modify
Evan Prodromou's avatar
Evan Prodromou committed
10 11 12 13 14 15 16 17 18 19 20
 * 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 22
 *
 * @category  Network
23
 * @package   StatusNet
24
 * @author    Evan Prodromou <evan@status.net>
25
 * @copyright 2008 StatusNet, Inc.
26
 * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
27
 * @link      http://status.net/
Evan Prodromou's avatar
Evan Prodromou committed
28 29
 */

30
if (!defined('STATUSNET') && !defined('LACONICA')) {
31 32
    exit(1);
}
Evan Prodromou's avatar
Evan Prodromou committed
33

34
require_once 'XMPPHP/XMPP.php';
Evan Prodromou's avatar
Evan Prodromou committed
35

36 37 38 39 40 41 42
/**
 * checks whether a string is a syntactically valid Jabber ID (JID)
 *
 * @param string $jid string to check
 *
 * @return     boolean whether the string is a valid JID
 */
Evan Prodromou's avatar
Evan Prodromou committed
43

44 45 46 47
function jabber_valid_base_jid($jid)
{
    // Cheap but effective
    return Validate::email($jid);
Evan Prodromou's avatar
Evan Prodromou committed
48
}
49

50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
/**
 * normalizes a Jabber ID for comparison
 *
 * @param string $jid JID to check
 *
 * @return string an equivalent JID in normalized (lowercase) form
 */

function jabber_normalize_jid($jid)
{
    if (preg_match("/(?:([^\@]+)\@)?([^\/]+)(?:\/(.*))?$/", $jid, $matches)) {
        $node   = $matches[1];
        $server = $matches[2];
        return strtolower($node.'@'.$server);
    } else {
        return null;
    }
67 68
}

69
/**
70
 * the JID of the Jabber daemon for this StatusNet instance
71 72 73 74 75 76 77
 *
 * @return string JID of the Jabber daemon
 */

function jabber_daemon_address()
{
    return common_config('xmpp', 'user') . '@' . common_config('xmpp', 'server');
78 79
}

80 81 82 83 84 85 86 87
class Sharing_XMPP extends XMPPHP_XMPP
{
    function getSocket()
    {
        return $this->socket;
    }
}

88 89 90 91 92 93 94 95 96 97 98 99
/**
 * connect the configured Jabber account to the configured server
 *
 * @param string $resource Resource to connect (defaults to configured resource)
 *
 * @return XMPPHP connection to the configured server
 */

function jabber_connect($resource=null)
{
    static $conn = null;
    if (!$conn) {
100
        $conn = new Sharing_XMPP(common_config('xmpp', 'host') ?
101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124
                                common_config('xmpp', 'host') :
                                common_config('xmpp', 'server'),
                                common_config('xmpp', 'port'),
                                common_config('xmpp', 'user'),
                                common_config('xmpp', 'password'),
                                ($resource) ? $resource :
                                common_config('xmpp', 'resource'),
                                common_config('xmpp', 'server'),
                                common_config('xmpp', 'debug') ?
                                true : false,
                                common_config('xmpp', 'debug') ?
                                XMPPHP_Log::LEVEL_VERBOSE :  null
                                );

        if (!$conn) {
            return false;
        }

        $conn->autoSubscribe();
        $conn->useEncryption(common_config('xmpp', 'encryption'));

        try {
            $conn->connect(true); // true = persistent connection
        } catch (XMPPHP_Exception $e) {
125
            common_log(LOG_ERR, $e->getMessage());
126 127 128 129 130 131
            return false;
        }

        $conn->processUntil('session_start');
    }
    return $conn;
132 133
}

134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
/**
 * send a single notice to a given Jabber address
 *
 * @param string $to     JID to send the notice to
 * @param Notice $notice notice to send
 *
 * @return boolean success value
 */

function jabber_send_notice($to, $notice)
{
    $conn = jabber_connect();
    if (!$conn) {
        return false;
    }
    $profile = Profile::staticGet($notice->profile_id);
    if (!$profile) {
        common_log(LOG_WARNING, 'Refusing to send notice with ' .
                   'unknown profile ' . common_log_objstring($notice),
                   __FILE__);
        return false;
    }
    $msg   = jabber_format_notice($profile, $notice);
    $entry = jabber_format_entry($profile, $notice);
    $conn->message($to, $msg, 'chat', null, $entry);
    $profile->free();
    return true;
161 162
}

163 164 165 166 167 168 169 170 171 172 173
/**
 * extra information for XMPP messages, as defined by Twitter
 *
 * @param Profile $profile Profile of the sending user
 * @param Notice  $notice  Notice being sent
 *
 * @return string Extra information (Atom, HTML, addresses) in string format
 */

function jabber_format_entry($profile, $notice)
{
174 175 176 177 178
    $entry = $notice->asAtomEntry(true, true);

    $xs = new XMLStringer();
    $xs->elementStart('html', array('xmlns' => 'http://jabber.org/protocol/xhtml-im'));
    $xs->elementStart('body', array('xmlns' => 'http://www.w3.org/1999/xhtml'));
179
    $xs->element("img", array('src'=> $profile->avatarUrl(AVATAR_MINI_SIZE) , 'alt' => $profile->nickname));
180 181 182 183 184 185 186
    $xs->element('a', array('href' => $profile->profileurl),
                 $profile->nickname);
    $xs->text(": ");
    if (!empty($notice->rendered)) {
        $xs->raw($notice->rendered);
    } else {
        $xs->raw(common_render_content($notice->content, $notice));
187
    }
188 189 190 191 192
    $xs->raw(" ");
    $xs->element('a', array(
        'href'=>common_local_url('conversation',
            array('id' => $notice->conversation)).'#notice-'.$notice->id
         ),sprintf(_('notice id: %s'),$notice->id));
193 194
    $xs->elementEnd('body');
    $xs->elementEnd('html');
195

196
    $html = $xs->getString();
197

198
    return $html . ' ' . $entry;
199 200
}

201 202 203 204 205 206 207 208 209 210 211 212
/**
 * sends a single text message to a given JID
 *
 * @param string $to      JID to send the message to
 * @param string $body    body of the message
 * @param string $type    type of the message
 * @param string $subject subject of the message
 *
 * @return boolean success flag
 */

function jabber_send_message($to, $body, $type='chat', $subject=null)
213
{
214 215 216 217 218 219
    $conn = jabber_connect();
    if (!$conn) {
        return false;
    }
    $conn->message($to, $body, $type, $subject);
    return true;
220 221
}

222 223 224 225 226 227 228 229 230 231 232
/**
 * sends a presence stanza on the Jabber network
 *
 * @param string $status   current status, free-form string
 * @param string $show     structured status value
 * @param string $to       recipient of presence, null for general
 * @param string $type     type of status message, related to $show
 * @param int    $priority priority of the presence
 *
 * @return boolean success value
 */
233

234 235 236 237 238 239 240 241 242
function jabber_send_presence($status, $show='available', $to=null,
                              $type = 'available', $priority=null)
{
    $conn = jabber_connect();
    if (!$conn) {
        return false;
    }
    $conn->presence($status, $show, $to, $type, $priority);
    return true;
243
}
244

245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265
/**
 * sends a confirmation request to a JID
 *
 * @param string $code     confirmation code for confirmation URL
 * @param string $nickname nickname of confirming user
 * @param string $address  JID to send confirmation to
 *
 * @return boolean success flag
 */

function jabber_confirm_address($code, $nickname, $address)
{
    $body = 'User "' . $nickname . '" on ' . common_config('site', 'name') . ' ' .
      'has said that your Jabber ID belongs to them. ' .
      'If that\'s true, you can confirm by clicking on this URL: ' .
      common_local_url('confirmaddress', array('code' => $code)) .
      ' . (If you cannot click it, copy-and-paste it into the ' .
      'address bar of your browser). If that user isn\'t you, ' .
      'or if you didn\'t request this confirmation, just ignore this message.';

    return jabber_send_message($address, $body);
266 267
}

268 269 270 271 272 273 274 275 276 277 278 279
/**
 * sends a "special" presence stanza on the Jabber network
 *
 * @param string $type   Type of presence
 * @param string $to     JID to send presence to
 * @param string $show   show value for presence
 * @param string $status status value for presence
 *
 * @return boolean success flag
 *
 * @see jabber_send_presence()
 */
280

281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309
function jabber_special_presence($type, $to=null, $show=null, $status=null)
{
    // FIXME: why use this instead of jabber_send_presence()?
    $conn = jabber_connect();

    $to     = htmlspecialchars($to);
    $status = htmlspecialchars($status);

    $out = "<presence";
    if ($to) {
        $out .= " to='$to'";
    }
    if ($type) {
        $out .= " type='$type'";
    }
    if ($show == 'available' and !$status) {
        $out .= "/>";
    } else {
        $out .= ">";
        if ($show && ($show != 'available')) {
            $out .= "<show>$show</show>";
        }
        if ($status) {
            $out .= "<status>$status</status>";
        }
        $out .= "</presence>";
    }
    $conn->send($out);
}
310

311 312 313 314 315 316 317
/**
 * broadcast a notice to all subscribers and reply recipients
 *
 * This function will send a notice to all subscribers on the local server
 * who have Jabber addresses, and have Jabber notification enabled, and
 * have this subscription enabled for Jabber. It also sends the notice to
 * all recipients of @-replies who have Jabber addresses and Jabber notification
318
 * enabled. This is really the heart of Jabber distribution in StatusNet.
319 320 321 322 323
 *
 * @param Notice $notice The notice to broadcast
 *
 * @return boolean success flag
 */
324

325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350
function jabber_broadcast_notice($notice)
{
    if (!common_config('xmpp', 'enabled')) {
        return true;
    }
    $profile = Profile::staticGet($notice->profile_id);

    if (!$profile) {
        common_log(LOG_WARNING, 'Refusing to broadcast notice with ' .
                   'unknown profile ' . common_log_objstring($notice),
                   __FILE__);
        return false;
    }

    $msg   = jabber_format_notice($profile, $notice);
    $entry = jabber_format_entry($profile, $notice);

    $profile->free();
    unset($profile);

    $sent_to = array();

    $conn = jabber_connect();

    // First, get users to whom this is a direct reply
    $user = new User();
351 352 353
    $UT = common_config('db','type')=='pgsql'?'"user"':'user';
    $user->query("SELECT $UT.id, $UT.jabber " .
                 "FROM $UT JOIN reply ON $UT.id = reply.profile_id " .
354
                 'WHERE reply.notice_id = ' . $notice->id . ' ' .
355 356 357
                 "AND $UT.jabber is not null " .
                 "AND $UT.jabbernotify = 1 " .
                 "AND $UT.jabberreplies = 1 ");
358 359 360 361 362 363 364 365 366 367 368 369 370 371 372

    while ($user->fetch()) {
        common_log(LOG_INFO,
                   'Sending reply notice ' . $notice->id . ' to ' . $user->jabber,
                   __FILE__);
        $conn->message($user->jabber, $msg, 'chat', null, $entry);
        $conn->processTime(0);
        $sent_to[$user->id] = 1;
    }

    $user->free();

    // Now, get users subscribed to this profile

    $user = new User();
373 374 375
    $user->query("SELECT $UT.id, $UT.jabber " .
                 "FROM $UT JOIN subscription " .
                 "ON $UT.id = subscription.subscriber " .
376
                 'WHERE subscription.subscribed = ' . $notice->profile_id . ' ' .
377 378
                 "AND $UT.jabber is not null " .
                 "AND $UT.jabbernotify = 1 " .
379 380
                 'AND subscription.jabber = 1 ');

381 382 383 384 385 386 387 388 389
    while ($user->fetch()) {
        if (!array_key_exists($user->id, $sent_to)) {
            common_log(LOG_INFO,
                       'Sending notice ' . $notice->id . ' to ' . $user->jabber,
                       __FILE__);
            $conn->message($user->jabber, $msg, 'chat', null, $entry);
            // To keep the incoming queue from filling up,
            // we service it after each send.
            $conn->processTime(0);
390
            $sent_to[$user->id] = 1;
391 392 393 394 395 396
        }
    }

    // Now, get users who have it in their inbox because of groups

    $user = new User();
397 398 399
    $user->query("SELECT $UT.id, $UT.jabber " .
                 "FROM $UT JOIN notice_inbox " .
                 "ON $UT.id = notice_inbox.user_id " .
400
                 'WHERE notice_inbox.notice_id = ' . $notice->id . ' ' .
401
                 'AND notice_inbox.source = 2 ' .
402 403
                 "AND $UT.jabber is not null " .
                 "AND $UT.jabbernotify = 1 ");
404

405 406 407 408 409 410 411 412 413
    while ($user->fetch()) {
        if (!array_key_exists($user->id, $sent_to)) {
            common_log(LOG_INFO,
                       'Sending notice ' . $notice->id . ' to ' . $user->jabber,
                       __FILE__);
            $conn->message($user->jabber, $msg, 'chat', null, $entry);
            // To keep the incoming queue from filling up,
            // we service it after each send.
            $conn->processTime(0);
414
            $sent_to[$user->id] = 1;
415 416 417 418 419 420
        }
    }

    $user->free();

    return true;
421 422
}

423 424 425 426 427 428 429 430 431 432
/**
 * send a notice to all public listeners
 *
 * For notices that are generated on the local system (by users), we can optionally
 * forward them to remote listeners by XMPP.
 *
 * @param Notice $notice notice to broadcast
 *
 * @return boolean success flag
 */
433

434 435 436
function jabber_public_notice($notice)
{
    // Now, users who want everything
437

438
    $public = common_config('xmpp', 'public');
439

Siebrand Mazeland's avatar
Siebrand Mazeland committed
440
    // FIXME PRIV do not send out private messages here
441 442
    // XXX: should we send out non-local messages if public,localonly
    // = false? I think not
443

444 445
    if ($public && $notice->is_local) {
        $profile = Profile::staticGet($notice->profile_id);
446

447 448 449 450 451 452
        if (!$profile) {
            common_log(LOG_WARNING, 'Refusing to broadcast notice with ' .
                       'unknown profile ' . common_log_objstring($notice),
                       __FILE__);
            return false;
        }
453

454 455
        $msg   = jabber_format_notice($profile, $notice);
        $entry = jabber_format_entry($profile, $notice);
456

457
        $conn = jabber_connect();
458

459 460 461 462 463 464 465 466 467 468
        foreach ($public as $address) {
            common_log(LOG_INFO,
                       'Sending notice ' . $notice->id .
                       ' to public listener ' . $address,
                       __FILE__);
            $conn->message($address, $msg, 'chat', null, $entry);
            $conn->processTime(0);
        }
        $profile->free();
    }
469

470
    return true;
Evan Prodromou's avatar
Evan Prodromou committed
471
}
472

473 474 475 476 477 478 479 480 481 482 483 484
/**
 * makes a plain-text formatted version of a notice, suitable for Jabber distribution
 *
 * @param Profile &$profile profile of the sending user
 * @param Notice  &$notice  notice being sent
 *
 * @return string plain-text version of the notice, with user nickname prefixed
 */

function jabber_format_notice(&$profile, &$notice)
{
    return $profile->nickname . ': ' . $notice->content;
Evan Prodromou's avatar
Evan Prodromou committed
485
}