]> git.mxchange.org Git - quix0rs-gnu-social.git/commitdiff
Merge branch '0.9.x' into pluginize-twitter-bridge
authorZach Copley <zach@controlyourself.ca>
Tue, 8 Sep 2009 23:02:57 +0000 (16:02 -0700)
committerZach Copley <zach@controlyourself.ca>
Tue, 8 Sep 2009 23:02:57 +0000 (16:02 -0700)
Conflicts:
plugins/TwitterBridge/twitterauthorization.php

1  2 
lib/common.php
lib/connectsettingsaction.php
lib/router.php
plugins/TwitterBridge/daemons/synctwitterfriends.php
plugins/TwitterBridge/daemons/twitterqueuehandler.php
plugins/TwitterBridge/daemons/twitterstatusfetcher.php
plugins/TwitterBridge/twitter.php
plugins/TwitterBridge/twitterauthorization.php
plugins/TwitterBridge/twitteroauthclient.php
plugins/TwitterBridge/twittersettings.php

diff --cc lib/common.php
Simple merge
Simple merge
diff --cc lib/router.php
Simple merge
index b7be5d043af7ab38b2067a770c8710bec336e0b8,0000000000000000000000000000000000000000..0668c6222f386affa86d7da722aad0aba0af0458
mode 100755,000000..100755
--- /dev/null
@@@ -1,280 -1,0 +1,287 @@@
-  * Laconica - a distributed open-source microblogging tool
-  * Copyright (C) 2008, 2009, Control Yourself, Inc.
 +#!/usr/bin/env php
 +<?php
 +/*
-  * @package  Laconica
-  * @author   Zach Copley <zach@controlyourself.ca>
-  * @author   Evan Prodromou <evan@controlyourself.ca>
++ * 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 INSTALLDIR . '/lib/parallelizingdaemon.php';
 +require_once INSTALLDIR . '/plugins/TwitterBridge/twitter.php';
 +
 +/**
 + * Daemon to sync local friends with Twitter friends
 + *
 + * @category Twitter
-  * @link     http://laconi.ca/
++ * @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
-         $token = TwitterOAuthClient::unpackToken($flink->credentials);
++ * @link     http://status.net/
 + */
 +
 +$helptext = <<<END_OF_TWITTER_HELP
 +Batch script for synching local friends with Twitter friends.
 +
 +END_OF_TWITTER_HELP;
 +
 +require_once INSTALLDIR . '/scripts/commandline.inc';
 +require_once INSTALLDIR . '/lib/parallelizingdaemon.php';
 +
 +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 = new TwitterOAuthClient($token->key, $token->secret);
++        $client = null;
 +
-         } catch (OAuthCurlException $e) {
++        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 {
++            $client = new TwitterBasicAuthClient($flink);
++            common_debug($this->name() . '- Grabbing friends IDs with basic auth.');
++        }
 +
 +        try {
 +            $friends_ids = $client->friendsIds();
-         } catch (OAuthCurlException $e) {
++        } catch (Exception $e) {
 +            common_log(LOG_WARNING, $this->name() .
 +                       ' - cURL error getting friend ids ' .
 +                       $e->getCode() . ' - ' . $e->getMessage());
 +            return $friends;
 +        }
 +
 +        if (empty($friends_ids)) {
 +            common_debug($this->name() .
 +                         " - Twitter user $flink->foreign_id " .
 +                         'doesn\'t have any friends!');
 +            return $friends;
 +        }
 +
 +        common_debug($this->name() . ' - Twitter\'s API says Twitter user id ' .
 +                     "$flink->foreign_id has " .
 +                     count($friends_ids) . ' friends.');
 +
 +        // Calculate how many pages to get...
 +        $pages = ceil(count($friends_ids) / 100);
 +
 +        if ($pages == 0) {
 +            common_debug($this->name() . " - $user seems to have no friends.");
 +        }
 +
 +        for ($i = 1; $i <= $pages; $i++) {
 +
 +        try {
 +            $more_friends = $client->statusesFriends(null, null, null, $i);
++        } catch (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 {
 +                $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;
 +        }
 +
 +        $user = $flink->getUser();
 +
 +        foreach ($friends as $friend) {
 +
 +            $friend_name = $friend->screen_name;
 +            $friend_id = (int) $friend->id;
 +
 +            // Update or create the Foreign_user record for each
 +            // Twitter friend
 +
 +            if (!save_twitter_user($friend_id, $friend_name)) {
 +                common_log(LOG_WARNING, $this-name() .
 +                           " - Couldn't save $screen_name's friend, $friend_name.");
 +                continue;
 +            }
 +
 +            // Check to see if there's a related local user
 +
 +            $friend_flink = Foreign_link::getByForeignID($friend_id,
 +                                                         TWITTER_SERVICE);
 +
 +            if (!empty($friend_flink)) {
 +
 +                // Get associated user and subscribe her
 +
 +                $friend_user = User::staticGet('id', $friend_flink->user_id);
 +
 +                if (!empty($friend_user)) {
 +                    $result = subs_subscribe_to($user, $friend_user);
 +
 +                    if ($result === true) {
 +                        common_log(LOG_INFO,
 +                                   $this->name() . ' - Subscribed ' .
 +                                   "$friend_user->nickname to $user->nickname.");
 +                    } else {
 +                        common_debug($this->name() .
 +                                     ' - Tried subscribing ' .
 +                                     "$friend_user->nickname to $user->nickname - " .
 +                                     $result);
 +                    }
 +                }
 +            }
 +        }
 +
 +        return true;
 +    }
 +
 +}
 +
 +$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();
 +
index 9aa9d1ed0a0df743741849a0871b2c2ec47fdc76,0000000000000000000000000000000000000000..f0e76bb74574955b7ecab65f0599cb6d26ddf49c
mode 100755,000000..100755
--- /dev/null
@@@ -1,73 -1,0 +1,73 @@@
-  * Laconica - a distributed open-source microblogging tool
-  * Copyright (C) 2008, 2009, Control Yourself, Inc.
 +#!/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 = 'i::';
 +$longoptions = array('id::');
 +
 +$helptext = <<<END_OF_ENJIT_HELP
 +Daemon script for pushing new notices to Twitter.
 +
 +    -i --id           Identity (default none)
 +
 +END_OF_ENJIT_HELP;
 +
 +require_once INSTALLDIR . '/scripts/commandline.inc';
 +require_once INSTALLDIR . '/lib/queuehandler.php';
 +require_once INSTALLDIR . '/plugins/TwitterBridge/twitter.php';
 +
 +class TwitterQueueHandler extends QueueHandler
 +{
 +    function transport()
 +    {
 +        return 'twitter';
 +    }
 +
 +    function start()
 +    {
 +        $this->log(LOG_INFO, "INITIALIZE");
 +        return true;
 +    }
 +
 +    function handle_notice($notice)
 +    {
 +        return broadcast_twitter($notice);
 +    }
 +
 +    function finish()
 +    {
 +    }
 +
 +}
 +
 +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;
 +}
 +
 +$handler = new TwitterQueueHandler($id);
 +
 +$handler->runOnce();
