]> git.mxchange.org Git - quix0rs-gnu-social.git/commitdiff
Merge branch 'twitter-oauth' into 0.8.x
authorZach Copley <zach@controlyourself.ca>
Mon, 10 Aug 2009 07:49:51 +0000 (07:49 +0000)
committerZach Copley <zach@controlyourself.ca>
Mon, 10 Aug 2009 07:49:51 +0000 (07:49 +0000)
Conflicts:

scripts/getvaliddaemons.php

14 files changed:
actions/twitterauthorization.php [new file with mode: 0644]
actions/twittersettings.php
config.php.sample
lib/common.php
lib/mail.php
lib/oauthclient.php [new file with mode: 0644]
lib/parallelizingdaemon.php [new file with mode: 0644]
lib/router.php
lib/twitter.php
lib/twitteroauthclient.php [new file with mode: 0644]
scripts/getvaliddaemons.php
scripts/stopdaemons.sh
scripts/synctwitterfriends.php
scripts/twitterstatusfetcher.php

diff --git a/actions/twitterauthorization.php b/actions/twitterauthorization.php
new file mode 100644 (file)
index 0000000..b04f353
--- /dev/null
@@ -0,0 +1,222 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Class for doing OAuth authentication against Twitter
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category  Twitter
+ * @package   Laconica
+ * @author    Zach Copely <zach@controlyourself.ca>
+ * @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 <zach@controlyourself.ca>
+ * @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);
+    }
+
+}
+
index 3343dba95a20d240a0a42f1ab14ae97e650e6223..0859ab9d34ef55c8b45bc9ad71f0e257f6aeb154 100644 (file)
@@ -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;
-    }
-
 }
index 21b6865e150e503444183bccbde45c621b37d1c6..c2dcf85169dc71541452646bd430f27ba59d3520 100644 (file)
@@ -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.
index be30519f47409bb7e7eefa6ae92e60379e9b7910..645ae8c3442c5f2aa58b5ddaba0d1f80a79072a8 100644 (file)
@@ -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',
index 48e2cff658003ff12da912eddfafb67e0fd39439..33d1eb7549bf19815f524b40a41ed2c0324a4de2 100644 (file)
@@ -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 (file)
index 0000000..b66a24b
--- /dev/null
@@ -0,0 +1,225 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Base class for doing OAuth calls as a consumer
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category  Action
+ * @package   Laconica
+ * @author    Zach Copley <zach@controlyourself.ca>
+ * @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 <zach@controlyourself.ca>
+ * @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 <zach@controlyourself.ca>
+ * @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 (file)
index 0000000..dc28b56
--- /dev/null
@@ -0,0 +1,229 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Base class for making daemons that can do several tasks in parallel.
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category  Daemon
+ * @package   Laconica
+ * @author    Zach Copley <zach@controlyourself.ca>
+ * @author    Evan Prodromou <evan@controlyourself.ca>
+ * @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 <zach@controlyourself.ca>
+ * @author   Evan Prodromou <evan@controlyourself.ca>
+ * @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
index 9ab46856d193c42e01e0963f521c3709dbce0b4d..f03cfcf6db753ec5d22344f63dbe7e0324ce1d61 100644 (file)
@@ -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'));
index 47af32e61f3221d72de65136e9b43805a31af504..280cdb0a33d93d6fcfcdef2e8669f9cee55e383d 100644 (file)
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-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 (file)
index 0000000..b7dc4a8
--- /dev/null
@@ -0,0 +1,220 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Class for doing OAuth calls against Twitter
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category  Integration
+ * @package   Laconica
+ * @author    Zach Copley <zach@controlyourself.ca>
+ * @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 <zach@controlyourself.ca>
+ * @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;
+    }
+
+}
index 8b127bd201be9516466c9541ef51b2ba8cb9428b..f1d8d8116d0deb6f1d60ec46b56d2dba721732b9 100755 (executable)
@@ -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 ";
index 60ffd83ad1e45d8991aa4115a3e8c305acba5ddb..894e5aaffe74a3dd900160d436ad17c3981b6aa9 100755 (executable)
@@ -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
index fe53ff44d634fa92895dd00c34ccc804fa245295..2de464bccc72625171afc6cd3ae38064f1329f1d 100755 (executable)
 
 define('INSTALLDIR', realpath(dirname(__FILE__) . '/..'));
 
-// Uncomment this to get useful console output
+$shortoptions = 'di::';
+$longoptions = array('id::', 'debug');
+
+$helptext = <<<END_OF_TRIM_HELP
+Batch script for synching local friends with Twitter friends.
+  -i --id              Identity (default 'generic')
+  -d --debug           Debug (lots of log output)
+
+END_OF_TRIM_HELP;
+
+require_once INSTALLDIR . '/scripts/commandline.inc';
+require_once INSTALLDIR . '/lib/parallelizingdaemon.php';
+
+/**
+ * Daemon to sync local friends with Twitter friends
+ *
+ * @category Twitter
+ * @package  Laconica
+ * @author   Zach Copley <zach@controlyourself.ca>
+ * @author   Evan Prodromou <evan@controlyourself.ca>
+ * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link     http://laconi.ca/
+ */
 
 $helptext = <<<END_OF_TWITTER_HELP
 Batch script for synching local friends with Twitter friends.
 
 END_OF_TWITTER_HELP;
 
-require_once INSTALLDIR.'/scripts/commandline.inc';
+require_once INSTALLDIR . '/scripts/commandline.inc';
+require_once INSTALLDIR . '/lib/parallelizingdaemon.php';
 
-// Make a lockfile
-$lockfilename = lockFilename();
-if (!($lockfile = @fopen($lockfilename, "w"))) {
-    print "Already running... exiting.\n";
-    exit(1);
-}
+class SyncTwitterFriendsDaemon extends ParallelizingDaemon
+{
+    /**
+     *  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
+     *
+     **/
 
-// Obtain an exlcusive lock on file (will fail if script is already going)
-if (!@flock( $lockfile, LOCK_EX | LOCK_NB, &$wouldblock) || $wouldblock) {
-    // Script already running - abort
-    @fclose($lockfile);
-    print "Already running... exiting.\n";
-    exit(1);
-}
+    function __construct($id = null, $interval = 60,
+                         $max_children = 2, $debug = null)
+    {
+        parent::__construct($id, $interval, $max_children, $debug);
+    }
 
-$flink = new Foreign_link();
-$flink->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);
index e1745cfc089614f41aa8a5766f179e72a924f836..f5289c5f4b7384a1daeaa7de0c8e6119cbbb820d 100755 (executable)
@@ -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();