Subscription.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. <?php
  2. declare(strict_types = 1);
  3. // {{{ License
  4. // This file is part of GNU social - https://www.gnu.org/software/social
  5. //
  6. // GNU social 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. // GNU social 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 GNU social. If not, see <http://www.gnu.org/licenses/>.
  18. // }}}
  19. namespace Component\Subscription;
  20. use App\Core\Cache;
  21. use App\Core\DB\DB;
  22. use App\Core\Event;
  23. use function App\Core\I18n\_m;
  24. use App\Core\Modules\Component;
  25. use App\Core\Router\RouteLoader;
  26. use App\Core\Router\Router;
  27. use App\Entity\Activity;
  28. use App\Entity\Actor;
  29. use App\Entity\LocalUser;
  30. use App\Util\Common;
  31. use App\Util\Exception\DuplicateFoundException;
  32. use App\Util\Exception\NotFoundException;
  33. use App\Util\Exception\ServerException;
  34. use App\Util\Nickname;
  35. use Component\Subscription\Controller\Subscribers as SubscribersController;
  36. use Component\Subscription\Controller\Subscriptions as SubscriptionsController;
  37. use Symfony\Component\HttpFoundation\Request;
  38. class Subscription extends Component
  39. {
  40. public function onAddRoute(RouteLoader $r): bool
  41. {
  42. $r->connect(id: 'actor_subscribe_add', uri_path: '/actor/subscribe/{object_id<\d+>}', target: [SubscribersController::class, 'subscribersAdd']);
  43. $r->connect(id: 'actor_subscribe_remove', uri_path: '/actor/unsubscribe/{object_id<\d+>}', target: [SubscribersController::class, 'subscribersRemove']);
  44. $r->connect(id: 'actor_subscriptions_id', uri_path: '/actor/{id<\d+>}/subscriptions', target: [SubscriptionsController::class, 'subscriptionsByActorId']);
  45. $r->connect(id: 'actor_subscriptions_nickname', uri_path: '/@{nickname<' . Nickname::DISPLAY_FMT . '>}/subscriptions', target: [SubscriptionsController::class, 'subscriptionsByActorNickname']);
  46. $r->connect(id: 'actor_subscribers_id', uri_path: '/actor/{id<\d+>}/subscribers', target: [SubscribersController::class, 'subscribersByActorId']);
  47. $r->connect(id: 'actor_subscribers_nickname', uri_path: '/@{nickname<' . Nickname::DISPLAY_FMT . '>}/subscribers', target: [SubscribersController::class, 'subscribersByActorNickname']);
  48. return Event::next;
  49. }
  50. /**
  51. * To use after Subscribe/Unsubscribe and DB::flush()
  52. *
  53. * @param Actor|int|LocalUser $subject The Actor who subscribed or unsubscribed
  54. * @param Actor|int|LocalUser $object The Actor who was subscribed or unsubscribed from
  55. */
  56. public static function refreshSubscriptionCount(int|Actor|LocalUser $subject, int|Actor|LocalUser $object): array
  57. {
  58. $subscriber_id = \is_int($subject) ? $subject : $subject->getId();
  59. $subscribed_id = \is_int($object) ? $object : $object->getId();
  60. $cache_subscriber = Cache::delete(Actor::cacheKeys($subscriber_id)['subscribed']);
  61. $cache_subscribed = Cache::delete(Actor::cacheKeys($subscribed_id)['subscribers']);
  62. return [$cache_subscriber,$cache_subscribed];
  63. }
  64. /**
  65. * Persists a new Subscription Entity from Subject to Object (Actor being subscribed) and Activity
  66. *
  67. * A new notification is then handled, informing all interested Actors of this action
  68. *
  69. * @param Actor|int|LocalUser $subject The actor performing the subscription
  70. * @param Actor|int|LocalUser $object The target of the subscription
  71. *
  72. * @throws DuplicateFoundException
  73. * @throws NotFoundException
  74. * @throws ServerException
  75. *
  76. * @return null|Activity a new Activity if changes were made
  77. *
  78. * @see self::refreshSubscriptionCount() to delete cache after this action
  79. */
  80. public static function subscribe(int|Actor|LocalUser $subject, int|Actor|LocalUser $object, string $source = 'web'): ?Activity
  81. {
  82. $subscriber_id = \is_int($subject) ? $subject : $subject->getId();
  83. $subscribed_id = \is_int($object) ? $object : $object->getId();
  84. $opts = [
  85. 'subscriber_id' => $subscriber_id,
  86. 'subscribed_id' => $subscribed_id,
  87. ];
  88. $subscription = DB::findOneBy(table: Entity\ActorSubscription::class, criteria: $opts, return_null: true);
  89. $activity = null;
  90. if (\is_null($subscription)) {
  91. DB::persist(Entity\ActorSubscription::create($opts));
  92. $activity = Activity::create([
  93. 'actor_id' => $subscriber_id,
  94. 'verb' => 'subscribe',
  95. 'object_type' => 'actor',
  96. 'object_id' => $subscribed_id,
  97. 'source' => $source,
  98. ]);
  99. DB::persist($activity);
  100. Event::handle('NewNotification', [
  101. \is_int($subject) ? $subject : Actor::getById($subscriber_id),
  102. $activity,
  103. ['object' => [$activity->getObjectId()]],
  104. _m('{subject} subscribed to {object}.', ['{subject}' => $activity->getActorId(), '{object}' => $activity->getObjectId()]),
  105. ]);
  106. }
  107. return $activity;
  108. }
  109. /**
  110. * Removes the Subscription Entity created beforehand, by the same Actor, and on the same object
  111. *
  112. * Informs all interested Actors of this action, handling out the NewNotification event
  113. *
  114. * @param Actor|int|LocalUser $subject The actor undoing the subscription
  115. * @param Actor|int|LocalUser $object The target of the subscription
  116. *
  117. * @throws DuplicateFoundException
  118. * @throws NotFoundException
  119. * @throws ServerException
  120. *
  121. * @return null|Activity a new Activity if changes were made
  122. *
  123. * @see self::refreshSubscriptionCount() to delete cache after this action
  124. */
  125. public static function unsubscribe(int|Actor|LocalUser $subject, int|Actor|LocalUser $object, string $source = 'web'): ?Activity
  126. {
  127. $subscriber_id = \is_int($subject) ? $subject : $subject->getId();
  128. $subscribed_id = \is_int($object) ? $object : $object->getId();
  129. $opts = [
  130. 'subscriber_id' => $subscriber_id,
  131. 'subscribed_id' => $subscribed_id,
  132. ];
  133. $subscription = DB::findOneBy(table: Entity\ActorSubscription::class, criteria: $opts, return_null: true);
  134. $activity = null;
  135. if (!\is_null($subscription)) {
  136. // Remove Subscription
  137. DB::remove($subscription);
  138. $previous_follow_activity = DB::findBy('activity', ['verb' => 'subscribe', 'object_type' => 'actor', 'object_id' => $subscribed_id], order_by: ['created' => 'DESC'])[0];
  139. // Store Activity
  140. $activity = Activity::create([
  141. 'actor_id' => $subscriber_id,
  142. 'verb' => 'undo',
  143. 'object_type' => 'activity',
  144. 'object_id' => $previous_follow_activity->getId(),
  145. 'source' => $source,
  146. ]);
  147. DB::persist($activity);
  148. Event::handle('NewNotification', [
  149. \is_int($subject) ? $subject : Actor::getById($subscriber_id),
  150. $activity,
  151. ['object' => [$previous_follow_activity->getObjectId()]],
  152. _m('{subject} unsubscribed from {object}.', ['{subject}' => $activity->getActorId(), '{object}' => $previous_follow_activity->getObjectId()]),
  153. ]);
  154. }
  155. return $activity;
  156. }
  157. /**
  158. * Provides ``\App\templates\cards\profile\view.html.twig`` an **additional action** to be performed **on the given
  159. * Actor** (which the profile card of is currently being rendered).
  160. *
  161. * In the case of ``\App\Component\Subscription``, the action added allows a **LocalUser** to **subscribe** or
  162. * **unsubscribe** a given **Actor**.
  163. *
  164. * @param Actor $object The Actor on which the action is to be performed
  165. * @param array $actions An array containing all actions added to the
  166. * current profile, this event adds an action to it
  167. *
  168. * @throws DuplicateFoundException
  169. * @throws NotFoundException
  170. * @throws ServerException
  171. *
  172. * @return bool EventHook
  173. */
  174. public function onAddProfileActions(Request $request, Actor $object, array &$actions): bool
  175. {
  176. // Action requires a user to be logged in
  177. // We know it's a LocalUser, which has the same id as Actor
  178. // We don't want the Actor to unfollow itself
  179. if ((\is_null($subject = Common::user())) || ($subject->getId() === $object->getId())) {
  180. return Event::next;
  181. }
  182. // Let's retrieve from here this subject came from to redirect it to previous location
  183. $from = $request->query->has('from')
  184. ? $request->query->get('from')
  185. : $request->getPathInfo();
  186. // Who is the subject attempting to subscribe to?
  187. $object_id = $object->getId();
  188. // The id of both the subject and object
  189. $opts = [
  190. 'subscriber_id' => $subject->getId(),
  191. 'subscribed_id' => $object_id,
  192. ];
  193. // If subject is not subbed to object already, then route it to add subscription
  194. // Else, route to remove subscription
  195. $subscribe_action_url = ($not_subscribed_already = \is_null(DB::findOneBy(table: Entity\ActorSubscription::class, criteria: $opts, return_null: true))) ? Router::url(
  196. 'actor_subscribe_add',
  197. [
  198. 'object_id' => $object_id,
  199. 'from' => $from . '#profile-' . $object_id,
  200. ],
  201. Router::ABSOLUTE_PATH,
  202. ) : Router::url(
  203. 'actor_subscribe_remove',
  204. [
  205. 'object_id' => $object_id,
  206. 'from' => $from . '#profile-' . $object_id,
  207. ],
  208. Router::ABSOLUTE_PATH,
  209. );
  210. // Finally, create an array with proper keys set accordingly
  211. // to provide Profile Card template, the info it needs in order to render it properly
  212. $action_extra_class = $not_subscribed_already ? 'add-actor-button-container' : 'remove-actor-button-container';
  213. $title = $not_subscribed_already ? 'Subscribe ' . $object->getNickname() : 'Unsubscribe ' . $object->getNickname();
  214. $subscribe_action = [
  215. 'url' => $subscribe_action_url,
  216. 'title' => _m($title),
  217. 'classes' => 'button-container note-actions-unset ' . $action_extra_class,
  218. 'id' => 'add-actor-button-container-' . $object_id,
  219. ];
  220. $actions[] = $subscribe_action;
  221. return Event::next;
  222. }
  223. }