code:
<?php
/**
 * StatusNet, 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  Plugin
 * @package   StatusNet
 * @author    Zach Copley <zach@status.net>
 * @author    Julien C <chaumond@gmail.com>
 * @copyright 2009-2010 StatusNet, Inc.
 * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
 * @link      http://status.net/
 */

if (!defined('GNUSOCIAL')) { exit(1); }

require_once dirname(__DIR__) . '/twitter.php';
require_once INSTALLDIR . '/lib/oauthclient.php';

/**
 * Class for doing OAuth authentication against Twitter
 *
 * Peforms the OAuth "dance" between StatusNet and Twitter -- requests a token,
 * authorizes it, and exchanges it for an access token.  It also creates a link
 * (Foreign_link) between the StatusNet user and Twitter user and stores the
 * access token and secret in the link.
 *
 * @category Plugin
 * @package  StatusNet
 * @author   Zach Copley <zach@status.net>
 * @author   Julien C <chaumond@gmail.com>
 * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
 * @link     http://status.net/
 *
 */
class TwitterauthorizationAction extends FormAction
{
    var $twuid        = null;
    var $tw_fields    = null;
    var $access_token = null;
    var $verifier     = null;

    protected $needLogin = false;   // authorization page can also be used to create a new user

    protected function doPreparation()
    {
        $this->oauth_token = $this->arg('oauth_token');
        $this->verifier    = $this->arg('oauth_verifier');

        if ($this->scoped instanceof Profile) {
            try {
                $flink = Foreign_link::getByUserID($this->scoped->getID(), TWITTER_SERVICE);
                $fuser = $flink->getForeignUser();

                // If there's already a foreign link record and a foreign user
                // (no exceptions were thrown when fetching either of them...)
                // it means the accounts are already linked, and this is unecessary.
                // So go back.

                common_redirect(common_local_url('twittersettings'));
            } catch (NoResultException $e) {
                // but if we don't have a foreign user linked, let's continue authorization procedure.
            }
        }
    }

    protected function doPost()
    {
        // User was not logged in to StatusNet before

        $this->twuid = $this->trimmed('twuid');

        $this->tw_fields = array('screen_name' => $this->trimmed('tw_fields_screen_name'),
                                 'fullname' => $this->trimmed('tw_fields_fullname'));

        $this->access_token = new OAuthToken($this->trimmed('access_token_key'), $this->trimmed('access_token_secret'));

        if ($this->arg('create')) {
            common_debug('TwitterBridgeDebug - POST with create');
            if (!$this->boolean('license')) {
                // TRANS: Form validation error displayed when the checkbox to agree to the license has not been checked.
                throw new ClientException(_m('You cannot register if you do not agree to the license.'));
            }
            return $this->createNewUser();
        } elseif ($this->arg('connect')) {
            common_debug('TwitterBridgeDebug - POST with connect');
            return $this->connectNewUser();
        }

        common_debug('TwitterBridgeDebug - ' . print_r($this->args, true));
        // TRANS: Form validation error displayed when an unhandled error occurs.
        throw new ClientException(_m('No known action for POST.'));
    }

    /**
     * Asks Twitter for a request token, and then redirects to Twitter
     * to authorize it.
     */
    protected function authorizeRequestToken()
    {
        try {
            // Get a new request token and authorize it
            $client  = new TwitterOAuthClient();
            $req_tok = $client->getTwitterRequestToken();

            // 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->getTwitterAuthorizeLink($req_tok, $this->boolean('signin'));
        } catch (OAuthClientException $e) {
            $msg = sprintf(
                'OAuth client error - code: %1s, msg: %2s',
                $e->getCode(),
                $e->getMessage()
            );
            common_log(LOG_INFO, 'Twitter bridge - ' . $msg);
            // TRANS: Server error displayed when linking to a Twitter account fails.
            throw new ServerException(_m('Could not 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) {
            // TRANS: Server error displayed when linking to a Twitter account fails because of an incorrect oauth_token.
            throw new ServerException(_m('Could not link your Twitter account: oauth_token mismatch.'));
        }

        $twitter_user = null;

        try {
            $client = new TwitterOAuthClient($_SESSION['twitter_request_token'], $_SESSION['twitter_request_token_secret']);

            // Exchange the request token for an access token
            $atok = $client->getTwitterAccessToken($this->verifier);

            // 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 error - code: %1$s, msg: %2$s',
                $e->getCode(),
                $e->getMessage()
            );
            common_log(LOG_INFO, 'Twitter bridge - ' . $msg);
            // TRANS: Server error displayed when linking to a Twitter account fails.
            throw new ServerException(_m('Could not link your Twitter account.'));
        }

        if ($this->scoped instanceof Profile) {
            // Save the access token and Twitter user info

            $this->saveForeignLink($this->scoped->getID(), $twitter_user->id, $atok);
            save_twitter_user($twitter_user->id, $twitter_user->screen_name);

        } else {

            $this->twuid = $twitter_user->id;
            $this->tw_fields = array("screen_name" => $twitter_user->screen_name,
                                     "fullname" => $twitter_user->name);
            $this->access_token = $atok;
            return $this->tryLogin();
        }

        // Clean up the the mess we made in the session

        unset($_SESSION['twitter_request_token']);
        unset($_SESSION['twitter_request_token_secret']);

        if (common_logged_in()) {
            common_redirect(common_local_url('twittersettings'));
        }
    }

    /**
     * Saves a Foreign_link between Twitter user and local user,
     * which includes the access token and secret.
     *
     * @param int        $user_id StatusNet user ID
     * @param int        $twuid   Twitter user ID
     * @param OAuthToken $token   the access token to save
     *
     * @return nothing
     */
    function saveForeignLink($user_id, $twuid, $access_token)
    {
        $flink = new Foreign_link();

        $flink->user_id = $user_id;
        $flink->service = TWITTER_SERVICE;

        // delete stale flink, if any
        $result = $flink->find(true);

        if (!empty($result)) {
            $flink->safeDelete();
        }

        $flink->user_id     = $user_id;
        $flink->foreign_id  = $twuid;
        $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, false);

        $flink_id = $flink->insert();

        // We want to make sure we got a numerical >0 value, not just failed the insert (which would be === false)
        if (empty($flink_id)) {
            common_log_db_error($flink, 'INSERT', __FILE__);
            // TRANS: Server error displayed when linking to a Twitter account fails.
            throw new ServerException(_m('Could not link your Twitter account.'));
        }

        return $flink_id;
    }

    function getInstructions()
    {
        // TRANS: Page instruction. %s is the StatusNet sitename.
        return sprintf(_m('This is the first time you have logged into %s so we must connect your Twitter account to a local account. You can either create a new account, or connect with your existing account, if you have one.'), common_config('site', 'name'));
    }

    function title()
    {
        // TRANS: Page title.
        return _m('Twitter Account Setup');
    }

    public function showPage()
    {
        // $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->error)) {
            if (empty($this->oauth_token)) {
                // authorizeRequestToken either throws an exception or redirects
                $this->authorizeRequestToken();
            } else {
                $this->saveAccessToken();
            }
        }