index 3023bf423ba4984ff82e2351e9974f5f8482c5fe,0000000000000000000000000000000000000000..4752ada7c8c09a7139253d62c50ed7fe87c8bce4
mode 100755,000000..100755
--- /dev/null
@@@ -1,559 -1,0 +1,566 @@@
-  * Laconica - a distributed open-source microblogging tool
-  * Copyright (C) 2008, 2009, Control Yourself, Inc.
 +#!/usr/bin/env php
 +<?php
 +/**
-  * @package  Laconica
-  * @author   Zach Copley <zach@controlyourself.ca>
-  * @author   Evan Prodromou <evan@controlyourself.ca>
++ * 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__) . '/../../..'));
 +
 +// Tune number of processes and how often to poll Twitter
 +// XXX: Should these things be in config.php?
 +define('MAXCHILDREN', 2);
 +define('POLL_INTERVAL', 60); // in seconds
 +
 +$shortoptions = 'di::';
 +$longoptions = array('id::', 'debug');
 +
 +$helptext = <<<END_OF_TRIM_HELP
 +Batch script for retrieving Twitter messages from foreign service.
 +
 +  -i --id              Identity (default 'generic')
 +  -d --debug           Debug (lots of log output)
 +
 +END_OF_TRIM_HELP;
 +
 +require_once INSTALLDIR . '/scripts/commandline.inc';
 +require_once INSTALLDIR . '/lib/daemon.php';
 +require_once INSTALLDIR . '/plugins/TwitterBridge/twitter.php';
 +
 +/**
 + * Fetcher for statuses from Twitter
 + *
 + * Fetches statuses from Twitter and inserts them as notices in local
 + * system.
 + *
 + * @category Twitter
-  * @link     http://laconi.ca/
++ * @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
- // script to work: e.g.: $config['avatar']['path'] = '/laconica/avatar';
++ * @link     http://status.net/
 + */
 +
 +// NOTE: an Avatar path MUST be set in config.php for this
