apitimelinefriendshiddenreplies.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486
  1. <?php
  2. /**
  3. * StatusNet, the distributed open-source microblogging tool
  4. *
  5. * Show the friends timeline, witch replies to non-friends hidden
  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 mac65 <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. * @author Hannes Mannerheim <h@nnesmannerhe.im>
  32. * @copyright 2009-2010 StatusNet, Inc.
  33. * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org
  34. * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
  35. * @link http://status.net/
  36. */
  37. /* External API usage documentation. Please update when you change how this method works. */
  38. /*! @page friendstimeline statuses/friends_timeline
  39. @section Description
  40. Returns the 20 most recent statuses posted by the authenticating
  41. user and that user's friends. This is the equivalent of "You and
  42. friends" page in the web interface.
  43. @par URL patterns
  44. @li /api/statuses/friends_timeline.:format
  45. @li /api/statuses/friends_timeline/:id.:format
  46. @par Formats (:format)
  47. xml, json, rss, atom
  48. @par ID (:id)
  49. username, user id
  50. @par HTTP Method(s)
  51. GET
  52. @par Requires Authentication
  53. Sometimes (see: @ref authentication)
  54. @param user_id (Optional) Specifies a user by ID
  55. @param screen_name (Optional) Specifies a user by screename (nickname)
  56. @param since_id (Optional) Returns only statuses with an ID greater
  57. than (that is, more recent than) the specified ID.
  58. @param max_id (Optional) Returns only statuses with an ID less than
  59. (that is, older than) or equal to the specified ID.
  60. @param count (Optional) Specifies the number of statuses to retrieve.
  61. @param page (Optional) Specifies the page of results to retrieve.
  62. @sa @ref authentication
  63. @sa @ref apiroot
  64. @subsection usagenotes Usage notes
  65. @li The URL pattern is relative to the @ref apiroot.
  66. @li The XML response uses <a href="http://georss.org/Main_Page">GeoRSS</a>
  67. to encode the latitude and longitude (see example response below <georss:point>).
  68. @subsection exampleusage Example usage
  69. @verbatim
  70. curl http://identi.ca/api/statuses/friends_timeline/evan.xml?count=1&page=2
  71. @endverbatim
  72. @subsection exampleresponse Example response
  73. @verbatim
  74. <?xml version="1.0"?>
  75. <statuses type="array">
  76. <status>
  77. <text>back from the !yul !drupal meet with Evolving Web folk, @anarcat, @webchick and others, and an interesting refresher on SQL indexing</text>
  78. <truncated>false</truncated>
  79. <created_at>Wed Mar 31 01:33:02 +0000 2010</created_at>
  80. <in_reply_to_status_id/>
  81. <source>&lt;a href="http://code.google.com/p/microblog-purple/"&gt;mbpidgin&lt;/a&gt;</source>
  82. <id>26674201</id>
  83. <in_reply_to_user_id/>
  84. <in_reply_to_screen_name/>
  85. <geo/>
  86. <favorited>false</favorited>
  87. <user>
  88. <id>246</id>
  89. <name>Mark</name>
  90. <screen_name>lambic</screen_name>
  91. <location>Montreal, Canada</location>
  92. <description>Geek</description>
  93. <profile_image_url>http://avatar.identi.ca/246-48-20080702141545.png</profile_image_url>
  94. <url>http://lambic.co.uk</url>
  95. <protected>false</protected>
  96. <followers_count>73</followers_count>
  97. <profile_background_color>#F0F2F5</profile_background_color>
  98. <profile_text_color/>
  99. <profile_link_color>#002E6E</profile_link_color>
  100. <profile_sidebar_fill_color>#CEE1E9</profile_sidebar_fill_color>
  101. <profile_sidebar_border_color/>
  102. <friends_count>58</friends_count>
  103. <created_at>Wed Jul 02 14:12:15 +0000 2008</created_at>
  104. <favourites_count>2</favourites_count>
  105. <utc_offset>-14400</utc_offset>
  106. <time_zone>US/Eastern</time_zone>
  107. <profile_background_image_url/>
  108. <profile_background_tile>false</profile_background_tile>
  109. <statuses_count>933</statuses_count>
  110. <following>false</following>
  111. <notifications>false</notifications>
  112. </user>
  113. </status>
  114. </statuses>
  115. @endverbatim
  116. */
  117. if (!defined('STATUSNET')) {
  118. exit(1);
  119. }
  120. /**
  121. * Returns the most recent notices (default 20) posted by the target user.
  122. * This is the equivalent of 'You and friends' page accessed via Web.
  123. *
  124. * @category API
  125. * @package StatusNet
  126. * @author Craig Andrews <candrews@integralblue.com>
  127. * @author Evan Prodromou <evan@status.net>
  128. * @author Jeffery To <jeffery.to@gmail.com>
  129. * @author mac65 <mac65@mac65.com>
  130. * @author Mike Cochrane <mikec@mikenz.geek.nz>
  131. * @author Robin Millette <robin@millette.info>
  132. * @author Zach Copley <zach@status.net>
  133. * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
  134. * @link http://status.net/
  135. */
  136. class ApiTimelineFriendsHiddenRepliesAction extends ApiBareAuthAction
  137. {
  138. var $notices = null;
  139. /**
  140. * Take arguments for running
  141. *
  142. * @param array $args $_REQUEST args
  143. *
  144. * @return boolean success flag
  145. *
  146. */
  147. protected function prepare(array $args=array())
  148. {
  149. parent::prepare($args);
  150. $this->target = $this->getTargetProfile($this->arg('id'));
  151. if (!($this->target instanceof Profile)) {
  152. // TRANS: Client error displayed when requesting dents of a user and friends for a user that does not exist.
  153. $this->clientError(_('No such user.'), 404);
  154. }
  155. $this->notices = $this->getNotices();
  156. return true;
  157. }
  158. /**
  159. * Handle the request
  160. *
  161. * Just show the notices
  162. *
  163. * @return void
  164. */
  165. protected function handle()
  166. {
  167. parent::handle();
  168. $this->showTimeline();
  169. }
  170. /**
  171. * Show the timeline of notices
  172. *
  173. * @return void
  174. */
  175. function showTimeline()
  176. {
  177. $sitename = common_config('site', 'name');
  178. // TRANS: Title of API timeline for a user and friends.
  179. // TRANS: %s is a username.
  180. $title = sprintf(_("%s and friends"), $this->target->nickname);
  181. $taguribase = TagURI::base();
  182. $id = "tag:$taguribase:FriendsTimelineHiddenReplies:" . $this->target->id;
  183. $subtitle = sprintf(
  184. // TRANS: Message is used as a subtitle. %1$s is a user nickname, %2$s is a site name.
  185. _('Updates from %1$s and friends on %2$s! (with replies to non-friends hidden)'),
  186. $this->target->nickname,
  187. $sitename
  188. );
  189. $logo = $this->target->avatarUrl(AVATAR_PROFILE_SIZE);
  190. $link = common_local_url('all',
  191. array('nickname' => $this->target->nickname));
  192. $self = $this->getSelfUri();
  193. switch($this->format) {
  194. case 'xml':
  195. $this->showXmlTimeline($this->notices);
  196. break;
  197. case 'rss':
  198. $this->showRssTimeline(
  199. $this->notices,
  200. $title,
  201. $link,
  202. $subtitle,
  203. null,
  204. $logo,
  205. $self
  206. );
  207. break;
  208. case 'atom':
  209. header('Content-Type: application/atom+xml; charset=utf-8');
  210. $atom = new AtomNoticeFeed($this->auth_user);
  211. $atom->setId($id);
  212. $atom->setTitle($title);
  213. $atom->setSubtitle($subtitle);
  214. $atom->setLogo($logo);
  215. $atom->setUpdated('now');
  216. $atom->addLink($link);
  217. $atom->setSelfLink($self);
  218. $atom->addEntryFromNotices($this->notices);
  219. $this->raw($atom->getString());
  220. break;
  221. case 'json':
  222. $this->showJsonTimeline($this->notices);
  223. break;
  224. case 'as':
  225. header('Content-Type: ' . ActivityStreamJSONDocument::CONTENT_TYPE);
  226. $doc = new ActivityStreamJSONDocument($this->auth_user, $title);
  227. $doc->addLink($link, 'alternate', 'text/html');
  228. $doc->addItemsFromNotices($this->notices);
  229. $this->raw($doc->asString());
  230. break;
  231. default:
  232. // TRANS: Client error displayed when coming across a non-supported API method.
  233. $this->clientError(_('API method not found.'), 404);
  234. }
  235. }
  236. /**
  237. * Get notices
  238. *
  239. * @return array notices
  240. */
  241. function getNotices()
  242. {
  243. $notices = array();
  244. $stream = new InboxNoticeStreamHiddenReplies($this->target, $this->scoped);
  245. $notice = $stream->getNotices(($this->page-1) * $this->count,
  246. $this->count,
  247. $this->since_id,
  248. $this->max_id);
  249. while ($notice->fetch()) {
  250. $notices[] = clone($notice);
  251. }
  252. return $notices;
  253. }
  254. /**
  255. * Is this action read only?
  256. *
  257. * @param array $args other arguments
  258. *
  259. * @return boolean true
  260. */
  261. function isReadOnly($args)
  262. {
  263. return true;
  264. }
  265. /**
  266. * When was this feed last modified?
  267. *
  268. * @return string datestamp of the latest notice in the stream
  269. */
  270. function lastModified()
  271. {
  272. if (!empty($this->notices) && (count($this->notices) > 0)) {
  273. return strtotime($this->notices[0]->created);
  274. }
  275. return null;
  276. }
  277. /**
  278. * An entity tag for this stream
  279. *
  280. * Returns an Etag based on the action name, language, user ID, and
  281. * timestamps of the first and last notice in the timeline
  282. *
  283. * @return string etag
  284. */
  285. function etag()
  286. {
  287. if (!empty($this->notices) && (count($this->notices) > 0)) {
  288. $last = count($this->notices) - 1;
  289. return '"' . implode(
  290. ':',
  291. array($this->arg('action'),
  292. common_user_cache_hash($this->auth_user),
  293. common_language(),
  294. $this->target->id,
  295. strtotime($this->notices[0]->created),
  296. strtotime($this->notices[$last]->created))
  297. )
  298. . '"';
  299. }
  300. return null;
  301. }
  302. }
  303. /**
  304. * Stream of notices for a profile's "all" feed, with hidden replies to non-friends
  305. *
  306. * @category General
  307. * @package StatusNet
  308. * @author Evan Prodromou <evan@status.net>
  309. * @author Mikael Nordfeldth <mmn@hethane.se>
  310. * @copyright 2011 StatusNet, Inc.
  311. * @copyright 2014 Free Software Foundation, Inc.
  312. * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
  313. * @link http://status.net/
  314. */
  315. class InboxNoticeStreamHiddenReplies extends ScopingNoticeStream
  316. {
  317. /**
  318. * Constructor
  319. *
  320. * @param Profile $target Profile to get a stream for
  321. * @param Profile $scoped Currently scoped profile (if null, it is fetched)
  322. */
  323. function __construct(Profile $target, Profile $scoped=null)
  324. {
  325. if ($scoped === null) {
  326. $scoped = Profile::current();
  327. }
  328. // FIXME: we don't use CachingNoticeStream - but maybe we should?
  329. parent::__construct(new CachingNoticeStream(new RawInboxNoticeStreamHiddenReplies($target), 'profileallhiddenreplies'), $scoped);
  330. }
  331. }
  332. /**
  333. * Raw stream of notices for the target's inbox, with hidden replies to non-friends
  334. *
  335. * @category General
  336. * @package StatusNet
  337. * @author Evan Prodromou <evan@status.net>
  338. * @copyright 2011 StatusNet, Inc.
  339. * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
  340. * @link http://status.net/
  341. */
  342. class RawInboxNoticeStreamHiddenReplies extends NoticeStream
  343. {
  344. protected $target = null;
  345. protected $inbox = null;
  346. protected $selectVerbs = array();
  347. /**
  348. * Constructor
  349. *
  350. * @param Profile $target Profile to get a stream for
  351. */
  352. function __construct(Profile $target)
  353. {
  354. parent::__construct();
  355. $this->target = $target;
  356. }
  357. /**
  358. * Get IDs in a range
  359. *
  360. * @param int $offset Offset from start
  361. * @param int $limit Limit of number to get
  362. * @param int $since_id Since this notice
  363. * @param int $max_id Before this notice
  364. *
  365. * @return Array IDs found
  366. */
  367. function getNoticeIds($offset, $limit, $since_id, $max_id)
  368. {
  369. $notice = new Notice();
  370. $notice->selectAdd();
  371. $notice->selectAdd('id');
  372. $notice->whereAdd(sprintf('notice.created > "%s"', $notice->escape($this->target->created)));
  373. // Reply:: is a table of mentions
  374. // Subscription:: is a table of subscriptions (every user is subscribed to themselves)
  375. $notice->whereAdd(
  376. // notices from profiles we subscribe to
  377. sprintf('( notice.profile_id IN (SELECT subscribed FROM subscription WHERE subscriber=%1$d) ' .
  378. // and in groups we're members of
  379. 'OR notice.id IN (SELECT notice_id FROM group_inbox WHERE group_id IN (SELECT group_id FROM group_member WHERE profile_id=%1$d))' .
  380. // and from attention table (i, hannes, don't know whats in that though...)
  381. 'OR notice.id IN (SELECT notice_id FROM attention WHERE profile_id=%1$d) ) ' .
  382. // all of the notices matching the above must also be either
  383. // 1) a non-reply
  384. 'AND (notice.reply_to IS NULL ' .
  385. // 2) OR a reply to myself
  386. 'OR notice.profile_id=%1$d ' .
  387. // 3) OR a reply to someone i'm subscibing to
  388. 'OR notice.reply_to IN (SELECT id FROM notice as noticereplies WHERE noticereplies.profile_id IN (SELECT subscribed FROM subscription WHERE subscriber=%1$d))) '.
  389. // lastly: include all notices mentioning me
  390. 'OR (notice.id IN (SELECT notice_id FROM reply WHERE profile_id=%1$d) ' .
  391. // but not if they are from someone i don't subscribe to
  392. 'AND notice.profile_id IN (SELECT subscribed FROM subscription WHERE subscriber=%1$d))',
  393. $this->target->id)
  394. );
  395. if (!empty($since_id)) {
  396. $notice->whereAdd(sprintf('notice.id > %d', $since_id));
  397. }
  398. if (!empty($max_id)) {
  399. $notice->whereAdd(sprintf('notice.id <= %d', $max_id));
  400. }
  401. // We have changed how selectVerbs work in GNUsocial, so it's an associative array
  402. // where each verb is in the key and then the value (true/false) is how to filter.
  403. // $this->unselectVerbs is always unset in newer GNUsocials.
  404. if (!isset($this->unselectVerbs)) {
  405. self::filterVerbs($notice, $this->selectVerbs);
  406. } elseif (!empty($this->selectVerbs)) {
  407. // old behaviour was just if there were selectVerbs set
  408. $notice->whereAddIn('verb', $this->selectVerbs, $notice->columnType('verb'));
  409. }
  410. $notice->limit($offset, $limit);
  411. // notice.id will give us even really old posts, which were
  412. // recently imported. For example if a remote instance had
  413. // problems and just managed to post here. Another solution
  414. // would be to have a 'notice.imported' field and order by it.
  415. $notice->orderBy('notice.id DESC');
  416. if (!$notice->find()) {
  417. return array();
  418. }
  419. $ids = $notice->fetchAll('id');
  420. return $ids;
  421. }
  422. }