#!/usr/bin/env php
<?php
-/*
- * Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2008, 2009, StatusNet, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* 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 console output
-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';
- }
-
- function run()
- {
- do {
-
- $flinks = $this->refreshFlinks();
-
- foreach ($flinks as $f){
+$shortoptions = 'di::';
+$longoptions = array('id::', 'debug');
- // 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!");
- }
+$helptext = <<<END_OF_TRIM_HELP
+Batch script for retrieving Twitter messages from foreign service.
- if ($pid) {
+ -i --id Identity (default 'generic')
+ -d --debug Debug (lots of log output)
- // Parent
- common_debug("Parent: forked new status fetcher process " . $pid);
+END_OF_TRIM_HELP;
- if (defined('SCRIPT_DEBUG')) {
- print "Parent: forked fetcher process " . $pid . "\n";
- }
+require_once INSTALLDIR .'/scripts/commandline.inc';
+require_once INSTALLDIR . '/lib/daemon.php';
- $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) {
-
- common_debug("Child $c finished.");
-
- if (defined('SCRIPT_DEBUG')) {
- print "Child $c finished.\n";
- }
-
- $this->remove_ps($this->children, $c);
- }
+/**
+ * Fetcher for statuses from Twitter
+ *
+ * Fetches statuses from Twitter and inserts them as notices in local
+ * system.
+ *
+ * @category Twitter
+ * @package StatusNet
+ * @author Zach Copley <zach@status.net>
+ * @author Evan Prodromou <evan@status.net>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
- // Wait! We have too many damn kids.
- if (sizeof($this->children) > MAXCHILDREN) {
+// NOTE: an Avatar path MUST be set in config.php for this
+// script to work: e.g.: $config['avatar']['path'] = '/statusnet/avatar';
- common_debug('Too many children. Waiting...');
+class TwitterStatusFetcher extends ParallelizingDaemon
+{
+ /**
+ * Constructor
+ *
+ * @param string $id the name/id of this daemon
+ * @param int $interval sleep this long before doing everything again
+ * @param int $max_children maximum number of child processes at a time
+ * @param boolean $debug debug output flag
+ *
+ * @return void
+ *
+ **/
+ function __construct($id = null, $interval = 60,
+ $max_children = 2, $debug = null)
+ {
+ parent::__construct($id, $interval, $max_children, $debug);
+ }
- if (defined('SCRIPT_DEBUG')) {
- print "Too many children. Waiting...\n";
- }
+ /**
+ * Name of this daemon
+ *
+ * @return string Name of the daemon.
+ */
- if (($c = pcntl_wait($status, WUNTRACED)) > 0){
+ function name()
+ {
+ return ('twitterstatusfetcher.'.$this->_id);
+ }
- common_debug("Finished waiting for $c");
+ /**
+ * Find all the Twitter foreign links for users who have requested
+ * importing of their friends' timelines
+ *
+ * @return array flinks an array of Foreign_link objects
+ */
- if (defined('SCRIPT_DEBUG')) {
- print "Finished waiting for $c\n";
- }
+ function getObjects()
+ {
+ global $_DB_DATAOBJECT;
- $this->remove_ps($this->children, $c);
- }
- }
- }
+ $flink = new Foreign_link();
+ $conn = &$flink->getDatabaseConnection();
- // Remove all children from the process list before restarting
- while(($c = pcntl_wait($status, WUNTRACED)) > 0) {
+ $flink->service = TWITTER_SERVICE;
+ $flink->orderBy('last_noticesync');
+ $flink->find();
- common_debug("Child $c finished.");
+ $flinks = array();
- if (defined('SCRIPT_DEBUG')) {
- print "Child $c finished.\n";
- }
+ while ($flink->fetch()) {
- $this->remove_ps($this->children, $c);
+ if (($flink->noticesync & FOREIGN_NOTICE_RECV) ==
+ FOREIGN_NOTICE_RECV) {
+ $flinks[] = clone($flink);
}
+ }
- // Rest for a bit before we fetch more statuses
- common_debug('Waiting ' . POLL_INTERVAL .
- ' secs before hitting Twitter again.');
- if (defined('SCRIPT_DEBUG')) {
- print 'Waiting ' . POLL_INTERVAL .
- " secs before hitting Twitter again.\n";
- }
+ $flink->free();
+ unset($flink);
- sleep(POLL_INTERVAL);
+ $conn->disconnect();
+ unset($_DB_DATAOBJECT['CONNECTIONS']);
- } while (true);
+ return $flinks;
}
- function refreshFlinks() {
+ function childTask($flink) {
- $flink = new Foreign_link();
- $flink->service = 1; // Twitter
- $flink->orderBy('last_noticesync');
+ // Each child ps needs its own DB connection
- $cnt = $flink->find();
+ // Note: DataObject::getDatabaseConnection() creates
+ // a new connection if there isn't one already
- if (defined('SCRIPT_DEBUG')) {
- print "Updating Twitter friends subscriptions for $cnt users.\n";
- }
+ $conn = &$flink->getDatabaseConnection();
- $flinks = array();
+ $this->getTimeline($flink);
- while ($flink->fetch()) {
+ $flink->last_friendsync = common_sql_now();
+ $flink->update();
- if (($flink->noticesync & FOREIGN_NOTICE_RECV) == FOREIGN_NOTICE_RECV) {
- $flinks[] = clone($flink);
- }
- }
+ $conn->disconnect();
- $flink->free();
- unset($flink);
+ // XXX: Couldn't find a less brutal way to blow
+ // away a cached connection
- 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;
- }
- }
+ 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");
- if (defined('SCRIPT_DEBUG')) {
- print "Can't retrieve Foreign_link for foreign ID $fid\n";
- }
+ if (empty($flink)) {
+ common_log(LOG_WARNING, $this->name() .
+ " - Can't retrieve Foreign_link for foreign ID $fid");
return;
}
- $fuser = $flink->getForeignUser();
+ common_debug($this->name() . ' - Trying to get timeline for Twitter user ' .
+ $flink->foreign_id);
- 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;
- }
+ // 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.
- common_debug('Trying to get timeline for Twitter user ' .
- "$fuser->nickname ($flink->foreign_id).");
- if (defined('SCRIPT_DEBUG')) {
- print 'Trying to get timeline for Twitter user ' .
- "$fuser->nickname ($flink->foreign_id).\n";
- }
+ $token = TwitterOAuthClient::unpackToken($flink->credentials);
- $url = 'http://twitter.com/statuses/friends_timeline.json';
+ $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.");
- if (defined('SCRIPT_DEBUG')) {
- print "Empty timeline!\n";
- }
+ common_log(LOG_WARNING, $this->name() . " - Empty timeline.");
return;
}
- foreach ($timeline as $status) {
+ // Reverse to preserve order
+
+ foreach (array_reverse($timeline) as $status) {
+
+ // Hacktastic: filter out stuff coming from this StatusNet
- // Hacktastic: filter out stuff coming from Laconica
$source = mb_strtolower(common_config('integration', 'source'));
if (preg_match("/$source/", mb_strtolower($status->source))) {
+ common_debug($this->name() . ' - Skipping import of status ' .
+ $status->id . ' with source ' . $source);
continue;
}
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 (defined('SCRIPT_DEBUG')) {
- print "Problem saving notice. No associated Profile.\n";
- }
+ if (empty($profile)) {
+ common_log(LOG_ERR, $this->name() .
+ ' - Problem saving notice. No associated Profile.');
return null;
}
+ // XXX: change of screen name?
+
$uri = 'http://twitter.com/' . $status->user->screen_name .
'/status/' . $status->id;
- // 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?
+ if (empty($notice)) {
- $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');
+ $notice = new Notice();
- if (defined('SCRIPT_DEBUG')) {
- print "Saved status $status->id as notice $notice->id.\n";
+ $notice->profile_id = $id;
+ $notice->uri = $uri;
+ $notice->created = strftime('%Y-%m-%d %H:%M:%S',
+ strtotime($status->created_at));
+ $notice->content = common_shorten_links($status->text); // XXX
+ $notice->rendered = common_render_content($notice->content, $notice);
+ $notice->source = 'twitter';
+ $notice->reply_to = null; // XXX: lookup reply
+ $notice->is_local = Notice::GATEWAY;
+
+ if (Event::handle('StartNoticeSave', array(&$notice))) {
+ $id = $notice->insert();
+ Event::handle('EndNoticeSave', array($notice));
}
}
- if (!Notice_inbox::staticGet('notice_id', $notice->id)) {
-
+ if (!Notice_inbox::pkeyGet(array('notice_id' => $notice->id,
+ 'user_id' => $flink->user_id))) {
// Add to inbox
$inbox = new Notice_inbox();
- $inbox->user_id = $flink->user_id;
+
+ $inbox->user_id = $flink->user_id;
$inbox->notice_id = $notice->id;
- $inbox->created = common_sql_now();
+ $inbox->created = $notice->created;
+ $inbox->source = NOTICE_INBOX_SOURCE_GATEWAY; // From a private source
$inbox->insert();
}
function ensureProfile($user)
{
// check to see if there's already a profile for this user
+
$profileurl = 'http://twitter.com/' . $user->screen_name;
$profile = Profile::staticGet('profileurl', $profileurl);
- if ($profile) {
- 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);
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;
- }
+ common_debug($this->name() . ' - Adding profile and remote profile ' .
+ "for Twitter user: $profileurl.");
$profile = new Profile();
$profile->query("BEGIN");
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) {
+ if (empty($remote_pro)) {
$remote_pro = new Remote_profile();
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;
}
}
}
- function checkAvatar($user, $profile)
+ function checkAvatar($twitter_user, $profile)
{
global $config;
- $path_parts = pathinfo($user->profile_image_url);
- $newname = 'Twitter_' . $user->id . '_' .
+ $path_parts = pathinfo($twitter_user->profile_image_url);
+
+ $newname = 'Twitter_' . $twitter_user->id . '_' .
$path_parts['basename'];
$oldname = $profile->getAvatar(48)->filename;
if ($newname != $oldname) {
+ common_debug($this->name() . ' - Avatar for Twitter user ' .
+ "$profile->nickname has changed.");
+ common_debug($this->name() . " - old: $oldname new: $newname");
- common_debug("Avatar for Twitter user $profile->nickname has changed.");
- common_debug("old: $oldname new: $newname");
+ $this->updateAvatars($twitter_user, $profile);
+ }
- if (defined('SCRIPT_DEBUG')) {
- print "Avatar for Twitter user $user->id has changed.\n";
- print "old: $oldname\n";
- print "new: $newname\n";
- }
+ if ($this->missingAvatarFile($profile)) {
+ common_debug($this->name() . ' - Twitter user ' .
+ $profile->nickname .
+ ' is missing one or more local avatars.');
+ common_debug($this->name() ." - old: $oldname new: $newname");
- $img_root = substr($path_parts['basename'], 0, -11);
- $ext = $path_parts['extension'];
- $mediatype = $this->getMediatype($ext);
+ $this->updateAvatars($twitter_user, $profile);
+ }
- 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 updateAvatars($twitter_user, $profile) {
+
+ global $config;
+
+ $path_parts = pathinfo($twitter_user->profile_image_url);
+
+ $img_root = substr($path_parts['basename'], 0, -11);
+ $ext = $path_parts['extension'];
+ $mediatype = $this->getMediatype($ext);
+
+ foreach (array('mini', 'normal', 'bigger') as $size) {
+ $url = $path_parts['dirname'] . '/' .
+ $img_root . '_' . $size . ".$ext";
+ $filename = 'Twitter_' . $twitter_user->id . '_' .
+ $img_root . "_$size.$ext";
+
+ $this->updateAvatar($profile->id, $size, $mediatype, $filename);
+ $this->fetchAvatar($url, $filename);
+ }
+ }
+
+ function missingAvatarFile($profile) {
+
+ foreach (array(24, 48, 73) as $size) {
+
+ $filename = $profile->getAvatar($size)->filename;
+ $avatarpath = Avatar::path($filename);
+
+ if (file_exists($avatarpath) == FALSE) {
+ return true;
}
}
+
+ return false;
}
function getMediatype($ext)
if ($this->fetchAvatar($url, $filename)) {
$this->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";
- }
+ common_log(LOG_WARNING, $this->id() .
+ " - Problem fetching Avatar: $url");
}
}
}
function updateAvatar($profile_id, $size, $mediatype, $filename) {
- common_debug("Updating avatar: $size");
- if (defined('SCRIPT_DEBUG')) {
- print "Updating avatar: $size\n";
- }
+ common_debug($this->name() . " - Updating avatar: $size");
$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";
- }
+ if (empty($profile)) {
+ common_debug($this->name() . " - Couldn't get profile: $profile_id!");
return;
}
$sizes = array('mini' => 24, 'normal' => 48, 'bigger' => 73);
$avatar = $profile->getAvatar($sizes[$size]);
+ // Delete the avatar, if present
+
if ($avatar) {
- common_debug("Deleting $size avatar for $profile->nickname.");
- @unlink(INSTALLDIR . '/avatar/' . $avatar->filename);
$avatar->delete();
}
default:
// Note: Twitter's big avatars are a different size than
- // Laconica's (Laconica's = 96)
+ // StatusNet's (Laconica's = 96)
$avatar->width = 73;
$avatar->height = 73;
$avatar->filename = $filename;
$avatar->url = Avatar::url($filename);
- common_debug("new filename: $avatar->url");
- if (defined('SCRIPT_DEBUG')) {
- print "New filename: $avatar->url\n";
- }
+ common_debug($this->name() . " - New filename: $avatar->url");
$avatar->created = common_sql_now();
$id = $avatar->insert();
- if (!$id) {
+ if (empty($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";
- }
+ common_debug($this->name() .
+ " - Saved new $size avatar for $profile_id.");
return $id;
}
$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";
- }
+ common_log(LOG_WARNING, $this->name() .
+ " - Couldn't open file $filename");
return false;
}
- common_debug("Fetching avatar: $url", __FILE__);
- if (defined('SCRIPT_DEBUG')) {
- print "Fetching avatar from Twitter: $url\n";
- }
+ common_debug($this->name() . " - Fetching Twitter avatar: $url");
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
}
}
-ini_set("max_execution_time", "0");
-ini_set("max_input_time", "0");
-set_time_limit(0);
-mb_internal_encoding('UTF-8');
-declare(ticks = 1);
+$id = null;
+$debug = null;
+
+if (have_option('i')) {
+ $id = get_option_value('i');
+} else if (have_option('--id')) {
+ $id = get_option_value('--id');
+} else if (count($args) > 0) {
+ $id = $args[0];
+} else {
+ $id = null;
+}
+
+if (have_option('d') || have_option('debug')) {
+ $debug = true;
+}
-$fetcher = new TwitterStatusFetcher();
+$fetcher = new TwitterStatusFetcher($id, 60, 2, $debug);
$fetcher->runOnce();