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/
48 class ActivityImporter extends QueueHandler
50 private $trusted = false;
60 function handle($data)
62 list($user, $author, $activity, $trusted) = $data;
64 $this->trusted = $trusted;
67 switch ($activity->verb) {
68 case ActivityVerb::FOLLOW:
69 $this->subscribeProfile($user, $author, $activity);
71 case ActivityVerb::JOIN:
72 $this->joinGroup($user, $activity);
74 case ActivityVerb::POST:
75 $this->postNote($user, $activity);
78 throw new Exception("Unknown verb: {$activity->verb}");
80 } catch (ClientException $ce) {
81 common_log(LOG_WARNING, $ce->getMessage());
83 } catch (ServerException $se) {
84 common_log(LOG_ERR, $ce->getMessage());
86 } catch (Exception $e) {
87 common_log(LOG_ERR, $ce->getMessage());
93 function subscribeProfile($user, $author, $activity)
95 $profile = $user->getProfile();
97 if ($activity->objects[0]->id == $author->id) {
98 $other = $activity->actor;
99 $otherUser = User::staticGet('uri', $other->id);
101 if (!empty($otherUser)) {
102 $otherProfile = $otherUser->getProfile();
104 throw new Exception("Can't force remote user to subscribe.");
107 // XXX: don't do this for untrusted input!
109 Subscription::start($otherProfile, $profile);
111 } else if (empty($activity->actor)
112 || $activity->actor->id == $author->id) {
114 $other = $activity->objects[0];
116 $otherProfile = Profile::fromUri($other->id);
118 if (empty($otherProfile)) {
119 throw new ClientException(_("Unknown profile."));
122 Subscription::start($profile, $otherProfile);
124 throw new Exception("This activity seems unrelated to our user.");
128 function joinGroup($user, $activity)
130 // XXX: check that actor == subject
132 $uri = $activity->objects[0]->id;
134 $group = User_group::staticGet('uri', $uri);
137 $oprofile = Ostatus_profile::ensureActivityObjectProfile($activity->objects[0]);
138 if (!$oprofile->isGroup()) {
139 throw new Exception("Remote profile is not a group!");
141 $group = $oprofile->localGroup();
144 assert(!empty($group));
146 if (Event::handle('StartJoinGroup', array($group, $user))) {
147 Group_member::join($group->id, $user->id);
148 Event::handle('EndJoinGroup', array($group, $user));
152 // XXX: largely cadged from Ostatus_profile::processNote()
154 function postNote($user, $activity)
156 $note = $activity->objects[0];
158 $sourceUri = $note->id;
160 $notice = Notice::staticGet('uri', $sourceUri);
162 if (!empty($notice)) {
164 $orig = clone($notice);
165 $notice->profile_id = $user->id;
166 $notice->update($orig);
170 // Use summary as fallback for content
172 if (!empty($note->content)) {
173 $sourceContent = $note->content;
174 } else if (!empty($note->summary)) {
175 $sourceContent = $note->summary;
176 } else if (!empty($note->title)) {
177 $sourceContent = $note->title;
179 // @fixme fetch from $sourceUrl?
180 // @todo i18n FIXME: use sprintf and add i18n.
181 throw new ClientException("No content for notice {$sourceUri}.");
184 // Get (safe!) HTML and text versions of the content
186 $rendered = $this->purify($sourceContent);
187 $content = html_entity_decode(strip_tags($rendered), ENT_QUOTES, 'UTF-8');
189 $shortened = $user->shortenLinks($content);
191 $options = array('is_local' => Notice::LOCAL_PUBLIC,
193 'rendered' => $rendered,
194 'replies' => array(),
199 // Check for optional attributes...
201 if (!empty($activity->time)) {
202 $options['created'] = common_sql_date($activity->time);
205 if ($activity->context) {
206 // Any individual or group attn: targets?
208 list($options['groups'], $options['replies']) = $this->filterAttention($activity->context->attention);
210 // Maintain direct reply associations
211 // @fixme what about conversation ID?
212 if (!empty($activity->context->replyToID)) {
213 $orig = Notice::staticGet('uri',
214 $activity->context->replyToID);
216 $options['reply_to'] = $orig->id;
220 $location = $activity->context->location;
223 $options['lat'] = $location->lat;
224 $options['lon'] = $location->lon;
225 if ($location->location_id) {
226 $options['location_ns'] = $location->location_ns;
227 $options['location_id'] = $location->location_id;
232 // Atom categories <-> hashtags
234 foreach ($activity->categories as $cat) {
236 $term = common_canonical_tag($cat->term);
238 $options['tags'][] = $term;
243 // Atom enclosures -> attachment URLs
244 foreach ($activity->enclosures as $href) {
245 // @fixme save these locally or....?
246 $options['urls'][] = $href;
249 $saved = Notice::saveNew($user->id,
251 'restore', // TODO: restore the actual source
257 function filterAttention($attn)
262 foreach (array_unique($attn) as $recipient) {
264 // Is the recipient a local user?
266 $user = User::staticGet('uri', $recipient);
269 // @fixme sender verification, spam etc?
270 $replies[] = $recipient;
274 // Is the recipient a remote group?
275 $oprofile = Ostatus_profile::ensureProfileURI($recipient);
278 if (!$oprofile->isGroup()) {
279 // may be canonicalized or something
280 $replies[] = $oprofile->uri;
285 // Is the recipient a local group?
286 // @fixme uri on user_group isn't reliable yet
287 // $group = User_group::staticGet('uri', $recipient);
288 $id = OStatusPlugin::localGroupFromUrl($recipient);
291 $group = User_group::staticGet('id', $id);
293 // Deliver to all members of this local group if allowed.
294 $profile = $sender->localProfile();
295 if ($profile->isMember($group)) {
296 $groups[] = $group->id;
298 common_log(LOG_INFO, "Skipping reply to local group {$group->nickname} as sender {$profile->id} is not a member");
302 common_log(LOG_INFO, "Skipping reply to bogus group $recipient");
307 return array($groups, $replies);
311 function purify($content)
313 $config = array('safe' => 1,
314 'deny_attribute' => 'id,style,on*');
315 return htmLawed($content, $config);