]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - lib/activityimporter.php
e936449c86bbc2848bc8ecd16c855a36f3d05357
[quix0rs-gnu-social.git] / lib / activityimporter.php
1 <?php
2 /**
3  * StatusNet - the distributed open-source microblogging tool
4  * Copyright (C) 2010, StatusNet, Inc.
5  *
6  * class to import activities as part of a user's timeline
7  * 
8  * PHP version 5
9  *
10  * This program is free software: you can redistribute it and/or modify
11  * it under the terms of the GNU Affero General Public License as published by
12  * the Free Software Foundation, either version 3 of the License, or
13  * (at your option) any later version.
14  *
15  * This program is distributed in the hope that it will be useful,
16  * but WITHOUT ANY WARRANTY; without even the implied warranty of
17  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18  * GNU Affero General Public License for more details.
19  *
20  * You should have received a copy of the GNU Affero General Public License
21  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
22  *
23  * @category  Cache
24  * @package   StatusNet
25  * @author    Evan Prodromou <evan@status.net>
26  * @copyright 2010 StatusNet, Inc.
27  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
28  * @link      http://status.net/
29  */
30
31 if (!defined('STATUSNET')) {
32     // This check helps protect against security problems;
33     // your code file can't be executed directly from the web.
34     exit(1);
35 }
36
37 /**
38  * Class comment
39  *
40  * @category  General
41  * @package   StatusNet
42  * @author    Evan Prodromou <evan@status.net>
43  * @copyright 2010 StatusNet, Inc.
44  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
45  * @link      http://status.net/
46  */
47
48 class ActivityImporter extends QueueHandler
49 {
50     private $trusted = false;
51
52     /**
53      * Function comment
54      *
55      * @param
56      *
57      * @return
58      */
59
60     function handle($data)
61     {
62         list($user, $author, $activity, $trusted) = $data;
63
64         $this->trusted = $trusted;
65
66         try {
67             switch ($activity->verb) {
68             case ActivityVerb::FOLLOW:
69                 $this->subscribeProfile($user, $author, $activity);
70                 break;
71             case ActivityVerb::JOIN:
72                 $this->joinGroup($user, $activity);
73                 break;
74             case ActivityVerb::POST:
75                 $this->postNote($user, $activity);
76                 break;
77             default:
78                 throw new Exception("Unknown verb: {$activity->verb}");
79             }
80         } catch (ClientException $ce) {
81             common_log(LOG_WARNING, $ce->getMessage());
82             return true;
83         } catch (ServerException $se) {
84             common_log(LOG_ERR, $se->getMessage());
85             return false;
86         } catch (Exception $e) {
87             common_log(LOG_ERR, $e->getMessage());
88             return false;
89         }
90         return true;
91     }
92     
93     function subscribeProfile($user, $author, $activity)
94     {
95         $profile = $user->getProfile();
96
97         if ($activity->objects[0]->id == $author->id) {
98
99             if (!$this->trusted) {
100                 throw new ClientException(_("Can't force subscription for untrusted user."));
101             }
102
103             $other = $activity->actor;
104             $otherUser = User::staticGet('uri', $other->id);
105             
106             if (!empty($otherUser)) {
107                 $otherProfile = $otherUser->getProfile();
108             } else {
109                 throw new Exception("Can't force remote user to subscribe.");
110             }
111
112             // XXX: don't do this for untrusted input!
113
114             Subscription::start($otherProfile, $profile);
115
116         } else if (empty($activity->actor) 
117                    || $activity->actor->id == $author->id) {
118
119             $other = $activity->objects[0];
120
121             $otherProfile = Profile::fromUri($other->id);
122
123             if (empty($otherProfile)) {
124                 throw new ClientException(_("Unknown profile."));
125             }
126
127             Subscription::start($profile, $otherProfile);
128         } else {
129             throw new Exception("This activity seems unrelated to our user.");
130         }
131     }
132
133     function joinGroup($user, $activity)
134     {
135         // XXX: check that actor == subject
136
137         $uri = $activity->objects[0]->id;
138
139         $group = User_group::staticGet('uri', $uri);
140
141         if (empty($group)) {
142             $oprofile = Ostatus_profile::ensureActivityObjectProfile($activity->objects[0]);
143             if (!$oprofile->isGroup()) {
144                 throw new Exception("Remote profile is not a group!");
145             }
146             $group = $oprofile->localGroup();
147         }
148
149         assert(!empty($group));
150
151         if (Event::handle('StartJoinGroup', array($group, $user))) {
152             Group_member::join($group->id, $user->id);
153             Event::handle('EndJoinGroup', array($group, $user));
154         }
155     }
156
157     // XXX: largely cadged from Ostatus_profile::processNote()
158
159     function postNote($user, $activity)
160     {
161         $note = $activity->objects[0];
162
163         $sourceUri = $note->id;
164
165         $notice = Notice::staticGet('uri', $sourceUri);
166
167         if (!empty($notice)) {
168             // This is weird.
169             $orig = clone($notice);
170             $notice->profile_id = $user->id;
171             $notice->update($orig);
172             return;
173         }
174
175         // Use summary as fallback for content
176
177         if (!empty($note->content)) {
178             $sourceContent = $note->content;
179         } else if (!empty($note->summary)) {
180             $sourceContent = $note->summary;
181         } else if (!empty($note->title)) {
182             $sourceContent = $note->title;
183         } else {
184             // @fixme fetch from $sourceUrl?
185             // @todo i18n FIXME: use sprintf and add i18n.
186             throw new ClientException("No content for notice {$sourceUri}.");
187         }
188
189         // Get (safe!) HTML and text versions of the content
190
191         $rendered = $this->purify($sourceContent);
192         $content = html_entity_decode(strip_tags($rendered), ENT_QUOTES, 'UTF-8');
193
194         $shortened = $user->shortenLinks($content);
195
196         $options = array('is_local' => Notice::LOCAL_PUBLIC,
197                          'uri' => $sourceUri,
198                          'rendered' => $rendered,
199                          'replies' => array(),
200                          'groups' => array(),
201                          'tags' => array(),
202                          'urls' => array());
203
204         // Check for optional attributes...
205
206         if (!empty($activity->time)) {
207             $options['created'] = common_sql_date($activity->time);
208         }
209
210         if ($activity->context) {
211             // Any individual or group attn: targets?
212
213             list($options['groups'], $options['replies']) = $this->filterAttention($activity->context->attention);
214
215             // Maintain direct reply associations
216             // @fixme what about conversation ID?
217             if (!empty($activity->context->replyToID)) {
218                 $orig = Notice::staticGet('uri',
219                                           $activity->context->replyToID);
220                 if (!empty($orig)) {
221                     $options['reply_to'] = $orig->id;
222                 }
223             }
224
225             $location = $activity->context->location;
226
227             if ($location) {
228                 $options['lat'] = $location->lat;
229                 $options['lon'] = $location->lon;
230                 if ($location->location_id) {
231                     $options['location_ns'] = $location->location_ns;
232                     $options['location_id'] = $location->location_id;
233                 }
234             }
235         }
236
237         // Atom categories <-> hashtags
238
239         foreach ($activity->categories as $cat) {
240             if ($cat->term) {
241                 $term = common_canonical_tag($cat->term);
242                 if ($term) {
243                     $options['tags'][] = $term;
244                 }
245             }
246         }
247
248         // Atom enclosures -> attachment URLs
249         foreach ($activity->enclosures as $href) {
250             // @fixme save these locally or....?
251             $options['urls'][] = $href;
252         }
253
254         $saved = Notice::saveNew($user->id,
255                                  $content,
256                                  'restore', // TODO: restore the actual source
257                                  $options);
258
259         return $saved;
260     }
261
262     function filterAttention($attn)
263     {
264         $groups = array();
265         $replies = array();
266
267         foreach (array_unique($attn) as $recipient) {
268
269             // Is the recipient a local user?
270
271             $user = User::staticGet('uri', $recipient);
272
273             if ($user) {
274                 // @fixme sender verification, spam etc?
275                 $replies[] = $recipient;
276                 continue;
277             }
278
279             // Is the recipient a remote group?
280             $oprofile = Ostatus_profile::ensureProfileURI($recipient);
281
282             if ($oprofile) {
283                 if (!$oprofile->isGroup()) {
284                     // may be canonicalized or something
285                     $replies[] = $oprofile->uri;
286                 }
287                 continue;
288             }
289
290             // Is the recipient a local group?
291             // @fixme uri on user_group isn't reliable yet
292             // $group = User_group::staticGet('uri', $recipient);
293             $id = OStatusPlugin::localGroupFromUrl($recipient);
294
295             if ($id) {
296                 $group = User_group::staticGet('id', $id);
297                 if ($group) {
298                     // Deliver to all members of this local group if allowed.
299                     $profile = $sender->localProfile();
300                     if ($profile->isMember($group)) {
301                         $groups[] = $group->id;
302                     } else {
303                         common_log(LOG_INFO, "Skipping reply to local group {$group->nickname} as sender {$profile->id} is not a member");
304                     }
305                     continue;
306                 } else {
307                     common_log(LOG_INFO, "Skipping reply to bogus group $recipient");
308                 }
309             }
310         }
311
312         return array($groups, $replies);
313     }
314  
315
316     function purify($content)
317     {
318         $config = array('safe' => 1,
319                         'deny_attribute' => 'id,style,on*');
320         return htmLawed($content, $config);
321     }
322 }