-          if (empty($flink)) {
++// script to work: e.g.: $config['avatar']['path'] = '/statusnet/avatar';
 +
 +class TwitterStatusFetcher extends ParallelizingDaemon
 +{
 +    /**
 +     *  Constructor
 +     *
 +     * @param string  $id           the name/id of this daemon
 +     * @param int     $interval     sleep this long before doing everything again
 +     * @param int     $max_children maximum number of child processes at a time
 +     * @param boolean $debug        debug output flag
 +     *
 +     * @return void
 +     *
 +     **/
 +    function __construct($id = null, $interval = 60,
 +                         $max_children = 2, $debug = null)
 +    {
 +        parent::__construct($id, $interval, $max_children, $debug);
 +    }
 +
 +    /**
 +     * Name of this daemon
 +     *
 +     * @return string Name of the daemon.
 +     */
 +
 +    function name()
 +    {
 +        return ('twitterstatusfetcher.'.$this->_id);
 +    }
 +
 +    /**
 +     * Find all the Twitter foreign links for users who have requested
 +     * importing of their friends' timelines
 +     *
 +     * @return array flinks an array of Foreign_link objects
 +     */
 +
 +    function getObjects()
 +    {
 +        global $_DB_DATAOBJECT;
 +
 +        $flink = new Foreign_link();
 +        $conn = &$flink->getDatabaseConnection();
 +
 +        $flink->service = TWITTER_SERVICE;
 +        $flink->orderBy('last_noticesync');
 +        $flink->find();
 +
 +        $flinks = array();
 +
 +        while ($flink->fetch()) {
 +
 +            if (($flink->noticesync & FOREIGN_NOTICE_RECV) ==
 +                FOREIGN_NOTICE_RECV) {
 +                $flinks[] = clone($flink);
 +            }
 +        }
 +
 +        $flink->free();
 +        unset($flink);
 +
 +        $conn->disconnect();
 +        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->getTimeline($flink);
 +
 +        $flink->last_friendsync = common_sql_now();
 +        $flink->update();
 +
 +        $conn->disconnect();
 +
 +        // XXX: Couldn't find a less brutal way to blow
 +        // away a cached connection
 +
 +        global $_DB_DATAOBJECT;
 +        unset($_DB_DATAOBJECT['CONNECTIONS']);
 +    }
 +
 +    function getTimeline($flink)
 +    {
-                 " - Can't retrieve Foreign_link for foreign ID $fid");
++        if (empty($flink)) {
 +            common_log(LOG_WARNING, $this->name() .
-         $token = TwitterOAuthClient::unpackToken($flink->credentials);
++                       " - Can't retrieve Foreign_link for foreign ID $fid");
 +            return;
 +        }
 +
 +        common_debug($this->name() . ' - Trying to get timeline for Twitter user ' .
 +                     $flink->foreign_id);
 +
 +        // XXX: Biggest remaining issue - How do we know at which status
 +        // to start importing?  How many statuses?  Right now I'm going
 +        // with the default last 20.
 +
-         $client = new TwitterOAuthClient($token->key, $token->secret);
++        $client = null;
 +
-         } catch (OAuthClientCurlException $e) {
++        if (TwitterOAuthClient::isPackedToken($flink->credentials)) {
++            $token = TwitterOAuthClient::unpackToken($flink->credentials);
++            $client = new TwitterOAuthClient($token->key, $token->secret);
++            common_debug($this->name() . ' - Grabbing friends timeline with OAuth.');
++        } else {
++            $client = new TwitterBasicAuthClient($flink);
++            common_debug($this->name() . ' - Grabbing friends timeline with basic auth.');
++        }
 +
 +        $timeline = null;
 +
 +        try {
 +            $timeline = $client->statusesFriendsTimeline();
-                        ' - OAuth client unable to get friends timeline for user ' .
++        } catch (Exception $e) {
 +            common_log(LOG_WARNING, $this->name() .
-             // Hacktastic: filter out stuff coming from this Laconica
++                       ' - Twitter client unable to get friends timeline for user ' .
 +                       $flink->user_id . ' - code: ' .
 +                       $e->getCode() . 'msg: ' . $e->getMessage());
 +        }
 +
 +        if (empty($timeline)) {
 +            common_log(LOG_WARNING, $this->name() .  " - Empty timeline.");
 +            return;
 +        }
 +
 +        // Reverse to preserve order
 +
 +        foreach (array_reverse($timeline) as $status) {
 +
-             // Laconica's (Laconica's = 96)
++            // Hacktastic: filter out stuff coming from this StatusNet
 +
 +            $source = mb_strtolower(common_config('integration', 'source'));
 +
 +            if (preg_match("/$source/", mb_strtolower($status->source))) {
 +                common_debug($this->name() . ' - Skipping import of status ' .
 +                             $status->id . ' with source ' . $source);
 +                continue;
 +            }
 +
 +            $this->saveStatus($status, $flink);
 +        }
 +
 +        // Okay, record the time we synced with Twitter for posterity
 +
 +        $flink->last_noticesync = common_sql_now();
 +        $flink->update();
 +    }
 +
 +    function saveStatus($status, $flink)
 +    {
 +        $id = $this->ensureProfile($status->user);
 +
 +        $profile = Profile::staticGet($id);
 +
 +        if (empty($profile)) {
 +            common_log(LOG_ERR, $this->name() .
 +                ' - Problem saving notice. No associated Profile.');
 +            return null;
 +        }
 +
 +        // XXX: change of screen name?
 +
 +        $uri = 'http://twitter.com/' . $status->user->screen_name .
 +            '/status/' . $status->id;
 +
 +        $notice = Notice::staticGet('uri', $uri);
 +
 +        // check to see if we've already imported the status
 +
 +        if (empty($notice)) {
 +
 +            $notice = new Notice();
 +
 +            $notice->profile_id = $id;
 +            $notice->uri        = $uri;
 +            $notice->created    = strftime('%Y-%m-%d %H:%M:%S',
 +                                           strtotime($status->created_at));
 +            $notice->content    = common_shorten_links($status->text); // XXX
 +            $notice->rendered   = common_render_content($notice->content, $notice);
 +            $notice->source     = 'twitter';
 +            $notice->reply_to   = null; // XXX: lookup reply
 +            $notice->is_local   = Notice::GATEWAY;
 +
 +            if (Event::handle('StartNoticeSave', array(&$notice))) {
 +                $id = $notice->insert();
 +                Event::handle('EndNoticeSave', array($notice));
 +            }
 +        }
 +
 +        if (!Notice_inbox::pkeyGet(array('notice_id' => $notice->id,
 +                                         'user_id' => $flink->user_id))) {
 +            // Add to inbox
 +            $inbox = new Notice_inbox();
 +
 +            $inbox->user_id   = $flink->user_id;
 +            $inbox->notice_id = $notice->id;
 +            $inbox->created   = $notice->created;
 +            $inbox->source    = NOTICE_INBOX_SOURCE_GATEWAY; // From a private source
 +
 +            $inbox->insert();
 +        }
 +    }
 +
 +    function ensureProfile($user)
 +    {
 +        // check to see if there's already a profile for this user
 +
 +        $profileurl = 'http://twitter.com/' . $user->screen_name;
 +        $profile = Profile::staticGet('profileurl', $profileurl);
 +
 +        if (!empty($profile)) {
 +            common_debug($this->name() .
 +                         " - Profile for $profile->nickname found.");
 +
 +            // Check to see if the user's Avatar has changed
 +
 +            $this->checkAvatar($user, $profile);
 +            return $profile->id;
 +
 +        } else {
 +            common_debug($this->name() . ' - Adding profile and remote profile ' .
 +                         "for Twitter user: $profileurl.");
 +
 +            $profile = new Profile();
 +            $profile->query("BEGIN");
 +
 +            $profile->nickname = $user->screen_name;
 +            $profile->fullname = $user->name;
 +            $profile->homepage = $user->url;
 +            $profile->bio = $user->description;
 +            $profile->location = $user->location;
 +            $profile->profileurl = $profileurl;
 +            $profile->created = common_sql_now();
 +
 +            $id = $profile->insert();
 +
 +            if (empty($id)) {
 +                common_log_db_error($profile, 'INSERT', __FILE__);
 +                $profile->query("ROLLBACK");
 +                return false;
 +            }
 +
 +            // check for remote profile
 +
 +            $remote_pro = Remote_profile::staticGet('uri', $profileurl);
 +
 +            if (empty($remote_pro)) {
 +
 +                $remote_pro = new Remote_profile();
 +
 +                $remote_pro->id = $id;
 +                $remote_pro->uri = $profileurl;
 +                $remote_pro->created = common_sql_now();
 +
 +                $rid = $remote_pro->insert();
 +
 +                if (empty($rid)) {
 +                    common_log_db_error($profile, 'INSERT', __FILE__);
 +                    $profile->query("ROLLBACK");
 +                    return false;
 +                }
 +            }
 +
 +            $profile->query("COMMIT");
 +
 +            $this->saveAvatars($user, $id);
 +
 +            return $id;
 +        }
 +    }
 +
 +    function checkAvatar($twitter_user, $profile)
 +    {
 +        global $config;
 +
 +        $path_parts = pathinfo($twitter_user->profile_image_url);
 +
 +        $newname = 'Twitter_' . $twitter_user->id . '_' .
 +            $path_parts['basename'];
 +
 +        $oldname = $profile->getAvatar(48)->filename;
 +
 +        if ($newname != $oldname) {
 +            common_debug($this->name() . ' - Avatar for Twitter user ' .
 +                         "$profile->nickname has changed.");
 +            common_debug($this->name() . " - old: $oldname new: $newname");
 +
 +            $this->updateAvatars($twitter_user, $profile);
 +        }
 +
 +        if ($this->missingAvatarFile($profile)) {
 +            common_debug($this->name() . ' - Twitter user ' .
 +                         $profile->nickname .
 +                         ' is missing one or more local avatars.');
 +            common_debug($this->name() ." - old: $oldname new: $newname");
 +
 +            $this->updateAvatars($twitter_user, $profile);
 +        }
 +
 +    }
 +
 +    function updateAvatars($twitter_user, $profile) {
 +
 +        global $config;
 +
 +        $path_parts = pathinfo($twitter_user->profile_image_url);
 +
 +        $img_root = substr($path_parts['basename'], 0, -11);
 +        $ext = $path_parts['extension'];
 +        $mediatype = $this->getMediatype($ext);
 +
 +        foreach (array('mini', 'normal', 'bigger') as $size) {
 +            $url = $path_parts['dirname'] . '/' .
 +                $img_root . '_' . $size . ".$ext";
 +            $filename = 'Twitter_' . $twitter_user->id . '_' .
 +                $img_root . "_$size.$ext";
 +
 +            $this->updateAvatar($profile->id, $size, $mediatype, $filename);
 +            $this->fetchAvatar($url, $filename);
 +        }
 +    }
 +
 +    function missingAvatarFile($profile) {
 +
 +        foreach (array(24, 48, 73) as $size) {
 +
 +            $filename = $profile->getAvatar($size)->filename;
 +            $avatarpath = Avatar::path($filename);
 +
 +            if (file_exists($avatarpath) == FALSE) {
 +                return true;
 +            }
 +        }
 +
 +        return false;
 +    }
 +
 +    function getMediatype($ext)
 +    {
 +        $mediatype = null;
 +
 +        switch (strtolower($ext)) {
 +        case 'jpg':
 +            $mediatype = 'image/jpg';
 +            break;
 +        case 'gif':
 +            $mediatype = 'image/gif';
 +            break;
 +        default:
 +            $mediatype = 'image/png';
 +        }
 +
 +        return $mediatype;
 +    }
 +
 +    function saveAvatars($user, $id)
 +    {
 +        global $config;
 +
 +        $path_parts = pathinfo($user->profile_image_url);
 +        $ext = $path_parts['extension'];
 +        $end = strlen('_normal' . $ext);
 +        $img_root = substr($path_parts['basename'], 0, -($end+1));
 +        $mediatype = $this->getMediatype($ext);
 +
 +        foreach (array('mini', 'normal', 'bigger') as $size) {
 +            $url = $path_parts['dirname'] . '/' .
 +                $img_root . '_' . $size . ".$ext";
 +            $filename = 'Twitter_' . $user->id . '_' .
 +                $img_root . "_$size.$ext";
 +
 +            if ($this->fetchAvatar($url, $filename)) {
 +                $this->newAvatar($id, $size, $mediatype, $filename);
 +            } else {
 +                common_log(LOG_WARNING, $this->id() .
 +                           " - Problem fetching Avatar: $url");
 +            }
 +        }
 +    }
 +
 +    function updateAvatar($profile_id, $size, $mediatype, $filename) {
 +
 +        common_debug($this->name() . " - Updating avatar: $size");
 +
 +        $profile = Profile::staticGet($profile_id);
 +
 +        if (empty($profile)) {
 +            common_debug($this->name() . " - Couldn't get profile: $profile_id!");
 +            return;
 +        }
 +
 +        $sizes = array('mini' => 24, 'normal' => 48, 'bigger' => 73);
 +        $avatar = $profile->getAvatar($sizes[$size]);
 +
 +        // Delete the avatar, if present
 +
 +        if ($avatar) {
 +            $avatar->delete();
 +        }
 +
 +        $this->newAvatar($profile->id, $size, $mediatype, $filename);
 +    }
 +
 +    function newAvatar($profile_id, $size, $mediatype, $filename)
 +    {
 +        global $config;
 +
 +        $avatar = new Avatar();
 +        $avatar->profile_id = $profile_id;
 +
 +        switch($size) {
 +        case 'mini':
 +            $avatar->width  = 24;
 +            $avatar->height = 24;
 +            break;
 +        case 'normal':
 +            $avatar->width  = 48;
 +            $avatar->height = 48;
 +            break;
 +        default:
 +
 +            // Note: Twitter's big avatars are a different size than
++            // StatusNet's (StatusNet's = 96)
 +
 +            $avatar->width  = 73;
 +            $avatar->height = 73;
 +        }
 +
 +        $avatar->original = 0; // we don't have the original
 +        $avatar->mediatype = $mediatype;
 +        $avatar->filename = $filename;
 +        $avatar->url = Avatar::url($filename);
 +
 +        common_debug($this->name() . " - New filename: $avatar->url");
 +
 +        $avatar->created = common_sql_now();
 +
 +        $id = $avatar->insert();
 +
 +        if (empty($id)) {
 +            common_log_db_error($avatar, 'INSERT', __FILE__);
 +            return null;
 +        }
 +
 +        common_debug($this->name() .
 +                     " - Saved new $size avatar for $profile_id.");
 +
 +        return $id;
 +    }
 +
 +    function fetchAvatar($url, $filename)
 +    {
 +        $avatar_dir = INSTALLDIR . '/avatar/';
 +
 +        $avatarfile = $avatar_dir . $filename;
 +
 +        $out = fopen($avatarfile, 'wb');
 +        if (!$out) {
 +            common_log(LOG_WARNING, $this->name() .
 +                       " - Couldn't open file $filename");
 +            return false;
 +        }
 +
 +        common_debug($this->name() . " - Fetching Twitter avatar: $url");
 +
 +        $ch = curl_init();
 +        curl_setopt($ch, CURLOPT_URL, $url);
 +        curl_setopt($ch, CURLOPT_FILE, $out);
 +        curl_setopt($ch, CURLOPT_BINARYTRANSFER, true);
 +        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
 +        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 0);
 +        $result = curl_exec($ch);
 +        curl_close($ch);
 +
 +        fclose($out);
 +
 +        return $result;
 +    }
 +}
 +
 +$id    = null;
 +$debug = null;
 +
 +if (have_option('i')) {
 +    $id = get_option_value('i');
 +} else if (have_option('--id')) {
 +    $id = get_option_value('--id');
 +} else if (count($args) > 0) {
 +    $id = $args[0];
 +} else {
 +    $id = null;
 +}
 +
 +if (have_option('d') || have_option('debug')) {
 +    $debug = true;
 +}
 +
 +$fetcher = new TwitterStatusFetcher($id, 60, 2, $debug);
 +$fetcher->runOnce();
 +
