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