]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - actions/twitapistatuses.php
added group status api, located at /api/statuses/group_timeline/ID.rss
[quix0rs-gnu-social.git] / actions / twitapistatuses.php
1 <?php
2 /*
3  * Laconica - a distributed open-source microblogging tool
4  * Copyright (C) 2008, 2009, Control Yourself, Inc.
5  *
6  * This program is free software: you can redistribute it and/or modify
7  * it under the terms of the GNU Affero General Public License as published by
8  * the Free Software Foundation, either version 3 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU Affero General Public License for more details.
15  *
16  * You should have received a copy of the GNU Affero General Public License
17  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
18  */
19
20 if (!defined('LACONICA')) {
21     exit(1);
22 }
23
24 require_once(INSTALLDIR.'/lib/twitterapi.php');
25
26 class TwitapistatusesAction extends TwitterapiAction
27 {
28
29     function public_timeline($args, $apidata)
30     {
31         // XXX: To really live up to the spec we need to build a list
32         // of notices by users who have custom avatars, so fix this SQL -- Zach
33
34         parent::handle($args);
35
36         $sitename   = common_config('site', 'name');
37         $title      = sprintf(_("%s public timeline"), $sitename);
38         $taguribase = common_config('integration', 'taguri');
39         $id         = "tag:$taguribase:PublicTimeline";
40         $link       = common_root_url();
41         $subtitle   = sprintf(_("%s updates from everyone!"), $sitename);
42
43         $page     = (int)$this->arg('page', 1);
44         $count    = (int)$this->arg('count', 20);
45         $max_id   = (int)$this->arg('max_id', 0);
46         $since_id = (int)$this->arg('since_id', 0);
47         $since    = $this->arg('since');
48
49         $notice = Notice::publicStream(($page-1)*$count, $count, $since_id,
50             $max_id, $since);
51
52         switch($apidata['content-type']) {
53         case 'xml':
54             $this->show_xml_timeline($notice);
55             break;
56         case 'rss':
57             $this->show_rss_timeline($notice, $title, $link, $subtitle);
58             break;
59         case 'atom':
60             $selfuri = common_root_url() . 'api/statuses/public_timeline.atom';
61             $this->show_atom_timeline($notice, $title, $id, $link,
62                 $subtitle, null, $selfuri);
63             break;
64         case 'json':
65             $this->show_json_timeline($notice);
66             break;
67         default:
68             $this->clientError(_('API method not found!'), $code = 404);
69             break;
70         }
71
72     }
73
74     function friends_timeline($args, $apidata)
75     {
76         parent::handle($args);
77
78         $this->auth_user = $apidata['user'];
79         $user = $this->get_user($apidata['api_arg'], $apidata);
80
81         if (empty($user)) {
82              $this->clientError(_('No such user!'), 404,
83              $apidata['content-type']);
84             return;
85         }
86
87         $profile    = $user->getProfile();
88         $sitename   = common_config('site', 'name');
89         $title      = sprintf(_("%s and friends"), $user->nickname);
90         $taguribase = common_config('integration', 'taguri');
91         $id         = "tag:$taguribase:FriendsTimeline:" . $user->id;
92         $link       = common_local_url('all',
93             array('nickname' => $user->nickname));
94         $subtitle   = sprintf(_('Updates from %1$s and friends on %2$s!'),
95             $user->nickname, $sitename);
96
97         $page     = (int)$this->arg('page', 1);
98         $count    = (int)$this->arg('count', 20);
99         $max_id   = (int)$this->arg('max_id', 0);
100         $since_id = (int)$this->arg('since_id', 0);
101         $since    = $this->arg('since');
102
103         if (!empty($this->auth_user) && $this->auth_user->id == $user->id) {
104             $notice = $user->noticeInbox(($page-1)*$count,
105                 $count, $since_id, $max_id, $since);
106         } else {
107             $notice = $user->noticesWithFriends(($page-1)*$count,
108                 $count, $since_id, $max_id, $since);
109         }
110
111         switch($apidata['content-type']) {
112         case 'xml':
113             $this->show_xml_timeline($notice);
114             break;
115         case 'rss':
116             $this->show_rss_timeline($notice, $title, $link, $subtitle);
117             break;
118         case 'atom':
119             if (isset($apidata['api_arg'])) {
120                 $selfuri = common_root_url() .
121                     'api/statuses/friends_timeline/' .
122                         $apidata['api_arg'] . '.atom';
123             } else {
124                 $selfuri = common_root_url() .
125                     'api/statuses/friends_timeline.atom';
126             }
127             $this->show_atom_timeline($notice, $title, $id, $link,
128                 $subtitle, null, $selfuri);
129             break;
130         case 'json':
131             $this->show_json_timeline($notice);
132             break;
133         default:
134             $this->clientError(_('API method not found!'), $code = 404);
135         }
136
137     }
138
139     function group_timeline($args, $apidata)
140     {
141         parent::handle($args);
142
143         $this->auth_user = $apidata['user'];
144         $group = $this->get_group($apidata['api_arg'], $apidata);
145
146         if (empty($group)) {
147             $this->clientError('Not Found', 404, $apidata['content-type']);
148             return;
149         }
150
151         $sitename   = common_config('site', 'name');
152         $title      = sprintf(_("%s timeline"), $group->nickname);
153         $taguribase = common_config('integration', 'taguri');
154         $id         = "tag:$taguribase:GroupTimeline:".$group->id;
155         $link       = common_local_url('showstream',
156             array('nickname' => $group->nickname));
157         $subtitle   = sprintf(_('Updates from %1$s on %2$s!'),
158             $group->nickname, $sitename);
159
160         $page     = (int)$this->arg('page', 1);
161         $count    = (int)$this->arg('count', 20);
162         $max_id   = (int)$this->arg('max_id', 0);
163         $since_id = (int)$this->arg('since_id', 0);
164         $since    = $this->arg('since');
165
166         $notice = $group->getNotices(($page-1)*$count,
167             $count, $since_id, $max_id, $since);
168
169         switch($apidata['content-type']) {
170          case 'xml':
171             $this->show_xml_timeline($notice);
172             break;
173          case 'rss':
174             $this->show_rss_timeline($notice, $title, $link,
175                 $subtitle, $suplink);
176             break;
177          case 'atom':
178             if (isset($apidata['api_arg'])) {
179                 $selfuri = common_root_url() .
180                     'api/statuses/group_timeline/' .
181                         $apidata['api_arg'] . '.atom';
182             } else {
183                 $selfuri = common_root_url() .
184                  'api/statuses/group_timeline.atom';
185             }
186             $this->show_atom_timeline($notice, $title, $id, $link,
187                 $subtitle, $suplink, $selfuri);
188             break;
189          case 'json':
190             $this->show_json_timeline($notice);
191             break;
192          default:
193             $this->clientError(_('API method not found!'), $code = 404);
194         }
195     }
196
197     function user_timeline($args, $apidata)
198     {
199         parent::handle($args);
200
201         $this->auth_user = $apidata['user'];
202         $user = $this->get_user($apidata['api_arg'], $apidata);
203
204         if (empty($user)) {
205             $this->clientError('Not Found', 404, $apidata['content-type']);
206             return;
207         }
208
209         $profile = $user->getProfile();
210
211         $sitename   = common_config('site', 'name');
212         $title      = sprintf(_("%s timeline"), $user->nickname);
213         $taguribase = common_config('integration', 'taguri');
214         $id         = "tag:$taguribase:UserTimeline:".$user->id;
215         $link       = common_local_url('showstream',
216             array('nickname' => $user->nickname));
217         $subtitle   = sprintf(_('Updates from %1$s on %2$s!'),
218             $user->nickname, $sitename);
219
220         # FriendFeed's SUP protocol
221         # Also added RSS and Atom feeds
222
223         $suplink = common_local_url('sup', null, null, $user->id);
224         header('X-SUP-ID: '.$suplink);
225
226         $page     = (int)$this->arg('page', 1);
227         $count    = (int)$this->arg('count', 20);
228         $max_id   = (int)$this->arg('max_id', 0);
229         $since_id = (int)$this->arg('since_id', 0);
230         $since    = $this->arg('since');
231
232         $notice = $user->getNotices(($page-1)*$count,
233             $count, $since_id, $max_id, $since);
234
235         switch($apidata['content-type']) {
236          case 'xml':
237             $this->show_xml_timeline($notice);
238             break;
239          case 'rss':
240             $this->show_rss_timeline($notice, $title, $link,
241                 $subtitle, $suplink);
242             break;
243          case 'atom':
244             if (isset($apidata['api_arg'])) {
245                 $selfuri = common_root_url() .
246                     'api/statuses/user_timeline/' .
247                         $apidata['api_arg'] . '.atom';
248             } else {
249                 $selfuri = common_root_url() .
250                  'api/statuses/user_timeline.atom';
251             }
252             $this->show_atom_timeline($notice, $title, $id, $link,
253                 $subtitle, $suplink, $selfuri);
254             break;
255          case 'json':
256             $this->show_json_timeline($notice);
257             break;
258          default:
259             $this->clientError(_('API method not found!'), $code = 404);
260         }
261
262     }
263
264     function update($args, $apidata)
265     {
266         parent::handle($args);
267
268         if (!in_array($apidata['content-type'], array('xml', 'json'))) {
269             $this->clientError(_('API method not found!'), $code = 404);
270             return;
271         }
272
273         if ($_SERVER['REQUEST_METHOD'] != 'POST') {
274             $this->clientError(_('This method requires a POST.'),
275                 400, $apidata['content-type']);
276             return;
277         }
278
279         $user = $apidata['user'];  // Always the auth user
280
281         $status = $this->trimmed('status');
282         $source = $this->trimmed('source');
283         $in_reply_to_status_id =
284             intval($this->trimmed('in_reply_to_status_id'));
285         $reserved_sources = array('web', 'omb', 'mail', 'xmpp', 'api');
286
287         if (empty($source) || in_array($source, $reserved_sources)) {
288             $source = 'api';
289         }
290
291         if (empty($status)) {
292
293             // XXX: Note: In this case, Twitter simply returns '200 OK'
294             // No error is given, but the status is not posted to the
295             // user's timeline.     Seems bad.     Shouldn't we throw an
296             // errror? -- Zach
297             return;
298
299         } else {
300
301             $status_shortened = common_shorten_links($status);
302
303             if (mb_strlen($status_shortened) > 140) {
304
305                 // XXX: Twitter truncates anything over 140, flags the status
306                 // as "truncated." Sending this error may screw up some clients
307                 // that assume Twitter will truncate for them.    Should we just
308                 // truncate too? -- Zach
309                 $this->clientError(_('That\'s too long. Max notice size is 140 chars.'),
310                     $code = 406, $apidata['content-type']);
311                 return;
312             }
313         }
314
315         // Check for commands
316         $inter = new CommandInterpreter();
317         $cmd = $inter->handle_command($user, $status_shortened);
318
319         if ($cmd) {
320
321             if ($this->supported($cmd)) {
322                 $cmd->execute(new Channel());
323             }
324
325             // cmd not supported?  Twitter just returns your latest status.
326             // And, it returns your last status whether the cmd was successful
327             // or not!
328             $n = $user->getCurrentNotice();
329             $apidata['api_arg'] = $n->id;
330         } else {
331
332             $reply_to = null;
333
334             if ($in_reply_to_status_id) {
335
336                 // check whether notice actually exists
337                 $reply = Notice::staticGet($in_reply_to_status_id);
338
339                 if ($reply) {
340                     $reply_to = $in_reply_to_status_id;
341                 } else {
342                     $this->clientError(_('Not found'), $code = 404,
343                         $apidata['content-type']);
344                     return;
345                 }
346             }
347
348             $notice = Notice::saveNew($user->id,
349                 html_entity_decode($status, ENT_NOQUOTES, 'UTF-8'),
350                     $source, 1, $reply_to);
351
352             if (is_string($notice)) {
353                 $this->serverError($notice);
354                 return;
355             }
356
357             common_broadcast_notice($notice);
358             $apidata['api_arg'] = $notice->id;
359         }
360
361         $this->show($args, $apidata);
362     }
363
364     function mentions($args, $apidata)
365     {
366         parent::handle($args);
367
368         $user = $this->get_user($apidata['api_arg'], $apidata);
369         $this->auth_user = $apidata['user'];
370
371         if (empty($user)) {
372              $this->clientError(_('No such user!'), 404,
373                  $apidata['content-type']);
374             return;
375         }
376
377         $profile = $user->getProfile();
378
379         $sitename   = common_config('site', 'name');
380         $title      = sprintf(_('%1$s / Updates mentioning %2$s'),
381             $sitename, $user->nickname);
382         $taguribase = common_config('integration', 'taguri');
383         $id         = "tag:$taguribase:Mentions:".$user->id;
384         $link       = common_local_url('replies',
385             array('nickname' => $user->nickname));
386         $subtitle   = sprintf(_('%1$s updates that reply to updates from %2$s / %3$s.'),
387             $sitename, $user->nickname, $profile->getBestName());
388
389         $page     = (int)$this->arg('page', 1);
390         $count    = (int)$this->arg('count', 20);
391         $max_id   = (int)$this->arg('max_id', 0);
392         $since_id = (int)$this->arg('since_id', 0);
393         $since    = $this->arg('since');
394
395         $notice = $user->getReplies(($page-1)*$count,
396             $count, $since_id, $max_id, $since);
397
398         switch($apidata['content-type']) {
399         case 'xml':
400             $this->show_xml_timeline($notice);
401             break;
402         case 'rss':
403             $this->show_rss_timeline($notice, $title, $link, $subtitle);
404             break;
405         case 'atom':
406             $selfuri = common_root_url() .
407                 ltrim($_SERVER['QUERY_STRING'], 'p=');
408             $this->show_atom_timeline($notice, $title, $id, $link, $subtitle,
409                 null, $selfuri);
410             break;
411         case 'json':
412             $this->show_json_timeline($notice);
413             break;
414         default:
415             $this->clientError(_('API method not found!'), $code = 404);
416         }
417
418     }
419
420     function replies($args, $apidata)
421     {
422         call_user_func(array($this, 'mentions'), $args, $apidata);
423     }
424
425     function show($args, $apidata)
426     {
427         parent::handle($args);
428
429         if (!in_array($apidata['content-type'], array('xml', 'json'))) {
430             $this->clientError(_('API method not found!'), $code = 404);
431             return;
432         }
433
434         // 'id' is an undocumented parameter in Twitter's API. Several
435         // clients make use of it, so we support it too.
436
437         // show.json?id=12345 takes precedence over /show/12345.json
438
439         $this->auth_user = $apidata['user'];
440         $notice_id       = $this->trimmed('id');
441
442         if (empty($notice_id)) {
443             $notice_id   = $apidata['api_arg'];
444         }
445
446         $notice          = Notice::staticGet((int)$notice_id);
447
448         if ($notice) {
449             if ($apidata['content-type'] == 'xml') {
450                 $this->show_single_xml_status($notice);
451             } elseif ($apidata['content-type'] == 'json') {
452                 $this->show_single_json_status($notice);
453             }
454         } else {
455             // XXX: Twitter just sets a 404 header and doens't bother
456             // to return an err msg
457             $this->clientError(_('No status with that ID found.'),
458                 404, $apidata['content-type']);
459         }
460     }
461
462     function destroy($args, $apidata)
463     {
464         parent::handle($args);
465
466         if (!in_array($apidata['content-type'], array('xml', 'json'))) {
467             $this->clientError(_('API method not found!'), $code = 404);
468             return;
469         }
470
471         // Check for RESTfulness
472         if (!in_array($_SERVER['REQUEST_METHOD'], array('POST', 'DELETE'))) {
473             // XXX: Twitter just prints the err msg, no XML / JSON.
474             $this->clientError(_('This method requires a POST or DELETE.'),
475                 400, $apidata['content-type']);
476             return;
477         }
478
479         $user      = $apidata['user']; // Always the auth user
480         $notice_id = $apidata['api_arg'];
481         $notice    = Notice::staticGet($notice_id);
482
483         if (empty($notice)) {
484             $this->clientError(_('No status found with that ID.'),
485                 404, $apidata['content-type']);
486             return;
487         }
488
489         if ($user->id == $notice->profile_id) {
490             $replies = new Reply;
491             $replies->get('notice_id', $notice_id);
492             $replies->delete();
493             $notice->delete();
494
495             if ($apidata['content-type'] == 'xml') {
496                 $this->show_single_xml_status($notice);
497             } elseif ($apidata['content-type'] == 'json') {
498                 $this->show_single_json_status($notice);
499             }
500         } else {
501             $this->clientError(_('You may not delete another user\'s status.'),
502                 403, $apidata['content-type']);
503         }
504
505     }
506
507     function friends($args, $apidata)
508     {
509         parent::handle($args);
510         return $this->subscriptions($apidata, 'subscribed', 'subscriber');
511     }
512
513     function friendsIDs($args, $apidata)
514     {
515         parent::handle($args);
516         return $this->subscriptions($apidata, 'subscribed', 'subscriber', true);
517     }
518
519     function followers($args, $apidata)
520     {
521         parent::handle($args);
522         return $this->subscriptions($apidata, 'subscriber', 'subscribed');
523     }
524
525     function followersIDs($args, $apidata)
526     {
527         parent::handle($args);
528         return $this->subscriptions($apidata, 'subscriber', 'subscribed', true);
529     }
530
531     function subscriptions($apidata, $other_attr, $user_attr, $onlyIDs=false)
532     {
533         $this->auth_user = $apidata['user'];
534         $user = $this->get_user($apidata['api_arg'], $apidata);
535
536         if (empty($user)) {
537             $this->clientError('Not Found', 404, $apidata['content-type']);
538             return;
539         }
540
541         $profile = $user->getProfile();
542
543         $sub = new Subscription();
544         $sub->$user_attr = $profile->id;
545
546         $sub->orderBy('created DESC');
547
548         // Normally, page 100 friends at a time
549
550         if (!$onlyIDs) {
551             $page  = $this->arg('page', 1);
552             $count = $this->arg('count', 100);
553             $sub->limit(($page-1)*$count, $count);
554         } else {
555
556             // If we're just looking at IDs, return
557             // ALL of them, unless the user specifies a page,
558             // in which case, return 500 per page.
559
560             $page = $this->arg('page');
561             if (!empty($page)) {
562                 if ($page < 1) {
563                     $page = 1;
564                 }
565                 $count = 500;
566                 $sub->limit(($page-1)*$count, $count);
567             }
568         }
569
570         $others = array();
571
572         if ($sub->find()) {
573             while ($sub->fetch()) {
574                 $others[] = Profile::staticGet($sub->$other_attr);
575             }
576         } else {
577             // user has no followers
578         }
579
580         $type = $apidata['content-type'];
581
582         $this->init_document($type);
583
584         if ($onlyIDs) {
585             $this->showIDs($others, $type);
586         } else {
587             $this->show_profiles($others, $type);
588         }
589
590         $this->end_document($type);
591     }
592
593     function show_profiles($profiles, $type)
594     {
595         switch ($type) {
596         case 'xml':
597             $this->elementStart('users', array('type' => 'array'));
598             foreach ($profiles as $profile) {
599                 $this->show_profile($profile);
600             }
601             $this->elementEnd('users');
602             break;
603         case 'json':
604             $arrays = array();
605             foreach ($profiles as $profile) {
606                 $arrays[] = $this->twitter_user_array($profile, true);
607             }
608             print json_encode($arrays);
609             break;
610         default:
611             $this->clientError(_('unsupported file type'));
612         }
613     }
614
615     function showIDs($profiles, $type)
616     {
617         switch ($type) {
618         case 'xml':
619             $this->elementStart('ids');
620             foreach ($profiles as $profile) {
621                 $this->element('id', null, $profile->id);
622             }
623             $this->elementEnd('ids');
624             break;
625         case 'json':
626             $ids = array();
627             foreach ($profiles as $profile) {
628                 $ids[] = (int)$profile->id;
629             }
630             print json_encode($ids);
631             break;
632         default:
633             $this->clientError(_('unsupported file type'));
634         }
635     }
636
637     function featured($args, $apidata)
638     {
639         parent::handle($args);
640         $this->serverError(_('API method under construction.'), $code=501);
641     }
642
643     function supported($cmd)
644     {
645         $cmdlist = array('MessageCommand', 'SubCommand', 'UnsubCommand',
646             'FavCommand', 'OnCommand', 'OffCommand');
647
648         if (in_array(get_class($cmd), $cmdlist)) {
649             return true;
650         }
651
652         return false;
653     }
654
655 }