        parent::showPage();
    }

    /**
     * @fixme much of this duplicates core code, which is very fragile.
     * Should probably be replaced with an extensible mini version of
     * the core registration form.
     */
    function showContent()
    {
        $this->elementStart('form', array('method' => 'post',
                                          'id' => 'form_settings_twitter_connect',
                                          'class' => 'form_settings',
                                          'action' => common_local_url('twitterauthorization')));
        $this->elementStart('fieldset', array('id' => 'settings_twitter_connect_options'));
        // TRANS: Fieldset legend.
        $this->element('legend', null, _m('Connection options'));

        $this->hidden('access_token_key', $this->access_token->key);
        $this->hidden('access_token_secret', $this->access_token->secret);
        $this->hidden('twuid', $this->twuid);
        $this->hidden('tw_fields_screen_name', $this->tw_fields['screen_name']);
        $this->hidden('tw_fields_name', $this->tw_fields['fullname']);
        $this->hidden('token', common_session_token());

        // Only allow new account creation if site is not flagged invite-only
        if (!common_config('site', 'inviteonly')) {
            $this->elementStart('fieldset');
            $this->element('legend', null,
                           // TRANS: Fieldset legend.
                           _m('Create new account'));
            $this->element('p', null,
                           // TRANS: Sub form introduction text.
                          _m('Create a new user with this nickname.'));
            $this->elementStart('ul', 'form_data');

            // Hook point for captcha etc
            Event::handle('StartRegistrationFormData', array($this));

            $this->elementStart('li');
            // TRANS: Field label.
            $this->input('newname', _m('New nickname'),
                         $this->username ?: '',
                         // TRANS: Field title for nickname field.
                         _m('1-64 lowercase letters or numbers, no punctuation or spaces.'));
            $this->elementEnd('li');
            $this->elementStart('li');
            // TRANS: Field label.
            $this->input('email', _m('LABEL','Email'), $this->getEmail(),
                         // TRANS: Field title for e-mail address field.
                         _m('Used only for updates, announcements, '.
                           'and password recovery'));
            $this->elementEnd('li');

            // Hook point for captcha etc
            Event::handle('EndRegistrationFormData', array($this));

            $this->elementEnd('ul');
            // TRANS: Button text for creating a new StatusNet account in the Twitter connect page.
            $this->submit('create', _m('BUTTON','Create'));
            $this->elementEnd('fieldset');
        }

        $this->elementStart('fieldset');
        $this->element('legend', null,
                       // TRANS: Fieldset legend.
                       _m('Connect existing account'));
        $this->element('p', null,
                       // TRANS: Sub form introduction text.
                       _m('If you already have an account, login with your username and password to connect it to your Twitter account.'));
        $this->elementStart('ul', 'form_data');
        $this->elementStart('li');
        // TRANS: Field label.
        $this->input('nickname', _m('Existing nickname'));
        $this->elementEnd('li');
        $this->elementStart('li');
        // TRANS: Field label.
        $this->password('password', _m('Password'));
        $this->elementEnd('li');
        $this->elementEnd('ul');
        $this->elementEnd('fieldset');

        $this->elementStart('fieldset');
        $this->element('legend', null,
                       // TRANS: Fieldset legend.
                       _m('License'));
        $this->elementStart('ul', 'form_data');
        $this->elementStart('li');
        $this->element('input', array('type' => 'checkbox',
                                      'id' => 'license',
                                      'class' => 'checkbox',
                                      'name' => 'license',
                                      'value' => 'true'));
        $this->elementStart('label', array('class' => 'checkbox', 'for' => 'license'));
        // TRANS: Text for license agreement checkbox.
        // TRANS: %s is the license as configured for the StatusNet site.
        $message = _m('My text and files are available under %s ' .
                     'except this private data: password, ' .
                     'email address, IM address, and phone number.');
        $link = '' .
                htmlspecialchars(common_config('license', 'title')) .
                '';
        $this->raw(sprintf(htmlspecialchars($message), $link));
        $this->elementEnd('label');
        $this->elementEnd('li');
        $this->elementEnd('ul');
        $this->elementEnd('fieldset');
        // TRANS: Button text for connecting an existing StatusNet account in the Twitter connect page..
        $this->submit('connect', _m('BUTTON','Connect'));
        $this->elementEnd('fieldset');
        $this->elementEnd('form');
    }

    /**
     * Get specified e-mail from the form, or the invite code.
     *
     * @return string
     */
    function getEmail()
    {
        $email = $this->trimmed('email');
        if (!empty($email)) {
            return $email;
        }

        // Terrible hack for invites...
        if (common_config('site', 'inviteonly')) {
            $code = $_SESSION['invitecode'];
            if ($code) {
                $invite = Invitation::getKV($code);

                if ($invite && $invite->address_type == 'email') {
                    return $invite->address;
                }
            }
        }
        return '';
    }

    protected function createNewUser()
    {
        common_debug('TwitterBridgeDebug - createNewUser');
        if (!Event::handle('StartRegistrationTry', array($this))) {
            common_debug('TwitterBridgeDebug - StartRegistrationTry failed');
            // TRANS: Client error displayed when trying to create a new user but a plugin aborted the process.
            throw new ClientException(_m('Registration of new user was aborted, maybe you failed a captcha?'));
        }

        if (common_config('site', 'closed')) {
            common_debug('TwitterBridgeDebug - site is closed for registrations');
            // TRANS: Client error displayed when trying to create a new user while creating new users is not allowed.
            throw new ClientException(_m('Registration not allowed.'));
        }

        $invite = null;

        if (common_config('site', 'inviteonly')) {
            common_debug('TwitterBridgeDebug - site is inviteonly');
            $code = $_SESSION['invitecode'];
            if (empty($code)) {
                // TRANS: Client error displayed when trying to create a new user while creating new users is not allowed.
                throw new ClientException(_m('Registration not allowed.'));
            }

            $invite = Invitation::getKV('code', $code);

            if (!$invite instanceof Invite) {
                common_debug('TwitterBridgeDebug - and we failed the invite code test');
                // TRANS: Client error displayed when trying to create a new user with an invalid invitation code.
                throw new ClientException(_m('Not a valid invitation code.'));
            }
        }

        common_debug('TwitterBridgeDebug - trying our nickname: '.$this->trimmed('newname'));
        // Nickname::normalize throws exception if the nickname is taken
        $nickname = Nickname::normalize($this->trimmed('newname'), true);

        $fullname = trim($this->tw_fields['fullname']);

        $args = array('nickname' => $nickname, 'fullname' => $fullname);

        if (!empty($invite)) {
            $args['code'] = $invite->code;
        }

        $email = $this->getEmail();
        if (!empty($email)) {
            $args['email'] = $email;
        }

        common_debug('TwitterBridgeDebug - registering user with args:'.var_export($args,true));
        $user = User::register($args);

        common_debug('TwitterBridgeDebug - registered the user and saving foreign link for '.$user->id);

        $this->saveForeignLink($user->id,
                               $this->twuid,
                               $this->access_token);

        common_debug('TwitterBridgeDebug - saving twitter user after creating new local user '.$user->id);
        save_twitter_user($this->twuid, $this->tw_fields['screen_name']);

        common_set_user($user);
        common_real_login(true);

        common_debug('TwitterBridge Plugin - ' .
                     "Registered new user $user->id from Twitter user $this->twuid");

        Event::handle('EndRegistrationTry', array($this));

        common_redirect(common_local_url('showstream', array('nickname' => $user->nickname)), 303);
    }

    function connectNewUser()
    {
        $nickname = $this->trimmed('nickname');
        $password = $this->trimmed('password');

        if (!common_check_user($nickname, $password)) {
            // TRANS: Form validation error displayed when connecting an existing user to a Twitter user fails because
            // TRANS: the provided username and/or password are incorrect.
            throw new ClientException(_m('Invalid username or password.'));
        }

        $user = User::getKV('nickname', $nickname);

        if ($user instanceof User) {
            common_debug('TwitterBridge Plugin - ' .
                         "Legit user to connect to Twitter: $nickname");
        }

        // throws exception on failure
        $this->saveForeignLink($user->id,
                               $this->twuid,
                               $this->access_token);

        save_twitter_user($this->twuid, $this->tw_fields['screen_name']);

        common_debug('TwitterBridge Plugin - ' .
                     "Connected Twitter user $this->twuid to local user $user->id");

        common_set_user($user);
        common_real_login(true);

        $this->goHome($user->nickname);
    }

    function connectUser()
    {
        $user = common_current_user();

        $result = $this->flinkUser($user->id, $this->twuid);

        if (empty($result)) {
            // TRANS: Server error displayed connecting a user to a Twitter user has failed.
            $this->serverError(_m('Error connecting user to Twitter.'));
        }

        common_debug('TwitterBridge Plugin - ' .
                     "Connected Twitter user $this->twuid to local user $user->id");

        // Return to Twitter connection settings tab
        common_redirect(common_local_url('twittersettings'), 303);
    }

    protected function tryLogin()
    {
        common_debug('TwitterBridge Plugin - ' .
                     "Trying login for Twitter user $this->twuid.");

        try {
            $flink = Foreign_link::getByForeignID($this->twuid, TWITTER_SERVICE);
            $user = $flink->getUser();

            common_debug('TwitterBridge Plugin - ' .
                         "Logged in Twitter user $flink->foreign_id as user $user->id ($user->nickname)");

            common_set_user($user);
            common_real_login(true);
            $this->goHome($user->nickname);
        } catch (NoResultException $e) {
            // Either no Foreign_link was found or not the user connected to it.
            // Let's just continue to allow creating or logging in as a new user.
        }
        common_debug("TwitterBridge Plugin - No flink found for twuid: {$this->twuid} - new user");

        // FIXME: what do we want to do here? I forgot
        return;
        throw new ServerException(_m('No foreign link found for Twitter user'));
    }

    function goHome($nickname)
    {
        $url = common_get_returnto();
        if ($url) {
            // We don't have to return to it again
            common_set_returnto(null);
        } else {
            $url = common_local_url('all',
                                    array('nickname' =>
                                          $nickname));
        }

        common_redirect($url, 303);
    }

    function bestNewNickname()
    {
        try {
            return Nickname::normalize($this->tw_fields['fullname'], true);
        } catch (NicknameException $e) {
            return null;
        }
    }
}


