Subscription.php 11 KB

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