We are no longer offering accounts on this server. Consider https://gitlab.freedesktop.org/ as a place to host projects.

OStatusPlugin.php 47.6 KB
Newer Older
1 2 3
<?php
/*
 * StatusNet - the distributed open-source microblogging tool
4
 * Copyright (C) 2009-2010, StatusNet, Inc.
5 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 22 23 24
 * OStatusPlugin implementation for GNU Social
 *
 * Depends on: WebFinger plugin
 *
25
 * @package OStatusPlugin
26 27 28
 * @maintainer Brion Vibber <brion@status.net>
 */

29
if (!defined('GNUSOCIAL')) { exit(1); }
30

31
set_include_path(get_include_path() . PATH_SEPARATOR . dirname(__FILE__) . '/extlib/phpseclib');
James Walker's avatar
James Walker committed
32

33 34
class FeedSubException extends Exception
{
35 36 37 38 39 40 41 42 43
    function __construct($msg=null)
    {
        $type = get_class($this);
        if ($msg) {
            parent::__construct("$type: $msg");
        } else {
            parent::__construct($type);
        }
    }
44 45
}

46
class OStatusPlugin extends Plugin
47 48 49 50 51 52 53 54 55
{
    /**
     * Hook for RouterInitialized event.
     *
     * @param Net_URL_Mapper $m path-to-action mapper
     * @return boolean hook return
     */
    function onRouterInitialized($m)
    {
James Walker's avatar
James Walker committed
56
        // Discovery actions
Shashi Gowda's avatar
Shashi Gowda committed
57 58 59 60
        $m->connect('main/ostatustag',
                    array('action' => 'ostatustag'));
        $m->connect('main/ostatustag?nickname=:nickname',
                    array('action' => 'ostatustag'), array('nickname' => '[A-Za-z0-9_-]+'));
61
        $m->connect('main/ostatus/nickname/:nickname',
62
                  array('action' => 'ostatusinit'), array('nickname' => '[A-Za-z0-9_-]+'));
63
        $m->connect('main/ostatus/group/:group',
64
                  array('action' => 'ostatusinit'), array('group' => '[A-Za-z0-9_-]+'));
65
        $m->connect('main/ostatus/peopletag/:peopletag/tagger/:tagger',
Shashi Gowda's avatar
Shashi Gowda committed
66 67
                  array('action' => 'ostatusinit'), array('tagger' => '[A-Za-z0-9_-]+',
                                                          'peopletag' => '[A-Za-z0-9_-]+'));
68 69
        $m->connect('main/ostatus',
                    array('action' => 'ostatusinit'));
Shashi Gowda's avatar
Shashi Gowda committed
70 71

        // Remote subscription actions
72
        $m->connect('main/ostatussub',
73
                    array('action' => 'ostatussub'));
74 75
        $m->connect('main/ostatusgroup',
                    array('action' => 'ostatusgroup'));
Shashi Gowda's avatar
Shashi Gowda committed
76 77
        $m->connect('main/ostatuspeopletag',
                    array('action' => 'ostatuspeopletag'));
James Walker's avatar
James Walker committed
78 79

        // PuSH actions
Brion Vibber's avatar
Brion Vibber committed
80
        $m->connect('main/push/hub', array('action' => 'pushhub'));
81

Brion Vibber's avatar
Brion Vibber committed
82 83
        $m->connect('main/push/callback/:feed',
                    array('action' => 'pushcallback'),
84
                    array('feed' => '[0-9]+'));
James Walker's avatar
James Walker committed
85 86

        // Salmon endpoint
87
        $m->connect('main/salmon/user/:id',
88
                    array('action' => 'usersalmon'),
James Walker's avatar
James Walker committed
89
                    array('id' => '[0-9]+'));
90
        $m->connect('main/salmon/group/:id',
91
                    array('action' => 'groupsalmon'),
92
                    array('id' => '[0-9]+'));
Shashi Gowda's avatar
Shashi Gowda committed
93 94 95
        $m->connect('main/salmon/peopletag/:id',
                    array('action' => 'peopletagsalmon'),
                    array('id' => '[0-9]+'));
96 97 98
        return true;
    }

99 100 101 102 103 104 105
    /**
     * Set up queue handlers for outgoing hub pushes
     * @param QueueManager $qm
     * @return boolean hook return
     */
    function onEndInitializeQueueManager(QueueManager $qm)
    {
106 107 108
        // Prepare outgoing distributions after notice save.
        $qm->connect('ostatus', 'OStatusQueueHandler');

Brion Vibber's avatar
Brion Vibber committed
109
        // Outgoing from our internal PuSH hub
110
        $qm->connect('hubconf', 'HubConfQueueHandler');
111 112
        $qm->connect('hubprep', 'HubPrepQueueHandler');

113
        $qm->connect('hubout', 'HubOutQueueHandler');
Brion Vibber's avatar
Brion Vibber committed
114

115
        // Outgoing Salmon replies (when we don't need a return value)
116
        $qm->connect('salmon', 'SalmonQueueHandler');
117

Brion Vibber's avatar
Brion Vibber committed
118
        // Incoming from a foreign PuSH hub
119
        $qm->connect('pushin', 'PushInQueueHandler');
120 121 122 123 124 125 126 127
        return true;
    }

    /**
     * Put saved notices into the queue for pubsub distribution.
     */
    function onStartEnqueueNotice($notice, &$transports)
    {
128 129 130 131
        if ($notice->inScope(null)) {
            // put our transport first, in case there's any conflict (like OMB)
            array_unshift($transports, 'ostatus');
            $this->log(LOG_INFO, "Notice {$notice->id} queued for OStatus processing");
132
        } else {
133 134 135
            // FIXME: we don't do privacy-controlled OStatus updates yet.
            // once that happens, finer grain of control here.
            $this->log(LOG_NOTICE, "Not queueing notice {$notice->id} for OStatus because of privacy; scope = {$notice->scope}");
136
        }
137 138 139 140 141
        return true;
    }

    /**
     * Set up a PuSH hub link to our internal link for canonical timeline
142
     * Atom feeds for users and groups.
143
     */
Brion Vibber's avatar
Brion Vibber committed
144
    function onStartApiAtom($feed)
145
    {
146 147 148
        $id = null;

        if ($feed instanceof AtomUserNoticeFeed) {
149 150 151 152
            $salmonAction = 'usersalmon';
            $user = $feed->getUser();
            $id   = $user->id;
            $profile = $user->getProfile();
153
        } else if ($feed instanceof AtomGroupNoticeFeed) {
154 155 156
            $salmonAction = 'groupsalmon';
            $group = $feed->getGroup();
            $id = $group->id;
Shashi Gowda's avatar
Shashi Gowda committed
157 158 159 160
        } else if ($feed instanceof AtomListNoticeFeed) {
            $salmonAction = 'peopletagsalmon';
            $peopletag = $feed->getList();
            $id = $peopletag->id;
161
        } else {
162
            return true;
163
        }
Brion Vibber's avatar
Brion Vibber committed
164

165
        if (!empty($id)) {
166 167 168 169
            $hub = common_config('ostatus', 'hub');
            if (empty($hub)) {
                // Updates will be handled through our internal PuSH hub.
                $hub = common_local_url('pushhub');
170
            }
171
            $feed->addLink($hub, array('rel' => 'hub'));
172 173 174

            // Also, we'll add in the salmon link
            $salmon = common_local_url($salmonAction, array('id' => $id));
175 176 177
            $feed->addLink($salmon, array('rel' => Salmon::REL_SALMON));

            // XXX: these are deprecated
178 179
            $feed->addLink($salmon, array('rel' => Salmon::NS_REPLIES));
            $feed->addLink($salmon, array('rel' => Salmon::NS_MENTIONS));
180
        }
181 182

        return true;
183
    }
184

185 186 187
    /**
     * Add in an OStatus subscribe button
     */
188
    function onStartProfileRemoteSubscribe($output, $profile)
Shashi Gowda's avatar
Shashi Gowda committed
189 190 191 192 193
    {
        $this->onStartProfileListItemActionElements($output, $profile);
        return false;
    }

194
    function onStartGroupSubscribe($widget, $group)
195 196 197 198
    {
        $cur = common_current_user();

        if (empty($cur)) {
199
            $widget->out->elementStart('li', 'entity_subscribe');
200

201
            $url = common_local_url('ostatusinit',
Shashi Gowda's avatar
Shashi Gowda committed
202
                                    array('group' => $group->nickname));
203
            $widget->out->element('a', array('href' => $url,
204
                                             'class' => 'entity_remote_subscribe'),
205
                                // TRANS: Link to subscribe to a remote entity.
206
                                _m('Subscribe'));
207

208
            $widget->out->elementEnd('li');
209
            return false;
210
        }
211

Shashi Gowda's avatar
Shashi Gowda committed
212
        return true;
213 214
    }

Shashi Gowda's avatar
Shashi Gowda committed
215
    function onStartSubscribePeopletagForm($output, $peopletag)
216 217 218 219
    {
        $cur = common_current_user();

        if (empty($cur)) {
Shashi Gowda's avatar
Shashi Gowda committed
220 221
            $output->elementStart('li', 'entity_subscribe');
            $profile = $peopletag->getTagger();
222
            $url = common_local_url('ostatusinit',
Shashi Gowda's avatar
Shashi Gowda committed
223
                                    array('tagger' => $profile->nickname, 'peopletag' => $peopletag->tag));
224 225
            $output->element('a', array('href' => $url,
                                        'class' => 'entity_remote_subscribe'),
226
                                // TRANS: Link to subscribe to a remote entity.
Shashi Gowda's avatar
Shashi Gowda committed
227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244
                                _m('Subscribe'));

            $output->elementEnd('li');
            return false;
        }

        return true;
    }

    function onStartShowTagProfileForm($action, $profile)
    {
        $action->elementStart('form', array('method' => 'post',
                                           'id' => 'form_tag_user',
                                           'class' => 'form_settings',
                                           'name' => 'tagprofile',
                                           'action' => common_local_url('tagprofile', array('id' => @$profile->id))));

        $action->elementStart('fieldset');
245
        // TRANS: Fieldset legend.
Siebrand Mazeland's avatar
Siebrand Mazeland committed
246
        $action->element('legend', null, _m('List remote profile'));
Shashi Gowda's avatar
Shashi Gowda committed
247 248 249 250 251 252 253
        $action->hidden('token', common_session_token());

        $user = common_current_user();

        $action->elementStart('ul', 'form_data');
        $action->elementStart('li');

254 255 256 257
        // TRANS: Field label.
        $action->input('uri', _m('LABEL','Remote profile'), $action->trimmed('uri'),
                     // TRANS: Field title.
                     _m('OStatus user\'s address, like nickname@example.com or http://example.net/nickname.'));
Shashi Gowda's avatar
Shashi Gowda committed
258 259
        $action->elementEnd('li');
        $action->elementEnd('ul');
260 261
        // TRANS: Button text to fetch remote profile.
        $action->submit('fetch', _m('BUTTON','Fetch'));
Shashi Gowda's avatar
Shashi Gowda committed
262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277
        $action->elementEnd('fieldset');
        $action->elementEnd('form');
    }

    function onStartTagProfileAction($action, $profile)
    {
        $err = null;
        $uri = $action->trimmed('uri');

        if (!$profile && $uri) {
            try {
                if (Validate::email($uri)) {
                    $oprofile = Ostatus_profile::ensureWebfinger($uri);
                } else if (Validate::uri($uri)) {
                    $oprofile = Ostatus_profile::ensureProfileURL($uri);
                } else {
278 279
                    // TRANS: Exception in OStatus when invalid URI was entered.
                    throw new Exception(_m('Invalid URI.'));
Shashi Gowda's avatar
Shashi Gowda committed
280 281 282 283 284 285 286
                }

                // redirect to the new profile.
                common_redirect(common_local_url('tagprofile', array('id' => $oprofile->profile_id)), 303);
                return false;

            } catch (Exception $e) {
287 288
                // TRANS: Error message in OStatus plugin. Do not translate the domain names example.com
                // TRANS: and example.net, as these are official standard domain names for use in examples.
289
                $err = _m("Sorry, we could not reach that address. Please make sure that the OStatus address is like nickname@example.com or http://example.net/nickname.");
Shashi Gowda's avatar
Shashi Gowda committed
290 291 292 293
            }

            $action->showForm($err);
            return false;
294
        }
Shashi Gowda's avatar
Shashi Gowda committed
295 296
        return true;
    }
297

Shashi Gowda's avatar
Shashi Gowda committed
298 299 300 301 302
    /*
     * If the field being looked for is URI look for the profile
     */
    function onStartProfileCompletionSearch($action, $profile, $search_engine) {
        if ($action->field == 'uri') {
303
            $profile->joinAdd(array('id', 'user:id'));
Shashi Gowda's avatar
Shashi Gowda committed
304 305 306 307 308 309 310 311 312 313
            $profile->whereAdd('uri LIKE "%' . $profile->escape($q) . '%"');
            $profile->query();

            if ($profile->N == 0) {
                try {
                    if (Validate::email($q)) {
                        $oprofile = Ostatus_profile::ensureWebfinger($q);
                    } else if (Validate::uri($q)) {
                        $oprofile = Ostatus_profile::ensureProfileURL($q);
                    } else {
314 315
                        // TRANS: Exception in OStatus when invalid URI was entered.
                        throw new Exception(_m('Invalid URI.'));
Shashi Gowda's avatar
Shashi Gowda committed
316 317 318 319
                    }
                    return $this->filter(array($oprofile->localProfile()));

                } catch (Exception $e) {
320 321
                // TRANS: Error message in OStatus plugin. Do not translate the domain names example.com
                // TRANS: and example.net, as these are official standard domain names for use in examples.
322
                    $this->msg = _m("Sorry, we could not reach that address. Please make sure that the OStatus address is like nickname@example.com or http://example.net/nickname.");
Shashi Gowda's avatar
Shashi Gowda committed
323 324 325 326 327
                    return array();
                }
            }
            return false;
        }
328
        return true;
329 330
    }

331
    /**
332 333 334 335 336 337 338
     * Find any explicit remote mentions. Accepted forms:
     *   Webfinger: @user@example.com
     *   Profile link: @example.com/mublog/user
     * @param Profile $sender (os user?)
     * @param string $text input markup text
     * @param array &$mention in/out param: set of found mentions
     * @return boolean hook return value
339
     */
340
    function onEndFindMentions(Profile $sender, $text, &$mentions)
341
    {
342 343 344
        $matches = array();

        // Webfinger matches: @user@example.com
345
        if (preg_match_all('!(?:^|\s+)@((?:\w+\.)*\w+@(?:\w+\-?\w+\.)*\w+(?:\w+\-\w+)*\.\w+)!',
346 347
                       $text,
                       $wmatches,
348 349 350 351
                       PREG_OFFSET_CAPTURE)) {
            foreach ($wmatches[1] as $wmatch) {
                list($target, $pos) = $wmatch;
                $this->log(LOG_INFO, "Checking webfinger '$target'");
352 353
                try {
                    $oprofile = Ostatus_profile::ensureWebfinger($target);
354 355 356 357 358 359 360
                    if ($oprofile && !$oprofile->isGroup()) {
                        $profile = $oprofile->localProfile();
                        $matches[$pos] = array('mentioned' => array($profile),
                                               'text' => $target,
                                               'position' => $pos,
                                               'url' => $profile->profileurl);
                    }
361 362 363
                } catch (Exception $e) {
                    $this->log(LOG_ERR, "Webfinger check failed: " . $e->getMessage());
                }
364 365 366 367 368 369 370 371 372 373 374
            }
        }

        // Profile matches: @example.com/mublog/user
        if (preg_match_all('!(?:^|\s+)@((?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+(?:/\w+)+)!',
                       $text,
                       $wmatches,
                       PREG_OFFSET_CAPTURE)) {
            foreach ($wmatches[1] as $wmatch) {
                list($target, $pos) = $wmatch;
                $schemes = array('http', 'https');
375 376 377 378
                foreach ($schemes as $scheme) {
                    $url = "$scheme://$target";
                    $this->log(LOG_INFO, "Checking profile address '$url'");
                    try {
379
                        $oprofile = Ostatus_profile::ensureProfileURL($url);
380 381 382 383 384 385 386
                        if ($oprofile && !$oprofile->isGroup()) {
                            $profile = $oprofile->localProfile();
                            $matches[$pos] = array('mentioned' => array($profile),
                                                   'text' => $target,
                                                   'position' => $pos,
                                                   'url' => $profile->profileurl);
                            break;
387 388 389 390 391 392
                        }
                    } catch (Exception $e) {
                        $this->log(LOG_ERR, "Profile check failed: " . $e->getMessage());
                    }
                }
            }
393
        }
394

395 396 397 398 399 400
        foreach ($mentions as $i => $other) {
            // If we share a common prefix with a local user, override it!
            $pos = $other['position'];
            if (isset($matches[$pos])) {
                $mentions[$i] = $matches[$pos];
                unset($matches[$pos]);
James Walker's avatar
James Walker committed
401 402
            }
        }
403 404 405
        foreach ($matches as $mention) {
            $mentions[] = $mention;
        }
406

407 408 409
        return true;
    }

410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480
    /**
     * Allow remote profile references to be used in commands:
     *   sub update@status.net
     *   whois evan@identi.ca
     *   reply http://identi.ca/evan hey what's up
     *
     * @param Command $command
     * @param string $arg
     * @param Profile &$profile
     * @return hook return code
     */
    function onStartCommandGetProfile($command, $arg, &$profile)
    {
        $oprofile = $this->pullRemoteProfile($arg);
        if ($oprofile && !$oprofile->isGroup()) {
            $profile = $oprofile->localProfile();
            return false;
        } else {
            return true;
        }
    }

    /**
     * Allow remote group references to be used in commands:
     *   join group+statusnet@identi.ca
     *   join http://identi.ca/group/statusnet
     *   drop identi.ca/group/statusnet
     *
     * @param Command $command
     * @param string $arg
     * @param User_group &$group
     * @return hook return code
     */
    function onStartCommandGetGroup($command, $arg, &$group)
    {
        $oprofile = $this->pullRemoteProfile($arg);
        if ($oprofile && $oprofile->isGroup()) {
            $group = $oprofile->localGroup();
            return false;
        } else {
            return true;
        }
    }

    protected function pullRemoteProfile($arg)
    {
        $oprofile = null;
        if (preg_match('!^((?:\w+\.)*\w+@(?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+)$!', $arg)) {
            // webfinger lookup
            try {
                return Ostatus_profile::ensureWebfinger($arg);
            } catch (Exception $e) {
                common_log(LOG_ERR, 'Webfinger lookup failed for ' .
                                    $arg . ': ' . $e->getMessage());
            }
        }

        // Look for profile URLs, with or without scheme:
        $urls = array();
        if (preg_match('!^https?://((?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+(?:/\w+)+)$!', $arg)) {
            $urls[] = $arg;
        }
        if (preg_match('!^((?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+(?:/\w+)+)$!', $arg)) {
            $schemes = array('http', 'https');
            foreach ($schemes as $scheme) {
                $urls[] = "$scheme://$arg";
            }
        }

        foreach ($urls as $url) {
            try {
481
                return Ostatus_profile::ensureProfileURL($url);
482 483 484 485 486 487 488 489
            } catch (Exception $e) {
                common_log(LOG_ERR, 'Profile lookup failed for ' .
                                    $arg . ': ' . $e->getMessage());
            }
        }
        return null;
    }

490 491 492
    /**
     * Make sure necessary tables are filled out.
     */
493 494
    function onCheckSchema() {
        $schema = Schema::get();
495
        $schema->ensureTable('ostatus_profile', Ostatus_profile::schemaDef());
496
        $schema->ensureTable('ostatus_source', Ostatus_source::schemaDef());
497
        $schema->ensureTable('feedsub', FeedSub::schemaDef());
498
        $schema->ensureTable('hubsub', HubSub::schemaDef());
499
        $schema->ensureTable('magicsig', Magicsig::schemaDef());
500
        return true;
501
    }
502

503
    public function onEndShowStylesheets(Action $action) {
Evan Prodromou's avatar
Evan Prodromou committed
504
        $action->cssLink($this->path('theme/base/css/ostatus.css'));
505 506
        return true;
    }
507 508

    function onEndShowStatusNetScripts($action) {
Evan Prodromou's avatar
Evan Prodromou committed
509
        $action->script($this->path('js/ostatus.js'));
510 511
        return true;
    }
512

Brion Vibber's avatar
Brion Vibber committed
513 514 515 516 517 518 519 520 521 522
    /**
     * Override the "from ostatus" bit in notice lists to link to the
     * original post and show the domain it came from.
     *
     * @param Notice in $notice
     * @param string out &$name
     * @param string out &$url
     * @param string out &$title
     * @return mixed hook return code
     */
523 524 525
    function onStartNoticeSourceLink($notice, &$name, &$url, &$title)
    {
        if ($notice->source == 'ostatus') {
526 527 528 529 530 531 532 533
            if ($notice->url) {
                $bits = parse_url($notice->url);
                $domain = $bits['host'];
                if (substr($domain, 0, 4) == 'www.') {
                    $name = substr($domain, 4);
                } else {
                    $name = $domain;
                }
534

535
                $url = $notice->url;
Siebrand Mazeland's avatar
Siebrand Mazeland committed
536
                // TRANS: Title. %s is a domain name.
537
                $title = sprintf(_m('Sent from %s via OStatus'), $domain);
538 539
                return false;
            }
540
        }
541
    return true;
542
    }
543 544 545 546 547 548 549 550 551 552

    /**
     * Send incoming PuSH feeds for OStatus endpoints in for processing.
     *
     * @param FeedSub $feedsub
     * @param DOMDocument $feed
     * @return mixed hook return code
     */
    function onStartFeedSubReceive($feedsub, $feed)
    {
553
        $oprofile = Ostatus_profile::getKV('feeduri', $feedsub->uri);
554
        if ($oprofile) {
555
            $oprofile->processFeed($feed, 'push');
556 557
        } else {
            common_log(LOG_DEBUG, "No ostatus profile for incoming feed $feedsub->uri");
558 559
        }
    }
560

561 562 563 564
    /**
     * Tell the FeedSub infrastructure whether we have any active OStatus
     * usage for the feed; if not it'll be able to garbage-collect the
     * feed subscription.
565
     *
566 567 568 569 570 571
     * @param FeedSub $feedsub
     * @param integer $count in/out
     * @return mixed hook return code
     */
    function onFeedSubSubscriberCount($feedsub, &$count)
    {
572
        $oprofile = Ostatus_profile::getKV('feeduri', $feedsub->uri);
573 574 575 576 577 578
        if ($oprofile) {
            $count += $oprofile->subscriberCount();
        }
        return true;
    }

579 580 581 582 583 584 585
    /**
     * When about to subscribe to a remote user, start a server-to-server
     * PuSH subscription if needed. If we can't establish that, abort.
     *
     * @fixme If something else aborts later, we could end up with a stray
     *        PuSH subscription. This is relatively harmless, though.
     *
586 587
     * @param Profile $profile  subscriber
     * @param Profile $other    subscribee
588 589 590 591 592
     *
     * @return hook return code
     *
     * @throws Exception
     */
593
    function onStartSubscribe(Profile $profile, Profile $other)
594
    {
595
        if (!$profile->isLocal()) {
596 597 598
            return true;
        }

599
        $oprofile = Ostatus_profile::getKV('profile_id', $other->id);
600 601 602 603 604 605

        if (empty($oprofile)) {
            return true;
        }

        if (!$oprofile->subscribe()) {
606
            // TRANS: Exception thrown when setup of remote subscription fails.
607 608 609 610 611 612 613 614
            throw new Exception(_m('Could not set up remote subscription.'));
        }
    }

    /**
     * Having established a remote subscription, send a notification to the
     * remote OStatus profile's endpoint.
     *
615 616
     * @param Profile $profile  subscriber
     * @param Profile $other    subscribee
617 618 619 620 621
     *
     * @return hook return code
     *
     * @throws Exception
     */
622
    function onEndSubscribe(Profile $profile, Profile $other)
623
    {
624
        if (!$profile->isLocal()) {
625 626 627
            return true;
        }

628
        $oprofile = Ostatus_profile::getKV('profile_id', $other->id);
629 630 631 632 633

        if (empty($oprofile)) {
            return true;
        }

634
        $sub = Subscription::pkeyGet(array('subscriber' => $profile->id,
635
                                           'subscribed' => $other->id));
636

637
        $act = $sub->asActivity();
638

639
        $oprofile->notifyActivity($act, $profile);
640 641 642 643

        return true;
    }

644 645
    /**
     * Notify remote server and garbage collect unused feeds on unsubscribe.
646
     * @todo FIXME: Send these operations to background queues
647 648 649 650 651
     *
     * @param User $user
     * @param Profile $other
     * @return hook return value
     */
652
    function onEndUnsubscribe(Profile $profile, Profile $other)
653
    {
654
        if (!$profile->isLocal()) {
655 656 657
            return true;
        }

658
        $oprofile = Ostatus_profile::getKV('profile_id', $other->id);
659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676

        if (empty($oprofile)) {
            return true;
        }

        // Drop the PuSH subscription if there are no other subscribers.
        $oprofile->garbageCollect();

        $act = new Activity();

        $act->verb = ActivityVerb::UNFOLLOW;

        $act->id   = TagURI::mint('unfollow:%d:%d:%s',
                                  $profile->id,
                                  $other->id,
                                  common_date_iso8601(time()));

        $act->time    = time();
677 678
        // TRANS: Title for unfollowing a remote profile.
        $act->title   = _m('TITLE','Unfollow');
679 680
        // TRANS: Success message for unsubscribe from user attempt through OStatus.
        // TRANS: %1$s is the unsubscriber's name, %2$s is the unsubscribed user's name.
Siebrand Mazeland's avatar
Siebrand Mazeland committed
681
        $act->content = sprintf(_m('%1$s stopped following %2$s.'),
682 683 684 685 686 687
                               $profile->getBestName(),
                               $other->getBestName());

        $act->actor   = ActivityObject::fromProfile($profile);
        $act->object  = ActivityObject::fromProfile($other);

688
        $oprofile->notifyActivity($act, $profile);
689 690 691 692

        return true;
    }

693 694 695 696 697 698
    /**
     * When one of our local users tries to join a remote group,
     * notify the remote server. If the notification is rejected,
     * deny the join.
     *
     * @param User_group $group
Evan Prodromou's avatar
Evan Prodromou committed
699
     * @param Profile    $profile
700 701 702
     *
     * @return mixed hook return value
     */
Evan Prodromou's avatar
Evan Prodromou committed
703
    function onStartJoinGroup($group, $profile)
704
    {
705
        $oprofile = Ostatus_profile::getKV('group_id', $group->id);
706
        if ($oprofile) {
707
            if (!$oprofile->subscribe()) {
708
                // TRANS: Exception thrown when setup of remote group membership fails.
709 710 711
                throw new Exception(_m('Could not set up remote group membership.'));
            }

712 713 714
            // NOTE: we don't use Group_member::asActivity() since that record
            // has not yet been created.

715 716
            $act = new Activity();
            $act->id = TagURI::mint('join:%d:%d:%s',
Evan Prodromou's avatar
Evan Prodromou committed
717
                                    $profile->id,
718 719 720
                                    $group->id,
                                    common_date_iso8601(time()));

Evan Prodromou's avatar
Evan Prodromou committed
721
            $act->actor = ActivityObject::fromProfile($profile);
722 723 724 725
            $act->verb = ActivityVerb::JOIN;
            $act->object = $oprofile->asActivityObject();

            $act->time = time();
Siebrand Mazeland's avatar
Siebrand Mazeland committed
726
            // TRANS: Title for joining a remote groep.
727
            $act->title = _m('TITLE','Join');
728 729
            // TRANS: Success message for subscribe to group attempt through OStatus.
            // TRANS: %1$s is the member name, %2$s is the subscribed group's name.
730
            $act->content = sprintf(_m('%1$s has joined group %2$s.'),
Evan Prodromou's avatar
Evan Prodromou committed
731
                                    $profile->getBestName(),
732 733
                                    $oprofile->getBestName());

Evan Prodromou's avatar
Evan Prodromou committed
734
            if ($oprofile->notifyActivity($act, $profile)) {
735 736
                return true;
            } else {
737
                $oprofile->garbageCollect();
738 739
                // TRANS: Exception thrown when joining a remote group fails.
                throw new Exception(_m('Failed joining remote group.'));
740 741 742 743 744 745 746 747 748 749 750 751 752 753
            }
        }
    }

    /**
     * When one of our local users leaves a remote group, notify the remote
     * server.
     *
     * @fixme Might be good to schedule a resend of the leave notification
     * if it failed due to a transitory error. We've canceled the local
     * membership already anyway, but if the remote server comes back up
     * it'll be left with a stray membership record.
     *
     * @param User_group $group
Evan Prodromou's avatar
Evan Prodromou committed
754
     * @param Profile $profile
755 756 757
     *
     * @return mixed hook return value
     */
758
    function onEndLeaveGroup($group, $profile)
759
    {
760
        $oprofile = Ostatus_profile::getKV('group_id', $group->id);
761 762
        if ($oprofile) {
            // Drop the PuSH subscription if there are no other subscribers.
763
            $oprofile->garbageCollect();
764

765
            $member = $profile;
766 767 768 769 770 771 772 773 774 775 776 777

            $act = new Activity();
            $act->id = TagURI::mint('leave:%d:%d:%s',
                                    $member->id,
                                    $group->id,
                                    common_date_iso8601(time()));

            $act->actor = ActivityObject::fromProfile($member);
            $act->verb = ActivityVerb::LEAVE;
            $act->object = $oprofile->asActivityObject();

            $act->time = time();
778 779
            // TRANS: Title for leaving a remote group.
            $act->title = _m('TITLE','Leave');
780 781
            // TRANS: Success message for unsubscribe from group attempt through OStatus.
            // TRANS: %1$s is the member name, %2$s is the unsubscribed group's name.
782
            $act->content = sprintf(_m('%1$s has left group %2$s.'),
783 784 785
                                    $member->getBestName(),
                                    $oprofile->getBestName());

786
            $oprofile->notifyActivity($act, $member);
787 788 789
        }
    }

Shashi Gowda's avatar
Shashi Gowda committed
790 791 792 793 794 795 796 797 798 799 800 801 802
    /**
     * When one of our local users tries to subscribe to a remote peopletag,
     * notify the remote server. If the notification is rejected,
     * deny the subscription.
     *
     * @param Profile_list $peopletag
     * @param User         $user
     *
     * @return mixed hook return value
     */

    function onStartSubscribePeopletag($peopletag, $user)
    {
803
        $oprofile = Ostatus_profile::getKV('peopletag_id', $peopletag->id);
Shashi Gowda's avatar
Shashi Gowda committed
804 805
        if ($oprofile) {
            if (!$oprofile->subscribe()) {
806
                // TRANS: Exception thrown when setup of remote list subscription fails.
807
                throw new Exception(_m('Could not set up remote list subscription.'));
Shashi Gowda's avatar
Shashi Gowda committed
808 809 810
            }

            $sub = $user->getProfile();
811
            $tagger = Profile::getKV($peopletag->tagger);
Shashi Gowda's avatar
Shashi Gowda committed
812 813 814 815 816 817 818 819 820 821 822 823

            $act = new Activity();
            $act->id = TagURI::mint('subscribe_peopletag:%d:%d:%s',
                                    $sub->id,
                                    $peopletag->id,
                                    common_date_iso8601(time()));

            $act->actor = ActivityObject::fromProfile($sub);
            $act->verb = ActivityVerb::FOLLOW;
            $act->object = $oprofile->asActivityObject();

            $act->time = time();
Siebrand Mazeland's avatar
Siebrand Mazeland committed
824
            // TRANS: Title for following a remote list.
825 826
            $act->title = _m('TITLE','Follow list');
            // TRANS: Success message for remote list follow through OStatus.
Siebrand Mazeland's avatar
Siebrand Mazeland committed
827
            // TRANS: %1$s is the subscriber name, %2$s is the list, %3$s is the lister's name.
Siebrand Mazeland's avatar
Siebrand Mazeland committed
828
            $act->content = sprintf(_m('%1$s is now following people listed in %2$s by %3$s.'),
Shashi Gowda's avatar
Shashi Gowda committed
829 830 831 832 833 834 835 836
                                    $sub->getBestName(),
                                    $oprofile->getBestName(),
                                    $tagger->getBestName());

            if ($oprofile->notifyActivity($act, $sub)) {
                return true;
            } else {
                $oprofile->garbageCollect();
837 838
                // TRANS: Exception thrown when subscription to remote list fails.
                throw new Exception(_m('Failed subscribing to remote list.'));
Shashi Gowda's avatar
Shashi Gowda committed
839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854
            }
        }
    }

    /**
     * When one of our local users unsubscribes to a remote peopletag, notify the remote
     * server.
     *
     * @param Profile_list $peopletag
     * @param User         $user
     *
     * @return mixed hook return value
     */

    function onEndUnsubscribePeopletag($peopletag, $user)
    {
855
        $oprofile = Ostatus_profile::getKV('peopletag_id', $peopletag->id);
Shashi Gowda's avatar
Shashi Gowda committed
856 857 858 859
        if ($oprofile) {
            // Drop the PuSH subscription if there are no other subscribers.
            $oprofile->garbageCollect();

860 861
            $sub = Profile::getKV($user->id);
            $tagger = Profile::getKV($peopletag->tagger);
Shashi Gowda's avatar
Shashi Gowda committed
862 863 864 865 866 867 868 869 870 871 872 873

            $act = new Activity();
            $act->id = TagURI::mint('unsubscribe_peopletag:%d:%d:%s',
                                    $sub->id,
                                    $peopletag->id,
                                    common_date_iso8601(time()));

            $act->actor = ActivityObject::fromProfile($member);
            $act->verb = ActivityVerb::UNFOLLOW;
            $act->object = $oprofile->asActivityObject();

            $act->time = time();
Siebrand Mazeland's avatar
Siebrand Mazeland committed
874
            // TRANS: Title for unfollowing a remote list.
875
            $act->title = _m('Unfollow list');
Siebrand Mazeland's avatar
Siebrand Mazeland committed
876
            // TRANS: Success message for remote list unfollow through OStatus.
Siebrand Mazeland's avatar
Siebrand Mazeland committed
877
            // TRANS: %1$s is the subscriber name, %2$s is the list, %3$s is the lister's name.
878
            $act->content = sprintf(_m('%1$s stopped following the list %2$s by %3$s.'),
Shashi Gowda's avatar
Shashi Gowda committed
879 880 881 882 883 884 885 886
                                    $sub->getBestName(),
                                    $oprofile->getBestName(),
                                    $tagger->getBestName());

            $oprofile->notifyActivity($act, $user);
        }
    }

887 888 889 890 891 892 893
    /**
     * Notify remote users when their notices get favorited.
     *
     * @param Profile or User $profile of local user doing the faving
     * @param Notice $notice being favored
     * @return hook return value
     */
894
    function onEndFavorNotice(Profile $profile, Notice $notice)
895
    {
896
        $user = User::getKV('id', $profile->id);
897 898 899

        if (empty($user)) {
            return true;
900
        }
901

902
        $oprofile = Ostatus_profile::getKV('profile_id', $notice->profile_id);
903

904 905
        if (empty($oprofile)) {
            return true;
906
        }
907

908 909
        $fav = Fave::pkeyGet(array('user_id' => $user->id,
                                   'notice_id' => $notice->id));
910

911 912 913 914
        if (empty($fav)) {
            // That's weird.
            return true;
        }
915

916
        $act = $fav->asActivity();
917

918
        $oprofile->notifyActivity($act, $profile);
919

920
        return true;
921 922
    }

923 924 925 926 927 928 929 930
    /**
     * Notify remote user it has got a new people tag
     *   - tag verb is queued
     *   - the subscription is done immediately if not present
     *
     * @param Profile_tag $ptag the people tag that was created
     * @return hook return value
     */
Shashi Gowda's avatar
Shashi Gowda committed
931 932
    function onEndTagProfile($ptag)
    {
933
        $oprofile = Ostatus_profile::getKV('profile_id', $ptag->tagged);
Shashi Gowda's avatar
Shashi Gowda committed
934 935 936 937 938 939 940 941 942 943 944 945 946

        if (empty($oprofile)) {
            return true;
        }

        $plist = $ptag->getMeta();
        if ($plist->private) {
            return true;
        }

        $act = new Activity();

        $tagger = $plist->getTagger();
947
        $tagged = Profile::getKV('id', $ptag->tagged);
Shashi Gowda's avatar
Shashi Gowda committed
948 949 950 951 952 953

        $act->verb = ActivityVerb::TAG;
        $act->id   = TagURI::mint('tag_profile:%d:%d:%s',
                                  $plist->tagger, $plist->id,
                                  common_date_iso8601(time()));
        $act->time = time();
Siebrand Mazeland's avatar
Siebrand Mazeland committed
954 955 956 957
        // TRANS: Title for listing a remote profile.
        $act->title = _m('TITLE','List');
        // TRANS: Success message for remote list addition through OStatus.
        // TRANS: %1$s is the list creator's name, %2$s is the added list member, %3$s is the list name.
958
        $act->content = sprintf(_m('%1$s listed %2$s in the list %3$s.'),
Shashi Gowda's avatar
Shashi Gowda committed
959 960 961 962 963 964 965 966
                                $tagger->getBestName(),
                                $tagged->getBestName(),
                                $plist->getBestName());

        $act->actor  = ActivityObject::fromProfile($tagger);
        $act->objects = array(ActivityObject::fromProfile($tagged));
        $act->target = ActivityObject::fromPeopletag($plist);

967
        $oprofile->notifyDeferred($act, $tagger);
Shashi Gowda's avatar
Shashi Gowda committed
968 969 970

        // initiate a PuSH subscription for the person being tagged
        if (!$oprofile->subscribe()) {
Siebrand Mazeland's avatar
Siebrand Mazeland committed
971
            // TRANS: Exception thrown when subscribing to a remote list fails.
972
            throw new Exception(sprintf(_m('Could not complete subscription to remote '.
Siebrand Mazeland's avatar
Siebrand Mazeland committed
973
                                          'profile\'s feed. List %s could not be saved.'), $ptag->tag));
Shashi Gowda's avatar
Shashi Gowda committed
974 975 976 977 978
            return false;
        }
        return true;
    }

979 980 981 982 983 984 985 986 987
    /**
     * Notify remote user that a people tag has been removed
     *   - untag verb is queued
     *   - the subscription is undone immediately if not required
     *     i.e garbageCollect()'d
     *
     * @param Profile_tag $ptag the people tag that was deleted
     * @return hook return value
     */
Shashi Gowda's avatar
Shashi Gowda committed
988 989
    function onEndUntagProfile($ptag)
    {
990
        $oprofile = Ostatus_profile::getKV('profile_id', $ptag->tagged);
Shashi Gowda's avatar
Shashi Gowda committed
991 992 993 994 995 996 997 998 999 1000 1001 1002 1003

        if (empty($oprofile)) {
            return true;
        }

        $plist = $ptag->getMeta();
        if ($plist->private) {
            return true;
        }

        $act = new Activity();

        $tagger = $plist->getTagger();
1004
        $tagged = Profile::getKV('id', $ptag->tagged);
Shashi Gowda's avatar
Shashi Gowda committed
1005 1006 1007 1008 1009 1010

        $act->verb = ActivityVerb::UNTAG;
        $act->id   = TagURI::mint('untag_profile:%d:%d:%s',
                                  $plist->tagger, $plist->id,
                                  common_date_iso8601(time()));
        $act->time = time();
Siebrand Mazeland's avatar
Siebrand Mazeland committed
1011 1012 1013 1014 1015
        // TRANS: Title for unlisting a remote profile.
        $act->title = _m('TITLE','Unlist');
        // TRANS: Success message for remote list removal through OStatus.
        // TRANS: %1$s is the list creator's name, %2$s is the removed list member, %3$s is the list name.
        $act->content = sprintf(_m('%1$s removed %2$s from the list %3$s.'),
Shashi Gowda's avatar
Shashi Gowda committed
1016 1017 1018 1019 1020 1021 1022 1023
                                $tagger->getBestName(),
                                $tagged->getBestName(),
                                $plist->getBestName());

        $act->actor  = ActivityObject::fromProfile($tagger);
        $act->objects = array(ActivityObject::fromProfile($tagged));
        $act->target = ActivityObject::fromPeopletag($plist);

1024
        $oprofile->notifyDeferred($act, $tagger);
Shashi Gowda's avatar
Shashi Gowda committed
1025 1026 1027 1028 1029 1030 1031

        // unsubscribe to PuSH feed if no more required
        $oprofile->garbageCollect();

        return true;
    }

1032 1033 1034
    /**
     * Notify remote users when their notices get de-favorited.
     *
1035 1036 1037
     * @param Profile $profile Profile person doing the de-faving
     * @param Notice  $notice  Notice being favored
     *
1038 1039 1040
     * @return hook return value
     */
    function onEndDisfavorNotice(Profile $profile, Notice $notice)
1041
    {
1042
        $user = User::getKV('id', $profile->id);
1043 1044 1045 1046 1047

        if (empty($user)) {
            return true;
        }

1048
        $oprofile = Ostatus_profile::getKV('profile_id', $notice->profile_id);
1049

1050 1051
        if (empty($oprofile)) {
            return true;
1052
        }
1053

1054 1055 1056 1057 1058 1059 1060 1061
        $act = new Activity();

        $act->verb = ActivityVerb::UNFAVORITE;
        $act->id   = TagURI::mint('disfavor:%d:%d:%s',
                                  $profile->id,
                                  $notice->id,
                                  common_date_iso8601(time()));
        $act->time    = time();
Siebrand Mazeland's avatar
Siebrand Mazeland committed
1062 1063
        // TRANS: Title for unliking a remote notice.
        $act->title   = _m('Unlike');
1064 1065
        // TRANS: Success message for remove a favorite notice through OStatus.
        // TRANS: %1$s is the unfavoring user's name, %2$s is URI to the no longer favored notice.
Siebrand Mazeland's avatar
Siebrand Mazeland committed
1066
        $act->content = sprintf(_m('%1$s no longer likes %2$s.'),
1067 1068 1069 1070 1071 1072
                               $profile->getBestName(),
                               $notice->uri);