3 * StatusNet - the distributed open-source microblogging tool
4 * Copyright (C) 2010, StatusNet, Inc.
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.
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.
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/>.
21 * Basic client class for Yammer's OAuth/JSON API.
23 * @package YammerImportPlugin
24 * @author Brion Vibber <brion@status.net>
29 protected $users=array();
30 protected $groups=array();
31 protected $notices=array();
33 function __construct(SN_YammerClient $client)
35 $this->client = $client;
39 * Load or create an imported profile from Yammer data.
41 * @param object $item loaded JSON data for Yammer importer
44 function importUser($item)
46 $data = $this->prepUser($item);
48 $profileId = $this->findImportedUser($data['orig_id']);
50 return Profile::staticGet('id', $profileId);
52 $user = User::register($data['options']);
53 $profile = $user->getProfile();
55 $this->saveAvatar($data['avatar'], $profile);
56 } catch (Exception $e) {
57 common_log(LOG_ERROR, "Error importing Yammer avatar: " . $e->getMessage());
59 $this->recordImportedUser($data['orig_id'], $profile->id);
65 * Load or create an imported group from Yammer data.
67 * @param object $item loaded JSON data for Yammer importer
70 function importGroup($item)
72 $data = $this->prepGroup($item);
74 $groupId = $this->findImportedGroup($data['orig_id']);
76 return User_group::staticGet('id', $groupId);
78 $group = User_group::register($data['options']);
80 $this->saveAvatar($data['avatar'], $group);
81 } catch (Exception $e) {
82 common_log(LOG_ERROR, "Error importing Yammer avatar: " . $e->getMessage());
84 $this->recordImportedGroup($data['orig_id'], $group->id);
90 * Load or create an imported notice from Yammer data.
92 * @param object $item loaded JSON data for Yammer importer
95 function importNotice($item)
97 $data = $this->prepNotice($item);
99 $noticeId = $this->findImportedNotice($data['orig_id']);
101 return Notice::staticGet('id', $noticeId);
103 $content = $data['content'];
104 $user = User::staticGet($data['profile']);
106 // Fetch file attachments and add the URLs...
108 foreach ($data['attachments'] as $url) {
110 $upload = $this->saveAttachment($url, $user);
111 $content .= ' ' . $upload->shortUrl();
112 $uploads[] = $upload;
113 } catch (Exception $e) {
114 common_log(LOG_ERROR, "Error importing Yammer attachment: " . $e->getMessage());
118 // Here's the meat! Actually save the dang ol' notice.
119 $notice = Notice::saveNew($user->id,
124 // Save "likes" as favorites...
125 foreach ($data['faves'] as $nickname) {
126 $user = User::staticGet('nickname', $nickname);
128 Fave::addNew($user->getProfile(), $notice);
132 // And finally attach the upload records...
133 foreach ($uploads as $upload) {
134 $upload->attachToNotice($notice);
136 $this->recordImportedNotice($data['orig_id'], $notice->id);
142 * Pull relevant info out of a Yammer data record for a user import.
147 function prepUser($item)
149 if ($item['type'] != 'user') {
150 throw new Exception('Wrong item type sent to Yammer user import processing.');
153 $origId = $item['id'];
154 $origUrl = $item['url'];
156 // @fixme check username rules?
158 $options['nickname'] = $item['name'];
159 $options['fullname'] = trim($item['full_name']);
161 // Avatar... this will be the "_small" variant.
162 // Remove that (pre-extension) suffix to get the orig-size image.
163 $avatar = $item['mugshot_url'];
165 // The following info is only available in full data, not in the reference version.
167 // There can be extensive contact info, but for now we'll only pull the primary email.
168 if (isset($item['contact'])) {
169 foreach ($item['contact']['email_addresses'] as $addr) {
170 if ($addr['type'] == 'primary') {
171 $options['email'] = $addr['address'];
172 $options['email_confirmed'] = true;
178 // There can be multiple external URLs; for now pull the first one as home page.
179 if (isset($item['external_urls'])) {
180 foreach ($item['external_urls'] as $url) {
181 if (common_valid_http_url($url)) {
182 $options['homepage'] = $url;
188 // Combine a few bits into the bio...
190 if (!empty($item['job_title'])) {
191 $bio[] = $item['job_title'];
193 if (!empty($item['summary'])) {
194 $bio[] = $item['summary'];
196 if (!empty($item['expertise'])) {
197 $bio[] = _m('Expertise:') . ' ' . $item['expertise'];
199 $options['bio'] = implode("\n\n", $bio);
201 // Pull raw location string, may be lookupable
202 if (!empty($item['location'])) {
203 $options['location'] = $item['location'];
206 // Timezone is in format like 'Pacific Time (US & Canada)'
207 // We need to convert that to a zone id. :P
208 // @fixme timezone not yet supported at registration time :)
209 if (!empty($item['timezone'])) {
210 $tz = $this->timezone($item['timezone']);
212 $options['timezone'] = $tz;
216 return array('orig_id' => $origId,
217 'orig_url' => $origUrl,
219 'options' => $options);
224 * Pull relevant info out of a Yammer data record for a group import.
229 function prepGroup($item)
231 if ($item['type'] != 'group') {
232 throw new Exception('Wrong item type sent to Yammer group import processing.');
235 $origId = $item['id'];
236 $origUrl = $item['url'];
238 $privacy = $item['privacy']; // Warning! only public groups in SN so far
240 $options['nickname'] = $item['name'];
241 $options['fullname'] = $item['full_name'];
242 $options['description'] = $item['description'];
243 $options['created'] = $this->timestamp($item['created_at']);
245 $avatar = $item['mugshot_url']; // as with user profiles...
248 $options['mainpage'] = common_local_url('showgroup',
249 array('nickname' => $options['nickname']));
251 // @fixme what about admin user for the group?
252 // bio? homepage etc? aliases?
254 $options['local'] = true;
255 return array('orig_id' => $origId,
256 'orig_url' => $origUrl,
257 'options' => $options);
261 * Pull relevant info out of a Yammer data record for a notice import.
266 function prepNotice($item)
268 if (isset($item['type']) && $item['type'] != 'message') {
269 throw new Exception('Wrong item type sent to Yammer message import processing.');
272 $origId = $item['id'];
273 $origUrl = $item['url'];
275 $profile = $this->findImportedUser($item['sender_id']);
276 $content = $item['body']['plain'];
280 if ($item['replied_to_id']) {
281 $replyTo = $this->findImportedNotice($item['replied_to_id']);
283 $options['reply_to'] = $replyTo;
286 $options['created'] = $this->timestamp($item['created_at']);
288 if ($item['group_id']) {
289 $groupId = $this->findImportedGroup($item['group_id']);
291 $options['groups'] = array($groupId);
296 foreach ($item['liked_by']['names'] as $liker) {
297 // "permalink" is the username. wtf?
298 $faves[] = $liker['permalink'];
301 $attachments = array();
302 foreach ($item['attachments'] as $attach) {
303 if ($attach['type'] == 'image') {
304 $attachments[] = $attach['image']['url'];
306 common_log(LOG_WARNING, "Unrecognized Yammer attachment type: " . $attach['type']);
310 return array('orig_id' => $origId,
311 'orig_url' => $origUrl,
312 'profile' => $profile,
313 'content' => $content,
315 'options' => $options,
317 'attachments' => $attachments);
320 private function findImportedUser($origId)
322 if (isset($this->users[$origId])) {
323 return $this->users[$origId];
329 private function findImportedGroup($origId)
331 if (isset($this->groups[$origId])) {
332 return $this->groups[$origId];
338 private function findImportedNotice($origId)
340 if (isset($this->notices[$origId])) {
341 return $this->notices[$origId];
347 private function recordImportedUser($origId, $userId)
349 $this->users[$origId] = $userId;
352 private function recordImportedGroup($origId, $groupId)
354 $this->groups[$origId] = $groupId;
357 private function recordImportedNotice($origId, $noticeId)
359 $this->notices[$origId] = $noticeId;
363 * Normalize timestamp format.
367 private function timestamp($ts)
369 return common_sql_date(strtotime($ts));
372 private function timezone($tz)
375 $known = array('Pacific Time (US & Canada)' => 'America/Los_Angeles',
376 'Eastern Time (US & Canada)' => 'America/New_York');
377 if (array_key_exists($known, $tz)) {
385 * Download and update given avatar image
388 * @param mixed $dest either a Profile or User_group object
389 * @throws Exception in various failure cases
391 private function saveAvatar($url, $dest)
393 // Yammer API data mostly gives us the small variant.
394 // Try hitting the source image if we can!
395 // @fixme no guarantee of this URL scheme I think.
396 $url = preg_replace('/_small(\..*?)$/', '$1', $url);
398 if (!common_valid_http_url($url)) {
399 throw new ServerException(sprintf(_m("Invalid avatar URL %s."), $url));
402 // @fixme this should be better encapsulated
403 // ripped from oauthstore.php (for old OMB client)
404 $temp_filename = tempnam(sys_get_temp_dir(), 'listener_avatar');
405 if (!copy($url, $temp_filename)) {
406 throw new ServerException(sprintf(_m("Unable to fetch avatar from %s."), $url));
410 // @fixme should we be using different ids?
411 $imagefile = new ImageFile($id, $temp_filename);
412 $filename = Avatar::filename($id,
413 image_type_to_extension($imagefile->type),
416 rename($temp_filename, Avatar::path($filename));
417 // @fixme hardcoded chmod is lame, but seems to be necessary to
418 // keep from accidentally saving images from command-line (queues)
419 // that can't be read from web server, which causes hard-to-notice
420 // problems later on:
422 // http://status.net/open-source/issues/2663
423 chmod(Avatar::path($filename), 0644);
425 $dest->setOriginal($filename);
429 * Fetch an attachment from Yammer and save it into our system.
430 * Unlike avatars, the attachment URLs are guarded by authentication,
431 * so we need to run the HTTP hit through our OAuth API client.
437 * @throws Exception on low-level network or HTTP error
439 private function saveAttachment($url, User $user)
441 // Fetch the attachment...
442 // WARNING: file must fit in memory here :(
443 $body = $this->client->fetchUrl($url);
445 // Save to a temporary file and shove it into our file-attachment space...
447 fwrite($temp, $body);
449 $upload = MediaFile::fromFileHandle($temp, $user);
452 } catch (Exception $e) {