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    Zach Copley <zach@status.net>
 * @author    Julien C <chaumond@gmail.com>
 * @copyright 2009-2010 Control Yourself, 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('STATUSNET')) {
    exit(1);
}

require_once __DIR__ . '/twitter.php';

/**
 * Plugin for sending and importing Twitter statuses
 *
 * This class allows users to link their Twitter accounts
 *
 * Depends on Favorite plugin.
 *
 * @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/
 * @link     http://twitter.com/
 */
class TwitterBridgePlugin extends Plugin
{
    const VERSION = GNUSOCIAL_VERSION;
    public $adminImportControl = false; // Should the 'import' checkbox be exposed in the admin panel?

    /**
     * Initializer for the plugin.
     */
    function initialize()
    {
        // Allow the key and secret to be passed in
        // Control panel will override

        if (isset($this->consumer_key)) {
            $key = common_config('twitter', 'consumer_key');
            if (empty($key)) {
                Config::save('twitter', 'consumer_key', $this->consumer_key);
            }
        }

        if (isset($this->consumer_secret)) {
            $secret = common_config('twitter', 'consumer_secret');
            if (empty($secret)) {
                Config::save(
                    'twitter',
                    'consumer_secret',
                    $this->consumer_secret
                );
            }
        }
    }

    /**
     * Check to see if there is a consumer key and secret defined
     * for Twitter integration.
     *
     * @return boolean result
     */
    static function hasKeys()
    {
        $ckey    = common_config('twitter', 'consumer_key');
        $csecret = common_config('twitter', 'consumer_secret');

        if (empty($ckey) && empty($csecret)) {
            $ckey    = common_config('twitter', 'global_consumer_key');
            $csecret = common_config('twitter', 'global_consumer_secret');
        }

        if (!empty($ckey) && !empty($csecret)) {
            return true;
        }

        return false;
    }

    /**
     * Add Twitter-related paths to the router table
     *
     * Hook for RouterInitialized event.
     *
     * @param URLMapper $m path-to-action mapper
     *
     * @return boolean hook return
     */
    public function onRouterInitialized(URLMapper $m)
    {
        $m->connect('panel/twitter', array('action' => 'twitteradminpanel'));

        if (self::hasKeys()) {
            $m->connect(
                'twitter/authorization',
                array('action' => 'twitterauthorization')
            );
            $m->connect(
                'settings/twitter', array(
                    'action' => 'twittersettings'
                    )
                );
            if (common_config('twitter', 'signin')) {
                $m->connect(
                    'main/twitterlogin',
                    array('action' => 'twitterlogin')
                );
            }
        }

        return true;
    }

    /*
     * Add a login tab for 'Sign in with Twitter'
     *
     * @param Action $action the current action
     *
     * @return void
     */
    function onEndLoginGroupNav($action)
    {
        $action_name = $action->trimmed('action');

        if (self::hasKeys() && common_config('twitter', 'signin')) {
            $action->menuItem(
                common_local_url('twitterlogin'),
                // TRANS: Menu item in login navigation.
                _m('MENU','Twitter'),
                // TRANS: Title for menu item in login navigation.
                _m('Login or register using Twitter.'),
                'twitterlogin' === $action_name
            );
        }

        return true;
    }

    /**
     * Add the Twitter Settings page to the Connect Settings menu
     *
     * @param Action $action The calling page
     *
     * @return boolean hook return
     */
    function onEndConnectSettingsNav($action)
    {
        if (self::hasKeys()) {
            $action_name = $action->trimmed('action');

            $action->menuItem(
                common_local_url('twittersettings'),
                // TRANS: Menu item in connection settings navigation.
                _m('MENU','Twitter'),
                // TRANS: Title for menu item in connection settings navigation.
                _m('Twitter integration options'),
                $action_name === 'twittersettings'
            );
        }
        return true;
    }

    /**
     * Add a Twitter queue item for each notice
     *
     * @param Notice $notice      the notice
     * @param array  &$transports the list of transports (queues)
     *
     * @return boolean hook return
     */
    function onStartEnqueueNotice($notice, &$transports)
    {
        if (self::hasKeys() && $notice->isLocal() && $notice->inScope(null)) {
            // Avoid a possible loop
            if ($notice->source != 'twitter') {
                array_push($transports, 'twitter');
            }
        }
        return true;
    }

