apiqvitterstatusesupdate.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408
  1. <?php
  2. /**
  3. * StatusNet, the distributed open-source microblogging tool
  4. *
  5. * Post a notice (update your status) through the API
  6. *
  7. * PHP version 5
  8. *
  9. * LICENCE: This program is free software: you can redistribute it and/or modify
  10. * it under the terms of the GNU Affero General Public License as published by
  11. * the Free Software Foundation, either version 3 of the License, or
  12. * (at your option) any later version.
  13. *
  14. * This program is distributed in the hope that it will be useful,
  15. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  17. * GNU Affero General Public License for more details.
  18. *
  19. * You should have received a copy of the GNU Affero General Public License
  20. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  21. *
  22. * @category API
  23. * @package StatusNet
  24. * @author Craig Andrews <candrews@integralblue.com>
  25. * @author Evan Prodromou <evan@status.net>
  26. * @author Jeffery To <jeffery.to@gmail.com>
  27. * @author Tom Blankenship <mac65@mac65.com>
  28. * @author Mike Cochrane <mikec@mikenz.geek.nz>
  29. * @author Robin Millette <robin@millette.info>
  30. * @author Zach Copley <zach@status.net>
  31. * @copyright 2009-2010 StatusNet, Inc.
  32. * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org
  33. * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
  34. * @link http://status.net/
  35. */
  36. /* External API usage documentation. Please update when you change how this method works. */
  37. /*! @page statusesupdate statuses/update
  38. @section Description
  39. Updates the authenticating user's status. Requires the status parameter specified below.
  40. Request must be a POST.
  41. @par URL pattern
  42. /api/statuses/update.:format
  43. @par Formats (:format)
  44. xml, json
  45. @par HTTP Method(s)
  46. POST
  47. @par Requires Authentication
  48. Yes
  49. @param status (Required) The URL-encoded text of the status update.
  50. @param source (Optional) The source application name, if using HTTP authentication or an anonymous OAuth consumer.
  51. @param in_reply_to_status_id (Optional) The ID of an existing status that the update is in reply to.
  52. @param lat (Optional) The latitude the status refers to.
  53. @param long (Optional) The longitude the status refers to.
  54. @param media (Optional) a media upload, such as an image or movie file.
  55. @sa @ref authentication
  56. @sa @ref apiroot
  57. @subsection usagenotes Usage notes
  58. @li The URL pattern is relative to the @ref apiroot.
  59. @li If the @e source parameter is not supplied the source of the status will default to 'api'. When authenticated via a registered OAuth application, the application's registered name and URL will always override the source parameter.
  60. @li The XML response uses <a href="http://georss.org/Main_Page">GeoRSS</a>
  61. to encode the latitude and longitude (see example response below <georss:point>).
  62. @li Data uploaded via the @e media parameter should be multipart/form-data encoded.
  63. @subsection exampleusage Example usage
  64. @verbatim
  65. curl -u username:password http://example.com/api/statuses/update.xml -d status='Howdy!' -d lat='30.468' -d long='-94.743'
  66. @endverbatim
  67. @subsection exampleresponse Example response
  68. @verbatim
  69. <?xml version="1.0" encoding="UTF-8"?>
  70. <status>
  71. <text>Howdy!</text>
  72. <truncated>false</truncated>
  73. <created_at>Tue Mar 30 23:28:05 +0000 2010</created_at>
  74. <in_reply_to_status_id/>
  75. <source>api</source>
  76. <id>26668724</id>
  77. <in_reply_to_user_id/>
  78. <in_reply_to_screen_name/>
  79. <geo xmlns:georss="http://www.georss.org/georss">
  80. <georss:point>30.468 -94.743</georss:point>
  81. </geo>
  82. <favorited>false</favorited>
  83. <user>
  84. <id>25803</id>
  85. <name>Jed Sanders</name>
  86. <screen_name>jedsanders</screen_name>
  87. <location>Hoop and Holler, Texas</location>
  88. <description>I like to think of myself as America's Favorite.</description>
  89. <profile_image_url>http://avatar.example.com/25803-48-20080924200604.png</profile_image_url>
  90. <url>http://jedsanders.net</url>
  91. <protected>false</protected>
  92. <followers_count>5</followers_count>
  93. <profile_background_color/>
  94. <profile_text_color/>
  95. <profile_link_color/>
  96. <profile_sidebar_fill_color/>
  97. <profile_sidebar_border_color/>
  98. <friends_count>2</friends_count>
  99. <created_at>Wed Sep 24 20:04:00 +0000 2008</created_at>
  100. <favourites_count>0</favourites_count>
  101. <utc_offset>0</utc_offset>
  102. <time_zone>UTC</time_zone>
  103. <profile_background_image_url/>
  104. <profile_background_tile>false</profile_background_tile>
  105. <statuses_count>70</statuses_count>
  106. <following>true</following>
  107. <notifications>true</notifications>
  108. </user>
  109. </status>
  110. @endverbatim
  111. */
  112. if (!defined('STATUSNET')) {
  113. exit(1);
  114. }
  115. /**
  116. * Updates the authenticating user's status (posts a notice).
  117. *
  118. * @category API
  119. * @package StatusNet
  120. * @author Craig Andrews <candrews@integralblue.com>
  121. * @author Evan Prodromou <evan@status.net>
  122. * @author Jeffery To <jeffery.to@gmail.com>
  123. * @author Tom Blankenship <mac65@mac65.com>
  124. * @author Mike Cochrane <mikec@mikenz.geek.nz>
  125. * @author Robin Millette <robin@millette.info>
  126. * @author Zach Copley <zach@status.net>
  127. * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
  128. * @link http://status.net/
  129. */
  130. class ApiQvitterStatusesUpdateAction extends ApiAuthAction
  131. {
  132. protected $needPost = true;
  133. var $status = null;
  134. var $in_reply_to_status_id = null;
  135. var $lat = null;
  136. var $lon = null;
  137. /**
  138. * Take arguments for running
  139. *
  140. * @param array $args $_REQUEST args
  141. *
  142. * @return boolean success flag
  143. */
  144. protected function prepare(array $args=array())
  145. {
  146. parent::prepare($args);
  147. $this->status = $this->trimmed('status');
  148. $this->post_to_groups = $this->trimmed('post_to_groups');
  149. $this->lat = $this->trimmed('lat');
  150. $this->lon = $this->trimmed('long');
  151. $this->in_reply_to_status_id
  152. = intval($this->trimmed('in_reply_to_status_id'));
  153. return true;
  154. }
  155. /**
  156. * Handle the request
  157. *
  158. * Make a new notice for the update, save it, and show it
  159. *
  160. * @return void
  161. */
  162. protected function handle()
  163. {
  164. parent::handle();
  165. // Workaround for PHP returning empty $_POST and $_FILES when POST
  166. // length > post_max_size in php.ini
  167. if (empty($_FILES)
  168. && empty($_POST)
  169. && ($_SERVER['CONTENT_LENGTH'] > 0)
  170. ) {
  171. // TRANS: Client error displayed when the number of bytes in a POST request exceeds a limit.
  172. // TRANS: %s is the number of bytes of the CONTENT_LENGTH.
  173. $msg = _m('The server was unable to handle that much POST data (%s byte) due to its current configuration.',
  174. 'The server was unable to handle that much POST data (%s bytes) due to its current configuration.',
  175. intval($_SERVER['CONTENT_LENGTH']));
  176. $this->clientError(sprintf($msg, $_SERVER['CONTENT_LENGTH']), 400);
  177. }
  178. if (empty($this->status)) {
  179. // TRANS: Client error displayed when the parameter "status" is missing.
  180. $this->clientError(_('Client must provide a \'status\' parameter with a value.'), 400);
  181. }
  182. if (is_null($this->scoped)) {
  183. // TRANS: Client error displayed when updating a status for a non-existing user.
  184. $this->clientError(_('No such user.'), 404);
  185. }
  186. /* Do not call shortenlinks until the whole notice has been build */
  187. // Check for commands
  188. $inter = new CommandInterpreter();
  189. $cmd = $inter->handle_command($this->auth_user, $this->status);
  190. if ($cmd) {
  191. if ($this->supported($cmd)) {
  192. $cmd->execute(new Channel());
  193. }
  194. // Cmd not supported? Twitter just returns your latest status.
  195. // And, it returns your last status whether the cmd was successful
  196. // or not!
  197. $this->notice = $this->auth_user->getCurrentNotice();
  198. } else {
  199. $reply_to = null;
  200. if (!empty($this->in_reply_to_status_id)) {
  201. // Check whether notice actually exists
  202. $reply = Notice::getKV($this->in_reply_to_status_id);
  203. if ($reply) {
  204. $reply_to = $this->in_reply_to_status_id;
  205. } else {
  206. // TRANS: Client error displayed when replying to a non-existing notice.
  207. $this->clientError(_('Parent notice not found.'), 404);
  208. }
  209. }
  210. $upload = null;
  211. try {
  212. $upload = MediaFile::fromUpload('media', $this->scoped);
  213. } catch (NoUploadedMediaException $e) {
  214. // There was no uploaded media for us today.
  215. }
  216. if (isset($upload)) {
  217. $this->status .= ' ' . $upload->shortUrl();
  218. /* Do not call shortenlinks until the whole notice has been build */
  219. }
  220. // in Qvitter we shorten _before_ posting, so disble shortening here
  221. $status_shortened = $this->status;
  222. if (Notice::contentTooLong($status_shortened)) {
  223. if ($upload instanceof MediaFile) {
  224. $upload->delete();
  225. }
  226. // TRANS: Client error displayed exceeding the maximum notice length.
  227. // TRANS: %d is the maximum lenth for a notice.
  228. $msg = _m('Maximum notice size is %d character, including attachment URL.',
  229. 'Maximum notice size is %d characters, including attachment URL.',
  230. Notice::maxContent());
  231. /* Use HTTP 413 error code (Request Entity Too Large)
  232. * instead of basic 400 for better understanding
  233. */
  234. $this->clientError(sprintf($msg, Notice::maxContent()), 413);
  235. }
  236. $content = html_entity_decode($status_shortened, ENT_NOQUOTES, 'UTF-8');
  237. $options = array('reply_to' => $reply_to);
  238. // -------------------------------------------------------------
  239. // -------- Qvitter's post-to-the-right-group stuff! -----------
  240. // -------------------------------------------------------------
  241. // guess the groups by the content first, if we don't have group id:s as meta data
  242. $profile = Profile::getKV('id', $this->scoped->id);
  243. $guessed_groups = User_group::groupsFromText($content, $profile);
  244. // if the user has specified any group id:s, correct the guesswork
  245. if(strlen($this->post_to_groups)>0) {
  246. // get the groups that the user wants to post to
  247. $group_ids = explode(':',$this->post_to_groups);
  248. $correct_groups = Array();
  249. foreach($group_ids as $group_id) {
  250. $correct_groups[] = User_group::getKV('id',$group_id);
  251. }
  252. // correct the guesses
  253. $corrected_group_ids = Array();
  254. foreach($guessed_groups as $guessed_group) {
  255. $id_to_keep = $guessed_group->id;
  256. foreach($correct_groups as $k=>$correct_group) {
  257. if($correct_group->nickname == $guessed_group->nickname) {
  258. $id_to_keep = $correct_group->id;
  259. unset($correct_groups[$k]);
  260. break;
  261. }
  262. }
  263. $corrected_group_ids[$id_to_keep] = true;
  264. }
  265. // but we still want to post to all of the groups that the user specified by id
  266. // even if we couldn't use it to correct a bad guess
  267. foreach($correct_groups as $correct_group) {
  268. $corrected_group_ids[$correct_group->id] = true;
  269. }
  270. $options['groups'] = array_keys($corrected_group_ids);
  271. }
  272. // if the user didn't send any group id:s, go with the guesses
  273. else {
  274. $guessed_ids = array();
  275. foreach ($guessed_groups as $guessed_group) {
  276. $guessed_ids[$guessed_group->id] = true;
  277. }
  278. $options['groups'] = array_keys($guessed_ids);
  279. }
  280. // -------------------------------------------------------------
  281. // ------ End of Qvitter's post-to-the-right-group stuff! ------
  282. // -------------------------------------------------------------
  283. if ($this->scoped->shareLocation()) {
  284. $locOptions = Notice::locationOptions($this->lat,
  285. $this->lon,
  286. null,
  287. null,
  288. $this->scoped);
  289. $options = array_merge($options, $locOptions);
  290. }
  291. try {
  292. $this->notice = Notice::saveNew(
  293. $this->scoped->id,
  294. $content,
  295. $this->source,
  296. $options
  297. );
  298. } catch (Exception $e) {
  299. $this->clientError($e->getMessage(), $e->getCode());
  300. }
  301. if (isset($upload)) {
  302. $upload->attachToNotice($this->notice);
  303. }
  304. }
  305. $this->showNotice();
  306. }
  307. /**
  308. * Show the resulting notice
  309. *
  310. * @return void
  311. */
  312. function showNotice()
  313. {
  314. if (!empty($this->notice)) {
  315. if ($this->format == 'xml') {
  316. $this->showSingleXmlStatus($this->notice);
  317. } elseif ($this->format == 'json') {
  318. $this->show_single_json_status($this->notice);
  319. }
  320. }
  321. }
  322. /**
  323. * Is this command supported when doing an update from the API?
  324. *
  325. * @param string $cmd the command to check for
  326. *
  327. * @return boolean true or false
  328. */
  329. function supported($cmd)
  330. {
  331. static $cmdlist = array('SubCommand', 'UnsubCommand',
  332. 'OnCommand', 'OffCommand', 'JoinCommand', 'LeaveCommand');
  333. $supported = null;
  334. if (Event::handle('CommandSupportedAPI', array($cmd, &$supported))) {
  335. $supported = $supported || in_array(get_class($cmd), $cmdlist);
  336. }
  337. return $supported;
  338. }
  339. }