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