    /**
     * Add Twitter bridge daemons to the list of daemons to start
     *
     * @param array $daemons the list fo daemons to run
     *
     * @return boolean hook return
     */
    function onGetValidDaemons(&$daemons)
    {
        if (self::hasKeys()) {
            array_push(
                $daemons,
                INSTALLDIR
                . '/plugins/TwitterBridge/daemons/synctwitterfriends.php'
            );
            if (common_config('twitterimport', 'enabled')) {
                array_push(
                    $daemons,
                    INSTALLDIR
                    . '/plugins/TwitterBridge/daemons/twitterstatusfetcher.php'
                    );
            }
        }

        return true;
    }

    /**
     * Register Twitter notice queue handler
     *
     * @param QueueManager $manager
     *
     * @return boolean hook return
     */
    function onEndInitializeQueueManager($manager)
    {
        if (self::hasKeys()) {
            // Outgoing notices -> twitter
            $manager->connect('twitter', 'TwitterQueueHandler');

            // Incoming statuses <- twitter
            $manager->connect('tweetin', 'TweetInQueueHandler');
        }
        return true;
    }

    /**
     * If the plugin's installed, this should be accessible to admins
     */
    function onAdminPanelCheck($name, &$isOK)
    {
        if ($name == 'twitter') {
            $isOK = true;
            return false;
        }
        return true;
    }

    /**
     * Add a Twitter tab to the admin panel
     *
     * @param Widget $nav Admin panel nav
     *
     * @return boolean hook value
     */

    function onEndAdminPanelNav($nav)
    {
        if (AdminPanelAction::canAdmin('twitter')) {

            $action_name = $nav->action->trimmed('action');

            $nav->out->menuItem(
                common_local_url('twitteradminpanel'),
                // TRANS: Menu item in administrative panel that leads to the Twitter bridge configuration.
                _m('Twitter'),
                // TRANS: Menu item title in administrative panel that leads to the Twitter bridge configuration.
                _m('Twitter bridge configuration page.'),
                $action_name == 'twitteradminpanel',
                'nav_twitter_admin_panel'
            );
        }

        return true;
    }

    /**
     * Plugin version data
     *
     * @param array &$versions array of version blocks
     *
     * @return boolean hook value
     */
    function onPluginVersion(&$versions)
    {
        $versions[] = array(
            'name' => 'TwitterBridge',
            'version' => self::VERSION,
            'author' => 'Zach Copley, Julien C, Jean Baptiste Favre',
            'homepage' => 'http://status.net/wiki/Plugin:TwitterBridge',
            // TRANS: Plugin description.
            'rawdescription' => _m('The Twitter "bridge" plugin allows integration ' .
                'of a StatusNet instance with ' .
                'Twitter.'
            )
        );
        return true;
    }

    /**
     * Expose the adminImportControl setting to the administration panel code.
     * This allows us to disable the import bridge enabling checkbox for administrators,
     * since on a bulk farm site we can't yet automate the import daemon setup.
     *
     * @return boolean hook value;
     */
    function onTwitterBridgeAdminImportControl()
    {
        return (bool)$this->adminImportControl;
    }

    /**
     * When the site is set to ssl=sometimes mode, we should make sure our
     * various auth-related pages are on SSL to keep things looking happy.
     * Although we're not submitting passwords directly, we do link out to
     * an authentication source and it's a lot happier if we've got some
     * protection against MitM.
     *
     * @param string $action name
     * @param boolean $ssl outval to force SSL
     * @return mixed hook return value
     */
    function onSensitiveAction($action, &$ssl)
    {
        $sensitive = array('twitteradminpanel',
                           'twittersettings',
                           'twitterauthorization',
                           'twitterlogin');
        if (in_array($action, $sensitive)) {
            $ssl = true;
            return false;
        } else {
            return true;
        }
    }

