<?php
/**
* StatusNet - the distributed open-source microblogging tool
- * Copyright (C) 2008, 2009, StatusNet, Inc.
+ * Copyright (C) 2008-2010, StatusNet, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
END_OF_TRIM_HELP;
require_once INSTALLDIR . '/scripts/commandline.inc';
+require_once INSTALLDIR . '/lib/common.php';
require_once INSTALLDIR . '/lib/daemon.php';
require_once INSTALLDIR . '/plugins/TwitterBridge/twitter.php';
+require_once INSTALLDIR . '/plugins/TwitterBridge/twitteroauthclient.php';
/**
- * Fetcher for statuses from Twitter
+ * Fetch statuses from Twitter
*
- * Fetches statuses from Twitter and inserts them as notices in local
- * system.
+ * Fetches statuses from Twitter and inserts them as notices
+ *
+ * NOTE: an Avatar path MUST be set in config.php for this
+ * script to work, e.g.:
+ * $config['avatar']['path'] = $config['site']['path'] . '/avatar/';
+ *
+ * @todo @fixme @gar Fix the above. For some reason $_path is always empty when
+ * this script is run, so the default avatar path is always set wrong in
+ * default.php. Therefore it must be set explicitly in config.php. --Z
*
* @category Twitter
* @package StatusNet
* @link http://status.net/
*/
-// NOTE: an Avatar path MUST be set in config.php for this
-// script to work: e.g.: $config['avatar']['path'] = '/statusnet/avatar';
-
class TwitterStatusFetcher extends ParallelizingDaemon
{
/**
if (($flink->noticesync & FOREIGN_NOTICE_RECV) ==
FOREIGN_NOTICE_RECV) {
$flinks[] = clone($flink);
+ common_log(LOG_INFO, "sync: foreign id $flink->foreign_id");
+ } else {
+ common_log(LOG_INFO, "nothing to sync");
}
}
$client = new TwitterOAuthClient($token->key, $token->secret);
common_debug($this->name() . ' - Grabbing friends timeline with OAuth.');
} else {
- $client = new TwitterBasicAuthClient($flink);
- common_debug($this->name() . ' - Grabbing friends timeline with basic auth.');
+ common_debug("Skipping friends timeline for $flink->foreign_id since not OAuth.");
}
$timeline = null;
try {
- $timeline = $client->statusesFriendsTimeline();
+ $timeline = $client->statusesHomeTimeline();
} catch (Exception $e) {
common_log(LOG_WARNING, $this->name() .
' - Twitter client unable to get friends timeline for user ' .
return;
}
+ common_debug(LOG_INFO, $this->name() . ' - Retrieved ' . sizeof($timeline) . ' statuses from Twitter.');
+
// Reverse to preserve order
foreach (array_reverse($timeline) as $status) {
continue;
}
- $this->saveStatus($status, $flink);
+ // Don't save it if the user is protected
+ // FIXME: save it but treat it as private
+
+ if ($status->user->protected) {
+ continue;
+ }
+
+ $notice = $this->saveStatus($status);
+
+ if (!empty($notice)) {
+ Inbox::insertNotice($flink->user_id, $notice->id);
+ }
}
// Okay, record the time we synced with Twitter for posterity
$flink->update();
}
- function saveStatus($status, $flink)
+ function saveStatus($status)
{
- $id = $this->ensureProfile($status->user);
-
- $profile = Profile::staticGet($id);
+ $profile = $this->ensureProfile($status->user);
if (empty($profile)) {
common_log(LOG_ERR, $this->name() .
return null;
}
- // XXX: change of screen name?
+ $statusUri = $this->makeStatusURI($status->user->screen_name, $status->id);
- $uri = 'http://twitter.com/' . $status->user->screen_name .
- '/status/' . $status->id;
+ // check to see if we've already imported the status
- $notice = Notice::staticGet('uri', $uri);
+ $n2s = Notice_to_status::staticGet('status_id', $status->id);
- // check to see if we've already imported the status
+ if (!empty($n2s)) {
+ common_log(
+ LOG_INFO,
+ $this->name() .
+ " - Ignoring duplicate import: {$status->id}"
+ );
+ return Notice::staticGet('id', $n2s->notice_id);
+ }
+
+ common_debug("Saving status {$status->id} with data " . print_r($status, true));
+
+ // If it's a retweet, save it as a repeat!
+
+ if (!empty($status->retweeted_status)) {
+ common_log(LOG_INFO, "Status {$status->id} is a retweet of {$status->retweeted_status->id}.");
+ $original = $this->saveStatus($status->retweeted_status);
+ if (empty($original)) {
+ return null;
+ } else {
+ $author = $original->getProfile();
+ // TRANS: Message used to repeat a notice. RT is the abbreviation of 'retweet'.
+ // TRANS: %1$s is the repeated user's name, %2$s is the repeated notice.
+ $content = sprintf(_('RT @%1$s %2$s'),
+ $author->nickname,
+ $original->content);
+ $repeat = Notice::saveNew($profile->id,
+ $content,
+ 'twitter',
+ array('repeat_of' => $original->id,
+ 'uri' => $statusUri));
+ common_log(LOG_INFO, "Saved {$repeat->id} as a repeat of {$original->id}");
+ Notice_to_status::saveNew($repeat->id, $status->id);
+ return $repeat;
+ }
+ }
+
+ $notice = new Notice();
- if (empty($notice)) {
+ $notice->profile_id = $profile->id;
+ $notice->uri = $statusUri;
+ $notice->url = $statusUri;
+ $notice->created = strftime(
+ '%Y-%m-%d %H:%M:%S',
+ strtotime($status->created_at)
+ );
- $notice = new Notice();
+ $notice->source = 'twitter';
- $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;
+ $notice->reply_to = null;
- if (Event::handle('StartNoticeSave', array(&$notice))) {
- $id = $notice->insert();
- Event::handle('EndNoticeSave', array($notice));
+ if (!empty($status->in_reply_to_status_id)) {
+ common_log(LOG_INFO, "Status {$status->id} is a reply to status {$status->in_reply_to_status_id}");
+ $n2s = Notice_to_status::staticGet('status_id', $status->in_reply_to_status_id);
+ if (empty($n2s)) {
+ common_log(LOG_INFO, "Couldn't find local notice for status {$status->in_reply_to_status_id}");
+ } else {
+ $reply = Notice::staticGet('id', $n2s->notice_id);
+ if (empty($reply)) {
+ common_log(LOG_INFO, "Couldn't find local notice for status {$status->in_reply_to_status_id}");
+ } else {
+ common_log(LOG_INFO, "Found local notice {$reply->id} for status {$status->in_reply_to_status_id}");
+ $notice->reply_to = $reply->id;
+ $notice->conversation = $reply->conversation;
+ }
}
}
- if (!Notice_inbox::pkeyGet(array('notice_id' => $notice->id,
- 'user_id' => $flink->user_id))) {
- // Add to inbox
- $inbox = new Notice_inbox();
+ if (empty($notice->conversation)) {
+ $conv = Conversation::create();
+ $notice->conversation = $conv->id;
+ common_log(LOG_INFO, "No known conversation for status {$status->id} so making a new one {$conv->id}.");
+ }
+
+ $notice->is_local = Notice::GATEWAY;
+
+ $notice->content = common_shorten_links($status->text);
+ $notice->rendered = common_render_content(
+ $notice->content,
+ $notice
+ );
+
+ if (Event::handle('StartNoticeSave', array(&$notice))) {
- $inbox->user_id = $flink->user_id;
- $inbox->notice_id = $notice->id;
- $inbox->created = $notice->created;
- $inbox->source = NOTICE_INBOX_SOURCE_GATEWAY; // From a private source
+ $id = $notice->insert();
- $inbox->insert();
+ if (!$id) {
+ common_log_db_error($notice, 'INSERT', __FILE__);
+ common_log(LOG_ERR, $this->name() .
+ ' - Problem saving notice.');
+ }
+
+ Event::handle('EndNoticeSave', array($notice));
}
+
+ Notice_to_status::saveNew($notice->id, $status->id);
+ $notice->blowOnInsert();
+
+ return $notice;
+ }
+
+ /**
+ * Make an URI for a status.
+ *
+ * @param object $status status object
+ *
+ * @return string URI
+ */
+
+ function makeStatusURI($username, $id)
+ {
+ return 'http://twitter.com/'
+ . $username
+ . '/status/'
+ . $id;
+ }
+
+ /**
+ * Look up a Profile by profileurl field. Profile::staticGet() was
+ * not working consistently.
+ *
+ * @param string $nickname local nickname of the Twitter user
+ * @param string $profileurl the profile url
+ *
+ * @return mixed value the first Profile with that url, or null
+ */
+
+ function getProfileByUrl($nickname, $profileurl)
+ {
+ $profile = new Profile();
+ $profile->nickname = $nickname;
+ $profile->profileurl = $profileurl;
+ $profile->limit(1);
+
+ if ($profile->find()) {
+ $profile->fetch();
+ return $profile;
+ }
+
+ return null;
+ }
+
+ /**
+ * Check to see if this Twitter status has already been imported
+ *
+ * @param Profile $profile Twitter user's local profile
+ * @param string $statusUri URI of the status on Twitter
+ *
+ * @return mixed value a matching Notice or null
+ */
+
+ function checkDupe($profile, $statusUri)
+ {
+ $notice = new Notice();
+ $notice->uri = $statusUri;
+ $notice->profile_id = $profile->id;
+ $notice->limit(1);
+
+ if ($notice->find()) {
+ $notice->fetch();
+ return $notice;
+ }
+
+ return null;
}
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);
+ $profile = $this->getProfileByUrl($user->screen_name, $profileurl);
if (!empty($profile)) {
common_debug($this->name() .
// Check to see if the user's Avatar has changed
$this->checkAvatar($user, $profile);
- return $profile->id;
+ return $profile;
} else {
+
common_debug($this->name() . ' - Adding profile and remote profile ' .
"for Twitter user: $profileurl.");
$profile->profileurl = $profileurl;
$profile->created = common_sql_now();
- $id = $profile->insert();
+ try {
+ $id = $profile->insert();
+ } catch(Exception $e) {
+ common_log(LOG_WARNING, $this->name . ' Couldn\'t insert profile - ' . $e->getMessage());
+ }
if (empty($id)) {
common_log_db_error($profile, 'INSERT', __FILE__);
$remote_pro->uri = $profileurl;
$remote_pro->created = common_sql_now();
- $rid = $remote_pro->insert();
+ try {
+ $rid = $remote_pro->insert();
+ } catch (Exception $e) {
+ common_log(LOG_WARNING, $this->name() . ' Couldn\'t save remote profile - ' . $e->getMessage());
+ }
if (empty($rid)) {
common_log_db_error($profile, 'INSERT', __FILE__);
$this->saveAvatars($user, $id);
- return $id;
+ return $profile;
}
}
$this->updateAvatars($twitter_user, $profile);
}
-
}
function updateAvatars($twitter_user, $profile) {
}
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;
}
if ($this->fetchAvatar($url, $filename)) {
$this->newAvatar($id, $size, $mediatype, $filename);
} else {
- common_log(LOG_WARNING, $this->id() .
+ common_log(LOG_WARNING, $id() .
" - Problem fetching Avatar: $url");
}
}
$avatar->filename = $filename;
$avatar->url = Avatar::url($filename);
- common_debug($this->name() . " - New filename: $avatar->url");
-
$avatar->created = common_sql_now();
- $id = $avatar->insert();
+ try {
+ $id = $avatar->insert();
+ } catch (Exception $e) {
+ common_log(LOG_WARNING, $this->name() . ' Couldn\'t insert avatar - ' . $e->getMessage());
+ }
if (empty($id)) {
common_log_db_error($avatar, 'INSERT', __FILE__);
return $id;
}
+ /**
+ * Fetch a remote avatar image and save to local storage.
+ *
+ * @param string $url avatar source URL
+ * @param string $filename bare local filename for download
+ * @return bool true on success, false on failure
+ */
function fetchAvatar($url, $filename)
{
- $avatar_dir = INSTALLDIR . '/avatar/';
-
- $avatarfile = $avatar_dir . $filename;
+ common_debug($this->name() . " - Fetching Twitter avatar: $url");
- $out = fopen($avatarfile, 'wb');
- if (!$out) {
- common_log(LOG_WARNING, $this->name() .
- " - Couldn't open file $filename");
+ $request = HTTPClient::start();
+ $response = $request->get($url);
+ if ($response->isOk()) {
+ $avatarfile = Avatar::path($filename);
+ $ok = file_put_contents($avatarfile, $response->getBody());
+ if (!$ok) {
+ common_log(LOG_WARNING, $this->name() .
+ " - Couldn't open file $filename");
+ return false;
+ }
+ } else {
return false;
}
- common_debug($this->name() . " - Fetching Twitter 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;
+ return true;
}
}