Subscription.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451
  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. * @copyright 2008, 2009 StatusNet, Inc.
  18. * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
  19. */
  20. defined('GNUSOCIAL') || die();
  21. /**
  22. * Table Definition for subscription
  23. */
  24. class Subscription extends Managed_DataObject
  25. {
  26. const CACHE_WINDOW = 201;
  27. const FORCE = true;
  28. public $__table = 'subscription'; // table name
  29. public $subscriber; // int(4) primary_key not_null
  30. public $subscribed; // int(4) primary_key not_null
  31. public $jabber; // bool default_true
  32. public $sms; // bool default_true
  33. public $token; // varchar(191) not 255 because utf8mb4 takes more space
  34. public $secret; // varchar(191) not 255 because utf8mb4 takes more space
  35. public $uri; // varchar(191) not 255 because utf8mb4 takes more space
  36. public $created; // datetime()
  37. public $modified; // timestamp() not_null default_CURRENT_TIMESTAMP
  38. public static function schemaDef()
  39. {
  40. return array(
  41. 'fields' => array(
  42. 'subscriber' => array('type' => 'int', 'not null' => true, 'description' => 'profile listening'),
  43. 'subscribed' => array('type' => 'int', 'not null' => true, 'description' => 'profile being listened to'),
  44. 'jabber' => array('type' => 'bool', 'default' => true, 'description' => 'deliver jabber messages'),
  45. 'sms' => array('type' => 'bool', 'default' => true, 'description' => 'deliver sms messages'),
  46. 'token' => array('type' => 'varchar', 'length' => 191, 'description' => 'authorization token'),
  47. 'secret' => array('type' => 'varchar', 'length' => 191, 'description' => 'token secret'),
  48. 'uri' => array('type' => 'varchar', 'length' => 191, 'description' => 'universally unique identifier'),
  49. 'created' => array('type' => 'datetime', 'description' => 'date this record was created'),
  50. 'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'),
  51. ),
  52. 'primary key' => array('subscriber', 'subscribed'),
  53. 'unique keys' => array(
  54. 'subscription_uri_key' => array('uri'),
  55. ),
  56. 'indexes' => array(
  57. 'subscription_subscriber_created_subscribed_idx' => array('subscriber', 'created', 'subscribed'),
  58. 'subscription_subscribed_created_subscriber_idx' => array('subscribed', 'created', 'subscriber'),
  59. 'subscription_token_idx' => array('token'),
  60. ),
  61. );
  62. }
  63. /**
  64. * Make a new subscription
  65. *
  66. * @param Profile $subscriber party to receive new notices
  67. * @param Profile $other party sending notices; publisher
  68. * @param bool $force pass Subscription::FORCE to override local subscription approval
  69. *
  70. * @return mixed Subscription or Subscription_queue: new subscription info
  71. */
  72. public static function start(Profile $subscriber, Profile $other, $force = false)
  73. {
  74. if (!$subscriber->hasRight(Right::SUBSCRIBE)) {
  75. // TRANS: Exception thrown when trying to subscribe while being banned from subscribing.
  76. throw new Exception(_('You have been banned from subscribing.'));
  77. }
  78. if (self::exists($subscriber, $other)) {
  79. // TRANS: Exception thrown when trying to subscribe while already subscribed.
  80. throw new AlreadyFulfilledException(_('Already subscribed!'));
  81. }
  82. if ($other->hasBlocked($subscriber)) {
  83. // TRANS: Exception thrown when trying to subscribe to a user who has blocked the subscribing user.
  84. throw new Exception(_('User has blocked you.'));
  85. }
  86. if (Event::handle('StartSubscribe', array($subscriber, $other))) {
  87. // unless subscription is forced, the user policy for subscription approvals is tested
  88. if (!$force && $other->requiresSubscriptionApproval($subscriber)) {
  89. try {
  90. $sub = Subscription_queue::saveNew($subscriber, $other);
  91. $sub->notify();
  92. } catch (AlreadyFulfilledException $e) {
  93. $sub = Subscription_queue::getSubQueue($subscriber, $other);
  94. }
  95. } else {
  96. $otherUser = User::getKV('id', $other->id);
  97. $sub = self::saveNew($subscriber, $other);
  98. $sub->notify();
  99. self::blow('user:notices_with_friends:%d', $subscriber->id);
  100. self::blow('subscription:by-subscriber:'.$subscriber->id);
  101. self::blow('subscription:by-subscribed:'.$other->id);
  102. $subscriber->blowSubscriptionCount();
  103. $other->blowSubscriberCount();
  104. if ($otherUser instanceof User &&
  105. $otherUser->autosubscribe &&
  106. !self::exists($other, $subscriber) &&
  107. !$subscriber->hasBlocked($other)) {
  108. try {
  109. self::start($other, $subscriber);
  110. } catch (AlreadyFulfilledException $e) {
  111. // This shouldn't happen due to !self::exists above
  112. common_debug('Tried to autosubscribe a user to its new subscriber.');
  113. } catch (Exception $e) {
  114. common_log(LOG_ERR, "Exception during autosubscribe of {$other->nickname} to profile {$subscriber->id}: {$e->getMessage()}");
  115. }
  116. }
  117. }
  118. if ($sub instanceof Subscription) { // i.e. not Subscription_queue
  119. Event::handle('EndSubscribe', array($subscriber, $other));
  120. }
  121. }
  122. return $sub;
  123. }
  124. public static function ensureStart(Profile $subscriber, Profile $other, $force = false)
  125. {
  126. try {
  127. $sub = self::start($subscriber, $other, $force);
  128. } catch (AlreadyFulfilledException $e) {
  129. return self::getSubscription($subscriber, $other);
  130. }
  131. return $sub;
  132. }
  133. /**
  134. * Low-level subscription save.
  135. * Outside callers should use Subscription::start()
  136. */
  137. protected static function saveNew(Profile $subscriber, Profile $other)
  138. {
  139. $sub = new Subscription();
  140. $sub->subscriber = $subscriber->getID();
  141. $sub->subscribed = $other->getID();
  142. $sub->jabber = true;
  143. $sub->sms = true;
  144. $sub->created = common_sql_now();
  145. $sub->uri = self::newUri(
  146. $subscriber,
  147. $other,
  148. $sub->created
  149. );
  150. $result = $sub->insert();
  151. if ($result===false) {
  152. common_log_db_error($sub, 'INSERT', __FILE__);
  153. // TRANS: Exception thrown when a subscription could not be stored on the server.
  154. throw new Exception(_('Could not save subscription.'));
  155. }
  156. return $sub;
  157. }
  158. public function notify()
  159. {
  160. // XXX: add other notifications (Jabber, SMS) here
  161. // XXX: queue this and handle it offline
  162. // XXX: Whatever happens, do it in Twitter-like API, too
  163. $this->notifyEmail();
  164. }
  165. public function notifyEmail()
  166. {
  167. $subscribedUser = User::getKV('id', $this->subscribed);
  168. if ($subscribedUser instanceof User) {
  169. $subscriber = Profile::getKV('id', $this->subscriber);
  170. mail_subscribe_notify_profile($subscribedUser, $subscriber);
  171. }
  172. }
  173. /**
  174. * Cancel a subscription
  175. *
  176. */
  177. public static function cancel(Profile $subscriber, Profile $other)
  178. {
  179. if (!self::exists($subscriber, $other)) {
  180. // TRANS: Exception thrown when trying to unsibscribe without a subscription.
  181. throw new AlreadyFulfilledException(_('Not subscribed!'));
  182. }
  183. // Don't allow deleting self subs
  184. if ($subscriber->id == $other->id) {
  185. // TRANS: Exception thrown when trying to unsubscribe a user from themselves.
  186. throw new Exception(_('Could not delete self-subscription.'));
  187. }
  188. if (Event::handle('StartUnsubscribe', array($subscriber, $other))) {
  189. $sub = Subscription::pkeyGet(array('subscriber' => $subscriber->id,
  190. 'subscribed' => $other->id));
  191. // note we checked for existence above
  192. assert(!empty($sub));
  193. $result = $sub->delete();
  194. if (!$result) {
  195. common_log_db_error($sub, 'DELETE', __FILE__);
  196. // TRANS: Exception thrown when a subscription could not be deleted on the server.
  197. throw new Exception(_('Could not delete subscription.'));
  198. }
  199. self::blow('user:notices_with_friends:%d', $subscriber->id);
  200. self::blow('subscription:by-subscriber:'.$subscriber->id);
  201. self::blow('subscription:by-subscribed:'.$other->id);
  202. $subscriber->blowSubscriptionCount();
  203. $other->blowSubscriberCount();
  204. Event::handle('EndUnsubscribe', array($subscriber, $other));
  205. }
  206. return;
  207. }
  208. public static function exists(Profile $subscriber, Profile $other)
  209. {
  210. try {
  211. $sub = self::getSubscription($subscriber, $other);
  212. } catch (NoResultException $e) {
  213. return false;
  214. }
  215. return true;
  216. }
  217. public static function getSubscription(Profile $subscriber, Profile $other)
  218. {
  219. // This is essentially a pkeyGet but we have an object to return in NoResultException
  220. $sub = new Subscription();
  221. $sub->subscriber = $subscriber->id;
  222. $sub->subscribed = $other->id;
  223. if (!$sub->find(true)) {
  224. throw new NoResultException($sub);
  225. }
  226. return $sub;
  227. }
  228. public function getSubscriber()
  229. {
  230. return Profile::getByID($this->subscriber);
  231. }
  232. public function getSubscribed()
  233. {
  234. return Profile::getByID($this->subscribed);
  235. }
  236. public function asActivity()
  237. {
  238. $subscriber = $this->getSubscriber();
  239. $subscribed = $this->getSubscribed();
  240. $act = new Activity();
  241. $act->verb = ActivityVerb::FOLLOW;
  242. // XXX: rationalize this with the URL
  243. $act->id = $this->getUri();
  244. $act->time = strtotime($this->created);
  245. // TRANS: Activity title when subscribing to another person.
  246. $act->title = _m('TITLE', 'Follow');
  247. // TRANS: Notification given when one person starts following another.
  248. // TRANS: %1$s is the subscriber, %2$s is the subscribed.
  249. $act->content = sprintf(
  250. _('%1$s is now following %2$s.'),
  251. $subscriber->getBestName(),
  252. $subscribed->getBestName()
  253. );
  254. $act->actor = $subscriber->asActivityObject();
  255. $act->objects[] = $subscribed->asActivityObject();
  256. $url = common_local_url(
  257. 'AtomPubShowSubscription',
  258. [
  259. 'subscriber' => $subscriber->id,
  260. 'subscribed' => $subscribed->id,
  261. ]
  262. );
  263. $act->selfLink = $url;
  264. $act->editLink = $url;
  265. return $act;
  266. }
  267. /**
  268. * Stream of subscriptions with the same subscriber
  269. *
  270. * Useful for showing pages that list subscriptions in reverse
  271. * chronological order. Has offset & limit to make paging
  272. * easy.
  273. *
  274. * @param integer $profile_id ID of the subscriber profile
  275. * @param integer $offset Offset from latest
  276. * @param integer $limit Maximum number to fetch
  277. *
  278. * @return Subscription stream of subscriptions; use fetch() to iterate
  279. */
  280. public static function bySubscriber($profile_id, $offset = 0, $limit = PROFILES_PER_PAGE)
  281. {
  282. // "by subscriber" means it is the list of subscribed users we want
  283. $ids = self::getSubscribedIDs($profile_id, $offset, $limit);
  284. return Subscription::listFind('subscribed', $ids);
  285. }
  286. /**
  287. * Stream of subscriptions with the same subscriber
  288. *
  289. * Useful for showing pages that list subscriptions in reverse
  290. * chronological order. Has offset & limit to make paging
  291. * easy.
  292. *
  293. * @param integer $profile_id ID of the subscribed profile
  294. * @param integer $offset Offset from latest
  295. * @param integer $limit Maximum number to fetch
  296. *
  297. * @return Subscription stream of subscriptions; use fetch() to iterate
  298. */
  299. public static function bySubscribed($profile_id, $offset = 0, $limit = PROFILES_PER_PAGE)
  300. {
  301. // "by subscribed" means it is the list of subscribers we want
  302. $ids = self::getSubscriberIDs($profile_id, $offset, $limit);
  303. return Subscription::listFind('subscriber', $ids);
  304. }
  305. // The following are helper functions to the subscription lists,
  306. // notably the public ones get used in places such as Profile
  307. public static function getSubscribedIDs($profile_id, $offset, $limit)
  308. {
  309. return self::getSubscriptionIDs('subscribed', $profile_id, $offset, $limit);
  310. }
  311. public static function getSubscriberIDs($profile_id, $offset, $limit)
  312. {
  313. return self::getSubscriptionIDs('subscriber', $profile_id, $offset, $limit);
  314. }
  315. private static function getSubscriptionIDs($get_type, $profile_id, $offset, $limit)
  316. {
  317. switch ($get_type) {
  318. case 'subscribed':
  319. $by_type = 'subscriber';
  320. break;
  321. case 'subscriber':
  322. $by_type = 'subscribed';
  323. break;
  324. default:
  325. throw new Exception('Bad type argument to getSubscriptionIDs');
  326. }
  327. $cacheKey = 'subscription:by-'.$by_type.':'.$profile_id;
  328. $queryoffset = $offset;
  329. $querylimit = $limit;
  330. if ($offset + $limit <= self::CACHE_WINDOW) {
  331. // Oh, it seems it should be cached
  332. $ids = self::cacheGet($cacheKey);
  333. if (is_array($ids)) {
  334. return array_slice($ids, $offset, $limit);
  335. }
  336. // Being here indicates we didn't find anything cached
  337. // so we'll have to fill it up simultaneously
  338. $queryoffset = 0;
  339. $querylimit = self::CACHE_WINDOW;
  340. }
  341. $sub = new Subscription();
  342. $sub->$by_type = $profile_id;
  343. $sub->selectAdd($get_type);
  344. $sub->whereAdd($get_type . ' <> ' . $profile_id);
  345. $sub->orderBy("created DESC, {$get_type} DESC");
  346. $sub->limit($queryoffset, $querylimit);
  347. if (!$sub->find()) {
  348. return array();
  349. }
  350. $ids = $sub->fetchAll($get_type);
  351. // If we're simultaneously filling up cache, remember to slice
  352. if ($queryoffset === 0 && $querylimit === self::CACHE_WINDOW) {
  353. self::cacheSet($cacheKey, $ids);
  354. return array_slice($ids, $offset, $limit);
  355. }
  356. return $ids;
  357. }
  358. /**
  359. * Flush cached subscriptions when subscription is updated
  360. *
  361. * Because we cache subscriptions, it's useful to flush them
  362. * here.
  363. *
  364. * @param mixed $dataObject Original version of object
  365. *
  366. * @return boolean success flag.
  367. */
  368. public function update($dataObject = false)
  369. {
  370. self::blow('subscription:by-subscriber:'.$this->subscriber);
  371. self::blow('subscription:by-subscribed:'.$this->subscribed);
  372. return parent::update($dataObject);
  373. }
  374. public function getUri()
  375. {
  376. return $this->uri ?: self::newUri($this->getSubscriber(), $this->getSubscribed(), $this->created);
  377. }
  378. }