summaryrefslogtreecommitdiff
path: root/plugins/YammerImport/lib
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/YammerImport/lib')
-rw-r--r--plugins/YammerImport/lib/sn_yammerclient.php246
-rw-r--r--plugins/YammerImport/lib/yammerapikeyform.php112
-rw-r--r--plugins/YammerImport/lib/yammerauthinitform.php76
-rw-r--r--plugins/YammerImport/lib/yammerauthverifyform.php111
-rw-r--r--plugins/YammerImport/lib/yammerimporter.php456
-rw-r--r--plugins/YammerImport/lib/yammerprogressform.php128
-rw-r--r--plugins/YammerImport/lib/yammerqueuehandler.php58
-rw-r--r--plugins/YammerImport/lib/yammerrunner.php398
8 files changed, 1585 insertions, 0 deletions
diff --git a/plugins/YammerImport/lib/sn_yammerclient.php b/plugins/YammerImport/lib/sn_yammerclient.php
new file mode 100644
index 000000000..5da1cc5e7
--- /dev/null
+++ b/plugins/YammerImport/lib/sn_yammerclient.php
@@ -0,0 +1,246 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * Basic client class for Yammer's OAuth/JSON API.
+ *
+ * @package YammerImportPlugin
+ * @author Brion Vibber <brion@status.net>
+ */
+class SN_YammerClient
+{
+ protected $apiBase = "https://www.yammer.com";
+ protected $consumerKey, $consumerSecret;
+ protected $token, $tokenSecret, $verifier;
+
+ public function __construct($consumerKey, $consumerSecret, $token=null, $tokenSecret=null)
+ {
+ $this->consumerKey = $consumerKey;
+ $this->consumerSecret = $consumerSecret;
+ $this->token = $token;
+ $this->tokenSecret = $tokenSecret;
+ }
+
+ /**
+ * Make an HTTP GET request with OAuth headers and return an HTTPResponse
+ * with the returned body and codes.
+ *
+ * @param string $url
+ * @return HTTPResponse
+ *
+ * @throws Exception on low-level network error
+ */
+ protected function httpGet($url)
+ {
+ $headers = array('Authorization: ' . $this->authHeader());
+
+ $client = HTTPClient::start();
+ return $client->get($url, $headers);
+ }
+
+ /**
+ * Make an HTTP GET request with OAuth headers and return the response body
+ * on success.
+ *
+ * @param string $url
+ * @return string
+ *
+ * @throws Exception on low-level network or HTTP error
+ */
+ public function fetchUrl($url)
+ {
+ $response = $this->httpGet($url);
+ if ($response->isOk()) {
+ return $response->getBody();
+ } else {
+ throw new Exception("Yammer API returned HTTP code " . $response->getStatus() . ': ' . $response->getBody());
+ }
+ }
+
+ /**
+ * Make an HTTP hit with OAuth headers and return the response body on success.
+ *
+ * @param string $path URL chunk for the API method
+ * @param array $params
+ * @return string
+ *
+ * @throws Exception on low-level network or HTTP error
+ */
+ protected function fetchApi($path, $params=array())
+ {
+ $url = $this->apiBase . '/' . $path;
+ if ($params) {
+ $url .= '?' . http_build_query($params, null, '&');
+ }
+ return $this->fetchUrl($url);
+ }
+
+ /**
+ * Hit the main Yammer API point and decode returned JSON data.
+ *
+ * @param string $method
+ * @param array $params
+ * @return array from JSON data
+ *
+ * @throws Exception for HTTP error or bad JSON return
+ */
+ public function api($method, $params=array())
+ {
+ $body = $this->fetchApi("api/v1/$method.json", $params);
+ $data = json_decode($body, true);
+ if ($data === null) {
+ common_log(LOG_ERR, "Invalid JSON response from Yammer API: " . $body);
+ throw new Exception("Invalid JSON response from Yammer API");
+ }
+ return $data;
+ }
+
+ /**
+ * Build an Authorization header value from the keys we have available.
+ */
+ protected function authHeader()
+ {
+ // token
+ // token_secret
+ $params = array('realm' => '',
+ 'oauth_consumer_key' => $this->consumerKey,
+ 'oauth_signature_method' => 'PLAINTEXT',
+ 'oauth_timestamp' => time(),
+ 'oauth_nonce' => time(),
+ 'oauth_version' => '1.0');
+ if ($this->token) {
+ $params['oauth_token'] = $this->token;
+ }
+ if ($this->tokenSecret) {
+ $params['oauth_signature'] = $this->consumerSecret . '&' . $this->tokenSecret;
+ } else {
+ $params['oauth_signature'] = $this->consumerSecret . '&';
+ }
+ if ($this->verifier) {
+ $params['oauth_verifier'] = $this->verifier;
+ }
+ $parts = array_map(array($this, 'authHeaderChunk'), array_keys($params), array_values($params));
+ return 'OAuth ' . implode(', ', $parts);
+ }
+
+ /**
+ * Encode a key-value pair for use in an authentication header.
+ *
+ * @param string $key
+ * @param string $val
+ * @return string
+ */
+ protected function authHeaderChunk($key, $val)
+ {
+ return urlencode($key) . '="' . urlencode($val) . '"';
+ }
+
+ /**
+ * Ask the Yammer server for a request token, which can be passed on
+ * to authorizeUrl() for the user to start the authentication process.
+ *
+ * @return array of oauth return data; should contain nice things
+ */
+ public function requestToken()
+ {
+ if ($this->token || $this->tokenSecret) {
+ throw new Exception("Requesting a token, but already set up with a token");
+ }
+ $data = $this->fetchApi('oauth/request_token');
+ $arr = array();
+ parse_str($data, $arr);
+ return $arr;
+ }
+
+ /**
+ * Get a final access token from the verifier/PIN code provided to
+ * the user from Yammer's auth pages.
+ *
+ * @return array of oauth return data; should contain nice things
+ */
+ public function accessToken($verifier)
+ {
+ $this->verifier = $verifier;
+ $data = $this->fetchApi('oauth/access_token');
+ $this->verifier = null;
+ $arr = array();
+ parse_str($data, $arr);
+ return $arr;
+ }
+
+ /**
+ * Give the URL to send users to to authorize a new app setup.
+ *
+ * @param string $token as returned from accessToken()
+ * @return string URL
+ */
+ public function authorizeUrl($token)
+ {
+ return $this->apiBase . '/oauth/authorize?oauth_token=' . urlencode($token);
+ }
+
+ /**
+ * High-level API hit: fetch all messages in the network (up to 20 at a time).
+ * Return data is the full JSON array returned, including meta and references
+ * sections.
+ *
+ * The matching messages themselves will be in the 'messages' item within.
+ *
+ * @param array $options optional set of additional params for the request.
+ * @return array
+ *
+ * @throws Exception on low-level or HTTP error
+ */
+ public function messages($params=array())
+ {
+ return $this->api('messages', $params);
+ }
+
+ /**
+ * High-level API hit: fetch all users in the network (up to 50 at a time).
+ * Return data is the full JSON array returned, listing user items.
+ *
+ * The matching messages themselves will be in the 'users' item within.
+ *
+ * @param array $options optional set of additional params for the request.
+ * @return array of JSON-sourced user data arrays
+ *
+ * @throws Exception on low-level or HTTP error
+ */
+ public function users($params=array())
+ {
+ return $this->api('users', $params);
+ }
+
+ /**
+ * High-level API hit: fetch all groups in the network (up to 20 at a time).
+ * Return data is the full JSON array returned, listing user items.
+ *
+ * The matching messages themselves will be in the 'users' item within.
+ *
+ * @param array $options optional set of additional params for the request.
+ * @return array of JSON-sourced user data arrays
+ *
+ * @throws Exception on low-level or HTTP error
+ */
+ public function groups($params=array())
+ {
+ return $this->api('groups', $params);
+ }
+}
diff --git a/plugins/YammerImport/lib/yammerapikeyform.php b/plugins/YammerImport/lib/yammerapikeyform.php
new file mode 100644
index 000000000..b2acec4ed
--- /dev/null
+++ b/plugins/YammerImport/lib/yammerapikeyform.php
@@ -0,0 +1,112 @@
+<?php
+
+class YammerApikeyForm extends Form
+{
+ private $runner;
+
+ function __construct($out)
+ {
+ parent::__construct($out);
+ $this->runner = $runner;
+ }
+
+ /**
+ * ID of the form
+ *
+ * @return int ID of the form
+ */
+
+ function id()
+ {
+ return 'yammer-apikey-form';
+ }
+
+
+ /**
+ * class of the form
+ *
+ * @return string of the form class
+ */
+
+ function formClass()
+ {
+ return 'form_yammer_apikey form_settings';
+ }
+
+
+ /**
+ * Action of the form
+ *
+ * @return string URL of the action
+ */
+
+ function action()
+ {
+ return common_local_url('yammeradminpanel');
+ }
+
+
+ /**
+ * Legend of the Form
+ *
+ * @return void
+ */
+ function formLegend()
+ {
+ $this->out->element('legend', null, _m('Yammer API registration'));
+ }
+
+ /**
+ * Data elements of the form
+ *
+ * @return void
+ */
+
+ function formData()
+ {
+ $this->out->hidden('subaction', 'apikey');
+
+ $this->out->elementStart('fieldset');
+
+ $this->out->elementStart('p');
+ $this->out->text(_m('Before we can connect to your Yammer network, ' .
+ 'you will need to register the importer as an ' .
+ 'application authorized to pull data on your behalf. ' .
+ 'This registration will work only for your own network. ' .
+ 'Follow this link to register the app at Yammer; ' .
+ 'you will be prompted to log in if necessary:'));
+ $this->out->elementEnd('p');
+
+ $this->out->elementStart('p', array('class' => 'magiclink'));
+ $this->out->element('a',
+ array('href' => 'https://www.yammer.com/client_applications/new',
+ 'target' => '_blank'),
+ _m('Open Yammer application registration form'));
+ $this->out->elementEnd('p');
+
+ $this->out->element('p', array(), _m('Copy the consumer key and secret you are given into the form below:'));
+
+ $this->out->elementStart('ul', array('class' => 'form_data'));
+ $this->out->elementStart('li');
+ $this->out->input('consumer_key', _m('Consumer key:'), common_config('yammer', 'consumer_key'));
+ $this->out->elementEnd('li');
+ $this->out->elementStart('li');
+ $this->out->input('consumer_secret', _m('Consumer secret:'), common_config('yammer', 'consumer_secret'));
+ $this->out->elementEnd('li');
+ $this->out->elementEnd('ul');
+
+ $this->out->submit('submit', _m('Save'), 'submit', null, _m('Save these consumer keys'));
+
+ $this->out->elementEnd('fieldset');
+ }
+
+ /**
+ * Action elements
+ *
+ * @return void
+ */
+
+ function formActions()
+ {
+ }
+}
diff --git a/plugins/YammerImport/lib/yammerauthinitform.php b/plugins/YammerImport/lib/yammerauthinitform.php
new file mode 100644
index 000000000..9f48fd82a
--- /dev/null
+++ b/plugins/YammerImport/lib/yammerauthinitform.php
@@ -0,0 +1,76 @@
+<?php
+
+class YammerAuthInitForm extends Form
+{
+ /**
+ * ID of the form
+ *
+ * @return int ID of the form
+ */
+
+ function id()
+ {
+ return 'yammer-auth-init-form';
+ }
+
+
+ /**
+ * class of the form
+ *
+ * @return string of the form class
+ */
+
+ function formClass()
+ {
+ return 'form_yammer_auth_init form_settings';
+ }
+
+
+ /**
+ * Action of the form
+ *
+ * @return string URL of the action
+ */
+
+ function action()
+ {
+ return common_local_url('yammeradminpanel');
+ }
+
+
+ /**
+ * Legend of the Form
+ *
+ * @return void
+ */
+ function formLegend()
+ {
+ $this->out->element('legend', null, _m('Connect to Yammer'));
+ }
+
+ /**
+ * Data elements of the form
+ *
+ * @return void
+ */
+
+ function formData()
+ {
+ $this->out->hidden('subaction', 'authinit');
+
+ $this->out->elementStart('fieldset');
+ $this->out->submit('submit', _m('Start authentication'), 'submit', null, _m('Request authorization to connect to Yammer account'));
+ $this->out->submit('change-apikey', _m('Change API key'));
+ $this->out->elementEnd('fieldset');
+ }
+
+ /**
+ * Action elements
+ *
+ * @return void
+ */
+
+ function formActions()
+ {
+ }
+}
diff --git a/plugins/YammerImport/lib/yammerauthverifyform.php b/plugins/YammerImport/lib/yammerauthverifyform.php
new file mode 100644
index 000000000..e119be96f
--- /dev/null
+++ b/plugins/YammerImport/lib/yammerauthverifyform.php
@@ -0,0 +1,111 @@
+<?php
+
+class YammerAuthVerifyForm extends Form
+{
+ private $runner;
+
+ function __construct($out, YammerRunner $runner)
+ {
+ parent::__construct($out);
+ $this->runner = $runner;
+ }
+
+ /**
+ * ID of the form
+ *
+ * @return int ID of the form
+ */
+
+ function id()
+ {
+ return 'yammer-auth-verify-form';
+ }
+
+
+ /**
+ * class of the form
+ *
+ * @return string of the form class
+ */
+
+ function formClass()
+ {
+ return 'form_yammer_auth_verify form_settings';
+ }
+
+
+ /**
+ * Action of the form
+ *
+ * @return string URL of the action
+ */
+
+ function action()
+ {
+ return common_local_url('yammeradminpanel');
+ }
+
+
+ /**
+ * Legend of the Form
+ *
+ * @return void
+ */
+ function formLegend()
+ {
+ $this->out->element('legend', null, _m('Connect to Yammer'));
+ }
+
+ /**
+ * Data elements of the form
+ *
+ * @return void
+ */
+
+ function formData()
+ {
+ $this->out->hidden('subaction', 'authverify');
+
+ $this->out->elementStart('fieldset');
+
+ $this->out->elementStart('p');
+ $this->out->text(_m('Follow this link to confirm authorization at Yammer; you will be prompted to log in if necessary:'));
+ $this->out->elementEnd('p');
+
+ // iframe would be nice to avoid leaving -- since they don't seem to have callback url O_O
+ /*
+ $this->out->element('iframe', array('id' => 'yammer-oauth',
+ 'src' => $this->runner->getAuthUrl()));
+ */
+ // yeah, it ignores the callback_url
+ // soo... crappy link. :(
+
+ $this->out->elementStart('p', array('class' => 'magiclink'));
+ $this->out->element('a',
+ array('href' => $this->runner->getAuthUrl(),
+ 'target' => '_blank'),
+ _m('Open Yammer authentication window'));
+ $this->out->elementEnd('p');
+
+ $this->out->element('p', array(), _m('Copy the verification code you are given below:'));
+
+ $this->out->elementStart('ul', array('class' => 'form_data'));
+ $this->out->elementStart('li');
+ $this->out->input('verify_token', _m('Verification code:'));
+ $this->out->elementEnd('li');
+ $this->out->elementEnd('ul');
+
+ $this->out->submit('submit', _m('Continue'), 'submit', null, _m('Save code and begin import'));
+ $this->out->elementEnd('fieldset');
+ }
+
+ /**
+ * Action elements
+ *
+ * @return void
+ */
+
+ function formActions()
+ {
+ }
+}
diff --git a/plugins/YammerImport/lib/yammerimporter.php b/plugins/YammerImport/lib/yammerimporter.php
new file mode 100644
index 000000000..0425b8b04
--- /dev/null
+++ b/plugins/YammerImport/lib/yammerimporter.php
@@ -0,0 +1,456 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * Basic client class for Yammer's OAuth/JSON API.
+ *
+ * @package YammerImportPlugin
+ * @author Brion Vibber <brion@status.net>
+ */
+class YammerImporter
+{
+ protected $client;
+
+ function __construct(SN_YammerClient $client)
+ {
+ $this->client = $client;
+ }
+
+ /**
+ * Load or create an imported profile from Yammer data.
+ *
+ * @param object $item loaded JSON data for Yammer importer
+ * @return Profile
+ */
+ function importUser($item)
+ {
+ $data = $this->prepUser($item);
+
+ $profileId = $this->findImportedUser($data['orig_id']);
+ if ($profileId) {
+ return Profile::staticGet('id', $profileId);
+ } else {
+ $user = User::register($data['options']);
+ $profile = $user->getProfile();
+ if ($data['avatar']) {
+ try {
+ $this->saveAvatar($data['avatar'], $profile);
+ } catch (Exception $e) {
+ common_log(LOG_ERR, "Error importing Yammer avatar: " . $e->getMessage());
+ }
+ }
+ $this->recordImportedUser($data['orig_id'], $profile->id);
+ return $profile;
+ }
+ }
+
+ /**
+ * Load or create an imported group from Yammer data.
+ *
+ * @param object $item loaded JSON data for Yammer importer
+ * @return User_group
+ */
+ function importGroup($item)
+ {
+ $data = $this->prepGroup($item);
+
+ $groupId = $this->findImportedGroup($data['orig_id']);
+ if ($groupId) {
+ return User_group::staticGet('id', $groupId);
+ } else {
+ $group = User_group::register($data['options']);
+ if ($data['avatar']) {
+ try {
+ $this->saveAvatar($data['avatar'], $group);
+ } catch (Exception $e) {
+ common_log(LOG_ERR, "Error importing Yammer avatar: " . $e->getMessage());
+ }
+ }
+ $this->recordImportedGroup($data['orig_id'], $group->id);
+ return $group;
+ }
+ }
+
+ /**
+ * Load or create an imported notice from Yammer data.
+ *
+ * @param object $item loaded JSON data for Yammer importer
+ * @return Notice
+ */
+ function importNotice($item)
+ {
+ $data = $this->prepNotice($item);
+
+ $noticeId = $this->findImportedNotice($data['orig_id']);
+ if ($noticeId) {
+ return Notice::staticGet('id', $noticeId);
+ } else {
+ $content = $data['content'];
+ $user = User::staticGet($data['profile']);
+
+ // Fetch file attachments and add the URLs...
+ $uploads = array();
+ foreach ($data['attachments'] as $url) {
+ try {
+ $upload = $this->saveAttachment($url, $user);
+ $content .= ' ' . $upload->shortUrl();
+ $uploads[] = $upload;
+ } catch (Exception $e) {
+ common_log(LOG_ERR, "Error importing Yammer attachment: " . $e->getMessage());
+ }
+ }
+
+ // Here's the meat! Actually save the dang ol' notice.
+ $notice = Notice::saveNew($user->id,
+ $content,
+ $data['source'],
+ $data['options']);
+
+ // Save "likes" as favorites...
+ foreach ($data['faves'] as $nickname) {
+ $user = User::staticGet('nickname', $nickname);
+ if ($user) {
+ Fave::addNew($user->getProfile(), $notice);
+ }
+ }
+
+ // And finally attach the upload records...
+ foreach ($uploads as $upload) {
+ $upload->attachToNotice($notice);
+ }
+ $this->recordImportedNotice($data['orig_id'], $notice->id);
+ return $notice;
+ }
+ }
+
+ /**
+ * Pull relevant info out of a Yammer data record for a user import.
+ *
+ * @param array $item
+ * @return array
+ */
+ function prepUser($item)
+ {
+ if ($item['type'] != 'user') {
+ throw new Exception('Wrong item type sent to Yammer user import processing.');
+ }
+
+ $origId = $item['id'];
+ $origUrl = $item['url'];
+
+ // @fixme check username rules?
+
+ $options['nickname'] = $item['name'];
+ $options['fullname'] = trim($item['full_name']);
+
+ // Avatar... this will be the "_small" variant.
+ // Remove that (pre-extension) suffix to get the orig-size image.
+ $avatar = $item['mugshot_url'];
+
+ // The following info is only available in full data, not in the reference version.
+
+ // There can be extensive contact info, but for now we'll only pull the primary email.
+ if (isset($item['contact'])) {
+ foreach ($item['contact']['email_addresses'] as $addr) {
+ if ($addr['type'] == 'primary') {
+ $options['email'] = $addr['address'];
+ $options['email_confirmed'] = true;
+ break;
+ }
+ }
+ }
+
+ // There can be multiple external URLs; for now pull the first one as home page.
+ if (isset($item['external_urls'])) {
+ foreach ($item['external_urls'] as $url) {
+ if (common_valid_http_url($url)) {
+ $options['homepage'] = $url;
+ break;
+ }
+ }
+ }
+
+ // Combine a few bits into the bio...
+ $bio = array();
+ if (!empty($item['job_title'])) {
+ $bio[] = $item['job_title'];
+ }
+ if (!empty($item['summary'])) {
+ $bio[] = $item['summary'];
+ }
+ if (!empty($item['expertise'])) {
+ $bio[] = _m('Expertise:') . ' ' . $item['expertise'];
+ }
+ $options['bio'] = implode("\n\n", $bio);
+
+ // Pull raw location string, may be lookupable
+ if (!empty($item['location'])) {
+ $options['location'] = $item['location'];
+ }
+
+ // Timezone is in format like 'Pacific Time (US & Canada)'
+ // We need to convert that to a zone id. :P
+ // @fixme timezone not yet supported at registration time :)
+ if (!empty($item['timezone'])) {
+ $tz = $this->timezone($item['timezone']);
+ if ($tz) {
+ $options['timezone'] = $tz;
+ }
+ }
+
+ return array('orig_id' => $origId,
+ 'orig_url' => $origUrl,
+ 'avatar' => $avatar,
+ 'options' => $options);
+
+ }
+
+ /**
+ * Pull relevant info out of a Yammer data record for a group import.
+ *
+ * @param array $item
+ * @return array
+ */
+ function prepGroup($item)
+ {
+ if ($item['type'] != 'group') {
+ throw new Exception('Wrong item type sent to Yammer group import processing.');
+ }
+
+ $origId = $item['id'];
+ $origUrl = $item['url'];
+
+ $privacy = $item['privacy']; // Warning! only public groups in SN so far
+
+ $options['nickname'] = $item['name'];
+ $options['fullname'] = $item['full_name'];
+ $options['description'] = $item['description'];
+ $options['created'] = $this->timestamp($item['created_at']);
+
+ $avatar = $item['mugshot_url']; // as with user profiles...
+
+
+ $options['mainpage'] = common_local_url('showgroup',
+ array('nickname' => $options['nickname']));
+
+ // @fixme what about admin user for the group?
+ // bio? homepage etc? aliases?
+
+ $options['local'] = true;
+ return array('orig_id' => $origId,
+ 'orig_url' => $origUrl,
+ 'options' => $options,
+ 'avatar' => $avatar);
+ }
+
+ /**
+ * Pull relevant info out of a Yammer data record for a notice import.
+ *
+ * @param array $item
+ * @return array
+ */
+ function prepNotice($item)
+ {
+ if (isset($item['type']) && $item['type'] != 'message') {
+ throw new Exception('Wrong item type sent to Yammer message import processing.');
+ }
+
+ $origId = $item['id'];
+ $origUrl = $item['url'];
+
+ $profile = $this->findImportedUser($item['sender_id']);
+ $content = $item['body']['plain'];
+ $source = 'yammer';
+ $options = array();
+
+ if ($item['replied_to_id']) {
+ $replyTo = $this->findImportedNotice($item['replied_to_id']);
+ if ($replyTo) {
+ $options['reply_to'] = $replyTo;
+ }
+ }
+ $options['created'] = $this->timestamp($item['created_at']);
+
+ if ($item['group_id']) {
+ $groupId = $this->findImportedGroup($item['group_id']);
+ if ($groupId) {
+ $options['groups'] = array($groupId);
+
+ // @fixme if we see a group link inline, don't add this?
+ $group = User_group::staticGet('id', $groupId);
+ if ($group) {
+ $content .= ' !' . $group->nickname;
+ }
+ }
+ }
+
+ $faves = array();
+ foreach ($item['liked_by']['names'] as $liker) {
+ // "permalink" is the username. wtf?
+ $faves[] = $liker['permalink'];
+ }
+
+ $attachments = array();
+ foreach ($item['attachments'] as $attach) {
+ if ($attach['type'] == 'image' || $attach['type'] == 'file') {
+ $attachments[] = $attach[$attach['type']]['url'];
+ } else {
+ common_log(LOG_WARNING, "Unrecognized Yammer attachment type: " . $attach['type']);
+ }
+ }
+
+ return array('orig_id' => $origId,
+ 'orig_url' => $origUrl,
+ 'profile' => $profile,
+ 'content' => $content,
+ 'source' => $source,
+ 'options' => $options,
+ 'faves' => $faves,
+ 'attachments' => $attachments);
+ }
+
+ private function findImportedUser($origId)
+ {
+ $map = Yammer_user::staticGet('id', $origId);
+ return $map ? $map->user_id : null;
+ }
+
+ private function findImportedGroup($origId)
+ {
+ $map = Yammer_group::staticGet('id', $origId);
+ return $map ? $map->group_id : null;
+ }
+
+ private function findImportedNotice($origId)
+ {
+ $map = Yammer_notice::staticGet('id', $origId);
+ return $map ? $map->notice_id : null;
+ }
+
+ private function recordImportedUser($origId, $userId)
+ {
+ Yammer_user::record($origId, $userId);
+ }
+
+ private function recordImportedGroup($origId, $groupId)
+ {
+ Yammer_group::record($origId, $groupId);
+ }
+
+ private function recordImportedNotice($origId, $noticeId)
+ {
+ Yammer_notice::record($origId, $noticeId);
+ }
+
+ /**
+ * Normalize timestamp format.
+ * @param string $ts
+ * @return string
+ */
+ private function timestamp($ts)
+ {
+ return common_sql_date(strtotime($ts));
+ }
+
+ private function timezone($tz)
+ {
+ // Blaaaaaarf!
+ $known = array('Pacific Time (US & Canada)' => 'America/Los_Angeles',
+ 'Eastern Time (US & Canada)' => 'America/New_York');
+ if (array_key_exists($tz, $known)) {
+ return $known[$tz];
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Download and update given avatar image
+ *
+ * @param string $url
+ * @param mixed $dest either a Profile or User_group object
+ * @throws Exception in various failure cases
+ */
+ private function saveAvatar($url, $dest)
+ {
+ // Yammer API data mostly gives us the small variant.
+ // Try hitting the source image if we can!
+ // @fixme no guarantee of this URL scheme I think.
+ $url = preg_replace('/_small(\..*?)$/', '$1', $url);
+
+ if (!common_valid_http_url($url)) {
+ throw new ServerException(sprintf(_m("Invalid avatar URL %s."), $url));
+ }
+
+ // @fixme this should be better encapsulated
+ // ripped from oauthstore.php (for old OMB client)
+ $temp_filename = tempnam(sys_get_temp_dir(), 'listener_avatar');
+ if (!copy($url, $temp_filename)) {
+ throw new ServerException(sprintf(_m("Unable to fetch avatar from %s."), $url));
+ }
+
+ $id = $dest->id;
+ // @fixme should we be using different ids?
+ $imagefile = new ImageFile($id, $temp_filename);
+ $filename = Avatar::filename($id,
+ image_type_to_extension($imagefile->type),
+ null,
+ common_timestamp());
+ rename($temp_filename, Avatar::path($filename));
+ // @fixme hardcoded chmod is lame, but seems to be necessary to
+ // keep from accidentally saving images from command-line (queues)
+ // that can't be read from web server, which causes hard-to-notice
+ // problems later on:
+ //
+ // http://status.net/open-source/issues/2663
+ chmod(Avatar::path($filename), 0644);
+
+ $dest->setOriginal($filename);
+ }
+
+ /**
+ * Fetch an attachment from Yammer and save it into our system.
+ * Unlike avatars, the attachment URLs are guarded by authentication,
+ * so we need to run the HTTP hit through our OAuth API client.
+ *
+ * @param string $url
+ * @param User $user
+ * @return MediaFile
+ *
+ * @throws Exception on low-level network or HTTP error
+ */
+ private function saveAttachment($url, User $user)
+ {
+ // Fetch the attachment...
+ // WARNING: file must fit in memory here :(
+ $body = $this->client->fetchUrl($url);
+
+ // Save to a temporary file and shove it into our file-attachment space...
+ $temp = tmpfile();
+ fwrite($temp, $body);
+ try {
+ $upload = MediaFile::fromFileHandle($temp, $user);
+ fclose($temp);
+ return $upload;
+ } catch (Exception $e) {
+ fclose($temp);
+ throw $e;
+ }
+ }
+}
diff --git a/plugins/YammerImport/lib/yammerprogressform.php b/plugins/YammerImport/lib/yammerprogressform.php
new file mode 100644
index 000000000..776efa100
--- /dev/null
+++ b/plugins/YammerImport/lib/yammerprogressform.php
@@ -0,0 +1,128 @@
+<?php
+
+class YammerProgressForm extends Form
+{
+ /**
+ * ID of the form
+ *
+ * @return string ID of the form
+ */
+ function id()
+ {
+ return 'yammer-progress';
+ }
+
+ /**
+ * class of the form
+ *
+ * @return string class of the form
+ */
+ function formClass()
+ {
+ return 'form_settings';
+ }
+
+ /**
+ * Action of the form
+ *
+ * @return string URL of the action
+ */
+ function action()
+ {
+ return common_local_url('yammeradminpanel');
+ }
+
+ /**
+ * Data elements of the form
+ *
+ * @return void
+ */
+ function formData()
+ {
+ $runner = YammerRunner::init();
+
+ $userCount = $runner->countUsers();
+ $groupCount = $runner->countGroups();
+ $fetchedCount = $runner->countFetchedNotices();
+ $savedCount = $runner->countSavedNotices();
+
+ $labels = array(
+ 'init' => array(
+ 'label' => _m("Initialize"),
+ 'progress' => _m('No import running'),
+ 'complete' => _m('Initiated Yammer server connection...'),
+ ),
+ 'requesting-auth' => array(
+ 'label' => _m('Connect to Yammer'),
+ 'progress' => _m('Awaiting authorization...'),
+ 'complete' => _m('Connected.'),
+ ),
+ 'import-users' => array(
+ 'label' => _m('Import user accounts'),
+ 'progress' => sprintf(_m("Importing %d user...", "Importing %d users...", $userCount), $userCount),
+ 'complete' => sprintf(_m("Imported %d user.", "Imported %d users.", $userCount), $userCount),
+ ),
+ 'import-groups' => array(
+ 'label' => _m('Import user groups'),
+ 'progress' => sprintf(_m("Importing %d group...", "Importing %d groups...", $groupCount), $groupCount),
+ 'complete' => sprintf(_m("Imported %d group.", "Imported %d groups.", $groupCount), $groupCount),
+ ),
+ 'fetch-messages' => array(
+ 'label' => _m('Prepare public notices for import'),
+ 'progress' => sprintf(_m("Preparing %d notice...", "Preparing %d notices...", $fetchedCount), $fetchedCount),
+ 'complete' => sprintf(_m("Prepared %d notice.", "Prepared %d notices.", $fetchedCount), $fetchedCount),
+ ),
+ 'save-messages' => array(
+ 'label' => _m('Import public notices'),
+ 'progress' => sprintf(_m("Importing %d notice...", "Importing %d notices...", $savedCount), $savedCount),
+ 'complete' => sprintf(_m("Imported %d notice.", "Imported %d notices.", $savedCount), $savedCount),
+ ),
+ 'done' => array(
+ 'label' => _m('Done'),
+ 'progress' => sprintf(_m("Import is complete!")),
+ 'complete' => sprintf(_m("Import is complete!")),
+ )
+ );
+ $steps = array_keys($labels);
+ $currentStep = array_search($runner->state(), $steps);
+
+ $this->out->elementStart('fieldset', array('class' => 'yammer-import'));
+ $this->out->element('legend', array(), _m('Import status'));
+ foreach ($steps as $step => $state) {
+ if ($state == 'init') {
+ // Don't show 'init', it's boring.
+ continue;
+ }
+ if ($step < $currentStep) {
+ // This step is done
+ $this->progressBar($state,
+ 'complete',
+ $labels[$state]['label'],
+ $labels[$state]['complete']);
+ } else if ($step == $currentStep) {
+ // This step is in progress
+ $this->progressBar($state,
+ 'progress',
+ $labels[$state]['label'],
+ $labels[$state]['progress']);
+ } else {
+ // This step has not yet been done.
+ $this->progressBar($state,
+ 'waiting',
+ $labels[$state]['label'],
+ _m("Waiting..."));
+ }
+ }
+ $this->out->elementEnd('fieldset');
+ }
+
+ private function progressBar($state, $class, $label, $status)
+ {
+ // @fixme prettify ;)
+ $this->out->elementStart('div', array('class' => "import-step import-step-$state $class"));
+ $this->out->element('div', array('class' => 'import-label'), $label);
+ $this->out->element('div', array('class' => 'import-status'), $status);
+ $this->out->elementEnd('div');
+ }
+
+}
diff --git a/plugins/YammerImport/lib/yammerqueuehandler.php b/plugins/YammerImport/lib/yammerqueuehandler.php
new file mode 100644
index 000000000..acc807311
--- /dev/null
+++ b/plugins/YammerImport/lib/yammerqueuehandler.php
@@ -0,0 +1,58 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+if (!defined('STATUSNET')) {
+ exit(1);
+}
+
+/**
+ * Queue handler for bumping the next chunk of Yammer import activity!
+ *
+ * @package YammerImportPlugin
+ * @author Brion Vibber <brion@status.net>
+ */
+class YammerQueueHandler extends QueueHandler
+{
+ function transport()
+ {
+ return 'yammer';
+ }
+
+ function handle($notice)
+ {
+ $runner = YammerRunner::init();
+ if ($runner->hasWork()) {
+ if ($runner->iterate()) {
+ if ($runner->hasWork()) {
+ // More to do? Shove us back on the queue...
+ $runner->startBackgroundImport();
+ }
+ return true;
+ } else {
+ // Something failed?
+ // @fixme should we be trying again here, or should we give warning?
+ return false;
+ }
+ } else {
+ // We're done!
+ common_log(LOG_INFO, "Yammer import has no work to do at this time; discarding.");
+ return true;
+ }
+ }
+}
diff --git a/plugins/YammerImport/lib/yammerrunner.php b/plugins/YammerImport/lib/yammerrunner.php
new file mode 100644
index 000000000..e0aec0d16
--- /dev/null
+++ b/plugins/YammerImport/lib/yammerrunner.php
@@ -0,0 +1,398 @@
+<?php
+/*
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+if (!defined('STATUSNET')) {
+ exit(1);
+}
+
+/**
+ * State machine for running through Yammer import.
+ *
+ * @package YammerImportPlugin
+ * @author Brion Vibber <brion@status.net>
+ */
+class YammerRunner
+{
+ private $state;
+ private $client;
+ private $importer;
+
+ /**
+ * Normalize our singleton state and give us a YammerRunner object to play with!
+ *
+ * @return YammerRunner
+ */
+ public static function init()
+ {
+ $state = Yammer_state::staticGet('id', 1);
+ if (!$state) {
+ $state = self::initState();
+ }
+ return new YammerRunner($state);
+ }
+
+ private static function initState()
+ {
+ $state = new Yammer_state();
+ $state->id = 1;
+ $state->state = 'init';
+ $state->created = common_sql_now();
+ $state->modified = common_sql_now();
+ $state->insert();
+ return $state;
+ }
+
+ private function __construct($state)
+ {
+ $this->state = $state;
+
+ $this->client = new SN_YammerClient(
+ common_config('yammer', 'consumer_key'),
+ common_config('yammer', 'consumer_secret'),
+ $this->state->oauth_token,
+ $this->state->oauth_secret);
+
+ $this->importer = new YammerImporter($this->client);
+ }
+
+ /**
+ * Check which state we're in
+ *
+ * @return string
+ */
+ public function state()
+ {
+ return $this->state->state;
+ }
+
+ /**
+ * Is the import done, finished, complete, finito?
+ *
+ * @return boolean
+ */
+ public function isDone()
+ {
+ $workStates = array('import-users', 'import-groups', 'fetch-messages', 'save-messages');
+ return ($this->state() == 'done');
+ }
+
+ /**
+ * Check if we have work to do in iterate().
+ *
+ * @return boolean
+ */
+ public function hasWork()
+ {
+ $workStates = array('import-users', 'import-groups', 'fetch-messages', 'save-messages');
+ return in_array($this->state(), $workStates);
+ }
+
+ /**
+ * Blow away any current state!
+ */
+ public function reset()
+ {
+ $this->state->delete();
+ $this->state = self::initState();
+ }
+
+ /**
+ * Start the authentication process! If all goes well, we'll get back a URL.
+ * Have the user visit that URL, log in on Yammer and verify the importer's
+ * permissions. They'll get back a verification code, which needs to be passed
+ * on to saveAuthToken().
+ *
+ * @return string URL
+ */
+ public function requestAuth()
+ {
+ if ($this->state->state != 'init') {
+ throw new ServerException("Cannot request Yammer auth; already there!");
+ }
+
+ $data = $this->client->requestToken();
+
+ $old = clone($this->state);
+ $this->state->state = 'requesting-auth';
+ $this->state->oauth_token = $data['oauth_token'];
+ $this->state->oauth_secret = $data['oauth_token_secret'];
+ $this->state->modified = common_sql_now();
+ $this->state->update($old);
+
+ return $this->getAuthUrl();
+ }
+
+ /**
+ * When already in requesting-auth state, grab the URL to send the user to
+ * to complete OAuth setup.
+ *
+ * @return string URL
+ */
+ function getAuthUrl()
+ {
+ if ($this->state() == 'requesting-auth') {
+ return $this->client->authorizeUrl($this->state->oauth_token);
+ } else {
+ throw new ServerException('Cannot get Yammer auth URL when not in requesting-auth state!');
+ }
+ }
+
+ /**
+ * Now that the user's given us this verification code from Yammer, we can
+ * request a final OAuth token/secret pair which we can use to access the
+ * API.
+ *
+ * After success here, we'll be ready to move on and run through iterate()
+ * until the import is complete.
+ *
+ * @param string $verifier
+ * @return boolean success
+ */
+ public function saveAuthToken($verifier)
+ {
+ if ($this->state->state != 'requesting-auth') {
+ throw new ServerException("Cannot save auth token in Yammer import state {$this->state->state}");
+ }
+
+ $data = $this->client->accessToken($verifier);
+
+ $old = clone($this->state);
+ $this->state->state = 'import-users';
+ $this->state->oauth_token = $data['oauth_token'];
+ $this->state->oauth_secret = $data['oauth_token_secret'];
+ $this->state->modified = common_sql_now();
+ $this->state->update($old);
+
+ return true;
+ }
+
+ /**
+ * Once authentication is complete, we need to call iterate() a bunch of times
+ * until state() returns 'done'.
+ *
+ * @return boolean success
+ */
+ public function iterate()
+ {
+ switch($this->state())
+ {
+ case 'init':
+ case 'requesting-auth':
+ // Neither of these should reach our background state!
+ common_log(LOG_ERR, "Non-background YammerImport state '$state->state' during import run!");
+ return false;
+ case 'import-users':
+ return $this->iterateUsers();
+ case 'import-groups':
+ return $this->iterateGroups();
+ case 'fetch-messages':
+ return $this->iterateFetchMessages();
+ case 'save-messages':
+ return $this->iterateSaveMessages();
+ default:
+ common_log(LOG_ERR, "Invalid YammerImport state '$state->state' during import run!");
+ return false;
+ }
+ }
+
+ /**
+ * Trundle through one 'page' return of up to 50 user accounts retrieved
+ * from the Yammer API, importing them as we go.
+ *
+ * When we run out of users, move on to groups.
+ *
+ * @return boolean success
+ */
+ private function iterateUsers()
+ {
+ $old = clone($this->state);
+
+ $page = intval($this->state->users_page) + 1;
+ $data = $this->client->users(array('page' => $page));
+
+ if (count($data) == 0) {
+ common_log(LOG_INFO, "Finished importing Yammer users; moving on to groups.");
+ $this->state->state = 'import-groups';
+ } else {
+ foreach ($data as $item) {
+ $user = $this->importer->importUser($item);
+ common_log(LOG_INFO, "Imported Yammer user " . $item['id'] . " as $user->nickname ($user->id)");
+ }
+ $this->state->users_page = $page;
+ }
+ $this->state->modified = common_sql_now();
+ $this->state->update($old);
+ return true;
+ }
+
+ /**
+ * Trundle through one 'page' return of up to 20 user groups retrieved
+ * from the Yammer API, importing them as we go.
+ *
+ * When we run out of groups, move on to messages.
+ *
+ * @return boolean success
+ */
+ private function iterateGroups()
+ {
+ $old = clone($this->state);
+
+ $page = intval($this->state->groups_page) + 1;
+ $data = $this->client->groups(array('page' => $page));
+
+ if (count($data) == 0) {
+ common_log(LOG_INFO, "Finished importing Yammer groups; moving on to messages.");
+ $this->state->state = 'fetch-messages';
+ } else {
+ foreach ($data as $item) {
+ $group = $this->importer->importGroup($item);
+ common_log(LOG_INFO, "Imported Yammer group " . $item['id'] . " as $group->nickname ($group->id)");
+ }
+ $this->state->groups_page = $page;
+ }
+ $this->state->modified = common_sql_now();
+ $this->state->update($old);
+ return true;
+ }
+
+ /**
+ * Trundle through one 'page' return of up to 20 public messages retrieved
+ * from the Yammer API, saving them to our stub table for future import in
+ * correct chronological order.
+ *
+ * When we run out of messages to fetch, move on to saving the messages.
+ *
+ * @return boolean success
+ */
+ private function iterateFetchMessages()
+ {
+ $old = clone($this->state);
+
+ $oldest = intval($this->state->messages_oldest);
+ if ($oldest) {
+ $params = array('older_than' => $oldest);
+ } else {
+ $params = array();
+ }
+ $data = $this->client->messages($params);
+ $messages = $data['messages'];
+
+ if (count($messages) == 0) {
+ common_log(LOG_INFO, "Finished fetching Yammer messages; moving on to save messages.");
+ $this->state->state = 'save-messages';
+ } else {
+ foreach ($messages as $item) {
+ Yammer_notice_stub::record($item['id'], $item);
+ $oldest = $item['id'];
+ }
+ $this->state->messages_oldest = $oldest;
+ }
+ $this->state->modified = common_sql_now();
+ $this->state->update($old);
+ return true;
+ }
+
+ private function iterateSaveMessages()
+ {
+ $old = clone($this->state);
+
+ $newest = intval($this->state->messages_newest);
+
+ $stub = new Yammer_notice_stub();
+ if ($newest) {
+ $stub->whereAdd('id > ' . $newest);
+ }
+ $stub->limit(20);
+ $stub->orderBy('id');
+ $stub->find();
+
+ if ($stub->N == 0) {
+ common_log(LOG_INFO, "Finished saving Yammer messages; import complete!");
+ $this->state->state = 'done';
+ } else {
+ while ($stub->fetch()) {
+ $item = $stub->getData();
+ $notice = $this->importer->importNotice($item);
+ common_log(LOG_INFO, "Imported Yammer notice " . $item['id'] . " as $notice->id");
+ $newest = $item['id'];
+ }
+ $this->state->messages_newest = $newest;
+ }
+ $this->state->modified = common_sql_now();
+ $this->state->update($old);
+ return true;
+ }
+
+ /**
+ * Count the number of Yammer users we've mapped into our system!
+ *
+ * @return int
+ */
+ public function countUsers()
+ {
+ $map = new Yammer_user();
+ return $map->count();
+ }
+
+
+ /**
+ * Count the number of Yammer groups we've mapped into our system!
+ *
+ * @return int
+ */
+ public function countGroups()
+ {
+ $map = new Yammer_group();
+ return $map->count();
+ }
+
+
+ /**
+ * Count the number of Yammer notices we've pulled down for pending import...
+ *
+ * @return int
+ */
+ public function countFetchedNotices()
+ {
+ $map = new Yammer_notice_stub();
+ return $map->count();
+ }
+
+
+ /**
+ * Count the number of Yammer notices we've mapped into our system!
+ *
+ * @return int
+ */
+ public function countSavedNotices()
+ {
+ $map = new Yammer_notice();
+ return $map->count();
+ }
+
+ /**
+ * Start running import work in the background queues...
+ */
+ public function startBackgroundImport()
+ {
+ $qm = QueueManager::get();
+ $qm->enqueue('YammerImport', 'yammer');
+ }
+
+}