From: Zach Copley Date: Tue, 8 Sep 2009 23:02:57 +0000 (-0700) Subject: Merge branch '0.9.x' into pluginize-twitter-bridge X-Git-Url: https://git.mxchange.org/?a=commitdiff_plain;h=9d87313eaebe8240393ac300a435f3b1332c8849;p=quix0rs-gnu-social.git Merge branch '0.9.x' into pluginize-twitter-bridge Conflicts: plugins/TwitterBridge/twitterauthorization.php --- 9d87313eaebe8240393ac300a435f3b1332c8849 diff --cc plugins/TwitterBridge/daemons/synctwitterfriends.php index b7be5d043a,0000000000..0668c6222f mode 100755,000000..100755 --- a/plugins/TwitterBridge/daemons/synctwitterfriends.php +++ b/plugins/TwitterBridge/daemons/synctwitterfriends.php @@@ -1,280 -1,0 +1,287 @@@ +#!/usr/bin/env php +. + */ + +define('INSTALLDIR', realpath(dirname(__FILE__) . '/../../..')); + +$shortoptions = 'di::'; +$longoptions = array('id::', 'debug'); + +$helptext = << - * @author Evan Prodromou ++ * @package StatusNet ++ * @author Zach Copley ++ * @author Evan Prodromou + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 - * @link http://laconi.ca/ ++ * @link http://status.net/ + */ + +$helptext = <<_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(); + - $token = TwitterOAuthClient::unpackToken($flink->credentials); ++ $client = null; + - $client = new TwitterOAuthClient($token->key, $token->secret); ++ 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 (OAuthCurlException $e) { ++ } 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(); + diff --cc plugins/TwitterBridge/daemons/twitterqueuehandler.php index 9aa9d1ed0a,0000000000..f0e76bb745 mode 100755,000000..100755 --- a/plugins/TwitterBridge/daemons/twitterqueuehandler.php +++ b/plugins/TwitterBridge/daemons/twitterqueuehandler.php @@@ -1,73 -1,0 +1,73 @@@ +#!/usr/bin/env php +. + */ + +define('INSTALLDIR', realpath(dirname(__FILE__) . '/../../..')); + +$shortoptions = 'i::'; +$longoptions = array('id::'); + +$helptext = <<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(); diff --cc plugins/TwitterBridge/daemons/twitterstatusfetcher.php index 3023bf423b,0000000000..4752ada7c8 mode 100755,000000..100755 --- a/plugins/TwitterBridge/daemons/twitterstatusfetcher.php +++ b/plugins/TwitterBridge/daemons/twitterstatusfetcher.php @@@ -1,559 -1,0 +1,566 @@@ +#!/usr/bin/env php +. + */ + +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 = << - * @author Evan Prodromou ++ * @package StatusNet ++ * @author Zach Copley ++ * @author Evan Prodromou + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 - * @link http://laconi.ca/ ++ * @link http://status.net/ + */ + +// NOTE: an Avatar path MUST be set in config.php for this - // script to work: e.g.: $config['avatar']['path'] = '/laconica/avatar'; ++// 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) + { - if (empty($flink)) { ++ if (empty($flink)) { + common_log(LOG_WARNING, $this->name() . - " - Can't retrieve Foreign_link for foreign ID $fid"); ++ " - 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. + - $token = TwitterOAuthClient::unpackToken($flink->credentials); ++ $client = null; + - $client = new TwitterOAuthClient($token->key, $token->secret); ++ 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(); - } catch (OAuthClientCurlException $e) { ++ } catch (Exception $e) { + common_log(LOG_WARNING, $this->name() . - ' - OAuth client unable to get friends timeline for user ' . ++ ' - 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) { + - // Hacktastic: filter out stuff coming from this Laconica ++ // 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 - // Laconica's (Laconica's = 96) ++ // 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(); + diff --cc plugins/TwitterBridge/twitter.php index 280cdb0a33,0000000000..b49e2e1190 mode 100644,000000..100644 --- a/plugins/TwitterBridge/twitter.php +++ b/plugins/TwitterBridge/twitter.php @@@ -1,258 -1,0 +1,311 @@@ +. + */ + - if (!defined('LACONICA')) { ++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); ++ } ++ } + - $user = $flink->getUser(); - - // XXX: Hack to get around PHP cURL's use of @ being a a meta character - $statustxt = preg_replace('/^@/', ' @', $notice->content); ++ return true; ++} + - $token = TwitterOAuthClient::unpackToken($flink->credentials); ++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); ++ } + - $client = new TwitterOAuthClient($token->key, $token->secret); ++ if (empty($status)) { + - $status = null; ++ // This could represent a failure posting, ++ // or the Twitter API might just be behaving flakey. + - try { - $status = $client->statusesUpdate($statustxt); - } catch (OAuthClientCurlException $e) { ++ $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); + - if ($e->getMessage() == 'The requested URL returned error: 401') { ++ return false; ++ } + - $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); ++ // Notice crossed the great divide + - // Bad auth token! We need to delete the foreign_link - // to Twitter and inform the user. ++ $msg = sprintf('Twitter bridge - posted notice %s to Twitter using OAuth.', ++ $notice->id); ++ common_log(LOG_INFO, $msg); + - remove_twitter_link($flink); - return true; ++ return true; ++} + - } else { ++function broadcast_basicauth($notice, $flink) ++{ ++ $user = $flink->getUser(); + - // Some other error happened, so we should probably - // try to send again later. ++ $statustxt = format_status($notice); + - $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); ++ $client = new TwitterBasicAuthClient($flink); ++ $status = null; + - return false; - } - } ++ try { ++ $status = $client->statusesUpdate($statustxt); ++ } catch (BasicAuthCurlException $e) { ++ return process_error($e, $flink); ++ } + - if (empty($status)) { ++ if (empty($status)) { + - // This could represent a failure posting, - // or the Twitter API might just be behaving flakey. ++ $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 = sprint('No data returned by Twitter API when ' . ++ $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; ++ } + - return false; - } ++ $msg = sprintf('Twitter bridge - posted notice %s to Twitter using basic auth.', ++ $notice->id); ++ common_log(LOG_INFO, $msg); + - // Notice crossed the great divide ++ return true; ++} + - $msg = sprintf('Twitter bridge posted notice %s to Twitter.', - $notice->id); - common_log(LOG_INFO, $msg); ++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; + } + - return true; ++ 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)."); ++ "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); + } + } + +} - diff --cc plugins/TwitterBridge/twitterauthorization.php index a545284342,0000000000..2a93ff13e2 mode 100644,000000..100644 --- a/plugins/TwitterBridge/twitterauthorization.php +++ b/plugins/TwitterBridge/twitterauthorization.php @@@ -1,224 -1,0 +1,224 @@@ +. + * - * @category Twitter - * @package Laconica - * @author Zach Copely - * @copyright 2009 Control Yourself, Inc. ++ * @category TwitterauthorizationAction ++ * @package StatusNet ++ * @author Zach Copely ++ * @copyright 2009 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 - * @link http://laconi.ca/ ++ * @link http://status.net/ + */ + - if (!defined('LACONICA')) { ++if (!defined('STATUSNET') && !defined('LACONICA')) { + exit(1); +} + +require_once INSTALLDIR . '/plugins/TwitterBridge/twitter.php'; + +/** + * Class for doing OAuth authentication against Twitter + * - * Peforms the OAuth "dance" between Laconica and Twitter -- requests a token, ++ * 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 Laconica user and Twitter user and stores the ++ * (Foreign_link) between the StatusNet user and Twitter user and stores the + * access token and secret in the link. + * + * @category Twitter - * @package Laconica - * @author Zach Copley ++ * @package StatusNet ++ * @author Zach Copley + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + * + */ +class TwitterauthorizationAction extends Action +{ + /** + * Initialize class members. Looks for 'oauth_token' parameter. + * + * @param array $args misc. arguments + * + * @return boolean true + */ + function prepare($args) + { + parent::prepare($args); + + $this->oauth_token = $this->arg('oauth_token'); + + return true; + } + + /** + * Handler method + * + * @param array $args is ignored since it's now passed in in prepare() + * + * @return nothing + */ + function handle($args) + { + parent::handle($args); + + if (!common_logged_in()) { + $this->clientError(_('Not logged in.'), 403); + } + + $user = common_current_user(); + $flink = Foreign_link::getByUserID($user->id, TWITTER_SERVICE); + + // If there's already a foreign link record, it means we already + // have an access token, and this is unecessary. So go back. + + if (isset($flink)) { + common_redirect(common_local_url('twittersettings')); + } + + // $this->oauth_token is only populated once Twitter authorizes our + // request token. If it's empty we're at the beginning of the auth + // process + + if (empty($this->oauth_token)) { + $this->authorizeRequestToken(); + } else { + $this->saveAccessToken(); + } + } + + /** + * Asks Twitter for a request token, and then redirects to Twitter + * to authorize it. + * + * @return nothing + */ + function authorizeRequestToken() + { + try { + + // Get a new request token and authorize it + + $client = new TwitterOAuthClient(); + $req_tok = + $client->getRequestToken(TwitterOAuthClient::$requestTokenURL); + + // Sock the request token away in the session temporarily + + $_SESSION['twitter_request_token'] = $req_tok->key; + $_SESSION['twitter_request_token_secret'] = $req_tok->secret; + + $auth_link = $client->getAuthorizeLink($req_tok); + + } catch (TwitterOAuthClientException $e) { + $msg = sprintf('OAuth client cURL error - code: %1s, msg: %2s', + $e->getCode(), $e->getMessage()); + $this->serverError(_('Couldn\'t link your Twitter account.')); + } + + common_redirect($auth_link); + } + + /** + * Called when Twitter returns an authorized request token. Exchanges + * it for an access token and stores it. + * + * @return nothing + */ + function saveAccessToken() + { + + // Check to make sure Twitter returned the same request + // token we sent them + + if ($_SESSION['twitter_request_token'] != $this->oauth_token) { + $this->serverError(_('Couldn\'t link your Twitter account.')); + } + + try { + + $client = new TwitterOAuthClient($_SESSION['twitter_request_token'], + $_SESSION['twitter_request_token_secret']); + + // Exchange the request token for an access token + + $atok = $client->getAccessToken(TwitterOAuthClient::$accessTokenURL); + + // Test the access token and get the user's Twitter info + + $client = new TwitterOAuthClient($atok->key, $atok->secret); + $twitter_user = $client->verifyCredentials(); + + } catch (OAuthClientException $e) { + $msg = sprintf('OAuth client cURL error - code: %1$s, msg: %2$s', + $e->getCode(), $e->getMessage()); + $this->serverError(_('Couldn\'t link your Twitter account.')); + } + + // Save the access token and Twitter user info + + $this->saveForeignLink($atok, $twitter_user); + + // Clean up the the mess we made in the session + + unset($_SESSION['twitter_request_token']); + unset($_SESSION['twitter_request_token_secret']); + + common_redirect(common_local_url('twittersettings')); + } + + /** + * Saves a Foreign_link between Twitter user and local user, + * which includes the access token and secret. + * + * @param OAuthToken $access_token the access token to save + * @param mixed $twitter_user twitter API user object + * + * @return nothing + */ + function saveForeignLink($access_token, $twitter_user) + { + $user = common_current_user(); + + $flink = new Foreign_link(); + + $flink->user_id = $user->id; + $flink->foreign_id = $twitter_user->id; + $flink->service = TWITTER_SERVICE; + + $creds = TwitterOAuthClient::packToken($access_token); + + $flink->credentials = $creds; + $flink->created = common_sql_now(); + + // Defaults: noticesync on, everything else off + + $flink->set_flags(true, false, false, false); + + $flink_id = $flink->insert(); + + if (empty($flink_id)) { + common_log_db_error($flink, 'INSERT', __FILE__); + $this->serverError(_('Couldn\'t link your Twitter account.')); + } + + save_twitter_user($twitter_user->id, $twitter_user->screen_name); + } + +} + diff --cc plugins/TwitterBridge/twitteroauthclient.php index b7dc4a80c3,0000000000..bad2b74ca3 mode 100644,000000..100644 --- a/plugins/TwitterBridge/twitteroauthclient.php +++ b/plugins/TwitterBridge/twitteroauthclient.php @@@ -1,220 -1,0 +1,229 @@@ +. + * + * @category Integration - * @package Laconica - * @author Zach Copley - * @copyright 2008 Control Yourself, Inc. ++ * @package StatusNet ++ * @author Zach Copley ++ * @copyright 2009 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 - * @link http://laconi.ca/ ++ * @link http://status.net/ + */ + - if (!defined('LACONICA')) { ++if (!defined('STATUSNET') && !defined('LACONICA')) { + exit(1); +} + +/** + * Class for talking to the Twitter API with OAuth. + * + * @category Integration - * @package Laconica - * @author Zach Copley ++ * @package StatusNet ++ * @author Zach Copley + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 - * @link http://laconi.ca/ ++ * @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/update 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_timeline 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 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 /stutuses/friends/ids API method ++ * 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; + } + +} diff --cc plugins/TwitterBridge/twittersettings.php index b3d4a971f6,0000000000..2afa85ba4c mode 100644,000000..100644 --- a/plugins/TwitterBridge/twittersettings.php +++ b/plugins/TwitterBridge/twittersettings.php @@@ -1,272 -1,0 +1,272 @@@ +. + * + * @category Settings - * @package Laconica - * @author Evan Prodromou - * @copyright 2008-2009 Control Yourself, Inc. ++ * @package StatusNet ++ * @author Evan Prodromou ++ * @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://laconi.ca/ ++ * @link http://status.net/ + */ + - if (!defined('LACONICA')) { ++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 - * @package Laconica - * @author Evan Prodromou ++ * @package StatusNet ++ * @author Evan Prodromou + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 - * @link http://laconi.ca/ ++ * @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 - * Laconica account. Also lets the user set preferences. ++ * 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); + } + +}