From 0eaa26476cac557484e295666b574340236452ff Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Tue, 5 Oct 2010 13:57:32 -0700 Subject: [PATCH] Split the guts of Twitter status -> notice import from twitterstatusfetcher daemon into TwitterImport class which can be called from other places, letting us reuse code for the streaming API. --- plugins/TwitterBridge/TwitterBridgePlugin.php | 1 + .../daemons/twitterstatusfetcher.php | 591 +--------------- plugins/TwitterBridge/twitterimport.php | 651 ++++++++++++++++++ 3 files changed, 655 insertions(+), 588 deletions(-) create mode 100644 plugins/TwitterBridge/twitterimport.php diff --git a/plugins/TwitterBridge/TwitterBridgePlugin.php b/plugins/TwitterBridge/TwitterBridgePlugin.php index 097d4486f9..128b062c7f 100644 --- a/plugins/TwitterBridge/TwitterBridgePlugin.php +++ b/plugins/TwitterBridge/TwitterBridgePlugin.php @@ -200,6 +200,7 @@ class TwitterBridgePlugin extends Plugin return false; case 'TwitterOAuthClient': case 'TwitterQueueHandler': + case 'TwitterImport': include_once $dir . '/' . strtolower($cls) . '.php'; return false; case 'Notice_to_status': diff --git a/plugins/TwitterBridge/daemons/twitterstatusfetcher.php b/plugins/TwitterBridge/daemons/twitterstatusfetcher.php index cef67b1806..9298d9e3a1 100755 --- a/plugins/TwitterBridge/daemons/twitterstatusfetcher.php +++ b/plugins/TwitterBridge/daemons/twitterstatusfetcher.php @@ -192,25 +192,12 @@ class TwitterStatusFetcher extends ParallelizingDaemon common_debug(LOG_INFO, $this->name() . ' - Retrieved ' . sizeof($timeline) . ' statuses from Twitter.'); + $importer = new TwitterImport(); + // Reverse to preserve order foreach (array_reverse($timeline) as $status) { - // Hacktastic: filter out stuff coming from this StatusNet - $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; - } - - // 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); + $notice = $importer->importStatus($status); if (!empty($notice)) { Inbox::insertNotice($flink->user_id, $notice->id); @@ -226,578 +213,6 @@ class TwitterStatusFetcher extends ParallelizingDaemon $flink->last_noticesync = common_sql_now(); $flink->update(); } - - function saveStatus($status) - { - $profile = $this->ensureProfile($status->user); - - if (empty($profile)) { - common_log(LOG_ERR, $this->name() . - ' - Problem saving notice. No associated Profile.'); - return null; - } - - $statusUri = $this->makeStatusURI($status->user->screen_name, $status->id); - - // check to see if we've already imported the status - $n2s = Notice_to_status::staticGet('status_id', $status->id); - - if (!empty($n2s)) { - common_log( - LOG_INFO, - $this->name() . - " - Ignoring duplicate import: {$status->id}" - ); - return Notice::staticGet('id', $n2s->notice_id); - } - - // If it's a retweet, save it as a repeat! - if (!empty($status->retweeted_status)) { - common_log(LOG_INFO, "Status {$status->id} is a retweet of {$status->retweeted_status->id}."); - $original = $this->saveStatus($status->retweeted_status); - if (empty($original)) { - return null; - } else { - $author = $original->getProfile(); - // TRANS: Message used to repeat a notice. RT is the abbreviation of 'retweet'. - // TRANS: %1$s is the repeated user's name, %2$s is the repeated notice. - $content = sprintf(_m('RT @%1$s %2$s'), - $author->nickname, - $original->content); - - if (Notice::contentTooLong($content)) { - $contentlimit = Notice::maxContent(); - $content = mb_substr($content, 0, $contentlimit - 4) . ' ...'; - } - - $repeat = Notice::saveNew($profile->id, - $content, - 'twitter', - array('repeat_of' => $original->id, - 'uri' => $statusUri, - 'is_local' => Notice::GATEWAY)); - common_log(LOG_INFO, "Saved {$repeat->id} as a repeat of {$original->id}"); - Notice_to_status::saveNew($repeat->id, $status->id); - return $repeat; - } - } - - $notice = new Notice(); - - $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->source = 'twitter'; - - $notice->reply_to = null; - - if (!empty($status->in_reply_to_status_id)) { - common_log(LOG_INFO, "Status {$status->id} is a reply to status {$status->in_reply_to_status_id}"); - $n2s = Notice_to_status::staticGet('status_id', $status->in_reply_to_status_id); - if (empty($n2s)) { - common_log(LOG_INFO, "Couldn't find local notice for status {$status->in_reply_to_status_id}"); - } else { - $reply = Notice::staticGet('id', $n2s->notice_id); - if (empty($reply)) { - common_log(LOG_INFO, "Couldn't find local notice for status {$status->in_reply_to_status_id}"); - } else { - common_log(LOG_INFO, "Found local notice {$reply->id} for status {$status->in_reply_to_status_id}"); - $notice->reply_to = $reply->id; - $notice->conversation = $reply->conversation; - } - } - } - - if (empty($notice->conversation)) { - $conv = Conversation::create(); - $notice->conversation = $conv->id; - common_log(LOG_INFO, "No known conversation for status {$status->id} so making a new one {$conv->id}."); - } - - $notice->is_local = Notice::GATEWAY; - - $notice->content = html_entity_decode($status->text, ENT_QUOTES, 'UTF-8'); - $notice->rendered = $this->linkify($status); - - if (Event::handle('StartNoticeSave', array(&$notice))) { - - $id = $notice->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); - - $this->saveStatusMentions($notice, $status); - - $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 = $this->getProfileByUrl($user->screen_name, $profileurl); - - 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; - - } else { - common_debug($this->name() . ' - 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(); - - 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__); - $profile->query("ROLLBACK"); - return false; - } - - // check for remote profile - - $remote_pro = Remote_profile::staticGet('uri', $profileurl); - - if (empty($remote_pro)) { - $remote_pro = new Remote_profile(); - - $remote_pro->id = $id; - $remote_pro->uri = $profileurl; - $remote_pro->created = common_sql_now(); - - 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__); - $profile->query("ROLLBACK"); - return false; - } - } - - $profile->query("COMMIT"); - - $this->saveAvatars($user, $id); - - return $profile; - } - } - - function checkAvatar($twitter_user, $profile) - { - global $config; - - $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"); - - $this->updateAvatars($twitter_user, $profile); - } - - 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"); - - $this->updateAvatars($twitter_user, $profile); - } - } - - 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) - { - $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, $id() . - " - Problem fetching Avatar: $url"); - } - } - } - - function updateAvatar($profile_id, $size, $mediatype, $filename) { - - common_debug($this->name() . " - Updating avatar: $size"); - - $profile = Profile::staticGet($profile_id); - - 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) { - $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 - // StatusNet's (StatusNet'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); - - $avatar->created = common_sql_now(); - - 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 null; - } - - common_debug($this->name() . - " - Saved new $size avatar for $profile_id."); - - 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) - { - common_debug($this->name() . " - Fetching Twitter avatar: $url"); - - $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; - } - - return true; - } - - const URL = 1; - const HASHTAG = 2; - const MENTION = 3; - - function linkify($status) - { - $text = $status->text; - - if (empty($status->entities)) { - common_log(LOG_WARNING, "No entities data for {$status->id}; trying to fake up links ourselves."); - $text = common_replace_urls_callback($text, 'common_linkify'); - $text = preg_replace('/(^|\"\;|\'|\(|\[|\{|\s+)#([\pL\pN_\-\.]{1,64})/e', "'\\1#'.TwitterStatusFetcher::tagLink('\\2')", $text); - $text = preg_replace('/(^|\s+)@([a-z0-9A-Z_]{1,64})/e', "'\\1@'.TwitterStatusFetcher::atLink('\\2')", $text); - return $text; - } - - // Move all the entities into order so we can - // replace them in reverse order and thus - // not mess up their indices - - $toReplace = array(); - - if (!empty($status->entities->urls)) { - foreach ($status->entities->urls as $url) { - $toReplace[$url->indices[0]] = array(self::URL, $url); - } - } - - if (!empty($status->entities->hashtags)) { - foreach ($status->entities->hashtags as $hashtag) { - $toReplace[$hashtag->indices[0]] = array(self::HASHTAG, $hashtag); - } - } - - if (!empty($status->entities->user_mentions)) { - foreach ($status->entities->user_mentions as $mention) { - $toReplace[$mention->indices[0]] = array(self::MENTION, $mention); - } - } - - // sort in reverse order by key - - krsort($toReplace); - - foreach ($toReplace as $part) { - list($type, $object) = $part; - switch($type) { - case self::URL: - $linkText = $this->makeUrlLink($object); - break; - case self::HASHTAG: - $linkText = $this->makeHashtagLink($object); - break; - case self::MENTION: - $linkText = $this->makeMentionLink($object); - break; - default: - continue; - } - $text = mb_substr($text, 0, $object->indices[0]) . $linkText . mb_substr($text, $object->indices[1]); - } - return $text; - } - - function makeUrlLink($object) - { - return "{$object->url}"; - } - - function makeHashtagLink($object) - { - return "#" . self::tagLink($object->text); - } - - function makeMentionLink($object) - { - return "@".self::atLink($object->screen_name, $object->name); - } - - static function tagLink($tag) - { - return "{$tag}"; - } - - static function atLink($screenName, $fullName=null) - { - if (!empty($fullName)) { - return "{$screenName}"; - } else { - return "{$screenName}"; - } - } - - function saveStatusMentions($notice, $status) - { - $mentions = array(); - - if (empty($status->entities) || empty($status->entities->user_mentions)) { - return; - } - - foreach ($status->entities->user_mentions as $mention) { - $flink = Foreign_link::getByForeignID($mention->id, TWITTER_SERVICE); - if (!empty($flink)) { - $user = User::staticGet('id', $flink->user_id); - if (!empty($user)) { - $reply = new Reply(); - $reply->notice_id = $notice->id; - $reply->profile_id = $user->id; - common_log(LOG_INFO, __METHOD__ . ": saving reply: notice {$notice->id} to profile {$user->id}"); - $id = $reply->insert(); - } - } - } - } } $id = null; diff --git a/plugins/TwitterBridge/twitterimport.php b/plugins/TwitterBridge/twitterimport.php new file mode 100644 index 0000000000..1b2d395304 --- /dev/null +++ b/plugins/TwitterBridge/twitterimport.php @@ -0,0 +1,651 @@ +. + * + * @category Plugin + * @package StatusNet + * @author Zach Copley + * @author Julien C + * @author Brion Vibber + * @copyright 2009-2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +require_once INSTALLDIR . '/plugins/TwitterBridge/twitter.php'; + +/** + * Encapsulation of the Twitter status -> notice incoming bridge import. + * Is used by both the polling twitterstatusfetcher.php daemon, and the + * in-progress streaming import. + * + * @category Plugin + * @package StatusNet + * @author Zach Copley + * @author Julien C + * @author Brion Vibber + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + * @link http://twitter.com/ + */ +class TwitterImport +{ + public function importStatus($status) + { + // Hacktastic: filter out stuff coming from this StatusNet + $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; + } + + // 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); + + return $notice; + } + + function name() + { + return get_class($this); + } + + function saveStatus($status) + { + $profile = $this->ensureProfile($status->user); + + if (empty($profile)) { + common_log(LOG_ERR, $this->name() . + ' - Problem saving notice. No associated Profile.'); + return null; + } + + $statusUri = $this->makeStatusURI($status->user->screen_name, $status->id); + + // check to see if we've already imported the status + $n2s = Notice_to_status::staticGet('status_id', $status->id); + + if (!empty($n2s)) { + common_log( + LOG_INFO, + $this->name() . + " - Ignoring duplicate import: {$status->id}" + ); + return Notice::staticGet('id', $n2s->notice_id); + } + + // If it's a retweet, save it as a repeat! + if (!empty($status->retweeted_status)) { + common_log(LOG_INFO, "Status {$status->id} is a retweet of {$status->retweeted_status->id}."); + $original = $this->saveStatus($status->retweeted_status); + if (empty($original)) { + return null; + } else { + $author = $original->getProfile(); + // TRANS: Message used to repeat a notice. RT is the abbreviation of 'retweet'. + // TRANS: %1$s is the repeated user's name, %2$s is the repeated notice. + $content = sprintf(_m('RT @%1$s %2$s'), + $author->nickname, + $original->content); + + if (Notice::contentTooLong($content)) { + $contentlimit = Notice::maxContent(); + $content = mb_substr($content, 0, $contentlimit - 4) . ' ...'; + } + + $repeat = Notice::saveNew($profile->id, + $content, + 'twitter', + array('repeat_of' => $original->id, + 'uri' => $statusUri, + 'is_local' => Notice::GATEWAY)); + common_log(LOG_INFO, "Saved {$repeat->id} as a repeat of {$original->id}"); + Notice_to_status::saveNew($repeat->id, $status->id); + return $repeat; + } + } + + $notice = new Notice(); + + $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->source = 'twitter'; + + $notice->reply_to = null; + + if (!empty($status->in_reply_to_status_id)) { + common_log(LOG_INFO, "Status {$status->id} is a reply to status {$status->in_reply_to_status_id}"); + $n2s = Notice_to_status::staticGet('status_id', $status->in_reply_to_status_id); + if (empty($n2s)) { + common_log(LOG_INFO, "Couldn't find local notice for status {$status->in_reply_to_status_id}"); + } else { + $reply = Notice::staticGet('id', $n2s->notice_id); + if (empty($reply)) { + common_log(LOG_INFO, "Couldn't find local notice for status {$status->in_reply_to_status_id}"); + } else { + common_log(LOG_INFO, "Found local notice {$reply->id} for status {$status->in_reply_to_status_id}"); + $notice->reply_to = $reply->id; + $notice->conversation = $reply->conversation; + } + } + } + + if (empty($notice->conversation)) { + $conv = Conversation::create(); + $notice->conversation = $conv->id; + common_log(LOG_INFO, "No known conversation for status {$status->id} so making a new one {$conv->id}."); + } + + $notice->is_local = Notice::GATEWAY; + + $notice->content = html_entity_decode($status->text, ENT_QUOTES, 'UTF-8'); + $notice->rendered = $this->linkify($status); + + if (Event::handle('StartNoticeSave', array(&$notice))) { + + $id = $notice->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); + + $this->saveStatusMentions($notice, $status); + + $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 = $this->getProfileByUrl($user->screen_name, $profileurl); + + 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; + + } else { + common_debug($this->name() . ' - 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(); + + 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__); + $profile->query("ROLLBACK"); + return false; + } + + // check for remote profile + + $remote_pro = Remote_profile::staticGet('uri', $profileurl); + + if (empty($remote_pro)) { + $remote_pro = new Remote_profile(); + + $remote_pro->id = $id; + $remote_pro->uri = $profileurl; + $remote_pro->created = common_sql_now(); + + 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__); + $profile->query("ROLLBACK"); + return false; + } + } + + $profile->query("COMMIT"); + + $this->saveAvatars($user, $id); + + return $profile; + } + } + + function checkAvatar($twitter_user, $profile) + { + global $config; + + $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"); + + $this->updateAvatars($twitter_user, $profile); + } + + 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"); + + $this->updateAvatars($twitter_user, $profile); + } + } + + 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) + { + $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, $id() . + " - Problem fetching Avatar: $url"); + } + } + } + + function updateAvatar($profile_id, $size, $mediatype, $filename) { + + common_debug($this->name() . " - Updating avatar: $size"); + + $profile = Profile::staticGet($profile_id); + + 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) { + $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 + // StatusNet's (StatusNet'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); + + $avatar->created = common_sql_now(); + + 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 null; + } + + common_debug($this->name() . + " - Saved new $size avatar for $profile_id."); + + 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) + { + common_debug($this->name() . " - Fetching Twitter avatar: $url"); + + $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; + } + + return true; + } + + const URL = 1; + const HASHTAG = 2; + const MENTION = 3; + + function linkify($status) + { + $text = $status->text; + + if (empty($status->entities)) { + common_log(LOG_WARNING, "No entities data for {$status->id}; trying to fake up links ourselves."); + $text = common_replace_urls_callback($text, 'common_linkify'); + $text = preg_replace('/(^|\"\;|\'|\(|\[|\{|\s+)#([\pL\pN_\-\.]{1,64})/e', "'\\1#'.TwitterStatusFetcher::tagLink('\\2')", $text); + $text = preg_replace('/(^|\s+)@([a-z0-9A-Z_]{1,64})/e', "'\\1@'.TwitterStatusFetcher::atLink('\\2')", $text); + return $text; + } + + // Move all the entities into order so we can + // replace them in reverse order and thus + // not mess up their indices + + $toReplace = array(); + + if (!empty($status->entities->urls)) { + foreach ($status->entities->urls as $url) { + $toReplace[$url->indices[0]] = array(self::URL, $url); + } + } + + if (!empty($status->entities->hashtags)) { + foreach ($status->entities->hashtags as $hashtag) { + $toReplace[$hashtag->indices[0]] = array(self::HASHTAG, $hashtag); + } + } + + if (!empty($status->entities->user_mentions)) { + foreach ($status->entities->user_mentions as $mention) { + $toReplace[$mention->indices[0]] = array(self::MENTION, $mention); + } + } + + // sort in reverse order by key + + krsort($toReplace); + + foreach ($toReplace as $part) { + list($type, $object) = $part; + switch($type) { + case self::URL: + $linkText = $this->makeUrlLink($object); + break; + case self::HASHTAG: + $linkText = $this->makeHashtagLink($object); + break; + case self::MENTION: + $linkText = $this->makeMentionLink($object); + break; + default: + continue; + } + $text = mb_substr($text, 0, $object->indices[0]) . $linkText . mb_substr($text, $object->indices[1]); + } + return $text; + } + + function makeUrlLink($object) + { + return "{$object->url}"; + } + + function makeHashtagLink($object) + { + return "#" . self::tagLink($object->text); + } + + function makeMentionLink($object) + { + return "@".self::atLink($object->screen_name, $object->name); + } + + static function tagLink($tag) + { + return "{$tag}"; + } + + static function atLink($screenName, $fullName=null) + { + if (!empty($fullName)) { + return "{$screenName}"; + } else { + return "{$screenName}"; + } + } + + function saveStatusMentions($notice, $status) + { + $mentions = array(); + + if (empty($status->entities) || empty($status->entities->user_mentions)) { + return; + } + + foreach ($status->entities->user_mentions as $mention) { + $flink = Foreign_link::getByForeignID($mention->id, TWITTER_SERVICE); + if (!empty($flink)) { + $user = User::staticGet('id', $flink->user_id); + if (!empty($user)) { + $reply = new Reply(); + $reply->notice_id = $notice->id; + $reply->profile_id = $user->id; + common_log(LOG_INFO, __METHOD__ . ": saving reply: notice {$notice->id} to profile {$user->id}"); + $id = $reply->insert(); + } + } + } + } +} \ No newline at end of file