    /**
     * Database schema setup
     *
     * We maintain a table mapping StatusNet notices to Twitter statuses
     *
     * @see Schema
     * @see ColumnDef
     *
     * @return boolean hook value; true means continue processing, false means stop.
     */
    function onCheckSchema()
    {
        $schema = Schema::get();

        // For saving the last-synched status of various timelines
        // home_timeline, messages (in), messages (out), ...
        $schema->ensureTable('twitter_synch_status', Twitter_synch_status::schemaDef());

        // For storing user-submitted flags on profiles
        $schema->ensureTable('notice_to_status', Notice_to_status::schemaDef());

        return true;
    }

    /**
     * If a notice gets deleted, remove the Notice_to_status mapping and
     * delete the status on Twitter.
     *
     * @param User   $user   The user doing the deleting
     * @param Notice $notice The notice getting deleted
     *
     * @return boolean hook value
     */
    function onStartDeleteOwnNotice(User $user, Notice $notice)
    {
        $n2s = Notice_to_status::getKV('notice_id', $notice->id);

        if (!empty($n2s)) {

            $flink = Foreign_link::getByUserID($notice->profile_id,
                                               TWITTER_SERVICE); // twitter service

            if (empty($flink)) {
                return true;
            }

            if (!TwitterOAuthClient::isPackedToken($flink->credentials)) {
                $this->log(LOG_INFO, "Skipping deleting notice for {$notice->id} since link is not OAuth.");
                return true;
            }

            try {
                $token = TwitterOAuthClient::unpackToken($flink->credentials);
                $client = new TwitterOAuthClient($token->key, $token->secret);

                $client->statusesDestroy($n2s->status_id);
            } catch (Exception $e) {
                common_log(LOG_ERR, "Error attempting to delete bridged notice from Twitter: " . $e->getMessage());
            }

            $n2s->delete();
        }
        return true;
    }

    /**
     * Notify remote users when their notices get favorited.
     *
     * @param Profile or User $profile of local user doing the faving
     * @param Notice $notice being favored
     * @return hook return value
     */
    function onEndFavorNotice(Profile $profile, Notice $notice)
    {
        $flink = Foreign_link::getByUserID($profile->id,
                                           TWITTER_SERVICE); // twitter service

        if (empty($flink)) {
            return true;
        }

        if (!TwitterOAuthClient::isPackedToken($flink->credentials)) {
            $this->log(LOG_INFO, "Skipping fave processing for {$profile->id} since link is not OAuth.");
            return true;
        }

        $status_id = twitter_status_id($notice);

        if (empty($status_id)) {
            return true;
        }

        try {
            $token = TwitterOAuthClient::unpackToken($flink->credentials);
            $client = new TwitterOAuthClient($token->key, $token->secret);

            $client->favoritesCreate($status_id);
        } catch (Exception $e) {
            common_log(LOG_ERR, "Error attempting to favorite bridged notice on Twitter: " . $e->getMessage());
        }

        return true;
    }

    /**
     * Notify remote users when their notices get de-favorited.
     *
     * @param Profile $profile Profile person doing the de-faving
     * @param Notice  $notice  Notice being favored
     *
     * @return hook return value
     */
    function onEndDisfavorNotice(Profile $profile, Notice $notice)
    {
        $flink = Foreign_link::getByUserID($profile->id,
                                           TWITTER_SERVICE); // twitter service

        if (empty($flink)) {
            return true;
        }

        if (!TwitterOAuthClient::isPackedToken($flink->credentials)) {
            $this->log(LOG_INFO, "Skipping fave processing for {$profile->id} since link is not OAuth.");
            return true;
        }

        $status_id = twitter_status_id($notice);

        if (empty($status_id)) {
            return true;
        }

        try {
            $token = TwitterOAuthClient::unpackToken($flink->credentials);
            $client = new TwitterOAuthClient($token->key, $token->secret);

            $client->favoritesDestroy($status_id);
        } catch (Exception $e) {
            common_log(LOG_ERR, "Error attempting to unfavorite bridged notice on Twitter: " . $e->getMessage());
        }

        return true;
    }

