4 * Laconica - a distributed open-source microblogging tool
5 * Copyright (C) 2008, Controlez-Vous, Inc.
7 * This program is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU Affero General Public License as published by
9 * the Free Software Foundation, either version 3 of the License, or
10 * (at your option) any later version.
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU Affero General Public License for more details.
17 * You should have received a copy of the GNU Affero General Public License
18 * along with this program. If not, see <http://www.gnu.org/licenses/>.
21 // Abort if called from a web server
22 if (isset($_SERVER) && array_key_exists('REQUEST_METHOD', $_SERVER)) {
23 print "This script must be run from the command line\n";
27 define('INSTALLDIR', realpath(dirname(__FILE__) . '/..'));
28 define('LACONICA', true);
30 // Tune number of processes and how often to poll Twitter
31 define('MAXCHILDREN', 5);
32 <<<<<<< HEAD:scripts/statusfetcher.php
33 define('POLL_INTERVAL', 60 * 10); // in seconds
35 define('POLL_INTERVAL', 60 * 5); // in seconds
36 >>>>>>> b8c700a7454db825b3867eadfa22afa1e5eb4f6c:scripts/statusfetcher.php
38 // Uncomment this to get useful console output
39 define('SCRIPT_DEBUG', true);
41 require_once(INSTALLDIR . '/lib/common.php');
47 <<<<<<< HEAD:scripts/statusfetcher.php
48 $flinks = refreshFlinks();
50 foreach ($flinks as $f){
52 // We have to disconnect from the DB before forking so
53 // each process will open its own connection and
54 // avoid stomping on each other
56 $flink_ids = refreshFlinks();
57 >>>>>>> b8c700a7454db825b3867eadfa22afa1e5eb4f6c:scripts/statusfetcher.php
59 $conn = &$f->getDatabaseConnection();
65 die ("Couldn't fork!");
72 if (defined('SCRIPT_DEBUG')) {
73 print "Parent: forked " . $pid . "\n";
82 <<<<<<< HEAD:scripts/statusfetcher.php
83 getTimeline($f, $child_db_name);
85 // XXX: Each child needs its own DB connection
87 >>>>>>> b8c700a7454db825b3867eadfa22afa1e5eb4f6c:scripts/statusfetcher.php
91 // Remove child from ps list as it finishes
92 while(($c = pcntl_wait($status, WNOHANG OR WUNTRACED)) > 0) {
94 if (defined('SCRIPT_DEBUG')) {
95 print "Child $c finished.\n";
98 remove_ps($children, $c);
101 // Wait if we have too many kids
102 <<<<<<< HEAD:scripts/statusfetcher.php
103 if (sizeof($children) > MAXCHILDREN) {
105 if (defined('SCRIPT_DEBUG')) {
106 print "Too many children. Waiting...\n";
109 if (($c = pcntl_wait($status, WUNTRACED)) > 0){
112 if(sizeof($children) > MAXCHILDREN) {
113 if (defined('SCRIPT_DEBUG')) {
114 print "Too many children. Waiting...\n";
116 if(($c = pcntl_wait($status, WUNTRACED)) > 0){
117 >>>>>>> b8c700a7454db825b3867eadfa22afa1e5eb4f6c:scripts/statusfetcher.php
118 if (defined('SCRIPT_DEBUG')) {
119 print "Finished waiting for $c\n";
122 remove_ps($children, $c);
127 // Remove all children from the process list before restarting
128 while(($c = pcntl_wait($status, WUNTRACED)) > 0) {
130 if (defined('SCRIPT_DEBUG')) {
131 print "Child $c finished.\n";
134 remove_ps($children, $c);
137 // Rest for a bit before we fetch more statuses
138 common_debug('Waiting ' . POLL_INTERVAL .
139 ' secs before hitting Twitter again.');
140 if (defined('SCRIPT_DEBUG')) {
141 print 'Waiting ' . POLL_INTERVAL .
142 " secs before hitting Twitter again.\n";
145 sleep(POLL_INTERVAL);
150 function refreshFlinks() {
152 <<<<<<< HEAD:scripts/statusfetcher.php
156 >>>>>>> b8c700a7454db825b3867eadfa22afa1e5eb4f6c:scripts/statusfetcher.php
157 $flink = new Foreign_link();
158 $flink->service = 1; // Twitter
159 $flink->orderBy('last_noticesync');
161 $cnt = $flink->find();
163 if (defined('SCRIPT_DEBUG')) {
164 print "Updating Twitter friends subscriptions for $cnt users.\n";
169 while ($flink->fetch()) {
171 if (($flink->noticesync & FOREIGN_NOTICE_RECV) == FOREIGN_NOTICE_RECV) {
172 $flinks[] = clone($flink);
182 function remove_ps(&$plist, $ps){
183 for ($i = 0; $i < sizeof($plist); $i++) {
184 if ($plist[$i] == $ps) {
186 $plist = array_values($plist);
192 function getTimeline($flink)
195 <<<<<<< HEAD:scripts/statusfetcher.php
197 common_log(LOG_WARNING, "Can't retrieve Foreign_link for foreign ID $fid");
198 if (defined('SCRIPT_DEBUG')) {
199 print "Can't retrieve Foreign_link for foreign ID $fid\n";
204 $fuser = $flink->getForeignUser();
207 $config['db'] = &PEAR::getStaticProperty('DB_DataObject','options');
208 require_once(INSTALLDIR . '/lib/common.php');
210 if (defined('SCRIPT_DEBUG')) {
211 print "Trying to get timeline for $flink->foreign_id\n";
215 common_log(LOG_WARNING, "Can't retrieve Foreign_link for foreign ID $fid");
216 if (defined('SCRIPT_DEBUG')) {
217 print "Can't retrieve Foreign_link for foreign ID $fid\n";
222 $fuser = new Foreign_user();
224 $fuser->id = $flink->foreign_id;
227 >>>>>>> b8c700a7454db825b3867eadfa22afa1e5eb4f6c:scripts/statusfetcher.php
230 common_log(LOG_WARNING, "Unmatched user for ID " . $flink->user_id);
231 if (defined('SCRIPT_DEBUG')) {
232 print "Unmatched user for ID $flink->user_id\n";
237 <<<<<<< HEAD:scripts/statusfetcher.php
238 common_debug('Trying to get timeline for Twitter user ' .
239 "$fuser->nickname ($flink->foreign_id).");
240 if (defined('SCRIPT_DEBUG')) {
241 print 'Trying to get timeline for Twitter user ' .
242 "$fuser->nickname ($flink->foreign_id).\n";
244 if (defined('SCRIPT_DEBUG')) {
245 // XXX: This is horrible and must be removed before releasing this
246 print 'username: ' . $fuser->nickname . ' password: ' . $flink->credentials . "\n";
247 >>>>>>> b8c700a7454db825b3867eadfa22afa1e5eb4f6c:scripts/statusfetcher.php
250 $url = 'http://twitter.com/statuses/friends_timeline.json';
252 $timeline_json = get_twitter_data($url, $fuser->nickname,
253 $flink->credentials);
255 $timeline = json_decode($timeline_json);
257 if (empty($timeline)) {
258 common_log(LOG_WARNING, "Empty timeline.");
259 if (defined('SCRIPT_DEBUG')) {
260 print "Empty timeline!\n";
265 foreach ($timeline as $status) {
267 // Hacktastic: filter out stuff coming from Laconica
268 $source = mb_strtolower(common_config('integration', 'source'));
270 if (preg_match("/$source/", mb_strtolower($status->source))) {
274 saveStatus($status, $flink);
277 // Okay, record the time we synced with Twitter for posterity
279 $flink->last_noticesync = common_sql_now();
283 function saveStatus($status, $flink)
285 <<<<<<< HEAD:scripts/statusfetcher.php
289 $config['db'] = &PEAR::getStaticProperty('DB_DataObject','options');
290 require_once(INSTALLDIR . '/lib/common.php');
292 // Do we have a profile for this Twitter user?
294 >>>>>>> b8c700a7454db825b3867eadfa22afa1e5eb4f6c:scripts/statusfetcher.php
295 $id = ensureProfile($status->user);
296 $profile = Profile::staticGet($id);
299 common_log(LOG_ERR, 'Problem saving notice. No associated Profile.');
300 if (defined('SCRIPT_DEBUG')) {
301 print "Problem saving notice. No associated Profile.\n";
306 $uri = 'http://twitter.com/' . $status->user->screen_name .
307 '/status/' . $status->id;
309 // Skip save if notice source is Laconica or Identi.ca?
311 $notice = Notice::staticGet('uri', $uri);
313 // check to see if we've already imported the status
316 $notice = new Notice();
317 $notice->profile_id = $id;
319 $notice->query('BEGIN');
321 // XXX: figure out reply_to
322 $notice->reply_to = null;
324 // XXX: Should this be common_sql_now() instead of status create date?
326 $notice->created = strftime('%Y-%m-%d %H:%M:%S',
327 strtotime($status->created_at));
328 $notice->content = $status->text;
329 $notice->rendered = common_render_content($status->text, $notice);
330 $notice->source = 'twitter';
331 $notice->is_local = 0;
334 $notice_id = $notice->insert();
337 common_log_db_error($notice, 'INSERT', __FILE__);
338 if (defined('SCRIPT_DEBUG')) {
339 print "Could not save notice!\n";
343 // XXX: Figure out a better way to link replies?
344 $notice->saveReplies();
346 // XXX: Do we want to polute our tag cloud with hashtags from Twitter?
348 $notice->saveGroups();
350 $notice->query('COMMIT');
352 <<<<<<< HEAD:scripts/statusfetcher.php
353 common_debug("Saved status $status->id as notice $notice->id.");
355 >>>>>>> b8c700a7454db825b3867eadfa22afa1e5eb4f6c:scripts/statusfetcher.php
356 if (defined('SCRIPT_DEBUG')) {
357 print "Saved status $status->id as notice $notice->id.\n";
361 if (!Notice_inbox::staticGet('notice_id', $notice->id)) {
364 $inbox = new Notice_inbox();
365 $inbox->user_id = $flink->user_id;
366 $inbox->notice_id = $notice->id;
367 $inbox->created = common_sql_now();
373 function ensureProfile($user)
375 <<<<<<< HEAD:scripts/statusfetcher.php
379 >>>>>>> b8c700a7454db825b3867eadfa22afa1e5eb4f6c:scripts/statusfetcher.php
380 // check to see if there's already a profile for this user
381 $profileurl = 'http://twitter.com/' . $user->screen_name;
382 $profile = Profile::staticGet('profileurl', $profileurl);
385 common_debug("Profile for $profile->nickname found.");
387 // Check to see if the user's Avatar has changed
388 checkAvatar($user, $profile);
392 $debugmsg = 'Adding profile and remote profile ' .
393 "for Twitter user: $profileurl\n";
394 common_debug($debugmsg, __FILE__);
395 if (defined('SCRIPT_DEBUG')) {
399 $profile = new Profile();
400 $profile->query("BEGIN");
402 $profile->nickname = $user->screen_name;
403 $profile->fullname = $user->name;
404 $profile->homepage = $user->url;
405 $profile->bio = $user->description;
406 $profile->location = $user->location;
407 $profile->profileurl = $profileurl;
408 $profile->created = common_sql_now();
410 $id = $profile->insert();
413 common_log_db_error($profile, 'INSERT', __FILE__);
414 if (defined('SCRIPT_DEBUG')) {
415 print 'Could not insert Profile: ' .
416 common_log_objstring($profile) . "\n";
418 $profile->query("ROLLBACK");
422 // check for remote profile
423 $remote_pro = Remote_profile::staticGet('uri', $profileurl);
427 $remote_pro = new Remote_profile();
429 $remote_pro->id = $id;
430 $remote_pro->uri = $profileurl;
431 $remote_pro->created = common_sql_now();
433 $rid = $remote_pro->insert();
436 common_log_db_error($profile, 'INSERT', __FILE__);
437 if (defined('SCRIPT_DEBUG')) {
438 print 'Could not insert Remote_profile: ' .
439 common_log_objstring($remote_pro) . "\n";
441 $profile->query("ROLLBACK");
446 $profile->query("COMMIT");
448 saveAvatars($user, $id);
454 function checkAvatar($user, $profile)
458 $path_parts = pathinfo($user->profile_image_url);
459 $newname = 'Twitter_' . $user->id . '_' .
460 $path_parts['basename'];
462 $oldname = $profile->getAvatar(48)->filename;
464 if ($newname != $oldname) {
466 common_debug("Avatar for Twitter user $profile->nickname has changed.");
467 common_debug("old: $oldname new: $newname");
469 if (defined('SCRIPT_DEBUG')) {
470 print "Avatar for Twitter user $user->id has changed.\n";
471 print "old: $oldname\n";
472 print "new: $newname\n";
475 $img_root = substr($path_parts['basename'], 0, -11);
476 $ext = $path_parts['extension'];
477 $mediatype = getMediatype($ext);
479 foreach (array('mini', 'normal', 'bigger') as $size) {
480 $url = $path_parts['dirname'] . '/' .
481 $img_root . '_' . $size . ".$ext";
482 $filename = 'Twitter_' . $user->id . '_' .
483 $img_root . "_$size.$ext";
485 if (fetchAvatar($url, $filename)) {
486 updateAvatar($profile->id, $size, $mediatype, $filename);
492 function getMediatype($ext)
496 switch (strtolower($ext)) {
498 $mediatype = 'image/jpg';
501 $mediatype = 'image/gif';
504 $mediatype = 'image/png';
510 function saveAvatars($user, $id)
514 $path_parts = pathinfo($user->profile_image_url);
515 $ext = $path_parts['extension'];
516 $end = strlen('_normal' . $ext);
517 $img_root = substr($path_parts['basename'], 0, -($end+1));
518 $mediatype = getMediatype($ext);
520 foreach (array('mini', 'normal', 'bigger') as $size) {
521 $url = $path_parts['dirname'] . '/' .
522 $img_root . '_' . $size . ".$ext";
523 $filename = 'Twitter_' . $user->id . '_' .
524 $img_root . "_$size.$ext";
526 if (fetchAvatar($url, $filename)) {
527 newAvatar($id, $size, $mediatype, $filename);
529 common_log(LOG_WARNING, "Problem fetching Avatar: $url", __FILE__);
530 if (defined('SCRIPT_DEBUG')) {
531 print "Problem fetching Avatar: $url\n";
537 function updateAvatar($profile_id, $size, $mediatype, $filename) {
539 <<<<<<< HEAD:scripts/statusfetcher.php
543 >>>>>>> b8c700a7454db825b3867eadfa22afa1e5eb4f6c:scripts/statusfetcher.php
544 common_debug("Updating avatar: $size");
545 if (defined('SCRIPT_DEBUG')) {
546 print "Updating avatar: $size\n";
549 $profile = Profile::staticGet($profile_id);
552 common_debug("Couldn't get profile: $profile_id!");
553 if (defined('SCRIPT_DEBUG')) {
554 print "Couldn't get profile: $profile_id!\n";
559 $sizes = array('mini' => 24, 'normal' => 48, 'bigger' => 73);
560 $avatar = $profile->getAvatar($sizes[$size]);
563 common_debug("Deleting $size avatar for $profile->nickname.");
564 @unlink(INSTALLDIR . '/avatar/' . $avatar->filename);
568 newAvatar($profile->id, $size, $mediatype, $filename);
571 function newAvatar($profile_id, $size, $mediatype, $filename)
575 $avatar = new Avatar();
576 $avatar->profile_id = $profile_id;
581 $avatar->height = 24;
585 $avatar->height = 48;
589 // Note: Twitter's big avatars are a different size than
590 // Laconica's (Laconica's = 96)
593 $avatar->height = 73;
596 $avatar->original = 0; // we don't have the original
597 $avatar->mediatype = $mediatype;
598 $avatar->filename = $filename;
599 $avatar->url = Avatar::url($filename);
601 common_debug("new filename: $avatar->url");
602 if (defined('SCRIPT_DEBUG')) {
603 print "New filename: $avatar->url\n";
606 $avatar->created = common_sql_now();
608 $id = $avatar->insert();
611 common_log_db_error($avatar, 'INSERT', __FILE__);
612 if (defined('SCRIPT_DEBUG')) {
613 print "Could not insert avatar!\n";
619 common_debug("Saved new $size avatar for $profile_id.");
620 if (defined('SCRIPT_DEBUG')) {
621 print "Saved new $size avatar for $profile_id.\n";
627 function fetchAvatar($url, $filename)
629 $avatar_dir = INSTALLDIR . '/avatar/';
631 $avatarfile = $avatar_dir . $filename;
633 $out = fopen($avatarfile, 'wb');
635 common_log(LOG_WARNING, "Couldn't open file $filename", __FILE__);
636 if (defined('SCRIPT_DEBUG')) {
637 print "Couldn't open file! $filename\n";
642 common_debug("Fetching avatar: $url", __FILE__);
643 if (defined('SCRIPT_DEBUG')) {
644 print "Fetching avatar from Twitter: $url\n";
648 curl_setopt($ch, CURLOPT_URL, $url);
649 curl_setopt($ch, CURLOPT_FILE, $out);
650 curl_setopt($ch, CURLOPT_BINARYTRANSFER, true);
651 curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
652 curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 0);
653 $result = curl_exec($ch);