Subscription.php 16 KB

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