index 280cdb0a33d93d6fcfcdef2e8669f9cee55e383d,0000000000000000000000000000000000000000..b49e2e11902997bdfb1681a48729361397f956f1
mode 100644,000000..100644
--- /dev/null
@@@ -1,258 -1,0 +1,311 @@@
-  * Laconica - a distributed open-source microblogging tool
-  * Copyright (C) 2008, 2009, Control Yourself, Inc.
 +<?php
 +/*
- if (!defined('LACONICA')) {
++ * 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/>.
 + */
 +
-         $user = $flink->getUser();
-         // XXX: Hack to get around PHP cURL's use of @ being a a meta character
-         $statustxt = preg_replace('/^@/', ' @', $notice->content);
++if (!defined('STATUSNET') && !defined('LACONICA')) {
 +    exit(1);
 +}
 +
 +define('TWITTER_SERVICE', 1); // Twitter is foreign_service ID 1
 +
 +function update_twitter_user($twitter_id, $screen_name)
 +{
 +    $uri = 'http://twitter.com/' . $screen_name;
 +    $fuser = new Foreign_user();
 +
 +    $fuser->query('BEGIN');
 +
 +    // Dropping down to SQL because regular DB_DataObject udpate stuff doesn't seem
 +    // to work so good with tables that have multiple column primary keys
 +
 +    // Any time we update the uri for a forein user we have to make sure there
 +    // are no dupe entries first -- unique constraint on the uri column
 +
 +    $qry = 'UPDATE foreign_user set uri = \'\' WHERE uri = ';
 +    $qry .= '\'' . $uri . '\'' . ' AND service = ' . TWITTER_SERVICE;
 +
 +    $fuser->query($qry);
 +
 +    // Update the user
 +
 +    $qry = 'UPDATE foreign_user SET nickname = ';
 +    $qry .= '\'' . $screen_name . '\'' . ', uri = \'' . $uri . '\' ';
 +    $qry .= 'WHERE id = ' . $twitter_id . ' AND service = ' . TWITTER_SERVICE;
 +
 +    $fuser->query('COMMIT');
 +
 +    $fuser->free();
 +    unset($fuser);
 +
 +    return true;
 +}
 +
 +function add_twitter_user($twitter_id, $screen_name)
 +{
 +
 +    $new_uri = 'http://twitter.com/' . $screen_name;
 +
 +    // Clear out any bad old foreign_users with the new user's legit URL
 +    // This can happen when users move around or fakester accounts get
 +    // repoed, and things like that.
 +
 +    $luser = new Foreign_user();
 +    $luser->uri = $new_uri;
 +    $luser->service = TWITTER_SERVICE;
 +    $result = $luser->delete();
 +
 +    if (empty($result)) {
 +        common_log(LOG_WARNING,
 +            "Twitter bridge - removed invalid Twitter user squatting on uri: $new_uri");
 +    }
 +
 +    $luser->free();
 +    unset($luser);
 +
 +    // Otherwise, create a new Twitter user
 +
 +    $fuser = new Foreign_user();
 +
 +    $fuser->nickname = $screen_name;
 +    $fuser->uri = 'http://twitter.com/' . $screen_name;
 +    $fuser->id = $twitter_id;
 +    $fuser->service = TWITTER_SERVICE;
 +    $fuser->created = common_sql_now();
 +    $result = $fuser->insert();
 +
 +    if (empty($result)) {
 +        common_log(LOG_WARNING,
 +            "Twitter bridge - failed to add new Twitter user: $twitter_id - $screen_name.");
 +        common_log_db_error($fuser, 'INSERT', __FILE__);
 +    } else {
 +        common_debug("Twitter bridge - Added new Twitter user: $screen_name ($twitter_id).");
 +    }
 +
 +    return $result;
 +}
 +
 +// Creates or Updates a Twitter user
 +function save_twitter_user($twitter_id, $screen_name)
 +{
 +
 +    // Check to see whether the Twitter user is already in the system,
 +    // and update its screen name and uri if so.
 +
 +    $fuser = Foreign_user::getForeignUser($twitter_id, TWITTER_SERVICE);
 +
 +    if (!empty($fuser)) {
 +
 +        $result = true;
 +
 +        // Only update if Twitter screen name has changed
 +
 +        if ($fuser->nickname != $screen_name) {
 +            $result = update_twitter_user($twitter_id, $screen_name);
 +
 +            common_debug('Twitter bridge - Updated nickname (and URI) for Twitter user ' .
 +                "$fuser->id to $screen_name, was $fuser->nickname");
 +        }
 +
 +        return $result;
 +
 +    } else {
 +        return add_twitter_user($twitter_id, $screen_name);
 +    }
 +
 +    $fuser->free();
 +    unset($fuser);
 +
 +    return true;
 +}
 +
 +function is_twitter_bound($notice, $flink) {
 +
 +    // Check to see if notice should go to Twitter
 +    if (!empty($flink) && ($flink->noticesync & FOREIGN_NOTICE_SEND)) {
 +
 +        // If it's not a Twitter-style reply, or if the user WANTS to send replies.
 +        if (!preg_match('/^@[a-zA-Z0-9_]{1,15}\b/u', $notice->content) ||
 +            ($flink->noticesync & FOREIGN_NOTICE_SEND_REPLY)) {
 +            return true;
 +        }
 +    }
 +
 +    return false;
 +}
 +
 +function broadcast_twitter($notice)
 +{
 +    $flink = Foreign_link::getByUserID($notice->profile_id,
 +                                       TWITTER_SERVICE);
 +
 +    if (is_twitter_bound($notice, $flink)) {
++        if (TwitterOAuthClient::isPackedToken($flink->credentials)) {
++            return broadcast_oauth($notice, $flink);
++        } else {
++            return broadcast_basicauth($notice, $flink);
++        }
++    }
 +
-         $token = TwitterOAuthClient::unpackToken($flink->credentials);
++    return true;
++}
 +
