Subscription.php 15 KB

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