]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - plugins/TwitterBridge/daemons/twitterstatusfetcher.php
debug code to dump new status data
[quix0rs-gnu-social.git] / plugins / TwitterBridge / daemons / twitterstatusfetcher.php
1 #!/usr/bin/env php
2 <?php
3 /**
4  * StatusNet - the distributed open-source microblogging tool
5  * Copyright (C) 2008-2010, StatusNet, 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 define('INSTALLDIR', realpath(dirname(__FILE__) . '/../../..'));
22
23 // Tune number of processes and how often to poll Twitter
24 // XXX: Should these things be in config.php?
25 define('MAXCHILDREN', 2);
26 define('POLL_INTERVAL', 60); // in seconds
27
28 $shortoptions = 'di::';
29 $longoptions = array('id::', 'debug');
30
31 $helptext = <<<END_OF_TRIM_HELP
32 Batch script for retrieving Twitter messages from foreign service.
33
34   -i --id              Identity (default 'generic')
35   -d --debug           Debug (lots of log output)
36
37 END_OF_TRIM_HELP;
38
39 require_once INSTALLDIR . '/scripts/commandline.inc';
40 require_once INSTALLDIR . '/lib/common.php';
41 require_once INSTALLDIR . '/lib/daemon.php';
42 require_once INSTALLDIR . '/plugins/TwitterBridge/twitter.php';
43 require_once INSTALLDIR . '/plugins/TwitterBridge/twitterbasicauthclient.php';
44 require_once INSTALLDIR . '/plugins/TwitterBridge/twitteroauthclient.php';
45
46 /**
47  * Fetch statuses from Twitter
48  *
49  * Fetches statuses from Twitter and inserts them as notices
50  *
51  * NOTE: an Avatar path MUST be set in config.php for this
52  * script to work, e.g.:
53  *     $config['avatar']['path'] = $config['site']['path'] . '/avatar/';
54  *
55  * @todo @fixme @gar Fix the above. For some reason $_path is always empty when
56  * this script is run, so the default avatar path is always set wrong in
57  * default.php. Therefore it must be set explicitly in config.php. --Z
58  *
59  * @category Twitter
60  * @package  StatusNet
61  * @author   Zach Copley <zach@status.net>
62  * @author   Evan Prodromou <evan@status.net>
63  * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
64  * @link     http://status.net/
65  */
66
67 class TwitterStatusFetcher extends ParallelizingDaemon
68 {
69     /**
70      *  Constructor
71      *
72      * @param string  $id           the name/id of this daemon
73      * @param int     $interval     sleep this long before doing everything again
74      * @param int     $max_children maximum number of child processes at a time
75      * @param boolean $debug        debug output flag
76      *
77      * @return void
78      *
79      **/
80     function __construct($id = null, $interval = 60,
81                          $max_children = 2, $debug = null)
82     {
83         parent::__construct($id, $interval, $max_children, $debug);
84     }
85
86     /**
87      * Name of this daemon
88      *
89      * @return string Name of the daemon.
90      */
91
92     function name()
93     {
94         return ('twitterstatusfetcher.'.$this->_id);
95     }
96
97     /**
98      * Find all the Twitter foreign links for users who have requested
99      * importing of their friends' timelines
100      *
101      * @return array flinks an array of Foreign_link objects
102      */
103
104     function getObjects()
105     {
106         global $_DB_DATAOBJECT;
107
108         $flink = new Foreign_link();
109         $conn = &$flink->getDatabaseConnection();
110
111         $flink->service = TWITTER_SERVICE;
112         $flink->orderBy('last_noticesync');
113         $flink->find();
114
115         $flinks = array();
116
117         while ($flink->fetch()) {
118
119             if (($flink->noticesync & FOREIGN_NOTICE_RECV) ==
120                 FOREIGN_NOTICE_RECV) {
121                 $flinks[] = clone($flink);
122                 common_log(LOG_INFO, "sync: foreign id $flink->foreign_id");
123             } else {
124                 common_log(LOG_INFO, "nothing to sync");
125             }
126         }
127
128         $flink->free();
129         unset($flink);
130
131         $conn->disconnect();
132         unset($_DB_DATAOBJECT['CONNECTIONS']);
133
134         return $flinks;
135     }
136
137     function childTask($flink) {
138
139         // Each child ps needs its own DB connection
140
141         // Note: DataObject::getDatabaseConnection() creates
142         // a new connection if there isn't one already
143
144         $conn = &$flink->getDatabaseConnection();
145
146         $this->getTimeline($flink);
147
148         $flink->last_friendsync = common_sql_now();
149         $flink->update();
150
151         $conn->disconnect();
152
153         // XXX: Couldn't find a less brutal way to blow
154         // away a cached connection
155
156         global $_DB_DATAOBJECT;
157         unset($_DB_DATAOBJECT['CONNECTIONS']);
158     }
159
160     function getTimeline($flink)
161     {
162         if (empty($flink)) {
163             common_log(LOG_WARNING, $this->name() .
164                        " - Can't retrieve Foreign_link for foreign ID $fid");
165             return;
166         }
167
168         common_debug($this->name() . ' - Trying to get timeline for Twitter user ' .
169                      $flink->foreign_id);
170
171         // XXX: Biggest remaining issue - How do we know at which status
172         // to start importing?  How many statuses?  Right now I'm going
173         // with the default last 20.
174
175         $client = null;
176
177         if (TwitterOAuthClient::isPackedToken($flink->credentials)) {
178             $token = TwitterOAuthClient::unpackToken($flink->credentials);
179             $client = new TwitterOAuthClient($token->key, $token->secret);
180             common_debug($this->name() . ' - Grabbing friends timeline with OAuth.');
181         } else {
182             $client = new TwitterBasicAuthClient($flink);
183             common_debug($this->name() . ' - Grabbing friends timeline with basic auth.');
184         }
185
186         $timeline = null;
187
188         try {
189             $timeline = $client->statusesFriendsTimeline();
190         } catch (Exception $e) {
191             common_log(LOG_WARNING, $this->name() .
192                        ' - Twitter client unable to get friends timeline for user ' .
193                        $flink->user_id . ' - code: ' .
194                        $e->getCode() . 'msg: ' . $e->getMessage());
195         }
196
197         if (empty($timeline)) {
198             common_log(LOG_WARNING, $this->name() .  " - Empty timeline.");
199             return;
200         }
201
202         common_debug(LOG_INFO, $this->name() . ' - Retrieved ' . sizeof($timeline) . ' statuses from Twitter.');
203
204         // Reverse to preserve order
205
206         foreach (array_reverse($timeline) as $status) {
207
208             // Hacktastic: filter out stuff coming from this StatusNet
209
210             $source = mb_strtolower(common_config('integration', 'source'));
211
212             if (preg_match("/$source/", mb_strtolower($status->source))) {
213                 common_debug($this->name() . ' - Skipping import of status ' .
214                              $status->id . ' with source ' . $source);
215                 continue;
216             }
217
218             // Don't save it if the user is protected
219             // FIXME: save it but treat it as private
220
221             if ($status->user->protected) {
222                 continue;
223             }
224
225             $notice = $this->saveStatus($status);
226
227             if (!empty($notice)) {
228                 Inbox::insertNotice($flink->user_id, $notice->id);
229             }
230         }
231
232         // Okay, record the time we synced with Twitter for posterity
233
234         $flink->last_noticesync = common_sql_now();
235         $flink->update();
236     }
237
238     function saveStatus($status)
239     {
240         $profile = $this->ensureProfile($status->user);
241
242         if (empty($profile)) {
243             common_log(LOG_ERR, $this->name() .
244                 ' - Problem saving notice. No associated Profile.');
245             return null;
246         }
247
248         $statusUri = $this->makeStatusURI($status->user->screen_name, $status->id);
249
250         // check to see if we've already imported the status
251
252         $dupe = $this->checkDupe($profile, $statusUri);
253
254         if (!empty($dupe)) {
255             common_log(
256                 LOG_INFO,
257                 $this->name() .
258                 " - Ignoring duplicate import: $statusUri"
259             );
260             return $dupe;
261         }
262
263         common_debug("Saving status {$status->id} with data " . print_r($status, true));
264
265         // If it's a retweet, save it as a repeat!
266
267         if (!empty($status->retweeted_status)) {
268             common_log(LOG_INFO, "Status {$status->id} is a retweet of {$status->retweeted_status->id}.");
269             $original = $this->saveStatus($status->retweeted_status);
270             $repeat = $original->repeat($profile->id, 'twitter');
271             common_log(LOG_INFO, "Saved {$repeat->id} as a repeat of {$original->id}");
272             return $repeat;
273         }
274
275         $notice = new Notice();
276
277         $notice->profile_id = $profile->id;
278         $notice->uri        = $statusUri;
279         $notice->url        = $statusUri;
280         $notice->created    = strftime(
281             '%Y-%m-%d %H:%M:%S',
282             strtotime($status->created_at)
283         );
284
285         $notice->source     = 'twitter';
286
287         $notice->reply_to   = null;
288
289         if (!empty($status->in_reply_to_status_id)) {
290             common_log(LOG_INFO, "Status {$status->id} is a reply to status {$status->in_reply_to_status_id}");
291             $replyUri = $this->makeStatusURI($status->in_reply_to_screen_name, $status->in_reply_to_status_id);
292             $reply = Notice::staticGet('uri', $replyUri);
293             if (empty($reply)) {
294                 common_log(LOG_INFO, "Couldn't find local notice for status {$status->in_reply_to_status_id}");
295             } else {
296                 common_log(LOG_INFO, "Found local notice {$reply->id} for status {$status->in_reply_to_status_id}");
297                 $notice->reply_to     = $reply->id;
298                 $notice->conversation = $reply->conversation;
299             }
300         }
301
302         if (empty($notice->conversation)) {
303             $conv = Conversation::create();
304             $notice->conversation = $conv->id;
305             common_log(LOG_INFO, "No known conversation for status {$status->id} so making a new one {$conv->id}.");
306         }
307
308         $notice->is_local   = Notice::GATEWAY;
309
310         $notice->content    = common_shorten_links($status->text);
311         $notice->rendered   = common_render_content(
312             $notice->content,
313             $notice
314         );
315
316         if (Event::handle('StartNoticeSave', array(&$notice))) {
317
318             $id = $notice->insert();
319
320             if (!$id) {
321                 common_log_db_error($notice, 'INSERT', __FILE__);
322                 common_log(LOG_ERR, $this->name() .
323                     ' - Problem saving notice.');
324             }
325
326             Event::handle('EndNoticeSave', array($notice));
327         }
328
329         $notice->blowOnInsert();
330
331         return $notice;
332     }
333
334     /**
335      * Make an URI for a status.
336      *
337      * @param object $status status object
338      *
339      * @return string URI
340      */
341
342     function makeStatusURI($username, $id)
343     {
344         return 'http://twitter.com/'
345           . $username
346           . '/status/'
347           . $id;
348     }
349
350     /**
351      * Look up a Profile by profileurl field.  Profile::staticGet() was
352      * not working consistently.
353      *
354      * @param string $nickname   local nickname of the Twitter user
355      * @param string $profileurl the profile url
356      *
357      * @return mixed value the first Profile with that url, or null
358      */
359
360     function getProfileByUrl($nickname, $profileurl)
361     {
362         $profile = new Profile();
363         $profile->nickname = $nickname;
364         $profile->profileurl = $profileurl;
365         $profile->limit(1);
366
367         if ($profile->find()) {
368             $profile->fetch();
369             return $profile;
370         }
371
372         return null;
373     }
374
375     /**
376      * Check to see if this Twitter status has already been imported
377      *
378      * @param Profile $profile   Twitter user's local profile
379      * @param string  $statusUri URI of the status on Twitter
380      *
381      * @return mixed value a matching Notice or null
382      */
383
384     function checkDupe($profile, $statusUri)
385     {
386         $notice = new Notice();
387         $notice->uri = $statusUri;
388         $notice->profile_id = $profile->id;
389         $notice->limit(1);
390
391         if ($notice->find()) {
392             $notice->fetch();
393             return $notice;
394         }
395
396         return null;
397     }
398
399     function ensureProfile($user)
400     {
401         // check to see if there's already a profile for this user
402
403         $profileurl = 'http://twitter.com/' . $user->screen_name;
404         $profile = $this->getProfileByUrl($user->screen_name, $profileurl);
405
406         if (!empty($profile)) {
407             common_debug($this->name() .
408                          " - Profile for $profile->nickname found.");
409
410             // Check to see if the user's Avatar has changed
411
412             $this->checkAvatar($user, $profile);
413             return $profile;
414
415         } else {
416
417             common_debug($this->name() . ' - Adding profile and remote profile ' .
418                          "for Twitter user: $profileurl.");
419
420             $profile = new Profile();
421             $profile->query("BEGIN");
422
423             $profile->nickname = $user->screen_name;
424             $profile->fullname = $user->name;
425             $profile->homepage = $user->url;
426             $profile->bio = $user->description;
427             $profile->location = $user->location;
428             $profile->profileurl = $profileurl;
429             $profile->created = common_sql_now();
430
431             try {
432                 $id = $profile->insert();
433             } catch(Exception $e) {
434                 common_log(LOG_WARNING, $this->name . ' Couldn\'t insert profile - ' . $e->getMessage());
435             }
436
437             if (empty($id)) {
438                 common_log_db_error($profile, 'INSERT', __FILE__);
439                 $profile->query("ROLLBACK");
440                 return false;
441             }
442
443             // check for remote profile
444
445             $remote_pro = Remote_profile::staticGet('uri', $profileurl);
446
447             if (empty($remote_pro)) {
448
449                 $remote_pro = new Remote_profile();
450
451                 $remote_pro->id = $id;
452                 $remote_pro->uri = $profileurl;
453                 $remote_pro->created = common_sql_now();
454
455                 try {
456                     $rid = $remote_pro->insert();
457                 } catch (Exception $e) {
458                     common_log(LOG_WARNING, $this->name() . ' Couldn\'t save remote profile - ' . $e->getMessage());
459                 }
460
461                 if (empty($rid)) {
462                     common_log_db_error($profile, 'INSERT', __FILE__);
463                     $profile->query("ROLLBACK");
464                     return false;
465                 }
466             }
467
468             $profile->query("COMMIT");
469
470             $this->saveAvatars($user, $id);
471
472             return $profile;
473         }
474     }
475
476     function checkAvatar($twitter_user, $profile)
477     {
478         global $config;
479
480         $path_parts = pathinfo($twitter_user->profile_image_url);
481
482         $newname = 'Twitter_' . $twitter_user->id . '_' .
483             $path_parts['basename'];
484
485         $oldname = $profile->getAvatar(48)->filename;
486
487         if ($newname != $oldname) {
488             common_debug($this->name() . ' - Avatar for Twitter user ' .
489                          "$profile->nickname has changed.");
490             common_debug($this->name() . " - old: $oldname new: $newname");
491
492             $this->updateAvatars($twitter_user, $profile);
493         }
494
495         if ($this->missingAvatarFile($profile)) {
496             common_debug($this->name() . ' - Twitter user ' .
497                          $profile->nickname .
498                          ' is missing one or more local avatars.');
499             common_debug($this->name() ." - old: $oldname new: $newname");
500
501             $this->updateAvatars($twitter_user, $profile);
502         }
503     }
504
505     function updateAvatars($twitter_user, $profile) {
506
507         global $config;
508
509         $path_parts = pathinfo($twitter_user->profile_image_url);
510
511         $img_root = substr($path_parts['basename'], 0, -11);
512         $ext = $path_parts['extension'];
513         $mediatype = $this->getMediatype($ext);
514
515         foreach (array('mini', 'normal', 'bigger') as $size) {
516             $url = $path_parts['dirname'] . '/' .
517                 $img_root . '_' . $size . ".$ext";
518             $filename = 'Twitter_' . $twitter_user->id . '_' .
519                 $img_root . "_$size.$ext";
520
521             $this->updateAvatar($profile->id, $size, $mediatype, $filename);
522             $this->fetchAvatar($url, $filename);
523         }
524     }
525
526     function missingAvatarFile($profile) {
527         foreach (array(24, 48, 73) as $size) {
528             $filename = $profile->getAvatar($size)->filename;
529             $avatarpath = Avatar::path($filename);
530             if (file_exists($avatarpath) == FALSE) {
531                 return true;
532             }
533         }
534         return false;
535     }
536
537     function getMediatype($ext)
538     {
539         $mediatype = null;
540
541         switch (strtolower($ext)) {
542         case 'jpg':
543             $mediatype = 'image/jpg';
544             break;
545         case 'gif':
546             $mediatype = 'image/gif';
547             break;
548         default:
549             $mediatype = 'image/png';
550         }
551
552         return $mediatype;
553     }
554
555     function saveAvatars($user, $id)
556     {
557         global $config;
558
559         $path_parts = pathinfo($user->profile_image_url);
560         $ext = $path_parts['extension'];
561         $end = strlen('_normal' . $ext);
562         $img_root = substr($path_parts['basename'], 0, -($end+1));
563         $mediatype = $this->getMediatype($ext);
564
565         foreach (array('mini', 'normal', 'bigger') as $size) {
566             $url = $path_parts['dirname'] . '/' .
567                 $img_root . '_' . $size . ".$ext";
568             $filename = 'Twitter_' . $user->id . '_' .
569                 $img_root . "_$size.$ext";
570
571             if ($this->fetchAvatar($url, $filename)) {
572                 $this->newAvatar($id, $size, $mediatype, $filename);
573             } else {
574                 common_log(LOG_WARNING, $id() .
575                            " - Problem fetching Avatar: $url");
576             }
577         }
578     }
579
580     function updateAvatar($profile_id, $size, $mediatype, $filename) {
581
582         common_debug($this->name() . " - Updating avatar: $size");
583
584         $profile = Profile::staticGet($profile_id);
585
586         if (empty($profile)) {
587             common_debug($this->name() . " - Couldn't get profile: $profile_id!");
588             return;
589         }
590
591         $sizes = array('mini' => 24, 'normal' => 48, 'bigger' => 73);
592         $avatar = $profile->getAvatar($sizes[$size]);
593
594         // Delete the avatar, if present
595
596         if ($avatar) {
597             $avatar->delete();
598         }
599
600         $this->newAvatar($profile->id, $size, $mediatype, $filename);
601     }
602
603     function newAvatar($profile_id, $size, $mediatype, $filename)
604     {
605         global $config;
606
607         $avatar = new Avatar();
608         $avatar->profile_id = $profile_id;
609
610         switch($size) {
611         case 'mini':
612             $avatar->width  = 24;
613             $avatar->height = 24;
614             break;
615         case 'normal':
616             $avatar->width  = 48;
617             $avatar->height = 48;
618             break;
619         default:
620
621             // Note: Twitter's big avatars are a different size than
622             // StatusNet's (StatusNet's = 96)
623
624             $avatar->width  = 73;
625             $avatar->height = 73;
626         }
627
628         $avatar->original = 0; // we don't have the original
629         $avatar->mediatype = $mediatype;
630         $avatar->filename = $filename;
631         $avatar->url = Avatar::url($filename);
632
633         $avatar->created = common_sql_now();
634
635         try {
636             $id = $avatar->insert();
637         } catch (Exception $e) {
638             common_log(LOG_WARNING, $this->name() . ' Couldn\'t insert avatar - ' . $e->getMessage());
639         }
640
641         if (empty($id)) {
642             common_log_db_error($avatar, 'INSERT', __FILE__);
643             return null;
644         }
645
646         common_debug($this->name() .
647                      " - Saved new $size avatar for $profile_id.");
648
649         return $id;
650     }
651
652     /**
653      * Fetch a remote avatar image and save to local storage.
654      *
655      * @param string $url avatar source URL
656      * @param string $filename bare local filename for download
657      * @return bool true on success, false on failure
658      */
659     function fetchAvatar($url, $filename)
660     {
661         common_debug($this->name() . " - Fetching Twitter avatar: $url");
662
663         $request = HTTPClient::start();
664         $response = $request->get($url);
665         if ($response->isOk()) {
666             $avatarfile = Avatar::path($filename);
667             $ok = file_put_contents($avatarfile, $response->getBody());
668             if (!$ok) {
669                 common_log(LOG_WARNING, $this->name() .
670                            " - Couldn't open file $filename");
671                 return false;
672             }
673         } else {
674             return false;
675         }
676
677         return true;
678     }
679 }
680
681 $id    = null;
682 $debug = null;
683
684 if (have_option('i')) {
685     $id = get_option_value('i');
686 } else if (have_option('--id')) {
687     $id = get_option_value('--id');
688 } else if (count($args) > 0) {
689     $id = $args[0];
690 } else {
691     $id = null;
692 }
693
694 if (have_option('d') || have_option('debug')) {
695     $debug = true;
696 }
697
698 $fetcher = new TwitterStatusFetcher($id, 60, 2, $debug);
699 $fetcher->runOnce();
700