]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - scripts/statusfetcher.php
82ae5bfd432bc92b2557a4fb1aa5593493abfbb3
[quix0rs-gnu-social.git] / scripts / statusfetcher.php
1 #!/usr/bin/env php
2 <?php
3 /*
4  * Laconica - a distributed open-source microblogging tool
5  * Copyright (C) 2008, Controlez-Vous, Inc.
6  *
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.
11  *
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.
16  *
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/>.
19  */
20
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";
24     exit();
25 }
26
27 define('INSTALLDIR', realpath(dirname(__FILE__) . '/..'));
28 define('LACONICA', true);
29
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
34 =======
35 define('POLL_INTERVAL', 60 * 5); // in seconds
36 >>>>>>> b8c700a7454db825b3867eadfa22afa1e5eb4f6c:scripts/statusfetcher.php
37
38 // Uncomment this to get useful console output
39 define('SCRIPT_DEBUG', true);
40
41 require_once(INSTALLDIR . '/lib/common.php');
42
43 $children = array();
44
45 do {
46
47 <<<<<<< HEAD:scripts/statusfetcher.php
48     $flinks = refreshFlinks();
49
50     foreach ($flinks as $f){
51
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
55 =======
56     $flink_ids = refreshFlinks();
57 >>>>>>> b8c700a7454db825b3867eadfa22afa1e5eb4f6c:scripts/statusfetcher.php
58
59         $conn = &$f->getDatabaseConnection();
60         $conn->disconnect();
61
62         $pid = pcntl_fork();
63
64         if ($pid == -1) {
65             die ("Couldn't fork!");
66         }
67
68         if ($pid) {
69
70             // Parent
71
72             if (defined('SCRIPT_DEBUG')) {
73                 print "Parent: forked " . $pid . "\n";
74             }
75
76             $children[] = $pid;
77
78         } else {
79
80             // Child
81
82 <<<<<<< HEAD:scripts/statusfetcher.php
83             getTimeline($f, $child_db_name);
84 =======
85             // XXX: Each child needs its own DB connection
86             getTimeline($f);
87 >>>>>>> b8c700a7454db825b3867eadfa22afa1e5eb4f6c:scripts/statusfetcher.php
88             exit();
89         }
90
91         // Remove child from ps list as it finishes
92         while(($c = pcntl_wait($status, WNOHANG OR WUNTRACED)) > 0) {
93
94             if (defined('SCRIPT_DEBUG')) {
95                 print "Child $c finished.\n";
96             }
97
98             remove_ps($children, $c);
99         }
100
101         // Wait if we have too many kids
102 <<<<<<< HEAD:scripts/statusfetcher.php
103         if (sizeof($children) > MAXCHILDREN) {
104
105             if (defined('SCRIPT_DEBUG')) {
106                 print "Too many children. Waiting...\n";
107             }
108
109             if (($c = pcntl_wait($status, WUNTRACED)) > 0){
110
111 =======
112         if(sizeof($children) > MAXCHILDREN) {
113             if (defined('SCRIPT_DEBUG')) {
114                 print "Too many children. Waiting...\n";
115             }
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";
120                 }
121
122                 remove_ps($children, $c);
123             }
124         }
125     }
126
127     // Remove all children from the process list before restarting
128     while(($c = pcntl_wait($status, WUNTRACED)) > 0) {
129
130         if (defined('SCRIPT_DEBUG')) {
131             print "Child $c finished.\n";
132         }
133
134         remove_ps($children, $c);
135     }
136
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";
143     }
144
145     sleep(POLL_INTERVAL);
146
147 } while (true);
148
149
150 function refreshFlinks() {
151
152 <<<<<<< HEAD:scripts/statusfetcher.php
153 =======
154     global $config;
155
156 >>>>>>> b8c700a7454db825b3867eadfa22afa1e5eb4f6c:scripts/statusfetcher.php
157     $flink = new Foreign_link();
158     $flink->service = 1; // Twitter
159     $flink->orderBy('last_noticesync');
160
161     $cnt = $flink->find();
162
163     if (defined('SCRIPT_DEBUG')) {
164         print "Updating Twitter friends subscriptions for $cnt users.\n";
165     }
166
167     $flinks = array();
168
169     while ($flink->fetch()) {
170
171         if (($flink->noticesync & FOREIGN_NOTICE_RECV) == FOREIGN_NOTICE_RECV) {
172             $flinks[] = clone($flink);
173         }
174     }
175
176     $flink->free();
177     unset($flink);
178
179     return $flinks;
180 }
181
182 function remove_ps(&$plist, $ps){
183     for ($i = 0; $i < sizeof($plist); $i++) {
184         if ($plist[$i] == $ps) {
185             unset($plist[$i]);
186             $plist = array_values($plist);
187             break;
188         }
189     }
190 }
191
192 function getTimeline($flink)
193 {
194
195 <<<<<<< HEAD:scripts/statusfetcher.php
196     if (empty($flink)) {
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";
200         }
201         return;
202     }
203
204     $fuser = $flink->getForeignUser();
205 =======
206     global $config;
207     $config['db'] = &PEAR::getStaticProperty('DB_DataObject','options');
208     require_once(INSTALLDIR . '/lib/common.php');
209
210     if (defined('SCRIPT_DEBUG')) {
211         print "Trying to get timeline for $flink->foreign_id\n";
212     }
213
214     if (empty($flink)) {
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";
218         }
219         return;
220     }
221
222     $fuser = new Foreign_user();
223     $fuser->service = 1;
224     $fuser->id = $flink->foreign_id;
225     $fuser->limit(1);
226     $fuser->find(true);
227 >>>>>>> b8c700a7454db825b3867eadfa22afa1e5eb4f6c:scripts/statusfetcher.php
228
229     if (empty($fuser)) {
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";
233         }
234         return;
235     }
236
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";
243 =======
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
248     }
249
250     $url = 'http://twitter.com/statuses/friends_timeline.json';
251
252     $timeline_json = get_twitter_data($url, $fuser->nickname,
253         $flink->credentials);
254
255     $timeline = json_decode($timeline_json);
256
257     if (empty($timeline)) {
258         common_log(LOG_WARNING, "Empty timeline.");
259          if (defined('SCRIPT_DEBUG')) {
260             print "Empty timeline!\n";
261         }
262         return;
263     }
264
265     foreach ($timeline as $status) {
266
267         // Hacktastic: filter out stuff coming from Laconica
268         $source = mb_strtolower(common_config('integration', 'source'));
269
270         if (preg_match("/$source/", mb_strtolower($status->source))) {
271             continue;
272         }
273
274         saveStatus($status, $flink);
275     }
276
277     // Okay, record the time we synced with Twitter for posterity
278
279     $flink->last_noticesync = common_sql_now();
280     $flink->update();
281 }
282
283 function saveStatus($status, $flink)
284 {
285 <<<<<<< HEAD:scripts/statusfetcher.php
286 =======
287
288     global $config;
289     $config['db'] = &PEAR::getStaticProperty('DB_DataObject','options');
290     require_once(INSTALLDIR . '/lib/common.php');
291
292     // Do we have a profile for this Twitter user?
293
294 >>>>>>> b8c700a7454db825b3867eadfa22afa1e5eb4f6c:scripts/statusfetcher.php
295     $id = ensureProfile($status->user);
296     $profile = Profile::staticGet($id);
297
298     if (!$profile) {
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";
302         }
303         return null;
304     }
305
306     $uri = 'http://twitter.com/' . $status->user->screen_name .
307         '/status/' . $status->id;
308
309     // Skip save if notice source is Laconica or Identi.ca?
310
311     $notice = Notice::staticGet('uri', $uri);
312
313     // check to see if we've already imported the status
314     if (!$notice) {
315
316         $notice = new Notice();
317         $notice->profile_id = $id;
318
319         $notice->query('BEGIN');
320
321         // XXX: figure out reply_to
322         $notice->reply_to = null;
323
324         // XXX: Should this be common_sql_now() instead of status create date?
325
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;
332         $notice->uri = $uri;
333
334         $notice_id = $notice->insert();
335
336         if (!$notice_id) {
337             common_log_db_error($notice, 'INSERT', __FILE__);
338             if (defined('SCRIPT_DEBUG')) {
339                 print "Could not save notice!\n";
340             }
341         }
342
343         // XXX: Figure out a better way to link replies?
344         $notice->saveReplies();
345
346         // XXX: Do we want to polute our tag cloud with hashtags from Twitter?
347         $notice->saveTags();
348         $notice->saveGroups();
349
350         $notice->query('COMMIT');
351
352 <<<<<<< HEAD:scripts/statusfetcher.php
353         common_debug("Saved status $status->id as notice $notice->id.");
354 =======
355 >>>>>>> b8c700a7454db825b3867eadfa22afa1e5eb4f6c:scripts/statusfetcher.php
356         if (defined('SCRIPT_DEBUG')) {
357             print "Saved status $status->id as notice $notice->id.\n";
358         }
359     }
360
361     if (!Notice_inbox::staticGet('notice_id', $notice->id)) {
362
363         // Add to inbox
364         $inbox = new Notice_inbox();
365         $inbox->user_id = $flink->user_id;
366         $inbox->notice_id = $notice->id;
367         $inbox->created = common_sql_now();
368
369         $inbox->insert();
370     }
371 }
372
373 function ensureProfile($user)
374 {
375 <<<<<<< HEAD:scripts/statusfetcher.php
376 =======
377     global $config;
378
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);
383
384     if ($profile) {
385         common_debug("Profile for $profile->nickname found.");
386
387         // Check to see if the user's Avatar has changed
388         checkAvatar($user, $profile);
389         return $profile->id;
390
391     } else {
392         $debugmsg = 'Adding profile and remote profile ' .
393             "for Twitter user: $profileurl\n";
394         common_debug($debugmsg, __FILE__);
395         if (defined('SCRIPT_DEBUG')) {
396             print $debugmsg;
397         }
398
399         $profile = new Profile();
400         $profile->query("BEGIN");
401
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();
409
410         $id = $profile->insert();
411
412         if (empty($id)) {
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";
417             }
418             $profile->query("ROLLBACK");
419             return false;
420         }
421
422         // check for remote profile
423         $remote_pro = Remote_profile::staticGet('uri', $profileurl);
424
425         if (!$remote_pro) {
426
427             $remote_pro = new Remote_profile();
428
429             $remote_pro->id = $id;
430             $remote_pro->uri = $profileurl;
431             $remote_pro->created = common_sql_now();
432
433             $rid = $remote_pro->insert();
434
435             if (empty($rid)) {
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";
440                 }
441                 $profile->query("ROLLBACK");
442                 return false;
443             }
444         }
445
446         $profile->query("COMMIT");
447
448         saveAvatars($user, $id);
449
450         return $id;
451     }
452 }
453
454 function checkAvatar($user, $profile)
455 {
456     global $config;
457
458     $path_parts = pathinfo($user->profile_image_url);
459     $newname = 'Twitter_' . $user->id . '_' .
460         $path_parts['basename'];
461
462     $oldname = $profile->getAvatar(48)->filename;
463
464     if ($newname != $oldname) {
465
466         common_debug("Avatar for Twitter user $profile->nickname has changed.");
467         common_debug("old: $oldname new: $newname");
468
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";
473         }
474
475         $img_root = substr($path_parts['basename'], 0, -11);
476         $ext = $path_parts['extension'];
477         $mediatype = getMediatype($ext);
478
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";
484
485             if (fetchAvatar($url, $filename)) {
486                 updateAvatar($profile->id, $size, $mediatype, $filename);
487             }
488         }
489     }
490 }
491
492 function getMediatype($ext)
493 {
494     $mediatype = null;
495
496     switch (strtolower($ext)) {
497     case 'jpg':
498         $mediatype = 'image/jpg';
499         break;
500     case 'gif':
501         $mediatype = 'image/gif';
502         break;
503     default:
504         $mediatype = 'image/png';
505     }
506
507     return $mediatype;
508 }
509
510 function saveAvatars($user, $id)
511 {
512     global $config;
513
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);
519
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";
525
526         if (fetchAvatar($url, $filename)) {
527             newAvatar($id, $size, $mediatype, $filename);
528         } else {
529             common_log(LOG_WARNING, "Problem fetching Avatar: $url", __FILE__);
530             if (defined('SCRIPT_DEBUG')) {
531                 print "Problem fetching Avatar: $url\n";
532             }
533         }
534     }
535 }
536
537 function updateAvatar($profile_id, $size, $mediatype, $filename) {
538
539 <<<<<<< HEAD:scripts/statusfetcher.php
540 =======
541     global $config;
542
543 >>>>>>> b8c700a7454db825b3867eadfa22afa1e5eb4f6c:scripts/statusfetcher.php
544     common_debug("Updating avatar: $size");
545     if (defined('SCRIPT_DEBUG')) {
546         print "Updating avatar: $size\n";
547     }
548
549     $profile = Profile::staticGet($profile_id);
550
551     if (!$profile) {
552         common_debug("Couldn't get profile: $profile_id!");
553         if (defined('SCRIPT_DEBUG')) {
554             print "Couldn't get profile: $profile_id!\n";
555         }
556         return;
557     }
558
559     $sizes = array('mini' => 24, 'normal' => 48, 'bigger' => 73);
560     $avatar = $profile->getAvatar($sizes[$size]);
561
562     if ($avatar) {
563         common_debug("Deleting $size avatar for $profile->nickname.");
564         @unlink(INSTALLDIR . '/avatar/' . $avatar->filename);
565         $avatar->delete();
566     }
567
568     newAvatar($profile->id, $size, $mediatype, $filename);
569 }
570
571 function newAvatar($profile_id, $size, $mediatype, $filename)
572 {
573     global $config;
574
575     $avatar = new Avatar();
576     $avatar->profile_id = $profile_id;
577
578     switch($size) {
579     case 'mini':
580         $avatar->width  = 24;
581         $avatar->height = 24;
582         break;
583     case 'normal':
584         $avatar->width  = 48;
585         $avatar->height = 48;
586         break;
587     default:
588
589         // Note: Twitter's big avatars are a different size than
590         // Laconica's (Laconica's = 96)
591
592         $avatar->width  = 73;
593         $avatar->height = 73;
594     }
595
596     $avatar->original = 0; // we don't have the original
597     $avatar->mediatype = $mediatype;
598     $avatar->filename = $filename;
599     $avatar->url = Avatar::url($filename);
600
601     common_debug("new filename: $avatar->url");
602     if (defined('SCRIPT_DEBUG')) {
603         print "New filename: $avatar->url\n";
604     }
605
606     $avatar->created = common_sql_now();
607
608     $id = $avatar->insert();
609
610     if (!$id) {
611         common_log_db_error($avatar, 'INSERT', __FILE__);
612         if (defined('SCRIPT_DEBUG')) {
613             print "Could not insert avatar!\n";
614         }
615
616         return null;
617     }
618
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";
622     }
623
624     return $id;
625 }
626
627 function fetchAvatar($url, $filename)
628 {
629     $avatar_dir = INSTALLDIR . '/avatar/';
630
631     $avatarfile = $avatar_dir . $filename;
632
633     $out = fopen($avatarfile, 'wb');
634     if (!$out) {
635         common_log(LOG_WARNING, "Couldn't open file $filename", __FILE__);
636         if (defined('SCRIPT_DEBUG')) {
637             print "Couldn't open file! $filename\n";
638         }
639         return false;
640     }
641
642     common_debug("Fetching avatar: $url", __FILE__);
643     if (defined('SCRIPT_DEBUG')) {
644         print "Fetching avatar from Twitter: $url\n";
645     }
646
647     $ch = curl_init();
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);
654     curl_close($ch);
655
656     fclose($out);
657
658     return $result;
659 }