3 * StatusNet - the distributed open-source microblogging tool
4 * Copyright (C) 2010, StatusNet, Inc.
6 * class to import activities as part of a user's timeline
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.
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.
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/>.
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/
31 if (!defined('STATUSNET')) {
32 // This check helps protect against security problems;
33 // your code file can't be executed directly from the web.
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/
47 class ActivityImporter extends QueueHandler
49 private $trusted = false;
58 function handle($data)
60 list($user, $author, $activity, $trusted) = $data;
62 $this->trusted = $trusted;
66 if (Event::handle('StartImportActivity',
67 array($user, $author, $activity, $trusted, &$done))) {
69 switch ($activity->verb) {
70 case ActivityVerb::FOLLOW:
71 $this->subscribeProfile($user, $author, $activity);
73 case ActivityVerb::JOIN:
74 $this->joinGroup($user, $activity);
76 case ActivityVerb::POST:
77 $this->postNote($user, $author, $activity);
80 // TRANS: Client exception thrown when using an unknown verb for the activity importer.
81 throw new ClientException(sprintf(_("Unknown verb: \"%s\"."),$activity->verb));
83 Event::handle('EndImportActivity',
84 array($user, $author, $activity, $trusted));
86 } catch (ClientException $ce) {
87 common_log(LOG_WARNING, $ce->getMessage());
89 } catch (ServerException $se) {
90 common_log(LOG_ERR, $se->getMessage());
92 } catch (Exception $e) {
93 common_log(LOG_ERR, $e->getMessage());
100 function subscribeProfile($user, $author, $activity)
102 $profile = $user->getProfile();
104 if ($activity->objects[0]->id == $author->id) {
105 if (!$this->trusted) {
106 // TRANS: Client exception thrown when trying to force a subscription for an untrusted user.
107 throw new ClientException(_("Cannot force subscription for untrusted user."));
110 $other = $activity->actor;
111 $otherUser = User::staticGet('uri', $other->id);
113 if (!empty($otherUser)) {
114 $otherProfile = $otherUser->getProfile();
116 // TRANS: Client exception thrown when trying to for a remote user to subscribe.
117 throw new Exception(_("Cannot force remote user to subscribe."));
120 // XXX: don't do this for untrusted input!
122 Subscription::start($otherProfile, $profile);
123 } else if (empty($activity->actor)
124 || $activity->actor->id == $author->id) {
126 $other = $activity->objects[0];
128 $otherProfile = Profile::fromUri($other->id);
130 if (empty($otherProfile)) {
131 // TRANS: Client exception thrown when trying to subscribe to an unknown profile.
132 throw new ClientException(_("Unknown profile."));
135 Subscription::start($profile, $otherProfile);
137 // TRANS: Client exception thrown when trying to import an event not related to the importing user.
138 throw new Exception(_("This activity seems unrelated to our user."));
142 function joinGroup($user, $activity)
144 // XXX: check that actor == subject
146 $uri = $activity->objects[0]->id;
148 $group = User_group::staticGet('uri', $uri);
151 $oprofile = Ostatus_profile::ensureActivityObjectProfile($activity->objects[0]);
152 if (!$oprofile->isGroup()) {
153 // TRANS: Client exception thrown when trying to join a remote group that is not a group.
154 throw new ClientException(_("Remote profile is not a group!"));
156 $group = $oprofile->localGroup();
159 assert(!empty($group));
161 if ($user->isMember($group)) {
162 // TRANS: Client exception thrown when trying to join a group the importing user is already a member of.
163 throw new ClientException(_("User is already a member of this group."));
166 if (Event::handle('StartJoinGroup', array($group, $user))) {
167 Group_member::join($group->id, $user->id);
168 Event::handle('EndJoinGroup', array($group, $user));
172 // XXX: largely cadged from Ostatus_profile::processNote()
174 function postNote($user, $author, $activity)
176 $note = $activity->objects[0];
178 $sourceUri = $note->id;
180 $notice = Notice::staticGet('uri', $sourceUri);
182 if (!empty($notice)) {
184 common_log(LOG_INFO, "Notice {$sourceUri} already exists.");
186 if ($this->trusted) {
188 $profile = $notice->getProfile();
190 $uri = $profile->getUri();
192 if ($uri == $author->id) {
193 common_log(LOG_INFO, "Updating notice author from $author->id to $user->uri");
194 $orig = clone($notice);
195 $notice->profile_id = $user->id;
196 $notice->update($orig);
199 // TRANS: Client exception thrown when trying to import a notice by another user.
200 // TRANS: %1$s is the source URI of the notice, %2$s is the URI of the author.
201 throw new ClientException(sprintf(_('Already know about notice %1$s and '.
202 ' it has a different author %2$s.'),
206 // TRANS: Client exception thrown when trying to overwrite the author information for a non-trusted user during import.
207 throw new ClientException(_("Not overwriting author info for non-trusted user."));
211 // Use summary as fallback for content
213 if (!empty($note->content)) {
214 $sourceContent = $note->content;
215 } else if (!empty($note->summary)) {
216 $sourceContent = $note->summary;
217 } else if (!empty($note->title)) {
218 $sourceContent = $note->title;
220 // @fixme fetch from $sourceUrl?
221 // TRANS: Client exception thrown when trying to import a notice without content.
222 // TRANS: %s is the notice URI.
223 throw new ClientException(sprintf(_("No content for notice %s."),$sourceUri));
226 // Get (safe!) HTML and text versions of the content
228 $rendered = $this->purify($sourceContent);
229 $content = html_entity_decode(strip_tags($rendered), ENT_QUOTES, 'UTF-8');
231 $shortened = $user->shortenLinks($content);
233 $options = array('is_local' => Notice::LOCAL_PUBLIC,
235 'rendered' => $rendered,
236 'replies' => array(),
240 'distribute' => false);
242 // Check for optional attributes...
244 if (!empty($activity->time)) {
245 $options['created'] = common_sql_date($activity->time);
248 if ($activity->context) {
249 // Any individual or group attn: targets?
251 list($options['groups'], $options['replies']) = $this->filterAttention($activity->context->attention);
253 // Maintain direct reply associations
254 // @fixme what about conversation ID?
255 if (!empty($activity->context->replyToID)) {
256 $orig = Notice::staticGet('uri',
257 $activity->context->replyToID);
259 $options['reply_to'] = $orig->id;
263 $location = $activity->context->location;
266 $options['lat'] = $location->lat;
267 $options['lon'] = $location->lon;
268 if ($location->location_id) {
269 $options['location_ns'] = $location->location_ns;
270 $options['location_id'] = $location->location_id;
275 // Atom categories <-> hashtags
277 foreach ($activity->categories as $cat) {
279 $term = common_canonical_tag($cat->term);
281 $options['tags'][] = $term;
286 // Atom enclosures -> attachment URLs
287 foreach ($activity->enclosures as $href) {
288 // @fixme save these locally or....?
289 $options['urls'][] = $href;
292 common_log(LOG_INFO, "Saving notice {$options['uri']}");
294 $saved = Notice::saveNew($user->id,
296 'restore', // TODO: restore the actual source
302 function filterAttention($attn)
307 foreach (array_unique($attn) as $recipient) {
309 // Is the recipient a local user?
311 $user = User::staticGet('uri', $recipient);
314 // @fixme sender verification, spam etc?
315 $replies[] = $recipient;
319 // Is the recipient a remote group?
320 $oprofile = Ostatus_profile::ensureProfileURI($recipient);
323 if (!$oprofile->isGroup()) {
324 // may be canonicalized or something
325 $replies[] = $oprofile->uri;
330 // Is the recipient a local group?
331 // @fixme uri on user_group isn't reliable yet
332 // $group = User_group::staticGet('uri', $recipient);
333 $id = OStatusPlugin::localGroupFromUrl($recipient);
336 $group = User_group::staticGet('id', $id);
338 // Deliver to all members of this local group if allowed.
339 $profile = $sender->localProfile();
340 if ($profile->isMember($group)) {
341 $groups[] = $group->id;
343 common_log(LOG_INFO, "Skipping reply to local group {$group->nickname} as sender {$profile->id} is not a member");
347 common_log(LOG_INFO, "Skipping reply to bogus group $recipient");
352 return array($groups, $replies);
356 function purify($content)
358 require_once INSTALLDIR.'/extlib/htmLawed/htmLawed.php';
360 $config = array('safe' => 1,
361 'deny_attribute' => 'id,style,on*');
363 return htmLawed($content, $config);