]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - scripts/twitterstatusfetcher.php
Merge branch '0.8.x' of git@gitorious.org:statusnet/mainline into 0.9.x
[quix0rs-gnu-social.git] / scripts / twitterstatusfetcher.php
1 #!/usr/bin/env php
2 <?php
3 /**
4  * StatusNet - the distributed open-source microblogging tool
5  * Copyright (C) 2008, 2009, 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/daemon.php';
41
42 /**
43  * Fetcher for statuses from Twitter
44  *
45  * Fetches statuses from Twitter and inserts them as notices in local
46  * system.
47  *
48  * @category Twitter
49  * @package  StatusNet
50  * @author   Zach Copley <zach@status.net>
51  * @author   Evan Prodromou <evan@status.net>
52  * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
53  * @link     http://status.net/
54  */
55
56 // NOTE: an Avatar path MUST be set in config.php for this
57 // script to work: e.g.: $config['avatar']['path'] = '/statusnet/avatar';
58
59 class TwitterStatusFetcher extends ParallelizingDaemon
60 {
61     /**
62      *  Constructor
63      *
64      * @param string  $id           the name/id of this daemon
65      * @param int     $interval     sleep this long before doing everything again
66      * @param int     $max_children maximum number of child processes at a time
67      * @param boolean $debug        debug output flag
68      *
69      * @return void
70      *
71      **/
72     function __construct($id = null, $interval = 60,
73                          $max_children = 2, $debug = null)
74     {
75         parent::__construct($id, $interval, $max_children, $debug);
76     }
77
78     /**
79      * Name of this daemon
80      *
81      * @return string Name of the daemon.
82      */
83
84     function name()
85     {
86         return ('twitterstatusfetcher.'.$this->_id);
87     }
88
89     /**
90      * Find all the Twitter foreign links for users who have requested
91      * importing of their friends' timelines
92      *
93      * @return array flinks an array of Foreign_link objects
94      */
95
96     function getObjects()
97     {
98         global $_DB_DATAOBJECT;
99
100         $flink = new Foreign_link();
101         $conn = &$flink->getDatabaseConnection();
102
103         $flink->service = TWITTER_SERVICE;
104         $flink->orderBy('last_noticesync');
105         $flink->find();
106
107         $flinks = array();
108
109         while ($flink->fetch()) {
110
111             if (($flink->noticesync & FOREIGN_NOTICE_RECV) ==
112                 FOREIGN_NOTICE_RECV) {
113                 $flinks[] = clone($flink);
114             }
115         }
116
117         $flink->free();
118         unset($flink);
119
120         $conn->disconnect();
121         unset($_DB_DATAOBJECT['CONNECTIONS']);
122
123         return $flinks;
124     }
125
126     function childTask($flink) {
127
128         // Each child ps needs its own DB connection
129
130         // Note: DataObject::getDatabaseConnection() creates
131         // a new connection if there isn't one already
132
133         $conn = &$flink->getDatabaseConnection();
134
135         $this->getTimeline($flink);
136
137         $flink->last_friendsync = common_sql_now();
138         $flink->update();
139
140         $conn->disconnect();
141
142         // XXX: Couldn't find a less brutal way to blow
143         // away a cached connection
144
145         global $_DB_DATAOBJECT;
146         unset($_DB_DATAOBJECT['CONNECTIONS']);
147     }
148
149     function getTimeline($flink)
150     {
151         if (empty($flink)) {
152             common_log(LOG_WARNING, $this->name() .
153                        " - Can't retrieve Foreign_link for foreign ID $fid");
154             return;
155         }
156
157         common_debug($this->name() . ' - Trying to get timeline for Twitter user ' .
158                      $flink->foreign_id);
159
160         // XXX: Biggest remaining issue - How do we know at which status
161         // to start importing?  How many statuses?  Right now I'm going
162         // with the default last 20.
163
164         $client = null;
165
166         if (TwitterOAuthClient::isPackedToken($flink->credentials)) {
167             $token = TwitterOAuthClient::unpackToken($flink->credentials);
168             $client = new TwitterOAuthClient($token->key, $token->secret);
169             common_debug($this->name() . ' - Grabbing friends timeline with OAuth.');
170         } else {
171             $client = new TwitterBasicAuthClient($flink);
172             common_debug($this->name() . ' - Grabbing friends timeline with basic auth.');
173         }
174
175         $timeline = null;
176
177         try {
178             $timeline = $client->statusesFriendsTimeline();
179         } catch (Exception $e) {
180             common_log(LOG_WARNING, $this->name() .
181                        ' - Twitter client unable to get friends timeline for user ' .
182                        $flink->user_id . ' - code: ' .
183                        $e->getCode() . 'msg: ' . $e->getMessage());
184         }
185
186         if (empty($timeline)) {
187             common_log(LOG_WARNING, $this->name() .  " - Empty timeline.");
188             return;
189         }
190
191         // Reverse to preserve order
192
193         foreach (array_reverse($timeline) as $status) {
194
195             // Hacktastic: filter out stuff coming from this StatusNet
196
197             $source = mb_strtolower(common_config('integration', 'source'));
198
199             if (preg_match("/$source/", mb_strtolower($status->source))) {
200                 common_debug($this->name() . ' - Skipping import of status ' .
201                              $status->id . ' with source ' . $source);
202                 continue;
203             }
204
205             $this->saveStatus($status, $flink);
206         }
207
208         // Okay, record the time we synced with Twitter for posterity
209
210         $flink->last_noticesync = common_sql_now();
211         $flink->update();
212     }
213
214     function saveStatus($status, $flink)
215     {
216         $id = $this->ensureProfile($status->user);
217
218         $profile = Profile::staticGet($id);
219
220         if (empty($profile)) {
221             common_log(LOG_ERR, $this->name() .
222                 ' - Problem saving notice. No associated Profile.');
223             return null;
224         }
225
226         // XXX: change of screen name?
227
228         $uri = 'http://twitter.com/' . $status->user->screen_name .
229             '/status/' . $status->id;
230
231         $notice = Notice::staticGet('uri', $uri);
232
233         // check to see if we've already imported the status
234
235         if (empty($notice)) {
236
237             $notice = new Notice();
238
239             $notice->profile_id = $id;
240             $notice->uri        = $uri;
241             $notice->created    = strftime('%Y-%m-%d %H:%M:%S',
242                                            strtotime($status->created_at));
243             $notice->content    = common_shorten_links($status->text); // XXX
244             $notice->rendered   = common_render_content($notice->content, $notice);
245             $notice->source     = 'twitter';
246             $notice->reply_to   = null; // XXX: lookup reply
247             $notice->is_local   = Notice::GATEWAY;
248
249             if (Event::handle('StartNoticeSave', array(&$notice))) {
250                 $id = $notice->insert();
251                 Event::handle('EndNoticeSave', array($notice));
252             }
253         }
254
255         if (!Notice_inbox::pkeyGet(array('notice_id' => $notice->id,
256                                          'user_id' => $flink->user_id))) {
257             // Add to inbox
258             $inbox = new Notice_inbox();
259
260             $inbox->user_id   = $flink->user_id;
261             $inbox->notice_id = $notice->id;
262             $inbox->created   = $notice->created;
263             $inbox->source    = NOTICE_INBOX_SOURCE_GATEWAY; // From a private source
264
265             $inbox->insert();
266         }
267     }
268
269     function ensureProfile($user)
270     {
271         // check to see if there's already a profile for this user
272
273         $profileurl = 'http://twitter.com/' . $user->screen_name;
274         $profile = Profile::staticGet('profileurl', $profileurl);
275
276         if (!empty($profile)) {
277             common_debug($this->name() .
278                          " - Profile for $profile->nickname found.");
279
280             // Check to see if the user's Avatar has changed
281
282             $this->checkAvatar($user, $profile);
283             return $profile->id;
284
285         } else {
286             common_debug($this->name() . ' - Adding profile and remote profile ' .
287                          "for Twitter user: $profileurl.");
288
289             $profile = new Profile();
290             $profile->query("BEGIN");
291
292             $profile->nickname = $user->screen_name;
293             $profile->fullname = $user->name;
294             $profile->homepage = $user->url;
295             $profile->bio = $user->description;
296             $profile->location = $user->location;
297             $profile->profileurl = $profileurl;
298             $profile->created = common_sql_now();
299
300             $id = $profile->insert();
301
302             if (empty($id)) {
303                 common_log_db_error($profile, 'INSERT', __FILE__);
304                 $profile->query("ROLLBACK");
305                 return false;
306             }
307
308             // check for remote profile
309
310             $remote_pro = Remote_profile::staticGet('uri', $profileurl);
311
312             if (empty($remote_pro)) {
313
314                 $remote_pro = new Remote_profile();
315
316                 $remote_pro->id = $id;
317                 $remote_pro->uri = $profileurl;
318                 $remote_pro->created = common_sql_now();
319
320                 $rid = $remote_pro->insert();
321
322                 if (empty($rid)) {
323                     common_log_db_error($profile, 'INSERT', __FILE__);
324                     $profile->query("ROLLBACK");
325                     return false;
326                 }
327             }
328
329             $profile->query("COMMIT");
330
331             $this->saveAvatars($user, $id);
332
333             return $id;
334         }
335     }
336
337     function checkAvatar($twitter_user, $profile)
338     {
339         global $config;
340
341         $path_parts = pathinfo($twitter_user->profile_image_url);
342
343         $newname = 'Twitter_' . $twitter_user->id . '_' .
344             $path_parts['basename'];
345
346         $oldname = $profile->getAvatar(48)->filename;
347
348         if ($newname != $oldname) {
349             common_debug($this->name() . ' - Avatar for Twitter user ' .
350                          "$profile->nickname has changed.");
351             common_debug($this->name() . " - old: $oldname new: $newname");
352
353             $this->updateAvatars($twitter_user, $profile);
354         }
355
356         if ($this->missingAvatarFile($profile)) {
357             common_debug($this->name() . ' - Twitter user ' .
358                          $profile->nickname .
359                          ' is missing one or more local avatars.');
360             common_debug($this->name() ." - old: $oldname new: $newname");
361
362             $this->updateAvatars($twitter_user, $profile);
363         }
364
365     }
366
367     function updateAvatars($twitter_user, $profile) {
368
369         global $config;
370
371         $path_parts = pathinfo($twitter_user->profile_image_url);
372
373         $img_root = substr($path_parts['basename'], 0, -11);
374         $ext = $path_parts['extension'];
375         $mediatype = $this->getMediatype($ext);
376
377         foreach (array('mini', 'normal', 'bigger') as $size) {
378             $url = $path_parts['dirname'] . '/' .
379                 $img_root . '_' . $size . ".$ext";
380             $filename = 'Twitter_' . $twitter_user->id . '_' .
381                 $img_root . "_$size.$ext";
382
383             $this->updateAvatar($profile->id, $size, $mediatype, $filename);
384             $this->fetchAvatar($url, $filename);
385         }
386     }
387
388     function missingAvatarFile($profile) {
389
390         foreach (array(24, 48, 73) as $size) {
391
392             $filename = $profile->getAvatar($size)->filename;
393             $avatarpath = Avatar::path($filename);
394
395             if (file_exists($avatarpath) == FALSE) {
396                 return true;
397             }
398         }
399
400         return false;
401     }
402
403     function getMediatype($ext)
404     {
405         $mediatype = null;
406
407         switch (strtolower($ext)) {
408         case 'jpg':
409             $mediatype = 'image/jpg';
410             break;
411         case 'gif':
412             $mediatype = 'image/gif';
413             break;
414         default:
415             $mediatype = 'image/png';
416         }
417
418         return $mediatype;
419     }
420
421     function saveAvatars($user, $id)
422     {
423         global $config;
424
425         $path_parts = pathinfo($user->profile_image_url);
426         $ext = $path_parts['extension'];
427         $end = strlen('_normal' . $ext);
428         $img_root = substr($path_parts['basename'], 0, -($end+1));
429         $mediatype = $this->getMediatype($ext);
430
431         foreach (array('mini', 'normal', 'bigger') as $size) {
432             $url = $path_parts['dirname'] . '/' .
433                 $img_root . '_' . $size . ".$ext";
434             $filename = 'Twitter_' . $user->id . '_' .
435                 $img_root . "_$size.$ext";
436
437             if ($this->fetchAvatar($url, $filename)) {
438                 $this->newAvatar($id, $size, $mediatype, $filename);
439             } else {
440                 common_log(LOG_WARNING, $this->id() .
441                            " - Problem fetching Avatar: $url");
442             }
443         }
444     }
445
446     function updateAvatar($profile_id, $size, $mediatype, $filename) {
447
448         common_debug($this->name() . " - Updating avatar: $size");
449
450         $profile = Profile::staticGet($profile_id);
451
452         if (empty($profile)) {
453             common_debug($this->name() . " - Couldn't get profile: $profile_id!");
454             return;
455         }
456
457         $sizes = array('mini' => 24, 'normal' => 48, 'bigger' => 73);
458         $avatar = $profile->getAvatar($sizes[$size]);
459
460         // Delete the avatar, if present
461
462         if ($avatar) {
463             $avatar->delete();
464         }
465
466         $this->newAvatar($profile->id, $size, $mediatype, $filename);
467     }
468
469     function newAvatar($profile_id, $size, $mediatype, $filename)
470     {
471         global $config;
472
473         $avatar = new Avatar();
474         $avatar->profile_id = $profile_id;
475
476         switch($size) {
477         case 'mini':
478             $avatar->width  = 24;
479             $avatar->height = 24;
480             break;
481         case 'normal':
482             $avatar->width  = 48;
483             $avatar->height = 48;
484             break;
485         default:
486
487             // Note: Twitter's big avatars are a different size than
488             // StatusNet's (StatusNet's = 96)
489
490             $avatar->width  = 73;
491             $avatar->height = 73;
492         }
493
494         $avatar->original = 0; // we don't have the original
495         $avatar->mediatype = $mediatype;
496         $avatar->filename = $filename;
497         $avatar->url = Avatar::url($filename);
498
499         common_debug($this->name() . " - New filename: $avatar->url");
500
501         $avatar->created = common_sql_now();
502
503         $id = $avatar->insert();
504
505         if (empty($id)) {
506             common_log_db_error($avatar, 'INSERT', __FILE__);
507             return null;
508         }
509
510         common_debug($this->name() .
511                      " - Saved new $size avatar for $profile_id.");
512
513         return $id;
514     }
515
516     function fetchAvatar($url, $filename)
517     {
518         $avatar_dir = INSTALLDIR . '/avatar/';
519
520         $avatarfile = $avatar_dir . $filename;
521
522         $out = fopen($avatarfile, 'wb');
523         if (!$out) {
524             common_log(LOG_WARNING, $this->name() .
525                        " - Couldn't open file $filename");
526             return false;
527         }
528
529         common_debug($this->name() . " - Fetching Twitter avatar: $url");
530
531         $ch = curl_init();
532         curl_setopt($ch, CURLOPT_URL, $url);
533         curl_setopt($ch, CURLOPT_FILE, $out);
534         curl_setopt($ch, CURLOPT_BINARYTRANSFER, true);
535         curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
536         curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 0);
537         $result = curl_exec($ch);
538         curl_close($ch);
539
540         fclose($out);
541
542         return $result;
543     }
544 }
545
546 $id    = null;
547 $debug = null;
548
549 if (have_option('i')) {
550     $id = get_option_value('i');
551 } else if (have_option('--id')) {
552     $id = get_option_value('--id');
553 } else if (count($args) > 0) {
554     $id = $args[0];
555 } else {
556     $id = null;
557 }
558
559 if (have_option('d') || have_option('debug')) {
560     $debug = true;
561 }
562
563 $fetcher = new TwitterStatusFetcher($id, 60, 2, $debug);
564 $fetcher->runOnce();
565