diff options
Diffstat (limited to 'plugins')
35 files changed, 1103 insertions, 469 deletions
diff --git a/plugins/DisqusPlugin.php b/plugins/DisqusPlugin.php index dc56f320c..c07eaaabd 100644 --- a/plugins/DisqusPlugin.php +++ b/plugins/DisqusPlugin.php @@ -148,7 +148,7 @@ ENDOFSCRIPT; $noticeUrl .= '#disqus_thread'; $noticeListItem->out->element( - 'a', array('href' => $noticeUrl, 'class' => 'disqus_count', 'Comments') + 'a', array('href' => $noticeUrl, 'class' => 'disqus_count'), 'Comments' ); $noticeListItem->showNoticeOptions(); diff --git a/plugins/OStatus/OStatusPlugin.php b/plugins/OStatus/OStatusPlugin.php index f9a8782fa..77bc9872b 100644 --- a/plugins/OStatus/OStatusPlugin.php +++ b/plugins/OStatus/OStatusPlugin.php @@ -564,7 +564,9 @@ class OStatusPlugin extends Plugin $act->time = time(); $act->title = _("Follow"); - $act->content = sprintf(_("%s is now following %s."), + // TRANS: Success message for subscribe to user attempt through OStatus. + // TRANS: %1$s is the subscriber name, %2$s is the subscribed user's name. + $act->content = sprintf(_("%1$s is now following %2$s."), $subscriber->getBestName(), $other->getBestName()); @@ -612,7 +614,9 @@ class OStatusPlugin extends Plugin $act->time = time(); $act->title = _("Unfollow"); - $act->content = sprintf(_("%s stopped following %s."), + // TRANS: Success message for unsubscribe from user attempt through OStatus. + // TRANS: %1$s is the unsubscriber's name, %2$s is the unsubscribed user's name. + $act->content = sprintf(_("%1$s stopped following %2$s."), $profile->getBestName(), $other->getBestName()); @@ -657,7 +661,9 @@ class OStatusPlugin extends Plugin $act->time = time(); $act->title = _m("Join"); - $act->content = sprintf(_m("%s has joined group %s."), + // TRANS: Success message for subscribe to group attempt through OStatus. + // TRANS: %1$s is the member name, %2$s is the subscribed group's name. + $act->content = sprintf(_m("%1$s has joined group %2$s."), $member->getBestName(), $oprofile->getBestName()); @@ -706,7 +712,9 @@ class OStatusPlugin extends Plugin $act->time = time(); $act->title = _m("Leave"); - $act->content = sprintf(_m("%s has left group %s."), + // TRANS: Success message for unsubscribe from group attempt through OStatus. + // TRANS: %1$s is the member name, %2$s is the unsubscribed group's name. + $act->content = sprintf(_m("%1$s has left group %2$s."), $member->getBestName(), $oprofile->getBestName()); @@ -746,7 +754,9 @@ class OStatusPlugin extends Plugin $act->time = time(); $act->title = _("Favor"); - $act->content = sprintf(_("%s marked notice %s as a favorite."), + // TRANS: Success message for adding a favorite notice through OStatus. + // TRANS: %1$s is the favoring user's name, %2$s is URI to the favored notice. + $act->content = sprintf(_("%1$s marked notice %2$s as a favorite."), $profile->getBestName(), $notice->uri); @@ -790,7 +800,9 @@ class OStatusPlugin extends Plugin common_date_iso8601(time())); $act->time = time(); $act->title = _("Disfavor"); - $act->content = sprintf(_("%s marked notice %s as no longer a favorite."), + // TRANS: Success message for remove a favorite notice through OStatus. + // TRANS: %1$s is the unfavoring user's name, %2$s is URI to the no longer favored notice. + $act->content = sprintf(_("%1$s marked notice %2$s as no longer a favorite."), $profile->getBestName(), $notice->uri); @@ -865,7 +877,7 @@ class OStatusPlugin extends Plugin 'class' => 'entity_subscribe')); $action->element('a', array('href' => common_local_url($target), 'class' => 'entity_remote_subscribe') - , _m('Remote')); + , _m('Remote')); // @todo: i18n: Add translator hint for this text. $action->elementEnd('p'); $action->elementEnd('div'); } @@ -905,6 +917,8 @@ class OStatusPlugin extends Plugin common_date_iso8601(time())); $act->time = time(); $act->title = _m("Profile update"); + // TRANS: Ping text for remote profile update through OStatus. + // TRANS: %s is user that updated their profile. $act->content = sprintf(_m("%s has updated their profile page."), $profile->getBestName()); @@ -934,7 +948,7 @@ class OStatusPlugin extends Plugin array('nickname' => $profileUser->nickname)); $output->element('a', array('href' => $url, 'class' => 'entity_remote_subscribe'), - _m('Subscribe')); + _m('Subscribe')); // @todo: i18n: Add context. $output->elementEnd('li'); } } @@ -950,7 +964,7 @@ class OStatusPlugin extends Plugin 'homepage' => 'http://status.net/wiki/Plugin:OStatus', 'rawdescription' => _m('Follow people across social networks that implement '. - '<a href="http://ostatus.org/">OStatus</a>.')); + '<a href="http://ostatus.org/">OStatus</a>.')); // @todo i18n: Add translator hint. return true; } diff --git a/plugins/OStatus/actions/ostatusgroup.php b/plugins/OStatus/actions/ostatusgroup.php index 1b368de63..1861e866f 100644 --- a/plugins/OStatus/actions/ostatusgroup.php +++ b/plugins/OStatus/actions/ostatusgroup.php @@ -74,7 +74,7 @@ class OStatusGroupAction extends OStatusSubAction $this->input('profile', _m('Join group'), $this->profile_uri, - _m("OStatus group's address, like http://example.net/group/nickname")); + _m("OStatus group's address, like http://example.net/group/nickname.")); $this->elementEnd('li'); $this->elementEnd('ul'); diff --git a/plugins/OStatus/actions/ostatusinit.php b/plugins/OStatus/actions/ostatusinit.php index 22aea9f70..0c991aba9 100644 --- a/plugins/OStatus/actions/ostatusinit.php +++ b/plugins/OStatus/actions/ostatusinit.php @@ -45,13 +45,13 @@ class OStatusInitAction extends Action // Local user or group the remote wants to subscribe to $this->nickname = $this->trimmed('nickname'); $this->group = $this->trimmed('group'); - + // Webfinger or profile URL of the remote user $this->profile = $this->trimmed('profile'); return true; } - + function handle($args) { parent::handle($args); @@ -69,7 +69,7 @@ class OStatusInitAction extends Action $this->showForm(); } } - + function showForm($err = null) { $this->err = $err; @@ -109,12 +109,12 @@ class OStatusInitAction extends Action $this->elementStart('ul', 'form_data'); $this->elementStart('li', array('id' => 'ostatus_nickname')); $this->input('nickname', _m('User nickname'), $this->nickname, - _m('Nickname of the user you want to follow')); + _m('Nickname of the user you want to follow.')); $this->hidden('group', $this->group); // pass-through for magic links $this->elementEnd('li'); $this->elementStart('li', array('id' => 'ostatus_profile')); $this->input('profile', _m('Profile Account'), $this->profile, - _m('Your account id (i.e. user@identi.ca)')); + _m('Your account id (e.g. user@identi.ca).')); $this->elementEnd('li'); $this->elementEnd('ul'); $this->submit('submit', $submit); @@ -199,7 +199,7 @@ class OStatusInitAction extends Action function title() { - return _m('OStatus Connect'); + return _m('OStatus Connect'); } - + } diff --git a/plugins/OStatus/actions/ostatussub.php b/plugins/OStatus/actions/ostatussub.php index 28714514f..4cbd7d034 100644 --- a/plugins/OStatus/actions/ostatussub.php +++ b/plugins/OStatus/actions/ostatussub.php @@ -64,11 +64,11 @@ class OStatusSubAction extends Action $this->input('profile', _m('Subscribe to'), $this->profile_uri, - _m("OStatus user's address, like nickname@example.com or http://example.net/nickname")); + _m("OStatus user's address, like nickname@example.com or http://example.net/nickname")); // @todo i18n FIXME: needs context/translator hint. $this->elementEnd('li'); $this->elementEnd('ul'); - $this->submit('validate', _m('Continue')); + $this->submit('validate', _m('Continue')); // @todo i18n FIXME: needs context/translator hint. $this->elementEnd('fieldset'); @@ -103,10 +103,10 @@ class OStatusSubAction extends Action $this->hidden('profile', $this->profile_uri); if ($this->oprofile->isGroup()) { $this->submit('submit', _m('Join'), 'submit', null, - _m('Join this group')); + _m('Join this group')); // @todo i18n FIXME: needs context/translator hint. } else { $this->submit('submit', _m('Confirm'), 'submit', null, - _m('Subscribe to this user')); + _m('Subscribe to this user')); // @todo i18n FIXME: needs context/translator hint. } $this->elementEnd('fieldset'); $this->elementEnd('form'); @@ -244,13 +244,13 @@ class OStatusSubAction extends Action } else if (Validate::uri($this->profile_uri)) { $this->oprofile = Ostatus_profile::ensureProfileURL($this->profile_uri); } else { - $this->error = _m("Sorry, we could not reach that address. Please make sure that the OStatus address is like nickname@example.com or http://example.net/nickname"); + $this->error = _m("Sorry, we could not reach that address. Please make sure that the OStatus address is like nickname@example.com or http://example.net/nickname."); common_debug('Invalid address format.', __FILE__); return false; } return true; } catch (FeedSubBadURLException $e) { - $this->error = _m("Sorry, we could not reach that address. Please make sure that the OStatus address is like nickname@example.com or http://example.net/nickname"); + $this->error = _m("Sorry, we could not reach that address. Please make sure that the OStatus address is like nickname@example.com or http://example.net/nickname."); common_debug('Invalid URL or could not reach server.', __FILE__); } catch (FeedSubBadResponseException $e) { $this->error = _m("Sorry, we could not reach that feed. Please try that OStatus address again later."); @@ -269,7 +269,7 @@ class OStatusSubAction extends Action common_debug('Not a recognized feed type.', __FILE__); } catch (Exception $e) { // Any new ones we forgot about - $this->error = _m("Sorry, we could not reach that address. Please make sure that the OStatus address is like nickname@example.com or http://example.net/nickname"); + $this->error = _m("Sorry, we could not reach that address. Please make sure that the OStatus address is like nickname@example.com or http://example.net/nickname."); common_debug(sprintf('Bad feed URL: %s %s', get_class($e), $e->getMessage()), __FILE__); } diff --git a/plugins/OStatus/actions/ownerxrd.php b/plugins/OStatus/actions/ownerxrd.php index 9c141d8c7..3fcb982b8 100644 --- a/plugins/OStatus/actions/ownerxrd.php +++ b/plugins/OStatus/actions/ownerxrd.php @@ -32,7 +32,7 @@ class OwnerxrdAction extends XrdAction function prepare($args) { $this->user = User::siteOwner(); - + if (!$this->user) { $this->clientError(_('No such user.'), 404); return false; @@ -40,7 +40,7 @@ class OwnerxrdAction extends XrdAction $nick = common_canonical_nickname($this->user->nickname); $acct = 'acct:' . $nick . '@' . common_config('site', 'server'); - + $this->xrd = new XRD(); // Check to see if a $config['webfinger']['owner'] has been set diff --git a/plugins/OStatus/actions/pushcallback.php b/plugins/OStatus/actions/pushcallback.php index 9a2067b8c..6c6978745 100644 --- a/plugins/OStatus/actions/pushcallback.php +++ b/plugins/OStatus/actions/pushcallback.php @@ -37,7 +37,7 @@ class PushCallbackAction extends Action $this->handleGet(); } } - + /** * Handler for POST content updates from the hub */ @@ -46,11 +46,12 @@ class PushCallbackAction extends Action $feedid = $this->arg('feed'); common_log(LOG_INFO, "POST for feed id $feedid"); if (!$feedid) { - throw new ServerException('Empty or invalid feed id', 400); + throw new ServerException('Empty or invalid feed id.', 400); } $feedsub = FeedSub::staticGet('id', $feedid); if (!$feedsub) { + // @todo i18n FIXME: added i18n and use sprintf when using parameters. throw new ServerException('Unknown PuSH feed id ' . $feedid, 400); } @@ -70,7 +71,7 @@ class PushCallbackAction extends Action $qm = QueueManager::get(); $qm->enqueue($data, 'pushin'); } - + /** * Handler for GET verification requests from the hub. */ @@ -88,20 +89,24 @@ class PushCallbackAction extends Action $feedsub = FeedSub::staticGet('uri', $topic); if (!$feedsub) { - throw new ClientException("Bad hub.topic feed $topic", 404); + // @todo i18n FIXME: added i18n and use sprintf when using parameters. + throw new ClientException("Bad hub.topic feed $topic.", 404); } if ($feedsub->verify_token !== $verify_token) { - throw new ClientException("Bad hub.verify_token $token for $topic", 404); + // @todo i18n FIXME: added i18n and use sprintf when using parameters. + throw new ClientException("Bad hub.verify_token $token for $topic.", 404); } if ($mode == 'subscribe') { // We may get re-sub requests legitimately. if ($feedsub->sub_state != 'subscribe' && $feedsub->sub_state != 'active') { + // @todo i18n FIXME: added i18n and use sprintf when using parameters. throw new ClientException("Unexpected subscribe request for $topic.", 404); } } else { if ($feedsub->sub_state != 'unsubscribe') { + // @todo i18n FIXME: added i18n and use sprintf when using parameters. throw new ClientException("Unexpected unsubscribe request for $topic.", 404); } } diff --git a/plugins/OStatus/actions/pushhub.php b/plugins/OStatus/actions/pushhub.php index 842d65e7d..6909b8539 100644 --- a/plugins/OStatus/actions/pushhub.php +++ b/plugins/OStatus/actions/pushhub.php @@ -36,7 +36,6 @@ Things to consider... */ - class PushHubAction extends Action { function arg($arg, $def=null) @@ -63,8 +62,10 @@ class PushHubAction extends Action $this->subunsub($mode); break; case "publish": + // @todo i18n FIXME: added i18n and use sprintf when using parameters. throw new ClientException("Publishing outside feeds not supported.", 400); default: + // @todo i18n FIXME: added i18n and use sprintf when using parameters. throw new ClientException("Unrecognized mode '$mode'.", 400); } } @@ -84,16 +85,19 @@ class PushHubAction extends Action $topic = $this->argUrl('hub.topic'); if (!$this->recognizedFeed($topic)) { + // @todo i18n FIXME: added i18n and use sprintf when using parameters. throw new ClientException("Unsupported hub.topic $topic; this hub only serves local user and group Atom feeds."); } $verify = $this->arg('hub.verify'); // @fixme may be multiple if ($verify != 'sync' && $verify != 'async') { + // @todo i18n FIXME: added i18n and use sprintf when using parameters. throw new ClientException("Invalid hub.verify $verify; must be sync or async."); } $lease = $this->arg('hub.lease_seconds', null); if ($mode == 'subscribe' && $lease != '' && !preg_match('/^\d+$/', $lease)) { + // @todo i18n FIXME: added i18n and use sprintf when using parameters. throw new ClientException("Invalid hub.lease $lease; must be empty or positive integer."); } @@ -101,6 +105,7 @@ class PushHubAction extends Action $secret = $this->arg('hub.secret', null); if ($secret != '' && strlen($secret) >= 200) { + // @todo i18n FIXME: added i18n and use sprintf when using parameters. throw new ClientException("Invalid hub.secret $secret; must be under 200 bytes."); } @@ -152,6 +157,7 @@ class PushHubAction extends Action if ($feed == $userFeed) { $user = User::staticGet('id', $id); if (!$user) { + // @todo i18n FIXME: added i18n and use sprintf when using parameters. throw new ClientException("Invalid hub.topic $feed; user doesn't exist."); } else { return true; @@ -160,6 +166,7 @@ class PushHubAction extends Action if ($feed == $groupFeed) { $user = User_group::staticGet('id', $id); if (!$user) { + // @todo i18n FIXME: added i18n and use sprintf when using parameters. throw new ClientException("Invalid hub.topic $feed; group doesn't exist."); } else { return true; @@ -183,6 +190,7 @@ class PushHubAction extends Action if (Validate::uri($url, $params)) { return $url; } else { + // @todo i18n FIXME: added i18n and use sprintf when using parameters. throw new ClientException("Invalid URL passed for $arg: '$url'"); } } @@ -199,4 +207,3 @@ class PushHubAction extends Action return HubSub::staticGet($feed, $callback); } } - diff --git a/plugins/OStatus/actions/userxrd.php b/plugins/OStatus/actions/userxrd.php index 6a6886eb8..dd720568b 100644 --- a/plugins/OStatus/actions/userxrd.php +++ b/plugins/OStatus/actions/userxrd.php @@ -33,7 +33,7 @@ class UserxrdAction extends XrdAction $this->uri = $this->trimmed('uri'); $this->uri = Discovery::normalize($this->uri); - + if (Discovery::isWebfinger($this->uri)) { $parts = explode('@', substr(urldecode($this->uri), 5)); if (count($parts) == 2) { diff --git a/plugins/OStatus/classes/FeedSub.php b/plugins/OStatus/classes/FeedSub.php index dd1968db1..6f9e0856a 100644 --- a/plugins/OStatus/classes/FeedSub.php +++ b/plugins/OStatus/classes/FeedSub.php @@ -249,7 +249,7 @@ class FeedSub extends Memcached_DataObject // We'll never actually get updates in this mode. return true; } else { - throw new ServerException("Attempting to start PuSH subscription for feed with no hub"); + throw new ServerException("Attempting to start PuSH subscription for feed with no hub."); } } @@ -279,7 +279,7 @@ class FeedSub extends Memcached_DataObject // We'll never actually get updates in this mode. return true; } else { - throw new ServerException("Attempting to end PuSH subscription for feed with no hub"); + throw new ServerException("Attempting to end PuSH subscription for feed with no hub."); } } @@ -502,4 +502,3 @@ class FeedSub extends Memcached_DataObject } } - diff --git a/plugins/OStatus/classes/HubSub.php b/plugins/OStatus/classes/HubSub.php index 7db528a4e..e01ae4e79 100644 --- a/plugins/OStatus/classes/HubSub.php +++ b/plugins/OStatus/classes/HubSub.php @@ -206,6 +206,7 @@ class HubSub extends Memcached_DataObject if ($status >= 200 && $status < 300) { common_log(LOG_INFO, "Verified $mode of $this->callback:$this->topic"); } else { + // @todo i18n FIXME: add i18n and use sprintf for parameter. throw new ClientException("Hub subscriber verification returned HTTP $status"); } @@ -307,9 +308,9 @@ class HubSub extends Memcached_DataObject /** * Queue up a large batch of pushes to multiple subscribers * for this same topic update. - * + * * If queues are disabled, this will run immediately. - * + * * @param string $atom well-formed Atom feed * @param array $pushCallbacks list of callback URLs */ @@ -359,4 +360,3 @@ class HubSub extends Memcached_DataObject } } } - diff --git a/plugins/OStatus/classes/Magicsig.php b/plugins/OStatus/classes/Magicsig.php index f8c56a05f..e39a6d8f7 100644 --- a/plugins/OStatus/classes/Magicsig.php +++ b/plugins/OStatus/classes/Magicsig.php @@ -33,21 +33,21 @@ class Magicsig extends Memcached_DataObject { const PUBLICKEYREL = 'magic-public-key'; - + public $__table = 'magicsig'; public $user_id; public $keypair; public $alg; - + public $publicKey; public $privateKey; - + public function __construct($alg = 'RSA-SHA256') { $this->alg = $alg; } - + public /*static*/ function staticGet($k, $v=null) { $obj = parent::staticGet(__CLASS__, $k, $v); @@ -111,7 +111,7 @@ class Magicsig extends Memcached_DataObject public function generate($user_id) { $rsa = new Crypt_RSA(); - + $keypair = $rsa->createKey(); $rsa->loadKey($keypair['privatekey']); @@ -121,7 +121,7 @@ class Magicsig extends Memcached_DataObject $this->publicKey = new Crypt_RSA(); $this->publicKey->loadKey($keypair['publickey']); - + $this->user_id = $user_id; $this->insert(); } @@ -136,13 +136,13 @@ class Magicsig extends Memcached_DataObject $private_exp = '.' . Magicsig::base64_url_encode($this->privateKey->exponent->toBytes()); } - return 'RSA.' . $mod . '.' . $exp . $private_exp; + return 'RSA.' . $mod . '.' . $exp . $private_exp; } - + public static function fromString($text) { $magic_sig = new Magicsig(); - + // remove whitespace $text = preg_replace('/\s+/', '', $text); @@ -150,7 +150,7 @@ class Magicsig extends Memcached_DataObject if (!preg_match('/RSA\.([^\.]+)\.([^\.]+)(.([^\.]+))?/', $text, $matches)) { return false; } - + $mod = $matches[1]; $exp = $matches[2]; if (!empty($matches[4])) { @@ -184,7 +184,7 @@ class Magicsig extends Memcached_DataObject $this->publicKey = $rsa; } } - + public function getName() { return $this->alg; @@ -199,7 +199,7 @@ class Magicsig extends Memcached_DataObject } } - + public function sign($bytes) { $sig = $this->privateKey->sign($bytes); @@ -223,5 +223,3 @@ class Magicsig extends Memcached_DataObject return base64_decode(strtr($input, '-_', '+/')); } } - - diff --git a/plugins/OStatus/classes/Ostatus_profile.php b/plugins/OStatus/classes/Ostatus_profile.php index 898c63e83..19fe5169b 100644 --- a/plugins/OStatus/classes/Ostatus_profile.php +++ b/plugins/OStatus/classes/Ostatus_profile.php @@ -188,9 +188,11 @@ class Ostatus_profile extends Memcached_DataObject } else if ($this->group_id && !$this->profile_id) { return true; } else if ($this->group_id && $this->profile_id) { - throw new ServerException("Invalid ostatus_profile state: both group and profile IDs set for $this->uri"); + // @todo i18n FIXME: use sprintf and add i18n. + throw new ServerException("Invalid ostatus_profile state: both group and profile IDs set for $this->uri."); } else { - throw new ServerException("Invalid ostatus_profile state: both group and profile IDs empty for $this->uri"); + // @todo i18n FIXME: use sprintf and add i18n. + throw new ServerException("Invalid ostatus_profile state: both group and profile IDs empty for $this->uri."); } } @@ -370,7 +372,8 @@ class Ostatus_profile extends Memcached_DataObject } else if ($entry instanceof Notice) { return $preamble . $entry->asAtomEntry(true, true); } else { - throw new ServerException("Invalid type passed to Ostatus_profile::notify; must be XML string or Activity entry"); + // @todo i18n FIXME: use sprintf and add i18n. + throw new ServerException("Invalid type passed to Ostatus_profile::notify; must be XML string or Activity entry."); } } @@ -549,7 +552,8 @@ class Ostatus_profile extends Memcached_DataObject $sourceContent = $note->title; } else { // @fixme fetch from $sourceUrl? - throw new ClientException("No content for notice {$sourceUri}"); + // @todo i18n FIXME: use sprintf and add i18n. + throw new ClientException("No content for notice {$sourceUri}."); } // Get (safe!) HTML and text versions of the content @@ -587,7 +591,7 @@ class Ostatus_profile extends Memcached_DataObject ' class="attachment more"' . ' title="'. htmlspecialchars(_m('Show more')) . '">' . '…' . - '</a>'; + '</a>'; // @todo i18n FIXME: add translator hint/context. } } @@ -772,6 +776,7 @@ class Ostatus_profile extends Memcached_DataObject $response = $client->get($profile_url); if (!$response->isOk()) { + // @todo i18n FIXME: use sprintf and add i18n. throw new Exception("Could not reach profile page: " . $profile_url); } @@ -829,6 +834,7 @@ class Ostatus_profile extends Memcached_DataObject return self::ensureFeedURL($feedurl, $hints); } + // @todo i18n FIXME: use sprintf and add i18n. throw new Exception("Could not find a feed URL for profile page " . $finalUrl); } @@ -861,6 +867,7 @@ class Ostatus_profile extends Memcached_DataObject $user = User::staticGet('id', $profile->id); if (!empty($user)) { + // @todo i18n FIXME: use sprintf and add i18n. throw new OStatusShadowException($profile, "'$profile_url' is the profile for local user '{$user->nickname}'."); } @@ -1025,7 +1032,7 @@ class Ostatus_profile extends Memcached_DataObject return; } if (!common_valid_http_url($url)) { - throw new ServerException(sprintf(_m("Invalid avatar URL %s"), $url)); + throw new ServerException(sprintf(_m("Invalid avatar URL %s."), $url)); } if ($this->isGroup()) { @@ -1035,7 +1042,7 @@ class Ostatus_profile extends Memcached_DataObject } if (!$self) { throw new ServerException(sprintf( - _m("Tried to update avatar for unsaved remote profile %s"), + _m("Tried to update avatar for unsaved remote profile %s."), $this->uri)); } @@ -1043,7 +1050,7 @@ class Ostatus_profile extends Memcached_DataObject // ripped from oauthstore.php (for old OMB client) $temp_filename = tempnam(sys_get_temp_dir(), 'listener_avatar'); if (!copy($url, $temp_filename)) { - throw new ServerException(sprintf(_m("Unable to fetch avatar from %s"), $url)); + throw new ServerException(sprintf(_m("Unable to fetch avatar from %s."), $url)); } if ($this->isGroup()) { @@ -1058,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); @@ -1226,7 +1241,7 @@ class Ostatus_profile extends Memcached_DataObject if ($object->link && common_valid_http_url($object->link)) { return $object->link; } - throw new ServerException("No author ID URI found"); + throw new ServerException("No author ID URI found."); } /** @@ -1256,10 +1271,12 @@ class Ostatus_profile extends Memcached_DataObject $user = User::staticGet('uri', $homeuri); if ($user) { + // @todo i18n FIXME: add i18n. throw new Exception("Local user can't be referenced as remote."); } if (OStatusPlugin::localGroupFromUrl($homeuri)) { + // @todo i18n FIXME: add i18n. throw new Exception("Local group can't be referenced as remote."); } @@ -1311,7 +1328,8 @@ class Ostatus_profile extends Memcached_DataObject $oprofile->profile_id = $profile->insert(); if (!$oprofile->profile_id) { - throw new ServerException("Can't save local profile"); + // @todo i18n FIXME: add i18n. + throw new ServerException("Can't save local profile."); } } else { $group = new User_group(); @@ -1321,14 +1339,16 @@ class Ostatus_profile extends Memcached_DataObject $oprofile->group_id = $group->insert(); if (!$oprofile->group_id) { - throw new ServerException("Can't save local profile"); + // @todo i18n FIXME: add i18n. + throw new ServerException("Can't save local profile."); } } $ok = $oprofile->insert(); if (!$ok) { - throw new ServerException("Can't save OStatus profile"); + // @todo i18n FIXME: add i18n. + throw new ServerException("Can't save OStatus profile."); } $avatar = self::getActivityObjectAvatar($object, $hints); @@ -1586,6 +1606,7 @@ class Ostatus_profile extends Memcached_DataObject if ($uri !== false) { if (is_null($uri)) { // Negative cache entry + // @todo i18n FIXME: add i18n. throw new Exception('Not a valid webfinger address.'); } $oprofile = Ostatus_profile::staticGet('uri', $uri); @@ -1613,6 +1634,7 @@ class Ostatus_profile extends Memcached_DataObject // Save negative cache entry so we don't waste time looking it up again. // @fixme distinguish temporary failures? self::cacheSet(sprintf('ostatus_profile:webfinger:%s', $addr), null); + // @todo i18n FIXME: add i18n. throw new Exception('Not a valid webfinger address.'); } @@ -1694,7 +1716,8 @@ class Ostatus_profile extends Memcached_DataObject if (!$profile_id) { common_log_db_error($profile, 'INSERT', __FILE__); - throw new Exception("Couldn't save profile for '$addr'"); + // @todo i18n FIXME: add i18n and use sprintf for parameter. + throw new Exception("Couldn't save profile for '$addr'."); } $oprofile = new Ostatus_profile(); @@ -1712,13 +1735,15 @@ class Ostatus_profile extends Memcached_DataObject if (!$result) { common_log_db_error($oprofile, 'INSERT', __FILE__); - throw new Exception("Couldn't save ostatus_profile for '$addr'"); + // @todo i18n FIXME: add i18n and use sprintf for parameter. + throw new Exception("Couldn't save ostatus_profile for '$addr'."); } self::cacheSet(sprintf('ostatus_profile:webfinger:%s', $addr), $oprofile->uri); return $oprofile; } + // @todo i18n FIXME: add i18n and use sprintf for parameter. throw new Exception("Couldn't find a valid profile for '$addr'"); } @@ -1818,4 +1843,3 @@ class OStatusShadowException extends Exception parent::__construct($message); } } - diff --git a/plugins/OStatus/lib/discovery.php b/plugins/OStatus/lib/discovery.php index 7187c1f3e..04c672720 100644 --- a/plugins/OStatus/lib/discovery.php +++ b/plugins/OStatus/lib/discovery.php @@ -106,7 +106,8 @@ class Discovery } } - throw new Exception('Unable to find services for '. $id); + // @todo Needs i18n. + throw new Exception('Unable to find services for '. $id . '.'); } public static function getService($links, $service) { @@ -160,7 +161,7 @@ class Discovery_LRDD_Host_Meta implements Discovery_LRDD } else { $domain = parse_url($uri, PHP_URL_HOST); } - + $url = 'http://'. $domain .'/.well-known/host-meta'; $xrd = Discovery::fetchXrd($url); diff --git a/plugins/OStatus/lib/hubconfqueuehandler.php b/plugins/OStatus/lib/hubconfqueuehandler.php index c8e0b72fe..8219f8420 100644 --- a/plugins/OStatus/lib/hubconfqueuehandler.php +++ b/plugins/OStatus/lib/hubconfqueuehandler.php @@ -51,4 +51,3 @@ class HubConfQueueHandler extends QueueHandler return true; } } - diff --git a/plugins/OStatus/lib/magicenvelope.php b/plugins/OStatus/lib/magicenvelope.php index 967e5f6d1..bbd4ce17a 100644 --- a/plugins/OStatus/lib/magicenvelope.php +++ b/plugins/OStatus/lib/magicenvelope.php @@ -32,7 +32,7 @@ class MagicEnvelope const ENCODING = 'base64url'; const NS = 'http://salmon-protocol.org/ns/magic-env'; - + private function normalizeUser($user_id) { if (substr($user_id, 0, 5) == 'http:' || @@ -70,13 +70,13 @@ class MagicEnvelope $keypair = $parts[1]; } } - + if ($keypair) { return $keypair; } } } - throw new Exception('Unable to locate signer public key'); + throw new Exception('Unable to locate signer public key.'); } @@ -92,8 +92,7 @@ class MagicEnvelope 'sig' => $signature_alg->sign($armored_text), 'alg' => $signature_alg->getName() ); - - + } public function toXML($env) { @@ -105,13 +104,13 @@ class MagicEnvelope $xs->element('me:alg', null, $env['alg']); $xs->element('me:sig', null, $env['sig']); $xs->elementEnd('me:env'); - + $string = $xs->getString(); common_debug($string); return $string; } - + public function unfold($env) { $dom = new DOMDocument(); @@ -137,7 +136,7 @@ class MagicEnvelope return $dom->saveXML(); } - + public function getAuthor($text) { $doc = new DOMDocument(); if (!$doc->loadXML($text)) { @@ -154,12 +153,12 @@ class MagicEnvelope } } } - + public function checkAuthor($text, $signer_uri) { return ($this->getAuthor($text) == $signer_uri); } - + public function verify($env) { if ($env['alg'] != 'RSA-SHA256') { @@ -181,14 +180,14 @@ class MagicEnvelope common_log(LOG_DEBUG, "Salmon error: ".$e->getMessage()); return false; } - + $verifier = Magicsig::fromString($keypair); if (!$verifier) { common_log(LOG_DEBUG, "Salmon error: unable to parse keypair"); return false; } - + return $verifier->verify($env['data'], $env['sig']); } diff --git a/plugins/OStatus/lib/ostatusqueuehandler.php b/plugins/OStatus/lib/ostatusqueuehandler.php index 5e318116a..9814cab9f 100644 --- a/plugins/OStatus/lib/ostatusqueuehandler.php +++ b/plugins/OStatus/lib/ostatusqueuehandler.php @@ -223,4 +223,3 @@ class OStatusQueueHandler extends QueueHandler } } - diff --git a/plugins/OStatus/lib/salmon.php b/plugins/OStatus/lib/salmon.php index ef7719a40..631ebc7d8 100644 --- a/plugins/OStatus/lib/salmon.php +++ b/plugins/OStatus/lib/salmon.php @@ -31,10 +31,10 @@ class Salmon const REL_SALMON = 'salmon'; const REL_MENTIONED = 'mentioned'; - // XXX: these are deprecated + // XXX: these are deprecated const NS_REPLIES = "http://salmon-protocol.org/ns/salmon-replies"; const NS_MENTIONS = "http://salmon-protocol.org/ns/salmon-mention"; - + /** * Sign and post the given Atom entry as a Salmon message. * @@ -87,9 +87,10 @@ class Salmon // No keypair yet, let's generate one. $magickey = new Magicsig(); $magickey->generate($user->id); - } + } } else { - throw new Exception("Salmon invalid actor for signing"); + // @todo i18n FIXME: added i18n and use sprintf when using parameters. + throw new Exception("Salmon invalid actor for signing."); } try { @@ -104,7 +105,7 @@ class Salmon public function verifyMagicEnv($text) { $magic_env = new MagicEnvelope(); - + $env = $magic_env->parse($text); return $magic_env->verify($env); diff --git a/plugins/OStatus/lib/salmonaction.php b/plugins/OStatus/lib/salmonaction.php index 9d6c6b269..5fdb11abe 100644 --- a/plugins/OStatus/lib/salmonaction.php +++ b/plugins/OStatus/lib/salmonaction.php @@ -42,7 +42,7 @@ class SalmonAction extends Action } if (empty($_SERVER['CONTENT_TYPE']) || $_SERVER['CONTENT_TYPE'] != 'application/magic-envelope+xml') { - $this->clientError(_m('Salmon requires application/magic-envelope+xml')); + $this->clientError(_m('Salmon requires "application/magic-envelope+xml".')); } $xml = file_get_contents('php://input'); diff --git a/plugins/OStatus/lib/salmonqueuehandler.php b/plugins/OStatus/lib/salmonqueuehandler.php index 7eeb5f8e9..56d3c9eff 100644 --- a/plugins/OStatus/lib/salmonqueuehandler.php +++ b/plugins/OStatus/lib/salmonqueuehandler.php @@ -36,7 +36,7 @@ class SalmonQueueHandler extends QueueHandler assert(is_string($data['entry'])); $actor = Profile::staticGet($data['actor']); - + $salmon = new Salmon(); $salmon->post($data['salmonuri'], $data['entry'], $actor); diff --git a/plugins/OStatus/lib/xrd.php b/plugins/OStatus/lib/xrd.php index a10b9f427..145cd64cb 100644 --- a/plugins/OStatus/lib/xrd.php +++ b/plugins/OStatus/lib/xrd.php @@ -31,11 +31,11 @@ class XRD { const XML_NS = 'http://www.w3.org/2000/xmlns/'; - + const XRD_NS = 'http://docs.oasis-open.org/ns/xri/xrd-1.0'; const HOST_META_NS = 'http://host-meta.net/xrd/1.0'; - + public $expires; public $subject; @@ -43,11 +43,11 @@ class XRD public $host; public $alias = array(); - + public $types = array(); - + public $links = array(); - + public static function parse($xml) { $xrd = new XRD(); @@ -61,11 +61,11 @@ class XRD error_reporting($old); if (!$ok) { - throw new Exception("Invalid XML"); + throw new Exception("Invalid XML."); } $xrd_element = $dom->getElementsByTagName('XRD')->item(0); if (!$xrd_element) { - throw new Exception("Invalid XML, missing XRD root"); + throw new Exception("Invalid XML, missing XRD root."); } // Check for host-meta host @@ -86,7 +86,7 @@ class XRD case 'Subject': $xrd->subject = $node->nodeValue; break; - + case 'Alias': $xrd->alias[] = $node->nodeValue; break; @@ -114,7 +114,7 @@ class XRD if ($this->host) { $xs->element('hm:Host', array('xmlns:hm' => XRD::HOST_META_NS), $this->host); } - + if ($this->expires) { $xs->element('Expires', null, $this->expires); } @@ -139,7 +139,7 @@ class XRD } $xs->elementEnd('Link'); } - + $xs->elementEnd('XRD'); return $xs->getString(); @@ -149,7 +149,7 @@ class XRD { return array(); } - + function parseLink($element) { $link = array(); @@ -169,4 +169,3 @@ class XRD return $link; } } - diff --git a/plugins/OStatus/lib/xrdaction.php b/plugins/OStatus/lib/xrdaction.php index d8cf648d6..91bb87cc2 100644 --- a/plugins/OStatus/lib/xrdaction.php +++ b/plugins/OStatus/lib/xrdaction.php @@ -26,13 +26,12 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } class XrdAction extends Action { - public $uri; - + public $user; public $xrd; - + function handle() { $nick = $this->user->nickname; diff --git a/plugins/OStatus/scripts/fixup-shadow.php b/plugins/OStatus/scripts/fixup-shadow.php index 6522ca240..4b6ad08a3 100644 --- a/plugins/OStatus/scripts/fixup-shadow.php +++ b/plugins/OStatus/scripts/fixup-shadow.php @@ -77,7 +77,7 @@ while ($oprofile->fetch()) { echo "$uri matched query, but we don't recognize it.\n"; continue; } - + if ($dry) { echo " - skipping\n"; } else { @@ -93,4 +93,3 @@ if ($count && $dry) { } else { echo "done.\n"; } - diff --git a/plugins/OStatus/scripts/testfeed.php b/plugins/OStatus/scripts/testfeed.php index 5e3ccd433..82a1c6586 100644 --- a/plugins/OStatus/scripts/testfeed.php +++ b/plugins/OStatus/scripts/testfeed.php @@ -86,4 +86,3 @@ if ($skip || $count) { } Event::handle('StartFeedSubReceive', array($sub, $feed)); - diff --git a/plugins/OStatus/scripts/updateostatus.php b/plugins/OStatus/scripts/updateostatus.php index ff0d86d37..052cca146 100644 --- a/plugins/OStatus/scripts/updateostatus.php +++ b/plugins/OStatus/scripts/updateostatus.php @@ -49,7 +49,7 @@ try { $nickname = get_option_value('n', 'nickname'); $user = User::staticGet('nickname', $nickname); if (empty($user)) { - throw new Exception("Can't find user with nickname '$nickname'"); + throw new Exception("Can't find user with nickname '$nickname'."); } updateOStatus($user); } else if (have_option('a', 'all')) { diff --git a/plugins/OStatus/tests/FeedDiscoveryTest.php b/plugins/OStatus/tests/FeedDiscoveryTest.php index 0e6354a86..3be4bf736 100644 --- a/plugins/OStatus/tests/FeedDiscoveryTest.php +++ b/plugins/OStatus/tests/FeedDiscoveryTest.php @@ -53,7 +53,7 @@ class FeedDiscoveryTest extends PHPUnit_Framework_TestCase </style> <link rel="EditURI" type="application/rsd+xml" title="RSD" href="http://leuksman.com/log/xmlrpc.php?rsd" /> -<link rel="wlwmanifest" type="application/wlwmanifest+xml" href="http://leuksman.com/log/wp-includes/wlwmanifest.xml" /> +<link rel="wlwmanifest" type="application/wlwmanifest+xml" href="http://leuksman.com/log/wp-includes/wlwmanifest.xml" /> <link rel='index' title='leÅksman' href='http://leuksman.com/log' /> <meta name="generator" content="WordPress 2.8.6" /> </head> diff --git a/plugins/OStatus/tests/remote-tests.php b/plugins/OStatus/tests/remote-tests.php index 24b4b1660..c2c9a5d97 100644 --- a/plugins/OStatus/tests/remote-tests.php +++ b/plugins/OStatus/tests/remote-tests.php @@ -500,7 +500,7 @@ class SNTestClient extends TestBase $me = $this->getProfileUri(); return $this->checkSubscription($profile_uri, $me); } - + protected function checkSubscription($subscriber, $subscribed) { // Using FOAF as the API methods for checking the social graph @@ -552,4 +552,3 @@ $b = $args[1]; $tester = new OStatusTester($a, $b); $tester->run(); - 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/TwitterBridgePlugin.php b/plugins/TwitterBridge/TwitterBridgePlugin.php index 8e3eba318..21a10775d 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,183 @@ 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))); + + // 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); + } + } + + return true; + } + + /** + * If a notice gets deleted, remove the Notice_to_status mapping + * + * @param Notice $notice The notice getting deleted + * + * @return boolean hook value + */ + function onNoticeDeleteRelated($notice) + { + $n2s = Notice_to_status::staticGet('notice_id', $notice->id); + + if (!empty($n2s)) { + + $user = common_current_user(); + + if (empty($user) || $user->id != $notice->profile_id) { + $this->log(LOG_INFO, "Skipping deleting notice for {$notice->id} since it doesn't seem to be by the author."); + return true; + } + + $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 7c624fdb3..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->statusesFriendsTimeline(); + $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/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 2c18c9469..000000000 --- a/plugins/TwitterBridge/twitterbasicauthclient.php +++ /dev/null @@ -1,229 +0,0 @@ -<?php -/** - * StatusNet, the distributed open-source microblogging tool - * - * Class for doing OAuth calls against Twitter - * - * PHP version 5 - * - * LICENCE: This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * - * @category Integration - * @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/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 d895d8c73..876e30425 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; } @@ -232,17 +238,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; } @@ -262,19 +276,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($id)) { + $params['id'] = $id; + } + + if (!empty($user_id)) { + $params['user_id'] = $user_id; + } + + if (!empty($screen_name)) { + $params['screen_name'] = $screen_name; + } - if (!empty($qry)) { - $url .= "?$qry"; + if (!empty($page)) { + $params['page'] = $page; } - $response = $this->oAuthGet($url); + $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; + } } |