code:
<?php
/**
 * StatusNet, the distributed open-source microblogging tool
 *
 * 'Sign in with Twitter' login page
 *
 * 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  Login
 * @package   StatusNet
 * @author    Julien Chaumond <chaumond@gmail.com>
 * @author    Zach Copley <zach@status.net>
 * @copyright 2010 StatusNet, Inc.
 * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
 * @link      http://status.net/
 */

if (!defined('GNUSOCIAL')) { exit(1); }

require_once dirname(__DIR__) . '/twitter.php';

/**
 * Page for logging in with Twitter
 *
 * @category Login
 * @package  StatusNet
 * @author   Julien Chaumond <chaumond@gmail.com>
 * @author   Zach Copley <zach@status.net>
 * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
 * @link     http://status.net/
 *
 * @see      SettingsAction
 */
class TwitterloginAction extends LoginAction
{
    function title()
    {
        // TRANS: Title for login using Twitter page.
        return _m('TITLE','Twitter Login');
    }

    function getInstructions()
    {
        // TRANS: Instructions for login using Twitter page.
        return _m('Login with your Twitter account');
    }

    function showContent()
    {
        $this->elementStart('a', array('href' => common_local_url('twitterauthorization',
                                                                  null,
                                                                  array('signin' => true))));
        $this->element('img', array('src' => Plugin::staticPath('TwitterBridge', 'Sign-in-with-Twitter-lighter.png'),
                                    // TRANS: Alternative text for "sign in with Twitter" image.
                                    'alt' => _m('Sign in with Twitter')));
        $this->elementEnd('a');
    }

    function showLocalNav()
    {
        $nav = new LoginGroupNav($this);
        $nav->show();
    }
}

repo_name: andremichelle/gnu

license: mit


code:
<?php
/**
 * StatusNet, the distributed open-source microblogging tool
 *
 * Settings for Twitter integration
 *
 * 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  Settings
 * @package   StatusNet
 * @author    Evan Prodromou <evan@status.net>
 * @copyright 2008-2009 StatusNet, Inc.
 * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
 * @link      http://status.net/
 */

if (!defined('GNUSOCIAL')) { exit(1); }

require_once dirname(__DIR__) . '/twitter.php';

/**
 * Settings for Twitter integration
 *
 * @category Settings
 * @package  StatusNet
 * @author   Evan Prodromou <evan@status.net>
 * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
 * @link     http://status.net/
 *
 * @see      SettingsAction
 */
class TwittersettingsAction extends ProfileSettingsAction
{
    protected $flink = null;
    protected $fuser = null;

    protected function doPreparation()
    {
        try {
            $this->flink = Foreign_link::getByUserID($this->scoped->getID(), TWITTER_SERVICE);
            $this->fuser = $this->flink->getForeignUser();
        } catch (NoResultException $e) {
            // No foreign link found for this user!
        }
    }
    /**
     * Title of the page
     *
     * @return string Title of the page
     */

    function title()
    {
        // TRANS: Title for page with Twitter integration settings.
        return _m('Twitter settings');
    }

    /**
     * Instructions for use
     *
     * @return instructions for use
     */

    function getInstructions()
    {
        // TRANS: Instructions for page with Twitter integration settings.
        return _m('Connect your Twitter account to share your updates ' .
                  'with your Twitter friends and vice-versa.');
    }

