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('GNUSOCIAL')) { exit(1); }
38 * @author Evan Prodromou <evan@status.net>
39 * @copyright 2010 StatusNet, Inc.
40 * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
41 * @link http://status.net/
43 class ActivityImporter extends QueueHandler
45 private $trusted = false;
54 function handle($data)
56 list($user, $author, $activity, $trusted) = $data;
58 $this->trusted = $trusted;
63 if (Event::handle('StartImportActivity',
64 array($user, $author, $activity, $trusted, &$done))) {
65 switch ($activity->verb) {
66 case ActivityVerb::FOLLOW:
67 $this->subscribeProfile($user, $author, $activity);
69 case ActivityVerb::JOIN:
70 $this->joinGroup($user, $activity);
72 case ActivityVerb::POST:
73 $this->postNote($user, $author, $activity);
76 // TRANS: Client exception thrown when using an unknown verb for the activity importer.
77 throw new ClientException(sprintf(_("Unknown verb: \"%s\"."),$activity->verb));
79 Event::handle('EndImportActivity',
80 array($user, $author, $activity, $trusted));
83 } catch (Exception $e) {
84 common_log(LOG_ERR, $e->getMessage());
90 function subscribeProfile($user, $author, $activity)
92 $profile = $user->getProfile();
94 if ($activity->objects[0]->id == $author->id) {
95 if (!$this->trusted) {
96 // TRANS: Client exception thrown when trying to force a subscription for an untrusted user.
97 throw new ClientException(_('Cannot force subscription for untrusted user.'));
100 $other = $activity->actor;
101 $otherUser = User::getKV('uri', $other->id);
103 if (!$otherUser instanceof User) {
104 // TRANS: Client exception thrown when trying to force a remote user to subscribe.
105 throw new Exception(_('Cannot force remote user to subscribe.'));
108 $otherProfile = $otherUser->getProfile();
110 // XXX: don't do this for untrusted input!
112 Subscription::start($otherProfile, $profile);
113 } else if (empty($activity->actor)
114 || $activity->actor->id == $author->id) {
116 $other = $activity->objects[0];
119 $otherProfile = Profile::fromUri($other->id);
120 // TRANS: Client exception thrown when trying to subscribe to an unknown profile.
121 } catch (UnknownUriException $e) {
122 // Let's convert it to a client exception instead of server.
123 throw new ClientException(_('Unknown profile.'));
126 Subscription::start($profile, $otherProfile);
128 // TRANS: Client exception thrown when trying to import an event not related to the importing user.
129 throw new Exception(_('This activity seems unrelated to our user.'));
133 function joinGroup($user, $activity)
135 // XXX: check that actor == subject
137 $uri = $activity->objects[0]->id;
139 $group = User_group::getKV('uri', $uri);
141 if (!$group instanceof User_group) {
142 $oprofile = Ostatus_profile::ensureActivityObjectProfile($activity->objects[0]);
143 if (!$oprofile->isGroup()) {
144 // TRANS: Client exception thrown when trying to join a remote group that is not a group.
145 throw new ClientException(_('Remote profile is not a group!'));
147 $group = $oprofile->localGroup();
150 assert(!empty($group));
152 if ($user->isMember($group)) {
153 // TRANS: Client exception thrown when trying to join a group the importing user is already a member of.
154 throw new ClientException(_("User is already a member of this group."));
157 $user->joinGroup($group);
160 // XXX: largely cadged from Ostatus_profile::processNote()
162 function postNote($user, $author, $activity)
164 $note = $activity->objects[0];
166 $sourceUri = $note->id;
168 $notice = Notice::getKV('uri', $sourceUri);
170 if ($notice instanceof Notice) {
172 common_log(LOG_INFO, "Notice {$sourceUri} already exists.");
174 if ($this->trusted) {
176 $profile = $notice->getProfile();
178 $uri = $profile->getUri();
180 if ($uri === $author->id) {
181 common_log(LOG_INFO, sprintf('Updating notice author from %s to %s', $author->id, $user->getUri()));
182 $orig = clone($notice);
183 $notice->profile_id = $user->id;
184 $notice->update($orig);
187 // TRANS: Client exception thrown when trying to import a notice by another user.
188 // TRANS: %1$s is the source URI of the notice, %2$s is the URI of the author.
189 throw new ClientException(sprintf(_('Already know about notice %1$s and '.
190 ' it has a different author %2$s.'),
194 // TRANS: Client exception thrown when trying to overwrite the author information for a non-trusted user during import.
195 throw new ClientException(_('Not overwriting author info for non-trusted user.'));
199 // Use summary as fallback for content
201 if (!empty($note->content)) {
202 $sourceContent = $note->content;
203 } else if (!empty($note->summary)) {
204 $sourceContent = $note->summary;
205 } else if (!empty($note->title)) {
206 $sourceContent = $note->title;
208 // @fixme fetch from $sourceUrl?
209 // TRANS: Client exception thrown when trying to import a notice without content.
210 // TRANS: %s is the notice URI.
211 throw new ClientException(sprintf(_('No content for notice %s.'),$sourceUri));
214 // Get (safe!) HTML and text versions of the content
216 $rendered = $this->purify($sourceContent);
217 $content = common_strip_html($rendered);
219 $shortened = $user->shortenLinks($content);
221 $options = array('is_local' => Notice::LOCAL_PUBLIC,
223 'rendered' => $rendered,
224 'replies' => array(),
228 'distribute' => false);
230 // Check for optional attributes...
232 if (!empty($activity->time)) {
233 $options['created'] = common_sql_date($activity->time);
236 if ($activity->context) {
237 // Any individual or group attn: targets?
239 list($options['groups'], $options['replies']) = $this->filterAttention($activity->context->attention);
241 // Maintain direct reply associations
242 // @fixme what about conversation ID?
243 if (!empty($activity->context->replyToID)) {
244 $orig = Notice::getKV('uri', $activity->context->replyToID);
245 if ($orig instanceof Notice) {
246 $options['reply_to'] = $orig->id;
250 $location = $activity->context->location;
253 $options['lat'] = $location->lat;
254 $options['lon'] = $location->lon;
255 if ($location->location_id) {
256 $options['location_ns'] = $location->location_ns;
257 $options['location_id'] = $location->location_id;
262 // Atom categories <-> hashtags
264 foreach ($activity->categories as $cat) {
266 $term = common_canonical_tag($cat->term);
268 $options['tags'][] = $term;
273 // Atom enclosures -> attachment URLs
274 foreach ($activity->enclosures as $href) {
275 // @fixme save these locally or....?
276 $options['urls'][] = $href;
279 common_log(LOG_INFO, "Saving notice {$options['uri']}");
281 $saved = Notice::saveNew($user->id,
283 'restore', // TODO: restore the actual source
289 protected function filterAttention(array $attn)
291 $groups = array(); // TODO: context->attention
292 $replies = array(); // TODO: context->attention
294 foreach ($attn as $recipient=>$type) {
296 // Is the recipient a local user?
298 $user = User::getKV('uri', $recipient);
300 if ($user instanceof User) {
301 // TODO: @fixme sender verification, spam etc?
302 $replies[] = $recipient;
306 // Is the recipient a remote group?
307 $oprofile = Ostatus_profile::ensureProfileURI($recipient);
309 if ($oprofile instanceof Ostatus_profile) {
310 if (!$oprofile->isGroup()) {
311 // may be canonicalized or something
312 $replies[] = $oprofile->uri;
317 // Is the recipient a local group?
318 // TODO: @fixme uri on user_group isn't reliable yet
319 // $group = User_group::getKV('uri', $recipient);
320 $id = OStatusPlugin::localGroupFromUrl($recipient);
323 $group = User_group::getKV('id', $id);
324 if ($group instanceof User_group) {
325 // Deliver to all members of this local group if allowed.
326 $profile = Profile::getKV('id', $recipient);
328 if (($profile instanceof Profile) && ($profile->isMember($group))) {
329 $groups[] = $group->id;
331 common_log(LOG_INFO, "Skipping reply to local group {$group->nickname} as sender {$profile->id} is not a member");
335 common_log(LOG_INFO, "Skipping reply to bogus group $recipient");
340 return array($groups, $replies);
344 function purify($content)
346 require_once INSTALLDIR.'/extlib/htmLawed/htmLawed.php';
348 $config = array('safe' => 1,
349 'deny_attribute' => 'id,style,on*');
351 return htmLawed($content, $config);