    function onStartGetProfileUri($profile, &$uri)
    {
        if (preg_match('!^https?://twitter.com/!', $profile->profileurl)) {
            $uri = $profile->profileurl;
            return false;
        }
        return true;
    }

    /**
     * Add links in the user's profile block to their Twitter profile URL.
     *
     * @param Profile $profile The profile being shown
     * @param Array   &$links  Writeable array of arrays (href, text, image).
     *
     * @return boolean hook value (true)
     */

    function onOtherAccountProfiles($profile, &$links)
    {
        $fuser = null;

        $flink = Foreign_link::getByUserID($profile->id, TWITTER_SERVICE);

        if (!empty($flink)) {
            $fuser = $flink->getForeignUser();

            if (!empty($fuser)) {
                $links[] = array("href" => $fuser->uri,
                                 "text" => sprintf(_("@%s on Twitter"), $fuser->nickname),
                                 "image" => $this->path("icons/twitter-bird-white-on-blue.png"));
            }
        }

        return true;
    }

    public function onEndShowHeadElements(Action $action)
    {
        if (!($action instanceof AttachmentAction)) {
            return true;
        }

        /* Twitter card support. See https://dev.twitter.com/docs/cards */
        /* @fixme: should we display twitter cards only for attachments posted
         *         by local users ? Seems mandatory to display twitter:creator
         *
         * Author: jbfavre
         */
        switch ($action->attachment->mimetype) {
            case 'image/pjpeg':
            case 'image/jpeg':
            case 'image/jpg':
            case 'image/png':
            case 'image/gif':
                $action->element('meta', array('name'    => 'twitter:card',
                                             'content' => 'photo'),
                                       null);
                $action->element('meta', array('name'    => 'twitter:url',
                                             'content' => common_local_url('attachment',
                                                              array('attachment' => $action->attachment->id))),
                                       null );
                $action->element('meta', array('name'    => 'twitter:image',
                                             'content' => $action->attachment->url));
                $action->element('meta', array('name'    => 'twitter:title',
                                             'content' => $action->attachment->title));

                $ns = new AttachmentNoticeSection($action);
                $notices = $ns->getNotices();
                $noticeArray = $notices->fetchAll();

                // Should not have more than 1 notice for this attachment.
                if( count($noticeArray) != 1 ) { break; }
                $post = $noticeArray[0];

                $flink = Foreign_link::getByUserID($post->profile_id, TWITTER_SERVICE);
                if( $flink ) { // Our local user has registered Twitter Gateway
                    $fuser = Foreign_user::getForeignUser($flink->foreign_id, TWITTER_SERVICE);
                    if( $fuser ) { // Got nickname for local user's Twitter account
                        $action->element('meta', array('name'    => 'twitter:creator',
                                                     'content' => '@'.$fuser->nickname));
                    }
                }
                break;
            default: break;
        }

        return true;
    }
}


code:
<?php
/**
 * StatusNet, the distributed open-source microblogging tool
 *
 * Twitter bridge administration panel
 *
 * 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    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('STATUSNET')) {
    exit(1);
}

/**
 * Administer global Twitter bridge settings
 *
 * @category Admin
 * @package  StatusNet
 * @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/
 */
class TwitteradminpanelAction extends AdminPanelAction
{
    /**
     * Returns the page title
     *
     * @return string page title
     */
    function title()
    {
        // TRANS: Page title for Twitter administration panel.
        return _m('TITLE','Twitter');
    }

    /**
     * Instructions for using this form.
     *
     * @return string instructions
     */
    function getInstructions()
    {
        // TRANS: Instructions for Twitter bridge administration page.
        return _m('Twitter bridge settings');
    }

    /**
     * Show the Twitter admin panel form
     *
     * @return void
     */
    function showForm()
    {
        $form = new TwitterAdminPanelForm($this);
        $form->show();
        return;
    }