    /**
     * Content area of the page
     *
     * Shows a form for associating a Twitter account with this
     * StatusNet account. Also lets the user set preferences.
     *
     * @return void
     */
    function showContent()
    {
        $this->elementStart('form', array('method' => 'post',
                                          'id' => 'form_settings_twitter',
                                          'class' => 'form_settings',
                                          'action' =>
                                          common_local_url('twittersettings')));

        $this->hidden('token', common_session_token());

        $this->elementStart('fieldset', array('id' => 'settings_twitter_account'));

        if ($this->fuser instanceof Foreign_user) {
            // TRANS: Fieldset legend.
            $this->element('legend', null, _m('Twitter account'));
            $this->elementStart('p', array('id' => 'form_confirmed'));
            $this->element('a', array('href' => $this->fuser->uri), $this->fuser->nickname);
            $this->elementEnd('p');
            $this->element('p', 'form_note',
                           // TRANS: Form note when a Twitter account has been connected.
                           _m('Connected Twitter account'));
            $this->elementEnd('fieldset');

            $this->elementStart('fieldset');

            // TRANS: Fieldset legend.
            $this->element('legend', null, _m('Disconnect my account from Twitter'));

            if (!$this->scoped->hasPassword()) {
                $this->elementStart('p', array('class' => 'form_guide'));
                // TRANS: Form guide. %s is a URL to the password settings.
                // TRANS: This message contains a Markdown link in the form [description](link).
                $message = sprintf(_m('Disconnecting your Twitter account ' .
                                      'could make it impossible to log in! Please ' .
                                      '[set a password](%s) first.'),
                                   common_local_url('passwordsettings'));
                $message = common_markup_to_html($message);
                $this->text($message);
                $this->elementEnd('p');
            } else {
                // TRANS: Form instructions. %1$s is the StatusNet sitename.
                $note = _m('Keep your %1$s account but disconnect from Twitter. ' .
                    'You can use your %1$s password to log in.');
                $site = common_config('site', 'name');

                $this->element('p', 'instructions',
                    sprintf($note, $site));

                // TRANS: Button text for disconnecting a Twitter account.
                $this->submit('disconnect', _m('BUTTON','Disconnect'));
            }

            $this->elementEnd('fieldset');

            $this->elementStart('fieldset', array('id' => 'settings_twitter_preferences'));

            // TRANS: Fieldset legend.
            $this->element('legend', null, _m('Preferences'));
            $this->elementStart('ul', 'form_data');
            $this->elementStart('li');
            $this->checkbox('noticesend',
                            // TRANS: Checkbox label.
                            _m('Automatically send my notices to Twitter.'),
                            $this->flink->noticesync & FOREIGN_NOTICE_SEND);
            $this->elementEnd('li');
            $this->elementStart('li');
            $this->checkbox('replysync',
                            // TRANS: Checkbox label.
                            _m('Send local "@" replies to Twitter.'),
                            $this->flink->noticesync & FOREIGN_NOTICE_SEND_REPLY);
            $this->elementEnd('li');
            $this->elementStart('li');
            $this->checkbox('repeatsync',
                            // TRANS: Checkbox label.
                            _m('Send local repeats to Twitter.'),
                            $this->flink->noticesync & FOREIGN_NOTICE_SEND_REPEAT);
            $this->elementEnd('li');
            $this->elementStart('li');
            $this->checkbox('friendsync',
                            // TRANS: Checkbox label.
                            _m('Subscribe to my Twitter friends here.'),
                            $this->flink->friendsync & FOREIGN_FRIEND_RECV);
            $this->elementEnd('li');

            if (common_config('twitterimport','enabled')) {
                $this->elementStart('li');
                $this->checkbox('noticerecv',
                                // TRANS: Checkbox label.
                                _m('Import my friends timeline.'),
                                $this->flink->noticesync & FOREIGN_NOTICE_RECV);
                $this->elementEnd('li');
            } else {
                // preserve setting even if bidrection bridge toggled off

                if ($this->flink->noticesync & FOREIGN_NOTICE_RECV) {
                    $this->hidden('noticerecv', true, 'noticerecv');
                }
            }

            $this->elementEnd('ul');

            if ($this->flink instanceof Foreign_link) {
                // TRANS: Button text for saving Twitter integration settings.
                $this->submit('save', _m('BUTTON','Save'));
            } else {
                // TRANS: Button text for adding Twitter integration.
                $this->submit('add', _m('BUTTON','Add'));
            }
        } else {
            $this->elementStart('ul', 'form_data');
            $this->elementStart('li', array('id' => 'settings_twitter_login_button'));
            $this->element('a', array('href' => common_local_url('twitterauthorization')),
                           // TRANS: Link description to connect to a Twitter account.
                           'Connect my Twitter account');
            $this->elementEnd('li');
            $this->elementEnd('ul');
        }

        $this->elementEnd('fieldset');

        $this->elementEnd('form');
    }

    /**
     * Handle posts to this form
     *
     * Based on the button that was pressed, muxes out to other functions
     * to do the actual task requested.
     *
     * All sub-functions reload the form with a message -- success or failure.
     *
     * @return void
     */
    protected function doPost()
    {
        if ($this->arg('save')) {
            return $this->savePreferences();
        } else if ($this->arg('disconnect')) {
            return $this->removeTwitterAccount();
        }
        // TRANS: Client error displayed when the submitted form contains unexpected data.
        throw new ClientException(_m('Unexpected form submission.'));
    }

    /**
     * Disassociate an existing Twitter account from this account
     *
     * @return void
     */
    protected function removeTwitterAccount()
    {
        if (!$this->flink instanceof Foreign_link) {
            // TRANS: Error message possibly displayed when trying to remove a connected Twitter account when there isn't one connected.
            throw new AlreadyFulfilledException(_m('No Twitter connection to remove.'));
        }

        $result = $this->flink->safeDelete();

        if ($result === false) {
            common_log_db_error($this->flink, 'DELETE', __FILE__);
            // TRANS: Server error displayed when trying to remove a connected Twitter account fails.
            throw new ServerException(_m('Could not remove Twitter user.'));
        }

        $this->flink = null;
        $this->fuser = null;

        // TRANS: Success message displayed after disconnecting a Twitter account.
        return _m('Twitter account disconnected.');
    }

    /**
     * Save user's Twitter-bridging preferences
     *
     * @return void
     */
    protected function savePreferences()
    {
        $noticesend = $this->boolean('noticesend');
        $noticerecv = $this->boolean('noticerecv');
        $friendsync = $this->boolean('friendsync');
        $replysync  = $this->boolean('replysync');
        $repeatsync  = $this->boolean('repeatsync');

        if (!$this->flink instanceof Foreign_link) {
            common_log_db_error($this->flink, 'SELECT', __FILE__);
            // TRANS: Server error displayed when saving Twitter integration preferences fails.
            throw new ServerException(_m('Your account is not linked to Twitter.'));
        }

        $original = clone($this->flink);
        $wasReceiving = (bool)($original->noticesync & FOREIGN_NOTICE_RECV);
        $this->flink->set_flags($noticesend, $noticerecv, $replysync, $repeatsync, $friendsync);
        $result = $this->flink->update($original);

        if ($result === false) {
            common_log_db_error($this->flink, 'UPDATE', __FILE__);
            // TRANS: Server error displayed when saving Twitter integration preferences fails.
            throw new ServerException(_m('Could not save Twitter preferences.'));
        }

        if ($wasReceiving xor $noticerecv) {
            $this->notifyDaemon($this->flink->foreign_id, $noticerecv);
        }

        // TRANS: Success message after saving Twitter integration preferences.
        return _m('Twitter preferences saved.');
    }

    /**
     * Tell the import daemon that we've updated a user's receive status.
     */
    function notifyDaemon($twitterUserId, $receiving)
    {
        // @todo... should use control signals rather than queues
    }
}


code:
<?php
/**
 * Data class for remembering notice-to-status mappings
 *
 * PHP version 5
 *
 * @category Data
 * @package  StatusNet
 * @author   Evan Prodromou <evan@status.net>
 * @license  http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
 * @link     http://status.net/
 *
 * StatusNet - the distributed open-source microblogging tool
 * Copyright (C) 2010, StatusNet, Inc.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.     See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

if (!defined('STATUSNET')) {
    exit(1);
}

require_once INSTALLDIR . '/classes/Memcached_DataObject.php';

/**
 * Data class for mapping notices to statuses
 *
 * Notices flow back and forth between Twitter and StatusNet. We use this
 * table to remember which StatusNet notice corresponds to which Twitter
 * status.
 *
 * Note that notice_id is unique only within a single database; if you
 * want to share this data for some reason, get the notice's URI and use
 * that instead, since it's universally unique.
 *
 * @category Action
 * @package  StatusNet
 * @author   Evan Prodromou <evan@status.net>
 * @license  http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
 * @link     http://status.net/
 *
 * @see      DB_DataObject
 */

class Notice_to_status extends Managed_DataObject
{
    public $__table = 'notice_to_status'; // table name
    public $notice_id;                    // int(4)  primary_key not_null
    public $status_id;                    // bigint not_null
    public $created;                      // datetime()   not_null
    public $modified;                     // datetime   not_null default_0000-00-00%2000%3A00%3A00

