]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - actions/twitapistatuses.php
base class is_readonly() now returns false by default
[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!"), $sitemap);
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 %s and friends on %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 %s on %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                 parent::handle($args);
375                 
376                 $user = $apidata['user'];
377                                 
378                 $this->is_readonly();
379                 
380                                 
381                 $notice = DB_DataObject::factory('notice');             
382                 
383                 $notice->profile_id = $user->id; # user id *is* profile id
384                 $notice->created = DB_DataObject_Cast::dateTime();      
385                 $notice->content = $this->trimmed('status');
386
387                 if (!$notice->content) {
388                         
389                         // XXX: Note: In this case, Twitter simply returns '200 OK'
390                         // No error is given, but the status is not posted to the 
391                         // user's timeline.  Seems bad.  Shouldn't we throw an 
392                         // errror? -- Zach
393                         exit();
394                         
395                 } else if (strlen($notice->content) > 140) {
396
397                         // XXX: Twitter truncates anything over 140, flags the status 
398                     // as "truncated."  Sending this error may screw up some clients
399                     // that assume Twitter will truncate for them.  Should we just
400                     // truncate too? -- Zach
401                         header('HTTP/1.1 406 Not Acceptable');                  
402                         print "That's too long. Max notice size is 140 chars.\n";
403                         exit();
404                 }
405
406                 $notice->rendered = common_render_content($notice->content, $notice);
407
408                 $id = $notice->insert();
409
410                 if (!$id) {
411                         common_server_error('Could not update status!', 500);
412                         exit();
413                 }
414
415                 $orig = clone($notice);
416                 $notice->uri = common_notice_uri($notice);
417
418                 if (!$notice->update($orig)) {
419                         common_server_error('Could not save status!', 500);
420                         exit();
421                 }
422
423         common_save_replies($notice);
424                 common_broadcast_notice($notice);
425
426                 // FIXME: Bad Hack 
427                 // I should be able to just sent this notice off for display,
428                 // but $notice->created does not contain a string at this
429                 // point and I don't know how to convert it to one here. So
430                 // I'm forced to have DBObject pull the notice back out of the
431                 // DB before printing. --Zach
432                 $apidata['api_arg'] = $id;
433                 $this->show($args, $apidata);
434
435                 exit();
436         }
437         
438         /*
439                 Returns the 20 most recent @replies (status updates prefixed with @username) for the authenticating user.
440                 URL: http://server/api/statuses/replies.format
441                 
442                 Formats: xml, json, rss, atom
443
444                 Parameters:
445
446                 * page. Optional. Retrieves the 20 next most recent replies. Ex: http://server/api/statuses/replies.xml?page=3 
447                 * since. Optional. Narrows the returned results to just those replies created after the specified HTTP-formatted date. The
448         same behavior is available by setting an If-Modified-Since header in your HTTP request. Ex:
449         http://server/api/statuses/replies.xml?since=Tue%2C+27+Mar+2007+22%3A55%3A48+GMT
450                 * since_id. Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified
451                 ID. Ex: http://server/api/statuses/replies.xml?since_id=12345
452         */
453         function replies($args, $apidata) {
454
455                 parent::handle($args);
456
457                 $since = $this->arg('since');
458
459                 $count = $this->arg('count');
460                 $page = $this->arg('page');
461
462                 $user = $apidata['user'];
463                 $profile = $user->getProfile();
464
465                 $sitename = common_config('site', 'name');
466                 $siteserver = common_config('site', 'server'); 
467
468                 $title = sprintf(_("%s / Updates replying to %s"), $sitename, $user->nickname);
469                 $id = "tag:$siteserver:replies:".$user->id;
470                 $link = common_local_url('replies', array('nickname' => $user->nickname));
471                 $subtitle = "gar";
472                 $subtitle = sprintf(_("%s updates that reply to updates from %s / %s."), $sitename, $user->nickname, $profile->getBestName());
473
474                 if (!$page) {
475                         $page = 1;
476                 }
477
478                 if (!$count) {
479                         $count = 20;
480                 }
481
482                 $reply = new Reply();
483
484                 $reply->profile_id = $user->id;
485
486                 $reply->orderBy('modified DESC');
487
488                 $page = ($this->arg('page')) ? ($this->arg('page')+0) : 1;
489
490                 $reply->limit((($page-1)*20), $count);
491
492                 $cnt = $reply->find();
493
494                 $notices = array();
495         
496                 if ($cnt) {
497                         while ($reply->fetch()) {
498                                 $notice = new Notice();
499                                 $notice->id = $reply->notice_id;
500                                 $result = $notice->find(true);
501                                 if (!$result) {
502                                         continue;
503                                 }
504                                 $notices[] = clone($notice);
505                         }
506                 }
507
508                 switch($apidata['content-type']) {
509                  case 'xml': 
510                         $this->show_xml_timeline($notices);
511                         break;
512                  case 'rss':
513                         $this->show_rss_timeline($notices, $title, $id, $link, $subtitle);
514                         break;
515                  case 'atom': 
516                         $this->show_atom_timeline($notices, $title, $id, $link, $subtitle);
517                         break;
518                  case 'json':
519                         $this->show_json_timeline($notices);
520                         break;
521                  default:
522                         common_user_error("API method not found!", $code = 404);
523                 }
524
525
526                 exit();
527
528
529         }
530
531         
532         
533         /*
534                 Destroys the status specified by the required ID parameter. The authenticating user must be
535         the author of the specified status.
536                 
537                  URL: http://server/api/statuses/destroy/id.format
538                 
539                  Formats: xml, json
540                 
541                  Parameters:
542                 
543                  * id. Required. The ID of the status to destroy. Ex:
544                 http://server/api/statuses/destroy/12345.json or
545                 http://server/api/statuses/destroy/23456.xml
546         
547         */
548         function destroy($args, $apidata) {
549                 parent::handle($args);
550                 common_server_error("API method under construction.", $code=501);
551         }
552         
553         # User Methods
554         
555         /*
556                 Returns up to 100 of the authenticating user's friends who have most recently updated, each with current status inline.
557         It's also possible to request another user's recent friends list via the id parameter below.
558                 
559                  URL: http://server/api/statuses/friends.format
560                 
561                  Formats: xml, json
562                 
563                  Parameters:
564                 
565                  * id. Optional. The ID or screen name of the user for whom to request a list of friends. Ex:
566                 http://server/api/statuses/friends/12345.json 
567                         or 
568                         http://server/api/statuses/friends/bob.xml
569                  * page. Optional. Retrieves the next 100 friends. Ex: http://server/api/statuses/friends.xml?page=2
570                  * lite. Optional. Prevents the inline inclusion of current status. Must be set to a value of true. Ex:
571                 http://server/api/statuses/friends.xml?lite=true
572                  * since. Optional. Narrows the returned results to just those friendships created after the specified
573                         HTTP-formatted date. The same behavior is available by setting an If-Modified-Since header in your HTTP
574                         request. Ex: http://server/api/statuses/friends.xml?since=Tue%2C+27+Mar+2007+22%3A55%3A48+GMT
575         */
576         function friends($args, $apidata) {
577                 parent::handle($args);
578                 return $this->subscriptions($apidata, 'subscribed', 'subscriber');
579         }
580         
581         /*
582                 Returns the authenticating user's followers, each with current status inline. They are ordered by the
583                 order in which they joined Twitter (this is going to be changed).
584                 
585                 URL: http://server/api/statuses/followers.format
586                 Formats: xml, json
587
588                 Parameters: 
589
590                     * id. Optional. The ID or screen name of the user for whom to request a list of followers. Ex:
591                 http://server/api/statuses/followers/12345.json 
592                                 or 
593                                 http://server/api/statuses/followers/bob.xml
594                     * page. Optional. Retrieves the next 100 followers. Ex: http://server/api/statuses/followers.xml?page=2   
595                     * lite. Optional. Prevents the inline inclusion of current status. Must be set to a value of true.
596                                 Ex: http://server/api/statuses/followers.xml?lite=true
597         */
598         function followers($args, $apidata) {
599                 parent::handle($args);
600
601                 return $this->subscriptions($apidata, 'subscriber', 'subscribed');
602         }
603
604         function subscriptions($apidata, $other_attr, $user_attr) {
605                 
606                 $user = $this->get_subs_user($apidata);
607                 
608                 # XXX: id
609                 # XXX: lite
610                 
611                 $page = $this->trimmed('page');
612                 
613                 if (!$page || !is_numeric($page)) {
614                         $page = 1;
615                 }
616                 
617                 $profile = $user->getProfile();
618                 
619                 if (!$profile) {
620                         common_server_error(_('User has no profile.'));
621                         return;
622                 }
623                                 
624                 $sub = new Subscription();
625                 $sub->$user_attr = $profile->id;
626                 $sub->orderBy('created DESC');
627                 $sub->limit(($page-1)*100, 100);
628                 
629                 $others = array();
630
631                 if ($sub->find()) {
632                         while ($sub->fetch()) {
633                                 $others[] = Profile::staticGet($sub->$other_attr);
634                         }
635                 } else {
636                         // user has no followers
637                 }
638                 
639                 $type = $apidata['content-type'];
640                 
641                 $this->init_document($type);
642                 $this->show_profiles($others, $type);
643                 $this->end_document($type);
644                 exit();
645         }
646
647         function get_subs_user($apidata) {
648                 
649                 // function was called with an argument /statuses/user_timeline/api_arg.format
650                 if (isset($apidata['api_arg'])) {
651                 
652                         if (is_numeric($apidata['api_arg'])) {
653                                 $user = User::staticGet($apidata['api_arg']);
654                         } else {
655                                 $nickname = common_canonical_nickname($apidata['api_arg']);
656                                 $user = User::staticGet('nickname', $nickname);
657                         } 
658                 } else {
659                         
660                         // if no user was specified, then we'll use the authenticated user
661                         $user = $apidata['user'];
662                 }
663
664                 if (!$user) {
665                         // Set the user to be the auth user if asked-for can't be found
666                         // honestly! This is what Twitter does, I swear --Zach
667                         $user = $apidata['user'];
668                 }
669                 
670                 return $user;
671         }
672         
673         function show_profiles($profiles, $type) {
674                 switch ($type) {
675                  case 'xml':
676                         common_element_start('users', array('type' => 'array'));
677                         foreach ($profiles as $profile) {
678                                 $this->show_profile($profile);
679                         }
680                         common_element_end('users');
681                         break;
682                  case 'json':
683                         $arrays = array();
684                         foreach ($profiles as $profile) {
685                                 $arrays[] = $this->twitter_user_array($profile, true);
686                         }
687                         print json_encode($arrays);
688                         break;
689                  default:
690                         $this->client_error(_('unsupported file type'));
691                         exit();
692                 }
693         }
694         
695         /*
696         Returns a list of the users currently featured on the site with their current statuses inline. 
697         URL: http://server/api/statuses/featured.format 
698
699         Formats: xml, json
700         */
701         function featured($args, $apidata) {
702                 parent::handle($args);
703                 common_server_error("API method under construction.", $code=501);
704         }
705
706         function get_user($id, $apidata) {
707                 if (!$id) {
708                         return $apidata['user'];
709                 } else if (is_numeric($id)) {
710                         return User::staticGet($id);
711                 } else {
712                         return User::staticGet('nickname', $id);
713                 }
714         }
715 }
716
717