Subscription.php 10 KB

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