    public static function schemaDef()
    {
        return array(
            'fields' => array(
                'notice_id' => array('type' => 'int', 'not null' => true, 'description' => 'local notice id'),
                'status_id' => array('type' => 'int', 'size' => 'big', 'not null' => true, 'description' => 'twitter status id'),
                'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date this record was created'),
                'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'),
            ),
            'primary key' => array('notice_id'),
            'unique keys' => array(
                'status_id_key' => array('status_id'),
            ),
            'foreign keys' => array(
                'notice_to_status_notice_id_fkey' => array('notice', array('notice_id' => 'id')),
            ),
        );
    }

    /**
     * Save a mapping between a notice and a status
     * Warning: status_id values may not fit in 32-bit integers.
     *
     * @param integer $notice_id ID of the notice in StatusNet
     * @param integer $status_id ID of the status in Twitter
     *
     * @return Notice_to_status new object for this value
     */
    static function saveNew($notice_id, $status_id)
    {
        if (empty($notice_id)) {
            throw new Exception("Invalid notice_id $notice_id");
        }
        $n2s = Notice_to_status::getKV('notice_id', $notice_id);

        if (!empty($n2s)) {
            return $n2s;
        }

        if (empty($status_id)) {
            throw new Exception("Invalid status_id $status_id");
        }
        $n2s = Notice_to_status::getKV('status_id', $status_id);

        if (!empty($n2s)) {
            return $n2s;
        }

        common_debug("Mapping notice {$notice_id} to Twitter status {$status_id}");

        $n2s = new Notice_to_status();

        $n2s->notice_id = $notice_id;
        $n2s->status_id = $status_id;
        $n2s->created   = common_sql_now();

        $n2s->insert();

        return $n2s;
    }
}


code:
<?php
/**
 * Store last-touched ID for various timelines
 *
 * PHP version 5
 *
 * @category Data
 * @package  StatusNet
 * @author   Evan Prodromou <evan@status.net>
 * @license  http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
 * @link     http://status.net/
 *
 * StatusNet - the distributed open-source microblogging tool
 * Copyright (C) 2010, StatusNet, Inc.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.     See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

if (!defined('STATUSNET')) {
    exit(1);
}

require_once INSTALLDIR . '/classes/Memcached_DataObject.php';

/**
 * Store various timeline data
 *
 * We don't want to keep re-fetching the same statuses and direct messages from Twitter.
 * So, we store the last ID we see from a timeline, and store it. Next time
 * around, we use that ID in the since_id parameter.
 *
 * @category Action
 * @package  StatusNet
 * @author   Evan Prodromou <evan@status.net>
 * @license  http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
 * @link     http://status.net/
 *
 * @see      DB_DataObject
 */
class Twitter_synch_status extends Managed_DataObject
{
    public $__table = 'twitter_synch_status'; // table name
    public $foreign_id;                      // bigint primary_key not_null
    public $timeline;                        // varchar(191)  primary_key not_null   not 255 because utf8mb4 takes more space
    public $last_id;                         // bigint not_null
    public $created;                         // datetime()   not_null
    public $modified;                        // timestamp()   not_null default_CURRENT_TIMESTAMP

    public static function schemaDef()
    {
        return array(
            'fields' => array(
                'foreign_id' => array('type' => 'int', 'size' => 'big', 'not null' => true, 'description' => 'Foreign message ID'),
                'timeline' => array('type' => 'varchar', 'length' => 191, 'description' => 'timeline name'),
                'last_id' => array('type' => 'int', 'size' => 'big', 'not null' => true, 'description' => 'last id fetched'),
                'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date this record was created'),
                'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'),
            ),
            'primary key' => array('foreign_id', 'timeline'),
        );
    }

    static function getLastId($foreign_id, $timeline)
    {
        $tss = self::pkeyGet(array('foreign_id' => $foreign_id,
                                   'timeline' => $timeline));

        if (empty($tss)) {
            return null;
        } else {
            return $tss->last_id;
        }
    }

    static function setLastId($foreign_id, $timeline, $last_id)
    {
        $tss = self::pkeyGet(array('foreign_id' => $foreign_id,
                                   'timeline' => $timeline));

        if (empty($tss)) {
            $tss = new Twitter_synch_status();

            $tss->foreign_id = $foreign_id;
            $tss->timeline   = $timeline;
            $tss->last_id    = $last_id;
            $tss->created    = common_sql_now();
            $tss->modified   = $tss->created;

            $tss->insert();

            return true;
        } else {
            $orig = clone($tss);

            $tss->last_id  = $last_id;
            $tss->modified = common_sql_now();

            $tss->update();

            return true;
        }
    }
}


code:
#!/usr/bin/env php
<?php
/*
 * StatusNet - the distributed open-source microblogging tool
 * Copyright (C) 2008, 2009, StatusNet, Inc.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.     See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.     If not, see <http://www.gnu.org/licenses/>.
 */

define('INSTALLDIR', realpath(dirname(__FILE__) . '/../../..'));

$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 dirname(__DIR__) . '/twitter.php';

/**
 * Daemon to sync local friends with Twitter friends
 *
 * @category Twitter
 * @package  StatusNet
 * @author   Zach Copley <zach@status.net>
 * @author   Evan Prodromou <evan@status.net>
 * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
 * @link     http://status.net/
 */
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
     *
     **/
    function __construct($id = null, $interval = 60,
                         $max_children = 2, $debug = null)
    {
        parent::__construct($id, $interval, $max_children, $debug);
    }

    /**
     * Name of this daemon
     *
     * @return string Name of the daemon.
     */
    function name()
    {
        return ('synctwitterfriends.' . $this->_id);
    }

    /**
     * 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();

        $conn = $flink->getDatabaseConnection();

        $flink->service = TWITTER_SERVICE;
        $flink->orderBy('last_friendsync');
        $flink->limit(25);  // sync this many users during this run
        $flink->find();

        while ($flink->fetch()) {
            if (($flink->friendsync & FOREIGN_FRIEND_RECV) == FOREIGN_FRIEND_RECV) {
                $flinks[] = clone($flink);
            }
        }

        $conn->disconnect();

        global $_DB_DATAOBJECT;
        unset($_DB_DATAOBJECT['CONNECTIONS']);

        return $flinks;
    }

    // FIXME: make it so we can force a Foreign_link here without colliding with parent
    function childTask($flink) {
        // 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();

        $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(Foreign_link $flink)
    {
        $friends = array();

        $client = null;

        if (TwitterOAuthClient::isPackedToken($flink->credentials)) {
            $token = TwitterOAuthClient::unpackToken($flink->credentials);
            $client = new TwitterOAuthClient($token->key, $token->secret);
            common_debug($this->name() . '- Grabbing friends IDs with OAuth.');
        } else {
            common_debug("Skipping Twitter friends for {$flink->user_id} since not OAuth.");
            return $friends;
        }

        try {
            $friends_ids = $client->friendsIds();
        } catch (Exception $e) {
            common_log(LOG_WARNING, $this->name() .
                       ' - error getting friend ids: ' .
                       $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 (Exception $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 {
                if (is_array($more_friends)) {
                    $friends = array_merge($friends, $more_friends);
                }
            }
        }

        return $friends;
    }

    function subscribeTwitterFriends(Foreign_link $flink)
    {
        try {
            $profile = $flink->getProfile();
        } catch (NoResultException $e) {
            common_log(LOG_WARNING, 'Foreign_link has no matching local profile for local ID: '.$flink->user_id);
        }

        $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;
        }

        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 and try to subscribe
            try {
                $friend_flink = Foreign_link::getByForeignID($friend_id, TWITTER_SERVICE);

                // Get associated user and subscribe her
                $friend_profile = $friend_flink->getProfile();

                Subscription::start($profile, $friend_profile);
                common_log(LOG_INFO,
                           $this->name() . ' - Subscribed ' .
                           "{$friend_profile->nickname} to {$profile->nickname}.");
            } catch (NoResultException $e) {
                // either no foreign link for this friend's foreign ID or no profile found on local ID.
            } catch (Exception $e) {
                common_debug($this->name() .
                             ' - Tried and failed subscribing ' .
                             "{$friend_profile->nickname} to {$profile->nickname} - " .
                             $e->getMessage());
            }
        }

        return true;
    }

}

$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();


code:
#!/usr/bin/env php
<?php
/*
 * StatusNet - the distributed open-source microblogging tool
 * Copyright (C) 2008-2010, StatusNet, Inc.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

define('INSTALLDIR', realpath(dirname(__FILE__) . '/../../..'));

$shortoptions = 'fi::a';
$longoptions = array('id::', 'foreground', 'all');

$helptext = <<<END_OF_TWITTERDAEMON_HELP
Daemon script for receiving new notices from Twitter users.

    -i --id           Identity (default none)
    -a --all          Handle Twitter for all local sites
                      (requires Stomp queue handler, status_network setup)
    -f --foreground   Stay in the foreground (default background)

END_OF_TWITTERDAEMON_HELP;

require_once INSTALLDIR.'/scripts/commandline.inc';

class TwitterDaemon extends SpawningDaemon
{
    protected $allsites = false;

    function __construct($id=null, $daemonize=true, $threads=1, $allsites=false)
    {
        if ($threads != 1) {
            // This should never happen. :)
            throw new Exception("TwitterDaemon must run single-threaded");
        }
        parent::__construct($id, $daemonize, $threads);
        $this->allsites = $allsites;
    }

    function runThread()
    {
        common_log(LOG_INFO, 'Waiting to listen to Twitter and queues');

        $master = new TwitterMaster($this->get_id(), $this->processManager());
        $master->init($this->allsites);
        $master->service();

        common_log(LOG_INFO, 'terminating normally');

        return $master->respawn ? self::EXIT_RESTART : self::EXIT_SHUTDOWN;
    }

}

class TwitterMaster extends IoMaster
{
    protected $processManager;

    function __construct($id, $processManager)
    {
        parent::__construct($id);
        $this->processManager = $processManager;
    }

    /**
     * Initialize IoManagers for the currently configured site
     * which are appropriate to this instance.
     */
    function initManagers()
    {
        $qm = QueueManager::get();
        $qm->setActiveGroup('twitter');
        $this->instantiate($qm);
        $this->instantiate(new TwitterManager());
        $this->instantiate($this->processManager);
    }
}

