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