diff options
48 files changed, 1545 insertions, 969 deletions
@@ -940,6 +940,8 @@ closed: If set to 'true', will disallow registration on your site. the service, *then* set this variable to 'true'. inviteonly: If set to 'true', will only allow registration if the user was invited by an existing user. +openidonly: If set to 'true', will only allow registrations and logins + through OpenID. private: If set to 'true', anonymous users will be redirected to the 'login' page. Also, API methods that normally require no authentication will require it. Note that this does not turn diff --git a/actions/all.php b/actions/all.php index f06ead2a8..5db09a0e6 100644 --- a/actions/all.php +++ b/actions/all.php @@ -88,7 +88,9 @@ class AllAction extends ProfileAction } } else { - $message .= sprintf(_('Why not [register an account](%%%%action.register%%%%) and then nudge %s or post a notice to his or her attention.'), $this->user->nickname); + $message .= sprintf(_('Why not [register an account](%%%%action.%s%%%%) and then nudge %s or post a notice to his or her attention.'), + (!common_config('site','openidonly')) ? 'register' : 'openidlogin', + $this->user->nickname); } $this->elementStart('div', 'guide'); diff --git a/actions/confirmaddress.php b/actions/confirmaddress.php index 725c1f1e3..3c41a5c70 100644 --- a/actions/confirmaddress.php +++ b/actions/confirmaddress.php @@ -67,7 +67,11 @@ class ConfirmaddressAction extends Action parent::handle($args); if (!common_logged_in()) { common_set_returnto($this->selfUrl()); - common_redirect(common_local_url('login')); + if (!common_config('site', 'openidonly')) { + common_redirect(common_local_url('login')); + } else { + common_redirect(common_local_url('openidlogin')); + } return; } $code = $this->trimmed('code'); diff --git a/actions/favorited.php b/actions/favorited.php index 156c7a700..a3d1a5e20 100644 --- a/actions/favorited.php +++ b/actions/favorited.php @@ -153,7 +153,8 @@ class FavoritedAction extends Action $message .= _('Be the first to add a notice to your favorites by clicking the fave button next to any notice you like.'); } else { - $message .= _('Why not [register an account](%%action.register%%) and be the first to add a notice to your favorites!'); + $message .= sprintf(_('Why not [register an account](%%%%action.%s%%%%) and be the first to add a notice to your favorites!'), + (!common_config('site','openidonly')) ? 'register' : 'openidlogin'); } $this->elementStart('div', 'guide'); diff --git a/actions/groupsearch.php b/actions/groupsearch.php index c50466ce6..7437166e6 100644 --- a/actions/groupsearch.php +++ b/actions/groupsearch.php @@ -82,7 +82,8 @@ class GroupsearchAction extends SearchAction $message = _('If you can\'t find the group you\'re looking for, you can [create it](%%action.newgroup%%) yourself.'); } else { - $message = _('Why not [register an account](%%action.register%%) and [create the group](%%action.newgroup%%) yourself!'); + $message = sprintf(_('Why not [register an account](%%%%action.%s%%%%) and [create the group](%%%%action.newgroup%%%%) yourself!'), + (!common_config('site','openidonly')) ? 'register' : 'openidlogin'); } $this->elementStart('div', 'guide'); $this->raw(common_markup_to_html($message)); diff --git a/actions/invite.php b/actions/invite.php index 26c951ed2..bdc0d34cb 100644 --- a/actions/invite.php +++ b/actions/invite.php @@ -235,7 +235,7 @@ class InviteAction extends CurrentUserDesignAction common_root_url(), $personal, common_local_url('showstream', array('nickname' => $user->nickname)), - common_local_url('register', array('code' => $invite->code))); + common_local_url((!common_config('site', 'openidonly')) ? 'register' : 'openidlogin', array('code' => $invite->code))); mail_send($recipients, $headers, $body); } diff --git a/actions/login.php b/actions/login.php index 50de83f6f..c20854f15 100644 --- a/actions/login.php +++ b/actions/login.php @@ -65,6 +65,8 @@ class LoginAction extends Action * * Switches on request method; either shows the form or handles its input. * + * Checks if only OpenID is allowed and redirects to openidlogin if so. + * * @param array $args $_REQUEST data * * @return void @@ -73,7 +75,9 @@ class LoginAction extends Action function handle($args) { parent::handle($args); - if (common_is_real_login()) { + if (common_config('site', 'openidonly')) { + common_redirect(common_local_url('openidlogin')); + } else if (common_is_real_login()) { $this->clientError(_('Already logged in.')); } else if ($_SERVER['REQUEST_METHOD'] == 'POST') { $this->checkLogin(); diff --git a/actions/noticesearch.php b/actions/noticesearch.php index 49b473d9e..90b3309cf 100644 --- a/actions/noticesearch.php +++ b/actions/noticesearch.php @@ -121,7 +121,9 @@ class NoticesearchAction extends SearchAction $message = sprintf(_('Be the first to [post on this topic](%%%%action.newnotice%%%%?status_textarea=%s)!'), urlencode($q)); } else { - $message = sprintf(_('Why not [register an account](%%%%action.register%%%%) and be the first to [post on this topic](%%%%action.newnotice%%%%?status_textarea=%s)!'), urlencode($q)); + $message = sprintf(_('Why not [register an account](%%%%action.%s%%%%) and be the first to [post on this topic](%%%%action.newnotice%%%%?status_textarea=%s)!'), + (!common_config('site','openidonly')) ? 'register' : 'openidlogin', + urlencode($q)); } $this->elementStart('div', 'guide'); diff --git a/actions/public.php b/actions/public.php index d0317ac70..dd128925b 100644 --- a/actions/public.php +++ b/actions/public.php @@ -183,7 +183,8 @@ class PublicAction extends Action } else { if (! (common_config('site','closed') || common_config('site','inviteonly'))) { - $message .= _('Why not [register an account](%%action.register%%) and be the first to post!'); + $message .= sprintf(_('Why not [register an account](%%%%action.%s%%%%) and be the first to post!'), + (!common_config('site','openidonly')) ? 'register' : 'openidlogin'); } } @@ -238,9 +239,11 @@ class PublicAction extends Action function showAnonymousMessage() { if (! (common_config('site','closed') || common_config('site','inviteonly'))) { - $m = _('This is %%site.name%%, a [micro-blogging](http://en.wikipedia.org/wiki/Micro-blogging) service ' . - 'based on the Free Software [Laconica](http://laconi.ca/) tool. ' . - '[Join now](%%action.register%%) to share notices about yourself with friends, family, and colleagues! ([Read more](%%doc.help%%))'); + $m = sprintf(_('This is %%%%site.name%%%%, a [micro-blogging](http://en.wikipedia.org/wiki/Micro-blogging) service ' . + 'based on the Free Software [Laconica](http://laconi.ca/) tool. ' . + '[Join now](%%%%action.%s%%%%) to share notices about yourself with friends, family, and colleagues! ' . + '([Read more](%%%%doc.help%%%%))'), + (!common_config('site','openidonly')) ? 'register' : 'openidlogin'); } else { $m = _('This is %%site.name%%, a [micro-blogging](http://en.wikipedia.org/wiki/Micro-blogging) service ' . 'based on the Free Software [Laconica](http://laconi.ca/) tool.'); diff --git a/actions/publictagcloud.php b/actions/publictagcloud.php index e9f33d58b..a2772869d 100644 --- a/actions/publictagcloud.php +++ b/actions/publictagcloud.php @@ -72,7 +72,8 @@ class PublictagcloudAction extends Action $message .= _('Be the first to post one!'); } else { - $message .= _('Why not [register an account](%%action.register%%) and be the first to post one!'); + $message .= sprintf(_('Why not [register an account](%%%%action.%s%%%%) and be the first to post one!'), + (!common_config('site','openidonly')) ? 'register' : 'openidlogin'); } $this->elementStart('div', 'guide'); diff --git a/actions/register.php b/actions/register.php index dcbbbdb6a..046a76b80 100644 --- a/actions/register.php +++ b/actions/register.php @@ -116,6 +116,8 @@ class RegisterAction extends Action * * Checks if registration is closed and shows an error if so. * + * Checks if only OpenID is allowed and redirects to openidlogin if so. + * * @param array $args $_REQUEST data * * @return void @@ -127,6 +129,8 @@ class RegisterAction extends Action if (common_config('site', 'closed')) { $this->clientError(_('Registration not allowed.')); + } else if (common_config('site', 'openidonly')) { + common_redirect(common_local_url('openidlogin')); } else if (common_logged_in()) { $this->clientError(_('Already logged in.')); } else if ($_SERVER['REQUEST_METHOD'] == 'POST') { diff --git a/actions/remotesubscribe.php b/actions/remotesubscribe.php index e658f8d37..7323103fc 100644 --- a/actions/remotesubscribe.php +++ b/actions/remotesubscribe.php @@ -71,11 +71,13 @@ class RemotesubscribeAction extends Action if ($this->err) { $this->element('div', 'error', $this->err); } else { - $inst = _('To subscribe, you can [login](%%action.login%%),' . - ' or [register](%%action.register%%) a new ' . - ' account. If you already have an account ' . - ' on a [compatible microblogging site](%%doc.openmublog%%), ' . - ' enter your profile URL below.'); + $inst = sprintf(_('To subscribe, you can [login](%%%%action.%s%%%%),' . + ' or [register](%%%%action.%s%%%%) a new ' . + ' account. If you already have an account ' . + ' on a [compatible microblogging site](%%doc.openmublog%%), ' . + ' enter your profile URL below.'), + (!common_config('site','openidonly')) ? 'login' : 'openidlogin', + (!common_config('site','openidonly')) ? 'register' : 'openidlogin'); $output = common_markup_to_html($inst); $this->elementStart('div', 'instructions'); $this->raw($output); diff --git a/actions/replies.php b/actions/replies.php index d7ed440e9..f14383d33 100644 --- a/actions/replies.php +++ b/actions/replies.php @@ -187,7 +187,9 @@ class RepliesAction extends OwnerDesignAction } } else { - $message .= sprintf(_('Why not [register an account](%%%%action.register%%%%) and then nudge %s or post a notice to his or her attention.'), $this->user->nickname); + $message .= sprintf(_('Why not [register an account](%%%%action.%s%%%%) and then nudge %s or post a notice to his or her attention.'), + (!common_config('site','openidonly')) ? 'register' : 'openidlogin', + $this->user->nickname); } $this->elementStart('div', 'guide'); diff --git a/actions/showfavorites.php b/actions/showfavorites.php index 8efe9d30a..9f549baf2 100644 --- a/actions/showfavorites.php +++ b/actions/showfavorites.php @@ -173,7 +173,9 @@ class ShowfavoritesAction extends OwnerDesignAction } } else { - $message = sprintf(_('%s hasn\'t added any notices to his favorites yet. Why not [register an account](%%%%action.register%%%%) and then post something interesting they would add to thier favorites :)'), $this->user->nickname); + $message = sprintf(_('%s hasn\'t added any notices to his favorites yet. Why not [register an account](%%%%action.%s%%%%) and then post something interesting they would add to their favorites :)'), + $this->user->nickname, + (!common_config('site','openidonly')) ? 'register' : 'openidlogin'); } $this->elementStart('div', 'guide'); diff --git a/actions/showgroup.php b/actions/showgroup.php index 32ec674a9..4d8ba5fa8 100644 --- a/actions/showgroup.php +++ b/actions/showgroup.php @@ -440,8 +440,9 @@ class ShowgroupAction extends GroupDesignAction $m = sprintf(_('**%s** is a user group on %%%%site.name%%%%, a [micro-blogging](http://en.wikipedia.org/wiki/Micro-blogging) service ' . 'based on the Free Software [Laconica](http://laconi.ca/) tool. Its members share ' . 'short messages about their life and interests. '. - '[Join now](%%%%action.register%%%%) to become part of this group and many more! ([Read more](%%%%doc.help%%%%))'), - $this->group->nickname); + '[Join now](%%%%action.%s%%%%) to become part of this group and many more! ([Read more](%%%%doc.help%%%%))'), + $this->group->nickname, + (!common_config('site','openidonly')) ? 'register' : 'openidlogin'); } else { $m = sprintf(_('**%s** is a user group on %%%%site.name%%%%, a [micro-blogging](http://en.wikipedia.org/wiki/Micro-blogging) service ' . 'based on the Free Software [Laconica](http://laconi.ca/) tool. Its members share ' . diff --git a/actions/shownotice.php b/actions/shownotice.php index 8f73dc824..4b179bc72 100644 --- a/actions/shownotice.php +++ b/actions/shownotice.php @@ -97,8 +97,8 @@ class ShownoticeAction extends OwnerDesignAction $this->user = User::staticGet('id', $this->profile->id); - if (empty($this->user)) { - $this->serverError(_('Not a local notice'), 500); + if (! $this->notice->is_local) { + common_redirect($this->notice->uri); return false; } diff --git a/actions/showstream.php b/actions/showstream.php index cd5d4bb70..3f603d64f 100644 --- a/actions/showstream.php +++ b/actions/showstream.php @@ -358,7 +358,9 @@ class ShowstreamAction extends ProfileAction } } else { - $message .= sprintf(_('Why not [register an account](%%%%action.register%%%%) and then nudge %s or post a notice to his or her attention.'), $this->user->nickname); + $message .= sprintf(_('Why not [register an account](%%%%action.%s%%%%) and then nudge %s or post a notice to his or her attention.'), + (!common_config('site','openidonly')) ? 'register' : 'openidlogin', + $this->user->nickname); } $this->elementStart('div', 'guide'); @@ -387,8 +389,10 @@ class ShowstreamAction extends ProfileAction if (!(common_config('site','closed') || common_config('site','inviteonly'))) { $m = sprintf(_('**%s** has an account on %%%%site.name%%%%, a [micro-blogging](http://en.wikipedia.org/wiki/Micro-blogging) service ' . 'based on the Free Software [Laconica](http://laconi.ca/) tool. ' . - '[Join now](%%%%action.register%%%%) to follow **%s**\'s notices and many more! ([Read more](%%%%doc.help%%%%))'), - $this->user->nickname, $this->user->nickname); + '[Join now](%%%%action.%s%%%%) to follow **%s**\'s notices and many more! ([Read more](%%%%doc.help%%%%))'), + $this->user->nickname, + (!common_config('site','openidonly')) ? 'register' : 'openidlogin', + $this->user->nickname); } else { $m = sprintf(_('**%s** has an account on %%%%site.name%%%%, a [micro-blogging](http://en.wikipedia.org/wiki/Micro-blogging) service ' . 'based on the Free Software [Laconica](http://laconi.ca/) tool. '), diff --git a/actions/subscribers.php b/actions/subscribers.php index 66ac00fb1..404738012 100644 --- a/actions/subscribers.php +++ b/actions/subscribers.php @@ -111,7 +111,9 @@ class SubscribersAction extends GalleryAction } } else { - $message = sprintf(_('%s has no subscribers. Why not [register an account](%%%%action.register%%%%) and be the first?'), $this->user->nickname); + $message = sprintf(_('%s has no subscribers. Why not [register an account](%%%%action.%s%%%%) and be the first?'), + $this->user->nickname, + (!common_config('site','openidonly')) ? 'register' : 'openidlogin'); } $this->elementStart('div', 'guide'); diff --git a/actions/twitterauthorization.php b/actions/twitterauthorization.php new file mode 100644 index 000000000..b04f35327 --- /dev/null +++ b/actions/twitterauthorization.php @@ -0,0 +1,222 @@ +<?php +/** + * Laconica, the distributed open-source microblogging tool + * + * Class for doing OAuth authentication against Twitter + * + * PHP version 5 + * + * LICENCE: This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @category Twitter + * @package Laconica + * @author Zach Copely <zach@controlyourself.ca> + * @copyright 2009 Control Yourself, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ + +if (!defined('LACONICA')) { + exit(1); +} + +/** + * Class for doing OAuth authentication against Twitter + * + * Peforms the OAuth "dance" between Laconica and Twitter -- requests a token, + * authorizes it, and exchanges it for an access token. It also creates a link + * (Foreign_link) between the Laconica user and Twitter user and stores the + * access token and secret in the link. + * + * @category Twitter + * @package Laconica + * @author Zach Copley <zach@controlyourself.ca> + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + * + */ +class TwitterauthorizationAction extends Action +{ + /** + * Initialize class members. Looks for 'oauth_token' parameter. + * + * @param array $args misc. arguments + * + * @return boolean true + */ + function prepare($args) + { + parent::prepare($args); + + $this->oauth_token = $this->arg('oauth_token'); + + return true; + } + + /** + * Handler method + * + * @param array $args is ignored since it's now passed in in prepare() + * + * @return nothing + */ + function handle($args) + { + parent::handle($args); + + if (!common_logged_in()) { + $this->clientError(_('Not logged in.'), 403); + } + + $user = common_current_user(); + $flink = Foreign_link::getByUserID($user->id, TWITTER_SERVICE); + + // If there's already a foreign link record, it means we already + // have an access token, and this is unecessary. So go back. + + if (isset($flink)) { + common_redirect(common_local_url('twittersettings')); + } + + // $this->oauth_token is only populated once Twitter authorizes our + // request token. If it's empty we're at the beginning of the auth + // process + + if (empty($this->oauth_token)) { + $this->authorizeRequestToken(); + } else { + $this->saveAccessToken(); + } + } + + /** + * Asks Twitter for a request token, and then redirects to Twitter + * to authorize it. + * + * @return nothing + */ + function authorizeRequestToken() + { + try { + + // Get a new request token and authorize it + + $client = new TwitterOAuthClient(); + $req_tok = + $client->getRequestToken(TwitterOAuthClient::$requestTokenURL); + + // Sock the request token away in the session temporarily + + $_SESSION['twitter_request_token'] = $req_tok->key; + $_SESSION['twitter_request_token_secret'] = $req_tok->secret; + + $auth_link = $client->getAuthorizeLink($req_tok); + + } catch (TwitterOAuthClientException $e) { + $msg = sprintf('OAuth client cURL error - code: %1s, msg: %2s', + $e->getCode(), $e->getMessage()); + $this->serverError(_('Couldn\'t link your Twitter account.')); + } + + common_redirect($auth_link); + } + + /** + * Called when Twitter returns an authorized request token. Exchanges + * it for an access token and stores it. + * + * @return nothing + */ + function saveAccessToken() + { + + // Check to make sure Twitter returned the same request + // token we sent them + + if ($_SESSION['twitter_request_token'] != $this->oauth_token) { + $this->serverError(_('Couldn\'t link your Twitter account.')); + } + + try { + + $client = new TwitterOAuthClient($_SESSION['twitter_request_token'], + $_SESSION['twitter_request_token_secret']); + + // Exchange the request token for an access token + + $atok = $client->getAccessToken(TwitterOAuthClient::$accessTokenURL); + + // Test the access token and get the user's Twitter info + + $client = new TwitterOAuthClient($atok->key, $atok->secret); + $twitter_user = $client->verifyCredentials(); + + } catch (OAuthClientException $e) { + $msg = sprintf('OAuth client cURL error - code: %1$s, msg: %2$s', + $e->getCode(), $e->getMessage()); + $this->serverError(_('Couldn\'t link your Twitter account.')); + } + + // Save the access token and Twitter user info + + $this->saveForeignLink($atok, $twitter_user); + + // Clean up the the mess we made in the session + + unset($_SESSION['twitter_request_token']); + unset($_SESSION['twitter_request_token_secret']); + + common_redirect(common_local_url('twittersettings')); + } + + /** + * Saves a Foreign_link between Twitter user and local user, + * which includes the access token and secret. + * + * @param OAuthToken $access_token the access token to save + * @param mixed $twitter_user twitter API user object + * + * @return nothing + */ + function saveForeignLink($access_token, $twitter_user) + { + $user = common_current_user(); + + $flink = new Foreign_link(); + + $flink->user_id = $user->id; + $flink->foreign_id = $twitter_user->id; + $flink->service = TWITTER_SERVICE; + + $creds = TwitterOAuthClient::packToken($access_token); + + $flink->credentials = $creds; + $flink->created = common_sql_now(); + + // Defaults: noticesync on, everything else off + + $flink->set_flags(true, false, false, false); + + $flink_id = $flink->insert(); + + if (empty($flink_id)) { + common_log_db_error($flink, 'INSERT', __FILE__); + $this->serverError(_('Couldn\'t link your Twitter account.')); + } + + save_twitter_user($twitter_user->id, $twitter_user->screen_name); + } + +} + diff --git a/actions/twittersettings.php b/actions/twittersettings.php index 3343dba95..0859ab9d3 100644 --- a/actions/twittersettings.php +++ b/actions/twittersettings.php @@ -34,8 +34,6 @@ if (!defined('LACONICA')) { require_once INSTALLDIR.'/lib/connectsettingsaction.php'; require_once INSTALLDIR.'/lib/twitter.php'; -define('SUBSCRIPTIONS', 80); - /** * Settings for Twitter integration * @@ -69,9 +67,8 @@ class TwittersettingsAction extends ConnectSettingsAction function getInstructions() { - return _('Add your Twitter account to automatically send '. - ' your notices to Twitter, ' . - 'and subscribe to Twitter friends already here.'); + return _('Connect your Twitter account to share your updates ' . + 'with your Twitter friends and vice-versa.'); } /** @@ -99,7 +96,7 @@ class TwittersettingsAction extends ConnectSettingsAction $flink = Foreign_link::getByUserID($user->id, TWITTER_SERVICE); - if ($flink) { + if (!empty($flink)) { $fuser = $flink->getForeignUser(); } @@ -108,192 +105,86 @@ class TwittersettingsAction extends ConnectSettingsAction 'class' => 'form_settings', 'action' => common_local_url('twittersettings'))); - $this->elementStart('fieldset', array('id' => 'settings_twitter_account')); - $this->element('legend', null, _('Twitter Account')); + $this->hidden('token', common_session_token()); - if ($fuser) { + + $this->elementStart('fieldset', array('id' => 'settings_twitter_account')); + + if (empty($fuser)) { $this->elementStart('ul', 'form_data'); - $this->elementStart('li', array('id' => 'settings_twitter_remove')); - $this->element('span', 'twitter_user', $fuser->nickname); - $this->element('a', array('href' => $fuser->uri), $fuser->uri); - $this->element('p', 'form_note', - _('Current verified Twitter account.')); - $this->hidden('flink_foreign_id', $flink->foreign_id); + $this->elementStart('li', array('id' => 'settings_twitter_login_button')); + $this->element('a', array('href' => common_local_url('twitterauthorization')), + 'Connect my Twitter account'); $this->elementEnd('li'); $this->elementEnd('ul'); - $this->submit('remove', _('Remove')); + + $this->elementEnd('fieldset'); } else { + $this->element('legend', null, _('Twitter account')); + $this->elementStart('p', array('id' => 'form_confirmed')); + $this->element('a', array('href' => $fuser->uri), $fuser->nickname); + $this->elementEnd('p'); + $this->element('p', 'form_note', + _('Connected Twitter account')); + + $this->submit('remove', _('Remove')); + + $this->elementEnd('fieldset'); + + $this->elementStart('fieldset', array('id' => 'settings_twitter_preferences')); + + $this->element('legend', null, _('Preferences')); $this->elementStart('ul', 'form_data'); - $this->elementStart('li', array('id' => 'settings_twitter_login')); - $this->input('twitter_username', _('Twitter user name'), - ($this->arg('twitter_username')) ? - $this->arg('twitter_username') : - $profile->nickname, - _('No spaces, please.')); // hey, it's what Twitter says + $this->elementStart('li'); + $this->checkbox('noticesend', + _('Automatically send my notices to Twitter.'), + ($flink) ? + ($flink->noticesync & FOREIGN_NOTICE_SEND) : + true); $this->elementEnd('li'); $this->elementStart('li'); - $this->password('twitter_password', _('Twitter password')); - $this->elementend('li'); - $this->elementEnd('ul'); - } - $this->elementEnd('fieldset'); - - $this->elementStart('fieldset', - array('id' => 'settings_twitter_preferences')); - $this->element('legend', null, _('Preferences')); - - $this->elementStart('ul', 'form_data'); - $this->elementStart('li'); - $this->checkbox('noticesend', - _('Automatically send my notices to Twitter.'), - ($flink) ? - ($flink->noticesync & FOREIGN_NOTICE_SEND) : - true); - $this->elementEnd('li'); - $this->elementStart('li'); - $this->checkbox('replysync', - _('Send local "@" replies to Twitter.'), - ($flink) ? - ($flink->noticesync & FOREIGN_NOTICE_SEND_REPLY) : - true); - $this->elementEnd('li'); - $this->elementStart('li'); - $this->checkbox('friendsync', - _('Subscribe to my Twitter friends here.'), - ($flink) ? - ($flink->friendsync & FOREIGN_FRIEND_RECV) : - false); - $this->elementEnd('li'); - - if (common_config('twitterbridge','enabled')) { + $this->checkbox('replysync', + _('Send local "@" replies to Twitter.'), + ($flink) ? + ($flink->noticesync & FOREIGN_NOTICE_SEND_REPLY) : + true); + $this->elementEnd('li'); $this->elementStart('li'); - $this->checkbox('noticerecv', - _('Import my Friends Timeline.'), + $this->checkbox('friendsync', + _('Subscribe to my Twitter friends here.'), ($flink) ? - ($flink->noticesync & FOREIGN_NOTICE_RECV) : + ($flink->friendsync & FOREIGN_FRIEND_RECV) : false); $this->elementEnd('li'); - } else { - // preserve setting even if bidrection bridge toggled off - if ($flink && ($flink->noticesync & FOREIGN_NOTICE_RECV)) { - $this->hidden('noticerecv', true, 'noticerecv'); - } - } - - $this->elementEnd('ul'); - - if ($flink) { - $this->submit('save', _('Save')); - } else { - $this->submit('add', _('Add')); - } - $this->elementEnd('fieldset'); - - $this->showTwitterSubscriptions(); - - $this->elementEnd('form'); - } - /** - * Gets some of the user's Twitter friends - * - * Gets the number of Twitter friends that are on this - * instance of Laconica. - * - * @return array array of User objects - */ - - function subscribedTwitterUsers() - { - - $current_user = common_current_user(); - - $qry = 'SELECT "user".* ' . - 'FROM subscription ' . - 'JOIN "user" ON subscription.subscribed = "user".id ' . - 'JOIN foreign_link ON foreign_link.user_id = "user".id ' . - 'WHERE subscriber = %d ' . - 'ORDER BY "user".nickname'; - - $user = new User(); - - $user->query(sprintf($qry, $current_user->id)); - - $users = array(); - - while ($user->fetch()) { - - // Don't include the user's own self-subscription - if ($user->id != $current_user->id) { - $users[] = clone($user); - } - } - - return $users; - } - - /** - * Show user's Twitter friends - * - * Gets the number of Twitter friends that are on this - * instance of Laconica, and shows their mini-avatars. - * - * @return void - */ - - function showTwitterSubscriptions() - { - - $friends = $this->subscribedTwitterUsers(); - - $friends_count = count($friends); - - if ($friends_count > 0) { - $this->elementStart('div', array('id' => 'entity_subscriptions', - 'class' => 'section')); - $this->element('h2', null, _('Twitter Friends')); - $this->elementStart('ul', 'entities users xoxo'); - - for ($i = 0; $i < min($friends_count, SUBSCRIPTIONS); $i++) { + if (common_config('twitterbridge','enabled')) { + $this->elementStart('li'); + $this->checkbox('noticerecv', + _('Import my Friends Timeline.'), + ($flink) ? + ($flink->noticesync & FOREIGN_NOTICE_RECV) : + false); + $this->elementEnd('li'); - $other = Profile::staticGet($friends[$i]->id); + // preserve setting even if bidrection bridge toggled off - if (!$other) { - common_log_db_error($subs, 'SELECT', __FILE__); - continue; + if ($flink && ($flink->noticesync & FOREIGN_NOTICE_RECV)) { + $this->hidden('noticerecv', true, 'noticerecv'); } - - $this->elementStart('li', 'vcard'); - $this->elementStart('a', array('title' => ($other->fullname) ? - $other->fullname : - $other->nickname, - 'href' => $other->profileurl, - 'class' => 'url')); - - $avatar = $other->getAvatar(AVATAR_MINI_SIZE); - - $avatar_url = ($avatar) ? - $avatar->displayUrl() : - Avatar::defaultImage(AVATAR_MINI_SIZE); - - $this->element('img', array('src' => $avatar_url, - 'width' => AVATAR_MINI_SIZE, - 'height' => AVATAR_MINI_SIZE, - 'class' => 'avatar photo', - 'alt' => ($other->fullname) ? - $other->fullname : - $other->nickname)); - - $this->element('span', 'fn nickname', $other->nickname); - $this->elementEnd('a'); - $this->elementEnd('li'); - } $this->elementEnd('ul'); - $this->elementEnd('div'); + if ($flink) { + $this->submit('save', _('Save')); + } else { + $this->submit('add', _('Add')); + } + + $this->elementEnd('fieldset'); } + + $this->elementEnd('form'); } /** @@ -309,7 +200,6 @@ class TwittersettingsAction extends ConnectSettingsAction function handlePost() { - // CSRF protection $token = $this->trimmed('token'); if (!$token || $token != common_session_token()) { @@ -320,8 +210,6 @@ class TwittersettingsAction extends ConnectSettingsAction if ($this->arg('save')) { $this->savePreferences(); - } else if ($this->arg('add')) { - $this->addTwitterAccount(); } else if ($this->arg('remove')) { $this->removeTwitterAccount(); } else { @@ -330,82 +218,6 @@ class TwittersettingsAction extends ConnectSettingsAction } /** - * Associate a Twitter account with the user's account - * - * Validates post input; verifies it against Twitter; and if - * successful stores in the database. - * - * @return void - */ - - function addTwitterAccount() - { - $screen_name = $this->trimmed('twitter_username'); - $password = $this->trimmed('twitter_password'); - $noticesend = $this->boolean('noticesend'); - $noticerecv = $this->boolean('noticerecv'); - $replysync = $this->boolean('replysync'); - $friendsync = $this->boolean('friendsync'); - - if (!Validate::string($screen_name, - array('min_length' => 1, - 'max_length' => 15, - 'format' => VALIDATE_NUM.VALIDATE_ALPHA.'_'))) { - $this->showForm(_('Username must have only numbers, '. - 'upper- and lowercase letters, '. - 'and underscore (_). 15 chars max.')); - return; - } - - if (!$this->verifyCredentials($screen_name, $password)) { - $this->showForm(_('Could not verify your Twitter credentials!')); - return; - } - - $twit_user = twitter_user_info($screen_name, $password); - - if (!$twit_user) { - $this->showForm(sprintf(_('Unable to retrieve account information '. - 'For "%s" from Twitter.'), - $screen_name)); - return; - } - - if (!save_twitter_user($twit_user->id, $screen_name)) { - $this->showForm(_('Unable to save your Twitter settings!')); - return; - } - - $user = common_current_user(); - - $flink = new Foreign_link(); - - $flink->user_id = $user->id; - $flink->foreign_id = $twit_user->id; - $flink->service = TWITTER_SERVICE; - $flink->credentials = $password; - $flink->created = common_sql_now(); - - $flink->set_flags($noticesend, $noticerecv, $replysync, $friendsync); - - $flink_id = $flink->insert(); - - if (!$flink_id) { - common_log_db_error($flink, 'INSERT', __FILE__); - $this->showForm(_('Unable to save your Twitter settings!')); - return; - } - - if ($friendsync) { - save_twitter_friends($user, $twit_user->id, $screen_name, $password); - $flink->last_friendsync = common_sql_now(); - $flink->update(); - } - - $this->showForm(_('Twitter settings saved.'), true); - } - - /** * Disassociate an existing Twitter account from this account * * @return void @@ -414,20 +226,11 @@ class TwittersettingsAction extends ConnectSettingsAction function removeTwitterAccount() { $user = common_current_user(); - - $flink = Foreign_link::getByUserID($user->id, 1); - - $flink_foreign_id = $this->arg('flink_foreign_id'); - - // Maybe an old tab open...? - if ($flink->foreign_id != $flink_foreign_id) { - $this->showForm(_('That is not your Twitter account.')); - return; - } + $flink = Foreign_link::getByUserID($user->id, TWITTER_SERVICE); $result = $flink->delete(); - if (!$result) { + if (empty($result)) { common_log_db_error($flink, 'DELETE', __FILE__); $this->serverError(_('Couldn\'t remove Twitter user.')); return; @@ -450,32 +253,16 @@ class TwittersettingsAction extends ConnectSettingsAction $replysync = $this->boolean('replysync'); $user = common_current_user(); + $flink = Foreign_link::getByUserID($user->id, TWITTER_SERVICE); - $flink = Foreign_link::getByUserID($user->id, 1); - - if (!$flink) { + if (empty($flink)) { common_log_db_error($flink, 'SELECT', __FILE__); $this->showForm(_('Couldn\'t save Twitter preferences.')); return; } - $twitter_id = $flink->foreign_id; - $password = $flink->credentials; - - $fuser = $flink->getForeignUser(); - - if (!$fuser) { - common_log_db_error($fuser, 'SELECT', __FILE__); - $this->showForm(_('Couldn\'t save Twitter preferences.')); - return; - } - - $screen_name = $fuser->nickname; - $original = clone($flink); - $flink->set_flags($noticesend, $noticerecv, $replysync, $friendsync); - $result = $flink->update($original); if ($result === false) { @@ -484,45 +271,7 @@ class TwittersettingsAction extends ConnectSettingsAction return; } - if ($friendsync) { - save_twitter_friends($user, $flink->foreign_id, $screen_name, $password); - } - $this->showForm(_('Twitter preferences saved.'), true); } - /** - * Verifies a username and password against Twitter's API - * - * @param string $screen_name Twitter user name - * @param string $password Twitter password - * - * @return boolean success flag - */ - - function verifyCredentials($screen_name, $password) - { - $uri = 'http://twitter.com/account/verify_credentials.json'; - - $data = get_twitter_data($uri, $screen_name, $password); - - if (!$data) { - return false; - } - - $user = json_decode($data); - - if (!$user) { - return false; - } - - $twitter_id = $user->id; - - if ($twitter_id) { - return $twitter_id; - } - - return false; - } - } diff --git a/actions/userauthorization.php b/actions/userauthorization.php index 00903ae01..7e397e888 100644 --- a/actions/userauthorization.php +++ b/actions/userauthorization.php @@ -47,7 +47,11 @@ class UserauthorizationAction extends Action # Go log in, and then come back common_set_returnto($_SERVER['REQUEST_URI']); - common_redirect(common_local_url('login')); + if (!common_config('site', 'openidonly')) { + common_redirect(common_local_url('login')); + } else { + common_redirect(common_local_url('openidlogin')); + } return; } diff --git a/config.php.sample b/config.php.sample index 21b6865e1..8b4b777f2 100644 --- a/config.php.sample +++ b/config.php.sample @@ -38,6 +38,8 @@ $config['site']['path'] = 'laconica'; // $config['site']['closed'] = true; // Only allow registration for people invited by another user // $config['site']['inviteonly'] = true; +// Only allow registrations and logins through OpenID +// $config['site']['openidonly'] = true; // Make the site invisible to non-logged-in users // $config['site']['private'] = true; @@ -182,6 +184,10 @@ $config['sphinx']['port'] = 3312; // // $config['twitterbridge']['enabled'] = true; +// Twitter OAuth settings +// $config['twitter']['consumer_key'] = 'YOURKEY'; +// $config['twitter']['consumer_secret'] = 'YOURSECRET'; + // Edit throttling. Off by default. If turned on, you can only post 20 notices // every 10 minutes. Admins may want to play with the settings to minimize inconvenience for // real users without getting uncontrollable floods from spammers or runaway bots. diff --git a/db/074to080.sql b/db/074to080.sql index ff0819159..e667798eb 100644 --- a/db/074to080.sql +++ b/db/074to080.sql @@ -17,7 +17,7 @@ alter table user_group create table file ( id integer primary key auto_increment, - url varchar(255) comment 'destination URL after following redirections', + url varchar(2047) comment 'destination URL after following redirections', mimetype varchar(50) comment 'mime type of resource', size integer comment 'size of resource when available', title varchar(255) comment 'title of resource when available', diff --git a/db/074to080_pg.sql b/db/074to080_pg.sql index 0a7171ae5..e1cb92c7f 100644 --- a/db/074to080_pg.sql +++ b/db/074to080_pg.sql @@ -36,7 +36,7 @@ alter table user_group create sequence file_seq; create table file ( id bigint default nextval('file_seq') primary key /* comment 'unique identifier' */, - url varchar(255) unique, + url varchar(2047) unique, mimetype varchar(50), size integer, title varchar(255), diff --git a/db/laconica.sql b/db/laconica.sql index 2c04f680a..c1b76d1fa 100644 --- a/db/laconica.sql +++ b/db/laconica.sql @@ -433,7 +433,7 @@ create table group_inbox ( create table file ( id integer primary key auto_increment, - url varchar(255) comment 'destination URL after following redirections', + url varchar(2047) comment 'destination URL after following redirections', mimetype varchar(50) comment 'mime type of resource', size integer comment 'size of resource when available', title varchar(255) comment 'title of resource when available', diff --git a/db/laconica_pg.sql b/db/laconica_pg.sql index ad34720a2..4fe223380 100644 --- a/db/laconica_pg.sql +++ b/db/laconica_pg.sql @@ -450,7 +450,7 @@ create index group_inbox_created_idx on group_inbox using btree(created); create sequence file_seq; create table file ( id bigint default nextval('file_seq') primary key /* comment 'unique identifier' */, - url varchar(255) unique, + url varchar(2047) unique, mimetype varchar(50), size integer, title varchar(255), @@ -182,12 +182,20 @@ function main() // If the site is private, and they're not on one of the "public" // parts of the site, redirect to login - if (!$user && common_config('site', 'private') && - !in_array($action, array('login', 'openidlogin', 'finishopenidlogin', - 'recoverpassword', 'api', 'doc', 'register')) && - !preg_match('/rss$/', $action)) { - common_redirect(common_local_url('login')); - return; + if (!$user && common_config('site', 'private')) { + $public_actions = array('openidlogin', 'finishopenidlogin', + 'recoverpassword', 'api', 'doc'); + $login_action = 'openidlogin'; + if (!common_config('site', 'openidonly')) { + $public_actions[] = 'login'; + $public_actions[] = 'register'; + $login_action = 'login'; + } + if (!in_array($action, $public_actions) && + !preg_match('/rss$/', $action)) { + common_redirect(common_local_url($login_action)); + return; + } } $action_class = ucfirst($action).'Action'; diff --git a/js/util.js b/js/util.js index 1ab711cd1..e5f117df0 100644 --- a/js/util.js +++ b/js/util.js @@ -17,28 +17,51 @@ */ $(document).ready(function(){ + var counterBlackout = false; + // count character on keyup function counter(event){ var maxLength = 140; var currentLength = $("#notice_data-text").val().length; var remaining = maxLength - currentLength; var counter = $("#notice_text-count"); - if (counter.text() != String(remaining)) { - counter.text(remaining); - } + + if (remaining.toString() != counter.text()) { + if (!counterBlackout || remaining == 0) { + if (counter.text() != String(remaining)) { + counter.text(remaining); + } - if (remaining < 0) { - $("#form_notice").addClass("warning"); - } else { - $("#form_notice").removeClass("warning"); - } + if (remaining < 0) { + $("#form_notice").addClass("warning"); + } else { + $("#form_notice").removeClass("warning"); + } + // Skip updates for the next 500ms. + // On slower hardware, updating on every keypress is unpleasant. + if (!counterBlackout) { + counterBlackout = true; + window.setTimeout(clearCounterBlackout, 500); + } + } + } + } + + function clearCounterBlackout() { + // Allow keyup events to poke the counter again + counterBlackout = false; + // Check if the string changed since we last looked + counter(null); } function submitonreturn(event) { - if (event.keyCode == 13) { + if (event.keyCode == 13 || event.keyCode == 10) { + // iPhone sends \n not \r for 'return' $("#form_notice").submit(); event.preventDefault(); event.stopPropagation(); + $("#notice_data-text").blur(); + $("body").focus(); return false; } return true; @@ -257,10 +280,10 @@ function NoticeReply() { function NoticeReplySet(nick,id) { rgx_username = /^[0-9a-zA-Z\-_.]*$/; if (nick.match(rgx_username)) { - replyto = "@" + nick + " "; var text = $("#notice_data-text"); if (text.length) { - text.val(replyto + text.val()); + replyto = "@" + nick + " "; + text.val(replyto + text.val().replace(RegExp(replyto, 'i'), '')); $("#form_notice input#notice_in-reply-to").val(id); if (text.get(0).setSelectionRange) { var len = text.val().length; diff --git a/lib/action.php b/lib/action.php index 326edf3a0..6da9adab5 100644 --- a/lib/action.php +++ b/lib/action.php @@ -436,12 +436,17 @@ class Action extends HTMLOutputter // lawsuit _('Logout'), _('Logout from the site'), false, 'nav_logout'); } else { - if (!common_config('site', 'closed')) { - $this->menuItem(common_local_url('register'), - _('Register'), _('Create an account'), false, 'nav_register'); + if (!common_config('site', 'openidonly')) { + if (!common_config('site', 'closed')) { + $this->menuItem(common_local_url('register'), + _('Register'), _('Create an account'), false, 'nav_register'); + } + $this->menuItem(common_local_url('login'), + _('Login'), _('Login to the site'), false, 'nav_login'); + } else { + $this->menuItem(common_local_url('openidlogin'), + _('OpenID'), _('Login with OpenID'), false, 'nav_openid'); } - $this->menuItem(common_local_url('login'), - _('Login'), _('Login to the site'), false, 'nav_login'); } $this->menuItem(common_local_url('doc', array('title' => 'help')), _('Help'), _('Help me!'), false, 'nav_help'); diff --git a/lib/common.php b/lib/common.php index be30519f4..f26155e70 100644 --- a/lib/common.php +++ b/lib/common.php @@ -109,6 +109,7 @@ $config = 'broughtbyurl' => null, 'closed' => false, 'inviteonly' => false, + 'openidonly' => false, 'private' => false, 'ssl' => 'never', 'sslserver' => null, @@ -194,6 +195,9 @@ $config = 'integration' => array('source' => 'Laconica', # source attribute for Twitter 'taguri' => $_server.',2009'), # base for tag URIs + 'twitter' => + array('consumer_key' => null, + 'consumer_secret' => null), 'memcached' => array('enabled' => false, 'server' => 'localhost', diff --git a/lib/facebookaction.php b/lib/facebookaction.php index ab11b613e..289e702c6 100644 --- a/lib/facebookaction.php +++ b/lib/facebookaction.php @@ -256,8 +256,13 @@ class FacebookAction extends Action $this->elementStart('dd'); $this->elementStart('p'); $this->text(sprintf($loginmsg_part1, common_config('site', 'name'))); - $this->element('a', - array('href' => common_local_url('register')), _('Register')); + if (!common_config('site', 'openidonly')) { + $this->element('a', + array('href' => common_local_url('register')), _('Register')); + } else { + $this->element('a', + array('href' => common_local_url('openidlogin')), _('Register')); + } $this->text($loginmsg_part2); $this->elementEnd('p'); $this->elementEnd('dd'); diff --git a/lib/htmloutputter.php b/lib/htmloutputter.php index 2684baca6..604597116 100644 --- a/lib/htmloutputter.php +++ b/lib/htmloutputter.php @@ -350,7 +350,7 @@ class HTMLOutputter extends XMLOutputter function script($src, $type='text/javascript') { $url = parse_url($src); - if(! ($url->scheme || $url->host || $url->query || $url->fragment)) + if( empty($url->scheme) && empty($url->host) && empty($url->query) && empty($url->fragment)) { $src = common_path($src) . '?version=' . LACONICA_VERSION; } @@ -371,7 +371,7 @@ class HTMLOutputter extends XMLOutputter function cssLink($src,$theme=null,$media=null) { $url = parse_url($src); - if(! ($url->scheme || $url->host || $url->query || $url->fragment)) + if( empty($url->scheme) && empty($url->host) && empty($url->query) && empty($url->fragment)) { if(file_exists(theme_file($src,$theme))){ $src = theme_path($src, $theme) . '?version=' . LACONICA_VERSION; diff --git a/lib/logingroupnav.php b/lib/logingroupnav.php index f23985f3a..919fd3db9 100644 --- a/lib/logingroupnav.php +++ b/lib/logingroupnav.php @@ -72,11 +72,13 @@ class LoginGroupNav extends Widget // action => array('prompt', 'title') $menu = array(); - $menu['login'] = array(_('Login'), - _('Login with a username and password')); - if (!(common_config('site','closed') || common_config('site','inviteonly'))) { - $menu['register'] = array(_('Register'), - _('Sign up for a new account')); + if (!common_config('site','openidonly')) { + $menu['login'] = array(_('Login'), + _('Login with a username and password')); + if (!(common_config('site','closed') || common_config('site','inviteonly'))) { + $menu['register'] = array(_('Register'), + _('Sign up for a new account')); + } } $menu['openidlogin'] = array(_('OpenID'), _('Login or register with OpenID')); diff --git a/lib/mail.php b/lib/mail.php index 48e2cff65..33d1eb754 100644 --- a/lib/mail.php +++ b/lib/mail.php @@ -657,13 +657,14 @@ function mail_twitter_bridge_removed($user) $subject = sprintf(_('Your Twitter bridge has been disabled.')); - $body = sprintf(_("Hi, %1\$s. We're sorry to inform you that your " . - 'link to Twitter has been disabled. Your Twitter credentials ' . - 'have either changed (did you recently change your Twitter ' . - 'password?) or you have otherwise revoked our access to your ' . - "Twitter account.\n\n" . - 'You can re-enable your Twitter bridge by visiting your ' . - "Twitter settings page:\n\n\t%2\$s\n\n" . + $site_name = common_config('site', 'name'); + + $body = sprintf(_('Hi, %1$s. We\'re sorry to inform you that your ' . + 'link to Twitter has been disabled. We no longer seem to have ' . + 'permission to update your Twitter status. (Did you revoke ' . + '%3$s\'s access?)' . "\n\n" . + 'You can re-enable your Twitter bridge by visiting your ' . + "Twitter settings page:\n\n\t%2\$s\n\n" . "Regards,\n%3\$s\n"), $profile->getBestName(), common_local_url('twittersettings'), @@ -691,11 +692,11 @@ function mail_facebook_app_removed($user) $site_name = common_config('site', 'name'); $subject = sprintf( - _('Your %1\$s Facebook application access has been disabled.', + _('Your %1$s Facebook application access has been disabled.', $site_name)); $body = sprintf(_("Hi, %1\$s. We're sorry to inform you that we are " . - 'unable to update your Facebook status from %2\$s, and have disabled ' . + 'unable to update your Facebook status from %2$s, and have disabled ' . 'the Facebook application for your account. This may be because ' . 'you have removed the Facebook application\'s authorization, or ' . 'have deleted your Facebook account. You can re-enable the ' . diff --git a/lib/noticelist.php b/lib/noticelist.php index a8d5059ca..5429d943f 100644 --- a/lib/noticelist.php +++ b/lib/noticelist.php @@ -350,11 +350,10 @@ class NoticeListItem extends Widget function showNoticeLink() { - $noticeurl = common_local_url('shownotice', + if($this->notice->is_local){ + $noticeurl = common_local_url('shownotice', array('notice' => $this->notice->id)); - // XXX: we need to figure this out better. Is this right? - if (strcmp($this->notice->uri, $noticeurl) != 0 && - preg_match('/^http/', $this->notice->uri)) { + }else{ $noticeurl = $this->notice->uri; } $this->out->elementStart('a', array('rel' => 'bookmark', diff --git a/lib/oauthclient.php b/lib/oauthclient.php new file mode 100644 index 000000000..b66a24be4 --- /dev/null +++ b/lib/oauthclient.php @@ -0,0 +1,225 @@ +<?php +/** + * Laconica, the distributed open-source microblogging tool + * + * Base class for doing OAuth calls as a consumer + * + * 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 Action + * @package Laconica + * @author Zach Copley <zach@controlyourself.ca> + * @copyright 2008 Control Yourself, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ + +if (!defined('LACONICA')) { + exit(1); +} + +require_once 'OAuth.php'; + +/** + * Exception wrapper for cURL errors + * + * @category Integration + * @package Laconica + * @author Zach Copley <zach@controlyourself.ca> + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + * + */ +class OAuthClientCurlException extends Exception +{ +} + +/** + * Base class for doing OAuth calls as a consumer + * + * @category Integration + * @package Laconica + * @author Zach Copley <zach@controlyourself.ca> + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + * + */ +class OAuthClient +{ + var $consumer; + var $token; + + /** + * Constructor + * + * Can be initialized with just consumer key and secret for requesting new + * tokens or with additional request token or access token + * + * @param string $consumer_key consumer key + * @param string $consumer_secret consumer secret + * @param string $oauth_token user's token + * @param string $oauth_token_secret user's secret + * + * @return nothing + */ + function __construct($consumer_key, $consumer_secret, + $oauth_token = null, $oauth_token_secret = null) + { + $this->sha1_method = new OAuthSignatureMethod_HMAC_SHA1(); + $this->consumer = new OAuthConsumer($consumer_key, $consumer_secret); + $this->token = null; + + if (isset($oauth_token) && isset($oauth_token_secret)) { + $this->token = new OAuthToken($oauth_token, $oauth_token_secret); + } + } + + /** + * Gets a request token from the given url + * + * @param string $url OAuth endpoint for grabbing request tokens + * + * @return OAuthToken $token the request token + */ + function getRequestToken($url) + { + $response = $this->oAuthGet($url); + parse_str($response); + $token = new OAuthToken($oauth_token, $oauth_token_secret); + return $token; + } + + /** + * Builds a link that can be redirected to in order to + * authorize a request token. + * + * @param string $url endpoint for authorizing request tokens + * @param OAuthToken $request_token the request token to be authorized + * @param string $oauth_callback optional callback url + * + * @return string $authorize_url the url to redirect to + */ + function getAuthorizeLink($url, $request_token, $oauth_callback = null) + { + $authorize_url = $url . '?oauth_token=' . + $request_token->key; + + if (isset($oauth_callback)) { + $authorize_url .= '&oauth_callback=' . urlencode($oauth_callback); + } + + return $authorize_url; + } + + /** + * Fetches an access token + * + * @param string $url OAuth endpoint for exchanging authorized request tokens + * for access tokens + * + * @return OAuthToken $token the access token + */ + function getAccessToken($url) + { + $response = $this->oAuthPost($url); + parse_str($response); + $token = new OAuthToken($oauth_token, $oauth_token_secret); + return $token; + } + + /** + * Use HTTP GET to make a signed OAuth request + * + * @param string $url OAuth endpoint + * + * @return mixed the request + */ + function oAuthGet($url) + { + $request = OAuthRequest::from_consumer_and_token($this->consumer, + $this->token, 'GET', $url, null); + $request->sign_request($this->sha1_method, + $this->consumer, $this->token); + + return $this->httpRequest($request->to_url()); + } + + /** + * Use HTTP POST to make a signed OAuth request + * + * @param string $url OAuth endpoint + * @param array $params additional post parameters + * + * @return mixed the request + */ + function oAuthPost($url, $params = null) + { + $request = OAuthRequest::from_consumer_and_token($this->consumer, + $this->token, 'POST', $url, $params); + $request->sign_request($this->sha1_method, + $this->consumer, $this->token); + + return $this->httpRequest($request->get_normalized_http_url(), + $request->to_postdata()); + } + + /** + * Make a HTTP request using cURL. + * + * @param string $url Where to make the + * @param array $params post parameters + * + * @return mixed the request + */ + function httpRequest($url, $params = null) + { + $options = array( + CURLOPT_RETURNTRANSFER => true, + CURLOPT_FAILONERROR => true, + CURLOPT_HEADER => false, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_USERAGENT => 'Laconica', + CURLOPT_CONNECTTIMEOUT => 120, + CURLOPT_TIMEOUT => 120, + CURLOPT_HTTPAUTH => CURLAUTH_ANY, + CURLOPT_SSL_VERIFYPEER => false, + + // Twitter is strict about accepting invalid "Expect" headers + + CURLOPT_HTTPHEADER => array('Expect:') + ); + + if (isset($params)) { + $options[CURLOPT_POST] = true; + $options[CURLOPT_POSTFIELDS] = $params; + } + + $ch = curl_init($url); + curl_setopt_array($ch, $options); + $response = curl_exec($ch); + + if ($response === false) { + $msg = curl_error($ch); + $code = curl_errno($ch); + throw new OAuthClientCurlException($msg, $code); + } + + curl_close($ch); + + return $response; + } + +} diff --git a/lib/parallelizingdaemon.php b/lib/parallelizingdaemon.php new file mode 100644 index 000000000..dc28b5643 --- /dev/null +++ b/lib/parallelizingdaemon.php @@ -0,0 +1,229 @@ +<?php +/** + * Laconica, the distributed open-source microblogging tool + * + * Base class for making daemons that can do several tasks in parallel. + * + * 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 Daemon + * @package Laconica + * @author Zach Copley <zach@controlyourself.ca> + * @author Evan Prodromou <evan@controlyourself.ca> + * @copyright 2009 Control Yourself, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ + +if (!defined('LACONICA')) { + exit(1); +} + +declare(ticks = 1); + +/** + * Daemon able to spawn multiple child processes to do work in parallel + * + * @category Daemon + * @package Laconica + * @author Zach Copley <zach@controlyourself.ca> + * @author Evan Prodromou <evan@controlyourself.ca> + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ + +class ParallelizingDaemon extends Daemon +{ + private $_children = array(); + private $_interval = 0; // seconds + private $_max_children = 0; // maximum number of children + private $_debug = false; + + /** + * Constructor + * + * @param string $id the name/id of this daemon + * @param int $interval sleep this long before doing everything again + * @param int $max_children maximum number of child processes at a time + * @param boolean $debug debug output flag + * + * @return void + * + **/ + + function __construct($id = null, $interval = 60, $max_children = 2, + $debug = null) + { + parent::__construct(true); // daemonize + + $this->_interval = $interval; + $this->_max_children = $max_children; + $this->_debug = $debug; + + if (isset($id)) { + $this->set_id($id); + } + } + + /** + * Run the daemon + * + * @return void + */ + + function run() + { + if (isset($this->_debug)) { + echo $this->name() . " - Debugging output enabled.\n"; + } + + do { + + $objects = $this->getObjects(); + + foreach ($objects as $o) { + + // Fork a child for each object + + $pid = pcntl_fork(); + + if ($pid == -1) { + die ($this->name() . ' - Couldn\'t fork!'); + } + + if ($pid) { + + // Parent + + if (isset($this->_debug)) { + echo $this->name() . + " - Forked new child - pid $pid.\n"; + + } + + $this->_children[] = $pid; + + } else { + + // Child + + // Do something with each object + + $this->childTask($o); + + exit(); + } + + // Remove child from ps list as it finishes + + while (($c = pcntl_wait($status, WNOHANG OR WUNTRACED)) > 0) { + + if (isset($this->_debug)) { + echo $this->name() . " - Child $c finished.\n"; + } + + $this->removePs($this->_children, $c); + } + + // Wait! We have too many damn kids. + + if (sizeof($this->_children) >= $this->_max_children) { + + if (isset($this->_debug)) { + echo $this->name() . " - Too many children. Waiting...\n"; + } + + if (($c = pcntl_wait($status, WUNTRACED)) > 0) { + + if (isset($this->_debug)) { + echo $this->name() . + " - Finished waiting for child $c.\n"; + } + + $this->removePs($this->_children, $c); + } + } + } + + // Remove all children from the process list before restarting + while (($c = pcntl_wait($status, WUNTRACED)) > 0) { + + if (isset($this->_debug)) { + echo $this->name() . " - Child $c finished.\n"; + } + + $this->removePs($this->_children, $c); + } + + // Rest for a bit + + if (isset($this->_debug)) { + echo $this->name() . ' - Waiting ' . $this->_interval . + " secs before running again.\n"; + } + + if ($this->_interval > 0) { + sleep($this->_interval); + } + + } while (true); + } + + /** + * Remove a child process from the list of children + * + * @param array &$plist array of processes + * @param int $ps process id + * + * @return void + */ + + function removePs(&$plist, $ps) + { + for ($i = 0; $i < sizeof($plist); $i++) { + if ($plist[$i] == $ps) { + unset($plist[$i]); + $plist = array_values($plist); + break; + } + } + } + + /** + * Get a list of objects to work on in parallel + * + * @return array An array of objects to work on + */ + + function getObjects() + { + die('Implement ParallelizingDaemon::getObjects().'); + } + + /** + * Do something with each object in parallel + * + * @param mixed $object data to work on + * + * @return void + */ + + function childTask($object) + { + die("Implement ParallelizingDaemon::childTask($object)."); + } + +}
\ No newline at end of file diff --git a/lib/router.php b/lib/router.php index 9ab46856d..f03cfcf6d 100644 --- a/lib/router.php +++ b/lib/router.php @@ -88,6 +88,10 @@ class Router $m->connect('doc/:title', array('action' => 'doc')); + // Twitter + + $m->connect('twitter/authorization', array('action' => 'twitterauthorization')); + // facebook $m->connect('facebook', array('action' => 'facebookhome')); diff --git a/lib/twitter.php b/lib/twitter.php index 47af32e61..280cdb0a3 100644 --- a/lib/twitter.php +++ b/lib/twitter.php @@ -17,83 +17,20 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -if (!defined('LACONICA')) { exit(1); } - -define('TWITTER_SERVICE', 1); // Twitter is foreign_service ID 1 - -function get_twitter_data($uri, $screen_name, $password) -{ - - $options = array( - CURLOPT_USERPWD => sprintf("%s:%s", $screen_name, $password), - CURLOPT_RETURNTRANSFER => true, - CURLOPT_FAILONERROR => true, - CURLOPT_HEADER => false, - CURLOPT_FOLLOWLOCATION => true, - CURLOPT_USERAGENT => "Laconica", - CURLOPT_CONNECTTIMEOUT => 120, - CURLOPT_TIMEOUT => 120, - # Twitter is strict about accepting invalid "Expect" headers - CURLOPT_HTTPHEADER => array('Expect:') - ); - - $ch = curl_init($uri); - curl_setopt_array($ch, $options); - $data = curl_exec($ch); - $errmsg = curl_error($ch); - - if ($errmsg) { - common_debug("Twitter bridge - cURL error: $errmsg - trying to load: $uri with user $screen_name.", - __FILE__); - - if (defined('SCRIPT_DEBUG')) { - print "cURL error: $errmsg - trying to load: $uri with user $screen_name.\n"; - } - } - - curl_close($ch); - - return $data; -} - -function twitter_json_data($uri, $screen_name, $password) -{ - $json_data = get_twitter_data($uri, $screen_name, $password); - - if (!$json_data) { - return false; - } - - $data = json_decode($json_data); - - if (!$data) { - return false; - } - - return $data; -} - -function twitter_user_info($screen_name, $password) -{ - $uri = "http://twitter.com/users/show/$screen_name.json"; - return twitter_json_data($uri, $screen_name, $password); +if (!defined('LACONICA')) { + exit(1); } -function twitter_friends_ids($screen_name, $password) -{ - $uri = "http://twitter.com/friends/ids/$screen_name.json"; - return twitter_json_data($uri, $screen_name, $password); -} +define('TWITTER_SERVICE', 1); // Twitter is foreign_service ID 1 function update_twitter_user($twitter_id, $screen_name) { $uri = 'http://twitter.com/' . $screen_name; - $fuser = new Foreign_user(); $fuser->query('BEGIN'); - // Dropping down to SQL because regular db_object udpate stuff doesn't seem + // Dropping down to SQL because regular DB_DataObject udpate stuff doesn't seem // to work so good with tables that have multiple column primary keys // Any time we update the uri for a forein user we have to make sure there @@ -102,35 +39,14 @@ function update_twitter_user($twitter_id, $screen_name) $qry = 'UPDATE foreign_user set uri = \'\' WHERE uri = '; $qry .= '\'' . $uri . '\'' . ' AND service = ' . TWITTER_SERVICE; - $result = $fuser->query($qry); - - if ($result) { - common_debug("Removed uri ($uri) from another foreign_user who was squatting on it."); - if (defined('SCRIPT_DEBUG')) { - print("Removed uri ($uri) from another Twitter user who was squatting on it.\n"); - } - } + $fuser->query($qry); // Update the user + $qry = 'UPDATE foreign_user SET nickname = '; $qry .= '\'' . $screen_name . '\'' . ', uri = \'' . $uri . '\' '; $qry .= 'WHERE id = ' . $twitter_id . ' AND service = ' . TWITTER_SERVICE; - $result = $fuser->query($qry); - - if (!$result) { - common_log(LOG_WARNING, - "Couldn't update foreign_user data for Twitter user: $screen_name"); - common_log_db_error($fuser, 'UPDATE', __FILE__); - if (defined('SCRIPT_DEBUG')) { - print "UPDATE failed: for Twitter user: $twitter_id - $screen_name. - "; - print common_log_objstring($fuser) . "\n"; - $error = &PEAR::getStaticProperty('DB_DataObject','lastError'); - print "DB_DataObject Error: " . $error->getMessage() . "\n"; - } - return false; - } - $fuser->query('COMMIT'); $fuser->free(); @@ -147,23 +63,22 @@ function add_twitter_user($twitter_id, $screen_name) // Clear out any bad old foreign_users with the new user's legit URL // This can happen when users move around or fakester accounts get // repoed, and things like that. + $luser = new Foreign_user(); $luser->uri = $new_uri; $luser->service = TWITTER_SERVICE; $result = $luser->delete(); - if ($result) { + if (empty($result)) { common_log(LOG_WARNING, "Twitter bridge - removed invalid Twitter user squatting on uri: $new_uri"); - if (defined('SCRIPT_DEBUG')) { - print "Removed invalid Twitter user squatting on uri: $new_uri\n"; - } } $luser->free(); unset($luser); // Otherwise, create a new Twitter user + $fuser = new Foreign_user(); $fuser->nickname = $screen_name; @@ -173,21 +88,12 @@ function add_twitter_user($twitter_id, $screen_name) $fuser->created = common_sql_now(); $result = $fuser->insert(); - if (!$result) { + if (empty($result)) { common_log(LOG_WARNING, "Twitter bridge - failed to add new Twitter user: $twitter_id - $screen_name."); common_log_db_error($fuser, 'INSERT', __FILE__); - if (defined('SCRIPT_DEBUG')) { - print "INSERT failed: could not add new Twitter user: $twitter_id - $screen_name. - "; - print common_log_objstring($fuser) . "\n"; - $error = &PEAR::getStaticProperty('DB_DataObject','lastError'); - print "DB_DataObject Error: " . $error->getMessage() . "\n"; - } } else { common_debug("Twitter bridge - Added new Twitter user: $screen_name ($twitter_id)."); - if (defined('SCRIPT_DEBUG')) { - print "Added new Twitter user: $screen_name ($twitter_id).\n"; - } } return $result; @@ -199,23 +105,20 @@ function save_twitter_user($twitter_id, $screen_name) // Check to see whether the Twitter user is already in the system, // and update its screen name and uri if so. + $fuser = Foreign_user::getForeignUser($twitter_id, TWITTER_SERVICE); - if ($fuser) { + if (!empty($fuser)) { $result = true; // Only update if Twitter screen name has changed + if ($fuser->nickname != $screen_name) { $result = update_twitter_user($twitter_id, $screen_name); common_debug('Twitter bridge - Updated nickname (and URI) for Twitter user ' . "$fuser->id to $screen_name, was $fuser->nickname"); - - if (defined('SCRIPT_DEBUG')) { - print 'Updated nickname (and URI) for Twitter user ' . - "$fuser->id to $screen_name, was $fuser->nickname\n"; - } } return $result; @@ -230,119 +133,6 @@ function save_twitter_user($twitter_id, $screen_name) return true; } -function retreive_twitter_friends($twitter_id, $screen_name, $password) -{ - $friends = array(); - - $uri = "http://twitter.com/statuses/friends/$twitter_id.json?page="; - $friends_ids = twitter_friends_ids($screen_name, $password); - - if (!$friends_ids) { - return $friends; - } - - if (defined('SCRIPT_DEBUG')) { - print "Twitter 'social graph' ids method says $screen_name has " . - count($friends_ids) . " friends.\n"; - } - - // Calculate how many pages to get... - $pages = ceil(count($friends_ids) / 100); - - if ($pages == 0) { - common_log(LOG_WARNING, - "Twitter bridge - $screen_name seems to have no friends."); - if (defined('SCRIPT_DEBUG')) { - print "$screen_name seems to have no friends.\n"; - } - } - - for ($i = 1; $i <= $pages; $i++) { - - $data = get_twitter_data($uri . $i, $screen_name, $password); - - if (!$data) { - common_log(LOG_WARNING, - "Twitter bridge - Couldn't retrieve page $i of $screen_name's friends."); - if (defined('SCRIPT_DEBUG')) { - print "Couldn't retrieve page $i of $screen_name's friends.\n"; - } - continue; - } - - $more_friends = json_decode($data); - - if (!$more_friends) { - - common_log(LOG_WARNING, - "Twitter bridge - No data for page $i of $screen_name's friends."); - if (defined('SCRIPT_DEBUG')) { - print "No data for page $i of $screen_name's friends.\n"; - } - continue; - } - - $friends = array_merge($friends, $more_friends); - } - - return $friends; -} - -function save_twitter_friends($user, $twitter_id, $screen_name, $password) -{ - - $friends = retreive_twitter_friends($twitter_id, $screen_name, $password); - - if (empty($friends)) { - common_debug("Twitter bridge - Couldn't get friends data from Twitter for $screen_name."); - if (defined('SCRIPT_DEBUG')) { - print "Couldn't get friends data from Twitter for $screen_name.\n"; - } - return false; - } - - foreach ($friends as $friend) { - - $friend_name = $friend->screen_name; - $friend_id = (int) $friend->id; - - // Update or create the Foreign_user record - if (!save_twitter_user($friend_id, $friend_name)) { - common_log(LOG_WARNING, - "Twitter bridge - couldn't save $screen_name's friend, $friend_name."); - if (defined('SCRIPT_DEBUG')) { - print "Couldn't save $screen_name's friend, $friend_name.\n"; - } - continue; - } - - // Check to see if there's a related local user - $flink = Foreign_link::getByForeignID($friend_id, 1); - - if ($flink) { - - // Get associated user and subscribe her - $friend_user = User::staticGet('id', $flink->user_id); - if (!empty($friend_user)) { - $result = subs_subscribe_to($user, $friend_user); - - if ($result === true) { - common_debug("Twitter bridge - subscribed $friend_user->nickname to $user->nickname."); - if (defined('SCRIPT_DEBUG')) { - print("Subscribed $friend_user->nickname to $user->nickname.\n"); - } - } else { - if (defined('SCRIPT_DEBUG')) { - print "$result ($friend_user->nickname to $user->nickname)\n"; - } - } - } - } - } - - return true; -} - function is_twitter_bound($notice, $flink) { // Check to see if notice should go to Twitter @@ -351,7 +141,7 @@ function is_twitter_bound($notice, $flink) { // If it's not a Twitter-style reply, or if the user WANTS to send replies. if (!preg_match('/^@[a-zA-Z0-9_]{1,15}\b/u', $notice->content) || ($flink->noticesync & FOREIGN_NOTICE_SEND_REPLY)) { - return true; + return true; } } @@ -360,104 +150,73 @@ function is_twitter_bound($notice, $flink) { function broadcast_twitter($notice) { - $flink = Foreign_link::getByUserID($notice->profile_id, - TWITTER_SERVICE); + TWITTER_SERVICE); if (is_twitter_bound($notice, $flink)) { - $fuser = $flink->getForeignUser(); - $twitter_user = $fuser->nickname; - $twitter_password = $flink->credentials; - $uri = 'http://www.twitter.com/statuses/update.json'; + $user = $flink->getUser(); // XXX: Hack to get around PHP cURL's use of @ being a a meta character $statustxt = preg_replace('/^@/', ' @', $notice->content); - $options = array( - CURLOPT_USERPWD => "$twitter_user:$twitter_password", - CURLOPT_POST => true, - CURLOPT_POSTFIELDS => - array( - 'status' => $statustxt, - 'source' => common_config('integration', 'source') - ), - CURLOPT_RETURNTRANSFER => true, - CURLOPT_FAILONERROR => true, - CURLOPT_HEADER => false, - CURLOPT_FOLLOWLOCATION => true, - CURLOPT_USERAGENT => "Laconica", - CURLOPT_CONNECTTIMEOUT => 120, // XXX: How long should this be? - CURLOPT_TIMEOUT => 120, - - # Twitter is strict about accepting invalid "Expect" headers - CURLOPT_HTTPHEADER => array('Expect:') - ); - - $ch = curl_init($uri); - curl_setopt_array($ch, $options); - $data = curl_exec($ch); - $errmsg = curl_error($ch); - $errno = curl_errno($ch); - - if (!empty($errmsg)) { - common_debug("cURL error ($errno): $errmsg - " . - "trying to send notice for $twitter_user.", - __FILE__); - - $user = $flink->getUser(); - - if ($errmsg == 'The requested URL returned error: 401') { - common_debug(sprintf('User %s (user id: %s) ' . - 'has bad Twitter credentials!', - $user->nickname, $user->id)); - - // Bad credentials we need to delete the foreign_link - // to Twitter and inform the user. - - remove_twitter_link($flink); - - return true; + $token = TwitterOAuthClient::unpackToken($flink->credentials); - } else { + $client = new TwitterOAuthClient($token->key, $token->secret); - // Some other error happened, so we should try to - // send again later + $status = null; - return false; - } + try { + $status = $client->statusesUpdate($statustxt); + } catch (OAuthClientCurlException $e) { - } - - curl_close($ch); + if ($e->getMessage() == 'The requested URL returned error: 401') { - if (empty($data)) { - common_debug("No data returned by Twitter's " . - "API trying to send update for $twitter_user", - __FILE__); + $errmsg = sprintf('User %1$s (user id: %2$s) has an invalid ' . + 'Twitter OAuth access token.', + $user->nickname, $user->id); + common_log(LOG_WARNING, $errmsg); - // XXX: Not sure this represents a failure to send, but it - // probably does + // Bad auth token! We need to delete the foreign_link + // to Twitter and inform the user. - return false; - - } else { + remove_twitter_link($flink); + return true; - // Twitter should return a status - $status = json_decode($data); + } else { - if (empty($status)) { - common_debug("Unexpected data returned by Twitter " . - " API trying to send update for $twitter_user", - __FILE__); + // Some other error happened, so we should probably + // try to send again later. - // XXX: Again, this could represent a failure posting - // or the Twitter API might just be behaving flakey. - // We're treating it as a failure to post. + $errmsg = sprintf('cURL error trying to send notice to Twitter ' . + 'for user %1$s (user id: %2$s) - ' . + 'code: %3$s message: $4$s.', + $user->nickname, $user->id, + $e->getCode(), $e->getMessage()); + common_log(LOG_WARNING, $errmsg); return false; } } + + if (empty($status)) { + + // This could represent a failure posting, + // or the Twitter API might just be behaving flakey. + + $errmsg = sprint('No data returned by Twitter API when ' . + 'trying to send update for %1$s (user id %2$s).', + $user->nickname, $user->id); + common_log(LOG_WARNING, $errmsg); + + return false; + } + + // Notice crossed the great divide + + $msg = sprintf('Twitter bridge posted notice %s to Twitter.', + $notice->id); + common_log(LOG_INFO, $msg); } return true; @@ -474,22 +233,25 @@ function remove_twitter_link($flink) if (empty($result)) { common_log(LOG_ERR, 'Could not remove Twitter bridge ' . - "Foreign_link for $user->nickname (user id: $user->id)!"); + "Foreign_link for $user->nickname (user id: $user->id)!"); common_log_db_error($flink, 'DELETE', __FILE__); } // Notify the user that her Twitter bridge is down - $result = mail_twitter_bridge_removed($user); + if (isset($user->email)) { + + $result = mail_twitter_bridge_removed($user); - if (!$result) { + if (!$result) { - $msg = 'Unable to send email to notify ' . - "$user->nickname (user id: $user->id) " . - 'that their Twitter bridge link was ' . - 'removed!'; + $msg = 'Unable to send email to notify ' . + "$user->nickname (user id: $user->id) " . + 'that their Twitter bridge link was ' . + 'removed!'; - common_log(LOG_WARNING, $msg); + common_log(LOG_WARNING, $msg); + } } } diff --git a/lib/twitteroauthclient.php b/lib/twitteroauthclient.php new file mode 100644 index 000000000..b7dc4a80c --- /dev/null +++ b/lib/twitteroauthclient.php @@ -0,0 +1,220 @@ +<?php +/** + * Laconica, 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 Laconica + * @author Zach Copley <zach@controlyourself.ca> + * @copyright 2008 Control Yourself, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ + +if (!defined('LACONICA')) { + exit(1); +} + +/** + * Class for talking to the Twitter API with OAuth. + * + * @category Integration + * @package Laconica + * @author Zach Copley <zach@controlyourself.ca> + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + * + */ +class TwitterOAuthClient extends OAuthClient +{ + public static $requestTokenURL = 'https://twitter.com/oauth/request_token'; + public static $authorizeURL = 'https://twitter.com/oauth/authorize'; + public static $accessTokenURL = 'https://twitter.com/oauth/access_token'; + + /** + * Constructor + * + * @param string $oauth_token the user's token + * @param string $oauth_token_secret the user's token secret + * + * @return nothing + */ + function __construct($oauth_token = null, $oauth_token_secret = null) + { + $consumer_key = common_config('twitter', 'consumer_key'); + $consumer_secret = common_config('twitter', 'consumer_secret'); + + parent::__construct($consumer_key, $consumer_secret, + $oauth_token, $oauth_token_secret); + } + + // XXX: the following two functions are to support the horrible hack + // of using the credentils field in Foreign_link to store both + // the access token and token secret. This hack should go away with + // 0.9, in which we can make DB changes and add a new column for the + // token itself. + + static function packToken($token) + { + return implode(chr(0), array($token->key, $token->secret)); + } + + static function unpackToken($str) + { + $vals = explode(chr(0), $str); + return new OAuthToken($vals[0], $vals[1]); + } + + /** + * Builds a link to Twitter's endpoint for authorizing a request token + * + * @param OAuthToken $request_token token to authorize + * + * @return the link + */ + function getAuthorizeLink($request_token) + { + return parent::getAuthorizeLink(self::$authorizeURL, + $request_token, + common_local_url('twitterauthorization')); + } + + /** + * Calls Twitter's /account/verify_credentials API method + * + * @return mixed the Twitter user + */ + function verifyCredentials() + { + $url = 'https://twitter.com/account/verify_credentials.json'; + $response = $this->oAuthGet($url); + $twitter_user = json_decode($response); + return $twitter_user; + } + + /** + * Calls Twitter's /stutuses/update API method + * + * @param string $status text of the status + * @param int $in_reply_to_status_id optional id of the status it's + * a reply to + * + * @return mixed the status + */ + function statusesUpdate($status, $in_reply_to_status_id = null) + { + $url = 'https://twitter.com/statuses/update.json'; + $params = array('status' => $status, + 'in_reply_to_status_id' => $in_reply_to_status_id); + $response = $this->oAuthPost($url, $params); + $status = json_decode($response); + return $status; + } + + /** + * Calls Twitter's /stutuses/friends_timeline API method + * + * @param int $since_id show statuses after this id + * @param int $max_id show statuses before this id + * @param int $cnt number of statuses to show + * @param int $page page number + * + * @return mixed an array of statuses + */ + function statusesFriendsTimeline($since_id = null, $max_id = null, + $cnt = null, $page = null) + { + + $url = 'https://twitter.com/statuses/friends_timeline.json'; + $params = array('since_id' => $since_id, + 'max_id' => $max_id, + 'count' => $cnt, + 'page' => $page); + $qry = http_build_query($params); + + if (!empty($qry)) { + $url .= "?$qry"; + } + + $response = $this->oAuthGet($url); + $statuses = json_decode($response); + return $statuses; + } + + /** + * Calls Twitter's /stutuses/friends API method + * + * @param int $id id of the user whom you wish to see friends of + * @param int $user_id numerical user id + * @param int $screen_name screen name + * @param int $page page number + * + * @return mixed an array of twitter users and their latest status + */ + function statusesFriends($id = null, $user_id = null, $screen_name = null, + $page = null) + { + $url = "https://twitter.com/statuses/friends.json"; + + $params = array('id' => $id, + 'user_id' => $user_id, + 'screen_name' => $screen_name, + 'page' => $page); + $qry = http_build_query($params); + + if (!empty($qry)) { + $url .= "?$qry"; + } + + $response = $this->oAuthGet($url); + $friends = json_decode($response); + return $friends; + } + + /** + * Calls Twitter's /stutuses/friends/ids API method + * + * @param int $id id of the user whom you wish to see friends of + * @param int $user_id numerical user id + * @param int $screen_name screen name + * @param int $page page number + * + * @return mixed a list of ids, 100 per page + */ + function friendsIds($id = null, $user_id = null, $screen_name = null, + $page = null) + { + $url = "https://twitter.com/friends/ids.json"; + + $params = array('id' => $id, + 'user_id' => $user_id, + 'screen_name' => $screen_name, + 'page' => $page); + $qry = http_build_query($params); + + if (!empty($qry)) { + $url .= "?$qry"; + } + + $response = $this->oAuthGet($url); + $ids = json_decode($response); + return $ids; + } + +} diff --git a/plugins/FBConnect/FBConnectPlugin.php b/plugins/FBConnect/FBConnectPlugin.php index 19b778c66..fd16d2bf1 100644 --- a/plugins/FBConnect/FBConnectPlugin.php +++ b/plugins/FBConnect/FBConnectPlugin.php @@ -116,13 +116,13 @@ class FBConnectPlugin extends Plugin // but we actually do, for IE and Safari. Gar. $html = sprintf('<script type="text/javascript"> - window.onload = function () { + $(document).ready(function () { FB_RequireFeatures( ["XFBML"], function() { FB.init("%s", "../xd_receiver.html"); } - ); } + ); }); function goto_login() { window.location = "%s"; diff --git a/plugins/FBConnect/README b/plugins/FBConnect/README index 914b774cb..11e5ce453 100644 --- a/plugins/FBConnect/README +++ b/plugins/FBConnect/README @@ -43,8 +43,7 @@ API key and secret to your Laconica config.php file: Finally, to enable the plugin, add the following stanza to your config.php: - require_once(INSTALLDIR.'/plugins/FBConnect/FBConnectPlugin.php'); - $fbc = new FBConnectPlugin(); + addPlugin('FBConnect'); To try out the plugin, fire up your browser and connect to: diff --git a/scripts/getvaliddaemons.php b/scripts/getvaliddaemons.php index 8b127bd20..f1d8d8116 100755 --- a/scripts/getvaliddaemons.php +++ b/scripts/getvaliddaemons.php @@ -45,6 +45,7 @@ if(common_config('twitterbridge','enabled')) { echo "ombqueuehandler.php "; if (common_config('twitter', 'enabled')) { echo "twitterqueuehandler.php "; + echo "synctwitterfriends.php "; } echo "facebookqueuehandler.php "; echo "pingqueuehandler.php "; diff --git a/scripts/stopdaemons.sh b/scripts/stopdaemons.sh index 60ffd83ad..894e5aaff 100755 --- a/scripts/stopdaemons.sh +++ b/scripts/stopdaemons.sh @@ -25,7 +25,7 @@ DIR=`php $SDIR/getpiddir.php` for f in jabberhandler ombhandler publichandler smshandler pinghandler \ xmppconfirmhandler xmppdaemon twitterhandler facebookhandler \ - twitterstatusfetcher; do + twitterstatusfetcher synctwitterfriends; do FILES="$DIR/$f.*.pid" for ff in "$FILES" ; do diff --git a/scripts/synctwitterfriends.php b/scripts/synctwitterfriends.php index fe53ff44d..2de464bcc 100755 --- a/scripts/synctwitterfriends.php +++ b/scripts/synctwitterfriends.php @@ -20,85 +20,260 @@ define('INSTALLDIR', realpath(dirname(__FILE__) . '/..')); -// Uncomment this to get useful console output +$shortoptions = 'di::'; +$longoptions = array('id::', 'debug'); + +$helptext = <<<END_OF_TRIM_HELP +Batch script for synching local friends with Twitter friends. + -i --id Identity (default 'generic') + -d --debug Debug (lots of log output) + +END_OF_TRIM_HELP; + +require_once INSTALLDIR . '/scripts/commandline.inc'; +require_once INSTALLDIR . '/lib/parallelizingdaemon.php'; + +/** + * Daemon to sync local friends with Twitter friends + * + * @category Twitter + * @package Laconica + * @author Zach Copley <zach@controlyourself.ca> + * @author Evan Prodromou <evan@controlyourself.ca> + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ $helptext = <<<END_OF_TWITTER_HELP Batch script for synching local friends with Twitter friends. END_OF_TWITTER_HELP; -require_once INSTALLDIR.'/scripts/commandline.inc'; +require_once INSTALLDIR . '/scripts/commandline.inc'; +require_once INSTALLDIR . '/lib/parallelizingdaemon.php'; -// Make a lockfile -$lockfilename = lockFilename(); -if (!($lockfile = @fopen($lockfilename, "w"))) { - print "Already running... exiting.\n"; - exit(1); -} +class SyncTwitterFriendsDaemon extends ParallelizingDaemon +{ + /** + * Constructor + * + * @param string $id the name/id of this daemon + * @param int $interval sleep this long before doing everything again + * @param int $max_children maximum number of child processes at a time + * @param boolean $debug debug output flag + * + * @return void + * + **/ -// Obtain an exlcusive lock on file (will fail if script is already going) -if (!@flock( $lockfile, LOCK_EX | LOCK_NB, &$wouldblock) || $wouldblock) { - // Script already running - abort - @fclose($lockfile); - print "Already running... exiting.\n"; - exit(1); -} + function __construct($id = null, $interval = 60, + $max_children = 2, $debug = null) + { + parent::__construct($id, $interval, $max_children, $debug); + } -$flink = new Foreign_link(); -$flink->service = 1; // Twitter -$flink->orderBy('last_friendsync'); -$flink->limit(25); // sync this many users during this run -$cnt = $flink->find(); + /** + * Name of this daemon + * + * @return string Name of the daemon. + */ -print "Updating Twitter friends subscriptions for $cnt users.\n"; + function name() + { + return ('synctwitterfriends.' . $this->_id); + } -while ($flink->fetch()) { + /** + * Find all the Twitter foreign links for users who have requested + * automatically subscribing to their Twitter friends locally. + * + * @return array flinks an array of Foreign_link objects + */ + function getObjects() + { + $flinks = array(); + $flink = new Foreign_link(); - if (($flink->friendsync & FOREIGN_FRIEND_RECV) == FOREIGN_FRIEND_RECV) { + $conn = &$flink->getDatabaseConnection(); - $user = User::staticGet($flink->user_id); + $flink->service = TWITTER_SERVICE; + $flink->orderBy('last_friendsync'); + $flink->limit(25); // sync this many users during this run + $flink->find(); - if (empty($user)) { - common_log(LOG_WARNING, "Unmatched user for ID " . $flink->user_id); - print "Unmatched user for ID $flink->user_id\n"; - continue; + while ($flink->fetch()) { + if (($flink->friendsync & FOREIGN_FRIEND_RECV) == FOREIGN_FRIEND_RECV) { + $flinks[] = clone($flink); + } } - print "Updating Twitter friends for $user->nickname (Laconica ID: $user->id)... "; + $conn->disconnect(); - $fuser = $flink->getForeignUser(); + global $_DB_DATAOBJECT; + unset($_DB_DATAOBJECT['CONNECTIONS']); - if (empty($fuser)) { - common_log(LOG_WARNING, "Unmatched user for ID " . $flink->user_id); - print "Unmatched user for ID $flink->user_id\n"; - continue; - } + return $flinks; + } + + function childTask($flink) { - save_twitter_friends($user, $fuser->id, $fuser->nickname, $flink->credentials); + // Each child ps needs its own DB connection + + // Note: DataObject::getDatabaseConnection() creates + // a new connection if there isn't one already + + $conn = &$flink->getDatabaseConnection(); + + $this->subscribeTwitterFriends($flink); $flink->last_friendsync = common_sql_now(); $flink->update(); - if (defined('SCRIPT_DEBUG')) { - print "\nDONE\n"; - } else { - print "DONE\n"; + $conn->disconnect(); + + // XXX: Couldn't find a less brutal way to blow + // away a cached connection + + global $_DB_DATAOBJECT; + unset($_DB_DATAOBJECT['CONNECTIONS']); + } + + function fetchTwitterFriends($flink) + { + $friends = array(); + + $token = TwitterOAuthClient::unpackToken($flink->credentials); + + $client = new TwitterOAuthClient($token->key, $token->secret); + + try { + $friends_ids = $client->friendsIds(); + } catch (OAuthCurlException $e) { + common_log(LOG_WARNING, $this->name() . + ' - cURL error getting friend ids ' . + $e->getCode() . ' - ' . $e->getMessage()); + return $friends; + } + + if (empty($friends_ids)) { + common_debug($this->name() . + " - Twitter user $flink->foreign_id " . + 'doesn\'t have any friends!'); + return $friends; + } + + common_debug($this->name() . ' - Twitter\'s API says Twitter user id ' . + "$flink->foreign_id has " . + count($friends_ids) . ' friends.'); + + // Calculate how many pages to get... + $pages = ceil(count($friends_ids) / 100); + + if ($pages == 0) { + common_debug($this->name() . " - $user seems to have no friends."); + } + + for ($i = 1; $i <= $pages; $i++) { + + try { + $more_friends = $client->statusesFriends(null, null, null, $i); + } catch (OAuthCurlException $e) { + common_log(LOG_WARNING, $this->name() . + ' - cURL error getting Twitter statuses/friends ' . + "page $i - " . $e->getCode() . ' - ' . + $e->getMessage()); } + + if (empty($more_friends)) { + common_log(LOG_WARNING, $this->name() . + " - Couldn't retrieve page $i " . + "of Twitter user $flink->foreign_id friends."); + continue; + } else { + $friends = array_merge($friends, $more_friends); + } + } + + return $friends; } -} -function lockFilename() -{ - $piddir = common_config('daemon', 'piddir'); - if (!$piddir) { - $piddir = '/var/run'; + function subscribeTwitterFriends($flink) + { + $friends = $this->fetchTwitterFriends($flink); + + if (empty($friends)) { + common_debug($this->name() . + ' - Couldn\'t get friends from Twitter for ' . + "Twitter user $flink->foreign_id."); + return false; + } + + $user = $flink->getUser(); + + foreach ($friends as $friend) { + + $friend_name = $friend->screen_name; + $friend_id = (int) $friend->id; + + // Update or create the Foreign_user record for each + // Twitter friend + + if (!save_twitter_user($friend_id, $friend_name)) { + common_log(LOG_WARNING, $this-name() . + " - Couldn't save $screen_name's friend, $friend_name."); + continue; + } + + // Check to see if there's a related local user + + $friend_flink = Foreign_link::getByForeignID($friend_id, + TWITTER_SERVICE); + + if (!empty($friend_flink)) { + + // Get associated user and subscribe her + + $friend_user = User::staticGet('id', $friend_flink->user_id); + + if (!empty($friend_user)) { + $result = subs_subscribe_to($user, $friend_user); + + if ($result === true) { + common_log(LOG_INFO, + $this->name() . ' - Subscribed ' . + "$friend_user->nickname to $user->nickname."); + } else { + common_debug($this->name() . + ' - Tried subscribing ' . + "$friend_user->nickname to $user->nickname - " . + $result); + } + } + } + } + + return true; } - return $piddir . '/synctwitterfriends.lock'; } -// Cleanup -fclose($lockfile); -unlink($lockfilename); +$id = null; +$debug = null; + +if (have_option('i')) { + $id = get_option_value('i'); +} else if (have_option('--id')) { + $id = get_option_value('--id'); +} else if (count($args) > 0) { + $id = $args[0]; +} else { + $id = null; +} + +if (have_option('d') || have_option('debug')) { + $debug = true; +} + +$syncer = new SyncTwitterFriendsDaemon($id, 60, 2, $debug); +$syncer->runOnce(); -exit(0); diff --git a/scripts/twitterstatusfetcher.php b/scripts/twitterstatusfetcher.php index e1745cfc0..f5289c5f4 100755 --- a/scripts/twitterstatusfetcher.php +++ b/scripts/twitterstatusfetcher.php @@ -56,17 +56,23 @@ require_once INSTALLDIR . '/lib/daemon.php'; // NOTE: an Avatar path MUST be set in config.php for this // script to work: e.g.: $config['avatar']['path'] = '/laconica/avatar'; -class TwitterStatusFetcher extends Daemon +class TwitterStatusFetcher extends ParallelizingDaemon { - private $_children = array(); - - function __construct($id=null, $daemonize=true) + /** + * Constructor + * + * @param string $id the name/id of this daemon + * @param int $interval sleep this long before doing everything again + * @param int $max_children maximum number of child processes at a time + * @param boolean $debug debug output flag + * + * @return void + * + **/ + function __construct($id = null, $interval = 60, + $max_children = 2, $debug = null) { - parent::__construct($daemonize); - - if ($id) { - $this->set_id($id); - } + parent::__construct($id, $interval, $max_children, $debug); } /** @@ -81,126 +87,22 @@ class TwitterStatusFetcher extends Daemon } /** - * Run the daemon + * Find all the Twitter foreign links for users who have requested + * importing of their friends' timelines * - * @return void + * @return array flinks an array of Foreign_link objects */ - function run() + function getObjects() { - if (defined('SCRIPT_DEBUG')) { - common_debug($this->name() . - ': debugging log output enabled.'); - } - - do { - - $flinks = $this->refreshFlinks(); - - foreach ($flinks as $f) { - - // We have to disconnect from the DB before forking so - // each sub-process will open its own connection and - // avoid stomping on the others - - $conn = &$f->getDatabaseConnection(); - $conn->disconnect(); - - $pid = pcntl_fork(); - - if ($pid == -1) { - die ("Couldn't fork!"); - } - - if ($pid) { - - // Parent - if (defined('SCRIPT_DEBUG')) { - common_debug("Parent: forked new status ". - " fetcher process " . $pid); - } - - $this->_children[] = $pid; - - } else { - - // Child - $this->getTimeline($f); - exit(); - } - - // Remove child from ps list as it finishes - while (($c = pcntl_wait($status, WNOHANG OR WUNTRACED)) > 0) { - - if (defined('SCRIPT_DEBUG')) { - common_debug("Child $c finished."); - } - - $this->removePs($this->_children, $c); - } - - // Wait! We have too many damn kids. - if (sizeof($this->_children) > MAXCHILDREN) { - - if (defined('SCRIPT_DEBUG')) { - common_debug('Too many children. Waiting...'); - } - - if (($c = pcntl_wait($status, WUNTRACED)) > 0) { - - if (defined('SCRIPT_DEBUG')) { - common_debug("Finished waiting for $c"); - } - - $this->removePs($this->_children, $c); - } - } - } - - // Remove all children from the process list before restarting - while (($c = pcntl_wait($status, WUNTRACED)) > 0) { - - if (defined('SCRIPT_DEBUG')) { - common_debug("Child $c finished."); - } - - $this->removePs($this->_children, $c); - } - - // Rest for a bit before we fetch more statuses - - if (defined('SCRIPT_DEBUG')) { - common_debug('Waiting ' . POLL_INTERVAL . - ' secs before hitting Twitter again.'); - } - - if (POLL_INTERVAL > 0) { - sleep(POLL_INTERVAL); - } - - } while (true); - } - - /** - * Refresh the foreign links for this user - * - * @return void - */ + global $_DB_DATAOBJECT; - function refreshFlinks() - { $flink = new Foreign_link(); + $conn = &$flink->getDatabaseConnection(); - $flink->service = 1; // Twitter - + $flink->service = TWITTER_SERVICE; $flink->orderBy('last_noticesync'); - - $cnt = $flink->find(); - - if (defined('SCRIPT_DEBUG')) { - common_debug('Updating Twitter friends subscriptions' . - " for $cnt users."); - } + $flink->find(); $flinks = array(); @@ -215,78 +117,81 @@ class TwitterStatusFetcher extends Daemon $flink->free(); unset($flink); + $conn->disconnect(); + unset($_DB_DATAOBJECT['CONNECTIONS']); + return $flinks; } - /** - * Unknown - * - * @param array &$plist unknown. - * @param string $ps unknown. - * - * @return unknown - * @todo document - */ + function childTask($flink) { - function removePs(&$plist, $ps) - { - for ($i = 0; $i < sizeof($plist); $i++) { - if ($plist[$i] == $ps) { - unset($plist[$i]); - $plist = array_values($plist); - break; - } - } + // Each child ps needs its own DB connection + + // Note: DataObject::getDatabaseConnection() creates + // a new connection if there isn't one already + + $conn = &$flink->getDatabaseConnection(); + + $this->getTimeline($flink); + + $flink->last_friendsync = common_sql_now(); + $flink->update(); + + $conn->disconnect(); + + // XXX: Couldn't find a less brutal way to blow + // away a cached connection + + global $_DB_DATAOBJECT; + unset($_DB_DATAOBJECT['CONNECTIONS']); } function getTimeline($flink) { - if (empty($flink)) { - common_log(LOG_WARNING, - "Can't retrieve Foreign_link for foreign ID $fid"); - return; - } - - $fuser = $flink->getForeignUser(); - - if (empty($fuser)) { - common_log(LOG_WARNING, "Unmatched user for ID " . - $flink->user_id); + if (empty($flink)) { + common_log(LOG_WARNING, $this->name() . + " - Can't retrieve Foreign_link for foreign ID $fid"); return; } - if (defined('SCRIPT_DEBUG')) { - common_debug('Trying to get timeline for Twitter user ' . - "$fuser->nickname ($flink->foreign_id)."); - } + 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. - $url = 'http://twitter.com/statuses/friends_timeline.json'; + $token = TwitterOAuthClient::unpackToken($flink->credentials); + + $client = new TwitterOAuthClient($token->key, $token->secret); - $timeline_json = get_twitter_data($url, $fuser->nickname, - $flink->credentials); + $timeline = null; - $timeline = json_decode($timeline_json); + try { + $timeline = $client->statusesFriendsTimeline(); + } catch (OAuthClientCurlException $e) { + common_log(LOG_WARNING, $this->name() . + ' - OAuth client unable to get friends timeline for user ' . + $flink->user_id . ' - code: ' . + $e->getCode() . 'msg: ' . $e->getMessage()); + } if (empty($timeline)) { - common_log(LOG_WARNING, "Empty timeline."); + common_log(LOG_WARNING, $this->name() . " - Empty timeline."); return; } // Reverse to preserve order + foreach (array_reverse($timeline) as $status) { // Hacktastic: filter out stuff coming from this Laconica + $source = mb_strtolower(common_config('integration', 'source')); if (preg_match("/$source/", mb_strtolower($status->source))) { - if (defined('SCRIPT_DEBUG')) { - common_debug('Skipping import of status ' . $status->id . - ' with source ' . $source); - } + common_debug($this->name() . ' - Skipping import of status ' . + $status->id . ' with source ' . $source); continue; } @@ -294,6 +199,7 @@ class TwitterStatusFetcher extends Daemon } // Okay, record the time we synced with Twitter for posterity + $flink->last_noticesync = common_sql_now(); $flink->update(); } @@ -301,11 +207,12 @@ class TwitterStatusFetcher extends Daemon function saveStatus($status, $flink) { $id = $this->ensureProfile($status->user); + $profile = Profile::staticGet($id); - if (!$profile) { - common_log(LOG_ERR, - 'Problem saving notice. No associated Profile.'); + if (empty($profile)) { + common_log(LOG_ERR, $this->name() . + ' - Problem saving notice. No associated Profile.'); return null; } @@ -318,7 +225,7 @@ class TwitterStatusFetcher extends Daemon // check to see if we've already imported the status - if (!$notice) { + if (empty($notice)) { $notice = new Notice(); @@ -329,7 +236,7 @@ class TwitterStatusFetcher extends Daemon $notice->content = common_shorten_links($status->text); // XXX $notice->rendered = common_render_content($notice->content, $notice); $notice->source = 'twitter'; - $notice->reply_to = null; // XXX lookup reply + $notice->reply_to = null; // XXX: lookup reply $notice->is_local = Notice::GATEWAY; if (Event::handle('StartNoticeSave', array(&$notice))) { @@ -355,24 +262,22 @@ class TwitterStatusFetcher extends Daemon function ensureProfile($user) { // check to see if there's already a profile for this user + $profileurl = 'http://twitter.com/' . $user->screen_name; $profile = Profile::staticGet('profileurl', $profileurl); - if ($profile) { - if (defined('SCRIPT_DEBUG')) { - common_debug("Profile for $profile->nickname found."); - } + if (!empty($profile)) { + common_debug($this->name() . + " - Profile for $profile->nickname found."); // Check to see if the user's Avatar has changed - $this->checkAvatar($user, $profile); + $this->checkAvatar($user, $profile); return $profile->id; } else { - if (defined('SCRIPT_DEBUG')) { - common_debug('Adding profile and remote profile ' . - "for Twitter user: $profileurl"); - } + common_debug($this->name() . ' - Adding profile and remote profile ' . + "for Twitter user: $profileurl."); $profile = new Profile(); $profile->query("BEGIN"); @@ -394,9 +299,10 @@ class TwitterStatusFetcher extends Daemon } // check for remote profile + $remote_pro = Remote_profile::staticGet('uri', $profileurl); - if (!$remote_pro) { + if (empty($remote_pro)) { $remote_pro = new Remote_profile(); @@ -433,23 +339,18 @@ class TwitterStatusFetcher extends Daemon $oldname = $profile->getAvatar(48)->filename; if ($newname != $oldname) { - - if (defined('SCRIPT_DEBUG')) { - common_debug('Avatar for Twitter user ' . - "$profile->nickname has changed."); - common_debug("old: $oldname new: $newname"); - } + common_debug($this->name() . ' - Avatar for Twitter user ' . + "$profile->nickname has changed."); + common_debug($this->name() . " - old: $oldname new: $newname"); $this->updateAvatars($twitter_user, $profile); } if ($this->missingAvatarFile($profile)) { - - if (defined('SCRIPT_DEBUG')) { - common_debug('Twitter user ' . $profile->nickname . - ' is missing one or more local avatars.'); - common_debug("old: $oldname new: $newname"); - } + common_debug($this->name() . ' - Twitter user ' . + $profile->nickname . + ' is missing one or more local avatars.'); + common_debug($this->name() ." - old: $oldname new: $newname"); $this->updateAvatars($twitter_user, $profile); } @@ -529,23 +430,20 @@ class TwitterStatusFetcher extends Daemon if ($this->fetchAvatar($url, $filename)) { $this->newAvatar($id, $size, $mediatype, $filename); } else { - common_log(LOG_WARNING, "Problem fetching Avatar: $url", __FILE__); + common_log(LOG_WARNING, $this->id() . + " - Problem fetching Avatar: $url"); } } } function updateAvatar($profile_id, $size, $mediatype, $filename) { - if (defined('SCRIPT_DEBUG')) { - common_debug("Updating avatar: $size"); - } + common_debug($this->name() . " - Updating avatar: $size"); $profile = Profile::staticGet($profile_id); if (empty($profile)) { - if (defined('SCRIPT_DEBUG')) { - common_debug("Couldn't get profile: $profile_id!"); - } + common_debug($this->name() . " - Couldn't get profile: $profile_id!"); return; } @@ -553,6 +451,7 @@ class TwitterStatusFetcher extends Daemon $avatar = $profile->getAvatar($sizes[$size]); // Delete the avatar, if present + if ($avatar) { $avatar->delete(); } @@ -590,9 +489,7 @@ class TwitterStatusFetcher extends Daemon $avatar->filename = $filename; $avatar->url = Avatar::url($filename); - if (defined('SCRIPT_DEBUG')) { - common_debug("new filename: $avatar->url"); - } + common_debug($this->name() . " - New filename: $avatar->url"); $avatar->created = common_sql_now(); @@ -603,9 +500,8 @@ class TwitterStatusFetcher extends Daemon return null; } - if (defined('SCRIPT_DEBUG')) { - common_debug("Saved new $size avatar for $profile_id."); - } + common_debug($this->name() . + " - Saved new $size avatar for $profile_id."); return $id; } @@ -618,13 +514,12 @@ class TwitterStatusFetcher extends Daemon $out = fopen($avatarfile, 'wb'); if (!$out) { - common_log(LOG_WARNING, "Couldn't open file $filename", __FILE__); + common_log(LOG_WARNING, $this->name() . + " - Couldn't open file $filename"); return false; } - if (defined('SCRIPT_DEBUG')) { - common_debug("Fetching avatar: $url"); - } + common_debug($this->name() . " - Fetching Twitter avatar: $url"); $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); @@ -641,7 +536,8 @@ class TwitterStatusFetcher extends Daemon } } -declare(ticks = 1); +$id = null; +$debug = null; if (have_option('i')) { $id = get_option_value('i'); @@ -654,9 +550,9 @@ if (have_option('i')) { } if (have_option('d') || have_option('debug')) { - define('SCRIPT_DEBUG', true); + $debug = true; } -$fetcher = new TwitterStatusFetcher($id); +$fetcher = new TwitterStatusFetcher($id, 60, 2, $debug); $fetcher->runOnce(); diff --git a/theme/default/css/display.css b/theme/default/css/display.css index 921a6b27b..646fb0445 100644 --- a/theme/default/css/display.css +++ b/theme/default/css/display.css @@ -66,7 +66,7 @@ div.notice-options input, .entity_nudge p, .form_settings input.form_action-primary, .form_make_admin input.submit { -color:#002E6E; +color:#002FA7; } .notice, diff --git a/theme/identica/css/display.css b/theme/identica/css/display.css index 8af5644b6..0688db425 100644 --- a/theme/identica/css/display.css +++ b/theme/identica/css/display.css @@ -66,7 +66,7 @@ div.notice-options input, .entity_nudge p, .form_settings input.form_action-primary, .form_make_admin input.submit { -color:#002E6E; +color:#002FA7; } .notice, |