    /**
     * Save settings from the form
     *
     * @return void
     */
    function saveSettings()
    {
        static $settings = array(
            'twitter'     => array('consumer_key', 'consumer_secret'),
            'integration' => array('source')
        );

        static $booleans = array(
            'twitter'       => array('signin')
        );
        if (Event::handle('TwitterBridgeAdminImportControl')) {
            $booleans['twitterimport'] = array('enabled');
        }

        $values = array();

        foreach ($settings as $section => $parts) {
            foreach ($parts as $setting) {
                $values[$section][$setting]
                    = $this->trimmed($setting);
            }
        }

        foreach ($booleans as $section => $parts) {
            foreach ($parts as $setting) {
                $values[$section][$setting]
                    = ($this->boolean($setting)) ? 1 : 0;
            }
        }

        // This throws an exception on validation errors

        $this->validate($values);

        // assert(all values are valid);

        $config = new Config();

        $config->query('BEGIN');

        foreach ($settings as $section => $parts) {
            foreach ($parts as $setting) {
                Config::save($section, $setting, $values[$section][$setting]);
            }
        }

        foreach ($booleans as $section => $parts) {
            foreach ($parts as $setting) {
                Config::save($section, $setting, $values[$section][$setting]);
            }
        }

        $config->query('COMMIT');

        // Flush the router cache: we may have enabled/disabled bridging,
        // which will add or remove some actions.
        $cache = Cache::instance();
        $cache->delete(Router::cacheKey());

        return;
    }

    function validate(&$values)
    {
        // Validate consumer key and secret (can't be too long)

        if (mb_strlen($values['twitter']['consumer_key']) > 255) {
            $this->clientError(
                // TRANS: Client error displayed when a consumer key is invalid because it is too long.
                _m('Invalid consumer key. Maximum length is 255 characters.')
            );
        }

        if (mb_strlen($values['twitter']['consumer_secret']) > 255) {
            $this->clientError(
                // TRANS: Client error displayed when a consumer secret is invalid because it is too long.
                _m('Invalid consumer secret. Maximum length is 255 characters.')
            );
        }
    }

    function isImportEnabled()
    {
        // Since daemon setup isn't automated yet...
        // @todo: if merged into main queues, detect presence of daemon config
        return true;
    }
}

class TwitterAdminPanelForm extends AdminForm
{
    /**
     * ID of the form
     *
     * @return int ID of the form
     */
    function id()
    {
        return 'twitteradminpanel';
    }

    /**
     * class of the form
     *
     * @return string class of the form
     */
    function formClass()
    {
        return 'form_settings';
    }

    /**
     * Action of the form
     *
     * @return string URL of the action
     */
    function action()
    {
        return common_local_url('twitteradminpanel');
    }

    /**
     * Data elements of the form
     *
     * @return void
     */
    function formData()
    {
        $this->out->elementStart(
            'fieldset',
            array('id' => 'settings_twitter-application')
        );
        // TRANS: Fieldset legend for Twitter application settings.
        $this->out->element('legend', null, _m('Twitter application settings'));
        $this->out->elementStart('ul', 'form_data');

        $this->li();
        $this->input(
            'consumer_key',
            // TRANS: Field label for Twitter assigned consumer key.
            _m('Consumer key'),
            // TRANS: Field title for Twitter assigned consumer key.
            _m('The consumer key assigned by Twitter.'),
            'twitter'
        );
        $this->unli();

        $this->li();
        $this->input(
            'consumer_secret',
            // TRANS: Field label for Twitter assigned consumer secret.
            _m('Consumer secret'),
            // TRANS: Field title for Twitter assigned consumer secret.
            _m('The consumer secret assigned by Twitter.'),
            'twitter'
        );
        $this->unli();

        $globalConsumerKey = common_config('twitter', 'global_consumer_key');
        $globalConsumerSec = common_config('twitter', 'global_consumer_secret');

        if (!empty($globalConsumerKey) && !empty($globalConsumerSec)) {
            $this->li();
            // TRANS: Form guide displayed when two required fields have already been provided.
            $this->out->element('p', 'form_guide', _m('Note: A global consumer key and secret are set.'));
            $this->unli();
        }

        $this->li();
        $this->input(
            'source',
            // TRANS: Field label for Twitter application name.
            _m('Integration source'),
            // TRANS: Field title for Twitter application name.
            _m('The name of your Twitter application.'),
            'integration'
        );
        $this->unli();

        $this->out->elementEnd('ul');
        $this->out->elementEnd('fieldset');

        $this->out->elementStart(
            'fieldset',
            array('id' => 'settings_twitter-options')
        );
        // TRANS: Fieldset legend for Twitter integration options.
        $this->out->element('legend', null, _m('Options'));

        $this->out->elementStart('ul', 'form_data');

        $this->li();

        $this->out->checkbox(
            // TRANS: Checkbox label for global setting.
            'signin', _m('Enable "Sign-in with Twitter"'),
            (bool) $this->value('signin', 'twitter'),
            // TRANS: Checkbox title.
            _m('This allow users to login with their Twitter credentials.')
        );
        $this->unli();

        if (Event::handle('TwitterBridgeAdminImportControl')) {
            $this->li();
            $this->out->checkbox(
                // TRANS: Checkbox label for global setting.
                'enabled', _m('Enable Twitter import'),
                (bool) $this->value('enabled', 'twitterimport'),
                // TRANS: Checkbox title for global setting.
                _m('Allow users to import their Twitter friends\' timelines. Requires daemons to be manually configured.')
            );
            $this->unli();
        }

        $this->out->elementEnd('ul');

        $this->out->elementEnd('fieldset');
    }

