From: Zach Copley Date: Mon, 10 Aug 2009 07:49:51 +0000 (+0000) Subject: Merge branch 'twitter-oauth' into 0.8.x X-Git-Url: https://git.mxchange.org/?a=commitdiff_plain;h=17dcf1c3170bc905e3b0fdffc804d6f7229ac61b;hp=c1b19929f6234c4b9e30e16bae419c89c38c1169;p=quix0rs-gnu-social.git Merge branch 'twitter-oauth' into 0.8.x Conflicts: scripts/getvaliddaemons.php --- diff --git a/actions/twitterauthorization.php b/actions/twitterauthorization.php new file mode 100644 index 0000000000..b04f353278 --- /dev/null +++ b/actions/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/actions/twittersettings.php b/actions/twittersettings.php index 3343dba95a..0859ab9d34 100644 --- a/actions/twittersettings.php +++ b/actions/twittersettings.php @@ -34,8 +34,6 @@ if (!defined('LACONICA')) { require_once INSTALLDIR.'/lib/connectsettingsaction.php'; require_once INSTALLDIR.'/lib/twitter.php'; -define('SUBSCRIPTIONS', 80); - /** * Settings for Twitter integration * @@ -69,9 +67,8 @@ class TwittersettingsAction extends ConnectSettingsAction function getInstructions() { - return _('Add your Twitter account to automatically send '. - ' your notices to Twitter, ' . - 'and subscribe to Twitter friends already here.'); + return _('Connect your Twitter account to share your updates ' . + 'with your Twitter friends and vice-versa.'); } /** @@ -99,7 +96,7 @@ class TwittersettingsAction extends ConnectSettingsAction $flink = Foreign_link::getByUserID($user->id, TWITTER_SERVICE); - if ($flink) { + if (!empty($flink)) { $fuser = $flink->getForeignUser(); } @@ -108,192 +105,86 @@ class TwittersettingsAction extends ConnectSettingsAction 'class' => 'form_settings', 'action' => common_local_url('twittersettings'))); - $this->elementStart('fieldset', array('id' => 'settings_twitter_account')); - $this->element('legend', null, _('Twitter Account')); + $this->hidden('token', common_session_token()); - if ($fuser) { + + $this->elementStart('fieldset', array('id' => 'settings_twitter_account')); + + if (empty($fuser)) { $this->elementStart('ul', 'form_data'); - $this->elementStart('li', array('id' => 'settings_twitter_remove')); - $this->element('span', 'twitter_user', $fuser->nickname); - $this->element('a', array('href' => $fuser->uri), $fuser->uri); - $this->element('p', 'form_note', - _('Current verified Twitter account.')); - $this->hidden('flink_foreign_id', $flink->foreign_id); + $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->submit('remove', _('Remove')); + + $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', array('id' => 'settings_twitter_login')); - $this->input('twitter_username', _('Twitter user name'), - ($this->arg('twitter_username')) ? - $this->arg('twitter_username') : - $profile->nickname, - _('No spaces, please.')); // hey, it's what Twitter says + $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->password('twitter_password', _('Twitter password')); - $this->elementend('li'); - $this->elementEnd('ul'); - } - $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->checkbox('replysync', + _('Send local "@" replies to Twitter.'), + ($flink) ? + ($flink->noticesync & FOREIGN_NOTICE_SEND_REPLY) : + true); + $this->elementEnd('li'); $this->elementStart('li'); - $this->checkbox('noticerecv', - _('Import my Friends Timeline.'), + $this->checkbox('friendsync', + _('Subscribe to my Twitter friends here.'), ($flink) ? - ($flink->noticesync & FOREIGN_NOTICE_RECV) : + ($flink->friendsync & FOREIGN_FRIEND_RECV) : false); $this->elementEnd('li'); - } else { - // 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->showTwitterSubscriptions(); - - $this->elementEnd('form'); - } - /** - * Gets some of the user's Twitter friends - * - * Gets the number of Twitter friends that are on this - * instance of Laconica. - * - * @return array array of User objects - */ - - function subscribedTwitterUsers() - { - - $current_user = common_current_user(); - - $qry = 'SELECT "user".* ' . - 'FROM subscription ' . - 'JOIN "user" ON subscription.subscribed = "user".id ' . - 'JOIN foreign_link ON foreign_link.user_id = "user".id ' . - 'WHERE subscriber = %d ' . - 'ORDER BY "user".nickname'; - - $user = new User(); - - $user->query(sprintf($qry, $current_user->id)); - - $users = array(); - - while ($user->fetch()) { - - // Don't include the user's own self-subscription - if ($user->id != $current_user->id) { - $users[] = clone($user); - } - } - - return $users; - } - - /** - * Show user's Twitter friends - * - * Gets the number of Twitter friends that are on this - * instance of Laconica, and shows their mini-avatars. - * - * @return void - */ - - function showTwitterSubscriptions() - { - - $friends = $this->subscribedTwitterUsers(); - - $friends_count = count($friends); - - if ($friends_count > 0) { - $this->elementStart('div', array('id' => 'entity_subscriptions', - 'class' => 'section')); - $this->element('h2', null, _('Twitter Friends')); - $this->elementStart('ul', 'entities users xoxo'); - - for ($i = 0; $i < min($friends_count, SUBSCRIPTIONS); $i++) { + 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'); - $other = Profile::staticGet($friends[$i]->id); + // preserve setting even if bidrection bridge toggled off - if (!$other) { - common_log_db_error($subs, 'SELECT', __FILE__); - continue; + if ($flink && ($flink->noticesync & FOREIGN_NOTICE_RECV)) { + $this->hidden('noticerecv', true, 'noticerecv'); } - - $this->elementStart('li', 'vcard'); - $this->elementStart('a', array('title' => ($other->fullname) ? - $other->fullname : - $other->nickname, - 'href' => $other->profileurl, - 'class' => 'url')); - - $avatar = $other->getAvatar(AVATAR_MINI_SIZE); - - $avatar_url = ($avatar) ? - $avatar->displayUrl() : - Avatar::defaultImage(AVATAR_MINI_SIZE); - - $this->element('img', array('src' => $avatar_url, - 'width' => AVATAR_MINI_SIZE, - 'height' => AVATAR_MINI_SIZE, - 'class' => 'avatar photo', - 'alt' => ($other->fullname) ? - $other->fullname : - $other->nickname)); - - $this->element('span', 'fn nickname', $other->nickname); - $this->elementEnd('a'); - $this->elementEnd('li'); - } $this->elementEnd('ul'); - $this->elementEnd('div'); + if ($flink) { + $this->submit('save', _('Save')); + } else { + $this->submit('add', _('Add')); + } + + $this->elementEnd('fieldset'); } + + $this->elementEnd('form'); } /** @@ -309,7 +200,6 @@ class TwittersettingsAction extends ConnectSettingsAction function handlePost() { - // CSRF protection $token = $this->trimmed('token'); if (!$token || $token != common_session_token()) { @@ -320,8 +210,6 @@ class TwittersettingsAction extends ConnectSettingsAction if ($this->arg('save')) { $this->savePreferences(); - } else if ($this->arg('add')) { - $this->addTwitterAccount(); } else if ($this->arg('remove')) { $this->removeTwitterAccount(); } else { @@ -329,82 +217,6 @@ class TwittersettingsAction extends ConnectSettingsAction } } - /** - * Associate a Twitter account with the user's account - * - * Validates post input; verifies it against Twitter; and if - * successful stores in the database. - * - * @return void - */ - - function addTwitterAccount() - { - $screen_name = $this->trimmed('twitter_username'); - $password = $this->trimmed('twitter_password'); - $noticesend = $this->boolean('noticesend'); - $noticerecv = $this->boolean('noticerecv'); - $replysync = $this->boolean('replysync'); - $friendsync = $this->boolean('friendsync'); - - if (!Validate::string($screen_name, - array('min_length' => 1, - 'max_length' => 15, - 'format' => VALIDATE_NUM.VALIDATE_ALPHA.'_'))) { - $this->showForm(_('Username must have only numbers, '. - 'upper- and lowercase letters, '. - 'and underscore (_). 15 chars max.')); - return; - } - - if (!$this->verifyCredentials($screen_name, $password)) { - $this->showForm(_('Could not verify your Twitter credentials!')); - return; - } - - $twit_user = twitter_user_info($screen_name, $password); - - if (!$twit_user) { - $this->showForm(sprintf(_('Unable to retrieve account information '. - 'For "%s" from Twitter.'), - $screen_name)); - return; - } - - if (!save_twitter_user($twit_user->id, $screen_name)) { - $this->showForm(_('Unable to save your Twitter settings!')); - return; - } - - $user = common_current_user(); - - $flink = new Foreign_link(); - - $flink->user_id = $user->id; - $flink->foreign_id = $twit_user->id; - $flink->service = TWITTER_SERVICE; - $flink->credentials = $password; - $flink->created = common_sql_now(); - - $flink->set_flags($noticesend, $noticerecv, $replysync, $friendsync); - - $flink_id = $flink->insert(); - - if (!$flink_id) { - common_log_db_error($flink, 'INSERT', __FILE__); - $this->showForm(_('Unable to save your Twitter settings!')); - return; - } - - if ($friendsync) { - save_twitter_friends($user, $twit_user->id, $screen_name, $password); - $flink->last_friendsync = common_sql_now(); - $flink->update(); - } - - $this->showForm(_('Twitter settings saved.'), true); - } - /** * Disassociate an existing Twitter account from this account * @@ -414,20 +226,11 @@ class TwittersettingsAction extends ConnectSettingsAction function removeTwitterAccount() { $user = common_current_user(); - - $flink = Foreign_link::getByUserID($user->id, 1); - - $flink_foreign_id = $this->arg('flink_foreign_id'); - - // Maybe an old tab open...? - if ($flink->foreign_id != $flink_foreign_id) { - $this->showForm(_('That is not your Twitter account.')); - return; - } + $flink = Foreign_link::getByUserID($user->id, TWITTER_SERVICE); $result = $flink->delete(); - if (!$result) { + if (empty($result)) { common_log_db_error($flink, 'DELETE', __FILE__); $this->serverError(_('Couldn\'t remove Twitter user.')); return; @@ -450,32 +253,16 @@ class TwittersettingsAction extends ConnectSettingsAction $replysync = $this->boolean('replysync'); $user = common_current_user(); + $flink = Foreign_link::getByUserID($user->id, TWITTER_SERVICE); - $flink = Foreign_link::getByUserID($user->id, 1); - - if (!$flink) { + if (empty($flink)) { common_log_db_error($flink, 'SELECT', __FILE__); $this->showForm(_('Couldn\'t save Twitter preferences.')); return; } - $twitter_id = $flink->foreign_id; - $password = $flink->credentials; - - $fuser = $flink->getForeignUser(); - - if (!$fuser) { - common_log_db_error($fuser, 'SELECT', __FILE__); - $this->showForm(_('Couldn\'t save Twitter preferences.')); - return; - } - - $screen_name = $fuser->nickname; - $original = clone($flink); - $flink->set_flags($noticesend, $noticerecv, $replysync, $friendsync); - $result = $flink->update($original); if ($result === false) { @@ -484,45 +271,7 @@ class TwittersettingsAction extends ConnectSettingsAction return; } - if ($friendsync) { - save_twitter_friends($user, $flink->foreign_id, $screen_name, $password); - } - $this->showForm(_('Twitter preferences saved.'), true); } - /** - * Verifies a username and password against Twitter's API - * - * @param string $screen_name Twitter user name - * @param string $password Twitter password - * - * @return boolean success flag - */ - - function verifyCredentials($screen_name, $password) - { - $uri = 'http://twitter.com/account/verify_credentials.json'; - - $data = get_twitter_data($uri, $screen_name, $password); - - if (!$data) { - return false; - } - - $user = json_decode($data); - - if (!$user) { - return false; - } - - $twitter_id = $user->id; - - if ($twitter_id) { - return $twitter_id; - } - - return false; - } - } diff --git a/config.php.sample b/config.php.sample index 21b6865e15..c2dcf85169 100644 --- a/config.php.sample +++ b/config.php.sample @@ -182,6 +182,10 @@ $config['sphinx']['port'] = 3312; // // $config['twitterbridge']['enabled'] = true; +// Twitter OAuth settings +// $config['twitter']['consumer_key'] = 'YOURKEY'; +// $config['twitter']['consumer_secret'] = 'YOURSECRET'; + // Edit throttling. Off by default. If turned on, you can only post 20 notices // every 10 minutes. Admins may want to play with the settings to minimize inconvenience for // real users without getting uncontrollable floods from spammers or runaway bots. diff --git a/lib/common.php b/lib/common.php index be30519f47..645ae8c344 100644 --- a/lib/common.php +++ b/lib/common.php @@ -194,6 +194,9 @@ $config = 'integration' => array('source' => 'Laconica', # source attribute for Twitter 'taguri' => $_server.',2009'), # base for tag URIs + 'twitter' => + array('consumer_key' => null, + 'consumer_secret' => null), 'memcached' => array('enabled' => false, 'server' => 'localhost', diff --git a/lib/mail.php b/lib/mail.php index 48e2cff658..33d1eb7549 100644 --- a/lib/mail.php +++ b/lib/mail.php @@ -657,13 +657,14 @@ function mail_twitter_bridge_removed($user) $subject = sprintf(_('Your Twitter bridge has been disabled.')); - $body = sprintf(_("Hi, %1\$s. We're sorry to inform you that your " . - 'link to Twitter has been disabled. Your Twitter credentials ' . - 'have either changed (did you recently change your Twitter ' . - 'password?) or you have otherwise revoked our access to your ' . - "Twitter account.\n\n" . - 'You can re-enable your Twitter bridge by visiting your ' . - "Twitter settings page:\n\n\t%2\$s\n\n" . + $site_name = common_config('site', 'name'); + + $body = sprintf(_('Hi, %1$s. We\'re sorry to inform you that your ' . + 'link to Twitter has been disabled. We no longer seem to have ' . + 'permission to update your Twitter status. (Did you revoke ' . + '%3$s\'s access?)' . "\n\n" . + 'You can re-enable your Twitter bridge by visiting your ' . + "Twitter settings page:\n\n\t%2\$s\n\n" . "Regards,\n%3\$s\n"), $profile->getBestName(), common_local_url('twittersettings'), @@ -691,11 +692,11 @@ function mail_facebook_app_removed($user) $site_name = common_config('site', 'name'); $subject = sprintf( - _('Your %1\$s Facebook application access has been disabled.', + _('Your %1$s Facebook application access has been disabled.', $site_name)); $body = sprintf(_("Hi, %1\$s. We're sorry to inform you that we are " . - 'unable to update your Facebook status from %2\$s, and have disabled ' . + 'unable to update your Facebook status from %2$s, and have disabled ' . 'the Facebook application for your account. This may be because ' . 'you have removed the Facebook application\'s authorization, or ' . 'have deleted your Facebook account. You can re-enable the ' . diff --git a/lib/oauthclient.php b/lib/oauthclient.php new file mode 100644 index 0000000000..b66a24be44 --- /dev/null +++ b/lib/oauthclient.php @@ -0,0 +1,225 @@ +. + * + * @category Action + * @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); +} + +require_once 'OAuth.php'; + +/** + * Exception wrapper for cURL errors + * + * @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 OAuthClientCurlException extends Exception +{ +} + +/** + * Base class for doing OAuth calls as a consumer + * + * @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 OAuthClient +{ + var $consumer; + var $token; + + /** + * Constructor + * + * Can be initialized with just consumer key and secret for requesting new + * tokens or with additional request token or access token + * + * @param string $consumer_key consumer key + * @param string $consumer_secret consumer secret + * @param string $oauth_token user's token + * @param string $oauth_token_secret user's secret + * + * @return nothing + */ + function __construct($consumer_key, $consumer_secret, + $oauth_token = null, $oauth_token_secret = null) + { + $this->sha1_method = new OAuthSignatureMethod_HMAC_SHA1(); + $this->consumer = new OAuthConsumer($consumer_key, $consumer_secret); + $this->token = null; + + if (isset($oauth_token) && isset($oauth_token_secret)) { + $this->token = new OAuthToken($oauth_token, $oauth_token_secret); + } + } + + /** + * Gets a request token from the given url + * + * @param string $url OAuth endpoint for grabbing request tokens + * + * @return OAuthToken $token the request token + */ + function getRequestToken($url) + { + $response = $this->oAuthGet($url); + parse_str($response); + $token = new OAuthToken($oauth_token, $oauth_token_secret); + return $token; + } + + /** + * Builds a link that can be redirected to in order to + * authorize a request token. + * + * @param string $url endpoint for authorizing request tokens + * @param OAuthToken $request_token the request token to be authorized + * @param string $oauth_callback optional callback url + * + * @return string $authorize_url the url to redirect to + */ + function getAuthorizeLink($url, $request_token, $oauth_callback = null) + { + $authorize_url = $url . '?oauth_token=' . + $request_token->key; + + if (isset($oauth_callback)) { + $authorize_url .= '&oauth_callback=' . urlencode($oauth_callback); + } + + return $authorize_url; + } + + /** + * Fetches an access token + * + * @param string $url OAuth endpoint for exchanging authorized request tokens + * for access tokens + * + * @return OAuthToken $token the access token + */ + function getAccessToken($url) + { + $response = $this->oAuthPost($url); + parse_str($response); + $token = new OAuthToken($oauth_token, $oauth_token_secret); + return $token; + } + + /** + * Use HTTP GET to make a signed OAuth request + * + * @param string $url OAuth endpoint + * + * @return mixed the request + */ + function oAuthGet($url) + { + $request = OAuthRequest::from_consumer_and_token($this->consumer, + $this->token, 'GET', $url, null); + $request->sign_request($this->sha1_method, + $this->consumer, $this->token); + + return $this->httpRequest($request->to_url()); + } + + /** + * Use HTTP POST to make a signed OAuth request + * + * @param string $url OAuth endpoint + * @param array $params additional post parameters + * + * @return mixed the request + */ + function oAuthPost($url, $params = null) + { + $request = OAuthRequest::from_consumer_and_token($this->consumer, + $this->token, 'POST', $url, $params); + $request->sign_request($this->sha1_method, + $this->consumer, $this->token); + + return $this->httpRequest($request->get_normalized_http_url(), + $request->to_postdata()); + } + + /** + * Make a HTTP request using cURL. + * + * @param string $url Where to make the + * @param array $params post parameters + * + * @return mixed the request + */ + function httpRequest($url, $params = null) + { + $options = array( + CURLOPT_RETURNTRANSFER => true, + CURLOPT_FAILONERROR => true, + CURLOPT_HEADER => false, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_USERAGENT => 'Laconica', + CURLOPT_CONNECTTIMEOUT => 120, + CURLOPT_TIMEOUT => 120, + CURLOPT_HTTPAUTH => CURLAUTH_ANY, + CURLOPT_SSL_VERIFYPEER => false, + + // Twitter is strict about accepting invalid "Expect" headers + + CURLOPT_HTTPHEADER => array('Expect:') + ); + + if (isset($params)) { + $options[CURLOPT_POST] = true; + $options[CURLOPT_POSTFIELDS] = $params; + } + + $ch = curl_init($url); + curl_setopt_array($ch, $options); + $response = curl_exec($ch); + + if ($response === false) { + $msg = curl_error($ch); + $code = curl_errno($ch); + throw new OAuthClientCurlException($msg, $code); + } + + curl_close($ch); + + return $response; + } + +} diff --git a/lib/parallelizingdaemon.php b/lib/parallelizingdaemon.php new file mode 100644 index 0000000000..dc28b56435 --- /dev/null +++ b/lib/parallelizingdaemon.php @@ -0,0 +1,229 @@ +. + * + * @category Daemon + * @package Laconica + * @author Zach Copley + * @author Evan Prodromou + * @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); +} + +declare(ticks = 1); + +/** + * Daemon able to spawn multiple child processes to do work in parallel + * + * @category Daemon + * @package Laconica + * @author Zach Copley + * @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/ + */ + +class ParallelizingDaemon extends Daemon +{ + private $_children = array(); + private $_interval = 0; // seconds + private $_max_children = 0; // maximum number of children + private $_debug = false; + + /** + * Constructor + * + * @param string $id the name/id of this daemon + * @param int $interval sleep this long before doing everything again + * @param int $max_children maximum number of child processes at a time + * @param boolean $debug debug output flag + * + * @return void + * + **/ + + function __construct($id = null, $interval = 60, $max_children = 2, + $debug = null) + { + parent::__construct(true); // daemonize + + $this->_interval = $interval; + $this->_max_children = $max_children; + $this->_debug = $debug; + + if (isset($id)) { + $this->set_id($id); + } + } + + /** + * Run the daemon + * + * @return void + */ + + function run() + { + if (isset($this->_debug)) { + echo $this->name() . " - Debugging output enabled.\n"; + } + + do { + + $objects = $this->getObjects(); + + foreach ($objects as $o) { + + // Fork a child for each object + + $pid = pcntl_fork(); + + if ($pid == -1) { + die ($this->name() . ' - Couldn\'t fork!'); + } + + if ($pid) { + + // Parent + + if (isset($this->_debug)) { + echo $this->name() . + " - Forked new child - pid $pid.\n"; + + } + + $this->_children[] = $pid; + + } else { + + // Child + + // Do something with each object + + $this->childTask($o); + + exit(); + } + + // Remove child from ps list as it finishes + + while (($c = pcntl_wait($status, WNOHANG OR WUNTRACED)) > 0) { + + if (isset($this->_debug)) { + echo $this->name() . " - Child $c finished.\n"; + } + + $this->removePs($this->_children, $c); + } + + // Wait! We have too many damn kids. + + if (sizeof($this->_children) >= $this->_max_children) { + + if (isset($this->_debug)) { + echo $this->name() . " - Too many children. Waiting...\n"; + } + + if (($c = pcntl_wait($status, WUNTRACED)) > 0) { + + if (isset($this->_debug)) { + echo $this->name() . + " - Finished waiting for child $c.\n"; + } + + $this->removePs($this->_children, $c); + } + } + } + + // Remove all children from the process list before restarting + while (($c = pcntl_wait($status, WUNTRACED)) > 0) { + + if (isset($this->_debug)) { + echo $this->name() . " - Child $c finished.\n"; + } + + $this->removePs($this->_children, $c); + } + + // Rest for a bit + + if (isset($this->_debug)) { + echo $this->name() . ' - Waiting ' . $this->_interval . + " secs before running again.\n"; + } + + if ($this->_interval > 0) { + sleep($this->_interval); + } + + } while (true); + } + + /** + * Remove a child process from the list of children + * + * @param array &$plist array of processes + * @param int $ps process id + * + * @return void + */ + + function removePs(&$plist, $ps) + { + for ($i = 0; $i < sizeof($plist); $i++) { + if ($plist[$i] == $ps) { + unset($plist[$i]); + $plist = array_values($plist); + break; + } + } + } + + /** + * Get a list of objects to work on in parallel + * + * @return array An array of objects to work on + */ + + function getObjects() + { + die('Implement ParallelizingDaemon::getObjects().'); + } + + /** + * Do something with each object in parallel + * + * @param mixed $object data to work on + * + * @return void + */ + + function childTask($object) + { + die("Implement ParallelizingDaemon::childTask($object)."); + } + +} \ No newline at end of file diff --git a/lib/router.php b/lib/router.php index 9ab46856d1..f03cfcf6db 100644 --- a/lib/router.php +++ b/lib/router.php @@ -88,6 +88,10 @@ class Router $m->connect('doc/:title', array('action' => 'doc')); + // Twitter + + $m->connect('twitter/authorization', array('action' => 'twitterauthorization')); + // facebook $m->connect('facebook', array('action' => 'facebookhome')); diff --git a/lib/twitter.php b/lib/twitter.php index 47af32e61f..280cdb0a33 100644 --- a/lib/twitter.php +++ b/lib/twitter.php @@ -17,83 +17,20 @@ * along with this program. If not, see . */ -if (!defined('LACONICA')) { exit(1); } - -define('TWITTER_SERVICE', 1); // Twitter is foreign_service ID 1 - -function get_twitter_data($uri, $screen_name, $password) -{ - - $options = array( - CURLOPT_USERPWD => sprintf("%s:%s", $screen_name, $password), - CURLOPT_RETURNTRANSFER => true, - CURLOPT_FAILONERROR => true, - CURLOPT_HEADER => false, - CURLOPT_FOLLOWLOCATION => true, - CURLOPT_USERAGENT => "Laconica", - CURLOPT_CONNECTTIMEOUT => 120, - CURLOPT_TIMEOUT => 120, - # Twitter is strict about accepting invalid "Expect" headers - CURLOPT_HTTPHEADER => array('Expect:') - ); - - $ch = curl_init($uri); - curl_setopt_array($ch, $options); - $data = curl_exec($ch); - $errmsg = curl_error($ch); - - if ($errmsg) { - common_debug("Twitter bridge - cURL error: $errmsg - trying to load: $uri with user $screen_name.", - __FILE__); - - if (defined('SCRIPT_DEBUG')) { - print "cURL error: $errmsg - trying to load: $uri with user $screen_name.\n"; - } - } - - curl_close($ch); - - return $data; -} - -function twitter_json_data($uri, $screen_name, $password) -{ - $json_data = get_twitter_data($uri, $screen_name, $password); - - if (!$json_data) { - return false; - } - - $data = json_decode($json_data); - - if (!$data) { - return false; - } - - return $data; -} - -function twitter_user_info($screen_name, $password) -{ - $uri = "http://twitter.com/users/show/$screen_name.json"; - return twitter_json_data($uri, $screen_name, $password); +if (!defined('LACONICA')) { + exit(1); } -function twitter_friends_ids($screen_name, $password) -{ - $uri = "http://twitter.com/friends/ids/$screen_name.json"; - return twitter_json_data($uri, $screen_name, $password); -} +define('TWITTER_SERVICE', 1); // Twitter is foreign_service ID 1 function update_twitter_user($twitter_id, $screen_name) { $uri = 'http://twitter.com/' . $screen_name; - $fuser = new Foreign_user(); $fuser->query('BEGIN'); - // Dropping down to SQL because regular db_object udpate stuff doesn't seem + // Dropping down to SQL because regular DB_DataObject udpate stuff doesn't seem // to work so good with tables that have multiple column primary keys // Any time we update the uri for a forein user we have to make sure there @@ -102,35 +39,14 @@ function update_twitter_user($twitter_id, $screen_name) $qry = 'UPDATE foreign_user set uri = \'\' WHERE uri = '; $qry .= '\'' . $uri . '\'' . ' AND service = ' . TWITTER_SERVICE; - $result = $fuser->query($qry); - - if ($result) { - common_debug("Removed uri ($uri) from another foreign_user who was squatting on it."); - if (defined('SCRIPT_DEBUG')) { - print("Removed uri ($uri) from another Twitter user who was squatting on it.\n"); - } - } + $fuser->query($qry); // Update the user + $qry = 'UPDATE foreign_user SET nickname = '; $qry .= '\'' . $screen_name . '\'' . ', uri = \'' . $uri . '\' '; $qry .= 'WHERE id = ' . $twitter_id . ' AND service = ' . TWITTER_SERVICE; - $result = $fuser->query($qry); - - if (!$result) { - common_log(LOG_WARNING, - "Couldn't update foreign_user data for Twitter user: $screen_name"); - common_log_db_error($fuser, 'UPDATE', __FILE__); - if (defined('SCRIPT_DEBUG')) { - print "UPDATE failed: for Twitter user: $twitter_id - $screen_name. - "; - print common_log_objstring($fuser) . "\n"; - $error = &PEAR::getStaticProperty('DB_DataObject','lastError'); - print "DB_DataObject Error: " . $error->getMessage() . "\n"; - } - return false; - } - $fuser->query('COMMIT'); $fuser->free(); @@ -147,23 +63,22 @@ function add_twitter_user($twitter_id, $screen_name) // Clear out any bad old foreign_users with the new user's legit URL // This can happen when users move around or fakester accounts get // repoed, and things like that. + $luser = new Foreign_user(); $luser->uri = $new_uri; $luser->service = TWITTER_SERVICE; $result = $luser->delete(); - if ($result) { + if (empty($result)) { common_log(LOG_WARNING, "Twitter bridge - removed invalid Twitter user squatting on uri: $new_uri"); - if (defined('SCRIPT_DEBUG')) { - print "Removed invalid Twitter user squatting on uri: $new_uri\n"; - } } $luser->free(); unset($luser); // Otherwise, create a new Twitter user + $fuser = new Foreign_user(); $fuser->nickname = $screen_name; @@ -173,21 +88,12 @@ function add_twitter_user($twitter_id, $screen_name) $fuser->created = common_sql_now(); $result = $fuser->insert(); - if (!$result) { + if (empty($result)) { common_log(LOG_WARNING, "Twitter bridge - failed to add new Twitter user: $twitter_id - $screen_name."); common_log_db_error($fuser, 'INSERT', __FILE__); - if (defined('SCRIPT_DEBUG')) { - print "INSERT failed: could not add new Twitter user: $twitter_id - $screen_name. - "; - print common_log_objstring($fuser) . "\n"; - $error = &PEAR::getStaticProperty('DB_DataObject','lastError'); - print "DB_DataObject Error: " . $error->getMessage() . "\n"; - } } else { common_debug("Twitter bridge - Added new Twitter user: $screen_name ($twitter_id)."); - if (defined('SCRIPT_DEBUG')) { - print "Added new Twitter user: $screen_name ($twitter_id).\n"; - } } return $result; @@ -199,23 +105,20 @@ function save_twitter_user($twitter_id, $screen_name) // Check to see whether the Twitter user is already in the system, // and update its screen name and uri if so. + $fuser = Foreign_user::getForeignUser($twitter_id, TWITTER_SERVICE); - if ($fuser) { + if (!empty($fuser)) { $result = true; // Only update if Twitter screen name has changed + if ($fuser->nickname != $screen_name) { $result = update_twitter_user($twitter_id, $screen_name); common_debug('Twitter bridge - Updated nickname (and URI) for Twitter user ' . "$fuser->id to $screen_name, was $fuser->nickname"); - - if (defined('SCRIPT_DEBUG')) { - print 'Updated nickname (and URI) for Twitter user ' . - "$fuser->id to $screen_name, was $fuser->nickname\n"; - } } return $result; @@ -230,119 +133,6 @@ function save_twitter_user($twitter_id, $screen_name) return true; } -function retreive_twitter_friends($twitter_id, $screen_name, $password) -{ - $friends = array(); - - $uri = "http://twitter.com/statuses/friends/$twitter_id.json?page="; - $friends_ids = twitter_friends_ids($screen_name, $password); - - if (!$friends_ids) { - return $friends; - } - - if (defined('SCRIPT_DEBUG')) { - print "Twitter 'social graph' ids method says $screen_name has " . - count($friends_ids) . " friends.\n"; - } - - // Calculate how many pages to get... - $pages = ceil(count($friends_ids) / 100); - - if ($pages == 0) { - common_log(LOG_WARNING, - "Twitter bridge - $screen_name seems to have no friends."); - if (defined('SCRIPT_DEBUG')) { - print "$screen_name seems to have no friends.\n"; - } - } - - for ($i = 1; $i <= $pages; $i++) { - - $data = get_twitter_data($uri . $i, $screen_name, $password); - - if (!$data) { - common_log(LOG_WARNING, - "Twitter bridge - Couldn't retrieve page $i of $screen_name's friends."); - if (defined('SCRIPT_DEBUG')) { - print "Couldn't retrieve page $i of $screen_name's friends.\n"; - } - continue; - } - - $more_friends = json_decode($data); - - if (!$more_friends) { - - common_log(LOG_WARNING, - "Twitter bridge - No data for page $i of $screen_name's friends."); - if (defined('SCRIPT_DEBUG')) { - print "No data for page $i of $screen_name's friends.\n"; - } - continue; - } - - $friends = array_merge($friends, $more_friends); - } - - return $friends; -} - -function save_twitter_friends($user, $twitter_id, $screen_name, $password) -{ - - $friends = retreive_twitter_friends($twitter_id, $screen_name, $password); - - if (empty($friends)) { - common_debug("Twitter bridge - Couldn't get friends data from Twitter for $screen_name."); - if (defined('SCRIPT_DEBUG')) { - print "Couldn't get friends data from Twitter for $screen_name.\n"; - } - return false; - } - - foreach ($friends as $friend) { - - $friend_name = $friend->screen_name; - $friend_id = (int) $friend->id; - - // Update or create the Foreign_user record - if (!save_twitter_user($friend_id, $friend_name)) { - common_log(LOG_WARNING, - "Twitter bridge - couldn't save $screen_name's friend, $friend_name."); - if (defined('SCRIPT_DEBUG')) { - print "Couldn't save $screen_name's friend, $friend_name.\n"; - } - continue; - } - - // Check to see if there's a related local user - $flink = Foreign_link::getByForeignID($friend_id, 1); - - if ($flink) { - - // Get associated user and subscribe her - $friend_user = User::staticGet('id', $flink->user_id); - if (!empty($friend_user)) { - $result = subs_subscribe_to($user, $friend_user); - - if ($result === true) { - common_debug("Twitter bridge - subscribed $friend_user->nickname to $user->nickname."); - if (defined('SCRIPT_DEBUG')) { - print("Subscribed $friend_user->nickname to $user->nickname.\n"); - } - } else { - if (defined('SCRIPT_DEBUG')) { - print "$result ($friend_user->nickname to $user->nickname)\n"; - } - } - } - } - } - - return true; -} - function is_twitter_bound($notice, $flink) { // Check to see if notice should go to Twitter @@ -351,7 +141,7 @@ function is_twitter_bound($notice, $flink) { // If it's not a Twitter-style reply, or if the user WANTS to send replies. if (!preg_match('/^@[a-zA-Z0-9_]{1,15}\b/u', $notice->content) || ($flink->noticesync & FOREIGN_NOTICE_SEND_REPLY)) { - return true; + return true; } } @@ -360,104 +150,73 @@ function is_twitter_bound($notice, $flink) { function broadcast_twitter($notice) { - $flink = Foreign_link::getByUserID($notice->profile_id, - TWITTER_SERVICE); + TWITTER_SERVICE); if (is_twitter_bound($notice, $flink)) { - $fuser = $flink->getForeignUser(); - $twitter_user = $fuser->nickname; - $twitter_password = $flink->credentials; - $uri = 'http://www.twitter.com/statuses/update.json'; + $user = $flink->getUser(); // XXX: Hack to get around PHP cURL's use of @ being a a meta character $statustxt = preg_replace('/^@/', ' @', $notice->content); - $options = array( - CURLOPT_USERPWD => "$twitter_user:$twitter_password", - CURLOPT_POST => true, - CURLOPT_POSTFIELDS => - array( - 'status' => $statustxt, - 'source' => common_config('integration', 'source') - ), - CURLOPT_RETURNTRANSFER => true, - CURLOPT_FAILONERROR => true, - CURLOPT_HEADER => false, - CURLOPT_FOLLOWLOCATION => true, - CURLOPT_USERAGENT => "Laconica", - CURLOPT_CONNECTTIMEOUT => 120, // XXX: How long should this be? - CURLOPT_TIMEOUT => 120, - - # Twitter is strict about accepting invalid "Expect" headers - CURLOPT_HTTPHEADER => array('Expect:') - ); - - $ch = curl_init($uri); - curl_setopt_array($ch, $options); - $data = curl_exec($ch); - $errmsg = curl_error($ch); - $errno = curl_errno($ch); - - if (!empty($errmsg)) { - common_debug("cURL error ($errno): $errmsg - " . - "trying to send notice for $twitter_user.", - __FILE__); - - $user = $flink->getUser(); - - if ($errmsg == 'The requested URL returned error: 401') { - common_debug(sprintf('User %s (user id: %s) ' . - 'has bad Twitter credentials!', - $user->nickname, $user->id)); - - // Bad credentials we need to delete the foreign_link - // to Twitter and inform the user. - - remove_twitter_link($flink); - - return true; + $token = TwitterOAuthClient::unpackToken($flink->credentials); - } else { + $client = new TwitterOAuthClient($token->key, $token->secret); - // Some other error happened, so we should try to - // send again later + $status = null; - return false; - } + try { + $status = $client->statusesUpdate($statustxt); + } catch (OAuthClientCurlException $e) { - } - - curl_close($ch); + if ($e->getMessage() == 'The requested URL returned error: 401') { - if (empty($data)) { - common_debug("No data returned by Twitter's " . - "API trying to send update for $twitter_user", - __FILE__); + $errmsg = sprintf('User %1$s (user id: %2$s) has an invalid ' . + 'Twitter OAuth access token.', + $user->nickname, $user->id); + common_log(LOG_WARNING, $errmsg); - // XXX: Not sure this represents a failure to send, but it - // probably does + // Bad auth token! We need to delete the foreign_link + // to Twitter and inform the user. - return false; - - } else { + remove_twitter_link($flink); + return true; - // Twitter should return a status - $status = json_decode($data); + } else { - if (empty($status)) { - common_debug("Unexpected data returned by Twitter " . - " API trying to send update for $twitter_user", - __FILE__); + // Some other error happened, so we should probably + // try to send again later. - // XXX: Again, this could represent a failure posting - // or the Twitter API might just be behaving flakey. - // We're treating it as a failure to post. + $errmsg = sprintf('cURL error trying to send notice to Twitter ' . + 'for user %1$s (user id: %2$s) - ' . + 'code: %3$s message: $4$s.', + $user->nickname, $user->id, + $e->getCode(), $e->getMessage()); + common_log(LOG_WARNING, $errmsg); return false; } } + + if (empty($status)) { + + // This could represent a failure posting, + // or the Twitter API might just be behaving flakey. + + $errmsg = sprint('No data returned by Twitter API when ' . + 'trying to send update for %1$s (user id %2$s).', + $user->nickname, $user->id); + common_log(LOG_WARNING, $errmsg); + + return false; + } + + // Notice crossed the great divide + + $msg = sprintf('Twitter bridge posted notice %s to Twitter.', + $notice->id); + common_log(LOG_INFO, $msg); } return true; @@ -474,22 +233,25 @@ function remove_twitter_link($flink) if (empty($result)) { common_log(LOG_ERR, 'Could not remove Twitter bridge ' . - "Foreign_link for $user->nickname (user id: $user->id)!"); + "Foreign_link for $user->nickname (user id: $user->id)!"); common_log_db_error($flink, 'DELETE', __FILE__); } // Notify the user that her Twitter bridge is down - $result = mail_twitter_bridge_removed($user); + if (isset($user->email)) { + + $result = mail_twitter_bridge_removed($user); - if (!$result) { + if (!$result) { - $msg = 'Unable to send email to notify ' . - "$user->nickname (user id: $user->id) " . - 'that their Twitter bridge link was ' . - 'removed!'; + $msg = 'Unable to send email to notify ' . + "$user->nickname (user id: $user->id) " . + 'that their Twitter bridge link was ' . + 'removed!'; - common_log(LOG_WARNING, $msg); + common_log(LOG_WARNING, $msg); + } } } diff --git a/lib/twitteroauthclient.php b/lib/twitteroauthclient.php new file mode 100644 index 0000000000..b7dc4a80c3 --- /dev/null +++ b/lib/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/scripts/getvaliddaemons.php b/scripts/getvaliddaemons.php index 8b127bd201..f1d8d8116d 100755 --- a/scripts/getvaliddaemons.php +++ b/scripts/getvaliddaemons.php @@ -45,6 +45,7 @@ if(common_config('twitterbridge','enabled')) { echo "ombqueuehandler.php "; if (common_config('twitter', 'enabled')) { echo "twitterqueuehandler.php "; + echo "synctwitterfriends.php "; } echo "facebookqueuehandler.php "; echo "pingqueuehandler.php "; diff --git a/scripts/stopdaemons.sh b/scripts/stopdaemons.sh index 60ffd83ad1..894e5aaffe 100755 --- a/scripts/stopdaemons.sh +++ b/scripts/stopdaemons.sh @@ -25,7 +25,7 @@ DIR=`php $SDIR/getpiddir.php` for f in jabberhandler ombhandler publichandler smshandler pinghandler \ xmppconfirmhandler xmppdaemon twitterhandler facebookhandler \ - twitterstatusfetcher; do + twitterstatusfetcher synctwitterfriends; do FILES="$DIR/$f.*.pid" for ff in "$FILES" ; do diff --git a/scripts/synctwitterfriends.php b/scripts/synctwitterfriends.php index fe53ff44d6..2de464bccc 100755 --- a/scripts/synctwitterfriends.php +++ b/scripts/synctwitterfriends.php @@ -20,85 +20,260 @@ define('INSTALLDIR', realpath(dirname(__FILE__) . '/..')); -// Uncomment this to get useful console output +$shortoptions = 'di::'; +$longoptions = array('id::', 'debug'); + +$helptext = << + * @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/ + */ $helptext = <<service = 1; // Twitter -$flink->orderBy('last_friendsync'); -$flink->limit(25); // sync this many users during this run -$cnt = $flink->find(); + /** + * Name of this daemon + * + * @return string Name of the daemon. + */ -print "Updating Twitter friends subscriptions for $cnt users.\n"; + function name() + { + return ('synctwitterfriends.' . $this->_id); + } -while ($flink->fetch()) { + /** + * Find all the Twitter foreign links for users who have requested + * automatically subscribing to their Twitter friends locally. + * + * @return array flinks an array of Foreign_link objects + */ + function getObjects() + { + $flinks = array(); + $flink = new Foreign_link(); - if (($flink->friendsync & FOREIGN_FRIEND_RECV) == FOREIGN_FRIEND_RECV) { + $conn = &$flink->getDatabaseConnection(); - $user = User::staticGet($flink->user_id); + $flink->service = TWITTER_SERVICE; + $flink->orderBy('last_friendsync'); + $flink->limit(25); // sync this many users during this run + $flink->find(); - if (empty($user)) { - common_log(LOG_WARNING, "Unmatched user for ID " . $flink->user_id); - print "Unmatched user for ID $flink->user_id\n"; - continue; + while ($flink->fetch()) { + if (($flink->friendsync & FOREIGN_FRIEND_RECV) == FOREIGN_FRIEND_RECV) { + $flinks[] = clone($flink); + } } - print "Updating Twitter friends for $user->nickname (Laconica ID: $user->id)... "; + $conn->disconnect(); - $fuser = $flink->getForeignUser(); + global $_DB_DATAOBJECT; + unset($_DB_DATAOBJECT['CONNECTIONS']); - if (empty($fuser)) { - common_log(LOG_WARNING, "Unmatched user for ID " . $flink->user_id); - print "Unmatched user for ID $flink->user_id\n"; - continue; - } + return $flinks; + } + + function childTask($flink) { - save_twitter_friends($user, $fuser->id, $fuser->nickname, $flink->credentials); + // Each child ps needs its own DB connection + + // Note: DataObject::getDatabaseConnection() creates + // a new connection if there isn't one already + + $conn = &$flink->getDatabaseConnection(); + + $this->subscribeTwitterFriends($flink); $flink->last_friendsync = common_sql_now(); $flink->update(); - if (defined('SCRIPT_DEBUG')) { - print "\nDONE\n"; - } else { - print "DONE\n"; + $conn->disconnect(); + + // XXX: Couldn't find a less brutal way to blow + // away a cached connection + + global $_DB_DATAOBJECT; + unset($_DB_DATAOBJECT['CONNECTIONS']); + } + + function fetchTwitterFriends($flink) + { + $friends = array(); + + $token = TwitterOAuthClient::unpackToken($flink->credentials); + + $client = new TwitterOAuthClient($token->key, $token->secret); + + try { + $friends_ids = $client->friendsIds(); + } catch (OAuthCurlException $e) { + common_log(LOG_WARNING, $this->name() . + ' - cURL error getting friend ids ' . + $e->getCode() . ' - ' . $e->getMessage()); + return $friends; + } + + if (empty($friends_ids)) { + common_debug($this->name() . + " - Twitter user $flink->foreign_id " . + 'doesn\'t have any friends!'); + return $friends; + } + + common_debug($this->name() . ' - Twitter\'s API says Twitter user id ' . + "$flink->foreign_id has " . + count($friends_ids) . ' friends.'); + + // Calculate how many pages to get... + $pages = ceil(count($friends_ids) / 100); + + if ($pages == 0) { + common_debug($this->name() . " - $user seems to have no friends."); + } + + for ($i = 1; $i <= $pages; $i++) { + + try { + $more_friends = $client->statusesFriends(null, null, null, $i); + } catch (OAuthCurlException $e) { + common_log(LOG_WARNING, $this->name() . + ' - cURL error getting Twitter statuses/friends ' . + "page $i - " . $e->getCode() . ' - ' . + $e->getMessage()); } + + if (empty($more_friends)) { + common_log(LOG_WARNING, $this->name() . + " - Couldn't retrieve page $i " . + "of Twitter user $flink->foreign_id friends."); + continue; + } else { + $friends = array_merge($friends, $more_friends); + } + } + + return $friends; } -} -function lockFilename() -{ - $piddir = common_config('daemon', 'piddir'); - if (!$piddir) { - $piddir = '/var/run'; + function subscribeTwitterFriends($flink) + { + $friends = $this->fetchTwitterFriends($flink); + + if (empty($friends)) { + common_debug($this->name() . + ' - Couldn\'t get friends from Twitter for ' . + "Twitter user $flink->foreign_id."); + return false; + } + + $user = $flink->getUser(); + + foreach ($friends as $friend) { + + $friend_name = $friend->screen_name; + $friend_id = (int) $friend->id; + + // Update or create the Foreign_user record for each + // Twitter friend + + if (!save_twitter_user($friend_id, $friend_name)) { + common_log(LOG_WARNING, $this-name() . + " - Couldn't save $screen_name's friend, $friend_name."); + continue; + } + + // Check to see if there's a related local user + + $friend_flink = Foreign_link::getByForeignID($friend_id, + TWITTER_SERVICE); + + if (!empty($friend_flink)) { + + // Get associated user and subscribe her + + $friend_user = User::staticGet('id', $friend_flink->user_id); + + if (!empty($friend_user)) { + $result = subs_subscribe_to($user, $friend_user); + + if ($result === true) { + common_log(LOG_INFO, + $this->name() . ' - Subscribed ' . + "$friend_user->nickname to $user->nickname."); + } else { + common_debug($this->name() . + ' - Tried subscribing ' . + "$friend_user->nickname to $user->nickname - " . + $result); + } + } + } + } + + return true; } - return $piddir . '/synctwitterfriends.lock'; } -// Cleanup -fclose($lockfile); -unlink($lockfilename); +$id = null; +$debug = null; + +if (have_option('i')) { + $id = get_option_value('i'); +} else if (have_option('--id')) { + $id = get_option_value('--id'); +} else if (count($args) > 0) { + $id = $args[0]; +} else { + $id = null; +} + +if (have_option('d') || have_option('debug')) { + $debug = true; +} + +$syncer = new SyncTwitterFriendsDaemon($id, 60, 2, $debug); +$syncer->runOnce(); -exit(0); diff --git a/scripts/twitterstatusfetcher.php b/scripts/twitterstatusfetcher.php index e1745cfc08..f5289c5f4b 100755 --- a/scripts/twitterstatusfetcher.php +++ b/scripts/twitterstatusfetcher.php @@ -56,17 +56,23 @@ require_once INSTALLDIR . '/lib/daemon.php'; // NOTE: an Avatar path MUST be set in config.php for this // script to work: e.g.: $config['avatar']['path'] = '/laconica/avatar'; -class TwitterStatusFetcher extends Daemon +class TwitterStatusFetcher extends ParallelizingDaemon { - private $_children = array(); - - function __construct($id=null, $daemonize=true) + /** + * Constructor + * + * @param string $id the name/id of this daemon + * @param int $interval sleep this long before doing everything again + * @param int $max_children maximum number of child processes at a time + * @param boolean $debug debug output flag + * + * @return void + * + **/ + function __construct($id = null, $interval = 60, + $max_children = 2, $debug = null) { - parent::__construct($daemonize); - - if ($id) { - $this->set_id($id); - } + parent::__construct($id, $interval, $max_children, $debug); } /** @@ -81,126 +87,22 @@ class TwitterStatusFetcher extends Daemon } /** - * Run the daemon + * Find all the Twitter foreign links for users who have requested + * importing of their friends' timelines * - * @return void + * @return array flinks an array of Foreign_link objects */ - function run() + function getObjects() { - if (defined('SCRIPT_DEBUG')) { - common_debug($this->name() . - ': debugging log output enabled.'); - } - - do { - - $flinks = $this->refreshFlinks(); - - foreach ($flinks as $f) { - - // We have to disconnect from the DB before forking so - // each sub-process will open its own connection and - // avoid stomping on the others - - $conn = &$f->getDatabaseConnection(); - $conn->disconnect(); - - $pid = pcntl_fork(); - - if ($pid == -1) { - die ("Couldn't fork!"); - } - - if ($pid) { - - // Parent - if (defined('SCRIPT_DEBUG')) { - common_debug("Parent: forked new status ". - " fetcher process " . $pid); - } - - $this->_children[] = $pid; - - } else { - - // Child - $this->getTimeline($f); - exit(); - } - - // Remove child from ps list as it finishes - while (($c = pcntl_wait($status, WNOHANG OR WUNTRACED)) > 0) { - - if (defined('SCRIPT_DEBUG')) { - common_debug("Child $c finished."); - } - - $this->removePs($this->_children, $c); - } - - // Wait! We have too many damn kids. - if (sizeof($this->_children) > MAXCHILDREN) { - - if (defined('SCRIPT_DEBUG')) { - common_debug('Too many children. Waiting...'); - } - - if (($c = pcntl_wait($status, WUNTRACED)) > 0) { - - if (defined('SCRIPT_DEBUG')) { - common_debug("Finished waiting for $c"); - } - - $this->removePs($this->_children, $c); - } - } - } - - // Remove all children from the process list before restarting - while (($c = pcntl_wait($status, WUNTRACED)) > 0) { - - if (defined('SCRIPT_DEBUG')) { - common_debug("Child $c finished."); - } - - $this->removePs($this->_children, $c); - } - - // Rest for a bit before we fetch more statuses - - if (defined('SCRIPT_DEBUG')) { - common_debug('Waiting ' . POLL_INTERVAL . - ' secs before hitting Twitter again.'); - } - - if (POLL_INTERVAL > 0) { - sleep(POLL_INTERVAL); - } - - } while (true); - } - - /** - * Refresh the foreign links for this user - * - * @return void - */ + global $_DB_DATAOBJECT; - function refreshFlinks() - { $flink = new Foreign_link(); + $conn = &$flink->getDatabaseConnection(); - $flink->service = 1; // Twitter - + $flink->service = TWITTER_SERVICE; $flink->orderBy('last_noticesync'); - - $cnt = $flink->find(); - - if (defined('SCRIPT_DEBUG')) { - common_debug('Updating Twitter friends subscriptions' . - " for $cnt users."); - } + $flink->find(); $flinks = array(); @@ -215,78 +117,81 @@ class TwitterStatusFetcher extends Daemon $flink->free(); unset($flink); + $conn->disconnect(); + unset($_DB_DATAOBJECT['CONNECTIONS']); + return $flinks; } - /** - * Unknown - * - * @param array &$plist unknown. - * @param string $ps unknown. - * - * @return unknown - * @todo document - */ + function childTask($flink) { - function removePs(&$plist, $ps) - { - for ($i = 0; $i < sizeof($plist); $i++) { - if ($plist[$i] == $ps) { - unset($plist[$i]); - $plist = array_values($plist); - break; - } - } + // Each child ps needs its own DB connection + + // Note: DataObject::getDatabaseConnection() creates + // a new connection if there isn't one already + + $conn = &$flink->getDatabaseConnection(); + + $this->getTimeline($flink); + + $flink->last_friendsync = common_sql_now(); + $flink->update(); + + $conn->disconnect(); + + // XXX: Couldn't find a less brutal way to blow + // away a cached connection + + global $_DB_DATAOBJECT; + unset($_DB_DATAOBJECT['CONNECTIONS']); } function getTimeline($flink) { - if (empty($flink)) { - common_log(LOG_WARNING, - "Can't retrieve Foreign_link for foreign ID $fid"); - return; - } - - $fuser = $flink->getForeignUser(); - - if (empty($fuser)) { - common_log(LOG_WARNING, "Unmatched user for ID " . - $flink->user_id); + if (empty($flink)) { + common_log(LOG_WARNING, $this->name() . + " - Can't retrieve Foreign_link for foreign ID $fid"); return; } - if (defined('SCRIPT_DEBUG')) { - common_debug('Trying to get timeline for Twitter user ' . - "$fuser->nickname ($flink->foreign_id)."); - } + common_debug($this->name() . ' - Trying to get timeline for Twitter user ' . + $flink->foreign_id); // XXX: Biggest remaining issue - How do we know at which status // to start importing? How many statuses? Right now I'm going // with the default last 20. - $url = 'http://twitter.com/statuses/friends_timeline.json'; + $token = TwitterOAuthClient::unpackToken($flink->credentials); + + $client = new TwitterOAuthClient($token->key, $token->secret); - $timeline_json = get_twitter_data($url, $fuser->nickname, - $flink->credentials); + $timeline = null; - $timeline = json_decode($timeline_json); + try { + $timeline = $client->statusesFriendsTimeline(); + } catch (OAuthClientCurlException $e) { + common_log(LOG_WARNING, $this->name() . + ' - OAuth client unable to get friends timeline for user ' . + $flink->user_id . ' - code: ' . + $e->getCode() . 'msg: ' . $e->getMessage()); + } if (empty($timeline)) { - common_log(LOG_WARNING, "Empty timeline."); + common_log(LOG_WARNING, $this->name() . " - Empty timeline."); return; } // Reverse to preserve order + foreach (array_reverse($timeline) as $status) { // Hacktastic: filter out stuff coming from this Laconica + $source = mb_strtolower(common_config('integration', 'source')); if (preg_match("/$source/", mb_strtolower($status->source))) { - if (defined('SCRIPT_DEBUG')) { - common_debug('Skipping import of status ' . $status->id . - ' with source ' . $source); - } + common_debug($this->name() . ' - Skipping import of status ' . + $status->id . ' with source ' . $source); continue; } @@ -294,6 +199,7 @@ class TwitterStatusFetcher extends Daemon } // Okay, record the time we synced with Twitter for posterity + $flink->last_noticesync = common_sql_now(); $flink->update(); } @@ -301,11 +207,12 @@ class TwitterStatusFetcher extends Daemon function saveStatus($status, $flink) { $id = $this->ensureProfile($status->user); + $profile = Profile::staticGet($id); - if (!$profile) { - common_log(LOG_ERR, - 'Problem saving notice. No associated Profile.'); + if (empty($profile)) { + common_log(LOG_ERR, $this->name() . + ' - Problem saving notice. No associated Profile.'); return null; } @@ -318,7 +225,7 @@ class TwitterStatusFetcher extends Daemon // check to see if we've already imported the status - if (!$notice) { + if (empty($notice)) { $notice = new Notice(); @@ -329,7 +236,7 @@ class TwitterStatusFetcher extends Daemon $notice->content = common_shorten_links($status->text); // XXX $notice->rendered = common_render_content($notice->content, $notice); $notice->source = 'twitter'; - $notice->reply_to = null; // XXX lookup reply + $notice->reply_to = null; // XXX: lookup reply $notice->is_local = Notice::GATEWAY; if (Event::handle('StartNoticeSave', array(&$notice))) { @@ -355,24 +262,22 @@ class TwitterStatusFetcher extends Daemon function ensureProfile($user) { // check to see if there's already a profile for this user + $profileurl = 'http://twitter.com/' . $user->screen_name; $profile = Profile::staticGet('profileurl', $profileurl); - if ($profile) { - if (defined('SCRIPT_DEBUG')) { - common_debug("Profile for $profile->nickname found."); - } + if (!empty($profile)) { + common_debug($this->name() . + " - Profile for $profile->nickname found."); // Check to see if the user's Avatar has changed - $this->checkAvatar($user, $profile); + $this->checkAvatar($user, $profile); return $profile->id; } else { - if (defined('SCRIPT_DEBUG')) { - common_debug('Adding profile and remote profile ' . - "for Twitter user: $profileurl"); - } + common_debug($this->name() . ' - Adding profile and remote profile ' . + "for Twitter user: $profileurl."); $profile = new Profile(); $profile->query("BEGIN"); @@ -394,9 +299,10 @@ class TwitterStatusFetcher extends Daemon } // check for remote profile + $remote_pro = Remote_profile::staticGet('uri', $profileurl); - if (!$remote_pro) { + if (empty($remote_pro)) { $remote_pro = new Remote_profile(); @@ -433,23 +339,18 @@ class TwitterStatusFetcher extends Daemon $oldname = $profile->getAvatar(48)->filename; if ($newname != $oldname) { - - if (defined('SCRIPT_DEBUG')) { - common_debug('Avatar for Twitter user ' . - "$profile->nickname has changed."); - common_debug("old: $oldname new: $newname"); - } + common_debug($this->name() . ' - Avatar for Twitter user ' . + "$profile->nickname has changed."); + common_debug($this->name() . " - old: $oldname new: $newname"); $this->updateAvatars($twitter_user, $profile); } if ($this->missingAvatarFile($profile)) { - - if (defined('SCRIPT_DEBUG')) { - common_debug('Twitter user ' . $profile->nickname . - ' is missing one or more local avatars.'); - common_debug("old: $oldname new: $newname"); - } + common_debug($this->name() . ' - Twitter user ' . + $profile->nickname . + ' is missing one or more local avatars.'); + common_debug($this->name() ." - old: $oldname new: $newname"); $this->updateAvatars($twitter_user, $profile); } @@ -529,23 +430,20 @@ class TwitterStatusFetcher extends Daemon if ($this->fetchAvatar($url, $filename)) { $this->newAvatar($id, $size, $mediatype, $filename); } else { - common_log(LOG_WARNING, "Problem fetching Avatar: $url", __FILE__); + common_log(LOG_WARNING, $this->id() . + " - Problem fetching Avatar: $url"); } } } function updateAvatar($profile_id, $size, $mediatype, $filename) { - if (defined('SCRIPT_DEBUG')) { - common_debug("Updating avatar: $size"); - } + common_debug($this->name() . " - Updating avatar: $size"); $profile = Profile::staticGet($profile_id); if (empty($profile)) { - if (defined('SCRIPT_DEBUG')) { - common_debug("Couldn't get profile: $profile_id!"); - } + common_debug($this->name() . " - Couldn't get profile: $profile_id!"); return; } @@ -553,6 +451,7 @@ class TwitterStatusFetcher extends Daemon $avatar = $profile->getAvatar($sizes[$size]); // Delete the avatar, if present + if ($avatar) { $avatar->delete(); } @@ -590,9 +489,7 @@ class TwitterStatusFetcher extends Daemon $avatar->filename = $filename; $avatar->url = Avatar::url($filename); - if (defined('SCRIPT_DEBUG')) { - common_debug("new filename: $avatar->url"); - } + common_debug($this->name() . " - New filename: $avatar->url"); $avatar->created = common_sql_now(); @@ -603,9 +500,8 @@ class TwitterStatusFetcher extends Daemon return null; } - if (defined('SCRIPT_DEBUG')) { - common_debug("Saved new $size avatar for $profile_id."); - } + common_debug($this->name() . + " - Saved new $size avatar for $profile_id."); return $id; } @@ -618,13 +514,12 @@ class TwitterStatusFetcher extends Daemon $out = fopen($avatarfile, 'wb'); if (!$out) { - common_log(LOG_WARNING, "Couldn't open file $filename", __FILE__); + common_log(LOG_WARNING, $this->name() . + " - Couldn't open file $filename"); return false; } - if (defined('SCRIPT_DEBUG')) { - common_debug("Fetching avatar: $url"); - } + common_debug($this->name() . " - Fetching Twitter avatar: $url"); $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); @@ -641,7 +536,8 @@ class TwitterStatusFetcher extends Daemon } } -declare(ticks = 1); +$id = null; +$debug = null; if (have_option('i')) { $id = get_option_value('i'); @@ -654,9 +550,9 @@ if (have_option('i')) { } if (have_option('d') || have_option('debug')) { - define('SCRIPT_DEBUG', true); + $debug = true; } -$fetcher = new TwitterStatusFetcher($id); +$fetcher = new TwitterStatusFetcher($id, 60, 2, $debug); $fetcher->runOnce();