class TwitterManager extends IoManager
{
    // Recommended resource limits from http://dev.twitter.com/pages/site_streams
    const MAX_STREAMS = 1000;
    const USERS_PER_STREAM = 100;
    const STREAMS_PER_SECOND = 20;

    protected $streams;
    protected $users;

    /**
     * Pull the site's active Twitter-importing users and start spawning
     * some data streams for them!
     *
     * @fixme check their last-id and check whether we'll need to do a manual pull.
     * @fixme abstract out the fetching so we can work over multiple sites.
     */
    protected function initStreams()
    {
        common_log(LOG_INFO, 'init...');
        // Pull Twitter user IDs for all users we want to pull data for
        $flink = new Foreign_link();
        $flink->service = TWITTER_SERVICE;
        // @fixme probably should do the bitfield check in a whereAdd but it's ugly :D
        $flink->find();

        $userIds = array();
        while ($flink->fetch()) {
            if (($flink->noticesync & FOREIGN_NOTICE_RECV) ==
                FOREIGN_NOTICE_RECV) {
                $userIds[] = $flink->foreign_id;

                if (count($userIds) >= self::USERS_PER_STREAM) {
                    $this->spawnStream($userIds);
                    $userIds = array();
                }
            }
        }

        if (count($userIds)) {
            $this->spawnStream($userIds);
        }
    }

    /**
     * Prepare a Site Stream connection for the given chunk of users.
     * The actual connection will be opened later.
     *
     * @param $userIds array of Twitter-side user IDs
     */
    protected function spawnStream($userIds)
    {
        $stream = $this->initSiteStream();
        $stream->followUsers($userIds);

        // Slip the stream reader into our list of active streams.
        // We'll manage its actual connection on the next go-around.
        $this->streams[] = $stream;

        // Record the user->stream mappings; this makes it easier for us to know
        // later if we need to kill something.
        foreach ($userIds as $id) {
            $this->users[$id] = $stream;
        }
    }

    /**
     * Initialize a generic site streams connection object.
     * All our connections will look like this, then we'll add users to them.
     *
     * @return TwitterStreamReader
     */
    protected function initSiteStream()
    {
        $auth = $this->siteStreamAuth();
        $stream = new TwitterSiteStream($auth);

        // Add our event handler callbacks. Whee!
        $this->setupEvents($stream);
        return $stream;
    }

    /**
     * Fetch the Twitter OAuth credentials to use to connect to the Site Streams API.
     *
     * This will use the locally-stored credentials for the applictation's owner account
     * from the site configuration. These should be configured through the administration
     * panels or manually in the config file.
     *
     * Will throw an exception if no credentials can be found -- but beware that invalid
     * credentials won't cause breakage until later.
     *
     * @return TwitterOAuthClient
     */
    protected function siteStreamAuth()
    {
        $token = common_config('twitter', 'stream_token');
        $secret = common_config('twitter', 'stream_secret');
        if (empty($token) || empty($secret)) {
            throw new ServerException('Twitter site streams have not been correctly configured. Configure the app owner account via the admin panel.');
        }
        return new TwitterOAuthClient($token, $secret);
    }

    /**
     * Collect the sockets for all active connections for i/o monitoring.
     *
     * @return array of resources
     */
    public function getSockets()
    {
        $sockets = array();
        foreach ($this->streams as $stream) {
            foreach ($stream->getSockets() as $socket) {
                $sockets[] = $socket;
            }
        }
        return $sockets;
    }

    /**
     * We're ready to process input from one of our data sources! Woooooo!
     * @fixme is there an easier way to map from socket back to owning module? :(
     *
     * @param resource $socket
     * @return boolean success
     */
    public function handleInput($socket)
    {
        foreach ($this->streams as $stream) {
            foreach ($stream->getSockets() as $aSocket) {
                if ($socket === $aSocket) {
                    $stream->handleInput($socket);
                }
            }
        }
        return true;
    }

    /**
     * Start the i/o system up! Prepare our connections and start opening them.
     *
     * @fixme do some rate-limiting on the stream setup
     * @fixme do some sensible backoff on failure etc
     */
    public function start()
    {
        $this->initStreams();
        foreach ($this->streams as $stream) {
            $stream->connect();
        }
        return true;
    }

    /**
     * Close down our connections when the daemon wraps up for business.
     */
    public function finish()
    {
        foreach ($this->streams as $index => $stream) {
            $stream->close();
            unset($this->streams[$index]);
        }
        return true;
    }

    public static function get()
    {
        throw new Exception('not a singleton');
    }

    /**
     * Set up event handlers on the streaming interface.
     *
     * @fixme add more event types as we add handling for them
     */
    protected function setupEvents(TwitterStreamReader $stream)
    {
        $handlers = array(
            'status',
        );
        foreach ($handlers as $event) {
            $stream->hookEvent($event, array($this, 'onTwitter' . ucfirst($event)));
        }
    }