    /**
     * Action elements
     *
     * @return void
     */
    function formActions()
    {
        // TRANS: Button text for saving the administrative Twitter bridge settings.
        $this->out->submit('submit', _m('BUTTON','Save'), 'submit', null,
        // TRANS: Button title for saving the administrative Twitter bridge settings.
        _m('Save the Twitter bridge settings.'));
    }
}

repo_name: piratas/tortuga

license: mit


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') && !defined('STATUSNET')) { exit(1); }

require_once dirname(__DIR__) . '/twitter.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 Action
{
    var $twuid        = null;
    var $tw_fields    = null;
    var $access_token = null;
    var $signin       = null;
    var $verifier     = null;

    /**
     * Initialize class members. Looks for 'oauth_token' parameter.
     *
     * @param array $args misc. arguments
     *
     * @return boolean true
     */
    function prepare($args)
    {
        parent::prepare($args);

        $this->signin      = $this->boolean('signin');
        $this->oauth_token = $this->arg('oauth_token');
        $this->verifier    = $this->arg('oauth_verifier');

        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()) {
            $user  = common_current_user();
            $flink = Foreign_link::getByUserID($user->id, TWITTER_SERVICE);

            // If there's already a foreign link record and a foreign user
            // it means the accounts are already linked, and this is unecessary.
            // So go back.

            if (isset($flink)) {
                $fuser = $flink->getForeignUser();
                if (!empty($fuser)) {
                    common_redirect(common_local_url('twittersettings'));
                }
            }
        }

        if ($_SERVER['REQUEST_METHOD'] == 'POST') {

            // 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'));

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

            if (!$token || $token != common_session_token()) {
                // TRANS: Client error displayed when the session token does not match or is not given.
                $this->showForm(_m('There was a problem with your session token. Try again, please.'));
                return;
            }

            if ($this->arg('create')) {
                if (!$this->boolean('license')) {
                    // TRANS: Form validation error displayed when the checkbox to agree to the license has not been checked.
                    $this->showForm(_m('You cannot register if you do not agree to the license.'),
                                    $this->trimmed('newname'));
                    return;
                }
                $this->createNewUser();
            } else if ($this->arg('connect')) {
                $this->connectNewUser();
            } else {
                common_debug('Twitter bridge - ' . print_r($this->args, true));
                // TRANS: Form validation error displayed when an unhandled error occurs.
                $this->showForm(_m('Something weird happened.'),
                                $this->trimmed('newname'));
            }
        } else {
            // $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();

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

        if (common_logged_in()) {
            // Save the access token and Twitter user info

            $user = common_current_user();
            $this->saveForeignLink($user->id, $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;
            $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);

        $flink_id = $flink->insert();

        if (empty($flink_id)) {
            common_log_db_error($flink, 'INSERT', __FILE__);
            // TRANS: Server error displayed when linking to a Twitter account fails.
            $this->serverError(_m('Could not link your Twitter account.'));
        }

        return $flink_id;
    }

    function showPageNotice()
    {
        if ($this->error) {
            $this->element('div', array('class' => 'error'), $this->error);
        } else {
            $this->element('div', 'instructions',
                           // TRANS: Page instruction. %s is the StatusNet sitename.
                           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');
    }

    function showForm($error=null, $username=null)
    {
        $this->error = $error;
        $this->username = $username;

        $this->showPage();
    }

    function showPage()
    {
        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()
    {
        if (!empty($this->message_text)) {
            $this->element('p', null, $this->message);
            return;
        }

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

        // Don't allow new account creation if site is flagged as invite only
	if (common_config('site', 'inviteonly') == false) {
            $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) ? $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 '';
    }

    function message($msg)
    {
        $this->message_text = $msg;
        $this->showPage();
    }

    function createNewUser()
    {
        if (!Event::handle('StartRegistrationTry', array($this))) {
            return;
        }

        if (common_config('site', 'closed')) {
            // TRANS: Client error displayed when trying to create a new user while creating new users is not allowed.
            $this->clientError(_m('Registration not allowed.'));
        }

        $invite = null;

        if (common_config('site', '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.
                $this->clientError(_m('Registration not allowed.'));
            }

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

            if (empty($invite)) {
                // TRANS: Client error displayed when trying to create a new user with an invalid invitation code.
                $this->clientError(_m('Not a valid invitation code.'));
            }
        }

        try {
            $nickname = Nickname::normalize($this->trimmed('newname'), true);
        } catch (NicknameException $e) {
            $this->showForm($e->getMessage());
            return;
        }

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

        try {
            $user = User::register($args);
        } catch (Exception $e) {
            $this->serverError($e->getMessage());
        }

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

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

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

        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.
            $this->showForm(_m('Invalid username or password.'));
            return;
        }

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

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

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

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

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

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

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

        $flink = Foreign_link::getByForeignID($this->twuid,
                                              TWITTER_SERVICE);

        if (!empty($flink)) {
            $user = $flink->getUser();

            if (!empty($user)) {

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

        } else {

            common_debug('TwitterBridge Plugin - ' .
                         "No flink found for twuid: $this->twuid - new user");

            $this->showForm(null, $this->bestNewNickname());
        }
    }

    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('STATUSNET') && !defined('LACONICA')) {
    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 Action
{
    function handle($args)
    {
        parent::handle($args);

        if (common_is_real_login()) {
            // TRANS: Client error displayed when trying to log in using Twitter while already logged in to StatusNet.
            $this->clientError(_m('Already logged in.'));
        }

        $this->showPage();
    }

    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 showPageNotice()
    {
        $instr = $this->getInstructions();
        $output = common_markup_to_html($instr);
        $this->elementStart('div', 'instructions');
        $this->raw($output);
        $this->elementEnd('div');
    }

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


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('STATUSNET') && !defined('LACONICA')) {
    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
{
    /**
     * 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()
    {

        $user = common_current_user();

        $profile = $user->getProfile();

        $fuser = null;

        $flink = Foreign_link::getByUserID($user->id, TWITTER_SERVICE);

        if (!empty($flink)) {
            $fuser = $flink->getForeignUser();
        }

        $this->elementStart('form', array('method' => 'post',
                                          'id' => 'form_settings_twitter',
                                          'class' => 'form_settings',
                                          'action' =>
                                          common_local_url('twittersettings')));

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

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

        if (empty($fuser)) {
            $this->elementStart('ul', 'form_data');
            $this->elementStart('li', array('id' => 'settings_twitter_login_button'));
            $this->element('a', array('href' => common_local_url('twitterauthorization')),
                           // TRANS: Link description to connect to a Twitter account.
                           'Connect my Twitter account');
            $this->elementEnd('li');
            $this->elementEnd('ul');

            $this->elementEnd('fieldset');
        } else {
            // TRANS: Fieldset legend.
            $this->element('legend', null, _m('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',
                           // 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 (!$user->password) {
                $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.'),
                            ($flink) ?
                            ($flink->noticesync & FOREIGN_NOTICE_SEND) :
                            true);
            $this->elementEnd('li');
            $this->elementStart('li');
            $this->checkbox('replysync',
                            // TRANS: Checkbox label.
                            _m('Send local "@" replies to Twitter.'),
                            ($flink) ?
                            ($flink->noticesync & FOREIGN_NOTICE_SEND_REPLY) :
                            true);
            $this->elementEnd('li');
            $this->elementStart('li');
            $this->checkbox('friendsync',
                            // TRANS: Checkbox label.
                            _m('Subscribe to my Twitter friends here.'),
                            ($flink) ?
                            ($flink->friendsync & FOREIGN_FRIEND_RECV) :
                            false);
            $this->elementEnd('li');

            if (common_config('twitterimport','enabled')) {
                $this->elementStart('li');
                $this->checkbox('noticerecv',
                                // TRANS: Checkbox label.
                                _m('Import my friends timeline.'),
                                ($flink) ?
                                ($flink->noticesync & FOREIGN_NOTICE_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) {
                // 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'));
            }

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

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

    /**
     * Handle posts to this form
     *
     * Based on the button that was pressed, muxes out to other functions
     * to do the actual task requested.
     *
     * All sub-functions reload the form with a message -- success or failure.
     *
     * @return void
     */
    function handlePost()
    {
        // CSRF protection
        $token = $this->trimmed('token');
        if (!$token || $token != common_session_token()) {
            // TRANS: Client error displayed when the session token does not match or is not given.
            $this->showForm(_m('There was a problem with your session token. '.
                               'Try again, please.'));
            return;
        }

        if ($this->arg('save')) {
            $this->savePreferences();
        } else if ($this->arg('disconnect')) {
            $this->removeTwitterAccount();
        } else {
            // TRANS: Client error displayed when the submitted form contains unexpected data.
            $this->showForm(_m('Unexpected form submission.'));
        }
    }

    /**
     * Disassociate an existing Twitter account from this account
     *
     * @return void
     */
    function removeTwitterAccount()
    {
        $user = common_current_user();
        $flink = Foreign_link::getByUserID($user->id, TWITTER_SERVICE);

        if (empty($flink)) {
            // TRANS: Client error displayed when trying to remove a connected Twitter account when there isn't one connected.
            $this->clientError(_m('No Twitter connection to remove.'));
        }

        $result = $flink->safeDelete();

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

        // TRANS: Success message displayed after disconnecting a Twitter account.
        $this->showForm(_m('Twitter account disconnected.'), true);
    }

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

        $user = common_current_user();
        $flink = Foreign_link::getByUserID($user->id, TWITTER_SERVICE);

        if (empty($flink)) {
            common_log_db_error($flink, 'SELECT', __FILE__);
            // @todo FIXME: Shouldn't this be a serverError()?
            // TRANS: Server error displayed when saving Twitter integration preferences fails.
            $this->showForm(_m('Could not save Twitter preferences.'));
            return;
        }

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

        if ($result === false) {
            common_log_db_error($flink, 'UPDATE', __FILE__);
            // @todo FIXME: Shouldn't this be a serverError()?
            // TRANS: Server error displayed when saving Twitter integration preferences fails.
            $this->showForm(_m('Could not save Twitter preferences.'));
            return;
        }

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

        // TRANS: Success message after saving Twitter integration preferences.
        $this->showForm(_m('Twitter preferences saved.'), true);
    }

    /**
     * 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;
    }

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

        $profile = $flink->getProfile();

        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_profile = Profile::getKV('id', $friend_flink->user_id);

                if ($friend_profile instanceof Profile) {
                    try {
                        $other = Profile::getKV('id', $invites->user_id);
                        Subscription::start($profile, $friend_profile);
                        common_log(LOG_INFO,
                                   $this->name() . ' - Subscribed ' .
                                   "{$friend_profile->nickname} to {$profile->nickname}.");
                    } 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();