]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - plugins/TwitterBridge/daemons/twitterstatusfetcher.php
Merge branch 'testing' into 0.9.x
[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/twitteroauthclient.php';
44
45 /**
46  * Fetch statuses from Twitter
47  *
48  * Fetches statuses from Twitter and inserts them as notices
49  *
50  * NOTE: an Avatar path MUST be set in config.php for this
51  * script to work, e.g.:
52  *     $config['avatar']['path'] = $config['site']['path'] . '/avatar/';
53  *
54  * @todo @fixme @gar Fix the above. For some reason $_path is always empty when
55  * this script is run, so the default avatar path is always set wrong in
56  * default.php. Therefore it must be set explicitly in config.php. --Z
57  *
58  * @category Twitter
59  * @package  StatusNet
60  * @author   Zach Copley <zach@status.net>
61  * @author   Evan Prodromou <evan@status.net>
62  * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
63  * @link     http://status.net/
64  */
65
66 class TwitterStatusFetcher extends ParallelizingDaemon
67 {
68     /**
69      *  Constructor
70      *
71      * @param string  $id           the name/id of this daemon
72      * @param int     $interval     sleep this long before doing everything again
73      * @param int     $max_children maximum number of child processes at a time
74      * @param boolean $debug        debug output flag
75      *
76      * @return void
77      *
78      **/
79     function __construct($id = null, $interval = 60,
80                          $max_children = 2, $debug = null)
81     {
82         parent::__construct($id, $interval, $max_children, $debug);
83     }
84
85     /**
86      * Name of this daemon
87      *
88      * @return string Name of the daemon.
89      */
90
91     function name()
92     {
93         return ('twitterstatusfetcher.'.$this->_id);
94     }
95
96     /**
97      * Find all the Twitter foreign links for users who have requested
98      * importing of their friends' timelines
99      *
100      * @return array flinks an array of Foreign_link objects
101      */
102
103     function getObjects()
104     {
105         global $_DB_DATAOBJECT;
106         $flink = new Foreign_link();
107         $conn = &$flink->getDatabaseConnection();
108
109         $flink->service = TWITTER_SERVICE;
110         $flink->orderBy('last_noticesync');
111         $flink->find();
112
113         $flinks = array();
114
115         while ($flink->fetch()) {
116
117             if (($flink->noticesync & FOREIGN_NOTICE_RECV) ==
118                 FOREIGN_NOTICE_RECV) {
119                 $flinks[] = clone($flink);
120                 common_log(LOG_INFO, "sync: foreign id $flink->foreign_id");
121             } else {
122                 common_log(LOG_INFO, "nothing to sync");
123             }
124         }
125
126         $flink->free();
127         unset($flink);
128
129         $conn->disconnect();
130         unset($_DB_DATAOBJECT['CONNECTIONS']);
131
132         return $flinks;
133     }
134
135     function childTask($flink) {
136
137         // Each child ps needs its own DB connection
138
139         // Note: DataObject::getDatabaseConnection() creates
140         // a new connection if there isn't one already
141
142         $conn = &$flink->getDatabaseConnection();
143
144         $this->getTimeline($flink);
145
146         $flink->last_friendsync = common_sql_now();
147         $flink->update();
148
149         $conn->disconnect();
150
151         // XXX: Couldn't find a less brutal way to blow
152         // away a cached connection
153
154         global $_DB_DATAOBJECT;
155         unset($_DB_DATAOBJECT['CONNECTIONS']);
156     }
157
158     function getTimeline($flink)
159     {
160         if (empty($flink)) {
161             common_log(LOG_WARNING, $this->name() .
162                        " - Can't retrieve Foreign_link for foreign ID $fid");
163             return;
164         }
165
166         common_debug($this->name() . ' - Trying to get timeline for Twitter user ' .
167                      $flink->foreign_id);
168
169         $client = null;
170
171         if (TwitterOAuthClient::isPackedToken($flink->credentials)) {
172             $token = TwitterOAuthClient::unpackToken($flink->credentials);
173             $client = new TwitterOAuthClient($token->key, $token->secret);
174             common_debug($this->name() . ' - Grabbing friends timeline with OAuth.');
175         } else {
176             common_debug("Skipping friends timeline for $flink->foreign_id since not OAuth.");
177         }
178
179         $timeline = null;
180
181         $lastId = Twitter_synch_status::getLastId($flink->foreign_id, 'home_timeline');
182
183         common_debug("Got lastId value '{$lastId}' for foreign id '{$flink->foreign_id}' and timeline 'home_timeline'");
184
185         try {
186             $timeline = $client->statusesHomeTimeline($lastId);
187         } catch (Exception $e) {
188             common_log(LOG_WARNING, $this->name() .
189                        ' - Twitter client unable to get friends timeline for user ' .
190                        $flink->user_id . ' - code: ' .
191                        $e->getCode() . 'msg: ' . $e->getMessage());
192         }
193
194         if (empty($timeline)) {
195             common_log(LOG_WARNING, $this->name() .  " - Empty timeline.");
196             return;
197         }
198
199         common_debug(LOG_INFO, $this->name() . ' - Retrieved ' . sizeof($timeline) . ' statuses from Twitter.');
200
201         // Reverse to preserve order
202
203         foreach (array_reverse($timeline) as $status) {
204
205             // Hacktastic: filter out stuff coming from this StatusNet
206
207             $source = mb_strtolower(common_config('integration', 'source'));
208
209             if (preg_match("/$source/", mb_strtolower($status->source))) {
210                 common_debug($this->name() . ' - Skipping import of status ' .
211                              $status->id . ' with source ' . $source);
212                 continue;
213             }
214
215             // Don't save it if the user is protected
216             // FIXME: save it but treat it as private
217
218             if ($status->user->protected) {
219                 continue;
220             }
221
222             $notice = $this->saveStatus($status);
223
224             if (!empty($notice)) {
225                 Inbox::insertNotice($flink->user_id, $notice->id);
226             }
227         }
228
229         if (!empty($timeline)) {
230             Twitter_synch_status::setLastId($flink->foreign_id, 'home_timeline', $timeline[0]->id);
231             common_debug("Set lastId value '{$timeline[0]->id}' for foreign id '{$flink->foreign_id}' and timeline 'home_timeline'");
232         }
233
234         // Okay, record the time we synced with Twitter for posterity
235
236         $flink->last_noticesync = common_sql_now();
237         $flink->update();
238     }
239
240     function saveStatus($status)
241     {
242         $profile = $this->ensureProfile($status->user);
243
244         if (empty($profile)) {
245             common_log(LOG_ERR, $this->name() .
246                 ' - Problem saving notice. No associated Profile.');
247             return null;
248         }
249
250         $statusUri = $this->makeStatusURI($status->user->screen_name, $status->id);
251
252         // check to see if we've already imported the status
253
254         $n2s = Notice_to_status::staticGet('status_id', $status->id);
255
256         if (!empty($n2s)) {
257             common_log(
258                 LOG_INFO,
259                 $this->name() .
260                 " - Ignoring duplicate import: {$status->id}"
261             );
262             return Notice::staticGet('id', $n2s->notice_id);
263         }
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             if (empty($original)) {
271                 return null;
272             } else {
273                 $author = $original->getProfile();
274                 // TRANS: Message used to repeat a notice. RT is the abbreviation of 'retweet'.
275                 // TRANS: %1$s is the repeated user's name, %2$s is the repeated notice.
276                 $content = sprintf(_('RT @%1$s %2$s'),
277                                    $author->nickname,
278                                    $original->content);
279
280                 if (Notice::contentTooLong($content)) {
281                     $contentlimit = Notice::maxContent();
282                     $content = mb_substr($content, 0, $contentlimit - 4) . ' ...';
283                 }
284
285                 $repeat = Notice::saveNew($profile->id,
286                                           $content,
287                                           'twitter',
288                                           array('repeat_of' => $original->id,
289                                                 'uri' => $statusUri,
290                                                 'is_local' => Notice::GATEWAY));
291                 common_log(LOG_INFO, "Saved {$repeat->id} as a repeat of {$original->id}");
292                 Notice_to_status::saveNew($repeat->id, $status->id);
293                 return $repeat;
294             }
295         }
296
297         $notice = new Notice();
298
299         $notice->profile_id = $profile->id;
300         $notice->uri        = $statusUri;
301         $notice->url        = $statusUri;
302         $notice->created    = strftime(
303             '%Y-%m-%d %H:%M:%S',
304             strtotime($status->created_at)
305         );
306
307         $notice->source     = 'twitter';
308
309         $notice->reply_to   = null;
310
311         if (!empty($status->in_reply_to_status_id)) {
312             common_log(LOG_INFO, "Status {$status->id} is a reply to status {$status->in_reply_to_status_id}");
313             $n2s = Notice_to_status::staticGet('status_id', $status->in_reply_to_status_id);
314             if (empty($n2s)) {
315                 common_log(LOG_INFO, "Couldn't find local notice for status {$status->in_reply_to_status_id}");
316             } else {
317                 $reply = Notice::staticGet('id', $n2s->notice_id);
318                 if (empty($reply)) {
319                     common_log(LOG_INFO, "Couldn't find local notice for status {$status->in_reply_to_status_id}");
320                 } else {
321                     common_log(LOG_INFO, "Found local notice {$reply->id} for status {$status->in_reply_to_status_id}");
322                     $notice->reply_to     = $reply->id;
323                     $notice->conversation = $reply->conversation;
324                 }
325             }
326         }
327
328         if (empty($notice->conversation)) {
329             $conv = Conversation::create();
330             $notice->conversation = $conv->id;
331             common_log(LOG_INFO, "No known conversation for status {$status->id} so making a new one {$conv->id}.");
332         }
333
334         $notice->is_local   = Notice::GATEWAY;
335
336         $notice->content  = html_entity_decode($status->text);
337         $notice->rendered = $this->linkify($status);
338
339         if (Event::handle('StartNoticeSave', array(&$notice))) {
340
341             $id = $notice->insert();
342
343             if (!$id) {
344                 common_log_db_error($notice, 'INSERT', __FILE__);
345                 common_log(LOG_ERR, $this->name() .
346                     ' - Problem saving notice.');
347             }
348
349             Event::handle('EndNoticeSave', array($notice));
350         }
351
352         Notice_to_status::saveNew($notice->id, $status->id);
353
354         $this->saveStatusMentions($notice, $status);
355
356         $notice->blowOnInsert();
357
358         return $notice;
359     }
360
361     /**
362      * Make an URI for a status.
363      *
364      * @param object $status status object
365      *
366      * @return string URI
367      */
368
369     function makeStatusURI($username, $id)
370     {
371         return 'http://twitter.com/'
372           . $username
373           . '/status/'
374           . $id;
375     }
376
377     /**
378      * Look up a Profile by profileurl field.  Profile::staticGet() was
379      * not working consistently.
380      *
381      * @param string $nickname   local nickname of the Twitter user
382      * @param string $profileurl the profile url
383      *
384      * @return mixed value the first Profile with that url, or null
385      */
386
387     function getProfileByUrl($nickname, $profileurl)
388     {
389         $profile = new Profile();
390         $profile->nickname = $nickname;
391         $profile->profileurl = $profileurl;
392         $profile->limit(1);
393
394         if ($profile->find()) {
395             $profile->fetch();
396             return $profile;
397         }
398
399         return null;
400     }
401
402     /**
403      * Check to see if this Twitter status has already been imported
404      *
405      * @param Profile $profile   Twitter user's local profile
406      * @param string  $statusUri URI of the status on Twitter
407      *
408      * @return mixed value a matching Notice or null
409      */
410
411     function checkDupe($profile, $statusUri)
412     {
413         $notice = new Notice();
414         $notice->uri = $statusUri;
415         $notice->profile_id = $profile->id;
416         $notice->limit(1);
417
418         if ($notice->find()) {
419             $notice->fetch();
420             return $notice;
421         }
422
423         return null;
424     }
425
426     function ensureProfile($user)
427     {
428         // check to see if there's already a profile for this user
429
430         $profileurl = 'http://twitter.com/' . $user->screen_name;
431         $profile = $this->getProfileByUrl($user->screen_name, $profileurl);
432
433         if (!empty($profile)) {
434             common_debug($this->name() .
435                          " - Profile for $profile->nickname found.");
436
437             // Check to see if the user's Avatar has changed
438
439             $this->checkAvatar($user, $profile);
440             return $profile;
441
442         } else {
443
444             common_debug($this->name() . ' - Adding profile and remote profile ' .
445                          "for Twitter user: $profileurl.");
446
447             $profile = new Profile();
448             $profile->query("BEGIN");
449
450             $profile->nickname = $user->screen_name;
451             $profile->fullname = $user->name;
452             $profile->homepage = $user->url;
453             $profile->bio = $user->description;
454             $profile->location = $user->location;
455             $profile->profileurl = $profileurl;
456             $profile->created = common_sql_now();
457
458             try {
459                 $id = $profile->insert();
460             } catch(Exception $e) {
461                 common_log(LOG_WARNING, $this->name . ' Couldn\'t insert profile - ' . $e->getMessage());
462             }
463
464             if (empty($id)) {
465                 common_log_db_error($profile, 'INSERT', __FILE__);
466                 $profile->query("ROLLBACK");
467                 return false;
468             }
469
470             // check for remote profile
471
472             $remote_pro = Remote_profile::staticGet('uri', $profileurl);
473
474             if (empty($remote_pro)) {
475
476                 $remote_pro = new Remote_profile();
477
478                 $remote_pro->id = $id;
479                 $remote_pro->uri = $profileurl;
480                 $remote_pro->created = common_sql_now();
481
482                 try {
483                     $rid = $remote_pro->insert();
484                 } catch (Exception $e) {
485                     common_log(LOG_WARNING, $this->name() . ' Couldn\'t save remote profile - ' . $e->getMessage());
486                 }
487
488                 if (empty($rid)) {
489                     common_log_db_error($profile, 'INSERT', __FILE__);
490                     $profile->query("ROLLBACK");
491                     return false;
492                 }
493             }
494
495             $profile->query("COMMIT");
496
497             $this->saveAvatars($user, $id);
498
499             return $profile;
500         }
501     }
502
503     function checkAvatar($twitter_user, $profile)
504     {
505         global $config;
506
507         $path_parts = pathinfo($twitter_user->profile_image_url);
508
509         $newname = 'Twitter_' . $twitter_user->id . '_' .
510             $path_parts['basename'];
511
512         $oldname = $profile->getAvatar(48)->filename;
513
514         if ($newname != $oldname) {
515             common_debug($this->name() . ' - Avatar for Twitter user ' .
516                          "$profile->nickname has changed.");
517             common_debug($this->name() . " - old: $oldname new: $newname");
518
519             $this->updateAvatars($twitter_user, $profile);
520         }
521
522         if ($this->missingAvatarFile($profile)) {
523             common_debug($this->name() . ' - Twitter user ' .
524                          $profile->nickname .
525                          ' is missing one or more local avatars.');
526             common_debug($this->name() ." - old: $oldname new: $newname");
527
528             $this->updateAvatars($twitter_user, $profile);
529         }
530     }
531
532     function updateAvatars($twitter_user, $profile) {
533
534         global $config;
535
536         $path_parts = pathinfo($twitter_user->profile_image_url);
537
538         $img_root = substr($path_parts['basename'], 0, -11);
539         $ext = $path_parts['extension'];
540         $mediatype = $this->getMediatype($ext);
541
542         foreach (array('mini', 'normal', 'bigger') as $size) {
543             $url = $path_parts['dirname'] . '/' .
544                 $img_root . '_' . $size . ".$ext";
545             $filename = 'Twitter_' . $twitter_user->id . '_' .
546                 $img_root . "_$size.$ext";
547
548             $this->updateAvatar($profile->id, $size, $mediatype, $filename);
549             $this->fetchAvatar($url, $filename);
550         }
551     }
552
553     function missingAvatarFile($profile) {
554         foreach (array(24, 48, 73) as $size) {
555             $filename = $profile->getAvatar($size)->filename;
556             $avatarpath = Avatar::path($filename);
557             if (file_exists($avatarpath) == FALSE) {
558                 return true;
559             }
560         }
561         return false;
562     }
563
564     function getMediatype($ext)
565     {
566         $mediatype = null;
567
568         switch (strtolower($ext)) {
569         case 'jpg':
570             $mediatype = 'image/jpg';
571             break;
572         case 'gif':
573             $mediatype = 'image/gif';
574             break;
575         default:
576             $mediatype = 'image/png';
577         }
578
579         return $mediatype;
580     }
581
582     function saveAvatars($user, $id)
583     {
584         global $config;
585
586         $path_parts = pathinfo($user->profile_image_url);
587         $ext = $path_parts['extension'];
588         $end = strlen('_normal' . $ext);
589         $img_root = substr($path_parts['basename'], 0, -($end+1));
590         $mediatype = $this->getMediatype($ext);
591
592         foreach (array('mini', 'normal', 'bigger') as $size) {
593             $url = $path_parts['dirname'] . '/' .
594                 $img_root . '_' . $size . ".$ext";
595             $filename = 'Twitter_' . $user->id . '_' .
596                 $img_root . "_$size.$ext";
597
598             if ($this->fetchAvatar($url, $filename)) {
599                 $this->newAvatar($id, $size, $mediatype, $filename);
600             } else {
601                 common_log(LOG_WARNING, $id() .
602                            " - Problem fetching Avatar: $url");
603             }
604         }
605     }
606
607     function updateAvatar($profile_id, $size, $mediatype, $filename) {
608
609         common_debug($this->name() . " - Updating avatar: $size");
610
611         $profile = Profile::staticGet($profile_id);
612
613         if (empty($profile)) {
614             common_debug($this->name() . " - Couldn't get profile: $profile_id!");
615             return;
616         }
617
618         $sizes = array('mini' => 24, 'normal' => 48, 'bigger' => 73);
619         $avatar = $profile->getAvatar($sizes[$size]);
620
621         // Delete the avatar, if present
622
623         if ($avatar) {
624             $avatar->delete();
625         }
626
627         $this->newAvatar($profile->id, $size, $mediatype, $filename);
628     }
629
630     function newAvatar($profile_id, $size, $mediatype, $filename)
631     {
632         global $config;
633
634         $avatar = new Avatar();
635         $avatar->profile_id = $profile_id;
636
637         switch($size) {
638         case 'mini':
639             $avatar->width  = 24;
640             $avatar->height = 24;
641             break;
642         case 'normal':
643             $avatar->width  = 48;
644             $avatar->height = 48;
645             break;
646         default:
647
648             // Note: Twitter's big avatars are a different size than
649             // StatusNet's (StatusNet's = 96)
650
651             $avatar->width  = 73;
652             $avatar->height = 73;
653         }
654
655         $avatar->original = 0; // we don't have the original
656         $avatar->mediatype = $mediatype;
657         $avatar->filename = $filename;
658         $avatar->url = Avatar::url($filename);
659
660         $avatar->created = common_sql_now();
661
662         try {
663             $id = $avatar->insert();
664         } catch (Exception $e) {
665             common_log(LOG_WARNING, $this->name() . ' Couldn\'t insert avatar - ' . $e->getMessage());
666         }
667
668         if (empty($id)) {
669             common_log_db_error($avatar, 'INSERT', __FILE__);
670             return null;
671         }
672
673         common_debug($this->name() .
674                      " - Saved new $size avatar for $profile_id.");
675
676         return $id;
677     }
678
679     /**
680      * Fetch a remote avatar image and save to local storage.
681      *
682      * @param string $url avatar source URL
683      * @param string $filename bare local filename for download
684      * @return bool true on success, false on failure
685      */
686     function fetchAvatar($url, $filename)
687     {
688         common_debug($this->name() . " - Fetching Twitter avatar: $url");
689
690         $request = HTTPClient::start();
691         $response = $request->get($url);
692         if ($response->isOk()) {
693             $avatarfile = Avatar::path($filename);
694             $ok = file_put_contents($avatarfile, $response->getBody());
695             if (!$ok) {
696                 common_log(LOG_WARNING, $this->name() .
697                            " - Couldn't open file $filename");
698                 return false;
699             }
700         } else {
701             return false;
702         }
703
704         return true;
705     }
706
707     const URL = 1;
708     const HASHTAG = 2;
709     const MENTION = 3;
710
711     function linkify($status)
712     {
713         $text = $status->text;
714
715         if (empty($status->entities)) {
716             return $text;
717         }
718
719         // Move all the entities into order so we can
720         // replace them in reverse order and thus
721         // not mess up their indices
722
723         $toReplace = array();
724
725         if (!empty($status->entities->urls)) {
726             foreach ($status->entities->urls as $url) {
727                 $toReplace[$url->indices[0]] = array(self::URL, $url);
728             }
729         }
730
731         if (!empty($status->entities->hashtags)) {
732             foreach ($status->entities->hashtags as $hashtag) {
733                 $toReplace[$hashtag->indices[0]] = array(self::HASHTAG, $hashtag);
734             }
735         }
736
737         if (!empty($status->entities->user_mentions)) {
738             foreach ($status->entities->user_mentions as $mention) {
739                 $toReplace[$mention->indices[0]] = array(self::MENTION, $mention);
740             }
741         }
742
743         // sort in reverse order by key
744
745         krsort($toReplace);
746
747         foreach ($toReplace as $part) {
748             list($type, $object) = $part;
749             switch($type) {
750             case self::URL:
751                 $linkText = $this->makeUrlLink($object);
752                 break;
753             case self::HASHTAG:
754                 $linkText = $this->makeHashtagLink($object);
755                 break;
756             case self::MENTION:
757                 $linkText = $this->makeMentionLink($object);
758                 break;
759             default:
760                 continue;
761             }
762             $text = mb_substr($text, 0, $object->indices[0]) . $linkText . mb_substr($text, $object->indices[1]);
763         }
764         return $text;
765     }
766
767     function makeUrlLink($object)
768     {
769         return "<a href='{$object->url}' class='extlink'>{$object->url}</a>";
770     }
771
772     function makeHashtagLink($object)
773     {
774         return "#<a href='https://twitter.com/search?q=%23{$object->text}' class='hashtag'>{$object->text}</a>";
775     }
776
777     function makeMentionLink($object)
778     {
779         return "@<a href='http://twitter.com/{$object->screen_name}' title='{$object->name}'>{$object->screen_name}</a>";
780     }
781
782     function saveStatusMentions($notice, $status)
783     {
784         $mentions = array();
785
786         if (empty($status->entities) || empty($status->entities->user_mentions)) {
787             return;
788         }
789
790         foreach ($status->entities->user_mentions as $mention) {
791             $flink = Foreign_link::getByForeignID($mention->id, TWITTER_SERVICE);
792             if (!empty($flink)) {
793                 $user = User::staticGet('id', $flink->user_id);
794                 if (!empty($user)) {
795                     $reply = new Reply();
796                     $reply->notice_id  = $notice->id;
797                     $reply->profile_id = $user->id;
798                     common_log(LOG_INFO, __METHOD__ . ": saving reply: notice {$notice->id} to profile {$user->id}");
799                     $id = $reply->insert();
800                 }
801             }
802         }
803     }
804 }
805
806 $id    = null;
807 $debug = null;
808
809 if (have_option('i')) {
810     $id = get_option_value('i');
811 } else if (have_option('--id')) {
812     $id = get_option_value('--id');
813 } else if (count($args) > 0) {
814     $id = $args[0];
815 } else {
816     $id = null;
817 }
818
819 if (have_option('d') || have_option('debug')) {
820     $debug = true;
821 }
822
823 $fetcher = new TwitterStatusFetcher($id, 60, 2, $debug);
824 $fetcher->runOnce();
825