From 9b9d80cd97704426e54434d8777c5c15154014ad Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Tue, 25 Aug 2009 14:52:25 -0700 Subject: Pluginized Twitter settings stuff --- actions/twitterauthorization.php | 222 -------------------- actions/twittersettings.php | 277 ------------------------- lib/connectsettingsaction.php | 49 +++-- lib/router.php | 6 +- lib/twitteroauthclient.php | 220 -------------------- plugins/TwitterBridge/TwitterBridgePlugin.php | 101 +++++++++ plugins/TwitterBridge/twitterauthorization.php | 222 ++++++++++++++++++++ plugins/TwitterBridge/twitteroauthclient.php | 220 ++++++++++++++++++++ plugins/TwitterBridge/twittersettings.php | 272 ++++++++++++++++++++++++ 9 files changed, 842 insertions(+), 747 deletions(-) delete mode 100644 actions/twitterauthorization.php delete mode 100644 actions/twittersettings.php delete mode 100644 lib/twitteroauthclient.php create mode 100644 plugins/TwitterBridge/TwitterBridgePlugin.php create mode 100644 plugins/TwitterBridge/twitterauthorization.php create mode 100644 plugins/TwitterBridge/twitteroauthclient.php create mode 100644 plugins/TwitterBridge/twittersettings.php diff --git a/actions/twitterauthorization.php b/actions/twitterauthorization.php deleted file mode 100644 index b04f35327..000000000 --- a/actions/twitterauthorization.php +++ /dev/null @@ -1,222 +0,0 @@ -. - * - * @category Twitter - * @package Laconica - * @author Zach Copely - * @copyright 2009 Control Yourself, Inc. - * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 - * @link http://laconi.ca/ - */ - -if (!defined('LACONICA')) { - exit(1); -} - -/** - * Class for doing OAuth authentication against Twitter - * - * Peforms the OAuth "dance" between Laconica and Twitter -- requests a token, - * authorizes it, and exchanges it for an access token. It also creates a link - * (Foreign_link) between the Laconica user and Twitter user and stores the - * access token and secret in the link. - * - * @category Twitter - * @package Laconica - * @author Zach Copley - * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 - * @link http://laconi.ca/ - * - */ -class TwitterauthorizationAction extends Action -{ - /** - * Initialize class members. Looks for 'oauth_token' parameter. - * - * @param array $args misc. arguments - * - * @return boolean true - */ - function prepare($args) - { - parent::prepare($args); - - $this->oauth_token = $this->arg('oauth_token'); - - return true; - } - - /** - * Handler method - * - * @param array $args is ignored since it's now passed in in prepare() - * - * @return nothing - */ - function handle($args) - { - parent::handle($args); - - if (!common_logged_in()) { - $this->clientError(_('Not logged in.'), 403); - } - - $user = common_current_user(); - $flink = Foreign_link::getByUserID($user->id, TWITTER_SERVICE); - - // If there's already a foreign link record, it means we already - // have an access token, and this is unecessary. So go back. - - if (isset($flink)) { - common_redirect(common_local_url('twittersettings')); - } - - // $this->oauth_token is only populated once Twitter authorizes our - // request token. If it's empty we're at the beginning of the auth - // process - - if (empty($this->oauth_token)) { - $this->authorizeRequestToken(); - } else { - $this->saveAccessToken(); - } - } - - /** - * Asks Twitter for a request token, and then redirects to Twitter - * to authorize it. - * - * @return nothing - */ - function authorizeRequestToken() - { - try { - - // Get a new request token and authorize it - - $client = new TwitterOAuthClient(); - $req_tok = - $client->getRequestToken(TwitterOAuthClient::$requestTokenURL); - - // Sock the request token away in the session temporarily - - $_SESSION['twitter_request_token'] = $req_tok->key; - $_SESSION['twitter_request_token_secret'] = $req_tok->secret; - - $auth_link = $client->getAuthorizeLink($req_tok); - - } catch (TwitterOAuthClientException $e) { - $msg = sprintf('OAuth client cURL error - code: %1s, msg: %2s', - $e->getCode(), $e->getMessage()); - $this->serverError(_('Couldn\'t link your Twitter account.')); - } - - common_redirect($auth_link); - } - - /** - * Called when Twitter returns an authorized request token. Exchanges - * it for an access token and stores it. - * - * @return nothing - */ - function saveAccessToken() - { - - // Check to make sure Twitter returned the same request - // token we sent them - - if ($_SESSION['twitter_request_token'] != $this->oauth_token) { - $this->serverError(_('Couldn\'t link your Twitter account.')); - } - - try { - - $client = new TwitterOAuthClient($_SESSION['twitter_request_token'], - $_SESSION['twitter_request_token_secret']); - - // Exchange the request token for an access token - - $atok = $client->getAccessToken(TwitterOAuthClient::$accessTokenURL); - - // Test the access token and get the user's Twitter info - - $client = new TwitterOAuthClient($atok->key, $atok->secret); - $twitter_user = $client->verifyCredentials(); - - } catch (OAuthClientException $e) { - $msg = sprintf('OAuth client cURL error - code: %1$s, msg: %2$s', - $e->getCode(), $e->getMessage()); - $this->serverError(_('Couldn\'t link your Twitter account.')); - } - - // Save the access token and Twitter user info - - $this->saveForeignLink($atok, $twitter_user); - - // Clean up the the mess we made in the session - - unset($_SESSION['twitter_request_token']); - unset($_SESSION['twitter_request_token_secret']); - - common_redirect(common_local_url('twittersettings')); - } - - /** - * Saves a Foreign_link between Twitter user and local user, - * which includes the access token and secret. - * - * @param OAuthToken $access_token the access token to save - * @param mixed $twitter_user twitter API user object - * - * @return nothing - */ - function saveForeignLink($access_token, $twitter_user) - { - $user = common_current_user(); - - $flink = new Foreign_link(); - - $flink->user_id = $user->id; - $flink->foreign_id = $twitter_user->id; - $flink->service = TWITTER_SERVICE; - - $creds = TwitterOAuthClient::packToken($access_token); - - $flink->credentials = $creds; - $flink->created = common_sql_now(); - - // Defaults: noticesync on, everything else off - - $flink->set_flags(true, false, false, false); - - $flink_id = $flink->insert(); - - if (empty($flink_id)) { - common_log_db_error($flink, 'INSERT', __FILE__); - $this->serverError(_('Couldn\'t link your Twitter account.')); - } - - save_twitter_user($twitter_user->id, $twitter_user->screen_name); - } - -} - diff --git a/actions/twittersettings.php b/actions/twittersettings.php deleted file mode 100644 index 0859ab9d3..000000000 --- a/actions/twittersettings.php +++ /dev/null @@ -1,277 +0,0 @@ -. - * - * @category Settings - * @package Laconica - * @author Evan Prodromou - * @copyright 2008-2009 Control Yourself, Inc. - * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 - * @link http://laconi.ca/ - */ - -if (!defined('LACONICA')) { - exit(1); -} - -require_once INSTALLDIR.'/lib/connectsettingsaction.php'; -require_once INSTALLDIR.'/lib/twitter.php'; - -/** - * Settings for Twitter integration - * - * @category Settings - * @package Laconica - * @author Evan Prodromou - * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 - * @link http://laconi.ca/ - * - * @see SettingsAction - */ - -class TwittersettingsAction extends ConnectSettingsAction -{ - /** - * Title of the page - * - * @return string Title of the page - */ - - function title() - { - return _('Twitter settings'); - } - - /** - * Instructions for use - * - * @return instructions for use - */ - - function getInstructions() - { - return _('Connect your Twitter account to share your updates ' . - 'with your Twitter friends and vice-versa.'); - } - - /** - * Content area of the page - * - * Shows a form for associating a Twitter account with this - * Laconica account. Also lets the user set preferences. - * - * @return void - */ - - function showContent() - { - if (!common_config('twitter', 'enabled')) { - $this->element('div', array('class' => 'error'), - _('Twitter is not available.')); - return; - } - - $user = common_current_user(); - - $profile = $user->getProfile(); - - $fuser = null; - - $flink = Foreign_link::getByUserID($user->id, TWITTER_SERVICE); - - if (!empty($flink)) { - $fuser = $flink->getForeignUser(); - } - - $this->elementStart('form', array('method' => 'post', - 'id' => 'form_settings_twitter', - 'class' => 'form_settings', - 'action' => - common_local_url('twittersettings'))); - - $this->hidden('token', common_session_token()); - - $this->elementStart('fieldset', array('id' => 'settings_twitter_account')); - - if (empty($fuser)) { - $this->elementStart('ul', 'form_data'); - $this->elementStart('li', array('id' => 'settings_twitter_login_button')); - $this->element('a', array('href' => common_local_url('twitterauthorization')), - 'Connect my Twitter account'); - $this->elementEnd('li'); - $this->elementEnd('ul'); - - $this->elementEnd('fieldset'); - } else { - $this->element('legend', null, _('Twitter account')); - $this->elementStart('p', array('id' => 'form_confirmed')); - $this->element('a', array('href' => $fuser->uri), $fuser->nickname); - $this->elementEnd('p'); - $this->element('p', 'form_note', - _('Connected Twitter account')); - - $this->submit('remove', _('Remove')); - - $this->elementEnd('fieldset'); - - $this->elementStart('fieldset', array('id' => 'settings_twitter_preferences')); - - $this->element('legend', null, _('Preferences')); - $this->elementStart('ul', 'form_data'); - $this->elementStart('li'); - $this->checkbox('noticesend', - _('Automatically send my notices to Twitter.'), - ($flink) ? - ($flink->noticesync & FOREIGN_NOTICE_SEND) : - true); - $this->elementEnd('li'); - $this->elementStart('li'); - $this->checkbox('replysync', - _('Send local "@" replies to Twitter.'), - ($flink) ? - ($flink->noticesync & FOREIGN_NOTICE_SEND_REPLY) : - true); - $this->elementEnd('li'); - $this->elementStart('li'); - $this->checkbox('friendsync', - _('Subscribe to my Twitter friends here.'), - ($flink) ? - ($flink->friendsync & FOREIGN_FRIEND_RECV) : - false); - $this->elementEnd('li'); - - if (common_config('twitterbridge','enabled')) { - $this->elementStart('li'); - $this->checkbox('noticerecv', - _('Import my Friends Timeline.'), - ($flink) ? - ($flink->noticesync & FOREIGN_NOTICE_RECV) : - false); - $this->elementEnd('li'); - - // preserve setting even if bidrection bridge toggled off - - if ($flink && ($flink->noticesync & FOREIGN_NOTICE_RECV)) { - $this->hidden('noticerecv', true, 'noticerecv'); - } - } - - $this->elementEnd('ul'); - - if ($flink) { - $this->submit('save', _('Save')); - } else { - $this->submit('add', _('Add')); - } - - $this->elementEnd('fieldset'); - } - - $this->elementEnd('form'); - } - - /** - * Handle posts to this form - * - * Based on the button that was pressed, muxes out to other functions - * to do the actual task requested. - * - * All sub-functions reload the form with a message -- success or failure. - * - * @return void - */ - - function handlePost() - { - // CSRF protection - $token = $this->trimmed('token'); - if (!$token || $token != common_session_token()) { - $this->showForm(_('There was a problem with your session token. '. - 'Try again, please.')); - return; - } - - if ($this->arg('save')) { - $this->savePreferences(); - } else if ($this->arg('remove')) { - $this->removeTwitterAccount(); - } else { - $this->showForm(_('Unexpected form submission.')); - } - } - - /** - * Disassociate an existing Twitter account from this account - * - * @return void - */ - - function removeTwitterAccount() - { - $user = common_current_user(); - $flink = Foreign_link::getByUserID($user->id, TWITTER_SERVICE); - - $result = $flink->delete(); - - if (empty($result)) { - common_log_db_error($flink, 'DELETE', __FILE__); - $this->serverError(_('Couldn\'t remove Twitter user.')); - return; - } - - $this->showForm(_('Twitter account removed.'), true); - } - - /** - * Save user's Twitter-bridging preferences - * - * @return void - */ - - function savePreferences() - { - $noticesend = $this->boolean('noticesend'); - $noticerecv = $this->boolean('noticerecv'); - $friendsync = $this->boolean('friendsync'); - $replysync = $this->boolean('replysync'); - - $user = common_current_user(); - $flink = Foreign_link::getByUserID($user->id, TWITTER_SERVICE); - - if (empty($flink)) { - common_log_db_error($flink, 'SELECT', __FILE__); - $this->showForm(_('Couldn\'t save Twitter preferences.')); - return; - } - - $original = clone($flink); - $flink->set_flags($noticesend, $noticerecv, $replysync, $friendsync); - $result = $flink->update($original); - - if ($result === false) { - common_log_db_error($flink, 'UPDATE', __FILE__); - $this->showForm(_('Couldn\'t save Twitter preferences.')); - return; - } - - $this->showForm(_('Twitter preferences saved.'), true); - } - -} diff --git a/lib/connectsettingsaction.php b/lib/connectsettingsaction.php index 02c468a35..7902931e0 100644 --- a/lib/connectsettingsaction.php +++ b/lib/connectsettingsaction.php @@ -98,34 +98,37 @@ class ConnectSettingsNav extends Widget function show() { - # action => array('prompt', 'title') - $menu = array(); - if (common_config('xmpp', 'enabled')) { - $menu['imsettings'] = - array(_('IM'), - _('Updates by instant messenger (IM)')); - } - if (common_config('sms', 'enabled')) { - $menu['smssettings'] = - array(_('SMS'), - _('Updates by SMS')); - } - if (common_config('twitter', 'enabled')) { - $menu['twittersettings'] = - array(_('Twitter'), - _('Twitter integration options')); - } - $action_name = $this->action->trimmed('action'); $this->action->elementStart('ul', array('class' => 'nav')); - foreach ($menu as $menuaction => $menudesc) { - $this->action->menuItem(common_local_url($menuaction), - $menudesc[0], - $menudesc[1], - $action_name === $menuaction); + if (Event::handle('StartConnectSettingsNav', array(&$this->action))) { + + # action => array('prompt', 'title') + $menu = array(); + if (common_config('xmpp', 'enabled')) { + $menu['imsettings'] = + array(_('IM'), + _('Updates by instant messenger (IM)')); + } + if (common_config('sms', 'enabled')) { + $menu['smssettings'] = + array(_('SMS'), + _('Updates by SMS')); + } + + foreach ($menu as $menuaction => $menudesc) { + $this->action->menuItem(common_local_url($menuaction), + $menudesc[0], + $menudesc[1], + $action_name === $menuaction); + } + + Event::handle('EndConnectSettingsNav', array(&$this->action)); } $this->action->elementEnd('ul'); } + } + + diff --git a/lib/router.php b/lib/router.php index 08bc0566d..5bffdb16b 100644 --- a/lib/router.php +++ b/lib/router.php @@ -86,10 +86,6 @@ class Router $m->connect('doc/:title', array('action' => 'doc')); - // Twitter - - $m->connect('twitter/authorization', array('action' => 'twitterauthorization')); - // facebook $m->connect('facebook', array('action' => 'facebookhome')); @@ -136,7 +132,7 @@ class Router // settings foreach (array('profile', 'avatar', 'password', 'im', - 'email', 'sms', 'twitter', 'userdesign', 'other') as $s) { + 'email', 'sms', 'userdesign', 'other') as $s) { $m->connect('settings/'.$s, array('action' => $s.'settings')); } diff --git a/lib/twitteroauthclient.php b/lib/twitteroauthclient.php deleted file mode 100644 index b7dc4a80c..000000000 --- a/lib/twitteroauthclient.php +++ /dev/null @@ -1,220 +0,0 @@ -. - * - * @category Integration - * @package Laconica - * @author Zach Copley - * @copyright 2008 Control Yourself, Inc. - * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 - * @link http://laconi.ca/ - */ - -if (!defined('LACONICA')) { - exit(1); -} - -/** - * Class for talking to the Twitter API with OAuth. - * - * @category Integration - * @package Laconica - * @author Zach Copley - * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 - * @link http://laconi.ca/ - * - */ -class TwitterOAuthClient extends OAuthClient -{ - public static $requestTokenURL = 'https://twitter.com/oauth/request_token'; - public static $authorizeURL = 'https://twitter.com/oauth/authorize'; - public static $accessTokenURL = 'https://twitter.com/oauth/access_token'; - - /** - * Constructor - * - * @param string $oauth_token the user's token - * @param string $oauth_token_secret the user's token secret - * - * @return nothing - */ - function __construct($oauth_token = null, $oauth_token_secret = null) - { - $consumer_key = common_config('twitter', 'consumer_key'); - $consumer_secret = common_config('twitter', 'consumer_secret'); - - parent::__construct($consumer_key, $consumer_secret, - $oauth_token, $oauth_token_secret); - } - - // XXX: the following two functions are to support the horrible hack - // of using the credentils field in Foreign_link to store both - // the access token and token secret. This hack should go away with - // 0.9, in which we can make DB changes and add a new column for the - // token itself. - - static function packToken($token) - { - return implode(chr(0), array($token->key, $token->secret)); - } - - static function unpackToken($str) - { - $vals = explode(chr(0), $str); - return new OAuthToken($vals[0], $vals[1]); - } - - /** - * Builds a link to Twitter's endpoint for authorizing a request token - * - * @param OAuthToken $request_token token to authorize - * - * @return the link - */ - function getAuthorizeLink($request_token) - { - return parent::getAuthorizeLink(self::$authorizeURL, - $request_token, - common_local_url('twitterauthorization')); - } - - /** - * Calls Twitter's /account/verify_credentials API method - * - * @return mixed the Twitter user - */ - function verifyCredentials() - { - $url = 'https://twitter.com/account/verify_credentials.json'; - $response = $this->oAuthGet($url); - $twitter_user = json_decode($response); - return $twitter_user; - } - - /** - * Calls Twitter's /stutuses/update API method - * - * @param string $status text of the status - * @param int $in_reply_to_status_id optional id of the status it's - * a reply to - * - * @return mixed the status - */ - function statusesUpdate($status, $in_reply_to_status_id = null) - { - $url = 'https://twitter.com/statuses/update.json'; - $params = array('status' => $status, - 'in_reply_to_status_id' => $in_reply_to_status_id); - $response = $this->oAuthPost($url, $params); - $status = json_decode($response); - return $status; - } - - /** - * Calls Twitter's /stutuses/friends_timeline API method - * - * @param int $since_id show statuses after this id - * @param int $max_id show statuses before this id - * @param int $cnt number of statuses to show - * @param int $page page number - * - * @return mixed an array of statuses - */ - function statusesFriendsTimeline($since_id = null, $max_id = null, - $cnt = null, $page = null) - { - - $url = 'https://twitter.com/statuses/friends_timeline.json'; - $params = array('since_id' => $since_id, - 'max_id' => $max_id, - 'count' => $cnt, - 'page' => $page); - $qry = http_build_query($params); - - if (!empty($qry)) { - $url .= "?$qry"; - } - - $response = $this->oAuthGet($url); - $statuses = json_decode($response); - return $statuses; - } - - /** - * Calls Twitter's /stutuses/friends API method - * - * @param int $id id of the user whom you wish to see friends of - * @param int $user_id numerical user id - * @param int $screen_name screen name - * @param int $page page number - * - * @return mixed an array of twitter users and their latest status - */ - function statusesFriends($id = null, $user_id = null, $screen_name = null, - $page = null) - { - $url = "https://twitter.com/statuses/friends.json"; - - $params = array('id' => $id, - 'user_id' => $user_id, - 'screen_name' => $screen_name, - 'page' => $page); - $qry = http_build_query($params); - - if (!empty($qry)) { - $url .= "?$qry"; - } - - $response = $this->oAuthGet($url); - $friends = json_decode($response); - return $friends; - } - - /** - * Calls Twitter's /stutuses/friends/ids API method - * - * @param int $id id of the user whom you wish to see friends of - * @param int $user_id numerical user id - * @param int $screen_name screen name - * @param int $page page number - * - * @return mixed a list of ids, 100 per page - */ - function friendsIds($id = null, $user_id = null, $screen_name = null, - $page = null) - { - $url = "https://twitter.com/friends/ids.json"; - - $params = array('id' => $id, - 'user_id' => $user_id, - 'screen_name' => $screen_name, - 'page' => $page); - $qry = http_build_query($params); - - if (!empty($qry)) { - $url .= "?$qry"; - } - - $response = $this->oAuthGet($url); - $ids = json_decode($response); - return $ids; - } - -} diff --git a/plugins/TwitterBridge/TwitterBridgePlugin.php b/plugins/TwitterBridge/TwitterBridgePlugin.php new file mode 100644 index 000000000..f7daa9a22 --- /dev/null +++ b/plugins/TwitterBridge/TwitterBridgePlugin.php @@ -0,0 +1,101 @@ +. + * + * @category Plugin + * @package Laconica + * @author Zach Copley + * @copyright 2009 Control Yourself, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ + +if (!defined('LACONICA')) { + exit(1); +} + +/** + * Plugin for sending and importing Twitter statuses + * + * This class allows users to link their Twitter accounts + * + * @category Plugin + * @package Laconica + * @author Zach Copley + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + * @link http://twitter.com/ + */ + +class TwitterBridgePlugin extends Plugin +{ + /** + * Initializer for the plugin. + */ + + function __construct() + { + parent::__construct(); + } + + /** + * Add Twitter-related paths to the router table + * + * Hook for RouterInitialized event. + * + * @return boolean hook return + */ + + function onRouterInitialized(&$m) + { + $m->connect('twitter/authorization', array('action' => 'twitterauthorization')); + $m->connect('settings/twitter', array('action' => 'twittersettings')); + + return true; + } + + function onEndConnectSettingsNav(&$action) + { + $action_name = $action->trimmed('action'); + + $action->menuItem(common_local_url('twittersettings'), + _('Twitter'), + _('Twitter integration options'), + $action_name === 'twittersettings'); + + return true; + } + + function onAutoload($cls) + { + switch ($cls) + { + case 'TwittersettingsAction': + case 'TwitterauthorizationAction': + require_once(INSTALLDIR.'/plugins/TwitterBridge/' . strtolower(mb_substr($cls, 0, -6)) . '.php'); + return false; + case 'TwitterOAuthClient': + require_once(INSTALLDIR.'/plugins/TwitterBridge/twitteroAuthclient.php'); + return false; + default: + return true; + } + } + + +} \ No newline at end of file diff --git a/plugins/TwitterBridge/twitterauthorization.php b/plugins/TwitterBridge/twitterauthorization.php new file mode 100644 index 000000000..b04f35327 --- /dev/null +++ b/plugins/TwitterBridge/twitterauthorization.php @@ -0,0 +1,222 @@ +. + * + * @category Twitter + * @package Laconica + * @author Zach Copely + * @copyright 2009 Control Yourself, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ + +if (!defined('LACONICA')) { + exit(1); +} + +/** + * Class for doing OAuth authentication against Twitter + * + * Peforms the OAuth "dance" between Laconica and Twitter -- requests a token, + * authorizes it, and exchanges it for an access token. It also creates a link + * (Foreign_link) between the Laconica user and Twitter user and stores the + * access token and secret in the link. + * + * @category Twitter + * @package Laconica + * @author Zach Copley + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + * + */ +class TwitterauthorizationAction extends Action +{ + /** + * Initialize class members. Looks for 'oauth_token' parameter. + * + * @param array $args misc. arguments + * + * @return boolean true + */ + function prepare($args) + { + parent::prepare($args); + + $this->oauth_token = $this->arg('oauth_token'); + + return true; + } + + /** + * Handler method + * + * @param array $args is ignored since it's now passed in in prepare() + * + * @return nothing + */ + function handle($args) + { + parent::handle($args); + + if (!common_logged_in()) { + $this->clientError(_('Not logged in.'), 403); + } + + $user = common_current_user(); + $flink = Foreign_link::getByUserID($user->id, TWITTER_SERVICE); + + // If there's already a foreign link record, it means we already + // have an access token, and this is unecessary. So go back. + + if (isset($flink)) { + common_redirect(common_local_url('twittersettings')); + } + + // $this->oauth_token is only populated once Twitter authorizes our + // request token. If it's empty we're at the beginning of the auth + // process + + if (empty($this->oauth_token)) { + $this->authorizeRequestToken(); + } else { + $this->saveAccessToken(); + } + } + + /** + * Asks Twitter for a request token, and then redirects to Twitter + * to authorize it. + * + * @return nothing + */ + function authorizeRequestToken() + { + try { + + // Get a new request token and authorize it + + $client = new TwitterOAuthClient(); + $req_tok = + $client->getRequestToken(TwitterOAuthClient::$requestTokenURL); + + // Sock the request token away in the session temporarily + + $_SESSION['twitter_request_token'] = $req_tok->key; + $_SESSION['twitter_request_token_secret'] = $req_tok->secret; + + $auth_link = $client->getAuthorizeLink($req_tok); + + } catch (TwitterOAuthClientException $e) { + $msg = sprintf('OAuth client cURL error - code: %1s, msg: %2s', + $e->getCode(), $e->getMessage()); + $this->serverError(_('Couldn\'t link your Twitter account.')); + } + + common_redirect($auth_link); + } + + /** + * Called when Twitter returns an authorized request token. Exchanges + * it for an access token and stores it. + * + * @return nothing + */ + function saveAccessToken() + { + + // Check to make sure Twitter returned the same request + // token we sent them + + if ($_SESSION['twitter_request_token'] != $this->oauth_token) { + $this->serverError(_('Couldn\'t link your Twitter account.')); + } + + try { + + $client = new TwitterOAuthClient($_SESSION['twitter_request_token'], + $_SESSION['twitter_request_token_secret']); + + // Exchange the request token for an access token + + $atok = $client->getAccessToken(TwitterOAuthClient::$accessTokenURL); + + // Test the access token and get the user's Twitter info + + $client = new TwitterOAuthClient($atok->key, $atok->secret); + $twitter_user = $client->verifyCredentials(); + + } catch (OAuthClientException $e) { + $msg = sprintf('OAuth client cURL error - code: %1$s, msg: %2$s', + $e->getCode(), $e->getMessage()); + $this->serverError(_('Couldn\'t link your Twitter account.')); + } + + // Save the access token and Twitter user info + + $this->saveForeignLink($atok, $twitter_user); + + // Clean up the the mess we made in the session + + unset($_SESSION['twitter_request_token']); + unset($_SESSION['twitter_request_token_secret']); + + common_redirect(common_local_url('twittersettings')); + } + + /** + * Saves a Foreign_link between Twitter user and local user, + * which includes the access token and secret. + * + * @param OAuthToken $access_token the access token to save + * @param mixed $twitter_user twitter API user object + * + * @return nothing + */ + function saveForeignLink($access_token, $twitter_user) + { + $user = common_current_user(); + + $flink = new Foreign_link(); + + $flink->user_id = $user->id; + $flink->foreign_id = $twitter_user->id; + $flink->service = TWITTER_SERVICE; + + $creds = TwitterOAuthClient::packToken($access_token); + + $flink->credentials = $creds; + $flink->created = common_sql_now(); + + // Defaults: noticesync on, everything else off + + $flink->set_flags(true, false, false, false); + + $flink_id = $flink->insert(); + + if (empty($flink_id)) { + common_log_db_error($flink, 'INSERT', __FILE__); + $this->serverError(_('Couldn\'t link your Twitter account.')); + } + + save_twitter_user($twitter_user->id, $twitter_user->screen_name); + } + +} + diff --git a/plugins/TwitterBridge/twitteroauthclient.php b/plugins/TwitterBridge/twitteroauthclient.php new file mode 100644 index 000000000..b7dc4a80c --- /dev/null +++ b/plugins/TwitterBridge/twitteroauthclient.php @@ -0,0 +1,220 @@ +. + * + * @category Integration + * @package Laconica + * @author Zach Copley + * @copyright 2008 Control Yourself, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ + +if (!defined('LACONICA')) { + exit(1); +} + +/** + * Class for talking to the Twitter API with OAuth. + * + * @category Integration + * @package Laconica + * @author Zach Copley + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + * + */ +class TwitterOAuthClient extends OAuthClient +{ + public static $requestTokenURL = 'https://twitter.com/oauth/request_token'; + public static $authorizeURL = 'https://twitter.com/oauth/authorize'; + public static $accessTokenURL = 'https://twitter.com/oauth/access_token'; + + /** + * Constructor + * + * @param string $oauth_token the user's token + * @param string $oauth_token_secret the user's token secret + * + * @return nothing + */ + function __construct($oauth_token = null, $oauth_token_secret = null) + { + $consumer_key = common_config('twitter', 'consumer_key'); + $consumer_secret = common_config('twitter', 'consumer_secret'); + + parent::__construct($consumer_key, $consumer_secret, + $oauth_token, $oauth_token_secret); + } + + // XXX: the following two functions are to support the horrible hack + // of using the credentils field in Foreign_link to store both + // the access token and token secret. This hack should go away with + // 0.9, in which we can make DB changes and add a new column for the + // token itself. + + static function packToken($token) + { + return implode(chr(0), array($token->key, $token->secret)); + } + + static function unpackToken($str) + { + $vals = explode(chr(0), $str); + return new OAuthToken($vals[0], $vals[1]); + } + + /** + * Builds a link to Twitter's endpoint for authorizing a request token + * + * @param OAuthToken $request_token token to authorize + * + * @return the link + */ + function getAuthorizeLink($request_token) + { + return parent::getAuthorizeLink(self::$authorizeURL, + $request_token, + common_local_url('twitterauthorization')); + } + + /** + * Calls Twitter's /account/verify_credentials API method + * + * @return mixed the Twitter user + */ + function verifyCredentials() + { + $url = 'https://twitter.com/account/verify_credentials.json'; + $response = $this->oAuthGet($url); + $twitter_user = json_decode($response); + return $twitter_user; + } + + /** + * Calls Twitter's /stutuses/update API method + * + * @param string $status text of the status + * @param int $in_reply_to_status_id optional id of the status it's + * a reply to + * + * @return mixed the status + */ + function statusesUpdate($status, $in_reply_to_status_id = null) + { + $url = 'https://twitter.com/statuses/update.json'; + $params = array('status' => $status, + 'in_reply_to_status_id' => $in_reply_to_status_id); + $response = $this->oAuthPost($url, $params); + $status = json_decode($response); + return $status; + } + + /** + * Calls Twitter's /stutuses/friends_timeline API method + * + * @param int $since_id show statuses after this id + * @param int $max_id show statuses before this id + * @param int $cnt number of statuses to show + * @param int $page page number + * + * @return mixed an array of statuses + */ + function statusesFriendsTimeline($since_id = null, $max_id = null, + $cnt = null, $page = null) + { + + $url = 'https://twitter.com/statuses/friends_timeline.json'; + $params = array('since_id' => $since_id, + 'max_id' => $max_id, + 'count' => $cnt, + 'page' => $page); + $qry = http_build_query($params); + + if (!empty($qry)) { + $url .= "?$qry"; + } + + $response = $this->oAuthGet($url); + $statuses = json_decode($response); + return $statuses; + } + + /** + * Calls Twitter's /stutuses/friends API method + * + * @param int $id id of the user whom you wish to see friends of + * @param int $user_id numerical user id + * @param int $screen_name screen name + * @param int $page page number + * + * @return mixed an array of twitter users and their latest status + */ + function statusesFriends($id = null, $user_id = null, $screen_name = null, + $page = null) + { + $url = "https://twitter.com/statuses/friends.json"; + + $params = array('id' => $id, + 'user_id' => $user_id, + 'screen_name' => $screen_name, + 'page' => $page); + $qry = http_build_query($params); + + if (!empty($qry)) { + $url .= "?$qry"; + } + + $response = $this->oAuthGet($url); + $friends = json_decode($response); + return $friends; + } + + /** + * Calls Twitter's /stutuses/friends/ids API method + * + * @param int $id id of the user whom you wish to see friends of + * @param int $user_id numerical user id + * @param int $screen_name screen name + * @param int $page page number + * + * @return mixed a list of ids, 100 per page + */ + function friendsIds($id = null, $user_id = null, $screen_name = null, + $page = null) + { + $url = "https://twitter.com/friends/ids.json"; + + $params = array('id' => $id, + 'user_id' => $user_id, + 'screen_name' => $screen_name, + 'page' => $page); + $qry = http_build_query($params); + + if (!empty($qry)) { + $url .= "?$qry"; + } + + $response = $this->oAuthGet($url); + $ids = json_decode($response); + return $ids; + } + +} diff --git a/plugins/TwitterBridge/twittersettings.php b/plugins/TwitterBridge/twittersettings.php new file mode 100644 index 000000000..a3e02e125 --- /dev/null +++ b/plugins/TwitterBridge/twittersettings.php @@ -0,0 +1,272 @@ +. + * + * @category Settings + * @package Laconica + * @author Evan Prodromou + * @copyright 2008-2009 Control Yourself, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ + +if (!defined('LACONICA')) { + exit(1); +} + +require_once INSTALLDIR.'/lib/connectsettingsaction.php'; +require_once INSTALLDIR.'/lib/twitter.php'; + +/** + * Settings for Twitter integration + * + * @category Settings + * @package Laconica + * @author Evan Prodromou + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + * + * @see SettingsAction + */ + +class TwittersettingsAction extends ConnectSettingsAction +{ + /** + * Title of the page + * + * @return string Title of the page + */ + + function title() + { + return _('Twitter settings'); + } + + /** + * Instructions for use + * + * @return instructions for use + */ + + function getInstructions() + { + return _('Connect your Twitter account to share your updates ' . + 'with your Twitter friends and vice-versa.'); + } + + /** + * Content area of the page + * + * Shows a form for associating a Twitter account with this + * Laconica account. Also lets the user set preferences. + * + * @return void + */ + + function showContent() + { + + $user = common_current_user(); + + $profile = $user->getProfile(); + + $fuser = null; + + $flink = Foreign_link::getByUserID($user->id, TWITTER_SERVICE); + + if (!empty($flink)) { + $fuser = $flink->getForeignUser(); + } + + $this->elementStart('form', array('method' => 'post', + 'id' => 'form_settings_twitter', + 'class' => 'form_settings', + 'action' => + common_local_url('twittersettings'))); + + $this->hidden('token', common_session_token()); + + $this->elementStart('fieldset', array('id' => 'settings_twitter_account')); + + if (empty($fuser)) { + $this->elementStart('ul', 'form_data'); + $this->elementStart('li', array('id' => 'settings_twitter_login_button')); + $this->element('a', array('href' => common_local_url('twitterauthorization')), + 'Connect my Twitter account'); + $this->elementEnd('li'); + $this->elementEnd('ul'); + + $this->elementEnd('fieldset'); + } else { + $this->element('legend', null, _('Twitter account')); + $this->elementStart('p', array('id' => 'form_confirmed')); + $this->element('a', array('href' => $fuser->uri), $fuser->nickname); + $this->elementEnd('p'); + $this->element('p', 'form_note', + _('Connected Twitter account')); + + $this->submit('remove', _('Remove')); + + $this->elementEnd('fieldset'); + + $this->elementStart('fieldset', array('id' => 'settings_twitter_preferences')); + + $this->element('legend', null, _('Preferences')); + $this->elementStart('ul', 'form_data'); + $this->elementStart('li'); + $this->checkbox('noticesend', + _('Automatically send my notices to Twitter.'), + ($flink) ? + ($flink->noticesync & FOREIGN_NOTICE_SEND) : + true); + $this->elementEnd('li'); + $this->elementStart('li'); + $this->checkbox('replysync', + _('Send local "@" replies to Twitter.'), + ($flink) ? + ($flink->noticesync & FOREIGN_NOTICE_SEND_REPLY) : + true); + $this->elementEnd('li'); + $this->elementStart('li'); + $this->checkbox('friendsync', + _('Subscribe to my Twitter friends here.'), + ($flink) ? + ($flink->friendsync & FOREIGN_FRIEND_RECV) : + false); + $this->elementEnd('li'); + + if (common_config('twitterbridge','enabled')) { + $this->elementStart('li'); + $this->checkbox('noticerecv', + _('Import my Friends Timeline.'), + ($flink) ? + ($flink->noticesync & FOREIGN_NOTICE_RECV) : + false); + $this->elementEnd('li'); + + // preserve setting even if bidrection bridge toggled off + + if ($flink && ($flink->noticesync & FOREIGN_NOTICE_RECV)) { + $this->hidden('noticerecv', true, 'noticerecv'); + } + } + + $this->elementEnd('ul'); + + if ($flink) { + $this->submit('save', _('Save')); + } else { + $this->submit('add', _('Add')); + } + + $this->elementEnd('fieldset'); + } + + $this->elementEnd('form'); + } + + /** + * Handle posts to this form + * + * Based on the button that was pressed, muxes out to other functions + * to do the actual task requested. + * + * All sub-functions reload the form with a message -- success or failure. + * + * @return void + */ + + function handlePost() + { + // CSRF protection + $token = $this->trimmed('token'); + if (!$token || $token != common_session_token()) { + $this->showForm(_('There was a problem with your session token. '. + 'Try again, please.')); + return; + } + + if ($this->arg('save')) { + $this->savePreferences(); + } else if ($this->arg('remove')) { + $this->removeTwitterAccount(); + } else { + $this->showForm(_('Unexpected form submission.')); + } + } + + /** + * Disassociate an existing Twitter account from this account + * + * @return void + */ + + function removeTwitterAccount() + { + $user = common_current_user(); + $flink = Foreign_link::getByUserID($user->id, TWITTER_SERVICE); + + $result = $flink->delete(); + + if (empty($result)) { + common_log_db_error($flink, 'DELETE', __FILE__); + $this->serverError(_('Couldn\'t remove Twitter user.')); + return; + } + + $this->showForm(_('Twitter account removed.'), true); + } + + /** + * Save user's Twitter-bridging preferences + * + * @return void + */ + + function savePreferences() + { + $noticesend = $this->boolean('noticesend'); + $noticerecv = $this->boolean('noticerecv'); + $friendsync = $this->boolean('friendsync'); + $replysync = $this->boolean('replysync'); + + $user = common_current_user(); + $flink = Foreign_link::getByUserID($user->id, TWITTER_SERVICE); + + if (empty($flink)) { + common_log_db_error($flink, 'SELECT', __FILE__); + $this->showForm(_('Couldn\'t save Twitter preferences.')); + return; + } + + $original = clone($flink); + $flink->set_flags($noticesend, $noticerecv, $replysync, $friendsync); + $result = $flink->update($original); + + if ($result === false) { + common_log_db_error($flink, 'UPDATE', __FILE__); + $this->showForm(_('Couldn\'t save Twitter preferences.')); + return; + } + + $this->showForm(_('Twitter preferences saved.'), true); + } + +} -- cgit v1.2.3-54-g00ecf