-         $client = new TwitterOAuthClient($token->key, $token->secret);
++function broadcast_oauth($notice, $flink) {
++    $user = $flink->getUser();
++    $statustxt = format_status($notice);
++    // Convert !groups to #hashes
++    $statustxt = preg_replace('/(^|\s)!([A-Za-z0-9]{1,64})/', "\\1#\\2", $statustxt);
++    $token = TwitterOAuthClient::unpackToken($flink->credentials);
++    $client = new TwitterOAuthClient($token->key, $token->secret);
++    $status = null;
++
++    try {
++        $status = $client->statusesUpdate($statustxt);
++    } catch (OAuthClientCurlException $e) {
++        return process_error($e, $flink);
++    }
 +
-         $status = null;
++    if (empty($status)) {
 +
-         try {
-             $status = $client->statusesUpdate($statustxt);
-         } catch (OAuthClientCurlException $e) {
++        // This could represent a failure posting,
++        // or the Twitter API might just be behaving flakey.
 +
-             if ($e->getMessage() == 'The requested URL returned error: 401') {
++        $errmsg = sprintf('Twitter bridge - No data returned by Twitter API when ' .
++                          'trying to send update for %1$s (user id %2$s).',
++                          $user->nickname, $user->id);
++        common_log(LOG_WARNING, $errmsg);
 +
-                 $errmsg = sprintf('User %1$s (user id: %2$s) has an invalid ' .
-                                   'Twitter OAuth access token.',
-                                   $user->nickname, $user->id);
-                 common_log(LOG_WARNING, $errmsg);
++        return false;
++    }
 +
-                 // Bad auth token! We need to delete the foreign_link
-                 // to Twitter and inform the user.
++    // Notice crossed the great divide
 +
-                 remove_twitter_link($flink);
-                 return true;
++    $msg = sprintf('Twitter bridge - posted notice %s to Twitter using OAuth.',
++                   $notice->id);
++    common_log(LOG_INFO, $msg);
 +
-             } else {
++    return true;
++}
 +
-                 // Some other error happened, so we should probably
-                 // try to send again later.
++function broadcast_basicauth($notice, $flink)
++{
++    $user = $flink->getUser();
 +
-                 $errmsg = sprintf('cURL error trying to send notice to Twitter ' .
-                                   'for user %1$s (user id: %2$s) - ' .
-                                   'code: %3$s message: $4$s.',
-                                   $user->nickname, $user->id,
-                                   $e->getCode(), $e->getMessage());
-                 common_log(LOG_WARNING, $errmsg);
++    $statustxt = format_status($notice);
 +
-                 return false;
-             }
-         }
++    $client = new TwitterBasicAuthClient($flink);
++    $status = null;
 +
-         if (empty($status)) {
++    try {
++        $status = $client->statusesUpdate($statustxt);
++    } catch (BasicAuthCurlException $e) {
++        return process_error($e, $flink);
++    }
 +
-             // This could represent a failure posting,
-             // or the Twitter API might just be behaving flakey.
++    if (empty($status)) {
 +
-             $errmsg = sprint('No data returned by Twitter API when ' .
++        $errmsg = sprintf('Twitter bridge - No data returned by Twitter API when ' .
++                          'trying to send update for %1$s (user id %2$s).',
++                          $user->nickname, $user->id);
++        common_log(LOG_WARNING, $errmsg);
 +
-             return false;
-         }
++            $errmsg = sprintf('No data returned by Twitter API when ' .
 +                             'trying to send update for %1$s (user id %2$s).',
 +                             $user->nickname, $user->id);
 +            common_log(LOG_WARNING, $errmsg);
++        return false;
++    }
 +
-         // Notice crossed the great divide
++    $msg = sprintf('Twitter bridge - posted notice %s to Twitter using basic auth.',
++                   $notice->id);
++    common_log(LOG_INFO, $msg);
 +
-         $msg = sprintf('Twitter bridge posted notice %s to Twitter.',
-                        $notice->id);
-         common_log(LOG_INFO, $msg);
++    return true;
++}
 +
-     return true;
++function process_error($e, $flink)
++{
++    $user        = $flink->getUser();
++    $errmsg      = $e->getMessage();
++    $delivered   = false;
++
++    switch($errmsg) {
++     case 'The requested URL returned error: 401':
++        $logmsg = sprintf('Twiter bridge - User %1$s (user id: %2$s) has an invalid ' .
++                          'Twitter screen_name/password combo or an invalid acesss token.',
++                          $user->nickname, $user->id);
++        $delivered = true;
++        remove_twitter_link($flink);
++        break;
++     case 'The requested URL returned error: 403':
++        $logmsg = sprintf('Twitter bridge - User %1$s (user id: %2$s) has exceeded ' .
++                          'his/her Twitter request limit.',
++                          $user->nickname, $user->id);
++        break;
++     default:
++        $logmsg = sprintf('Twitter bridge - cURL error trying to send notice to Twitter ' .
++                          'for user %1$s (user id: %2$s) - ' .
++                          'code: %3$s message: %4$s.',
++                          $user->nickname, $user->id,
++                          $e->getCode(), $e->getMessage());
++        break;
 +    }
 +
-         "user $user->nickname (user id: $user->id).");
++    common_log(LOG_WARNING, $logmsg);
++
++    return $delivered;
++}
++
++function format_status($notice)
++{
++    // XXX: Hack to get around PHP cURL's use of @ being a a meta character
++    return preg_replace('/^@/', ' @', $notice->content);
 +}
 +
 +function remove_twitter_link($flink)
 +{
 +    $user = $flink->getUser();
 +
 +    common_log(LOG_INFO, 'Removing Twitter bridge Foreign link for ' .
++               "user $user->nickname (user id: $user->id).");
 +
 +    $result = $flink->delete();
 +
 +    if (empty($result)) {
 +        common_log(LOG_ERR, 'Could not remove Twitter bridge ' .
 +                   "Foreign_link for $user->nickname (user id: $user->id)!");
 +        common_log_db_error($flink, 'DELETE', __FILE__);
 +    }
 +
 +    // Notify the user that her Twitter bridge is down
 +
 +    if (isset($user->email)) {
 +
 +        $result = mail_twitter_bridge_removed($user);
 +
 +        if (!$result) {
 +
 +            $msg = 'Unable to send email to notify ' .
 +              "$user->nickname (user id: $user->id) " .
 +              'that their Twitter bridge link was ' .
 +              'removed!';
 +
 +            common_log(LOG_WARNING, $msg);
 +        }
 +    }
 +
 +}
