Lines 90.47% 19 / 21
Functions and Methods 75.00% 6 / 8
Classes and Traits 0.00% 0 / 1
Name Lines Functions and Methods CRAP Classes and Traits
ChatActivity 90.47% 19 / 21 75.00% 6 / 8 14.17 0.00% 0 / 1
 __construct 100.00% 1 / 1 100.00% 1 / 1 1
 userRegistered 100.00% 1 / 1 100.00% 1 / 1 1
 statsUploaded 100.00% 7 / 7 100.00% 1 / 1 3
 statsImported 75.00% 3 / 4 0.00% 0 / 1 3.14
 send 80.00% 4 / 5 0.00% 0 / 1 3.07
 reset 100.00% 1 / 1 100.00% 1 / 1 1
 agent 100.00% 1 / 1 100.00% 1 / 1 1
 userUrl 100.00% 1 / 1 100.00% 1 / 1 1
1<?php
2
3declare(strict_types=1);
4
5namespace App\Notifier;
6
7use App\Entity\AgentStat;
8use App\Entity\User;
9use App\Repository\AgentStatRepository;
10use Psr\Log\LoggerInterface;
11use Symfony\Component\Console\ConsoleEvents;
12use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
13use Symfony\Component\HttpKernel\KernelEvents;
14use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
15use Symfony\Contracts\Service\ResetInterface;
16
17/**
18 * Posts the site's activity to Slack and Telegram (ChatFeed::Activity): new users, stats uploads
19 * and CSV imports. The messages are sent after the response (kernel.terminate), so a chat never
20 * slows a request down; a failure is only logged as a warning, below the level ChatLogHandler posts.
21 */
22final class ChatActivity implements ResetInterface
23{
24    /** @var list<ChatText> */
25    private array $messages = [];
26
27    public function __construct(
28        private readonly ChatSender $chatSender,
29        private readonly UrlGeneratorInterface $urlGenerator,
30        private readonly AgentStatRepository $agentStatRepository,
31        private readonly LoggerInterface $logger,
32    ) {
33    }
34
35    public function userRegistered(User $user): void
36    {
37        $this->messages[] = new ChatText('👋 New user ')->link($user->getIdentifier(), $this->userUrl($user));
38    }
39
40    /**
41     * Stats uploaded through the API (the app); imports are reported with statsImported().
42     */
43    public function statsUploaded(AgentStat $agentStat): void
44    {
45        $previous = $this->agentStatRepository->findPrevious($agentStat);
46
47        $this->messages[] = $this->agent(new ChatText('📊 '), $agentStat->owner)->text(\sprintf(
48            ' uploaded stats: %sAP %s%s',
49            null === $agentStat->level ? '' : 'level '.$agentStat->level.', ',
50            \number_format($agentStat->ap),
51            $previous instanceof AgentStat ? \sprintf(' (+%s)', \number_format($agentStat->ap - $previous->ap)) : ' (first upload)',
52        ));
53    }
54
55    public function statsImported(User $owner, int $imported): void
56    {
57        if ($imported < 1) {
58            return;
59        }
60
61        $this->messages[] = $this->agent(new ChatText('📥 '), $owner)
62            ->text(\sprintf(' imported %d stats %s', $imported, 1 === $imported ? 'upload' : 'uploads'));
63    }
64
65    #[AsEventListener(KernelEvents::TERMINATE)]
66    #[AsEventListener(ConsoleEvents::TERMINATE)]
67    public function send(): void
68    {
69        $messages = $this->messages;
70        $this->messages = [];
71
72        foreach ($messages as $message) {
73            foreach ($this->chatSender->send(ChatFeed::Activity, $message) as $transport => $exception) {
74                $this->logger->warning('{transport}: {message} was not sent: {error}', ['transport' => $transport, 'message' => $message->plain(), 'error' => $exception->getMessage()]);
75            }
76        }
77    }
78
79    public function reset(): void
80    {
81        $this->messages = [];
82    }
83
84    /** The agent name, linked to the admins' page of the user. */
85    private function agent(ChatText $text, User $user): ChatText
86    {
87        return $text->link($user->getAgentName() ?? $user->getIdentifier(), $this->userUrl($user));
88    }
89
90    private function userUrl(User $user): string
91    {
92        return $this->urlGenerator->generate('app_user_show', ['id' => $user->id], UrlGeneratorInterface::ABSOLUTE_URL);
93    }
94}