]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - plugins/OStatus/OStatusPlugin.php
- break OMB profile update pings to a background queue
[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') && !defined('LACONICA')) { exit(1); }
26
27 set_include_path(get_include_path() . PATH_SEPARATOR . dirname(__FILE__) . '/extlib/');
28
29 class FeedSubException extends Exception
30 {
31 }
32
33 class OStatusPlugin extends Plugin
34 {
35     /**
36      * Hook for RouterInitialized event.
37      *
38      * @param Net_URL_Mapper $m path-to-action mapper
39      * @return boolean hook return
40      */
41     function onRouterInitialized($m)
42     {
43         // Discovery actions
44         $m->connect('.well-known/host-meta',
45                     array('action' => 'hostmeta'));
46         $m->connect('main/webfinger',
47                     array('action' => 'webfinger'));
48         $m->connect('main/ostatus',
49                     array('action' => 'ostatusinit'));
50         $m->connect('main/ostatus?nickname=:nickname',
51                   array('action' => 'ostatusinit'), array('nickname' => '[A-Za-z0-9_-]+'));
52         $m->connect('main/ostatussub',
53                     array('action' => 'ostatussub'));
54         $m->connect('main/ostatussub',
55                     array('action' => 'ostatussub'), array('feed' => '[A-Za-z0-9\.\/\:]+'));
56
57         // PuSH actions
58         $m->connect('main/push/hub', array('action' => 'pushhub'));
59
60         $m->connect('main/push/callback/:feed',
61                     array('action' => 'pushcallback'),
62                     array('feed' => '[0-9]+'));
63
64         // Salmon endpoint
65         $m->connect('main/salmon/user/:id',
66                     array('action' => 'usersalmon'),
67                     array('id' => '[0-9]+'));
68         $m->connect('main/salmon/group/:id',
69                     array('action' => 'groupsalmon'),
70                     array('id' => '[0-9]+'));
71         return true;
72     }
73
74     /**
75      * Set up queue handlers for outgoing hub pushes
76      * @param QueueManager $qm
77      * @return boolean hook return
78      */
79     function onEndInitializeQueueManager(QueueManager $qm)
80     {
81         // Prepare outgoing distributions after notice save.
82         $qm->connect('ostatus', 'OStatusQueueHandler');
83
84         // Outgoing from our internal PuSH hub
85         $qm->connect('hubconf', 'HubConfQueueHandler');
86         $qm->connect('hubout', 'HubOutQueueHandler');
87
88         // Outgoing Salmon replies (when we don't need a return value)
89         $qm->connect('salmon', 'SalmonQueueHandler');
90
91         // Incoming from a foreign PuSH hub
92         $qm->connect('pushin', 'PushInQueueHandler');
93         return true;
94     }
95
96     /**
97      * Put saved notices into the queue for pubsub distribution.
98      */
99     function onStartEnqueueNotice($notice, &$transports)
100     {
101         $transports[] = 'ostatus';
102         return true;
103     }
104
105     /**
106      * Set up a PuSH hub link to our internal link for canonical timeline
107      * Atom feeds for users and groups.
108      */
109     function onStartApiAtom($feed)
110     {
111         $id = null;
112
113         if ($feed instanceof AtomUserNoticeFeed) {
114             $salmonAction = 'usersalmon';
115             $user = $feed->getUser();
116             $id   = $user->id;
117             $profile = $user->getProfile();
118             $feed->setActivitySubject($profile->asActivityNoun('subject'));
119         } else if ($feed instanceof AtomGroupNoticeFeed) {
120             $salmonAction = 'groupsalmon';
121             $group = $feed->getGroup();
122             $id = $group->id;
123             $feed->setActivitySubject($group->asActivitySubject());
124         } else {
125             return true;
126         }
127
128         if (!empty($id)) {
129             $hub = common_config('ostatus', 'hub');
130             if (empty($hub)) {
131                 // Updates will be handled through our internal PuSH hub.
132                 $hub = common_local_url('pushhub');
133             }
134             $feed->addLink($hub, array('rel' => 'hub'));
135
136             // Also, we'll add in the salmon link
137             $salmon = common_local_url($salmonAction, array('id' => $id));
138             $feed->addLink($salmon, array('rel' => 'salmon'));
139         }
140
141         return true;
142     }
143
144     /**
145      * Automatically load the actions and libraries used by the plugin
146      *
147      * @param Class $cls the class
148      *
149      * @return boolean hook return
150      *
151      */
152     function onAutoload($cls)
153     {
154         $base = dirname(__FILE__);
155         $lower = strtolower($cls);
156         $map = array('activityverb' => 'activity',
157                      'activityobject' => 'activity',
158                      'activityutils' => 'activity');
159         if (isset($map[$lower])) {
160             $lower = $map[$lower];
161         }
162         $files = array("$base/classes/$cls.php",
163                        "$base/lib/$lower.php");
164         if (substr($lower, -6) == 'action') {
165             $files[] = "$base/actions/" . substr($lower, 0, -6) . ".php";
166         }
167         foreach ($files as $file) {
168             if (file_exists($file)) {
169                 include_once $file;
170                 return false;
171             }
172         }
173         return true;
174     }
175
176     /**
177      * Add in an OStatus subscribe button
178      */
179     function onStartProfileRemoteSubscribe($output, $profile)
180     {
181         $cur = common_current_user();
182
183         if (empty($cur)) {
184             // Add an OStatus subscribe
185             $output->elementStart('li', 'entity_subscribe');
186             $url = common_local_url('ostatusinit',
187                                     array('nickname' => $profile->nickname));
188             $output->element('a', array('href' => $url,
189                                         'class' => 'entity_remote_subscribe'),
190                                 _m('Subscribe'));
191
192             $output->elementEnd('li');
193         }
194
195         return false;
196     }
197
198     /**
199      * Check if we've got remote replies to send via Salmon.
200      *
201      * @fixme push webfinger lookup & sending to a background queue
202      * @fixme also detect short-form name for remote subscribees where not ambiguous
203      */
204
205     function onEndNoticeSave($notice)
206     {
207     }
208
209     /**
210      *
211      */
212
213     function onStartFindMentions($sender, $text, &$mentions)
214     {
215         preg_match_all('/(?:^|\s+)@((?:\w+\.)*\w+@(?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+)/',
216                        $text,
217                        $wmatches,
218                        PREG_OFFSET_CAPTURE);
219
220         foreach ($wmatches[1] as $wmatch) {
221
222             $webfinger = $wmatch[0];
223
224             $oprofile = Ostatus_profile::ensureWebfinger($webfinger);
225
226             if (!empty($oprofile)) {
227
228                 $profile = $oprofile->localProfile();
229
230                 $mentions[] = array('mentioned' => array($profile),
231                                     'text' => $wmatch[0],
232                                     'position' => $wmatch[1],
233                                     'url' => $profile->profileurl);
234             }
235         }
236
237         return true;
238     }
239
240     /**
241      * Make sure necessary tables are filled out.
242      */
243     function onCheckSchema() {
244         $schema = Schema::get();
245         $schema->ensureTable('ostatus_profile', Ostatus_profile::schemaDef());
246         $schema->ensureTable('ostatus_source', Ostatus_source::schemaDef());
247         $schema->ensureTable('feedsub', FeedSub::schemaDef());
248         $schema->ensureTable('hubsub', HubSub::schemaDef());
249         $schema->ensureTable('magicsig', Magicsig::schemaDef());
250         return true;
251     }
252
253     function onEndShowStatusNetStyles($action) {
254         $action->cssLink(common_path('plugins/OStatus/theme/base/css/ostatus.css'));
255         return true;
256     }
257
258     function onEndShowStatusNetScripts($action) {
259         $action->script(common_path('plugins/OStatus/js/ostatus.js'));
260         return true;
261     }
262
263     /**
264      * Override the "from ostatus" bit in notice lists to link to the
265      * original post and show the domain it came from.
266      *
267      * @param Notice in $notice
268      * @param string out &$name
269      * @param string out &$url
270      * @param string out &$title
271      * @return mixed hook return code
272      */
273     function onStartNoticeSourceLink($notice, &$name, &$url, &$title)
274     {
275         if ($notice->source == 'ostatus') {
276             if ($notice->url) {
277                 $bits = parse_url($notice->url);
278                 $domain = $bits['host'];
279                 if (substr($domain, 0, 4) == 'www.') {
280                     $name = substr($domain, 4);
281                 } else {
282                     $name = $domain;
283                 }
284
285                 $url = $notice->url;
286                 $title = sprintf(_m("Sent from %s via OStatus"), $domain);
287                 return false;
288             }
289         }
290     }
291
292     /**
293      * Send incoming PuSH feeds for OStatus endpoints in for processing.
294      *
295      * @param FeedSub $feedsub
296      * @param DOMDocument $feed
297      * @return mixed hook return code
298      */
299     function onStartFeedSubReceive($feedsub, $feed)
300     {
301         $oprofile = Ostatus_profile::staticGet('feeduri', $feedsub->uri);
302         if ($oprofile) {
303             $oprofile->processFeed($feed, 'push');
304         } else {
305             common_log(LOG_DEBUG, "No ostatus profile for incoming feed $feedsub->uri");
306         }
307     }
308
309     /**
310      * When about to subscribe to a remote user, start a server-to-server
311      * PuSH subscription if needed. If we can't establish that, abort.
312      *
313      * @fixme If something else aborts later, we could end up with a stray
314      *        PuSH subscription. This is relatively harmless, though.
315      *
316      * @param Profile $subscriber
317      * @param Profile $other
318      *
319      * @return hook return code
320      *
321      * @throws Exception
322      */
323     function onStartSubscribe($subscriber, $other)
324     {
325         $user = User::staticGet('id', $subscriber->id);
326
327         if (empty($user)) {
328             return true;
329         }
330
331         $oprofile = Ostatus_profile::staticGet('profile_id', $other->id);
332
333         if (empty($oprofile)) {
334             return true;
335         }
336
337         if (!$oprofile->subscribe()) {
338             throw new Exception(_m('Could not set up remote subscription.'));
339         }
340     }
341
342     /**
343      * Having established a remote subscription, send a notification to the
344      * remote OStatus profile's endpoint.
345      *
346      * @param Profile $subscriber
347      * @param Profile $other
348      *
349      * @return hook return code
350      *
351      * @throws Exception
352      */
353     function onEndSubscribe($subscriber, $other)
354     {
355         $user = User::staticGet('id', $subscriber->id);
356
357         if (empty($user)) {
358             return true;
359         }
360
361         $oprofile = Ostatus_profile::staticGet('profile_id', $other->id);
362
363         if (empty($oprofile)) {
364             return true;
365         }
366
367         $act = new Activity();
368
369         $act->verb = ActivityVerb::FOLLOW;
370
371         $act->id   = TagURI::mint('follow:%d:%d:%s',
372                                   $subscriber->id,
373                                   $other->id,
374                                   common_date_iso8601(time()));
375
376         $act->time    = time();
377         $act->title   = _("Follow");
378         $act->content = sprintf(_("%s is now following %s."),
379                                $subscriber->getBestName(),
380                                $other->getBestName());
381
382         $act->actor   = ActivityObject::fromProfile($subscriber);
383         $act->object  = ActivityObject::fromProfile($other);
384
385         $oprofile->notifyActivity($act);
386
387         return true;
388     }
389
390     /**
391      * Notify remote server and garbage collect unused feeds on unsubscribe.
392      * @fixme send these operations to background queues
393      *
394      * @param User $user
395      * @param Profile $other
396      * @return hook return value
397      */
398     function onEndUnsubscribe($profile, $other)
399     {
400         $user = User::staticGet('id', $profile->id);
401
402         if (empty($user)) {
403             return true;
404         }
405
406         $oprofile = Ostatus_profile::staticGet('profile_id', $other->id);
407
408         if (empty($oprofile)) {
409             return true;
410         }
411
412         // Drop the PuSH subscription if there are no other subscribers.
413         $oprofile->garbageCollect();
414
415         $act = new Activity();
416
417         $act->verb = ActivityVerb::UNFOLLOW;
418
419         $act->id   = TagURI::mint('unfollow:%d:%d:%s',
420                                   $profile->id,
421                                   $other->id,
422                                   common_date_iso8601(time()));
423
424         $act->time    = time();
425         $act->title   = _("Unfollow");
426         $act->content = sprintf(_("%s stopped following %s."),
427                                $profile->getBestName(),
428                                $other->getBestName());
429
430         $act->actor   = ActivityObject::fromProfile($profile);
431         $act->object  = ActivityObject::fromProfile($other);
432
433         $oprofile->notifyActivity($act);
434
435         return true;
436     }
437
438     /**
439      * When one of our local users tries to join a remote group,
440      * notify the remote server. If the notification is rejected,
441      * deny the join.
442      *
443      * @param User_group $group
444      * @param User $user
445      *
446      * @return mixed hook return value
447      */
448
449     function onStartJoinGroup($group, $user)
450     {
451         $oprofile = Ostatus_profile::staticGet('group_id', $group->id);
452         if ($oprofile) {
453             if (!$oprofile->subscribe()) {
454                 throw new Exception(_m('Could not set up remote group membership.'));
455             }
456
457             $member = Profile::staticGet($user->id);
458
459             $act = new Activity();
460             $act->id = TagURI::mint('join:%d:%d:%s',
461                                     $member->id,
462                                     $group->id,
463                                     common_date_iso8601(time()));
464
465             $act->actor = ActivityObject::fromProfile($member);
466             $act->verb = ActivityVerb::JOIN;
467             $act->object = $oprofile->asActivityObject();
468
469             $act->time = time();
470             $act->title = _m("Join");
471             $act->content = sprintf(_m("%s has joined group %s."),
472                                     $member->getBestName(),
473                                     $oprofile->getBestName());
474
475             if ($oprofile->notifyActivity($act)) {
476                 return true;
477             } else {
478                 $oprofile->garbageCollect();
479                 throw new Exception(_m("Failed joining remote group."));
480             }
481         }
482     }
483
484     /**
485      * When one of our local users leaves a remote group, notify the remote
486      * server.
487      *
488      * @fixme Might be good to schedule a resend of the leave notification
489      * if it failed due to a transitory error. We've canceled the local
490      * membership already anyway, but if the remote server comes back up
491      * it'll be left with a stray membership record.
492      *
493      * @param User_group $group
494      * @param User $user
495      *
496      * @return mixed hook return value
497      */
498
499     function onEndLeaveGroup($group, $user)
500     {
501         $oprofile = Ostatus_profile::staticGet('group_id', $group->id);
502         if ($oprofile) {
503             // Drop the PuSH subscription if there are no other subscribers.
504             $oprofile->garbageCollect();
505
506
507             $member = Profile::staticGet($user->id);
508
509             $act = new Activity();
510             $act->id = TagURI::mint('leave:%d:%d:%s',
511                                     $member->id,
512                                     $group->id,
513                                     common_date_iso8601(time()));
514
515             $act->actor = ActivityObject::fromProfile($member);
516             $act->verb = ActivityVerb::LEAVE;
517             $act->object = $oprofile->asActivityObject();
518
519             $act->time = time();
520             $act->title = _m("Leave");
521             $act->content = sprintf(_m("%s has left group %s."),
522                                     $member->getBestName(),
523                                     $oprofile->getBestName());
524
525             $oprofile->notifyActivity($act);
526         }
527     }
528
529     /**
530      * Notify remote users when their notices get favorited.
531      *
532      * @param Profile or User $profile of local user doing the faving
533      * @param Notice $notice being favored
534      * @return hook return value
535      */
536
537     function onEndFavorNotice(Profile $profile, Notice $notice)
538     {
539         $user = User::staticGet('id', $profile->id);
540
541         if (empty($user)) {
542             return true;
543         }
544
545         $oprofile = Ostatus_profile::staticGet('profile_id', $notice->profile_id);
546
547         if (empty($oprofile)) {
548             return true;
549         }
550
551         $act = new Activity();
552
553         $act->verb = ActivityVerb::FAVORITE;
554         $act->id   = TagURI::mint('favor:%d:%d:%s',
555                                   $profile->id,
556                                   $notice->id,
557                                   common_date_iso8601(time()));
558
559         $act->time    = time();
560         $act->title   = _("Favor");
561         $act->content = sprintf(_("%s marked notice %s as a favorite."),
562                                $profile->getBestName(),
563                                $notice->uri);
564
565         $act->actor   = ActivityObject::fromProfile($profile);
566         $act->object  = ActivityObject::fromNotice($notice);
567
568         $oprofile->notifyActivity($act);
569
570         return true;
571     }
572
573     /**
574      * Notify remote users when their notices get de-favorited.
575      *
576      * @param Profile $profile Profile person doing the de-faving
577      * @param Notice  $notice  Notice being favored
578      *
579      * @return hook return value
580      */
581
582     function onEndDisfavorNotice(Profile $profile, Notice $notice)
583     {
584         $user = User::staticGet('id', $profile->id);
585
586         if (empty($user)) {
587             return true;
588         }
589
590         $oprofile = Ostatus_profile::staticGet('profile_id', $notice->profile_id);
591
592         if (empty($oprofile)) {
593             return true;
594         }
595
596         $act = new Activity();
597
598         $act->verb = ActivityVerb::UNFAVORITE;
599         $act->id   = TagURI::mint('disfavor:%d:%d:%s',
600                                   $profile->id,
601                                   $notice->id,
602                                   common_date_iso8601(time()));
603         $act->time    = time();
604         $act->title   = _("Disfavor");
605         $act->content = sprintf(_("%s marked notice %s as no longer a favorite."),
606                                $profile->getBestName(),
607                                $notice->uri);
608
609         $act->actor   = ActivityObject::fromProfile($profile);
610         $act->object  = ActivityObject::fromNotice($notice);
611
612         $oprofile->notifyActivity($act);
613
614         return true;
615     }
616
617     function onStartGetProfileUri($profile, &$uri)
618     {
619         $oprofile = Ostatus_profile::staticGet('profile_id', $profile->id);
620         if (!empty($oprofile)) {
621             $uri = $oprofile->uri;
622             return false;
623         }
624         return true;
625     }
626
627     function onStartUserGroupHomeUrl($group, &$url)
628     {
629         return $this->onStartUserGroupPermalink($group, &$url);
630     }
631
632     function onStartUserGroupPermalink($group, &$url)
633     {
634         $oprofile = Ostatus_profile::staticGet('group_id', $group->id);
635         if ($oprofile) {
636             // @fixme this should probably be in the user_group table
637             // @fixme this uri not guaranteed to be a profile page
638             $url = $oprofile->uri;
639             return false;
640         }
641     }
642
643     function onStartShowSubscriptionsContent($action)
644     {
645         $user = common_current_user();
646         if ($user && ($user->id == $action->profile->id)) {
647             $action->elementStart('div', 'entity_actions');
648             $action->elementStart('p', array('id' => 'entity_remote_subscribe',
649                                              'class' => 'entity_subscribe'));
650             $action->element('a', array('href' => common_local_url('ostatussub'),
651                                         'class' => 'entity_remote_subscribe')
652                                 , _m('Subscribe to remote user'));
653             $action->elementEnd('p');
654             $action->elementEnd('div');
655         }
656
657         return true;
658     }
659
660     /**
661      * Ping remote profiles with updates to this profile.
662      * Salmon pings are queued for background processing.
663      */
664     function onEndBroadcastProfile(Profile $profile)
665     {
666         $user = User::staticGet('id', $profile->id);
667
668         // Find foreign accounts I'm subscribed to that support Salmon pings.
669         //
670         // @fixme we could run updates through the PuSH feed too,
671         // in which case we can skip Salmon pings to folks who
672         // are also subscribed to me.
673         $sql = "SELECT * FROM ostatus_profile " .
674                "WHERE profile_id IN " .
675                "(SELECT subscribed FROM subscription WHERE subscriber=%d) " .
676                "OR group_id IN " .
677                "(SELECT group_id FROM group_member WHERE profile_id=%d)";
678         $oprofile = new Ostatus_profile();
679         $oprofile->query(sprintf($sql, $profile->id, $profile->id));
680
681         if ($oprofile->N == 0) {
682             common_log(LOG_DEBUG, "No OStatus remote subscribees for $profile->nickname");
683             return true;
684         }
685
686         $act = new Activity();
687
688         $act->verb = ActivityVerb::UPDATE_PROFILE;
689         $act->id   = TagURI::mint('update-profile:%d:%s',
690                                   $profile->id,
691                                   common_date_iso8601(time()));
692         $act->time    = time();
693         $act->title   = _m("Profile update");
694         $act->content = sprintf(_m("%s has updated their profile page."),
695                                $profile->getBestName());
696
697         $act->actor   = ActivityObject::fromProfile($profile);
698         $act->object  = $act->actor;
699
700         while ($oprofile->fetch()) {
701             $oprofile->notifyDeferred($act);
702         }
703
704         return true;
705     }
706 }