index a5452843423d8546fb15e72ee88e06590e51063b,0000000000000000000000000000000000000000..2a93ff13e20fc306a963951da7eb0fb8ceae3b0a
mode 100644,000000..100644
--- /dev/null
@@@ -1,224 -1,0 +1,224 @@@
-  * Laconica, the distributed open-source microblogging tool
 +<?php
 +/**
-  * @category  Twitter
-  * @package   Laconica
-  * @author    Zach Copely <zach@controlyourself.ca>
-  * @copyright 2009 Control Yourself, Inc.
++ * 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/>.
 + *
-  * @link      http://laconi.ca/
++ * @category  TwitterauthorizationAction
++ * @package   StatusNet
++ * @author    Zach Copely <zach@status.net>
++ * @copyright 2009 StatusNet, Inc.
 + * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
- if (!defined('LACONICA')) {
++ * @link      http://status.net/
 + */
 +
-  * Peforms the OAuth "dance" between Laconica and Twitter -- requests a token,
++if (!defined('STATUSNET') && !defined('LACONICA')) {
 +    exit(1);
 +}
 +
 +require_once INSTALLDIR . '/plugins/TwitterBridge/twitter.php';
 +
 +/**
 + * Class for doing OAuth authentication against Twitter
 + *
-  * (Foreign_link) between the Laconica user and Twitter user and stores the
++ * 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
-  * @package  Laconica
-  * @author   Zach Copley <zach@controlyourself.ca>
++ * (Foreign_link) between the StatusNet user and Twitter user and stores the
 + * access token and secret in the link.
 + *
 + * @category Twitter
++ * @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://laconi.ca/
 + *
 + */
 +class TwitterauthorizationAction extends Action
 +{
 +    /**
 +     * Initialize class members. Looks for 'oauth_token' parameter.
 +     *
 +     * @param array $args misc. arguments
 +     *
 +     * @return boolean true
 +     */
 +    function prepare($args)
 +    {
 +        parent::prepare($args);
 +
 +        $this->oauth_token = $this->arg('oauth_token');
 +
 +        return true;
 +    }
 +
 +    /**
 +     * Handler method
 +     *
 +     * @param array $args is ignored since it's now passed in in prepare()
 +     *
 +     * @return nothing
 +     */
 +    function handle($args)
 +    {
 +        parent::handle($args);
 +
 +        if (!common_logged_in()) {
 +            $this->clientError(_('Not logged in.'), 403);
 +        }
 +
 +        $user  = common_current_user();
 +        $flink = Foreign_link::getByUserID($user->id, TWITTER_SERVICE);
 +
 +        // If there's already a foreign link record, it means we already
 +        // have an access token, and this is unecessary. So go back.
 +
 +        if (isset($flink)) {
 +            common_redirect(common_local_url('twittersettings'));
 +        }
 +
 +        // $this->oauth_token is only populated once Twitter authorizes our
 +        // request token. If it's empty we're at the beginning of the auth
 +        // process
 +
 +        if (empty($this->oauth_token)) {
 +            $this->authorizeRequestToken();
 +        } else {
 +            $this->saveAccessToken();
 +        }
 +    }
 +
 +    /**
 +     * Asks Twitter for a request token, and then redirects to Twitter
 +     * to authorize it.
 +     *
 +     * @return nothing
 +     */
 +    function authorizeRequestToken()
 +    {
 +        try {
 +
 +            // Get a new request token and authorize it
 +
 +            $client  = new TwitterOAuthClient();
 +            $req_tok =
 +              $client->getRequestToken(TwitterOAuthClient::$requestTokenURL);
 +
 +            // Sock the request token away in the session temporarily
 +
 +            $_SESSION['twitter_request_token']        = $req_tok->key;
 +            $_SESSION['twitter_request_token_secret'] = $req_tok->secret;
 +
 +            $auth_link = $client->getAuthorizeLink($req_tok);
 +
 +        } catch (TwitterOAuthClientException $e) {
 +            $msg = sprintf('OAuth client cURL error - code: %1s, msg: %2s',
 +                           $e->getCode(), $e->getMessage());
 +            $this->serverError(_('Couldn\'t link your Twitter account.'));
 +        }
 +
 +        common_redirect($auth_link);
 +    }
 +
 +    /**
 +     * Called when Twitter returns an authorized request token. Exchanges
 +     * it for an access token and stores it.
 +     *
 +     * @return nothing
 +     */
 +    function saveAccessToken()
 +    {
 +
 +        // Check to make sure Twitter returned the same request
 +        // token we sent them
 +
 +        if ($_SESSION['twitter_request_token'] != $this->oauth_token) {
 +            $this->serverError(_('Couldn\'t link your Twitter account.'));
 +        }
 +
 +        try {
 +
 +            $client = new TwitterOAuthClient($_SESSION['twitter_request_token'],
 +                $_SESSION['twitter_request_token_secret']);
 +
 +            // Exchange the request token for an access token
 +
 +            $atok = $client->getAccessToken(TwitterOAuthClient::$accessTokenURL);
 +
 +            // Test the access token and get the user's Twitter info
 +
 +            $client       = new TwitterOAuthClient($atok->key, $atok->secret);
 +            $twitter_user = $client->verifyCredentials();
 +
 +        } catch (OAuthClientException $e) {
 +            $msg = sprintf('OAuth client cURL error - code: %1$s, msg: %2$s',
 +                           $e->getCode(), $e->getMessage());
 +            $this->serverError(_('Couldn\'t link your Twitter account.'));
 +        }
 +
 +        // Save the access token and Twitter user info
 +
 +        $this->saveForeignLink($atok, $twitter_user);
 +
 +        // Clean up the the mess we made in the session
 +
 +        unset($_SESSION['twitter_request_token']);
 +        unset($_SESSION['twitter_request_token_secret']);
 +
 +        common_redirect(common_local_url('twittersettings'));
 +    }
 +
 +    /**
 +     * Saves a Foreign_link between Twitter user and local user,
 +     * which includes the access token and secret.
 +     *
 +     * @param OAuthToken $access_token the access token to save
 +     * @param mixed      $twitter_user twitter API user object
 +     *
 +     * @return nothing
 +     */
 +    function saveForeignLink($access_token, $twitter_user)
 +    {
 +        $user = common_current_user();
 +
 +        $flink = new Foreign_link();
 +
 +        $flink->user_id     = $user->id;
 +        $flink->foreign_id  = $twitter_user->id;
 +        $flink->service     = TWITTER_SERVICE;
 +
 +        $creds = TwitterOAuthClient::packToken($access_token);
 +
 +        $flink->credentials = $creds;
 +        $flink->created     = common_sql_now();
 +
 +        // Defaults: noticesync on, everything else off
 +
 +        $flink->set_flags(true, false, false, false);
 +
 +        $flink_id = $flink->insert();
 +
 +        if (empty($flink_id)) {
 +            common_log_db_error($flink, 'INSERT', __FILE__);
 +                $this->serverError(_('Couldn\'t link your Twitter account.'));
 +        }
 +
 +        save_twitter_user($twitter_user->id, $twitter_user->screen_name);
 +    }
 +
 +}
 +