    /**
     * Event callback notifying that a user has a new message in their home timeline.
     * We store the incoming message into the queues for processing, keeping our own
     * daemon running as shiny-fast as possible.
     *
     * @param object $status JSON data: Twitter status update
     * @fixme in all-sites mode we may need to route queue items into another site's
     *        destination queues, or multiple sites.
     */
    protected function onTwitterStatus($status, $context)
    {
        $data = array(
            'status' => $status,
            'for_user' => $context->for_user,
        );
        $qm = QueueManager::get();
        $qm->enqueue($data, 'tweetin');
    }
}

if (have_option('i', 'id')) {
    $id = get_option_value('i', 'id');
} else if (count($args) > 0) {
    $id = $args[0];
} else {
    $id = null;
}

$foreground = have_option('f', 'foreground');
$all = have_option('a') || have_option('--all');

$daemon = new TwitterDaemon($id, !$foreground, 1, $all);

$daemon->runOnce();


code:
#!/usr/bin/env php
<?php
/**
 * StatusNet - the distributed open-source microblogging tool
 * Copyright (C) 2008-2010, StatusNet, Inc.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.     See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.     If not, see <http://www.gnu.org/licenses/>.
 */

define('INSTALLDIR', realpath(dirname(__FILE__) . '/../../..'));

// Tune number of processes and how often to poll Twitter
// XXX: Should these things be in config.php?
define('MAXCHILDREN', 2);
define('POLL_INTERVAL', 70); // in seconds, Twitter API v1.1 says 15 calls every 15 mins

$shortoptions = 'di::';
$longoptions = array('id::', 'debug');

$helptext = <<<END_OF_TRIM_HELP
Batch script for retrieving Twitter messages from foreign service.

  -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/common.php';
require_once INSTALLDIR . '/lib/daemon.php';
require_once dirname(__DIR__) . '/twitter.php';

/**
 * Fetch statuses from Twitter
 *
 * Fetches statuses from Twitter and inserts them as notices
 *
 * NOTE: an Avatar path MUST be set in config.php for this
 * script to work, e.g.:
 *     $config['avatar']['path'] = $config['site']['path'] . '/avatar/';
 *
 * @todo @fixme @gar Fix the above. For some reason $_path is always empty when
 * this script is run, so the default avatar path is always set wrong in
 * default.php. Therefore it must be set explicitly in config.php. --Z
 *
 * @category Twitter
 * @package  StatusNet
 * @author   Zach Copley <zach@status.net>
 * @author   Evan Prodromou <evan@status.net>
 * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
 * @link     http://status.net/
 */
class TwitterStatusFetcher 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
     *
     **/
    function __construct($id = null, $interval = 60,
                         $max_children = 2, $debug = null)
    {
        parent::__construct($id, $interval, $max_children, $debug);
    }

    /**
     * Name of this daemon
     *
     * @return string Name of the daemon.
     */
    function name()
    {
        return ('twitterstatusfetcher.'.$this->_id);
    }

    /**
     * Find all the Twitter foreign links for users who have requested
     * importing of their friends' timelines
     *
     * @return array flinks an array of Foreign_link objects
     */
    function getObjects()
    {
        global $_DB_DATAOBJECT;
        $flink = new Foreign_link();
        $conn = &$flink->getDatabaseConnection();

        $flink->service = TWITTER_SERVICE;
        $flink->orderBy('last_noticesync');
        $flink->find();

        $flinks = array();

        while ($flink->fetch()) {

            if (($flink->noticesync & FOREIGN_NOTICE_RECV) ==
                FOREIGN_NOTICE_RECV) {
                $flinks[] = clone($flink);
                common_log(LOG_INFO, "sync: foreign id $flink->foreign_id");
            } else {
                common_log(LOG_INFO, "nothing to sync");
            }
        }

        $flink->free();
        unset($flink);

        $conn->disconnect();
        unset($_DB_DATAOBJECT['CONNECTIONS']);

        return $flinks;
    }

    // FIXME: make it so we can force a Foreign_link here without colliding with parent
    function childTask($flink) {
        // 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, 'home_timeline');
        $this->getTimeline($flink, 'mentions_timeline');

        $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(Foreign_link $flink, $timelineUri = 'home_timeline')
    {
        common_log(LOG_DEBUG, $this->name() . ' - Trying to get ' . $timelineUri .
                   ' timeline for Twitter user ' . $flink->foreign_id);

        $client = null;

        if (TwitterOAuthClient::isPackedToken($flink->credentials)) {
            $token = TwitterOAuthClient::unpackToken($flink->credentials);
            $client = new TwitterOAuthClient($token->key, $token->secret);
            common_log(LOG_DEBUG, $this->name() . ' - Grabbing ' . $timelineUri . ' timeline with OAuth.');
        } else {
            common_log(LOG_ERR, "Skipping " . $timelineUri . " timeline for " .
                       $flink->foreign_id . " since not OAuth.");
        }

        $timeline = null;

        $lastId = Twitter_synch_status::getLastId($flink->foreign_id, $timelineUri);

        common_log(LOG_DEBUG, "Got lastId value '" . $lastId . "' for foreign id '" .
                     $flink->foreign_id . "' and timeline '" . $timelineUri. "'");

        try {
            $timeline = $client->statusesTimeline($lastId, $timelineUri);
        } catch (Exception $e) {
            common_log(LOG_ERR, $this->name() .
                       ' - Unable to get ' . $timelineUri . ' timeline for user ' . $flink->user_id .
                       ' - code: ' . $e->getCode() . 'msg: ' . $e->getMessage());
        }

        if (empty($timeline)) {
            common_log(LOG_DEBUG, $this->name() .  " - Empty '" . $timelineUri . "' timeline.");
            return;
        }

        common_log(LOG_INFO, $this->name() .
                   ' - Retrieved ' . sizeof($timeline) . ' statuses from ' . $timelineUri . ' timeline' .
                   ' - for user ' . $flink->user_id);

        if (!empty($timeline)) {
            $qm = QueueManager::get();

            // Reverse to preserve order
            foreach (array_reverse($timeline) as $status) {
                $data = array(
                    'status' => $status,
                    'for_user' => $flink->foreign_id,
                );
                $qm->enqueue($data, 'tweetin');
            }

            $lastId = twitter_id($timeline[0]);
            Twitter_synch_status::setLastId($flink->foreign_id, $timelineUri, $lastId);
            common_debug("Set lastId value '$lastId' for foreign id '{$flink->foreign_id}' and timeline '" .
                         $timelineUri . "'");
        }

        // Okay, record the time we synced with Twitter for posterity
        $flink->last_noticesync = common_sql_now();
        $flink->update();
    }
}

$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;
}

$fetcher = new TwitterStatusFetcher($id, POLL_INTERVAL, MAXCHILDREN, $debug);
$fetcher->runOnce();


code:
<?php
/**
 * StatusNet, the distributed open-source microblogging tool
 *
 * 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  Plugin
 * @package   StatusNet
 * @author    Brion Vibber <brion@status.net>
 * @copyright 2010 StatusNet, Inc.
 * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
 * @link      http://status.net/
 */

class OAuthData
{
    public $consumer_key, $consumer_secret, $token, $token_secret;
}

/**
 *
 */
abstract class JsonStreamReader
{
    const CRLF = "\r\n";

    public $id;
    protected $socket = null;
    protected $state = 'init'; // 'init', 'connecting', 'waiting', 'headers', 'active'

    public function __construct()
    {
        $this->id = get_class($this) . '.' . substr(md5(mt_rand()), 0, 8);
    }

