]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - actions/twitapistatuses.php
Twitter-compatible API - moved show() to the right file
[quix0rs-gnu-social.git] / actions / twitapistatuses.php
1 <?php
2 /*
3  * Laconica - a distributed open-source microblogging tool
4  * Copyright (C) 2008, Controlez-Vous, 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')) { exit(1); }
21
22 require_once(INSTALLDIR.'/lib/twitterapi.php');
23
24 /* XXX: Please don't freak out about all the ugly comments in this file.
25  * They are mostly in here for reference while I work on the
26  * API. I'll fix things up later to make them look better later. -- Zach
27  */
28 class TwitapistatusesAction extends TwitterapiAction {
29
30         function is_readonly() {
31
32                 static $write_methods = array(  'update',
33                                                                                 'destroy');
34
35                 $cmdtext = explode('.', $this->arg('method'));
36
37                 if (in_array($cmdtext[0], $write_methods)) {
38                         return false;
39                 }
40
41                 return true;
42         }
43
44         function public_timeline($args, $apidata) {
45                 parent::handle($args);
46
47                 $sitename = common_config('site', 'name');
48                 $siteserver = common_config('site', 'server');
49                 $title = sprintf(_("%s public timeline"), $sitename);
50                 $id = "tag:$siteserver:Statuses";
51                 $link = common_root_url();
52                 $subtitle = sprintf(_("%s updates from everyone!"), $sitename);
53
54                 // Number of public statuses to return by default -- Twitter sends 20
55                 $MAX_PUBSTATUSES = 20;
56
57                 $notice = new Notice();
58
59                 // FIXME: To really live up to the spec we need to build a list
60                 // of notices by users who have custom avatars, so fix this SQL -- Zach
61
62                 # XXX: sub-optimal performance
63
64                 $notice->is_local = 1;
65                 $notice->orderBy('created DESC, notice.id DESC');
66                 $notice->limit($MAX_PUBSTATUSES);
67                 $cnt = $notice->find();
68
69                 if ($cnt > 0) {
70
71                         switch($apidata['content-type']) {
72                                 case 'xml':
73                                         $this->show_xml_timeline($notice);
74                                         break;
75                                 case 'rss':
76                                         $this->show_rss_timeline($notice, $title, $id, $link, $subtitle);
77                                         break;
78                                 case 'atom':
79                                         $this->show_atom_timeline($notice, $title, $id, $link, $subtitle);
80                                         break;
81                                 case 'json':
82                                         $this->show_json_timeline($notice);
83                                         break;
84                                 default:
85                                         common_user_error(_('API method not found!'), $code = 404);
86                                         break;
87                         }
88
89                 } else {
90                         common_server_error(_('Couldn\'t find any statuses.'), $code = 503);
91                 }
92
93                 exit();
94         }
95
96         function show_xml_timeline($notice) {
97
98                 $this->init_document('xml');
99                 common_element_start('statuses', array('type' => 'array'));
100
101                 if (is_array($notice)) {
102                         foreach ($notice as $n) {
103                                 $twitter_status = $this->twitter_status_array($n);
104                                 $this->show_twitter_xml_status($twitter_status);
105                         }
106                 } else {
107                         while ($notice->fetch()) {
108                                 $twitter_status = $this->twitter_status_array($notice);
109                                 $this->show_twitter_xml_status($twitter_status);
110                         }
111                 }
112
113                 common_element_end('statuses');
114                 $this->end_document('xml');
115         }
116
117         function show_rss_timeline($notice, $title, $id, $link, $subtitle) {
118
119                 $this->init_document('rss');
120
121                 common_element_start('channel');
122                 common_element('title', NULL, $title);
123                 common_element('link', NULL, $link);
124                 common_element('description', NULL, $subtitle);
125                 common_element('language', NULL, 'en-us');
126                 common_element('ttl', NULL, '40');
127
128
129                 if (is_array($notice)) {
130                         foreach ($notice as $n) {
131                                 $entry = $this->twitter_rss_entry_array($n);
132                                 $this->show_twitter_rss_item($entry);
133                         }
134                 } else {
135                         while ($notice->fetch()) {
136                                 $entry = $this->twitter_rss_entry_array($notice);
137                                 $this->show_twitter_rss_item($entry);
138                         }
139                 }
140
141                 common_element_end('channel');
142                 $this->end_twitter_rss();
143         }
144
145         function show_atom_timeline($notice, $title, $id, $link, $subtitle=NULL) {
146
147                 $this->init_document('atom');
148
149                 common_element('title', NULL, $title);
150                 common_element('id', NULL, $id);
151                 common_element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), NULL);
152                 common_element('subtitle', NULL, $subtitle);
153
154                 if (is_array($notice)) {
155                         foreach ($notice as $n) {
156                                 $entry = $this->twitter_rss_entry_array($n);
157                                 $this->show_twitter_atom_entry($entry);
158                         }
159                 } else {
160                         while ($notice->fetch()) {
161                                 $entry = $this->twitter_rss_entry_array($notice);
162                                 $this->show_twitter_atom_entry($entry);
163                         }
164                 }
165
166                 $this->end_document('atom');
167
168         }
169
170         function show_json_timeline($notice) {
171
172                 $this->init_document('json');
173
174                 $statuses = array();
175
176                 if (is_array($notice)) {
177                         foreach ($notice as $n) {
178                                 $twitter_status = $this->twitter_status_array($n);
179                                 array_push($statuses, $twitter_status);
180                         }
181                 } else {
182                         while ($notice->fetch()) {
183                                 $twitter_status = $this->twitter_status_array($notice);
184                                 array_push($statuses, $twitter_status);
185                         }
186                 }
187
188                 $this->show_twitter_json_statuses($statuses);
189
190                 $this->end_document('json');
191         }
192
193         /*
194         Returns the 20 most recent statuses posted by the authenticating user and that user's friends.
195         This is the equivalent of /home on the Web.
196
197         URL: http://server/api/statuses/friends_timeline.format
198
199         Parameters:
200
201             * since.  Optional.  Narrows the returned results to just those statuses created after the specified
202                         HTTP-formatted date.  The same behavior is available by setting an If-Modified-Since header in
203                         your HTTP request.
204                         Ex: http://server/api/statuses/friends_timeline.rss?since=Tue%2C+27+Mar+2007+22%3A55%3A48+GMT
205             * since_id.  Optional.  Returns only statuses with an ID greater than (that is, more recent than)
206                         the specified ID.  Ex: http://server/api/statuses/friends_timeline.xml?since_id=12345
207             * count.  Optional.  Specifies the number of statuses to retrieve. May not be greater than 200.
208                         Ex: http://server/api/statuses/friends_timeline.xml?count=5
209             * page. Optional. Ex: http://server/api/statuses/friends_timeline.rss?page=3
210
211         Formats: xml, json, rss, atom
212         */
213         function friends_timeline($args, $apidata) {
214                 parent::handle($args);
215
216                 $since = $this->arg('since');
217                 $since_id = $this->arg('since_id');
218                 $count = $this->arg('count');
219                 $page = $this->arg('page');
220
221                 if (!$page) {
222                         $page = 1;
223                 }
224
225                 if (!$count) {
226                         $count = 20;
227                 }
228
229                 $user = $this->get_user($id, $apidata);
230                 $profile = $user->getProfile();
231
232                 $sitename = common_config('site', 'name');
233                 $siteserver = common_config('site', 'server');
234
235                 $title = sprintf(_("%s and friends"), $user->nickname);
236                 $id = "tag:$siteserver:friends:".$user->id;
237                 $link = common_local_url('all', array('nickname' => $user->nickname));
238                 $subtitle = sprintf(_('Updates from %1$s and friends on %2$s!'), $user->nickname, $sitename);
239
240                 $notice = $user->noticesWithFriends(($page-1)*20, $count);
241
242                 switch($apidata['content-type']) {
243                  case 'xml':
244                         $this->show_xml_timeline($notice);
245                         break;
246                  case 'rss':
247                         $this->show_rss_timeline($notice, $title, $id, $link, $subtitle);
248                         break;
249                  case 'atom':
250                         $this->show_atom_timeline($notice, $title, $id, $link, $subtitle);
251                         break;
252                  case 'json':
253                         $this->show_json_timeline($notice);
254                         break;
255                  default:
256                         common_user_error(_('API method not found!'), $code = 404);
257                 }
258
259                 exit();
260         }
261
262         /*
263                 Returns the 20 most recent statuses posted from the authenticating user. It's also possible to
264         request another user's timeline via the id parameter below. This is the equivalent of the Web
265         /archive page for your own user, or the profile page for a third party.
266
267                 URL: http://server/api/statuses/user_timeline.format
268
269                 Formats: xml, json, rss, atom
270
271                 Parameters:
272
273                     * id. Optional. Specifies the ID or screen name of the user for whom to return the
274             friends_timeline. Ex: http://server/api/statuses/user_timeline/12345.xml or
275             http://server/api/statuses/user_timeline/bob.json.
276                         * count. Optional. Specifies the number of
277             statuses to retrieve. May not be greater than 200. Ex:
278             http://server/api/statuses/user_timeline.xml?count=5
279                         * since. Optional. Narrows the returned
280             results to just those statuses created after the specified HTTP-formatted date. The same
281             behavior is available by setting an If-Modified-Since header in your HTTP request. Ex:
282             http://server/api/statuses/user_timeline.rss?since=Tue%2C+27+Mar+2007+22%3A55%3A48+GMT
283                         * since_id. Optional. Returns only statuses with an ID greater than (that is, more recent than)
284             the specified ID. Ex: http://server/api/statuses/user_timeline.xml?since_id=12345 * page.
285             Optional. Ex: http://server/api/statuses/friends_timeline.rss?page=3
286         */
287         function user_timeline($args, $apidata) {
288                 parent::handle($args);
289
290                 $user = null;
291
292                 // function was called with an argument /statuses/user_timeline/api_arg.format
293                 if (isset($apidata['api_arg'])) {
294
295                         if (is_numeric($apidata['api_arg'])) {
296                                 $user = User::staticGet($apidata['api_arg']);
297                         } else {
298                                 $nickname = common_canonical_nickname($apidata['api_arg']);
299                                 $user = User::staticGet('nickname', $nickname);
300                         }
301                 } else {
302
303                         // if no user was specified, then we'll use the authenticated user
304                         $user = $apidata['user'];
305                 }
306
307                 if (!$user) {
308                         // Set the user to be the auth user if asked-for can't be found
309                         // honestly! This is what Twitter does, I swear --Zach
310                         $user = $apidata['user'];
311                 }
312
313                 $profile = $user->getProfile();
314
315                 if (!$profile) {
316                         common_server_error(_('User has no profile.'));
317                         exit();
318                 }
319
320                 $count = $this->arg('count');
321                 $since = $this->arg('since');
322                 $since_id = $this->arg('since_id');
323
324                 if (!$page) {
325                         $page = 1;
326                 }
327
328                 if (!$count) {
329                         $count = 20;
330                 }
331
332                 $sitename = common_config('site', 'name');
333                 $siteserver = common_config('site', 'server');
334
335                 $title = sprintf(_("%s timeline"), $user->nickname);
336                 $id = "tag:$siteserver:user:".$user->id;
337                 $link = common_local_url('showstream', array('nickname' => $user->nickname));
338                 $subtitle = sprintf(_('Updates from %1$s on %2$s!'), $user->nickname, $sitename);
339
340                 $notice = new Notice();
341
342                 $notice->profile_id = $user->id;
343
344                 # XXX: since
345                 # XXX: since_id
346
347                 $notice->orderBy('created DESC, notice.id DESC');
348
349                 $notice->limit((($page-1)*20), $count);
350
351                 $cnt = $notice->find();
352
353                 switch($apidata['content-type']) {
354                  case 'xml':
355                         $this->show_xml_timeline($notice);
356                         break;
357                  case 'rss':
358                         $this->show_rss_timeline($notice, $title, $id, $link, $subtitle);
359                         break;
360                  case 'atom':
361                         $this->show_atom_timeline($notice, $title, $id, $link, $subtitle);
362                         break;
363                  case 'json':
364                         $this->show_json_timeline($notice);
365                         break;
366                  default:
367                         common_user_error(_('API method not found!'), $code = 404);
368                 }
369
370                 exit();
371         }
372
373         function update($args, $apidata) {
374
375                 parent::handle($args);
376
377                 $user = $apidata['user'];
378                 $status = $this->trimmed('status');
379                 $source = $this->trimmed('source');
380                 $in_reply_to_status_id = intval($this->trimmed('in_reply_to_status_id'));
381
382                 if (!$source) {
383                         $source = 'api';
384                 }
385
386                 if (!$status) {
387
388                         // XXX: Note: In this case, Twitter simply returns '200 OK'
389                         // No error is given, but the status is not posted to the
390                         // user's timeline.  Seems bad.  Shouldn't we throw an
391                         // errror? -- Zach
392                         exit();
393
394                 } else if (mb_strlen($status) > 140) {
395
396                         // XXX: Twitter truncates anything over 140, flags the status
397                     // as "truncated."  Sending this error may screw up some clients
398                     // that assume Twitter will truncate for them.  Should we just
399                     // truncate too? -- Zach
400                         $this->client_error(_('That\'s too long. Max notice size is 140 chars.'), $code = 406, $apidata['content-type']);
401                         exit();
402                 }
403
404                 $reply_to = NULL;
405
406                 if ($in_reply_to_status_id) {
407                                                 
408                         // check whether notice actually exists
409                         $reply = Notice::staticGet($in_reply_to_status_id);
410                         
411                         if ($reply) {
412                                 $reply_to = $in_reply_to_status_id;
413                         } else {
414                                 $this->client_error(_('Not found'), $code = 404, $apidata['content-type']);
415                                 exit();
416                         }
417                 }
418                         
419                 $notice = Notice::saveNew($user->id, $status, $source, 1, $reply_to);
420
421                 if (is_string($notice)) {
422                         $this->server_error($notice);
423                         exit();
424                 }
425
426                 common_broadcast_notice($notice);
427
428                 // FIXME: Bad Hack
429                 // I should be able to just sent this notice off for display,
430                 // but $notice->created does not contain a string at this
431                 // point and I don't know how to convert it to one here. So
432                 // I'm forced to have DBObject pull the notice back out of the
433                 // DB before printing. --Zach
434                 $apidata['api_arg'] = $notice->id;
435                 $this->show($args, $apidata);
436
437                 exit();
438         }
439
440         /*
441                 Returns the 20 most recent @replies (status updates prefixed with @username) for the authenticating user.
442                 URL: http://server/api/statuses/replies.format
443
444                 Formats: xml, json, rss, atom
445
446                 Parameters:
447
448                 * page. Optional. Retrieves the 20 next most recent replies. Ex: http://server/api/statuses/replies.xml?page=3
449                 * since. Optional. Narrows the returned results to just those replies created after the specified HTTP-formatted date. The
450         same behavior is available by setting an If-Modified-Since header in your HTTP request. Ex:
451         http://server/api/statuses/replies.xml?since=Tue%2C+27+Mar+2007+22%3A55%3A48+GMT
452                 * since_id. Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified
453                 ID. Ex: http://server/api/statuses/replies.xml?since_id=12345
454         */
455         function replies($args, $apidata) {
456
457                 parent::handle($args);
458
459                 $since = $this->arg('since');
460
461                 $count = $this->arg('count');
462                 $page = $this->arg('page');
463
464                 $user = $apidata['user'];
465                 $profile = $user->getProfile();
466
467                 $sitename = common_config('site', 'name');
468                 $siteserver = common_config('site', 'server');
469
470                 $title = sprintf(_('%1$s / Updates replying to %2$s'), $sitename, $user->nickname);
471                 $id = "tag:$siteserver:replies:".$user->id;
472                 $link = common_local_url('replies', array('nickname' => $user->nickname));
473                 $subtitle = "gar";
474                 $subtitle = sprintf(_('%1$s updates that reply to updates from %2$s / %3$s.'), $sitename, $user->nickname, $profile->getBestName());
475
476                 if (!$page) {
477                         $page = 1;
478                 }
479
480                 if (!$count) {
481                         $count = 20;
482                 }
483
484                 $reply = new Reply();
485
486                 $reply->profile_id = $user->id;
487
488                 $reply->orderBy('modified DESC');
489
490                 $page = ($this->arg('page')) ? ($this->arg('page')+0) : 1;
491
492                 $reply->limit((($page-1)*20), $count);
493
494                 $cnt = $reply->find();
495
496                 $notices = array();
497
498                 if ($cnt) {
499                         while ($reply->fetch()) {
500                                 $notice = new Notice();
501                                 $notice->id = $reply->notice_id;
502                                 $result = $notice->find(true);
503                                 if (!$result) {
504                                         continue;
505                                 }
506                                 $notices[] = clone($notice);
507                         }
508                 }
509
510                 switch($apidata['content-type']) {
511                  case 'xml':
512                         $this->show_xml_timeline($notices);
513                         break;
514                  case 'rss':
515                         $this->show_rss_timeline($notices, $title, $id, $link, $subtitle);
516                         break;
517                  case 'atom':
518                         $this->show_atom_timeline($notices, $title, $id, $link, $subtitle);
519                         break;
520                  case 'json':
521                         $this->show_json_timeline($notices);
522                         break;
523                  default:
524                         common_user_error(_('API method not found!'), $code = 404);
525                 }
526
527
528                 exit();
529
530
531         }
532
533         function show($args, $apidata) {
534                 parent::handle($args);
535                 
536                 $notice_id = $apidata['api_arg'];               
537                 $notice = Notice::staticGet($notice_id);
538
539                 if ($notice) {
540                         if ($apidata['content-type'] == 'xml') { 
541                                 $this->show_single_xml_status($notice);
542                         } elseif ($apidata['content-type'] == 'json') {
543                                 $this->show_single_json_status($notice);
544                         }
545                 } else {
546                         
547                         // XXX: This is all that Twitter does.  It doesn't show an XML or JSON error msg.
548                         // Should we call client_error() to be more consistent?
549                         header('HTTP/1.1 404 Not Found');
550                 }
551                 
552                 exit();
553         }
554
555
556         /*
557                 Destroys the status specified by the required ID parameter. The authenticating user must be
558         the author of the specified status.
559
560                  URL: http://server/api/statuses/destroy/id.format
561
562                  Formats: xml, json
563
564                  Parameters:
565
566                  * id. Required. The ID of the status to destroy. Ex:
567                 http://server/api/statuses/destroy/12345.json or
568                 http://server/api/statuses/destroy/23456.xml
569
570         */
571         function destroy($args, $apidata) {
572         
573                 parent::handle($args);
574                 common_server_error("API method under construction.", $code=501);
575         }
576
577         # User Methods
578
579         /*
580                 Returns up to 100 of the authenticating user's friends who have most recently updated, each with current status inline.
581         It's also possible to request another user's recent friends list via the id parameter below.
582
583                  URL: http://server/api/statuses/friends.format
584
585                  Formats: xml, json
586
587                  Parameters:
588
589                  * id. Optional. The ID or screen name of the user for whom to request a list of friends. Ex:
590                 http://server/api/statuses/friends/12345.json
591                         or
592                         http://server/api/statuses/friends/bob.xml
593                  * page. Optional. Retrieves the next 100 friends. Ex: http://server/api/statuses/friends.xml?page=2
594                  * lite. Optional. Prevents the inline inclusion of current status. Must be set to a value of true. Ex:
595                 http://server/api/statuses/friends.xml?lite=true
596                  * since. Optional. Narrows the returned results to just those friendships created after the specified
597                         HTTP-formatted date. The same behavior is available by setting an If-Modified-Since header in your HTTP
598                         request. Ex: http://server/api/statuses/friends.xml?since=Tue%2C+27+Mar+2007+22%3A55%3A48+GMT
599         */
600         function friends($args, $apidata) {
601                 parent::handle($args);
602                 return $this->subscriptions($apidata, 'subscribed', 'subscriber');
603         }
604
605         /*
606                 Returns the authenticating user's followers, each with current status inline. They are ordered by the
607                 order in which they joined Twitter (this is going to be changed).
608
609                 URL: http://server/api/statuses/followers.format
610                 Formats: xml, json
611
612                 Parameters:
613
614                     * id. Optional. The ID or screen name of the user for whom to request a list of followers. Ex:
615                 http://server/api/statuses/followers/12345.json
616                                 or
617                                 http://server/api/statuses/followers/bob.xml
618                     * page. Optional. Retrieves the next 100 followers. Ex: http://server/api/statuses/followers.xml?page=2
619                     * lite. Optional. Prevents the inline inclusion of current status. Must be set to a value of true.
620                                 Ex: http://server/api/statuses/followers.xml?lite=true
621         */
622         function followers($args, $apidata) {
623                 parent::handle($args);
624
625                 return $this->subscriptions($apidata, 'subscriber', 'subscribed');
626         }
627
628         function subscriptions($apidata, $other_attr, $user_attr) {
629
630                 $user = $this->get_subs_user($apidata);
631
632                 # XXX: id
633                 # XXX: lite
634
635                 $page = $this->trimmed('page');
636
637                 if (!$page || !is_numeric($page)) {
638                         $page = 1;
639                 }
640
641                 $profile = $user->getProfile();
642
643                 if (!$profile) {
644                         common_server_error(_('User has no profile.'));
645                         return;
646                 }
647
648                 $sub = new Subscription();
649                 $sub->$user_attr = $profile->id;
650                 $sub->orderBy('created DESC');
651                 $sub->limit(($page-1)*100, 100);
652
653                 $others = array();
654
655                 if ($sub->find()) {
656                         while ($sub->fetch()) {
657                                 $others[] = Profile::staticGet($sub->$other_attr);
658                         }
659                 } else {
660                         // user has no followers
661                 }
662
663                 $type = $apidata['content-type'];
664
665                 $this->init_document($type);
666                 $this->show_profiles($others, $type);
667                 $this->end_document($type);
668                 exit();
669         }
670
671         function get_subs_user($apidata) {
672
673                 // function was called with an argument /statuses/user_timeline/api_arg.format
674                 if (isset($apidata['api_arg'])) {
675
676                         if (is_numeric($apidata['api_arg'])) {
677                                 $user = User::staticGet($apidata['api_arg']);
678                         } else {
679                                 $nickname = common_canonical_nickname($apidata['api_arg']);
680                                 $user = User::staticGet('nickname', $nickname);
681                         }
682                 } else {
683
684                         // if no user was specified, then we'll use the authenticated user
685                         $user = $apidata['user'];
686                 }
687
688                 if (!$user) {
689                         // Set the user to be the auth user if asked-for can't be found
690                         // honestly! This is what Twitter does, I swear --Zach
691                         $user = $apidata['user'];
692                 }
693
694                 return $user;
695         }
696
697         function show_profiles($profiles, $type) {
698                 switch ($type) {
699                  case 'xml':
700                         common_element_start('users', array('type' => 'array'));
701                         foreach ($profiles as $profile) {
702                                 $this->show_profile($profile);
703                         }
704                         common_element_end('users');
705                         break;
706                  case 'json':
707                         $arrays = array();
708                         foreach ($profiles as $profile) {
709                                 $arrays[] = $this->twitter_user_array($profile, true);
710                         }
711                         print json_encode($arrays);
712                         break;
713                  default:
714                         $this->client_error(_('unsupported file type'));
715                         exit();
716                 }
717         }
718
719         /*
720         Returns a list of the users currently featured on the site with their current statuses inline.
721         URL: http://server/api/statuses/featured.format
722
723         Formats: xml, json
724         */
725         function featured($args, $apidata) {
726                 parent::handle($args);
727                 common_server_error(_('API method under construction.'), $code=501);
728         }
729
730         function get_user($id, $apidata) {
731                 if (!$id) {
732                         return $apidata['user'];
733                 } else if (is_numeric($id)) {
734                         return User::staticGet($id);
735                 } else {
736                         return User::staticGet('nickname', $id);
737                 }
738         }
739 }
740