diff options
Diffstat (limited to 'plugins')
-rw-r--r-- | plugins/OStatus/classes/Ostatus_profile.php | 8 | ||||
-rw-r--r-- | plugins/OpenID/openid.php | 16 | ||||
-rw-r--r-- | plugins/RSSCloud/RSSCloudQueueHandler.php | 7 | ||||
-rw-r--r-- | plugins/TwitterBridge/Notice_to_status.php | 180 | ||||
-rw-r--r-- | plugins/TwitterBridge/README | 8 | ||||
-rw-r--r-- | plugins/TwitterBridge/TwitterBridgePlugin.php | 166 | ||||
-rw-r--r-- | plugins/TwitterBridge/Twitter_synch_status.php | 202 | ||||
-rwxr-xr-x | plugins/TwitterBridge/daemons/synctwitterfriends.php | 5 | ||||
-rwxr-xr-x | plugins/TwitterBridge/daemons/twitterstatusfetcher.php | 240 | ||||
-rw-r--r-- | plugins/TwitterBridge/scripts/initialize_notice_to_status.php | 51 | ||||
-rw-r--r-- | plugins/TwitterBridge/twitter.php | 112 | ||||
-rw-r--r-- | plugins/TwitterBridge/twitterbasicauthclient.php | 258 | ||||
-rw-r--r-- | plugins/TwitterBridge/twitteroauthclient.php | 141 |
13 files changed, 1009 insertions, 385 deletions
diff --git a/plugins/OStatus/classes/Ostatus_profile.php b/plugins/OStatus/classes/Ostatus_profile.php index 6a0fd1f3b..19fe5169b 100644 --- a/plugins/OStatus/classes/Ostatus_profile.php +++ b/plugins/OStatus/classes/Ostatus_profile.php @@ -1065,6 +1065,14 @@ class Ostatus_profile extends Memcached_DataObject null, common_timestamp()); rename($temp_filename, Avatar::path($filename)); + // @fixme hardcoded chmod is lame, but seems to be necessary to + // keep from accidentally saving images from command-line (queues) + // that can't be read from web server, which causes hard-to-notice + // problems later on: + // + // http://status.net/open-source/issues/2663 + chmod(Avatar::path($filename), 0644); + $self->setOriginal($filename); $orig = clone($this); diff --git a/plugins/OpenID/openid.php b/plugins/OpenID/openid.php index 4ce350f77..1b93163e5 100644 --- a/plugins/OpenID/openid.php +++ b/plugins/OpenID/openid.php @@ -182,7 +182,19 @@ function oid_authenticate($openid_url, $returnto, $immediate=false) $trust_root = common_root_url(true); $process_url = common_local_url($returnto); - if ($auth_request->shouldSendRedirect()) { + // Net::OpenID::Server as used on LiveJournal appears to incorrectly + // reject POST requests for data submissions that OpenID 1.1 specs + // as GET, although 2.0 allows them: + // https://rt.cpan.org/Public/Bug/Display.html?id=42202 + // + // Our OpenID libraries would have switched in the redirect automatically + // if it were detecting 1.1 compatibility mode, however the server is + // advertising itself as 2.0-compatible, so we got switched to the POST. + // + // Since the GET should always work anyway, we'll just take out the + // autosubmitter for now. + // + //if ($auth_request->shouldSendRedirect()) { $redirect_url = $auth_request->redirectURL($trust_root, $process_url, $immediate); @@ -194,6 +206,7 @@ function oid_authenticate($openid_url, $returnto, $immediate=false) } else { common_redirect($redirect_url, 303); } + /* } else { // Generate form markup and render it. $form_id = 'openid_message'; @@ -219,6 +232,7 @@ function oid_authenticate($openid_url, $returnto, $immediate=false) $action->handle(array('action' => 'autosubmit')); } } + */ } # Half-assed attempt at a module-private function diff --git a/plugins/RSSCloud/RSSCloudQueueHandler.php b/plugins/RSSCloud/RSSCloudQueueHandler.php index 295c26189..ef11eda2e 100644 --- a/plugins/RSSCloud/RSSCloudQueueHandler.php +++ b/plugins/RSSCloud/RSSCloudQueueHandler.php @@ -28,7 +28,12 @@ class RSSCloudQueueHandler extends QueueHandler function handle($notice) { - $profile = $notice->getProfile(); + try { + $profile = $notice->getProfile(); + } catch (Exception $e) { + common_log(LOG_ERR, "Dropping RSSCloud item for notice with bogus profile: " . $e->getMessage()); + return true; + } $notifier = new RSSCloudNotifier(); return $notifier->notify($profile); } diff --git a/plugins/TwitterBridge/Notice_to_status.php b/plugins/TwitterBridge/Notice_to_status.php new file mode 100644 index 000000000..2e32ba963 --- /dev/null +++ b/plugins/TwitterBridge/Notice_to_status.php @@ -0,0 +1,180 @@ +<?php +/** + * Data class for remembering notice-to-status mappings + * + * PHP version 5 + * + * @category Data + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://status.net/ + * + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2010, StatusNet, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +require_once INSTALLDIR . '/classes/Memcached_DataObject.php'; + +/** + * Data class for mapping notices to statuses + * + * Notices flow back and forth between Twitter and StatusNet. We use this + * table to remember which StatusNet notice corresponds to which Twitter + * status. + * + * Note that notice_id is unique only within a single database; if you + * want to share this data for some reason, get the notice's URI and use + * that instead, since it's universally unique. + * + * @category Action + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://status.net/ + * + * @see DB_DataObject + */ + +class Notice_to_status extends Memcached_DataObject +{ + public $__table = 'notice_to_status'; // table name + public $notice_id; // int(4) primary_key not_null + public $status_id; // int(4) + public $created; // datetime + + /** + * Get an instance by key + * + * This is a utility method to get a single instance with a given key value. + * + * @param string $k Key to use to lookup + * @param mixed $v Value to lookup + * + * @return Notice_to_status object found, or null for no hits + * + */ + + function staticGet($k, $v=null) + { + return Memcached_DataObject::staticGet('Notice_to_status', $k, $v); + } + + /** + * return table definition for DB_DataObject + * + * DB_DataObject needs to know something about the table to manipulate + * instances. This method provides all the DB_DataObject needs to know. + * + * @return array array of column definitions + */ + + function table() + { + return array('notice_id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL, + 'status_id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL, + 'created' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL); + } + + /** + * return key definitions for DB_DataObject + * + * DB_DataObject needs to know about keys that the table has, since it + * won't appear in StatusNet's own keys list. In most cases, this will + * simply reference your keyTypes() function. + * + * @return array list of key field names + */ + + function keys() + { + return array_keys($this->keyTypes()); + } + + /** + * return key definitions for Memcached_DataObject + * + * Our caching system uses the same key definitions, but uses a different + * method to get them. This key information is used to store and clear + * cached data, so be sure to list any key that will be used for static + * lookups. + * + * @return array associative array of key definitions, field name to type: + * 'K' for primary key: for compound keys, add an entry for each component; + * 'U' for unique keys: compound keys are not well supported here. + */ + + function keyTypes() + { + return array('notice_id' => 'K', 'status_id' => 'U'); + } + + /** + * Magic formula for non-autoincrementing integer primary keys + * + * If a table has a single integer column as its primary key, DB_DataObject + * assumes that the column is auto-incrementing and makes a sequence table + * to do this incrementation. Since we don't need this for our class, we + * overload this method and return the magic formula that DB_DataObject needs. + * + * @return array magic three-false array that stops auto-incrementing. + */ + + function sequenceKey() + { + return array(false, false, false); + } + + /** + * Save a mapping between a notice and a status + * + * @param integer $notice_id ID of the notice in StatusNet + * @param integer $status_id ID of the status in Twitter + * + * @return Notice_to_status new object for this value + */ + + static function saveNew($notice_id, $status_id) + { + $n2s = Notice_to_status::staticGet('notice_id', $notice_id); + + if (!empty($n2s)) { + return $n2s; + } + + $n2s = Notice_to_status::staticGet('status_id', $status_id); + + if (!empty($n2s)) { + return $n2s; + } + + common_debug("Mapping notice {$notice_id} to Twitter status {$status_id}"); + + $n2s = new Notice_to_status(); + + $n2s->notice_id = $notice_id; + $n2s->status_id = $status_id; + $n2s->created = common_sql_now(); + + $n2s->insert(); + + return $n2s; + } +} diff --git a/plugins/TwitterBridge/README b/plugins/TwitterBridge/README index d7dfe20de..10ea35b2b 100644 --- a/plugins/TwitterBridge/README +++ b/plugins/TwitterBridge/README @@ -62,6 +62,14 @@ unless you configure it with a consumer key and secret.) $config['twitter']['global_consumer_key'] = 'YOUR_CONSUMER_KEY'; $config['twitter']['global_consumer_secret'] = 'YOUR_CONSUMER_SECRET'; +Upgrade +------- + +If you've used the Twitter bridge plugin prior to version 0.9.5, +you'll need to run the new scripts/initialize_notice_to_status.php +script to initialize the new notice-to-status mapping file, which +greatly improves the integration between StatusNet and Twitter. + Administration panel -------------------- diff --git a/plugins/TwitterBridge/TwitterBridgePlugin.php b/plugins/TwitterBridge/TwitterBridgePlugin.php index 8e3eba318..34b82ef83 100644 --- a/plugins/TwitterBridge/TwitterBridgePlugin.php +++ b/plugins/TwitterBridge/TwitterBridgePlugin.php @@ -194,18 +194,22 @@ class TwitterBridgePlugin extends Plugin */ function onAutoload($cls) { + $dir = dirname(__FILE__); + switch ($cls) { case 'TwittersettingsAction': case 'TwitterauthorizationAction': case 'TwitterloginAction': case 'TwitteradminpanelAction': - include_once INSTALLDIR . '/plugins/TwitterBridge/' . - strtolower(mb_substr($cls, 0, -6)) . '.php'; + include_once $dir . '/' . strtolower(mb_substr($cls, 0, -6)) . '.php'; return false; case 'TwitterOAuthClient': case 'TwitterQueueHandler': - include_once INSTALLDIR . '/plugins/TwitterBridge/' . - strtolower($cls) . '.php'; + include_once $dir . '/' . strtolower($cls) . '.php'; + return false; + case 'Notice_to_status': + case 'Twitter_synch_status': + include_once $dir . '/' . $cls . '.php'; return false; default: return true; @@ -360,5 +364,157 @@ class TwitterBridgePlugin extends Plugin } } -} + /** + * Database schema setup + * + * We maintain a table mapping StatusNet notices to Twitter statuses + * + * @see Schema + * @see ColumnDef + * + * @return boolean hook value; true means continue processing, false means stop. + */ + + function onCheckSchema() + { + $schema = Schema::get(); + + // For saving the last-synched status of various timelines + // home_timeline, messages (in), messages (out), ... + + $schema->ensureTable('twitter_synch_status', + array(new ColumnDef('foreign_id', 'bigint', null, + false, 'PRI'), + new ColumnDef('timeline', 'varchar', 255, + false, 'PRI'), + new ColumnDef('last_id', 'bigint', null, // XXX: check for PostgreSQL + false), + new ColumnDef('created', 'datetime', null, + false), + new ColumnDef('modified', 'datetime', null, + false))); + + // For storing user-submitted flags on profiles + + $schema->ensureTable('notice_to_status', + array(new ColumnDef('notice_id', 'integer', null, + false, 'PRI'), + new ColumnDef('status_id', 'bigint', null, // XXX: check for PostgreSQL + false, 'UNI'), + new ColumnDef('created', 'datetime', null, + false))); + + return true; + } + + /** + * If a notice gets deleted, remove the Notice_to_status mapping and + * delete the status on Twitter. + * + * @param User $user The user doing the deleting + * @param Notice $notice The notice getting deleted + * + * @return boolean hook value + */ + + function onStartDeleteOwnNotice(User $user, Notice $notice) + { + $n2s = Notice_to_status::staticGet('notice_id', $notice->id); + + if (!empty($n2s)) { + + $flink = Foreign_link::getByUserID($notice->profile_id, + TWITTER_SERVICE); // twitter service + + if (empty($flink)) { + return true; + } + + if (!TwitterOAuthClient::isPackedToken($flink->credentials)) { + $this->log(LOG_INFO, "Skipping deleting notice for {$notice->id} since link is not OAuth."); + return true; + } + + $token = TwitterOAuthClient::unpackToken($flink->credentials); + $client = new TwitterOAuthClient($token->key, $token->secret); + + $client->statusesDestroy($n2s->status_id); + + $n2s->delete(); + } + return true; + } + + /** + * Notify remote users when their notices get favorited. + * + * @param Profile or User $profile of local user doing the faving + * @param Notice $notice being favored + * @return hook return value + */ + + function onEndFavorNotice(Profile $profile, Notice $notice) + { + $flink = Foreign_link::getByUserID($profile->id, + TWITTER_SERVICE); // twitter service + + if (empty($flink)) { + return true; + } + + if (!TwitterOAuthClient::isPackedToken($flink->credentials)) { + $this->log(LOG_INFO, "Skipping fave processing for {$profile->id} since link is not OAuth."); + return true; + } + + $status_id = twitter_status_id($notice); + + if (empty($status_id)) { + return true; + } + + $token = TwitterOAuthClient::unpackToken($flink->credentials); + $client = new TwitterOAuthClient($token->key, $token->secret); + + $client->favoritesCreate($status_id); + + return true; + } + + /** + * Notify remote users when their notices get de-favorited. + * + * @param Profile $profile Profile person doing the de-faving + * @param Notice $notice Notice being favored + * + * @return hook return value + */ + + function onEndDisfavorNotice(Profile $profile, Notice $notice) + { + $flink = Foreign_link::getByUserID($profile->id, + TWITTER_SERVICE); // twitter service + + if (empty($flink)) { + return true; + } + + if (!TwitterOAuthClient::isPackedToken($flink->credentials)) { + $this->log(LOG_INFO, "Skipping fave processing for {$profile->id} since link is not OAuth."); + return true; + } + $status_id = twitter_status_id($notice); + + if (empty($status_id)) { + return true; + } + + $token = TwitterOAuthClient::unpackToken($flink->credentials); + $client = new TwitterOAuthClient($token->key, $token->secret); + + $client->favoritesDestroy($status_id); + + return true; + } +} diff --git a/plugins/TwitterBridge/Twitter_synch_status.php b/plugins/TwitterBridge/Twitter_synch_status.php new file mode 100644 index 000000000..2a5f1fd60 --- /dev/null +++ b/plugins/TwitterBridge/Twitter_synch_status.php @@ -0,0 +1,202 @@ +<?php +/** + * Store last-touched ID for various timelines + * + * PHP version 5 + * + * @category Data + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://status.net/ + * + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2010, StatusNet, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +require_once INSTALLDIR . '/classes/Memcached_DataObject.php'; + +/** + * Store various timeline data + * + * We don't want to keep re-fetching the same statuses and direct messages from Twitter. + * So, we store the last ID we see from a timeline, and store it. Next time + * around, we use that ID in the since_id parameter. + * + * @category Action + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://status.net/ + * + * @see DB_DataObject + */ + +class Twitter_synch_status extends Memcached_DataObject +{ + public $__table = 'twitter_synch_status'; // table name + public $foreign_id; // int(4) primary_key not_null + public $timeline; // varchar(255) primary_key not_null + public $last_id; // bigint not_null + public $created; // datetime not_null + public $modified; // datetime not_null + + /** + * Get an instance by key + * + * @param string $k Key to use to lookup (usually 'foreign_id' for this class) + * @param mixed $v Value to lookup + * + * @return Twitter_synch_status object found, or null for no hits + * + */ + + function staticGet($k, $v=null) + { + throw new Exception("Use pkeyGet() for this class."); + } + + /** + * Get an instance by compound primary key + * + * @param array $kv key-value pair array + * + * @return Twitter_synch_status object found, or null for no hits + * + */ + + function pkeyGet($kv) + { + return Memcached_DataObject::pkeyGet('Twitter_synch_status', $kv); + } + + /** + * return table definition for DB_DataObject + * + * DB_DataObject needs to know something about the table to manipulate + * instances. This method provides all the DB_DataObject needs to know. + * + * @return array array of column definitions + */ + + function table() + { + return array('foreign_id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL, + 'timeline' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL, + 'last_id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL, + 'created' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL, + 'modified' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL + ); + } + + /** + * return key definitions for DB_DataObject + * + * DB_DataObject needs to know about keys that the table has, since it + * won't appear in StatusNet's own keys list. In most cases, this will + * simply reference your keyTypes() function. + * + * @return array list of key field names + */ + + function keys() + { + return array_keys($this->keyTypes()); + } + + /** + * return key definitions for Memcached_DataObject + * + * Our caching system uses the same key definitions, but uses a different + * method to get them. This key information is used to store and clear + * cached data, so be sure to list any key that will be used for static + * lookups. + * + * @return array associative array of key definitions, field name to type: + * 'K' for primary key: for compound keys, add an entry for each component; + * 'U' for unique keys: compound keys are not well supported here. + */ + + function keyTypes() + { + return array('foreign_id' => 'K', + 'timeline' => 'K'); + } + + /** + * Magic formula for non-autoincrementing integer primary keys + * + * If a table has a single integer column as its primary key, DB_DataObject + * assumes that the column is auto-incrementing and makes a sequence table + * to do this incrementation. Since we don't need this for our class, we + * overload this method and return the magic formula that DB_DataObject needs. + * + * @return array magic three-false array that stops auto-incrementing. + */ + + function sequenceKey() + { + return array(false, false, false); + } + + static function getLastId($foreign_id, $timeline) + { + $tss = self::pkeyGet(array('foreign_id' => $foreign_id, + 'timeline' => $timeline)); + + if (empty($tss)) { + return null; + } else { + return $tss->last_id; + } + } + + static function setLastId($foreign_id, $timeline, $last_id) + { + $tss = self::pkeyGet(array('foreign_id' => $foreign_id, + 'timeline' => $timeline)); + + if (empty($tss)) { + + $tss = new Twitter_synch_status(); + + $tss->foreign_id = $foreign_id; + $tss->timeline = $timeline; + $tss->last_id = $last_id; + $tss->created = common_sql_now(); + $tss->modified = $tss->created; + + $tss->insert(); + + return true; + + } else { + + $orig = clone($tss); + + $tss->last_id = $last_id; + $tss->modified = common_sql_now(); + + $tss->update(); + + return true; + } + } +} diff --git a/plugins/TwitterBridge/daemons/synctwitterfriends.php b/plugins/TwitterBridge/daemons/synctwitterfriends.php index df7da0943..02546a02c 100755 --- a/plugins/TwitterBridge/daemons/synctwitterfriends.php +++ b/plugins/TwitterBridge/daemons/synctwitterfriends.php @@ -33,7 +33,6 @@ END_OF_TRIM_HELP; require_once INSTALLDIR . '/scripts/commandline.inc'; require_once INSTALLDIR . '/lib/parallelizingdaemon.php'; require_once INSTALLDIR . '/plugins/TwitterBridge/twitter.php'; -require_once INSTALLDIR . '/plugins/TwitterBridge/twitterbasicauthclient.php'; require_once INSTALLDIR . '/plugins/TwitterBridge/twitteroauthclient.php'; /** @@ -144,8 +143,8 @@ class SyncTwitterFriendsDaemon extends ParallelizingDaemon $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.'); + common_debug("Skipping Twitter friends for {$flink->user_id} since not OAuth."); + return $friends; } try { diff --git a/plugins/TwitterBridge/daemons/twitterstatusfetcher.php b/plugins/TwitterBridge/daemons/twitterstatusfetcher.php index 03a4bd3f3..f1305696b 100755 --- a/plugins/TwitterBridge/daemons/twitterstatusfetcher.php +++ b/plugins/TwitterBridge/daemons/twitterstatusfetcher.php @@ -40,7 +40,6 @@ require_once INSTALLDIR . '/scripts/commandline.inc'; require_once INSTALLDIR . '/lib/common.php'; require_once INSTALLDIR . '/lib/daemon.php'; require_once INSTALLDIR . '/plugins/TwitterBridge/twitter.php'; -require_once INSTALLDIR . '/plugins/TwitterBridge/twitterbasicauthclient.php'; require_once INSTALLDIR . '/plugins/TwitterBridge/twitteroauthclient.php'; /** @@ -104,7 +103,6 @@ class TwitterStatusFetcher extends ParallelizingDaemon function getObjects() { global $_DB_DATAOBJECT; - $flink = new Foreign_link(); $conn = &$flink->getDatabaseConnection(); @@ -168,10 +166,6 @@ class TwitterStatusFetcher extends ParallelizingDaemon 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 = null; if (TwitterOAuthClient::isPackedToken($flink->credentials)) { @@ -179,14 +173,17 @@ class TwitterStatusFetcher extends ParallelizingDaemon $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.'); + common_debug("Skipping friends timeline for $flink->foreign_id since not OAuth."); } $timeline = null; + $lastId = Twitter_synch_status::getLastId($flink->foreign_id, 'home_timeline'); + + common_debug("Got lastId value '{$lastId}' for foreign id '{$flink->foreign_id}' and timeline 'home_timeline'"); + try { - $timeline = $client->statusesHomeTimeline(); + $timeline = $client->statusesHomeTimeline($lastId); } catch (Exception $e) { common_log(LOG_WARNING, $this->name() . ' - Twitter client unable to get friends timeline for user ' . @@ -215,7 +212,23 @@ class TwitterStatusFetcher extends ParallelizingDaemon continue; } - $this->saveStatus($status, $flink); + // Don't save it if the user is protected + // FIXME: save it but treat it as private + + if ($status->user->protected) { + continue; + } + + $notice = $this->saveStatus($status); + + if (!empty($notice)) { + Inbox::insertNotice($flink->user_id, $notice->id); + } + } + + if (!empty($timeline)) { + Twitter_synch_status::setLastId($flink->foreign_id, 'home_timeline', $timeline[0]->id); + common_debug("Set lastId value '{$timeline[0]->id}' for foreign id '{$flink->foreign_id}' and timeline 'home_timeline'"); } // Okay, record the time we synced with Twitter for posterity @@ -224,32 +237,61 @@ class TwitterStatusFetcher extends ParallelizingDaemon $flink->update(); } - function saveStatus($status, $flink) + function saveStatus($status) { $profile = $this->ensureProfile($status->user); if (empty($profile)) { common_log(LOG_ERR, $this->name() . ' - Problem saving notice. No associated Profile.'); - return; + return null; } - $statusUri = 'http://twitter.com/' - . $status->user->screen_name - . '/status/' - . $status->id; + $statusUri = $this->makeStatusURI($status->user->screen_name, $status->id); // check to see if we've already imported the status - $dupe = $this->checkDupe($profile, $statusUri); + $n2s = Notice_to_status::staticGet('status_id', $status->id); - if (!empty($dupe)) { + if (!empty($n2s)) { common_log( LOG_INFO, $this->name() . - " - Ignoring duplicate import: $statusUri" + " - Ignoring duplicate import: {$status->id}" ); - return; + return Notice::staticGet('id', $n2s->notice_id); + } + + // If it's a retweet, save it as a repeat! + + if (!empty($status->retweeted_status)) { + common_log(LOG_INFO, "Status {$status->id} is a retweet of {$status->retweeted_status->id}."); + $original = $this->saveStatus($status->retweeted_status); + if (empty($original)) { + return null; + } else { + $author = $original->getProfile(); + // TRANS: Message used to repeat a notice. RT is the abbreviation of 'retweet'. + // TRANS: %1$s is the repeated user's name, %2$s is the repeated notice. + $content = sprintf(_('RT @%1$s %2$s'), + $author->nickname, + $original->content); + + if (Notice::contentTooLong($content)) { + $contentlimit = Notice::maxContent(); + $content = mb_substr($content, 0, $contentlimit - 4) . ' ...'; + } + + $repeat = Notice::saveNew($profile->id, + $content, + 'twitter', + array('repeat_of' => $original->id, + 'uri' => $statusUri, + 'is_local' => Notice::GATEWAY)); + common_log(LOG_INFO, "Saved {$repeat->id} as a repeat of {$original->id}"); + Notice_to_status::saveNew($repeat->id, $status->id); + return $repeat; + } } $notice = new Notice(); @@ -263,14 +305,36 @@ class TwitterStatusFetcher extends ParallelizingDaemon ); $notice->source = 'twitter'; + $notice->reply_to = null; + + if (!empty($status->in_reply_to_status_id)) { + common_log(LOG_INFO, "Status {$status->id} is a reply to status {$status->in_reply_to_status_id}"); + $n2s = Notice_to_status::staticGet('status_id', $status->in_reply_to_status_id); + if (empty($n2s)) { + common_log(LOG_INFO, "Couldn't find local notice for status {$status->in_reply_to_status_id}"); + } else { + $reply = Notice::staticGet('id', $n2s->notice_id); + if (empty($reply)) { + common_log(LOG_INFO, "Couldn't find local notice for status {$status->in_reply_to_status_id}"); + } else { + common_log(LOG_INFO, "Found local notice {$reply->id} for status {$status->in_reply_to_status_id}"); + $notice->reply_to = $reply->id; + $notice->conversation = $reply->conversation; + } + } + } + + if (empty($notice->conversation)) { + $conv = Conversation::create(); + $notice->conversation = $conv->id; + common_log(LOG_INFO, "No known conversation for status {$status->id} so making a new one {$conv->id}."); + } + $notice->is_local = Notice::GATEWAY; - $notice->content = common_shorten_links($status->text); - $notice->rendered = common_render_content( - $notice->content, - $notice - ); + $notice->content = html_entity_decode($status->text); + $notice->rendered = $this->linkify($status); if (Event::handle('StartNoticeSave', array(&$notice))) { @@ -285,24 +349,32 @@ class TwitterStatusFetcher extends ParallelizingDaemon Event::handle('EndNoticeSave', array($notice)); } - $orig = clone($notice); - $conv = Conversation::create(); + Notice_to_status::saveNew($notice->id, $status->id); - $notice->conversation = $conv->id; + $this->saveStatusMentions($notice, $status); - if (!$notice->update($orig)) { - common_log_db_error($notice, 'UPDATE', __FILE__); - common_log(LOG_ERR, $this->name() . - ' - Problem saving notice.'); - } - - Inbox::insertNotice($flink->user_id, $notice->id); $notice->blowOnInsert(); return $notice; } /** + * Make an URI for a status. + * + * @param object $status status object + * + * @return string URI + */ + + function makeStatusURI($username, $id) + { + return 'http://twitter.com/' + . $username + . '/status/' + . $id; + } + + /** * Look up a Profile by profileurl field. Profile::staticGet() was * not working consistently. * @@ -631,6 +703,104 @@ class TwitterStatusFetcher extends ParallelizingDaemon return true; } + + const URL = 1; + const HASHTAG = 2; + const MENTION = 3; + + function linkify($status) + { + $text = $status->text; + + if (empty($status->entities)) { + return $text; + } + + // Move all the entities into order so we can + // replace them in reverse order and thus + // not mess up their indices + + $toReplace = array(); + + if (!empty($status->entities->urls)) { + foreach ($status->entities->urls as $url) { + $toReplace[$url->indices[0]] = array(self::URL, $url); + } + } + + if (!empty($status->entities->hashtags)) { + foreach ($status->entities->hashtags as $hashtag) { + $toReplace[$hashtag->indices[0]] = array(self::HASHTAG, $hashtag); + } + } + + if (!empty($status->entities->user_mentions)) { + foreach ($status->entities->user_mentions as $mention) { + $toReplace[$mention->indices[0]] = array(self::MENTION, $mention); + } + } + + // sort in reverse order by key + + krsort($toReplace); + + foreach ($toReplace as $part) { + list($type, $object) = $part; + switch($type) { + case self::URL: + $linkText = $this->makeUrlLink($object); + break; + case self::HASHTAG: + $linkText = $this->makeHashtagLink($object); + break; + case self::MENTION: + $linkText = $this->makeMentionLink($object); + break; + default: + continue; + } + $text = mb_substr($text, 0, $object->indices[0]) . $linkText . mb_substr($text, $object->indices[1]); + } + return $text; + } + + function makeUrlLink($object) + { + return "<a href='{$object->url}' class='extlink'>{$object->url}</a>"; + } + + function makeHashtagLink($object) + { + return "#<a href='https://twitter.com/search?q=%23{$object->text}' class='hashtag'>{$object->text}</a>"; + } + + function makeMentionLink($object) + { + return "@<a href='http://twitter.com/{$object->screen_name}' title='{$object->name}'>{$object->screen_name}</a>"; + } + + function saveStatusMentions($notice, $status) + { + $mentions = array(); + + if (empty($status->entities) || empty($status->entities->user_mentions)) { + return; + } + + foreach ($status->entities->user_mentions as $mention) { + $flink = Foreign_link::getByForeignID($mention->id, TWITTER_SERVICE); + if (!empty($flink)) { + $user = User::staticGet('id', $flink->user_id); + if (!empty($user)) { + $reply = new Reply(); + $reply->notice_id = $notice->id; + $reply->profile_id = $user->id; + common_log(LOG_INFO, __METHOD__ . ": saving reply: notice {$notice->id} to profile {$user->id}"); + $id = $reply->insert(); + } + } + } + } } $id = null; diff --git a/plugins/TwitterBridge/scripts/initialize_notice_to_status.php b/plugins/TwitterBridge/scripts/initialize_notice_to_status.php new file mode 100644 index 000000000..d1acfd53f --- /dev/null +++ b/plugins/TwitterBridge/scripts/initialize_notice_to_status.php @@ -0,0 +1,51 @@ +#!/usr/bin/env php +<?php +/* + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2010, StatusNet, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +define('INSTALLDIR', realpath(dirname(__FILE__) . '/../../..')); + +$helptext = <<<ENDOFHELP +USAGE: initialize_notice_to_status.php + +Initializes the notice_to_status table with existing Twitter synch +data. Only necessary if you've had the Twitter bridge enabled before +version 0.9.5. + +ENDOFHELP; + +require_once INSTALLDIR.'/scripts/commandline.inc'; + +// We update any notices that may have come in from +// Twitter that we don't have a status_id for. Note that +// this won't catch notices that originated at this StatusNet site. + +$n = new Notice(); + +$n->query('SELECT notice.id, notice.uri ' . + 'FROM notice LEFT JOIN notice_to_status ' . + 'ON notice.id = notice_to_status.notice_id ' . + 'WHERE notice.source = "twitter"' . + 'AND notice_to_status.status_id IS NULL'); + +while ($n->fetch()) { + if (preg_match('#^http://twitter.com/[\w_.]+/status/(\d+)$#', $n->uri, $match)) { + $status_id = $match[1]; + Notice_to_status::saveNew($n->id, $status_id); + } +} diff --git a/plugins/TwitterBridge/twitter.php b/plugins/TwitterBridge/twitter.php index 306ba2442..90b0f0f14 100644 --- a/plugins/TwitterBridge/twitter.php +++ b/plugins/TwitterBridge/twitter.php @@ -23,7 +23,6 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { define('TWITTER_SERVICE', 1); // Twitter is foreign_service ID 1 -require_once INSTALLDIR . '/plugins/TwitterBridge/twitterbasicauthclient.php'; require_once INSTALLDIR . '/plugins/TwitterBridge/twitteroauthclient.php'; function add_twitter_user($twitter_id, $screen_name) @@ -115,9 +114,12 @@ 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 it's not a Twitter-style reply, or if the user WANTS to send replies, + // or if it's in reply to a twitter notice + if (!preg_match('/^@[a-zA-Z0-9_]{1,15}\b/u', $notice->content) || - ($flink->noticesync & FOREIGN_NOTICE_SEND_REPLY)) { + ($flink->noticesync & FOREIGN_NOTICE_SEND_REPLY) || + is_twitter_notice($notice->reply_to)) { return true; } } @@ -125,22 +127,64 @@ function is_twitter_bound($notice, $flink) { return false; } +function is_twitter_notice($id) +{ + $n2s = Notice_to_status::staticGet('notice_id', $id); + + return (!empty($n2s)); +} + function broadcast_twitter($notice) { $flink = Foreign_link::getByUserID($notice->profile_id, TWITTER_SERVICE); - if (is_twitter_bound($notice, $flink)) { - if (TwitterOAuthClient::isPackedToken($flink->credentials)) { + // Don't bother with basic auth, since it's no longer allowed + + if (!empty($flink) && TwitterOAuthClient::isPackedToken($flink->credentials)) { + if (!empty($notice->repeat_of) && is_twitter_notice($notice->repeat_of)) { + $retweet = retweet_notice($flink, Notice::staticGet('id', $notice->repeat_of)); + if (!empty($retweet)) { + Notice_to_status::saveNew($notice->id, $retweet->id); + } + } else if (is_twitter_bound($notice, $flink)) { return broadcast_oauth($notice, $flink); - } else { - return broadcast_basicauth($notice, $flink); } } return true; } +function retweet_notice($flink, $notice) +{ + $token = TwitterOAuthClient::unpackToken($flink->credentials); + $client = new TwitterOAuthClient($token->key, $token->secret); + + $id = twitter_status_id($notice); + + if (empty($id)) { + common_log(LOG_WARNING, "Trying to retweet notice {$notice->id} with no known status id."); + return null; + } + + try { + $status = $client->statusesRetweet($id); + return $status; + } catch (OAuthClientException $e) { + return process_error($e, $flink, $notice); + } +} + +function twitter_status_id($notice) +{ + $n2s = Notice_to_status::staticGet('notice_id', $notice->id); + if (empty($n2s)) { + return null; + } else { + return $n2s->status_id; + } +} + /** * Pull any extra information from a notice that we should transfer over * to Twitter beyond the notice text itself. @@ -156,10 +200,13 @@ function twitter_update_params($notice) $params['lat'] = $notice->lat; $params['long'] = $notice->lon; } + if (!empty($notice->reply_to) && is_twitter_notice($notice->reply_to)) { + $reply = Notice::staticGet('id', $notice->reply_to); + $params['in_reply_to_status_id'] = twitter_status_id($reply); + } return $params; } - function broadcast_oauth($notice, $flink) { $user = $flink->getUser(); $statustxt = format_status($notice); @@ -171,6 +218,9 @@ function broadcast_oauth($notice, $flink) { try { $status = $client->statusesUpdate($statustxt, $params); + if (!empty($status)) { + Notice_to_status::saveNew($notice->id, $status->id); + } } catch (OAuthClientException $e) { return process_error($e, $flink, $notice); } @@ -204,52 +254,6 @@ function broadcast_oauth($notice, $flink) { return true; } -function broadcast_basicauth($notice, $flink) -{ - $user = $flink->getUser(); - - $statustxt = format_status($notice); - $params = twitter_update_params($notice); - - $client = new TwitterBasicAuthClient($flink); - $status = null; - - try { - $status = $client->statusesUpdate($statustxt, $params); - } catch (BasicAuthException $e) { - return process_error($e, $flink, $notice); - } - - if (empty($status)) { - - $errmsg = sprintf('Twitter bridge - No data returned by Twitter API when ' . - 'trying to post notice %d for %s (user id %d).', - $notice->id, - $user->nickname, - $user->id); - - common_log(LOG_WARNING, $errmsg); - - $errmsg = sprintf('No data returned by Twitter API when ' . - 'trying to post notice %d for %s (user id %d).', - $notice->id, - $user->nickname, - $user->id); - common_log(LOG_WARNING, $errmsg); - return false; - } - - $msg = sprintf('Twitter bridge - posted notice %d to Twitter using ' . - 'HTTP basic auth for User %s (user id %d).', - $notice->id, - $user->nickname, - $user->id); - - common_log(LOG_INFO, $msg); - - return true; -} - function process_error($e, $flink, $notice) { $user = $flink->getUser(); diff --git a/plugins/TwitterBridge/twitterbasicauthclient.php b/plugins/TwitterBridge/twitterbasicauthclient.php deleted file mode 100644 index 23828ed4a..000000000 --- a/plugins/TwitterBridge/twitterbasicauthclient.php +++ /dev/null @@ -1,258 +0,0 @@ -<?php -/** - * StatusNet, the distributed open-source microblogging tool - * - * Class for doing HTTP basic auth calls against Twitter - * - * PHP version 5 - * - * LICENCE: This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * - * @category Integration - * @package 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 - * @link http://status.net/ - */ - -if (!defined('STATUSNET') && !defined('LACONICA')) { - exit(1); -} - -/** - * General Exception wrapper for HTTP basic auth errors - * - * @category Integration - * @package StatusNet - * @author Zach Copley <zach@status.net> - * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 - * @link http://status.net/ - * - */ -class BasicAuthException extends Exception -{ -} - -/** - * Class for talking to the Twitter API with HTTP Basic Auth. - * - * @category Integration - * @package StatusNet - * @author Zach Copley <zach@status.net> - * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 - * @link http://status.net/ - * - */ -class TwitterBasicAuthClient -{ - var $screen_name = null; - var $password = null; - - /** - * constructor - * - * @param Foreign_link $flink a Foreign_link storing the - * Twitter user's password, etc. - */ - function __construct($flink) - { - $fuser = $flink->getForeignUser(); - $this->screen_name = $fuser->nickname; - $this->password = $flink->credentials; - } - - /** - * Calls Twitter's /statuses/update API method - * - * @param string $status text of the status - * @param mixed $params optional other parameters to pass to Twitter, - * as defined. For back-compatibility, if an int - * is passed we'll consider it a reply-to ID. - * - * @return mixed the status - */ - function statusesUpdate($status, $in_reply_to_status_id = null) - { - $url = 'https://twitter.com/statuses/update.json'; - if (is_numeric($params)) { - $params = array('in_reply_to_status_id' => intval($params)); - } - $params['status'] = $status; - $params['source'] = common_config('integration', 'source'); - $response = $this->httpRequest($url, $params); - $status = json_decode($response); - return $status; - } - - /** - * 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->httpRequest($url); - $statuses = json_decode($response); - return $statuses; - } - - /** - * Calls Twitter's /statuses/home_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 similar to friends timeline but including retweets - */ - function statusesHomeTimeline($since_id = null, $max_id = null, - $cnt = null, $page = null) - { - $url = 'https://twitter.com/statuses/home_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->httpRequest($url); - $statuses = json_decode($response); - return $statuses; - } - - /** - * 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->httpRequest($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->httpRequest($url); - $ids = json_decode($response); - return $ids; - } - - /** - * Make an HTTP request - * - * @param string $url Where to make the request - * @param array $params post parameters - * - * @return mixed the request - * @throws BasicAuthException - */ - function httpRequest($url, $params = null, $auth = true) - { - $request = HTTPClient::start(); - $request->setConfig(array( - 'follow_redirects' => true, - 'connect_timeout' => 120, - 'timeout' => 120, - 'ssl_verify_peer' => false, - 'ssl_verify_host' => false - )); - - if ($auth) { - $request->setAuth($this->screen_name, $this->password); - } - - if (isset($params)) { - // Twitter is strict about accepting invalid "Expect" headers - $headers = array('Expect:'); - $response = $request->post($url, $headers, $params); - } else { - $response = $request->get($url); - } - - $code = $response->getStatus(); - - if ($code < 200 || $code >= 400) { - throw new BasicAuthException($response->getBody(), $code); - } - - return $response->getBody(); - } - -} diff --git a/plugins/TwitterBridge/twitteroauthclient.php b/plugins/TwitterBridge/twitteroauthclient.php index f6ef78675..dae76ec84 100644 --- a/plugins/TwitterBridge/twitteroauthclient.php +++ b/plugins/TwitterBridge/twitteroauthclient.php @@ -188,7 +188,7 @@ class TwitterOAuthClient extends OAuthClient } /** - * Calls Twitter's /statuses/friends_timeline API method + * Calls Twitter's /statuses/home_timeline API method * * @param int $since_id show statuses after this id * @param int $max_id show statuses before this id @@ -197,22 +197,28 @@ class TwitterOAuthClient extends OAuthClient * * @return mixed an array of statuses */ - function statusesFriendsTimeline($since_id = null, $max_id = null, - $cnt = null, $page = null) + function statusesHomeTimeline($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); + $url = 'https://twitter.com/statuses/home_timeline.json'; - if (!empty($qry)) { - $url .= "?$qry"; + $params = array('include_entities' => 'true'); + + if (!empty($since_id)) { + $params['since_id'] = $since_id; + } + if (!empty($max_id)) { + $params['max_id'] = $max_id; + } + if (!empty($cnt)) { + $params['count'] = $cnt; + } + if (!empty($page)) { + $params['page'] = $page; } - $response = $this->oAuthGet($url); + $response = $this->oAuthGet($url, $params); $statuses = json_decode($response); return $statuses; } @@ -262,17 +268,25 @@ class TwitterOAuthClient extends OAuthClient { $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); + $params = array(); - if (!empty($qry)) { - $url .= "?$qry"; + if (!empty($id)) { + $params['id'] = $id; } - $response = $this->oAuthGet($url); + if (!empty($user_id)) { + $params['user_id'] = $user_id; + } + + if (!empty($screen_name)) { + $params['screen_name'] = $screen_name; + } + + if (!empty($page)) { + $params['page'] = $page; + } + + $response = $this->oAuthGet($url, $params); $friends = json_decode($response); return $friends; } @@ -292,19 +306,90 @@ class TwitterOAuthClient extends OAuthClient { $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); + $params = array(); - if (!empty($qry)) { - $url .= "?$qry"; + if (!empty($id)) { + $params['id'] = $id; } - $response = $this->oAuthGet($url); + if (!empty($user_id)) { + $params['user_id'] = $user_id; + } + + if (!empty($screen_name)) { + $params['screen_name'] = $screen_name; + } + + if (!empty($page)) { + $params['page'] = $page; + } + + $response = $this->oAuthGet($url, $params); $ids = json_decode($response); return $ids; } + /** + * Calls Twitter's /statuses/retweet/id.json API method + * + * @param int $id id of the notice to retweet + * + * @return retweeted status + */ + + function statusesRetweet($id) + { + $url = "http://api.twitter.com/1/statuses/retweet/$id.json"; + $response = $this->oAuthPost($url); + $status = json_decode($response); + return $status; + } + + /** + * Calls Twitter's /favorites/create API method + * + * @param int $id ID of the status to favorite + * + * @return object faved status + */ + + function favoritesCreate($id) + { + $url = "http://api.twitter.com/1/favorites/create/$id.json"; + $response = $this->oAuthPost($url); + $status = json_decode($response); + return $status; + } + + /** + * Calls Twitter's /favorites/destroy API method + * + * @param int $id ID of the status to unfavorite + * + * @return object unfaved status + */ + + function favoritesDestroy($id) + { + $url = "http://api.twitter.com/1/favorites/destroy/$id.json"; + $response = $this->oAuthPost($url); + $status = json_decode($response); + return $status; + } + + /** + * Calls Twitter's /statuses/destroy API method + * + * @param int $id ID of the status to destroy + * + * @return object destroyed + */ + + function statusesDestroy($id) + { + $url = "http://api.twitter.com/1/statuses/destroy/$id.json"; + $response = $this->oAuthPost($url); + $status = json_decode($response); + return $status; + } } |