index b7dc4a80c3b81098a608ddc97f518a7a62944ebe,0000000000000000000000000000000000000000..bad2b74ca324101388eaa6feae1e787e65c72892
mode 100644,000000..100644
--- /dev/null
@@@ -1,220 -1,0 +1,229 @@@
-  * Laconica, the distributed open-source microblogging tool
 +<?php
 +/**
-  * @package   Laconica
-  * @author    Zach Copley <zach@controlyourself.ca>
-  * @copyright 2008 Control Yourself, Inc.
++ * StatusNet, the distributed open-source microblogging tool
 + *
 + * Class for doing OAuth calls against Twitter
 + *
 + * PHP version 5
 + *
 + * LICENCE: This program is free software: you can redistribute it and/or modify
 + * it under the terms of the GNU Affero General Public License as published by
 + * the Free Software Foundation, either version 3 of the License, or
 + * (at your option) any later version.
 + *
 + * This program is distributed in the hope that it will be useful,
 + * but WITHOUT ANY WARRANTY; without even the implied warranty of
 + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 + * GNU Affero General Public License for more details.
 + *
 + * You should have received a copy of the GNU Affero General Public License
 + * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 + *
 + * @category  Integration
-  * @link      http://laconi.ca/
++ * @package   StatusNet
++ * @author    Zach Copley <zach@status.net>
++ * @copyright 2009 StatusNet, Inc.
 + * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
- if (!defined('LACONICA')) {
++ * @link      http://status.net/
 + */
 +
-  * @package  Laconica
-  * @author   Zach Copley <zach@controlyourself.ca>
++if (!defined('STATUSNET') && !defined('LACONICA')) {
 +    exit(1);
 +}
 +
 +/**
 + * Class for talking to the Twitter API with OAuth.
 + *
 + * @category Integration
-  * @link     http://laconi.ca/
++ * @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
-      * Calls Twitter's /stutuses/update API method
++ * @link     http://status.net/
 + *
 + */
 +class TwitterOAuthClient extends OAuthClient
 +{
 +    public static $requestTokenURL = 'https://twitter.com/oauth/request_token';
 +    public static $authorizeURL    = 'https://twitter.com/oauth/authorize';
 +    public static $accessTokenURL  = 'https://twitter.com/oauth/access_token';
 +
 +    /**
 +     * Constructor
 +     *
 +     * @param string $oauth_token        the user's token
 +     * @param string $oauth_token_secret the user's token secret
 +     *
 +     * @return nothing
 +     */
 +    function __construct($oauth_token = null, $oauth_token_secret = null)
 +    {
 +        $consumer_key    = common_config('twitter', 'consumer_key');
 +        $consumer_secret = common_config('twitter', 'consumer_secret');
 +
 +        parent::__construct($consumer_key, $consumer_secret,
 +                            $oauth_token, $oauth_token_secret);
 +    }
 +
 +    // XXX: the following two functions are to support the horrible hack
 +    // of using the credentils field in Foreign_link to store both
 +    // the access token and token secret.  This hack should go away with
 +    // 0.9, in which we can make DB changes and add a new column for the
 +    // token itself.
 +
 +    static function packToken($token)
 +    {
 +        return implode(chr(0), array($token->key, $token->secret));
 +    }
 +
 +    static function unpackToken($str)
 +    {
 +        $vals = explode(chr(0), $str);
 +        return new OAuthToken($vals[0], $vals[1]);
 +    }
 +
++    static function isPackedToken($str)
++    {
++        if (strpos($str, chr(0)) === false) {
++            return false;
++        } else {
++            return true;
++        }
++    }
++
 +    /**
 +     * Builds a link to Twitter's endpoint for authorizing a request token
 +     *
 +     * @param OAuthToken $request_token token to authorize
 +     *
 +     * @return the link
 +     */
 +    function getAuthorizeLink($request_token)
 +    {
 +        return parent::getAuthorizeLink(self::$authorizeURL,
 +                                        $request_token,
 +                                        common_local_url('twitterauthorization'));
 +    }
 +
 +    /**
 +     * Calls Twitter's /account/verify_credentials API method
 +     *
 +     * @return mixed the Twitter user
 +     */
 +    function verifyCredentials()
 +    {
 +        $url          = 'https://twitter.com/account/verify_credentials.json';
 +        $response     = $this->oAuthGet($url);
 +        $twitter_user = json_decode($response);
 +        return $twitter_user;
 +    }
 +
 +    /**
-      * Calls Twitter's /stutuses/friends_timeline API method
++     * Calls Twitter's /statuses/update API method
 +     *
 +     * @param string $status                text of the status
 +     * @param int    $in_reply_to_status_id optional id of the status it's
 +     *                                      a reply to
 +     *
 +     * @return mixed the status
 +     */
 +    function statusesUpdate($status, $in_reply_to_status_id = null)
 +    {
 +        $url      = 'https://twitter.com/statuses/update.json';
 +        $params   = array('status' => $status,
 +            'in_reply_to_status_id' => $in_reply_to_status_id);
 +        $response = $this->oAuthPost($url, $params);
 +        $status   = json_decode($response);
 +        return $status;
 +    }
 +
 +    /**
-      * Calls Twitter's /stutuses/friends API method
++     * Calls Twitter's /statuses/friends_timeline API method
 +     *
 +     * @param int $since_id show statuses after this id
 +     * @param int $max_id   show statuses before this id
 +     * @param int $cnt      number of statuses to show
 +     * @param int $page     page number
 +     *
 +     * @return mixed an array of statuses
 +     */
 +    function statusesFriendsTimeline($since_id = null, $max_id = null,
 +                                     $cnt = null, $page = null)
 +    {
 +
 +        $url    = 'https://twitter.com/statuses/friends_timeline.json';
 +        $params = array('since_id' => $since_id,
 +                        'max_id' => $max_id,
 +                        'count' => $cnt,
 +                        'page' => $page);
 +        $qry    = http_build_query($params);
 +
 +        if (!empty($qry)) {
 +            $url .= "?$qry";
 +        }
 +
 +        $response = $this->oAuthGet($url);
 +        $statuses = json_decode($response);
 +        return $statuses;
 +    }
 +
 +    /**
-      * Calls Twitter's /stutuses/friends/ids API method
++     * Calls Twitter's /statuses/friends API method
 +     *
 +     * @param int $id          id of the user whom you wish to see friends of
 +     * @param int $user_id     numerical user id
 +     * @param int $screen_name screen name
 +     * @param int $page        page number
 +     *
 +     * @return mixed an array of twitter users and their latest status
 +     */
 +    function statusesFriends($id = null, $user_id = null, $screen_name = null,
 +                             $page = null)
 +    {
 +        $url = "https://twitter.com/statuses/friends.json";
 +
 +        $params = array('id' => $id,
 +                        'user_id' => $user_id,
 +                        'screen_name' => $screen_name,
 +                        'page' => $page);
 +        $qry    = http_build_query($params);
 +
 +        if (!empty($qry)) {
 +            $url .= "?$qry";
 +        }
 +
 +        $response = $this->oAuthGet($url);
 +        $friends  = json_decode($response);
 +        return $friends;
 +    }
 +
 +    /**
++     * Calls Twitter's /statuses/friends/ids API method
 +     *
 +     * @param int $id          id of the user whom you wish to see friends of
 +     * @param int $user_id     numerical user id
 +     * @param int $screen_name screen name
 +     * @param int $page        page number
 +     *
 +     * @return mixed a list of ids, 100 per page
 +     */
 +    function friendsIds($id = null, $user_id = null, $screen_name = null,
 +                         $page = null)
 +    {
 +        $url = "https://twitter.com/friends/ids.json";
 +
 +        $params = array('id' => $id,
 +                        'user_id' => $user_id,
 +                        'screen_name' => $screen_name,
 +                        'page' => $page);
 +        $qry    = http_build_query($params);
 +
 +        if (!empty($qry)) {
 +            $url .= "?$qry";
 +        }
 +
 +        $response = $this->oAuthGet($url);
 +        $ids      = json_decode($response);
 +        return $ids;
 +    }
 +
 +}
