useractivitystream.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  1. <?php
  2. // This file is part of GNU social - https://www.gnu.org/software/social
  3. //
  4. // GNU social is free software: you can redistribute it and/or modify
  5. // it under the terms of the GNU Affero General Public License as published by
  6. // the Free Software Foundation, either version 3 of the License, or
  7. // (at your option) any later version.
  8. //
  9. // GNU social is distributed in the hope that it will be useful,
  10. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. // GNU Affero General Public License for more details.
  13. //
  14. // You should have received a copy of the GNU Affero General Public License
  15. // along with GNU social. If not, see <http://www.gnu.org/licenses/>.
  16. /*
  17. * Class for activity streams
  18. *
  19. * @package GNUsocial
  20. * @copyright 2010 StatusNet, Inc.
  21. * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
  22. */
  23. defined('GNUSOCIAL') || die();
  24. /**
  25. * Class for activity streams
  26. *
  27. * Includes objects like notices, subscriptions and from plugins.
  28. *
  29. * We extend atomusernoticefeed since it does some nice setup for us.
  30. *
  31. * @package GNUsocial
  32. * @copyright 2010 StatusNet, Inc.
  33. * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
  34. */
  35. class UserActivityStream extends AtomUserNoticeFeed
  36. {
  37. public $activities = array();
  38. public $after = null;
  39. const OUTPUT_STRING = 1;
  40. const OUTPUT_RAW = 2;
  41. public $outputMode = self::OUTPUT_STRING;
  42. /**
  43. *
  44. * @param User $user
  45. * @param bool $indent
  46. * @param bool $outputMode: UserActivityStream::OUTPUT_STRING to return a string,
  47. * or UserActivityStream::OUTPUT_RAW to go to raw output.
  48. * Raw output mode will attempt to stream, keeping less
  49. * data in memory but will leave $this->activities incomplete.
  50. */
  51. public function __construct(
  52. $user,
  53. $indent = true,
  54. $outputMode = UserActivityStream::OUTPUT_STRING,
  55. $after = null
  56. ) {
  57. parent::__construct($user, null, $indent);
  58. $this->outputMode = $outputMode;
  59. if ($this->outputMode == self::OUTPUT_STRING) {
  60. // String buffering? Grab all the notices now.
  61. $notices = $this->getNotices();
  62. } elseif ($this->outputMode == self::OUTPUT_RAW) {
  63. // Raw output... need to restructure from the stringer init.
  64. $this->xw = new XMLWriter();
  65. $this->xw->openURI('php://output');
  66. if (is_null($indent)) {
  67. $indent = common_config('site', 'indent');
  68. }
  69. $this->xw->setIndent($indent);
  70. // We'll fetch notices later.
  71. $notices = array();
  72. } else {
  73. throw new Exception('Invalid outputMode provided to ' . __METHOD__);
  74. }
  75. $this->after = $after;
  76. // Assume that everything but notices is feasible
  77. // to pull at once and work with in memory...
  78. $subscriptions = $this->getSubscriptions();
  79. $subscribers = $this->getSubscribers();
  80. $groups = $this->getGroups();
  81. $objs = array_merge($subscriptions, $subscribers, $groups, $notices);
  82. Event::handle('AppendUserActivityStreamObjects', array($this, &$objs));
  83. $subscriptions = null;
  84. $subscribers = null;
  85. $groups = null;
  86. unset($subscriptions);
  87. unset($subscribers);
  88. unset($groups);
  89. // Sort by create date
  90. usort($objs, 'UserActivityStream::compareObject');
  91. // We'll keep these around for later, and interleave them into
  92. // the output stream with the user's notices.
  93. $this->objs = $objs;
  94. }
  95. /**
  96. * Interleave the pre-sorted objects with the user's
  97. * notices, all in reverse chron order.
  98. */
  99. public function renderEntries($format = Feed::ATOM, $handle = null)
  100. {
  101. $haveOne = false;
  102. $end = time() + 1;
  103. foreach ($this->objs as $obj) {
  104. set_time_limit(10);
  105. try {
  106. $act = $obj->asActivity();
  107. } catch (Exception $e) {
  108. common_log(LOG_ERR, $e->getMessage());
  109. continue;
  110. }
  111. $start = $act->time;
  112. if ($this->outputMode == self::OUTPUT_RAW && $start != $end) {
  113. // In raw mode, we haven't pre-fetched notices.
  114. // Grab the chunks of notices between other activities.
  115. try {
  116. $notices = $this->getNoticesBetween($start, $end);
  117. foreach ($notices as $noticeAct) {
  118. try {
  119. $nact = $noticeAct->asActivity($this->user->getProfile());
  120. if ($format == Feed::ATOM) {
  121. $nact->outputTo($this, false, false);
  122. } else {
  123. if ($haveOne) {
  124. fwrite($handle, ",");
  125. }
  126. fwrite($handle, json_encode($nact->asArray()));
  127. $haveOne = true;
  128. }
  129. } catch (Exception $e) {
  130. common_log(LOG_ERR, $e->getMessage());
  131. continue;
  132. }
  133. $nact = null;
  134. unset($nact);
  135. }
  136. } catch (Exception $e) {
  137. common_log(LOG_ERR, $e->getMessage());
  138. }
  139. }
  140. $notices = null;
  141. unset($notices);
  142. try {
  143. if ($format == Feed::ATOM) {
  144. // Only show the author sub-element if it's different from default user
  145. $act->outputTo($this, false, ($act->actor->id != $this->user->getUri()));
  146. } else {
  147. if ($haveOne) {
  148. fwrite($handle, ",");
  149. }
  150. fwrite($handle, json_encode($act->asArray()));
  151. $haveOne = true;
  152. }
  153. } catch (Exception $e) {
  154. common_log(LOG_ERR, $e->getMessage());
  155. }
  156. $act = null;
  157. unset($act);
  158. $end = $start;
  159. }
  160. if ($this->outputMode == self::OUTPUT_RAW) {
  161. // Grab anything after the last pre-sorted activity.
  162. try {
  163. if (!empty($this->after)) {
  164. $notices = $this->getNoticesBetween($this->after, $end);
  165. } else {
  166. $notices = $this->getNoticesBetween(0, $end);
  167. }
  168. foreach ($notices as $noticeAct) {
  169. try {
  170. $nact = $noticeAct->asActivity($this->user->getProfile());
  171. if ($format == Feed::ATOM) {
  172. $nact->outputTo($this, false, false);
  173. } else {
  174. if ($haveOne) {
  175. fwrite($handle, ",");
  176. }
  177. fwrite($handle, json_encode($nact->asArray()));
  178. $haveOne = true;
  179. }
  180. } catch (Exception $e) {
  181. common_log(LOG_ERR, $e->getMessage());
  182. continue;
  183. }
  184. }
  185. } catch (Exception $e) {
  186. common_log(LOG_ERR, $e->getMessage());
  187. }
  188. }
  189. if (empty($this->after) || strtotime($this->user->created) > $this->after) {
  190. // We always add the registration activity at the end, even if
  191. // they have older activities (from restored backups) in their stream.
  192. try {
  193. $ract = $this->user->registrationActivity();
  194. if ($format == Feed::ATOM) {
  195. $ract->outputTo($this, false, false);
  196. } else {
  197. if ($haveOne) {
  198. fwrite($handle, ",");
  199. }
  200. fwrite($handle, json_encode($ract->asArray()));
  201. $haveOne = true;
  202. }
  203. } catch (Exception $e) {
  204. common_log(LOG_ERR, $e->getMessage());
  205. }
  206. }
  207. }
  208. public function compareObject($a, $b)
  209. {
  210. $ac = strtotime((empty($a->created)) ? $a->modified : $a->created);
  211. $bc = strtotime((empty($b->created)) ? $b->modified : $b->created);
  212. return (($ac == $bc) ? 0 : (($ac < $bc) ? 1 : -1));
  213. }
  214. public function getSubscriptions()
  215. {
  216. $subs = array();
  217. $sub = new Subscription();
  218. $sub->subscriber = $this->user->id;
  219. if (!empty($this->after)) {
  220. $sub->whereAdd("created > '" . common_sql_date($this->after) . "'");
  221. }
  222. if ($sub->find()) {
  223. while ($sub->fetch()) {
  224. if ($sub->subscribed != $this->user->id) {
  225. $subs[] = clone($sub);
  226. }
  227. }
  228. }
  229. return $subs;
  230. }
  231. public function getSubscribers()
  232. {
  233. $subs = array();
  234. $sub = new Subscription();
  235. $sub->subscribed = $this->user->id;
  236. if (!empty($this->after)) {
  237. $sub->whereAdd("created > '" . common_sql_date($this->after) . "'");
  238. }
  239. if ($sub->find()) {
  240. while ($sub->fetch()) {
  241. if ($sub->subscriber != $this->user->id) {
  242. $subs[] = clone($sub);
  243. }
  244. }
  245. }
  246. return $subs;
  247. }
  248. /**
  249. *
  250. * @param int $start unix timestamp for earliest
  251. * @param int $end unix timestamp for latest
  252. * @return array of Notice objects
  253. */
  254. public function getNoticesBetween($start = 0, $end = 0)
  255. {
  256. $notices = [];
  257. $notice = new Notice();
  258. $notice->profile_id = $this->user->id;
  259. // Only stuff after $this->after
  260. if (!empty($this->after)) {
  261. if ($start) {
  262. $start = max($start, $this->after);
  263. }
  264. if ($end) {
  265. $end = max($end, $this->after);
  266. }
  267. }
  268. if ($start) {
  269. $tsstart = common_sql_date($start);
  270. $notice->whereAdd("created >= '$tsstart'");
  271. }
  272. if ($end) {
  273. $tsend = common_sql_date($end);
  274. $notice->whereAdd("created < '$tsend'");
  275. }
  276. $notice->orderBy('created DESC, id DESC');
  277. if ($notice->find()) {
  278. while ($notice->fetch()) {
  279. $notices[] = clone($notice);
  280. }
  281. }
  282. return $notices;
  283. }
  284. public function getNotices()
  285. {
  286. if (!empty($this->after)) {
  287. return $this->getNoticesBetween($this->after);
  288. } else {
  289. return $this->getNoticesBetween();
  290. }
  291. }
  292. public function getGroups()
  293. {
  294. $groups = array();
  295. $gm = new Group_member();
  296. $gm->profile_id = $this->user->id;
  297. if (!empty($this->after)) {
  298. $gm->whereAdd("created > '" . common_sql_date($this->after) . "'");
  299. }
  300. if ($gm->find()) {
  301. while ($gm->fetch()) {
  302. $groups[] = clone($gm);
  303. }
  304. }
  305. return $groups;
  306. }
  307. public function createdAfter($item)
  308. {
  309. $created = strtotime((empty($item->created)) ? $item->modified : $item->created);
  310. return ($created >= $this->after);
  311. }
  312. public function writeJSON($handle)
  313. {
  314. require_once INSTALLDIR . '/lib/activitystreams/activitystreamjsondocument.php';
  315. fwrite($handle, '{"items": [');
  316. $this->renderEntries(Feed::JSON, $handle);
  317. fwrite($handle, ']}');
  318. }
  319. }