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