index b3d4a971f6d80ca88c6759d2fe6e836b1d7f45bd,0000000000000000000000000000000000000000..2afa85ba4c0b3905ee24c349f37e5dc07e83dc9a
mode 100644,000000..100644
--- /dev/null
@@@ -1,272 -1,0 +1,272 @@@
-  * Laconica, the distributed open-source microblogging tool
 +<?php
 +/**
-  * @package   Laconica
-  * @author    Evan Prodromou <evan@controlyourself.ca>
-  * @copyright 2008-2009 Control Yourself, Inc.
++ * 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
-  * @link      http://laconi.ca/
++ * @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
- if (!defined('LACONICA')) {
++ * @link      http://status.net/
 + */
 +
-  * @package  Laconica
-  * @author   Evan Prodromou <evan@controlyourself.ca>
++if (!defined('STATUSNET') && !defined('LACONICA')) {
 +    exit(1);
 +}
 +
 +require_once INSTALLDIR . '/lib/connectsettingsaction.php';
 +require_once INSTALLDIR . '/plugins/TwitterBridge/twitter.php';
 +
 +/**
 + * Settings for Twitter integration
 + *
 + * @category Settings
-  * @link     http://laconi.ca/
++ * @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
-      * Laconica account. Also lets the user set preferences.
++ * @link     http://status.net/
 + *
 + * @see      SettingsAction
 + */
 +
 +class TwittersettingsAction extends ConnectSettingsAction
 +{
 +    /**
 +     * Title of the page
 +     *
 +     * @return string Title of the page
 +     */
 +
 +    function title()
 +    {
 +        return _('Twitter settings');
 +    }
 +
 +    /**
 +     * Instructions for use
 +     *
 +     * @return instructions for use
 +     */
 +
 +    function getInstructions()
 +    {
 +        return _('Connect your Twitter account to share your updates ' .
 +                 'with your Twitter friends and vice-versa.');
 +    }
 +
 +    /**
 +     * Content area of the page
 +     *
 +     * Shows a form for associating a Twitter account with this
++     * 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')),
 +                           'Connect my Twitter account');
 +            $this->elementEnd('li');
 +            $this->elementEnd('ul');
 +
 +            $this->elementEnd('fieldset');
 +        } else {
 +            $this->element('legend', null, _('Twitter account'));
 +            $this->elementStart('p', array('id' => 'form_confirmed'));
 +            $this->element('a', array('href' => $fuser->uri), $fuser->nickname);
 +            $this->elementEnd('p');
 +            $this->element('p', 'form_note',
 +                           _('Connected Twitter account'));
 +
 +            $this->submit('remove', _('Remove'));
 +
 +            $this->elementEnd('fieldset');
 +
 +            $this->elementStart('fieldset', array('id' => 'settings_twitter_preferences'));
 +
 +            $this->element('legend', null, _('Preferences'));
 +            $this->elementStart('ul', 'form_data');
 +            $this->elementStart('li');
 +            $this->checkbox('noticesend',
 +                            _('Automatically send my notices to Twitter.'),
 +                            ($flink) ?
 +                            ($flink->noticesync & FOREIGN_NOTICE_SEND) :
 +                            true);
 +            $this->elementEnd('li');
 +            $this->elementStart('li');
 +            $this->checkbox('replysync',
 +                            _('Send local "@" replies to Twitter.'),
 +                            ($flink) ?
 +                            ($flink->noticesync & FOREIGN_NOTICE_SEND_REPLY) :
 +                            true);
 +            $this->elementEnd('li');
 +            $this->elementStart('li');
 +            $this->checkbox('friendsync',
 +                            _('Subscribe to my Twitter friends here.'),
 +                            ($flink) ?
 +                            ($flink->friendsync & FOREIGN_FRIEND_RECV) :
 +                            false);
 +            $this->elementEnd('li');
 +
 +            if (common_config('twitterbridge','enabled')) {
 +                $this->elementStart('li');
 +                $this->checkbox('noticerecv',
 +                                _('Import my Friends Timeline.'),
 +                                ($flink) ?
 +                                ($flink->noticesync & FOREIGN_NOTICE_RECV) :
 +                                false);
 +                $this->elementEnd('li');
++            } else {
 +                // preserve setting even if bidrection bridge toggled off
 +
 +                if ($flink && ($flink->noticesync & FOREIGN_NOTICE_RECV)) {
 +                    $this->hidden('noticerecv', true, 'noticerecv');
 +                }
 +            }
 +
 +            $this->elementEnd('ul');
 +
 +            if ($flink) {
 +                $this->submit('save', _('Save'));
 +            } else {
 +                $this->submit('add', _('Add'));
 +            }
 +
 +            $this->elementEnd('fieldset');
 +        }
 +
 +        $this->elementEnd('form');
 +    }
 +
 +    /**
 +     * Handle posts to this form
 +     *
 +     * Based on the button that was pressed, muxes out to other functions
 +     * to do the actual task requested.
 +     *
 +     * All sub-functions reload the form with a message -- success or failure.
 +     *
 +     * @return void
 +     */
 +
 +    function handlePost()
 +    {
 +        // CSRF protection
 +        $token = $this->trimmed('token');
 +        if (!$token || $token != common_session_token()) {
 +            $this->showForm(_('There was a problem with your session token. '.
 +                              'Try again, please.'));
 +            return;
 +        }
 +
 +        if ($this->arg('save')) {
 +            $this->savePreferences();
 +        } else if ($this->arg('remove')) {
 +            $this->removeTwitterAccount();
 +        } else {
 +            $this->showForm(_('Unexpected form submission.'));
 +        }
 +    }
 +
 +    /**
 +     * Disassociate an existing Twitter account from this account
 +     *
 +     * @return void
 +     */
 +
 +    function removeTwitterAccount()
 +    {
 +        $user = common_current_user();
 +        $flink = Foreign_link::getByUserID($user->id, TWITTER_SERVICE);
 +
 +        $result = $flink->delete();
 +
 +        if (empty($result)) {
 +            common_log_db_error($flink, 'DELETE', __FILE__);
 +            $this->serverError(_('Couldn\'t remove Twitter user.'));
 +            return;
 +        }
 +
 +        $this->showForm(_('Twitter account removed.'), true);
 +    }
 +
 +    /**
 +     * Save user's Twitter-bridging preferences
 +     *
 +     * @return void
 +     */
 +
 +    function savePreferences()
 +    {
 +        $noticesend = $this->boolean('noticesend');
 +        $noticerecv = $this->boolean('noticerecv');
 +        $friendsync = $this->boolean('friendsync');
 +        $replysync  = $this->boolean('replysync');
 +
 +        $user = common_current_user();
 +        $flink = Foreign_link::getByUserID($user->id, TWITTER_SERVICE);
 +
 +        if (empty($flink)) {
 +            common_log_db_error($flink, 'SELECT', __FILE__);
 +            $this->showForm(_('Couldn\'t save Twitter preferences.'));
 +            return;
 +        }
 +
 +        $original = clone($flink);
 +        $flink->set_flags($noticesend, $noticerecv, $replysync, $friendsync);
 +        $result = $flink->update($original);
 +
 +        if ($result === false) {
 +            common_log_db_error($flink, 'UPDATE', __FILE__);
 +            $this->showForm(_('Couldn\'t save Twitter preferences.'));
 +            return;
 +        }
 +
 +        $this->showForm(_('Twitter preferences saved.'), true);
 +    }
 +
 +}