summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--config.php.sample3
-rw-r--r--extlib/DB/DataObject.php17
-rw-r--r--lib/common.php2
-rwxr-xr-xscripts/getvaliddaemons.php3
-rw-r--r--scripts/statusfetcher.php582
-rwxr-xr-xscripts/stopdaemons.sh2
-rwxr-xr-xscripts/twitterstatusfetcher.php576
7 files changed, 600 insertions, 585 deletions
diff --git a/config.php.sample b/config.php.sample
index b8ed45fa8..6d6a9b533 100644
--- a/config.php.sample
+++ b/config.php.sample
@@ -150,6 +150,9 @@ $config['sphinx']['port'] = 3312;
#$config['memcached']['server'] = 'localhost';
#$config['memcached']['port'] = 11211;
+# Enable bidirectional Twitter bridge
+#$config['twitterbridge']['enabled'] = true;
+
#Twitter integration source attribute. Note: default is Laconica
#$config['integration']['source'] = 'Laconica';
diff --git a/extlib/DB/DataObject.php b/extlib/DB/DataObject.php
index b1a1a4e21..0c6a13dc2 100644
--- a/extlib/DB/DataObject.php
+++ b/extlib/DB/DataObject.php
@@ -2357,6 +2357,8 @@ class DB_DataObject extends DB_DataObject_Overload
$t= explode(' ',microtime());
$_DB_DATAOBJECT['QUERYENDTIME'] = $time = $t[0]+$t[1];
+
+ do {
if ($_DB_driver == 'DB') {
$result = $DB->query($string);
@@ -2374,8 +2376,19 @@ class DB_DataObject extends DB_DataObject_Overload
break;
}
}
-
-
+
+ // try to reconnect, at most 3 times
+ $again = false;
+ if (is_a($result, 'PEAR_Error')
+ AND $result->getCode() == DB_ERROR_NODBSELECTED
+ AND $cpt++<3) {
+ $DB->disconnect();
+ sleep(1);
+ $DB->connect($DB->dsn);
+ $again = true;
+ }
+
+ } while ($again);
if (is_a($result,'PEAR_Error')) {
if (!empty($_DB_DATAOBJECT['CONFIG']['debug'])) {
diff --git a/lib/common.php b/lib/common.php
index 00e5b0bc2..abdc22c0e 100644
--- a/lib/common.php
+++ b/lib/common.php
@@ -143,6 +143,8 @@ $config =
array('piddir' => '/var/run',
'user' => false,
'group' => false),
+ 'twitterbridge' =>
+ array('enabled' => false),
'integration' =>
array('source' => 'Laconica', # source attribute for Twitter
'taguri' => $_server.',2009'), # base for tag URIs
diff --git a/scripts/getvaliddaemons.php b/scripts/getvaliddaemons.php
index 482e63af7..a10233e69 100755
--- a/scripts/getvaliddaemons.php
+++ b/scripts/getvaliddaemons.php
@@ -44,6 +44,9 @@ if(common_config('xmpp','enabled')) {
if(common_config('memcached','enabled')) {
echo "memcachedqueuehandler.php ";
}
+if(common_config('twitterbridge','enabled')) {
+ echo "twitterstatusfetcher.php ";
+}
echo "ombqueuehandler.php ";
echo "twitterqueuehandler.php ";
echo "facebookqueuehandler.php ";
diff --git a/scripts/statusfetcher.php b/scripts/statusfetcher.php
deleted file mode 100644
index 8c3ee4330..000000000
--- a/scripts/statusfetcher.php
+++ /dev/null
@@ -1,582 +0,0 @@
-#!/usr/bin/env php
-<?php
-/*
- * Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
-
-// Abort if called from a web server
-if (isset($_SERVER) && array_key_exists('REQUEST_METHOD', $_SERVER)) {
- print "This script must be run from the command line\n";
- exit();
-}
-
-define('INSTALLDIR', realpath(dirname(__FILE__) . '/..'));
-define('LACONICA', true);
-
-// Tune number of processes and how often to poll Twitter
-define('MAXCHILDREN', 5);
-define('POLL_INTERVAL', 60 * 5); // in seconds
-
-// Uncomment this to get useful console output
-define('SCRIPT_DEBUG', true);
-
-require_once(INSTALLDIR . '/lib/common.php');
-
-$children = array();
-
-do {
-
- $flink_ids = refreshFlinks();
-
- foreach ($flink_ids as $f){
-
- $pid = pcntl_fork();
-
- if ($pid == -1) {
- die ("Couldn't fork!");
- }
-
- // Parent
- if ($pid) {
- if (defined('SCRIPT_DEBUG')) {
- print "Parent: forked " . $pid . "\n";
- }
- $children[] = $pid;
- } else {
-
- // Child
-
- // XXX: Each child needs its own DB connection
- getTimeline($f);
- exit();
- }
-
- // Remove child from ps list as it finishes
- while(($c = pcntl_wait($status, WNOHANG OR WUNTRACED)) > 0) {
- if (defined('SCRIPT_DEBUG')) {
- print "Child $c finished.\n";
- }
- remove_ps($children, $c);
- }
-
- // Wait if we have too many kids
- if(sizeof($children) > MAXCHILDREN) {
- if (defined('SCRIPT_DEBUG')) {
- print "Too many children. Waiting...\n";
- }
- if(($c = pcntl_wait($status, WUNTRACED)) > 0){
- if (defined('SCRIPT_DEBUG')) {
- print "Finished waiting for $c\n";
- }
- remove_ps($children, $c);
- }
- }
- }
-
- // Remove all children from the process list before restarting
- while(($c = pcntl_wait($status, WUNTRACED)) > 0) {
- if (defined('SCRIPT_DEBUG')) {
- print "Child $c finished.\n";
- }
- remove_ps($children, $c);
- }
-
- // Rest for a bit before we fetch more statuses
- if (defined('SCRIPT_DEBUG')) {
- print 'Waiting ' . POLL_INTERVAL .
- " secs before hitting Twitter again.\n";
- }
-
- sleep(POLL_INTERVAL);
-
-} while (true);
-
-
-function refreshFlinks() {
-
- global $config;
-
- $flink = new Foreign_link();
- $flink->service = 1; // Twitter
- $flink->orderBy('last_noticesync');
-
- $cnt = $flink->find();
-
- if (defined('SCRIPT_DEBUG')) {
- print "Updating Twitter friends subscriptions for $cnt users.\n";
- }
-
- $flinks = array();
-
- while ($flink->fetch()) {
-
- if (($flink->noticesync & FOREIGN_NOTICE_RECV) == FOREIGN_NOTICE_RECV) {
- $flinks[] = clone($flink);
- }
- }
-
- $flink->free();
- unset($flink);
-
- return $flinks;
-}
-
-function remove_ps(&$plist, $ps){
- for($i = 0; $i < sizeof($plist); $i++){
- if($plist[$i] == $ps){
- unset($plist[$i]);
- $plist = array_values($plist);
- break;
- }
- }
-}
-
-function getTimeline($flink)
-{
-
- global $config;
- $config['db'] = &PEAR::getStaticProperty('DB_DataObject','options');
- require_once(INSTALLDIR . '/lib/common.php');
-
- if (defined('SCRIPT_DEBUG')) {
- print "Trying to get timeline for $flink->foreign_id\n";
- }
-
- if (empty($flink)) {
- common_log(LOG_WARNING, "Can't retrieve Foreign_link for foreign ID $fid");
- if (defined('SCRIPT_DEBUG')) {
- print "Can't retrieve Foreign_link for foreign ID $fid\n";
- }
- return;
- }
-
- $fuser = new Foreign_user();
- $fuser->service = 1;
- $fuser->id = $flink->foreign_id;
- $fuser->limit(1);
- $fuser->find(true);
-
- if (empty($fuser)) {
- common_log(LOG_WARNING, "Unmatched user for ID " . $flink->user_id);
- if (defined('SCRIPT_DEBUG')) {
- print "Unmatched user for ID $flink->user_id\n";
- }
- return;
- }
-
- if (defined('SCRIPT_DEBUG')) {
- // XXX: This is horrible and must be removed before releasing this
- print 'username: ' . $fuser->nickname . ' password: ' . $flink->credentials . "\n";
- }
-
- $url = 'http://twitter.com/statuses/friends_timeline.json';
-
- $timeline_json = get_twitter_data($url, $fuser->nickname,
- $flink->credentials);
-
- $timeline = json_decode($timeline_json);
-
- if (empty($timeline)) {
- common_log(LOG_WARNING, "Empty timeline.");
- if (defined('SCRIPT_DEBUG')) {
- print "Empty timeline!\n";
- }
- return;
- }
-
- foreach ($timeline as $status) {
-
- // Hacktastic: filter out stuff coming from Laconica
- $source = mb_strtolower(common_config('integration', 'source'));
-
- if (preg_match("/$source/", mb_strtolower($status->source))) {
- continue;
- }
-
- saveStatus($status, $flink);
- }
-
- // Okay, record the time we synced with Twitter for posterity
-
- $flink->last_noticesync = common_sql_now();
- $flink->update();
-}
-
-function saveStatus($status, $flink)
-{
-
- global $config;
- $config['db'] = &PEAR::getStaticProperty('DB_DataObject','options');
- require_once(INSTALLDIR . '/lib/common.php');
-
- // Do we have a profile for this Twitter user?
-
- $id = ensureProfile($status->user);
- $profile = Profile::staticGet($id);
-
- if (!$profile) {
- common_log(LOG_ERR, 'Problem saving notice. No associated Profile.');
- if (defined('SCRIPT_DEBUG')) {
- print "Problem saving notice. No associated Profile.\n";
- }
- return null;
- }
-
- $uri = 'http://twitter.com/' . $status->user->screen_name .
- '/status/' . $status->id;
-
- // Skip save if notice source is Laconica or Identi.ca?
-
- $notice = Notice::staticGet('uri', $uri);
-
- // check to see if we've already imported the status
- if (!$notice) {
-
- $notice = new Notice();
- $notice->profile_id = $id;
-
- $notice->query('BEGIN');
-
- // XXX: figure out reply_to
- $notice->reply_to = null;
-
- // XXX: Should this be common_sql_now() instead of status create date?
-
- $notice->created = strftime('%Y-%m-%d %H:%M:%S',
- strtotime($status->created_at));
- $notice->content = $status->text;
- $notice->rendered = common_render_content($status->text, $notice);
- $notice->source = 'twitter';
- $notice->is_local = 0;
- $notice->uri = $uri;
-
- $notice_id = $notice->insert();
-
- if (!$notice_id) {
- common_log_db_error($notice, 'INSERT', __FILE__);
- if (defined('SCRIPT_DEBUG')) {
- print "Could not save notice!\n";
- }
- }
-
- // XXX: Figure out a better way to link replies?
- $notice->saveReplies();
-
- // XXX: Do we want to polute our tag cloud with hashtags from Twitter?
- $notice->saveTags();
- $notice->saveGroups();
-
- $notice->query('COMMIT');
-
- if (defined('SCRIPT_DEBUG')) {
- print "Saved status $status->id as notice $notice->id.\n";
- }
- }
-
- if (!Notice_inbox::staticGet('notice_id', $notice->id)) {
-
- // Add to inbox
- $inbox = new Notice_inbox();
- $inbox->user_id = $flink->user_id;
- $inbox->notice_id = $notice->id;
- $inbox->created = common_sql_now();
-
- $inbox->insert();
- }
-}
-
-function ensureProfile($user)
-{
- global $config;
-
- // 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) {
- common_debug("Profile for $profile->nickname found.");
-
- // Check to see if the user's Avatar has changed
- checkAvatar($user, $profile);
- return $profile->id;
-
- } else {
- $debugmsg = 'Adding profile and remote profile ' .
- "for Twitter user: $profileurl\n";
- common_debug($debugmsg, __FILE__);
- if (defined('SCRIPT_DEBUG')) {
- print $debugmsg;
- }
-
- $profile = new Profile();
- $profile->query("BEGIN");
-
- $profile->nickname = $user->screen_name;
- $profile->fullname = $user->name;
- $profile->homepage = $user->url;
- $profile->bio = $user->description;
- $profile->location = $user->location;
- $profile->profileurl = $profileurl;
- $profile->created = common_sql_now();
-
- $id = $profile->insert();
-
- if (empty($id)) {
- common_log_db_error($profile, 'INSERT', __FILE__);
- if (defined('SCRIPT_DEBUG')) {
- print 'Could not insert Profile: ' .
- common_log_objstring($profile) . "\n";
- }
- $profile->query("ROLLBACK");
- return false;
- }
-
- // check for remote profile
- $remote_pro = Remote_profile::staticGet('uri', $profileurl);
-
- if (!$remote_pro) {
-
- $remote_pro = new Remote_profile();
-
- $remote_pro->id = $id;
- $remote_pro->uri = $profileurl;
- $remote_pro->created = common_sql_now();
-
- $rid = $remote_pro->insert();
-
- if (empty($rid)) {
- common_log_db_error($profile, 'INSERT', __FILE__);
- if (defined('SCRIPT_DEBUG')) {
- print 'Could not insert Remote_profile: ' .
- common_log_objstring($remote_pro) . "\n";
- }
- $profile->query("ROLLBACK");
- return false;
- }
- }
-
- $profile->query("COMMIT");
-
- saveAvatars($user, $id);
-
- return $id;
- }
-}
-
-function checkAvatar($user, $profile)
-{
- global $config;
-
- $path_parts = pathinfo($user->profile_image_url);
- $newname = 'Twitter_' . $user->id . '_' .
- $path_parts['basename'];
-
- $oldname = $profile->getAvatar(48)->filename;
-
- if ($newname != $oldname) {
-
- common_debug("Avatar for Twitter user $profile->nickname has changed.");
- common_debug("old: $oldname new: $newname");
-
- if (defined('SCRIPT_DEBUG')) {
- print "Avatar for Twitter user $user->id has changed.\n";
- print "old: $oldname\n";
- print "new: $newname\n";
- }
-
- $img_root = substr($path_parts['basename'], 0, -11);
- $ext = $path_parts['extension'];
- $mediatype = getMediatype($ext);
-
- foreach (array('mini', 'normal', 'bigger') as $size) {
- $url = $path_parts['dirname'] . '/' .
- $img_root . '_' . $size . ".$ext";
- $filename = 'Twitter_' . $user->id . '_' .
- $img_root . "_$size.$ext";
-
- if (fetchAvatar($url, $filename)) {
- updateAvatar($profile->id, $size, $mediatype, $filename);
- }
- }
- }
-}
-
-function getMediatype($ext)
-{
- $mediatype = null;
-
- switch (strtolower($ext)) {
- case 'jpg':
- $mediatype = 'image/jpg';
- break;
- case 'gif':
- $mediatype = 'image/gif';
- break;
- default:
- $mediatype = 'image/png';
- }
-
- return $mediatype;
-}
-
-function saveAvatars($user, $id)
-{
- global $config;
-
- $path_parts = pathinfo($user->profile_image_url);
- $ext = $path_parts['extension'];
- $end = strlen('_normal' . $ext);
- $img_root = substr($path_parts['basename'], 0, -($end+1));
- $mediatype = getMediatype($ext);
-
- foreach (array('mini', 'normal', 'bigger') as $size) {
- $url = $path_parts['dirname'] . '/' .
- $img_root . '_' . $size . ".$ext";
- $filename = 'Twitter_' . $user->id . '_' .
- $img_root . "_$size.$ext";
-
- if (fetchAvatar($url, $filename)) {
- newAvatar($id, $size, $mediatype, $filename);
- } else {
- common_log(LOG_WARNING, "Problem fetching Avatar: $url", __FILE__);
- if (defined('SCRIPT_DEBUG')) {
- print "Problem fetching Avatar: $url\n";
- }
- }
- }
-}
-
-function updateAvatar($profile_id, $size, $mediatype, $filename) {
-
- global $config;
-
- common_debug("Updating avatar: $size");
- if (defined('SCRIPT_DEBUG')) {
- print "Updating avatar: $size\n";
- }
-
- $profile = Profile::staticGet($profile_id);
-
- if (!$profile) {
- common_debug("Couldn't get profile: $profile_id!");
- if (defined('SCRIPT_DEBUG')) {
- print "Couldn't get profile: $profile_id!\n";
- }
- return;
- }
-
- $sizes = array('mini' => 24, 'normal' => 48, 'bigger' => 73);
- $avatar = $profile->getAvatar($sizes[$size]);
-
- if ($avatar) {
- common_debug("Deleting $size avatar for $profile->nickname.");
- @unlink(INSTALLDIR . '/avatar/' . $avatar->filename);
- $avatar->delete();
- }
-
- newAvatar($profile->id, $size, $mediatype, $filename);
-}
-
-function newAvatar($profile_id, $size, $mediatype, $filename)
-{
- global $config;
-
- $avatar = new Avatar();
- $avatar->profile_id = $profile_id;
-
- switch($size) {
- case 'mini':
- $avatar->width = 24;
- $avatar->height = 24;
- break;
- case 'normal':
- $avatar->width = 48;
- $avatar->height = 48;
- break;
- default:
-
- // Note: Twitter's big avatars are a different size than
- // Laconica's (Laconica's = 96)
-
- $avatar->width = 73;
- $avatar->height = 73;
- }
-
- $avatar->original = 0; // we don't have the original
- $avatar->mediatype = $mediatype;
- $avatar->filename = $filename;
- $avatar->url = Avatar::url($filename);
-
- common_debug("new filename: $avatar->url");
- if (defined('SCRIPT_DEBUG')) {
- print "New filename: $avatar->url\n";
- }
-
- $avatar->created = common_sql_now();
-
- $id = $avatar->insert();
-
- if (!$id) {
- common_log_db_error($avatar, 'INSERT', __FILE__);
- if (defined('SCRIPT_DEBUG')) {
- print "Could not insert avatar!\n";
- }
-
- return null;
- }
-
- common_debug("Saved new $size avatar for $profile_id.");
- if (defined('SCRIPT_DEBUG')) {
- print "Saved new $size avatar for $profile_id.\n";
- }
-
- return $id;
-}
-
-function fetchAvatar($url, $filename)
-{
- $avatar_dir = INSTALLDIR . '/avatar/';
-
- $avatarfile = $avatar_dir . $filename;
-
- $out = fopen($avatarfile, 'wb');
- if (!$out) {
- common_log(LOG_WARNING, "Couldn't open file $filename", __FILE__);
- if (defined('SCRIPT_DEBUG')) {
- print "Couldn't open file! $filename\n";
- }
- return false;
- }
-
- common_debug("Fetching avatar: $url", __FILE__);
- if (defined('SCRIPT_DEBUG')) {
- print "Fetching avatar from Twitter: $url\n";
- }
-
- $ch = curl_init();
- curl_setopt($ch, CURLOPT_URL, $url);
- curl_setopt($ch, CURLOPT_FILE, $out);
- curl_setopt($ch, CURLOPT_BINARYTRANSFER, true);
- curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
- curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 0);
- $result = curl_exec($ch);
- curl_close($ch);
-
- fclose($out);
-
- return $result;
-}
diff --git a/scripts/stopdaemons.sh b/scripts/stopdaemons.sh
index f6d71eddf..764037e8f 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 \
- memcachehandler inboxhandler; do
+ memcachehandler inboxhandler twitterstatusfetcher; do
FILES="$DIR/$f.*.pid"
for ff in "$FILES" ; do
diff --git a/scripts/twitterstatusfetcher.php b/scripts/twitterstatusfetcher.php
new file mode 100755
index 000000000..9dfadc760
--- /dev/null
+++ b/scripts/twitterstatusfetcher.php
@@ -0,0 +1,576 @@
+#!/usr/bin/env php
+<?php
+/*
+ * Laconica - a distributed open-source microblogging tool
+ * Copyright (C) 2008, Controlez-Vous, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+// Abort if called from a web server
+if (isset($_SERVER) && array_key_exists('REQUEST_METHOD', $_SERVER)) {
+ print "This script must be run from the command line\n";
+ exit();
+}
+
+define('INSTALLDIR', realpath(dirname(__FILE__) . '/..'));
+define('LACONICA', true);
+
+// Tune number of processes and how often to poll Twitter
+// XXX: Should these things be in config.php?
+define('MAXCHILDREN', 2);
+define('POLL_INTERVAL', 60); // in seconds
+
+// Uncomment this to get useful logging
+define('SCRIPT_DEBUG', true);
+
+require_once(INSTALLDIR . '/lib/common.php');
+require_once(INSTALLDIR . '/lib/daemon.php');
+
+class TwitterStatusFetcher extends Daemon
+{
+
+ private $children = array();
+
+ function name()
+ {
+ return ('twitterstatusfetcher.generic');
+ }
+
+ function run()
+ {
+ 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->remove_ps($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->remove_ps($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->remove_ps($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);
+ }
+
+ function refreshFlinks() {
+
+ $flink = new Foreign_link();
+ $flink->service = 1; // Twitter
+ $flink->orderBy('last_noticesync');
+
+ $cnt = $flink->find();
+
+ if (defined('SCRIPT_DEBUG')) {
+ common_debug('Updating Twitter friends subscriptions' .
+ " for $cnt users.");
+ }
+
+ $flinks = array();
+
+ while ($flink->fetch()) {
+
+ if (($flink->noticesync & FOREIGN_NOTICE_RECV) ==
+ FOREIGN_NOTICE_RECV) {
+ $flinks[] = clone($flink);
+ }
+ }
+
+ $flink->free();
+ unset($flink);
+
+ return $flinks;
+ }
+
+ function remove_ps(&$plist, $ps){
+ for ($i = 0; $i < sizeof($plist); $i++) {
+ if ($plist[$i] == $ps) {
+ unset($plist[$i]);
+ $plist = array_values($plist);
+ break;
+ }
+ }
+ }
+
+ 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);
+ return;
+ }
+
+ if (defined('SCRIPT_DEBUG')) {
+ common_debug('Trying to get timeline for Twitter user ' .
+ "$fuser->nickname ($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';
+
+ $timeline_json = get_twitter_data($url, $fuser->nickname,
+ $flink->credentials);
+
+ $timeline = json_decode($timeline_json);
+
+ if (empty($timeline)) {
+ common_log(LOG_WARNING, "Empty timeline.");
+ return;
+ }
+
+ foreach ($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);
+ }
+ continue;
+ }
+
+ $this->saveStatus($status, $flink);
+ }
+
+ // Okay, record the time we synced with Twitter for posterity
+ $flink->last_noticesync = common_sql_now();
+ $flink->update();
+ }
+
+ function saveStatus($status, $flink)
+ {
+ $id = $this->ensureProfile($status->user);
+ $profile = Profile::staticGet($id);
+
+ if (!$profile) {
+ common_log(LOG_ERR,
+ 'Problem saving notice. No associated Profile.');
+ return null;
+ }
+
+ $uri = 'http://twitter.com/' . $status->user->screen_name .
+ '/status/' . $status->id;
+
+ $notice = Notice::staticGet('uri', $uri);
+
+ // check to see if we've already imported the status
+ if (!$notice) {
+
+ $notice = new Notice();
+ $notice->profile_id = $id;
+
+ $notice->query('BEGIN');
+
+ // XXX: figure out reply_to
+ $notice->reply_to = null;
+
+ // XXX: Should this be common_sql_now() instead of status create date?
+
+ $notice->created = strftime('%Y-%m-%d %H:%M:%S',
+ strtotime($status->created_at));
+ $notice->content = $status->text;
+ $notice->rendered = common_render_content($status->text, $notice);
+ $notice->source = 'twitter';
+ $notice->is_local = 0;
+ $notice->uri = $uri;
+
+ $notice_id = $notice->insert();
+
+ if (!$notice_id) {
+ common_log_db_error($notice, 'INSERT', __FILE__);
+ if (defined('SCRIPT_DEBUG')) {
+ common_debug('Could not save notice!');
+ }
+ }
+
+ // XXX: Figure out a better way to link Twitter replies?
+ $notice->saveReplies();
+
+ // XXX: Do we want to pollute our tag cloud with
+ // hashtags from Twitter?
+ $notice->saveTags();
+ $notice->saveGroups();
+
+ $notice->query('COMMIT');
+
+ if (defined('SCRIPT_DEBUG')) {
+ common_debug("Saved status $status->id" .
+ " as notice $notice->id.");
+ }
+ }
+
+ if (!Notice_inbox::staticGet('notice_id', $notice->id)) {
+
+ // Add to inbox
+ $inbox = new Notice_inbox();
+ $inbox->user_id = $flink->user_id;
+ $inbox->notice_id = $notice->id;
+ $inbox->created = common_sql_now();
+
+ $inbox->insert();
+ }
+ }
+
+ function ensureProfile($user)
+ {
+ // check to see if there's already a profile for this user
+ $profileurl = 'http://twitter.com/' . $user->screen_name;
+ $profile = Profile::staticGet('profileurl', $profileurl);
+
+ if ($profile) {
+ if (defined('SCRIPT_DEBUG')) {
+ common_debug("Profile for $profile->nickname found.");
+ }
+
+ // Check to see if the user's Avatar has changed
+ $this->checkAvatar($user, $profile);
+
+ return $profile->id;
+
+ } else {
+ if (defined('SCRIPT_DEBUG')) {
+ common_debug('Adding profile and remote profile ' .
+ "for Twitter user: $profileurl");
+ }
+
+ $profile = new Profile();
+ $profile->query("BEGIN");
+
+ $profile->nickname = $user->screen_name;
+ $profile->fullname = $user->name;
+ $profile->homepage = $user->url;
+ $profile->bio = $user->description;
+ $profile->location = $user->location;
+ $profile->profileurl = $profileurl;
+ $profile->created = common_sql_now();
+
+ $id = $profile->insert();
+
+ if (empty($id)) {
+ common_log_db_error($profile, 'INSERT', __FILE__);
+ $profile->query("ROLLBACK");
+ return false;
+ }
+
+ // check for remote profile
+ $remote_pro = Remote_profile::staticGet('uri', $profileurl);
+
+ if (!$remote_pro) {
+
+ $remote_pro = new Remote_profile();
+
+ $remote_pro->id = $id;
+ $remote_pro->uri = $profileurl;
+ $remote_pro->created = common_sql_now();
+
+ $rid = $remote_pro->insert();
+
+ if (empty($rid)) {
+ common_log_db_error($profile, 'INSERT', __FILE__);
+ $profile->query("ROLLBACK");
+ return false;
+ }
+ }
+
+ $profile->query("COMMIT");
+
+ $this->saveAvatars($user, $id);
+
+ return $id;
+ }
+ }
+
+ function checkAvatar($user, $profile)
+ {
+ global $config;
+
+ $path_parts = pathinfo($user->profile_image_url);
+ $newname = 'Twitter_' . $user->id . '_' .
+ $path_parts['basename'];
+
+ $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");
+ }
+
+ $img_root = substr($path_parts['basename'], 0, -11);
+ $ext = $path_parts['extension'];
+ $mediatype = $this->getMediatype($ext);
+
+ foreach (array('mini', 'normal', 'bigger') as $size) {
+ $url = $path_parts['dirname'] . '/' .
+ $img_root . '_' . $size . ".$ext";
+ $filename = 'Twitter_' . $user->id . '_' .
+ $img_root . "_$size.$ext";
+
+ if ($this->fetchAvatar($url, $filename)) {
+ $this->updateAvatar($profile->id, $size, $mediatype, $filename);
+ }
+ }
+ }
+ }
+
+ function getMediatype($ext)
+ {
+ $mediatype = null;
+
+ switch (strtolower($ext)) {
+ case 'jpg':
+ $mediatype = 'image/jpg';
+ break;
+ case 'gif':
+ $mediatype = 'image/gif';
+ break;
+ default:
+ $mediatype = 'image/png';
+ }
+
+ return $mediatype;
+ }
+
+ function saveAvatars($user, $id)
+ {
+ global $config;
+
+ $path_parts = pathinfo($user->profile_image_url);
+ $ext = $path_parts['extension'];
+ $end = strlen('_normal' . $ext);
+ $img_root = substr($path_parts['basename'], 0, -($end+1));
+ $mediatype = $this->getMediatype($ext);
+
+ foreach (array('mini', 'normal', 'bigger') as $size) {
+ $url = $path_parts['dirname'] . '/' .
+ $img_root . '_' . $size . ".$ext";
+ $filename = 'Twitter_' . $user->id . '_' .
+ $img_root . "_$size.$ext";
+
+ if ($this->fetchAvatar($url, $filename)) {
+ $this->newAvatar($id, $size, $mediatype, $filename);
+ } else {
+ common_log(LOG_WARNING, "Problem fetching Avatar: $url", __FILE__);
+ }
+ }
+ }
+
+ function updateAvatar($profile_id, $size, $mediatype, $filename) {
+
+ if (defined('SCRIPT_DEBUG')) {
+ common_debug("Updating avatar: $size");
+ }
+
+ $profile = Profile::staticGet($profile_id);
+
+ if (!$profile) {
+ if (defined('SCRIPT_DEBUG')) {
+ common_debug("Couldn't get profile: $profile_id!");
+ }
+ return;
+ }
+
+ $sizes = array('mini' => 24, 'normal' => 48, 'bigger' => 73);
+ $avatar = $profile->getAvatar($sizes[$size]);
+
+ if ($avatar) {
+ if (defined('SCRIPT_DEBUG')) {
+ common_debug("Deleting $size avatar for $profile->nickname.");
+ }
+ @unlink(INSTALLDIR . '/avatar/' . $avatar->filename);
+ $avatar->delete();
+ }
+
+ $this->newAvatar($profile->id, $size, $mediatype, $filename);
+ }
+
+ function newAvatar($profile_id, $size, $mediatype, $filename)
+ {
+ global $config;
+
+ $avatar = new Avatar();
+ $avatar->profile_id = $profile_id;
+
+ switch($size) {
+ case 'mini':
+ $avatar->width = 24;
+ $avatar->height = 24;
+ break;
+ case 'normal':
+ $avatar->width = 48;
+ $avatar->height = 48;
+ break;
+ default:
+
+ // Note: Twitter's big avatars are a different size than
+ // Laconica's (Laconica's = 96)
+
+ $avatar->width = 73;
+ $avatar->height = 73;
+ }
+
+ $avatar->original = 0; // we don't have the original
+ $avatar->mediatype = $mediatype;
+ $avatar->filename = $filename;
+ $avatar->url = Avatar::url($filename);
+
+ if (defined('SCRIPT_DEBUG')) {
+ common_debug("new filename: $avatar->url");
+ }
+
+ $avatar->created = common_sql_now();
+
+ $id = $avatar->insert();
+
+ if (!$id) {
+ common_log_db_error($avatar, 'INSERT', __FILE__);
+ return null;
+ }
+
+ if (defined('SCRIPT_DEBUG')) {
+ common_debug("Saved new $size avatar for $profile_id.");
+ }
+
+ return $id;
+ }
+
+ function fetchAvatar($url, $filename)
+ {
+ $avatar_dir = INSTALLDIR . '/avatar/';
+
+ $avatarfile = $avatar_dir . $filename;
+
+ $out = fopen($avatarfile, 'wb');
+ if (!$out) {
+ common_log(LOG_WARNING, "Couldn't open file $filename", __FILE__);
+ return false;
+ }
+
+ if (defined('SCRIPT_DEBUG')) {
+ common_debug("Fetching avatar: $url");
+ }
+
+ $ch = curl_init();
+ curl_setopt($ch, CURLOPT_URL, $url);
+ curl_setopt($ch, CURLOPT_FILE, $out);
+ curl_setopt($ch, CURLOPT_BINARYTRANSFER, true);
+ curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
+ curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 0);
+ $result = curl_exec($ch);
+ curl_close($ch);
+
+ fclose($out);
+
+ return $result;
+ }
+}
+
+ini_set("max_execution_time", "0");
+ini_set("max_input_time", "0");
+set_time_limit(0);
+mb_internal_encoding('UTF-8');
+declare(ticks = 1);
+
+$fetcher = new TwitterStatusFetcher();
+$fetcher->runOnce();
+