    /**
     * Starts asynchronous connect operation...
     *
     * @fixme Can we do the open-socket fully async to? (need write select infrastructure)
     *
     * @param string $url
     */
    public function connect($url)
    {
        common_log(LOG_DEBUG, "$this->id opening connection to $url");

        $scheme = parse_url($url, PHP_URL_SCHEME);
        if ($scheme == 'http') {
            $rawScheme = 'tcp';
        } else if ($scheme == 'https') {
            $rawScheme = 'ssl';
        } else {
            // TRANS: Server exception thrown when an invalid URL scheme is detected.
            throw new ServerException(_m('Invalid URL scheme for HTTP stream reader.'));
        }

        $host = parse_url($url, PHP_URL_HOST);
        $port = parse_url($url, PHP_URL_PORT);
        if (!$port) {
            if ($scheme == 'https') {
                $port = 443;
            } else {
                $port = 80;
            }
        }

        $path = parse_url($url, PHP_URL_PATH);
        $query = parse_url($url, PHP_URL_QUERY);
        if ($query) {
            $path .= '?' . $query;
        }

        $errno = $errstr = null;
        $timeout = 5;
        //$flags = STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT;
        $flags = STREAM_CLIENT_CONNECT;
        // @fixme add SSL params
        $this->socket = stream_socket_client("$rawScheme://$host:$port", $errno, $errstr, $timeout, $flags);

        $this->send($this->httpOpen($host, $path));

        stream_set_blocking($this->socket, false);
        $this->state = 'waiting';
    }

    /**
     * Send some fun data off to the server.
     *
     * @param string $buffer
     */
    function send($buffer)
    {
        fwrite($this->socket, $buffer);
    }

    /**
     * Read next packet of data from the socket.
     *
     * @return string
     */
    function read()
    {
        $buffer = fread($this->socket, 65536);
        return $buffer;
    }

    /**
     * Build HTTP request headers.
     *
     * @param string $host
     * @param string $path
     * @return string
     */
    protected function httpOpen($host, $path)
    {
        $lines = array(
            "GET $path HTTP/1.1",
            "Host: $host",
            'User-Agent: ' . HTTPClient::userAgent() . ' (TwitterBridgePlugin)',
            "Connection: close",
            "",
            ""
        );
        return implode(self::CRLF, $lines);
    }

    /**
     * Close the current connection, if open.
     */
    public function close()
    {
        if ($this->isConnected()) {
            common_log(LOG_DEBUG, "$this->id closing connection.");
            fclose($this->socket);
            $this->socket = null;
        }
    }

    /**
     * Are we currently connected?
     *
     * @return boolean
     */
    public function isConnected()
    {
        return $this->socket !== null;
    }

    /**
     * Send any sockets we're listening on to the IO manager
     * to wait for input.
     *
     * @return array of resources
     */
    public function getSockets()
    {
        if ($this->isConnected()) {
            return array($this->socket);
        }
        return array();
    }

    /**
     * Take a chunk of input over the horn and go go go! :D
     *
     * @param string $buffer
     */
    public function handleInput($socket)
    {
        if ($this->socket !== $socket) {
            // TRANS: Exception thrown when input from an inexpected socket is encountered.
            throw new Exception(_m('Got input from unexpected socket!'));
        }

        try {
            $buffer = $this->read();
            $lines = explode(self::CRLF, $buffer);
            foreach ($lines as $line) {
                $this->handleLine($line);
            }
        } catch (Exception $e) {
            common_log(LOG_ERR, "$this->id aborting connection due to error: " . $e->getMessage());
            fclose($this->socket);
            throw $e;
        }
    }

    protected function handleLine($line)
    {
        switch ($this->state)
        {
            case 'waiting':
                $this->handleLineWaiting($line);
                break;
            case 'headers':
                $this->handleLineHeaders($line);
                break;
            case 'active':
                $this->handleLineActive($line);
                break;
            default:
                // TRANS: Exception thrown when an invalid state is encountered in handleLine.
                // TRANS: %s is the invalid state.
                throw new Exception(sprintf(_m('Invalid state in handleLine: %s.'),$this->state));
        }
    }

    /**
     *
     * @param <type> $line
     */
    protected function handleLineWaiting($line)
    {
        $bits = explode(' ', $line, 3);
        if (count($bits) != 3) {
            // TRANS: Exception thrown when an invalid response line is encountered.
            // TRANS: %s is the invalid line.
            throw new Exception(sprintf(_m('Invalid HTTP response line: %s.'),$line));
        }

        list($http, $status, $text) = $bits;
        if (substr($http, 0, 5) != 'HTTP/') {
            // TRANS: Exception thrown when an invalid response line part is encountered.
            // TRANS: %1$s is the chunk, %2$s is the line.
            throw new Exception(sprintf(_m('Invalid HTTP response line chunk "%1$s": %2$s.'),$http, $line));
        }
        if ($status != '200') {
            // TRANS: Exception thrown when an invalid response code is encountered.
            // TRANS: %1$s is the response code, %2$s is the line.
            throw new Exception(sprintf(_m('Bad HTTP response code %1$s: %2$s.'),$status,$line));
        }
        common_log(LOG_DEBUG, "$this->id $line");
        $this->state = 'headers';
    }

    protected function handleLineHeaders($line)
    {
        if ($line == '') {
            $this->state = 'active';
            common_log(LOG_DEBUG, "$this->id connection is active!");
        } else {
            common_log(LOG_DEBUG, "$this->id read HTTP header: $line");
            $this->responseHeaders[] = $line;
        }
    }

    protected function handleLineActive($line)
    {
        if ($line == "") {
            // Server sends empty lines as keepalive.
            return;
        }
        $data = json_decode($line);
        if ($data) {
            $this->handleJson($data);
        } else {
            common_log(LOG_ERR, "$this->id received bogus JSON data: " . var_export($line, true));
        }
    }

    abstract protected function handleJson(stdClass $data);
}


code:
<?php
/*
 * StatusNet - the distributed open-source microblogging tool
 * Copyright (C) 2010, StatusNet, Inc.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }

require_once dirname(__DIR__) . '/twitter.php';

/**
 * Queue handler to deal with incoming Twitter status updates, as retrieved by
 * TwitterDaemon (twitterdaemon.php).
 *
 * The queue handler passes the status through TwitterImporter for import into the
 * local database (if necessary), then adds the imported notice to the local inbox
 * of the attached Twitter user.
 *
 * Warning: the way we do inbox distribution manually means that realtime, XMPP, etc
 * don't work on Twitter-borne messages. When TwitterImporter is changed to handle
 * that correctly, we'll only need to do this once...?
 */
class TweetInQueueHandler extends QueueHandler
{
    function transport()
    {
        return 'tweetin';
    }

    function handle($data)
    {
        // JSON object with Twitter data
        $status = $data['status'];

        // Twitter user ID this incoming data belongs to.
        $receiver = $data['for_user'];

        $importer = new TwitterImport();
        $notice = $importer->importStatus($status);
        if ($notice instanceof Notice) {
            try {
                $flink = Foreign_link::getByForeignID($receiver, TWITTER_SERVICE);
                common_log(LOG_DEBUG, "TweetInQueueHandler - Got flink so add notice ".
                           $notice->id." to attentions for user ".$flink->user_id);
                try {
                    Attention::saveNew($notice, $flink->getProfile());
                } catch (Exception $e) {
                    // Log the exception, but make sure we don't bail out, we
                    // still have a queue item to remove here-after.
                    common_log(LOG_ERR, "Failed adding notice {$notice->id} to attentions for user {$flink->user_id}: " .
                                        $e->getMessage());
                }
            } catch (NoResultException $e) {
               common_log(LOG_DEBUG, "TweetInQueueHandler - No flink found for foreign user ".$receiver);
            }
        }

        return true;
    }
}