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