]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - plugins/OStatus/OStatusPlugin.php
Ostatus_source filled no purpose whatsoever
[quix0rs-gnu-social.git] / plugins / OStatus / OStatusPlugin.php
1 <?php
2 /*
3  * StatusNet - the distributed open-source microblogging tool
4  * Copyright (C) 2009-2010, 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
20 /**
21  * OStatusPlugin implementation for GNU Social
22  *
23  * Depends on: WebFinger plugin
24  *
25  * @package OStatusPlugin
26  * @maintainer Brion Vibber <brion@status.net>
27  */
28
29 if (!defined('GNUSOCIAL')) { exit(1); }
30
31 set_include_path(get_include_path() . PATH_SEPARATOR . dirname(__FILE__) . '/extlib/phpseclib');
32
33 class OStatusPlugin extends Plugin
34 {
35     /**
36      * Hook for RouterInitialized event.
37      *
38      * @param URLMapper $m path-to-action mapper
39      * @return boolean hook return
40      */
41     public function onRouterInitialized(URLMapper $m)
42     {
43         // Discovery actions
44         $m->connect('main/ostatustag',
45                     array('action' => 'ostatustag'));
46         $m->connect('main/ostatustag?nickname=:nickname',
47                     array('action' => 'ostatustag'), array('nickname' => '[A-Za-z0-9_-]+'));
48         $m->connect('main/ostatus/nickname/:nickname',
49                   array('action' => 'ostatusinit'), array('nickname' => '[A-Za-z0-9_-]+'));
50         $m->connect('main/ostatus/group/:group',
51                   array('action' => 'ostatusinit'), array('group' => '[A-Za-z0-9_-]+'));
52         $m->connect('main/ostatus/peopletag/:peopletag/tagger/:tagger',
53                   array('action' => 'ostatusinit'), array('tagger' => '[A-Za-z0-9_-]+',
54                                                           'peopletag' => '[A-Za-z0-9_-]+'));
55         $m->connect('main/ostatus',
56                     array('action' => 'ostatusinit'));
57
58         // Remote subscription actions
59         $m->connect('main/ostatussub',
60                     array('action' => 'ostatussub'));
61         $m->connect('main/ostatusgroup',
62                     array('action' => 'ostatusgroup'));
63         $m->connect('main/ostatuspeopletag',
64                     array('action' => 'ostatuspeopletag'));
65
66         // PuSH actions
67         $m->connect('main/push/hub', array('action' => 'pushhub'));
68
69         $m->connect('main/push/callback/:feed',
70                     array('action' => 'pushcallback'),
71                     array('feed' => '[0-9]+'));
72
73         // Salmon endpoint
74         $m->connect('main/salmon/user/:id',
75                     array('action' => 'usersalmon'),
76                     array('id' => '[0-9]+'));
77         $m->connect('main/salmon/group/:id',
78                     array('action' => 'groupsalmon'),
79                     array('id' => '[0-9]+'));
80         $m->connect('main/salmon/peopletag/:id',
81                     array('action' => 'peopletagsalmon'),
82                     array('id' => '[0-9]+'));
83         return true;
84     }
85
86     public function onAutoload($cls)
87     {
88         switch ($cls) {
89         case 'Crypt_AES':
90         case 'Crypt_RSA':
91             // Crypt_AES becomes Crypt/AES.php which is found in extlib/phpseclib/
92             // which has been added to our include_path before
93             require_once str_replace('_', '/', $cls) . '.php';
94             return false;
95         }
96
97         return parent::onAutoload($cls);
98     }
99
100     /**
101      * Set up queue handlers for outgoing hub pushes
102      * @param QueueManager $qm
103      * @return boolean hook return
104      */
105     function onEndInitializeQueueManager(QueueManager $qm)
106     {
107         // Prepare outgoing distributions after notice save.
108         $qm->connect('ostatus', 'OStatusQueueHandler');
109
110         // Outgoing from our internal PuSH hub
111         $qm->connect('hubconf', 'HubConfQueueHandler');
112         $qm->connect('hubprep', 'HubPrepQueueHandler');
113
114         $qm->connect('hubout', 'HubOutQueueHandler');
115
116         // Outgoing Salmon replies (when we don't need a return value)
117         $qm->connect('salmon', 'SalmonQueueHandler');
118
119         // Incoming from a foreign PuSH hub
120         $qm->connect('pushin', 'PushInQueueHandler');
121
122         // Re-subscribe feeds that need renewal
123         $qm->connect('pushrenew', 'PushRenewQueueHandler');
124         return true;
125     }
126
127     /**
128      * Put saved notices into the queue for pubsub distribution.
129      */
130     function onStartEnqueueNotice($notice, &$transports)
131     {
132         if ($notice->inScope(null)) {
133             // put our transport first, in case there's any conflict (like OMB)
134             array_unshift($transports, 'ostatus');
135             $this->log(LOG_INFO, "Notice {$notice->id} queued for OStatus processing");
136         } else {
137             // FIXME: we don't do privacy-controlled OStatus updates yet.
138             // once that happens, finer grain of control here.
139             $this->log(LOG_NOTICE, "Not queueing notice {$notice->id} for OStatus because of privacy; scope = {$notice->scope}");
140         }
141         return true;
142     }
143
144     /**
145      * Set up a PuSH hub link to our internal link for canonical timeline
146      * Atom feeds for users and groups.
147      */
148     function onStartApiAtom($feed)
149     {
150         $id = null;
151
152         if ($feed instanceof AtomUserNoticeFeed) {
153             $salmonAction = 'usersalmon';
154             $user = $feed->getUser();
155             $id   = $user->id;
156             $profile = $user->getProfile();
157         } else if ($feed instanceof AtomGroupNoticeFeed) {
158             $salmonAction = 'groupsalmon';
159             $group = $feed->getGroup();
160             $id = $group->id;
161         } else if ($feed instanceof AtomListNoticeFeed) {
162             $salmonAction = 'peopletagsalmon';
163             $peopletag = $feed->getList();
164             $id = $peopletag->id;
165         } else {
166             return true;
167         }
168
169         if (!empty($id)) {
170             $hub = common_config('ostatus', 'hub');
171             if (empty($hub)) {
172                 // Updates will be handled through our internal PuSH hub.
173                 $hub = common_local_url('pushhub');
174             }
175             $feed->addLink($hub, array('rel' => 'hub'));
176
177             // Also, we'll add in the salmon link
178             $salmon = common_local_url($salmonAction, array('id' => $id));
179             $feed->addLink($salmon, array('rel' => Salmon::REL_SALMON));
180
181             // XXX: these are deprecated, but StatusNet only looks for NS_REPLIES
182             $feed->addLink($salmon, array('rel' => Salmon::NS_REPLIES));
183             $feed->addLink($salmon, array('rel' => Salmon::NS_MENTIONS));
184         }
185
186         return true;
187     }
188
189     /**
190      * Add in an OStatus subscribe button
191      */
192     function onStartProfileRemoteSubscribe($output, $profile)
193     {
194         $this->onStartProfileListItemActionElements($output, $profile);
195         return false;
196     }
197
198     function onStartGroupSubscribe($widget, $group)
199     {
200         $cur = common_current_user();
201
202         if (empty($cur)) {
203             $widget->out->elementStart('li', 'entity_subscribe');
204
205             $url = common_local_url('ostatusinit',
206                                     array('group' => $group->nickname));
207             $widget->out->element('a', array('href' => $url,
208                                              'class' => 'entity_remote_subscribe'),
209                                 // TRANS: Link to subscribe to a remote entity.
210                                 _m('Subscribe'));
211
212             $widget->out->elementEnd('li');
213             return false;
214         }
215
216         return true;
217     }
218
219     function onStartSubscribePeopletagForm($output, $peopletag)
220     {
221         $cur = common_current_user();
222
223         if (empty($cur)) {
224             $output->elementStart('li', 'entity_subscribe');
225             $profile = $peopletag->getTagger();
226             $url = common_local_url('ostatusinit',
227                                     array('tagger' => $profile->nickname, 'peopletag' => $peopletag->tag));
228             $output->element('a', array('href' => $url,
229                                         'class' => 'entity_remote_subscribe'),
230                                 // TRANS: Link to subscribe to a remote entity.
231                                 _m('Subscribe'));
232
233             $output->elementEnd('li');
234             return false;
235         }
236
237         return true;
238     }
239
240     /*
241      * If the field being looked for is URI look for the profile
242      */
243     function onStartProfileCompletionSearch($action, $profile, $search_engine) {
244         if ($action->field == 'uri') {
245             $profile->joinAdd(array('id', 'user:id'));
246             $profile->whereAdd('uri LIKE "%' . $profile->escape($q) . '%"');
247             $profile->query();
248
249             $validate = new Validate();
250
251             if ($profile->N == 0) {
252                 try {
253                     if ($validate->email($q)) {
254                         $oprofile = Ostatus_profile::ensureWebfinger($q);
255                     } else if ($validate->uri($q)) {
256                         $oprofile = Ostatus_profile::ensureProfileURL($q);
257                     } else {
258                         // TRANS: Exception in OStatus when invalid URI was entered.
259                         throw new Exception(_m('Invalid URI.'));
260                     }
261                     return $this->filter(array($oprofile->localProfile()));
262
263                 } catch (Exception $e) {
264                 // TRANS: Error message in OStatus plugin. Do not translate the domain names example.com
265                 // TRANS: and example.net, as these are official standard domain names for use in examples.
266                     $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.");
267                     return array();
268                 }
269             }
270             return false;
271         }
272         return true;
273     }
274
275     /**
276      * Find any explicit remote mentions. Accepted forms:
277      *   Webfinger: @user@example.com
278      *   Profile link: @example.com/mublog/user
279      * @param Profile $sender
280      * @param string $text input markup text
281      * @param array &$mention in/out param: set of found mentions
282      * @return boolean hook return value
283      */
284     function onEndFindMentions(Profile $sender, $text, &$mentions)
285     {
286         $matches = array();
287
288         $wmatches = array();
289         // Webfinger matches: @user@example.com or even @user--one.george_orwell@1984.biz
290         if (preg_match_all('!(?:^|\s+)@((?:\w+[\w\-\_\.]?)*(?:[\w\-\_\.]*\w+)@(?:\w+\-?\w+\.)*\w+(?:\w+\-\w+)*\.\w+)!',
291                        $text,
292                        $wmatches,
293                        PREG_OFFSET_CAPTURE)) {
294             foreach ($wmatches[1] as $wmatch) {
295                 list($target, $pos) = $wmatch;
296                 $this->log(LOG_INFO, "Checking webfinger '$target'");
297                 $profile = null;
298                 try {
299                     $oprofile = Ostatus_profile::ensureWebfinger($target);
300                     if (!$oprofile instanceof Ostatus_profile || !$oprofile->isPerson()) {
301                         continue;
302                     }
303                     $profile = $oprofile->localProfile();
304                 } catch (OStatusShadowException $e) {
305                     // This means we got a local user in the webfinger lookup
306                     $profile = $e->profile;
307                 } catch (Exception $e) {
308                     $this->log(LOG_ERR, "Webfinger check failed: " . $e->getMessage());
309                     continue;
310                 }
311
312                 assert($profile instanceof Profile);
313
314                 $text = !empty($profile->nickname) && mb_strlen($profile->nickname) < mb_strlen($target)
315                         ? $profile->getNickname()   // TODO: we could do getFancyName() or getFullname() here
316                         : $target;
317                 $url = $profile->getUri();
318                 if (!common_valid_http_url($url)) {
319                     $url = $profile->getUrl();
320                 }
321                 $matches[$pos] = array('mentioned' => array($profile),
322                                        'type' => 'mention',
323                                        'text' => $text,
324                                        'position' => $pos,
325                                        'length' => mb_strlen($target),
326                                        'url' => $url);
327             }
328         }
329
330         // Profile matches: @example.com/mublog/user
331         if (preg_match_all('!(?:^|\s+)@((?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+(?:/\w+)*)!',
332                        $text,
333                        $wmatches,
334                        PREG_OFFSET_CAPTURE)) {
335             foreach ($wmatches[1] as $wmatch) {
336                 list($target, $pos) = $wmatch;
337                 $schemes = array('http', 'https');
338                 foreach ($schemes as $scheme) {
339                     $url = "$scheme://$target";
340                     $this->log(LOG_INFO, "Checking profile address '$url'");
341                     try {
342                         $oprofile = Ostatus_profile::ensureProfileURL($url);
343                         if ($oprofile instanceof Ostatus_profile && !$oprofile->isGroup()) {
344                             $profile = $oprofile->localProfile();
345                             $text = !empty($profile->nickname) && mb_strlen($profile->nickname) < mb_strlen($target) ?
346                                     $profile->nickname : $target;
347                             $matches[$pos] = array('mentioned' => array($profile),
348                                                    'type' => 'mention',
349                                                    'text' => $text,
350                                                    'position' => $pos,
351                                                    'length' => mb_strlen($target),
352                                                    'url' => $profile->getUrl());
353                             break;
354                         }
355                     } catch (Exception $e) {
356                         $this->log(LOG_ERR, "Profile check failed: " . $e->getMessage());
357                     }
358                 }
359             }
360         }
361
362         foreach ($mentions as $i => $other) {
363             // If we share a common prefix with a local user, override it!
364             $pos = $other['position'];
365             if (isset($matches[$pos])) {
366                 $mentions[$i] = $matches[$pos];
367                 unset($matches[$pos]);
368             }
369         }
370         foreach ($matches as $mention) {
371             $mentions[] = $mention;
372         }
373
374         return true;
375     }
376
377     /**
378      * Allow remote profile references to be used in commands:
379      *   sub update@status.net
380      *   whois evan@identi.ca
381      *   reply http://identi.ca/evan hey what's up
382      *
383      * @param Command $command
384      * @param string $arg
385      * @param Profile &$profile
386      * @return hook return code
387      */
388     function onStartCommandGetProfile($command, $arg, &$profile)
389     {
390         $oprofile = $this->pullRemoteProfile($arg);
391         if ($oprofile instanceof Ostatus_profile && !$oprofile->isGroup()) {
392             try {
393                 $profile = $oprofile->localProfile();
394             } catch (NoProfileException $e) {
395                 // No locally stored profile found for remote profile
396                 return true;
397             }
398             return false;
399         } else {
400             return true;
401         }
402     }
403
404     /**
405      * Allow remote group references to be used in commands:
406      *   join group+statusnet@identi.ca
407      *   join http://identi.ca/group/statusnet
408      *   drop identi.ca/group/statusnet
409      *
410      * @param Command $command
411      * @param string $arg
412      * @param User_group &$group
413      * @return hook return code
414      */
415     function onStartCommandGetGroup($command, $arg, &$group)
416     {
417         $oprofile = $this->pullRemoteProfile($arg);
418         if ($oprofile instanceof Ostatus_profile && $oprofile->isGroup()) {
419             $group = $oprofile->localGroup();
420             return false;
421         } else {
422             return true;
423         }
424     }
425
426     protected function pullRemoteProfile($arg)
427     {
428         $oprofile = null;
429         if (preg_match('!^((?:\w+\.)*\w+@(?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+)$!', $arg)) {
430             // webfinger lookup
431             try {
432                 return Ostatus_profile::ensureWebfinger($arg);
433             } catch (Exception $e) {
434                 common_log(LOG_ERR, 'Webfinger lookup failed for ' .
435                                     $arg . ': ' . $e->getMessage());
436             }
437         }
438
439         // Look for profile URLs, with or without scheme:
440         $urls = array();
441         if (preg_match('!^https?://((?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+(?:/\w+)+)$!', $arg)) {
442             $urls[] = $arg;
443         }
444         if (preg_match('!^((?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+(?:/\w+)+)$!', $arg)) {
445             $schemes = array('http', 'https');
446             foreach ($schemes as $scheme) {
447                 $urls[] = "$scheme://$arg";
448             }
449         }
450
451         foreach ($urls as $url) {
452             try {
453                 return Ostatus_profile::ensureProfileURL($url);
454             } catch (Exception $e) {
455                 common_log(LOG_ERR, 'Profile lookup failed for ' .
456                                     $arg . ': ' . $e->getMessage());
457             }
458         }
459         return null;
460     }
461
462     function onEndProfileSettingsActions($out) {
463         $siteName = common_config('site', 'name');
464         $js = 'navigator.registerContentHandler("application/vnd.mozilla.maybe.feed", "'.addslashes(common_local_url('ostatussub', null, array('profile' => '%s'))).'", "'.addslashes($siteName).'")';
465         $out->elementStart('li');
466         $out->element('a',
467                       array('href' => 'javascript:'.$js),
468                       // TRANS: Option in profile settings to add this instance to Firefox as a feedreader
469                       _('Add to Firefox as feedreader'));
470         $out->elementEnd('li');
471     }
472
473     /**
474      * Make sure necessary tables are filled out.
475      */
476     function onCheckSchema() {
477         $schema = Schema::get();
478         $schema->ensureTable('ostatus_profile', Ostatus_profile::schemaDef());
479         $schema->ensureTable('feedsub', FeedSub::schemaDef());
480         $schema->ensureTable('hubsub', HubSub::schemaDef());
481         $schema->ensureTable('magicsig', Magicsig::schemaDef());
482         return true;
483     }
484
485     public function onEndShowStylesheets(Action $action) {
486         $action->cssLink($this->path('theme/base/css/ostatus.css'));
487         return true;
488     }
489
490     function onEndShowStatusNetScripts($action) {
491         $action->script($this->path('js/ostatus.js'));
492         return true;
493     }
494
495     /**
496      * Override the "from ostatus" bit in notice lists to link to the
497      * original post and show the domain it came from.
498      *
499      * @param Notice in $notice
500      * @param string out &$name
501      * @param string out &$url
502      * @param string out &$title
503      * @return mixed hook return code
504      */
505     function onStartNoticeSourceLink($notice, &$name, &$url, &$title)
506     {
507         // If we don't handle this, keep the event handler going
508         if (!in_array($notice->source, array('ostatus', 'share'))) {
509             return true;
510         }
511
512         try {
513             $url = $notice->getUrl();
514             // If getUrl() throws exception, $url is never set
515             
516             $bits = parse_url($url);
517             $domain = $bits['host'];
518             if (substr($domain, 0, 4) == 'www.') {
519                 $name = substr($domain, 4);
520             } else {
521                 $name = $domain;
522             }
523
524             // TRANS: Title. %s is a domain name.
525             $title = sprintf(_m('Sent from %s via OStatus'), $domain);
526
527             // Abort event handler, we have a name and URL!
528             return false;
529         } catch (InvalidUrlException $e) {
530             // This just means we don't have the notice source data
531             return true;
532         }
533     }
534
535     /**
536      * Send incoming PuSH feeds for OStatus endpoints in for processing.
537      *
538      * @param FeedSub $feedsub
539      * @param DOMDocument $feed
540      * @return mixed hook return code
541      */
542     function onStartFeedSubReceive($feedsub, $feed)
543     {
544         $oprofile = Ostatus_profile::getKV('feeduri', $feedsub->uri);
545         if ($oprofile instanceof Ostatus_profile) {
546             $oprofile->processFeed($feed, 'push');
547         } else {
548             common_log(LOG_DEBUG, "No ostatus profile for incoming feed $feedsub->uri");
549         }
550     }
551
552     /**
553      * Tell the FeedSub infrastructure whether we have any active OStatus
554      * usage for the feed; if not it'll be able to garbage-collect the
555      * feed subscription.
556      *
557      * @param FeedSub $feedsub
558      * @param integer $count in/out
559      * @return mixed hook return code
560      */
561     function onFeedSubSubscriberCount($feedsub, &$count)
562     {
563         $oprofile = Ostatus_profile::getKV('feeduri', $feedsub->uri);
564         if ($oprofile instanceof Ostatus_profile) {
565             $count += $oprofile->subscriberCount();
566         }
567         return true;
568     }
569
570     /**
571      * When about to subscribe to a remote user, start a server-to-server
572      * PuSH subscription if needed. If we can't establish that, abort.
573      *
574      * @fixme If something else aborts later, we could end up with a stray
575      *        PuSH subscription. This is relatively harmless, though.
576      *
577      * @param Profile $profile  subscriber
578      * @param Profile $other    subscribee
579      *
580      * @return hook return code
581      *
582      * @throws Exception
583      */
584     function onStartSubscribe(Profile $profile, Profile $other)
585     {
586         if (!$profile->isLocal()) {
587             return true;
588         }
589
590         $oprofile = Ostatus_profile::getKV('profile_id', $other->id);
591         if (!$oprofile instanceof Ostatus_profile) {
592             return true;
593         }
594
595         $oprofile->subscribe();
596     }
597
598     /**
599      * Having established a remote subscription, send a notification to the
600      * remote OStatus profile's endpoint.
601      *
602      * @param Profile $profile  subscriber
603      * @param Profile $other    subscribee
604      *
605      * @return hook return code
606      *
607      * @throws Exception
608      */
609     function onEndSubscribe(Profile $profile, Profile $other)
610     {
611         if (!$profile->isLocal()) {
612             return true;
613         }
614
615         $oprofile = Ostatus_profile::getKV('profile_id', $other->id);
616         if (!$oprofile instanceof Ostatus_profile) {
617             return true;
618         }
619
620         $sub = Subscription::pkeyGet(array('subscriber' => $profile->id,
621                                            'subscribed' => $other->id));
622
623         $act = $sub->asActivity();
624
625         $oprofile->notifyActivity($act, $profile);
626
627         return true;
628     }
629
630     /**
631      * Notify remote server and garbage collect unused feeds on unsubscribe.
632      * @todo FIXME: Send these operations to background queues
633      *
634      * @param User $user
635      * @param Profile $other
636      * @return hook return value
637      */
638     function onEndUnsubscribe(Profile $profile, Profile $other)
639     {
640         if (!$profile->isLocal()) {
641             return true;
642         }
643
644         $oprofile = Ostatus_profile::getKV('profile_id', $other->id);
645         if (!$oprofile instanceof Ostatus_profile) {
646             return true;
647         }
648
649         // Drop the PuSH subscription if there are no other subscribers.
650         $oprofile->garbageCollect();
651
652         $act = new Activity();
653
654         $act->verb = ActivityVerb::UNFOLLOW;
655
656         $act->id   = TagURI::mint('unfollow:%d:%d:%s',
657                                   $profile->id,
658                                   $other->id,
659                                   common_date_iso8601(time()));
660
661         $act->time    = time();
662         // TRANS: Title for unfollowing a remote profile.
663         $act->title   = _m('TITLE','Unfollow');
664         // TRANS: Success message for unsubscribe from user attempt through OStatus.
665         // TRANS: %1$s is the unsubscriber's name, %2$s is the unsubscribed user's name.
666         $act->content = sprintf(_m('%1$s stopped following %2$s.'),
667                                $profile->getBestName(),
668                                $other->getBestName());
669
670         $act->actor   = $profile->asActivityObject();
671         $act->objects[] = $other->asActivityObject();
672
673         $oprofile->notifyActivity($act, $profile);
674
675         return true;
676     }
677
678     /**
679      * When one of our local users tries to join a remote group,
680      * notify the remote server. If the notification is rejected,
681      * deny the join.
682      *
683      * @param User_group $group
684      * @param Profile    $profile
685      *
686      * @return mixed hook return value
687      * @throws Exception of various kinds, some from $oprofile->subscribe();
688      */
689     function onStartJoinGroup($group, $profile)
690     {
691         $oprofile = Ostatus_profile::getKV('group_id', $group->id);
692         if (!$oprofile instanceof Ostatus_profile) {
693             return true;
694         }
695
696         $oprofile->subscribe();
697
698         // NOTE: we don't use Group_member::asActivity() since that record
699         // has not yet been created.
700
701         $act = new Activity();
702         $act->id = TagURI::mint('join:%d:%d:%s',
703                                 $profile->id,
704                                 $group->id,
705                                 common_date_iso8601(time()));
706
707         $act->actor = $profile->asActivityObject();
708         $act->verb = ActivityVerb::JOIN;
709         $act->objects[] = $oprofile->asActivityObject();
710
711         $act->time = time();
712         // TRANS: Title for joining a remote groep.
713         $act->title = _m('TITLE','Join');
714         // TRANS: Success message for subscribe to group attempt through OStatus.
715         // TRANS: %1$s is the member name, %2$s is the subscribed group's name.
716         $act->content = sprintf(_m('%1$s has joined group %2$s.'),
717                                 $profile->getBestName(),
718                                 $oprofile->getBestName());
719
720         if ($oprofile->notifyActivity($act, $profile)) {
721             return true;
722         } else {
723             $oprofile->garbageCollect();
724             // TRANS: Exception thrown when joining a remote group fails.
725             throw new Exception(_m('Failed joining remote group.'));
726         }
727     }
728
729     /**
730      * When one of our local users leaves a remote group, notify the remote
731      * server.
732      *
733      * @fixme Might be good to schedule a resend of the leave notification
734      * if it failed due to a transitory error. We've canceled the local
735      * membership already anyway, but if the remote server comes back up
736      * it'll be left with a stray membership record.
737      *
738      * @param User_group $group
739      * @param Profile $profile
740      *
741      * @return mixed hook return value
742      */
743     function onEndLeaveGroup($group, $profile)
744     {
745         $oprofile = Ostatus_profile::getKV('group_id', $group->id);
746         if (!$oprofile instanceof Ostatus_profile) {
747             return true;
748         }
749
750         // Drop the PuSH subscription if there are no other subscribers.
751         $oprofile->garbageCollect();
752
753         $member = $profile;
754
755         $act = new Activity();
756         $act->id = TagURI::mint('leave:%d:%d:%s',
757                                 $member->id,
758                                 $group->id,
759                                 common_date_iso8601(time()));
760
761         $act->actor = $member->asActivityObject();
762         $act->verb = ActivityVerb::LEAVE;
763         $act->objects[] = $oprofile->asActivityObject();
764
765         $act->time = time();
766         // TRANS: Title for leaving a remote group.
767         $act->title = _m('TITLE','Leave');
768         // TRANS: Success message for unsubscribe from group attempt through OStatus.
769         // TRANS: %1$s is the member name, %2$s is the unsubscribed group's name.
770         $act->content = sprintf(_m('%1$s has left group %2$s.'),
771                                 $member->getBestName(),
772                                 $oprofile->getBestName());
773
774         $oprofile->notifyActivity($act, $member);
775     }
776
777     /**
778      * When one of our local users tries to subscribe to a remote peopletag,
779      * notify the remote server. If the notification is rejected,
780      * deny the subscription.
781      *
782      * @param Profile_list $peopletag
783      * @param User         $user
784      *
785      * @return mixed hook return value
786      * @throws Exception of various kinds, some from $oprofile->subscribe();
787      */
788
789     function onStartSubscribePeopletag($peopletag, $user)
790     {
791         $oprofile = Ostatus_profile::getKV('peopletag_id', $peopletag->id);
792         if (!$oprofile instanceof Ostatus_profile) {
793             return true;
794         }
795
796         $oprofile->subscribe();
797
798         $sub = $user->getProfile();
799         $tagger = Profile::getKV($peopletag->tagger);
800
801         $act = new Activity();
802         $act->id = TagURI::mint('subscribe_peopletag:%d:%d:%s',
803                                 $sub->id,
804                                 $peopletag->id,
805                                 common_date_iso8601(time()));
806
807         $act->actor = $sub->asActivityObject();
808         $act->verb = ActivityVerb::FOLLOW;
809         $act->objects[] = $oprofile->asActivityObject();
810
811         $act->time = time();
812         // TRANS: Title for following a remote list.
813         $act->title = _m('TITLE','Follow list');
814         // TRANS: Success message for remote list follow through OStatus.
815         // TRANS: %1$s is the subscriber name, %2$s is the list, %3$s is the lister's name.
816         $act->content = sprintf(_m('%1$s is now following people listed in %2$s by %3$s.'),
817                                 $sub->getBestName(),
818                                 $oprofile->getBestName(),
819                                 $tagger->getBestName());
820
821         if ($oprofile->notifyActivity($act, $sub)) {
822             return true;
823         } else {
824             $oprofile->garbageCollect();
825             // TRANS: Exception thrown when subscription to remote list fails.
826             throw new Exception(_m('Failed subscribing to remote list.'));
827         }
828     }
829
830     /**
831      * When one of our local users unsubscribes to a remote peopletag, notify the remote
832      * server.
833      *
834      * @param Profile_list $peopletag
835      * @param User         $user
836      *
837      * @return mixed hook return value
838      */
839
840     function onEndUnsubscribePeopletag($peopletag, $user)
841     {
842         $oprofile = Ostatus_profile::getKV('peopletag_id', $peopletag->id);
843         if (!$oprofile instanceof Ostatus_profile) {
844             return true;
845         }
846
847         // Drop the PuSH subscription if there are no other subscribers.
848         $oprofile->garbageCollect();
849
850         $sub = Profile::getKV($user->id);
851         $tagger = Profile::getKV($peopletag->tagger);
852
853         $act = new Activity();
854         $act->id = TagURI::mint('unsubscribe_peopletag:%d:%d:%s',
855                                 $sub->id,
856                                 $peopletag->id,
857                                 common_date_iso8601(time()));
858
859         $act->actor = $member->asActivityObject();
860         $act->verb = ActivityVerb::UNFOLLOW;
861         $act->objects[] = $oprofile->asActivityObject();
862
863         $act->time = time();
864         // TRANS: Title for unfollowing a remote list.
865         $act->title = _m('Unfollow list');
866         // TRANS: Success message for remote list unfollow through OStatus.
867         // TRANS: %1$s is the subscriber name, %2$s is the list, %3$s is the lister's name.
868         $act->content = sprintf(_m('%1$s stopped following the list %2$s by %3$s.'),
869                                 $sub->getBestName(),
870                                 $oprofile->getBestName(),
871                                 $tagger->getBestName());
872
873         $oprofile->notifyActivity($act, $user);
874     }
875
876     /**
877      * Notify remote users when their notices get favorited.
878      *
879      * @param Profile or User $profile of local user doing the faving
880      * @param Notice $notice being favored
881      * @return hook return value
882      */
883     function onEndFavorNotice(Profile $profile, Notice $notice)
884     {
885         // Only distribute local users' favor actions, remote users
886         // will have already distributed theirs.
887         if (!$profile->isLocal()) {
888             return true;
889         }
890
891         $oprofile = Ostatus_profile::getKV('profile_id', $notice->profile_id);
892         if (!$oprofile instanceof Ostatus_profile) {
893             return true;
894         }
895
896         $fav = Fave::pkeyGet(array('user_id' => $profile->id,
897                                    'notice_id' => $notice->id));
898
899         if (!$fav instanceof Fave) {
900             // That's weird.
901             // TODO: Make pkeyGet throw exception, since this is a critical failure.
902             return true;
903         }
904
905         $act = $fav->asActivity();
906
907         $oprofile->notifyActivity($act, $profile);
908
909         return true;
910     }
911
912     /**
913      * Notify remote user it has got a new people tag
914      *   - tag verb is queued
915      *   - the subscription is done immediately if not present
916      *
917      * @param Profile_tag $ptag the people tag that was created
918      * @return hook return value
919      * @throws Exception of various kinds, some from $oprofile->subscribe();
920      */
921     function onEndTagProfile($ptag)
922     {
923         $oprofile = Ostatus_profile::getKV('profile_id', $ptag->tagged);
924         if (!$oprofile instanceof Ostatus_profile) {
925             return true;
926         }
927
928         $plist = $ptag->getMeta();
929         if ($plist->private) {
930             return true;
931         }
932
933         $act = new Activity();
934
935         $tagger = $plist->getTagger();
936         $tagged = Profile::getKV('id', $ptag->tagged);
937
938         $act->verb = ActivityVerb::TAG;
939         $act->id   = TagURI::mint('tag_profile:%d:%d:%s',
940                                   $plist->tagger, $plist->id,
941                                   common_date_iso8601(time()));
942         $act->time = time();
943         // TRANS: Title for listing a remote profile.
944         $act->title = _m('TITLE','List');
945         // TRANS: Success message for remote list addition through OStatus.
946         // TRANS: %1$s is the list creator's name, %2$s is the added list member, %3$s is the list name.
947         $act->content = sprintf(_m('%1$s listed %2$s in the list %3$s.'),
948                                 $tagger->getBestName(),
949                                 $tagged->getBestName(),
950                                 $plist->getBestName());
951
952         $act->actor  = $tagger->asActivityObject();
953         $act->objects = array($tagged->asActivityObject());
954         $act->target = ActivityObject::fromPeopletag($plist);
955
956         $oprofile->notifyDeferred($act, $tagger);
957
958         // initiate a PuSH subscription for the person being tagged
959         $oprofile->subscribe();
960         return true;
961     }
962
963     /**
964      * Notify remote user that a people tag has been removed
965      *   - untag verb is queued
966      *   - the subscription is undone immediately if not required
967      *     i.e garbageCollect()'d
968      *
969      * @param Profile_tag $ptag the people tag that was deleted
970      * @return hook return value
971      */
972     function onEndUntagProfile($ptag)
973     {
974         $oprofile = Ostatus_profile::getKV('profile_id', $ptag->tagged);
975         if (!$oprofile instanceof Ostatus_profile) {
976             return true;
977         }
978
979         $plist = $ptag->getMeta();
980         if ($plist->private) {
981             return true;
982         }
983
984         $act = new Activity();
985
986         $tagger = $plist->getTagger();
987         $tagged = Profile::getKV('id', $ptag->tagged);
988
989         $act->verb = ActivityVerb::UNTAG;
990         $act->id   = TagURI::mint('untag_profile:%d:%d:%s',
991                                   $plist->tagger, $plist->id,
992                                   common_date_iso8601(time()));
993         $act->time = time();
994         // TRANS: Title for unlisting a remote profile.
995         $act->title = _m('TITLE','Unlist');
996         // TRANS: Success message for remote list removal through OStatus.
997         // TRANS: %1$s is the list creator's name, %2$s is the removed list member, %3$s is the list name.
998         $act->content = sprintf(_m('%1$s removed %2$s from the list %3$s.'),
999                                 $tagger->getBestName(),
1000                                 $tagged->getBestName(),
1001                                 $plist->getBestName());
1002
1003         $act->actor  = $tagger->asActivityObject();
1004         $act->objects = array($tagged->asActivityObject());
1005         $act->target = ActivityObject::fromPeopletag($plist);
1006
1007         $oprofile->notifyDeferred($act, $tagger);
1008
1009         // unsubscribe to PuSH feed if no more required
1010         $oprofile->garbageCollect();
1011
1012         return true;
1013     }
1014
1015     /**
1016      * Notify remote users when their notices get de-favorited.
1017      *
1018      * @param Profile $profile Profile person doing the de-faving
1019      * @param Notice  $notice  Notice being favored
1020      *
1021      * @return hook return value
1022      */
1023     function onEndDisfavorNotice(Profile $profile, Notice $notice)
1024     {
1025         // Only distribute local users' disfavor actions, remote users
1026         // will have already distributed theirs.
1027         if (!$profile->isLocal()) {
1028             return true;
1029         }
1030
1031         $oprofile = Ostatus_profile::getKV('profile_id', $notice->profile_id);
1032         if (!$oprofile instanceof Ostatus_profile) {
1033             return true;
1034         }
1035
1036         $act = new Activity();
1037
1038         $act->verb = ActivityVerb::UNFAVORITE;
1039         $act->id   = TagURI::mint('disfavor:%d:%d:%s',
1040                                   $profile->id,
1041                                   $notice->id,
1042                                   common_date_iso8601(time()));
1043         $act->time    = time();
1044         // TRANS: Title for unliking a remote notice.
1045         $act->title   = _m('Unlike');
1046         // TRANS: Success message for remove a favorite notice through OStatus.
1047         // TRANS: %1$s is the unfavoring user's name, %2$s is URI to the no longer favored notice.
1048         $act->content = sprintf(_m('%1$s no longer likes %2$s.'),
1049                                $profile->getBestName(),
1050                                $notice->getUrl());
1051
1052         $act->actor   = $profile->asActivityObject();
1053         $act->objects[]  = $notice->asActivityObject();
1054
1055         $oprofile->notifyActivity($act, $profile);
1056
1057         return true;
1058     }
1059
1060     function onStartGetProfileUri($profile, &$uri)
1061     {
1062         $oprofile = Ostatus_profile::getKV('profile_id', $profile->id);
1063         if ($oprofile instanceof Ostatus_profile) {
1064             $uri = $oprofile->uri;
1065             return false;
1066         }
1067         return true;
1068     }
1069
1070     function onStartUserGroupHomeUrl($group, &$url)
1071     {
1072         return $this->onStartUserGroupPermalink($group, $url);
1073     }
1074
1075     function onStartUserGroupPermalink($group, &$url)
1076     {
1077         $oprofile = Ostatus_profile::getKV('group_id', $group->id);
1078         if ($oprofile instanceof Ostatus_profile) {
1079             // @fixme this should probably be in the user_group table
1080             // @fixme this uri not guaranteed to be a profile page
1081             $url = $oprofile->uri;
1082             return false;
1083         }
1084     }
1085
1086     function onStartShowSubscriptionsContent($action)
1087     {
1088         $this->showEntityRemoteSubscribe($action);
1089
1090         return true;
1091     }
1092
1093     function onStartShowUserGroupsContent($action)
1094     {
1095         $this->showEntityRemoteSubscribe($action, 'ostatusgroup');
1096
1097         return true;
1098     }
1099
1100     function onEndShowSubscriptionsMiniList($action)
1101     {
1102         $this->showEntityRemoteSubscribe($action);
1103
1104         return true;
1105     }
1106
1107     function onEndShowGroupsMiniList($action)
1108     {
1109         $this->showEntityRemoteSubscribe($action, 'ostatusgroup');
1110
1111         return true;
1112     }
1113
1114     function showEntityRemoteSubscribe($action, $target='ostatussub')
1115     {
1116         if (!$action->getScoped() instanceof Profile) {
1117             // early return if we're not logged in
1118             return true;
1119         }
1120
1121         if ($action->getScoped()->sameAs($action->getTarget())) {
1122             $action->elementStart('div', 'entity_actions');
1123             $action->elementStart('p', array('id' => 'entity_remote_subscribe',
1124                                              'class' => 'entity_subscribe'));
1125             $action->element('a', array('href' => common_local_url($target),
1126                                         'class' => 'entity_remote_subscribe'),
1127                                 // TRANS: Link text for link to remote subscribe.
1128                                 _m('Remote'));
1129             $action->elementEnd('p');
1130             $action->elementEnd('div');
1131         }
1132     }
1133
1134     /**
1135      * Ping remote profiles with updates to this profile.
1136      * Salmon pings are queued for background processing.
1137      */
1138     function onEndBroadcastProfile(Profile $profile)
1139     {
1140         $user = User::getKV('id', $profile->id);
1141
1142         // Find foreign accounts I'm subscribed to that support Salmon pings.
1143         //
1144         // @fixme we could run updates through the PuSH feed too,
1145         // in which case we can skip Salmon pings to folks who
1146         // are also subscribed to me.
1147         $sql = "SELECT * FROM ostatus_profile " .
1148                "WHERE profile_id IN " .
1149                "(SELECT subscribed FROM subscription WHERE subscriber=%d) " .
1150                "OR group_id IN " .
1151                "(SELECT group_id FROM group_member WHERE profile_id=%d)";
1152         $oprofile = new Ostatus_profile();
1153         $oprofile->query(sprintf($sql, $profile->id, $profile->id));
1154
1155         if ($oprofile->N == 0) {
1156             common_log(LOG_DEBUG, "No OStatus remote subscribees for $profile->nickname");
1157             return true;
1158         }
1159
1160         $act = new Activity();
1161
1162         $act->verb = ActivityVerb::UPDATE_PROFILE;
1163         $act->id   = TagURI::mint('update-profile:%d:%s',
1164                                   $profile->id,
1165                                   common_date_iso8601(time()));
1166         $act->time    = time();
1167         // TRANS: Title for activity.
1168         $act->title   = _m('Profile update');
1169         // TRANS: Ping text for remote profile update through OStatus.
1170         // TRANS: %s is user that updated their profile.
1171         $act->content = sprintf(_m('%s has updated their profile page.'),
1172                                $profile->getBestName());
1173
1174         $act->actor   = $profile->asActivityObject();
1175         $act->objects[]  = $act->actor;
1176
1177         while ($oprofile->fetch()) {
1178             $oprofile->notifyDeferred($act, $profile);
1179         }
1180
1181         return true;
1182     }
1183
1184     // FIXME: This one can accept both an Action and a Widget. Confusing! Refactor to (HTMLOutputter $out, Profile $target)!
1185     function onStartProfileListItemActionElements($item)
1186     {
1187         if (common_logged_in()) {
1188             // only non-logged in users get to see the "remote subscribe" form
1189             return true;
1190         } elseif (!$item->getTarget()->isLocal()) {
1191             // we can (for now) only provide remote subscribe forms for local users
1192             return true;
1193         }
1194
1195         if ($item instanceof ProfileAction) {
1196             $output = $item;
1197         } elseif ($item instanceof Widget) {
1198             $output = $item->out;
1199         } else {
1200             // Bad $item class, don't know how to use this for outputting!
1201             throw new ServerException('Bad item type for onStartProfileListItemActionElements');
1202         }
1203
1204         // Add an OStatus subscribe
1205         $output->elementStart('li', 'entity_subscribe');
1206         $url = common_local_url('ostatusinit',
1207                                 array('nickname' => $item->getTarget()->getNickname()));
1208         $output->element('a', array('href' => $url,
1209                                     'class' => 'entity_remote_subscribe'),
1210                           // TRANS: Link text for a user to subscribe to an OStatus user.
1211                          _m('Subscribe'));
1212         $output->elementEnd('li');
1213
1214         $output->elementStart('li', 'entity_tag');
1215         $url = common_local_url('ostatustag',
1216                                 array('nickname' => $item->getTarget()->getNickname()));
1217         $output->element('a', array('href' => $url,
1218                                     'class' => 'entity_remote_tag'),
1219                           // TRANS: Link text for a user to list an OStatus user.
1220                          _m('List'));
1221         $output->elementEnd('li');
1222
1223         return true;
1224     }
1225
1226     function onPluginVersion(array &$versions)
1227     {
1228         $versions[] = array('name' => 'OStatus',
1229                             'version' => GNUSOCIAL_VERSION,
1230                             'author' => 'Evan Prodromou, James Walker, Brion Vibber, Zach Copley',
1231                             'homepage' => 'https://git.gnu.io/gnu/gnu-social/tree/master/plugins/OStatus',
1232                             // TRANS: Plugin description.
1233                             'rawdescription' => _m('Follow people across social networks that implement '.
1234                                '<a href="http://ostatus.org/">OStatus</a>.'));
1235
1236         return true;
1237     }
1238
1239     /**
1240      * Utility function to check if the given URI is a canonical group profile
1241      * page, and if so return the ID number.
1242      *
1243      * @param string $url
1244      * @return mixed int or false
1245      */
1246     public static function localGroupFromUrl($url)
1247     {
1248         $group = User_group::getKV('uri', $url);
1249         if ($group instanceof User_group) {
1250             if ($group->isLocal()) {
1251                 return $group->id;
1252             }
1253         } else {
1254             // To find local groups which haven't had their uri fields filled out...
1255             // If the domain has changed since a subscriber got the URI, it'll
1256             // be broken.
1257             $template = common_local_url('groupbyid', array('id' => '31337'));
1258             $template = preg_quote($template, '/');
1259             $template = str_replace('31337', '(\d+)', $template);
1260             if (preg_match("/$template/", $url, $matches)) {
1261                 return intval($matches[1]);
1262             }
1263         }
1264         return false;
1265     }
1266
1267     public function onStartProfileGetAtomFeed($profile, &$feed)
1268     {
1269         $oprofile = Ostatus_profile::getKV('profile_id', $profile->id);
1270
1271         if (!$oprofile instanceof Ostatus_profile) {
1272             return true;
1273         }
1274
1275         $feed = $oprofile->feeduri;
1276         return false;
1277     }
1278
1279     function onStartGetProfileFromURI($uri, &$profile)
1280     {
1281         // Don't want to do Web-based discovery on our own server,
1282         // so we check locally first. This duplicates the functionality
1283         // in the Profile class, since the plugin always runs before
1284         // that local lookup, but since we return false it won't run double.
1285
1286         $user = User::getKV('uri', $uri);
1287         if ($user instanceof User) {
1288             $profile = $user->getProfile();
1289             return false;
1290         } else {
1291             $group = User_group::getKV('uri', $uri);
1292             if ($group instanceof User_group) {
1293                 $profile = $group->getProfile();
1294                 return false;
1295             }
1296         }
1297
1298         // Now, check remotely
1299         try {
1300             $oprofile = Ostatus_profile::ensureProfileURI($uri);
1301             $profile = $oprofile->localProfile();
1302             return !($profile instanceof Profile);  // localProfile won't throw exception but can return null
1303         } catch (Exception $e) {
1304             return true; // It's not an OStatus profile as far as we know, continue event handling
1305         }
1306     }
1307
1308     function onEndWebFingerNoticeLinks(XML_XRD $xrd, Notice $target)
1309     {
1310         $author = $target->getProfile();
1311         $profiletype = $this->profileTypeString($author);
1312         $salmon_url = common_local_url("{$profiletype}salmon", array('id' => $author->id));
1313         $xrd->links[] = new XML_XRD_Element_Link(Salmon::REL_SALMON, $salmon_url);
1314         return true;
1315     }
1316
1317     function onEndWebFingerProfileLinks(XML_XRD $xrd, Profile $target)
1318     {
1319         if ($target->getObjectType() === ActivityObject::PERSON) {
1320             $this->addWebFingerPersonLinks($xrd, $target);
1321         }
1322
1323         // Salmon
1324         $profiletype = $this->profileTypeString($target);
1325         $salmon_url = common_local_url("{$profiletype}salmon", array('id' => $target->id));
1326
1327         $xrd->links[] = new XML_XRD_Element_Link(Salmon::REL_SALMON, $salmon_url);
1328
1329         // XXX: these are deprecated, but StatusNet only looks for NS_REPLIES
1330         $xrd->links[] = new XML_XRD_Element_Link(Salmon::NS_REPLIES, $salmon_url);
1331         $xrd->links[] = new XML_XRD_Element_Link(Salmon::NS_MENTIONS, $salmon_url);
1332
1333         // TODO - finalize where the redirect should go on the publisher
1334         $xrd->links[] = new XML_XRD_Element_Link('http://ostatus.org/schema/1.0/subscribe',
1335                               common_local_url('ostatussub') . '?profile={uri}',
1336                               null, // type not set
1337                               true); // isTemplate
1338
1339         return true;
1340     }
1341
1342     protected function profileTypeString(Profile $target)
1343     {
1344         // This is just used to have a definitive string response to "USERsalmon" or "GROUPsalmon"
1345         switch ($target->getObjectType()) {
1346         case ActivityObject::PERSON:
1347             return 'user';
1348         case ActivityObject::GROUP:
1349             return 'group';
1350         default:
1351             throw new ServerException('Unknown profile type for WebFinger profile links');
1352         }
1353     }
1354
1355     protected function addWebFingerPersonLinks(XML_XRD $xrd, Profile $target)
1356     {
1357         $xrd->links[] = new XML_XRD_Element_Link(Discovery::UPDATESFROM,
1358                             common_local_url('ApiTimelineUser',
1359                                 array('id' => $target->id, 'format' => 'atom')),
1360                             'application/atom+xml');
1361
1362         // Get this profile's keypair
1363         $magicsig = Magicsig::getKV('user_id', $target->id);
1364         if (!$magicsig instanceof Magicsig && $target->isLocal()) {
1365             $magicsig = Magicsig::generate($target->getUser());
1366         }
1367
1368         if (!$magicsig instanceof Magicsig) {
1369             return false;   // value doesn't mean anything, just figured I'd indicate this function didn't do anything
1370         }
1371         if (Event::handle('StartAttachPubkeyToUserXRD', array($magicsig, $xrd, $target))) {
1372             $xrd->links[] = new XML_XRD_Element_Link(Magicsig::PUBLICKEYREL,
1373                                 'data:application/magic-public-key,'. $magicsig->toString());
1374             // The following event handles plugins like Diaspora which add their own version of the Magicsig pubkey
1375             Event::handle('EndAttachPubkeyToUserXRD', array($magicsig, $xrd, $target));
1376         }
1377     }
1378
1379     public function onGetLocalAttentions(Profile $actor, array $attention_uris, array &$mentions, array &$groups)
1380     {
1381         list($groups, $mentions) = Ostatus_profile::filterAttention($actor, $attention_uris);
1382     }
1383
1384     // FIXME: Maybe this shouldn't be so authoritative that it breaks other remote profile lookups?
1385     static public function onCheckActivityAuthorship(Activity $activity, Profile &$profile)
1386     {
1387         try {
1388             $oprofile = Ostatus_profile::ensureProfileURL($profile->getUrl());
1389             $profile = $oprofile->checkAuthorship($activity);
1390         } catch (Exception $e) {
1391             common_log(LOG_ERR, 'Could not get a profile or check authorship ('.get_class($e).': "'.$e->getMessage().'") for activity ID: '.$activity->id);
1392             $profile = null;
1393             return false;
1394         }
1395         return true;
1396     }
1397
1398     public function onProfileDeleteRelated($profile, &$related)
1399     {
1400         // Ostatus_profile has a 'profile_id' property, which will be used to find the object
1401         $related[] = 'Ostatus_profile';
1402
1403         // Magicsig has a "user_id" column instead, so we have to delete it more manually:
1404         $magicsig = Magicsig::getKV('user_id', $profile->id);
1405         if ($magicsig instanceof Magicsig) {
1406             $magicsig->delete();
1407         }
1408         return true;
1409     }
1410
1411     public function onSalmonSlap($endpoint_uri, MagicEnvelope $magic_env, Profile $target=null)
1412     {
1413         $envxml = $magic_env->toXML($target);
1414
1415         $headers = array('Content-Type: application/magic-envelope+xml');
1416
1417         try {
1418             $client = new HTTPClient();
1419             $client->setBody($envxml);
1420             $response = $client->post($endpoint_uri, $headers);
1421         } catch (Exception $e) {
1422             common_log(LOG_ERR, "Salmon post to $endpoint_uri failed: " . $e->getMessage());
1423             return false;
1424         }
1425         if ($response->getStatus() === 422) {
1426             common_debug(sprintf('Salmon (from profile %d) endpoint %s returned status %s. We assume it is a Diaspora seed; will adapt and try again if that plugin is enabled!', $magic_env->getActor()->getID(), $endpoint_uri, $response->getStatus()));
1427             return true;
1428         }
1429
1430         // 200 OK is the best response
1431         // 202 Accepted is what we get from Diaspora for example
1432         if (!in_array($response->getStatus(), array(200, 202))) {
1433             common_log(LOG_ERR, sprintf('Salmon (from profile %d) endpoint %s returned status %s: %s',
1434                                 $magic_env->getActor()->getID(), $endpoint_uri, $response->getStatus(), $response->getBody()));
1435             return true;
1436         }
1437
1438         // Since we completed the salmon slap, we discontinue the event
1439         return false;
1440     }
1441
1442     public function onCronDaily()
1443     {
1444         try {
1445             $sub = FeedSub::renewalCheck();
1446         } catch (NoResultException $e) {
1447             common_log(LOG_INFO, "There were no expiring feeds.");
1448             return;
1449         }
1450
1451         $qm = QueueManager::get();
1452         while ($sub->fetch()) {
1453             $item = array('feedsub_id' => $sub->id);
1454             $qm->enqueue($item, 'pushrenew');
1455         }
1456     }
1457 }