src/Service/ActivityFeedService.php line 349

Open in your IDE?
  1. <?php
  2. namespace App\Service;
  3. use App\Entity\ActivityFeed;
  4. use App\Entity\BlogEntry;
  5. use App\Entity\Bonus;
  6. use App\Entity\Content;
  7. use App\Entity\ContentComment;
  8. use App\Entity\ContentVideo;
  9. use App\Entity\Friendship;
  10. use App\Entity\GuestbookEntry;
  11. use App\Entity\Member;
  12. use App\Repository\ActivityFeedRepository;
  13. use App\Repository\BalanceMemberRepository;
  14. use App\StructSerializer\Main\StructGroup;
  15. use App\StructSerializer\Main\StructOptions;
  16. use App\StructSerializer\Main\StructSerializer;
  17. use Doctrine\ORM\Tools\Pagination\Paginator;
  18. use Frivol\Common\Dict\ActivityFeedType;
  19. use Frivol\Common\Service\CacheService;
  20. use Symfony\Component\HttpFoundation\ParameterBag;
  21. use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
  22. use Symfony\Contracts\Cache\ItemInterface;
  23. /**
  24.  * @method ActivityFeedRepository getRepository()
  25.  */
  26. class ActivityFeedService extends AbstractService
  27. {
  28.     /** @var int How many activities to keep in DB for visitors */
  29.     public const MAX_VISITOR_ACTIVITIES 180;
  30.     /** @var int How many activities to keep in DB per Member/Amateur/Operator (excl. profile visits) */
  31.     public const MAX_MEMBER_ACTIVITIES 100;
  32.     /** @var int How many activities to keep in DB per Member/Amateur/Operator (only profile visits) */
  33.     public const MAX_PROFILE_VISIT_ACTIVITIES 100;
  34.     /**
  35.      * @var ActivityFeedRepository
  36.      */
  37.     protected $repo;
  38.     protected MemberService $memberService;
  39.     protected BalanceMemberRepository $balanceMemberRepo;
  40.     public function __construct(ActivityFeedRepository $repositoryMemberService $memberServiceBalanceMemberRepository $bmRepository, private CacheService $cacheService, private StructSerializer $structSerializer)
  41.     {
  42.         parent::__construct($repository);
  43.         $this->memberService $memberService;
  44.         $this->balanceMemberRepo $bmRepository;
  45.     }
  46.     public function createEntityByMember(Member $member, ?int $type null): ActivityFeed
  47.     {
  48.         $entity = new ActivityFeed();
  49.         $entity->setMember($member);
  50.         if ($type) {
  51.             $entity->setType($type);
  52.         }
  53.         return $entity;
  54.     }
  55.     public function createBonusStartedActivity(Member $memberBonus $bonus): ActivityFeed
  56.     {
  57.         return $this->createEntityByMember($memberActivityFeedType::COIN_BONUS_STARTED)->setSpecs([
  58.             'id' => $bonus->getId(),
  59.             'start' => $bonus->getBonusBegin()->format(\DateTime::ATOM),
  60.             'end' => $bonus->getBonusEnd()->format(\DateTime::ATOM),
  61.             'percent' => $bonus->getPercent(),
  62.         ]);
  63.     }
  64.     public function createGuestbookEntryActivity(GuestbookEntry $entry): ActivityFeed
  65.     {
  66.         if ($entry->getIsHidden()) {
  67.             $type ActivityFeedType::GUESTBOOK_ENTRY_RECEIVED;
  68.             $recipient $entry->getRecipient();
  69.             $sender $entry->getSender();
  70.         } else {
  71.             $type ActivityFeedType::GUESTBOOK_ENTRY_PUBLISHED;
  72.             $recipient null;
  73.             $sender $entry->getRecipient();
  74.         }
  75.         $af $this->createEntityByMember($sender$type);
  76.         $af->setGuestbookEntry($entry)
  77.             ->setCreatedAt($entry->getCreatedAt())
  78.             ->setVisibleTo($recipient);
  79.         return $af;
  80.     }
  81.     /**
  82.      * Keeps ActivityItem state in sync with GuestbookEntry isHidden()/isSpam() state.
  83.      *
  84.      * @throws \Doctrine\ORM\ORMException
  85.      * @throws \Doctrine\ORM\OptimisticLockException
  86.      */
  87.     public function syncGuestbookActivity(GuestbookEntry $entrybool $flush true)
  88.     {
  89.         // There may be a ActivityFeedType::GUESTBOOK_ENTRY or ActivityFeedType::GUESTBOOK_ENTRY_COMMENT present
  90.         $presentFeedItems $this->repo->findByGuestbookEntry($entry->getId());
  91.         $activityCount count($presentFeedItems);
  92.         if ($activityCount && ($entry->getIsHidden() || $entry->getIsSpam())) {
  93.             foreach ($presentFeedItems as $activity) {
  94.                 $this->deleteEntity($activity->getId(), $flush);
  95.             }
  96.             return;
  97.         }
  98.         if (=== $activityCount) {
  99.             return;
  100.         }
  101.         if (false === $entry->getIsHidden() && false === $entry->getIsSpam()) {
  102.             if (=== $activityCount) {
  103.                 $newItem $this->createGuestbookEntryActivity($entry);
  104.                 if (null !== $entry->getComment()) {
  105.                     $newItem $this->createGuestbookCommentActivity($entry);
  106.                     $this->storeEntity($newItem$flush);
  107.                 }
  108.                 $this->storeEntity($newItem$flush);
  109.                 return;
  110.             }
  111.             $activity array_pop($presentFeedItems);
  112.             // Activity item for guestbook comment is missing
  113.             if ($activity->isGuestbookEntry() && null !== $entry->getCommentedAt()) {
  114.                 $newItem $this->createGuestbookCommentActivity($entry);
  115.                 $this->storeEntity($newItem$flush);
  116.                 return;
  117.             }
  118.             // Activity item for guestbook entry is missing - probably because of error
  119.             if ($activity->isGuestbookComment()) {
  120.                 $newItem $this->createGuestbookEntryActivity($entry);
  121.                 $this->storeEntity($newItem$flush);
  122.             }
  123.         }
  124.     }
  125.     public function createGuestbookCommentActivity(GuestbookEntry $entry): ActivityFeed
  126.     {
  127.         $af $this->createEntityByMember($entry->getRecipient(), ActivityFeedType::GUESTBOOK_ENTRY_COMMENT);
  128.         if (null !== $entry->getCommentedAt()) {
  129.             $createdAt = clone $entry->getCommentedAt();
  130.         } else {
  131.             $createdAt = clone $entry->getCreatedAt();
  132.             $createdAt->modify('+1 minute');
  133.         }
  134.         $af->setGuestbookEntry($entry)
  135.             ->setCreatedAt($createdAt)
  136.             ->setVisibleTo($entry->getSender());
  137.         return $af;
  138.     }
  139.     public function createBlogEntryActivity(BlogEntry $blog): ?ActivityFeed
  140.     {
  141.         $af $this->createEntityByMember($blog->getMember(), ActivityFeedType::BLOG_POST);
  142.         $af->setBlogEntry($blog)
  143.            ->setCreatedAt($blog->getPublishAt());
  144.         return $af;
  145.     }
  146.     public function createContentCommentActivity(ContentComment $comment): ?ActivityFeed
  147.     {
  148.         if (null === $content $comment->getContent()) {
  149.             return null;
  150.         }
  151.         $af $this->createEntityByMember($comment->getMember(), ActivityFeedType::CONTENT_COMMENT);
  152.         if (null !== $comment->getCreatedAt()) {
  153.             $createdAt = clone $comment->getCreatedAt();
  154.         } else {
  155.             // fallback on content create time when no comment createdAt is given
  156.             $createdAt = clone $content->getCreatedAt();
  157.         }
  158.         $af->setContent($comment->getContent())
  159.             ->setContentComment($comment)
  160.             ->setCreatedAt($createdAt);
  161.         return $af;
  162.     }
  163.     public function createFriendshipAskActivity(Member $fromMember $visibleTo): ActivityFeed
  164.     {
  165.         return $this
  166.             ->createEntityByMember($fromActivityFeedType::FRIENDSHIP_ASK)
  167.             ->setVisibleTo($visibleTo);
  168.     }
  169.     public function createContentPublishedActivity(Member $fromContent $content): ActivityFeed
  170.     {
  171.         return $this
  172.             ->createEntityByMember($fromActivityFeedType::CONTENT_PUBLISHED)
  173.             ->setContent($content);
  174.     }
  175.     public function createContentVideoPublishedActivity(Member $fromContent $content): ?ActivityFeed
  176.     {
  177.         if (!$content->isVideo()) {
  178.             return null;
  179.         }
  180.         return $this
  181.             ->createEntityByMember($fromActivityFeedType::CONTENT_PUBLISHED_VIDEO)
  182.             ->setContent($content);
  183.     }
  184.     public function createContentImagesetPublishedActivity(Member $fromContent $content): ?ActivityFeed
  185.     {
  186.         if (!$content->isImageset()) {
  187.             return null;
  188.         }
  189.         return $this
  190.             ->createEntityByMember($fromActivityFeedType::CONTENT_PUBLISHED_IMAGESET)
  191.             ->setContent($content);
  192.     }
  193.     public function createContentPurchaseActivity(Member $purchasedByContent $content): ActivityFeed
  194.     {
  195.         return $this->createEntityByMember($purchasedByActivityFeedType::CONTENT_PURCHASE)->setContent($content);
  196.     }
  197.     public function createMemberActivityActivity(Member $memberWhoIsActive): ActivityFeed
  198.     {
  199.         return $this->createEntityByMember($memberWhoIsActiveActivityFeedType::MEMBER_ACTIVITY);
  200.     }
  201.     public function createContentVideoPurchaseActivity(Member $purchasedByContent $content): ?ActivityFeed
  202.     {
  203.         if (!$content->isVideo()) {
  204.             return null;
  205.         }
  206.         return $this->createEntityByMember($purchasedByActivityFeedType::CONTENT_PURCHASE_VIDEO)->setContent($content);
  207.     }
  208.     public function createContentImagesetPurchaseActivity(Member $purchasedByContent $content): ?ActivityFeed
  209.     {
  210.         if (!$content->isImageset()) {
  211.             return null;
  212.         }
  213.         return $this->createEntityByMember($purchasedByActivityFeedType::CONTENT_PURCHASE_IMAGESET)->setContent($content);
  214.     }
  215.     public function createLoginActivity(Member $from): ActivityFeed
  216.     {
  217.         $activity $this->createEntityByMember($fromActivityFeedType::LOGIN);
  218.         $activity->setSpecs([
  219.             'livecamUsage' => $this->balanceMemberRepo->getLivecamUsageSumByMember($from),
  220.         ]);
  221.         return $activity;
  222.     }
  223.     public function createWebcamOnlineActivity(Member $from): ActivityFeed
  224.     {
  225.         return $this->createEntityByMember($fromActivityFeedType::WEBCAM_ONLINE);
  226.     }
  227.     public function createRegistrationCompletedActivity(Member $from): ActivityFeed
  228.     {
  229.         return $this->createEntityByMember($fromActivityFeedType::REGISTRATION_COMPLETE);
  230.     }
  231.     public function createFriendLoginActivities(Member $loggedInMember, array $friends): array
  232.     {
  233.         $activities = [];
  234.         /** @var Friendship $friendship */
  235.         foreach ($friends as $friendship) {
  236.             $targetMember $friendship->getTargetMember();
  237.             if ($targetMember->getId() === $loggedInMember->getId()) {
  238.                 continue;
  239.             }
  240.             $newActivity $this->createEntityByMember($loggedInMemberActivityFeedType::LOGIN_FRIEND);
  241.             $newActivity->setVisibleTo($targetMember);
  242.             $activities[] = $newActivity;
  243.         }
  244.         return $activities;
  245.     }
  246.     public function createNewMessageActivity(Member $senderMember $recipient): ActivityFeed
  247.     {
  248.         return $this->createEntityByMember($senderActivityFeedType::MESSENGER_NEW_MESSAGE)->setVisibleTo($recipient);
  249.     }
  250.     /**
  251.      * @throws \Exception
  252.      */
  253.     public function getActivityFeedForNonAmateur(Member $memberint $pageint $limit): Paginator
  254.     {
  255.         $qb $this->repo->getActivityFeedForNonAmateurQueryBuilder($member->getId());
  256.         $qb->setMaxResults($limit);
  257.         $qb->setFirstResult($page $limit $limit);
  258.         return new Paginator($qb->getQuery());
  259.     }
  260.     /**
  261.      * @throws \Exception
  262.      */
  263.     public function getActivityFeedForAmateur(Member $memberint $pageint $limit): Paginator
  264.     {
  265.         $qb $this->repo->getActivityFeedForAmateurQueryBuilder($member->getId());
  266.         $qb->setMaxResults($limit);
  267.         $qb->setFirstResult($page $limit $limit);
  268.         return new Paginator($qb->getQuery());
  269.     }
  270.     /**
  271.      * @throws \Exception
  272.      */
  273.     public function getTimelineFeedForMember(Member $memberint $pageint $limit): Paginator
  274.     {
  275.         $qb $this->repo->getTimelineFeedForMemberQueryBuilder($member);
  276.         $qb->setMaxResults($limit);
  277.         $qb->setFirstResult($page $limit $limit);
  278.         return new Paginator($qb->getQuery());
  279.     }
  280.     /**
  281.      * @return Paginator
  282.      *
  283.      * @throws \Exception
  284.      */
  285.     public function getGlobalTimelineFeedData(int $pageint $limit)
  286.     {
  287.         $key = (new \ReflectionClass($this))->getShortName().'_'.__FUNCTION__.'_'.$page.'_'.$limit;
  288.         return $this->cacheService->get($key, function (ItemInterface $item) use ($page$limit) {
  289.             $item->expiresAfter(300);
  290.             $item->tag([
  291.                 'api-timelinefeed-global',
  292.                 'api-timelinefeed-global-'.$page,
  293.             ]);
  294.             $qb $this->repo->getGlobalTimelineFeedQueryBuilder();
  295.             $qb->setMaxResults($limit);
  296.             $qb->setFirstResult($page $limit $limit);
  297.             $paginator = new Paginator($qb->getQuery());
  298.             $options StructOptions::create();
  299.             $options->setGroupFor(ActivityFeed::class, StructGroup::INFOSTREAM);
  300.             $options->setGroupFor(BlogEntry::class, StructGroup::INFOSTREAM);
  301.             $options->skipProperty(Content::class, 'comments');
  302.             $options->skipProperty(Content::class, 'categories');
  303.             $options->skipProperty(Content::class, 'ranking');
  304.             $options->skipProperty(Content::class, 'actors');
  305.             $options->skipProperty(Content::class, 'video');
  306.             $options->skipProperty(Content::class, 'imageset');
  307.             $options->skipProperty(Member::class, 'member_online');
  308.             $options->skipProperty(Member::class, 'profile_values');
  309.             $options->skipProperty(Member::class, 'account');
  310.             $options->skipProperty(ContentVideo::class, 'content');
  311.             return [
  312.                 'data' => $this->structSerializer->multipleToArray($options$paginator),
  313.                 'total' => $paginator->count(),
  314.             ];
  315.         });
  316.     }
  317.     public function applyValuesToEntity(ParameterBag $bagActivityFeed $entity): ActivityFeed
  318.     {
  319.         if ($bag->has('type')) {
  320.             $entity->setType($bag->getInt('type'));
  321.         }
  322.         if ($bag->has('specs')) {
  323.             $entity->setSpecs($bag->get('specs'));
  324.         }
  325.         if ($bag->has('visible_to')) {
  326.             $memberId $bag->getInt('visible_to');
  327.             $member $this->memberService->getMemberById($memberId);
  328.             if (!$member) {
  329.                 throw new NotFoundHttpException('Member with ID '.$memberId.' not found.');
  330.             }
  331.             $entity->setVisibleTo($member);
  332.         }
  333.         return $entity;
  334.     }
  335.     /**
  336.      * @throws \Doctrine\ORM\ORMException
  337.      * @throws \Doctrine\ORM\OptimisticLockException
  338.      */
  339.     public function storeActivity(ActivityFeed $entity): ActivityFeed
  340.     {
  341.         return $this->storeEntity($entity);
  342.     }
  343.     public function removeActivityForBlogEntry(BlogEntry $blogEntry): void
  344.     {
  345.         $af $this->getRepository()->findOneBy([
  346.             'blogEntry' => $blogEntry,
  347.         ]);
  348.         if (null !== $af) {
  349.             $this->getRepository()->removeEntity($affalse);
  350.         }
  351.         if ($blogEntry->getActivityFeedId() > 0) {
  352.             $blogEntry->setActivityFeedId(null);
  353.         }
  354.         $this->getRepository()->flush();
  355.     }
  356.     /**
  357.      * @throws \Doctrine\ORM\ORMException
  358.      * @throws \Doctrine\ORM\OptimisticLockException
  359.      */
  360.     public function removeActivitiesForContent(Content $content): int
  361.     {
  362.         /**
  363.          * @var $items ActivityFeed[]
  364.          */
  365.         $items $this->getRepository()->findBy([
  366.             'content' => $content,
  367.         ]);
  368.         $removed 0;
  369.         foreach ($items as $activity) {
  370.             $response $this->removeEntity($activity);
  371.             if ($response->getSuccess()) {
  372.                 ++$removed;
  373.             }
  374.         }
  375.         return $removed;
  376.     }
  377.     public function removeActivitiesForGuestbookEntry(GuestbookEntry $entry): int
  378.     {
  379.         $removed 0;
  380.         $activities $this->getRepository()->findBy(['guestbookEntry' => $entry]);
  381.         /** @var ActivityFeed $activity */
  382.         foreach ($activities as $activity) {
  383.             if ($this->removeEntity($activity)->getSuccess()) {
  384.                 ++$removed;
  385.             }
  386.         }
  387.         return $removed;
  388.     }
  389.     public function canStoreNewActivity(int $memberIdint $activityType\DateTime $since = new \DateTime('-1 minute')): bool
  390.     {
  391.         switch ($activityType) {
  392.             case ActivityFeedType::LOGIN:
  393.                 $maxAllowedSince 1;
  394.                 break;
  395.             default:
  396.                 $maxAllowedSince null;
  397.         }
  398.         if (null === $maxAllowedSince) {
  399.             return true;
  400.         }
  401.         return $this->repo->countActivitiesFromMemberSince($memberId$activityType$since) < $maxAllowedSince;
  402.     }
  403.     public function getProfileVisitActivities(array $memberIds, ?\DateTime $fromDateTime null): array
  404.     {
  405.         $activities $this->repo->getProfileVisitActivities($memberIds$fromDateTime);
  406.         $this->limitMaxElementCount($activitiesself::MAX_PROFILE_VISIT_ACTIVITIES);
  407.         return $activities;
  408.     }
  409.     public function getMemberInfostreamActivities(array $memberActivityIds = []): array
  410.     {
  411.         $memberIds array_keys($memberActivityIds);
  412.         $memberIsAmateurMap $this->memberService->getMembersAmateurStatus($memberIds);
  413.         $activities = [];
  414.         // Not a good idea to do this kind of request for every online member.
  415.         foreach ($memberActivityIds as $memberId => $minActivityId) {
  416.             if ($memberIsAmateurMap[$memberId] ?? false) {
  417.                 $activities[$memberId] = $this->repo->getActivityFeedForAmateurQueryBuilder($memberId$minActivityId)->getQuery()->getResult();
  418.                 continue;
  419.             }
  420.             $activities[$memberId] = $this->repo->getActivityFeedForNonAmateurQueryBuilder($memberId$minActivityId)->getQuery()->getResult();
  421.         }
  422.         return $activities;
  423.     }
  424.     public function getPublicInfostreamActivities(): array
  425.     {
  426.         return $this->repo->getPublicInfostream();
  427.     }
  428.     private function limitMaxElementCount(array &$activitiesint $maxPerMember): void
  429.     {
  430.         $countsByMemberId = [];
  431.         foreach ($activities as $key => $activity) {
  432.             $currentMemberId $activity->getVisibleTo()?->getId();
  433.             if (!$currentMemberId || (isset($countsByMemberId[$currentMemberId]) && $countsByMemberId[$currentMemberId] > $maxPerMember)) {
  434.                 unset($activities[$key]);
  435.                 continue;
  436.             }
  437.             if (!isset($countsByMemberId[$currentMemberId])) {
  438.                 $countsByMemberId[$currentMemberId] = 1;
  439.             } else {
  440.                 ++$countsByMemberId[$currentMemberId];
  441.             }
  442.         }
  443.         $activities array_values($activities);
  444.     }
  445. }