]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - actions/twitapistatuses.php
Twitter-compatible API: support for new in_reply_to_status_id in statuses/update
[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 (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
534
535         /*
536                 Destroys the status specified by the required ID parameter. The authenticating user must be
537         the author of the specified status.
538
539                  URL: http://server/api/statuses/destroy/id.format
540
541                  Formats: xml, json
542
543                  Parameters:
544
545                  * id. Required. The ID of the status to destroy. Ex:
546                 http://server/api/statuses/destroy/12345.json or
547                 http://server/api/statuses/destroy/23456.xml
548
549         */
550         function destroy($args, $apidata) {
551                 parent::handle($args);
552                 common_server_error("API method under construction.", $code=501);
553         }
554
555         # User Methods
556
557         /*
558                 Returns up to 100 of the authenticating user's friends who have most recently updated, each with current status inline.
559         It's also possible to request another user's recent friends list via the id parameter below.
560
561                  URL: http://server/api/statuses/friends.format
562
563                  Formats: xml, json
564
565                  Parameters:
566
567                  * id. Optional. The ID or screen name of the user for whom to request a list of friends. Ex:
568                 http://server/api/statuses/friends/12345.json
569                         or
570                         http://server/api/statuses/friends/bob.xml
571                  * page. Optional. Retrieves the next 100 friends. Ex: http://server/api/statuses/friends.xml?page=2
572                  * lite. Optional. Prevents the inline inclusion of current status. Must be set to a value of true. Ex:
573                 http://server/api/statuses/friends.xml?lite=true
574                  * since. Optional. Narrows the returned results to just those friendships created after the specified
575                         HTTP-formatted date. The same behavior is available by setting an If-Modified-Since header in your HTTP
576                         request. Ex: http://server/api/statuses/friends.xml?since=Tue%2C+27+Mar+2007+22%3A55%3A48+GMT
577         */
578         function friends($args, $apidata) {
579                 parent::handle($args);
580                 return $this->subscriptions($apidata, 'subscribed', 'subscriber');
581         }
582
583         /*
584                 Returns the authenticating user's followers, each with current status inline. They are ordered by the
585                 order in which they joined Twitter (this is going to be changed).
586
587                 URL: http://server/api/statuses/followers.format
588                 Formats: xml, json
589
590                 Parameters:
591
592                     * id. Optional. The ID or screen name of the user for whom to request a list of followers. Ex:
593                 http://server/api/statuses/followers/12345.json
594                                 or
595                                 http://server/api/statuses/followers/bob.xml
596                     * page. Optional. Retrieves the next 100 followers. Ex: http://server/api/statuses/followers.xml?page=2
597                     * lite. Optional. Prevents the inline inclusion of current status. Must be set to a value of true.
598                                 Ex: http://server/api/statuses/followers.xml?lite=true
599         */
600         function followers($args, $apidata) {
601                 parent::handle($args);
602
603                 return $this->subscriptions($apidata, 'subscriber', 'subscribed');
604         }
605
606         function subscriptions($apidata, $other_attr, $user_attr) {
607
608                 $user = $this->get_subs_user($apidata);
609
610                 # XXX: id
611                 # XXX: lite
612
613                 $page = $this->trimmed('page');
614
615                 if (!$page || !is_numeric($page)) {
616                         $page = 1;
617                 }
618
619                 $profile = $user->getProfile();
620
621                 if (!$profile) {
622                         common_server_error(_('User has no profile.'));
623                         return;
624                 }
625
626                 $sub = new Subscription();
627                 $sub->$user_attr = $profile->id;
628                 $sub->orderBy('created DESC');
629                 $sub->limit(($page-1)*100, 100);
630
631                 $others = array();
632
633                 if ($sub->find()) {
634                         while ($sub->fetch()) {
635                                 $others[] = Profile::staticGet($sub->$other_attr);
636                         }
637                 } else {
638                         // user has no followers
639                 }
640
641                 $type = $apidata['content-type'];
642
643                 $this->init_document($type);
644                 $this->show_profiles($others, $type);
645                 $this->end_document($type);
646                 exit();
647         }
648
649         function get_subs_user($apidata) {
650
651                 // function was called with an argument /statuses/user_timeline/api_arg.format
652                 if (isset($apidata['api_arg'])) {
653
654                         if (is_numeric($apidata['api_arg'])) {
655                                 $user = User::staticGet($apidata['api_arg']);
656                         } else {
657                                 $nickname = common_canonical_nickname($apidata['api_arg']);
658                                 $user = User::staticGet('nickname', $nickname);
659                         }
660                 } else {
661
662                         // if no user was specified, then we'll use the authenticated user
663                         $user = $apidata['user'];
664                 }
665
666                 if (!$user) {
667                         // Set the user to be the auth user if asked-for can't be found
668                         // honestly! This is what Twitter does, I swear --Zach
669                         $user = $apidata['user'];
670                 }
671
672                 return $user;
673         }
674
675         function show_profiles($profiles, $type) {
676                 switch ($type) {
677                  case 'xml':
678                         common_element_start('users', array('type' => 'array'));
679                         foreach ($profiles as $profile) {
680                                 $this->show_profile($profile);
681                         }
682                         common_element_end('users');
683                         break;
684                  case 'json':
685                         $arrays = array();
686                         foreach ($profiles as $profile) {
687                                 $arrays[] = $this->twitter_user_array($profile, true);
688                         }
689                         print json_encode($arrays);
690                         break;
691                  default:
692                         $this->client_error(_('unsupported file type'));
693                         exit();
694                 }
695         }
696
697         /*
698         Returns a list of the users currently featured on the site with their current statuses inline.
699         URL: http://server/api/statuses/featured.format
700
701         Formats: xml, json
702         */
703         function featured($args, $apidata) {
704                 parent::handle($args);
705                 common_server_error("API method under construction.", $code=501);
706         }
707
708         function get_user($id, $apidata) {
709                 if (!$id) {
710                         return $apidata['user'];
711                 } else if (is_numeric($id)) {
712                         return User::staticGet($id);
713                 } else {
714                         return User::staticGet('nickname', $id);
715                 }
716         }
717 }
718
719