Subscription.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451
  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
  36. public $modified; // timestamp() 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, 'description' => 'date this record was created'),
  49. 'modified' => array('type' => 'timestamp', 'not null' => true, '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. $otherUser = User::getKV('id', $other->id);
  87. if ($otherUser instanceof User && $otherUser->subscribe_policy == User::SUBSCRIBE_POLICY_MODERATE && !$force) {
  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. $sub = self::saveNew($subscriber->id, $other->id);
  96. $sub->notify();
  97. self::blow('user:notices_with_friends:%d', $subscriber->id);
  98. self::blow('subscription:by-subscriber:'.$subscriber->id);
  99. self::blow('subscription:by-subscribed:'.$other->id);
  100. $subscriber->blowSubscriptionCount();
  101. $other->blowSubscriberCount();
  102. if ($otherUser instanceof User &&
  103. $otherUser->autosubscribe &&
  104. !self::exists($other, $subscriber) &&
  105. !$subscriber->hasBlocked($other)) {
  106. try {
  107. self::start($other, $subscriber);
  108. } catch (AlreadyFulfilledException $e) {
  109. // This shouldn't happen due to !self::exists above
  110. common_debug('Tried to autosubscribe a user to its new subscriber.');
  111. } catch (Exception $e) {
  112. common_log(LOG_ERR, "Exception during autosubscribe of {$other->nickname} to profile {$subscriber->id}: {$e->getMessage()}");
  113. }
  114. }
  115. }
  116. if ($sub instanceof Subscription) { // i.e. not Subscription_queue
  117. Event::handle('EndSubscribe', array($subscriber, $other));
  118. }
  119. }
  120. return $sub;
  121. }
  122. static function ensureStart(Profile $subscriber, Profile $other, $force=false)
  123. {
  124. try {
  125. $sub = self::start($subscriber, $other, $force);
  126. } catch (AlreadyFulfilledException $e) {
  127. return self::getSubscription($subscriber, $other);
  128. }
  129. return $sub;
  130. }
  131. /**
  132. * Low-level subscription save.
  133. * Outside callers should use Subscription::start()
  134. */
  135. protected function saveNew($subscriber_id, $other_id)
  136. {
  137. $sub = new Subscription();
  138. $sub->subscriber = $subscriber_id;
  139. $sub->subscribed = $other_id;
  140. $sub->jabber = 1;
  141. $sub->sms = 1;
  142. $sub->created = common_sql_now();
  143. $sub->uri = self::newURI($sub->subscriber,
  144. $sub->subscribed,
  145. $sub->created);
  146. $result = $sub->insert();
  147. if ($result===false) {
  148. common_log_db_error($sub, 'INSERT', __FILE__);
  149. // TRANS: Exception thrown when a subscription could not be stored on the server.
  150. throw new Exception(_('Could not save subscription.'));
  151. }
  152. return $sub;
  153. }
  154. function notify()
  155. {
  156. // XXX: add other notifications (Jabber, SMS) here
  157. // XXX: queue this and handle it offline
  158. // XXX: Whatever happens, do it in Twitter-like API, too
  159. $this->notifyEmail();
  160. }
  161. function notifyEmail()
  162. {
  163. $subscribedUser = User::getKV('id', $this->subscribed);
  164. if ($subscribedUser instanceof User) {
  165. $subscriber = Profile::getKV('id', $this->subscriber);
  166. mail_subscribe_notify_profile($subscribedUser, $subscriber);
  167. }
  168. }
  169. /**
  170. * Cancel a subscription
  171. *
  172. */
  173. static function cancel(Profile $subscriber, Profile $other)
  174. {
  175. if (!self::exists($subscriber, $other)) {
  176. // TRANS: Exception thrown when trying to unsibscribe without a subscription.
  177. throw new AlreadyFulfilledException(_('Not subscribed!'));
  178. }
  179. // Don't allow deleting self subs
  180. if ($subscriber->id == $other->id) {
  181. // TRANS: Exception thrown when trying to unsubscribe a user from themselves.
  182. throw new Exception(_('Could not delete self-subscription.'));
  183. }
  184. if (Event::handle('StartUnsubscribe', array($subscriber, $other))) {
  185. $sub = Subscription::pkeyGet(array('subscriber' => $subscriber->id,
  186. 'subscribed' => $other->id));
  187. // note we checked for existence above
  188. assert(!empty($sub));
  189. $result = $sub->delete();
  190. if (!$result) {
  191. common_log_db_error($sub, 'DELETE', __FILE__);
  192. // TRANS: Exception thrown when a subscription could not be deleted on the server.
  193. throw new Exception(_('Could not delete subscription.'));
  194. }
  195. self::blow('user:notices_with_friends:%d', $subscriber->id);
  196. self::blow('subscription:by-subscriber:'.$subscriber->id);
  197. self::blow('subscription:by-subscribed:'.$other->id);
  198. $subscriber->blowSubscriptionCount();
  199. $other->blowSubscriberCount();
  200. Event::handle('EndUnsubscribe', array($subscriber, $other));
  201. }
  202. return;
  203. }
  204. static function exists(Profile $subscriber, Profile $other)
  205. {
  206. try {
  207. $sub = self::getSubscription($subscriber, $other);
  208. } catch (NoResultException $e) {
  209. return false;
  210. }
  211. return true;
  212. }
  213. static function getSubscription(Profile $subscriber, Profile $other)
  214. {
  215. // This is essentially a pkeyGet but we have an object to return in NoResultException
  216. $sub = new Subscription();
  217. $sub->subscriber = $subscriber->id;
  218. $sub->subscribed = $other->id;
  219. if (!$sub->find(true)) {
  220. throw new NoResultException($sub);
  221. }
  222. return $sub;
  223. }
  224. function asActivity()
  225. {
  226. $subscriber = Profile::getKV('id', $this->subscriber);
  227. $subscribed = Profile::getKV('id', $this->subscribed);
  228. if (!$subscriber instanceof Profile) {
  229. throw new NoProfileException($this->subscriber);
  230. }
  231. if (!$subscribed instanceof Profile) {
  232. throw new NoProfileException($this->subscribed);
  233. }
  234. $act = new Activity();
  235. $act->verb = ActivityVerb::FOLLOW;
  236. // XXX: rationalize this with the URL
  237. $act->id = $this->getURI();
  238. $act->time = strtotime($this->created);
  239. // TRANS: Activity title when subscribing to another person.
  240. $act->title = _m('TITLE','Follow');
  241. // TRANS: Notification given when one person starts following another.
  242. // TRANS: %1$s is the subscriber, %2$s is the subscribed.
  243. $act->content = sprintf(_('%1$s is now following %2$s.'),
  244. $subscriber->getBestName(),
  245. $subscribed->getBestName());
  246. $act->actor = $subscriber->asActivityObject();
  247. $act->objects[] = $subscribed->asActivityObject();
  248. $url = common_local_url('AtomPubShowSubscription',
  249. array('subscriber' => $subscriber->id,
  250. 'subscribed' => $subscribed->id));
  251. $act->selfLink = $url;
  252. $act->editLink = $url;
  253. return $act;
  254. }
  255. /**
  256. * Stream of subscriptions with the same subscriber
  257. *
  258. * Useful for showing pages that list subscriptions in reverse
  259. * chronological order. Has offset & limit to make paging
  260. * easy.
  261. *
  262. * @param integer $profile_id ID of the subscriber profile
  263. * @param integer $offset Offset from latest
  264. * @param integer $limit Maximum number to fetch
  265. *
  266. * @return Subscription stream of subscriptions; use fetch() to iterate
  267. */
  268. public static function bySubscriber($profile_id, $offset = 0, $limit = PROFILES_PER_PAGE)
  269. {
  270. // "by subscriber" means it is the list of subscribed users we want
  271. $ids = self::getSubscribedIDs($profile_id, $offset, $limit);
  272. return Subscription::listFind('subscribed', $ids);
  273. }
  274. /**
  275. * Stream of subscriptions with the same subscriber
  276. *
  277. * Useful for showing pages that list subscriptions in reverse
  278. * chronological order. Has offset & limit to make paging
  279. * easy.
  280. *
  281. * @param integer $profile_id ID of the subscribed profile
  282. * @param integer $offset Offset from latest
  283. * @param integer $limit Maximum number to fetch
  284. *
  285. * @return Subscription stream of subscriptions; use fetch() to iterate
  286. */
  287. public static function bySubscribed($profile_id, $offset = 0, $limit = PROFILES_PER_PAGE)
  288. {
  289. // "by subscribed" means it is the list of subscribers we want
  290. $ids = self::getSubscriberIDs($profile_id, $offset, $limit);
  291. return Subscription::listFind('subscriber', $ids);
  292. }
  293. // The following are helper functions to the subscription lists,
  294. // notably the public ones get used in places such as Profile
  295. public static function getSubscribedIDs($profile_id, $offset, $limit) {
  296. return self::getSubscriptionIDs('subscribed', $profile_id, $offset, $limit);
  297. }
  298. public static function getSubscriberIDs($profile_id, $offset, $limit) {
  299. return self::getSubscriptionIDs('subscriber', $profile_id, $offset, $limit);
  300. }
  301. private static function getSubscriptionIDs($get_type, $profile_id, $offset, $limit)
  302. {
  303. switch ($get_type) {
  304. case 'subscribed':
  305. $by_type = 'subscriber';
  306. break;
  307. case 'subscriber':
  308. $by_type = 'subscribed';
  309. break;
  310. default:
  311. throw new Exception('Bad type argument to getSubscriptionIDs');
  312. }
  313. $cacheKey = 'subscription:by-'.$by_type.':'.$profile_id;
  314. $queryoffset = $offset;
  315. $querylimit = $limit;
  316. if ($offset + $limit <= self::CACHE_WINDOW) {
  317. // Oh, it seems it should be cached
  318. $ids = self::cacheGet($cacheKey);
  319. if (is_array($ids)) {
  320. return array_slice($ids, $offset, $limit);
  321. }
  322. // Being here indicates we didn't find anything cached
  323. // so we'll have to fill it up simultaneously
  324. $queryoffset = 0;
  325. $querylimit = self::CACHE_WINDOW;
  326. }
  327. $sub = new Subscription();
  328. $sub->$by_type = $profile_id;
  329. $sub->selectAdd($get_type);
  330. $sub->whereAdd("{$get_type} != {$profile_id}");
  331. $sub->orderBy('created DESC');
  332. $sub->limit($queryoffset, $querylimit);
  333. if (!$sub->find()) {
  334. return array();
  335. }
  336. $ids = $sub->fetchAll($get_type);
  337. // If we're simultaneously filling up cache, remember to slice
  338. if ($queryoffset === 0 && $querylimit === self::CACHE_WINDOW) {
  339. self::cacheSet($cacheKey, $ids);
  340. return array_slice($ids, $offset, $limit);
  341. }
  342. return $ids;
  343. }
  344. /**
  345. * Flush cached subscriptions when subscription is updated
  346. *
  347. * Because we cache subscriptions, it's useful to flush them
  348. * here.
  349. *
  350. * @param mixed $dataObject Original version of object
  351. *
  352. * @return boolean success flag.
  353. */
  354. function update($dataObject=false)
  355. {
  356. self::blow('subscription:by-subscriber:'.$this->subscriber);
  357. self::blow('subscription:by-subscribed:'.$this->subscribed);
  358. return parent::update($dataObject);
  359. }
  360. function getURI()
  361. {
  362. if (!empty($this->uri)) {
  363. return $this->uri;
  364. } else {
  365. return self::newURI($this->subscriber, $this->subscribed, $this->created);
  366. }
  367. }
  368. static function newURI($subscriber_id, $subscribed_id, $created)
  369. {
  370. return TagURI::mint('follow:%d:%d:%s',
  371. $subscriber_id,
  372. $subscribed_id,
  373. common_date_iso8601($created));
  374. }
  375. }