Passed
Push — master ( 419992...af632f )
by
unknown
18:07 queued 08:29
created

SendEventRemindersCommand::sendReminderMessage()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 35
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 18
c 1
b 0
f 0
nc 2
nop 6
dl 0
loc 35
rs 9.6666
1
<?php
2
3
/* For licensing terms, see /license.txt */
4
5
declare(strict_types=1);
6
7
namespace Chamilo\CoreBundle\Command;
8
9
use Chamilo\CoreBundle\Entity\AgendaReminder;
10
use Chamilo\CoreBundle\Entity\Session;
11
use Chamilo\CoreBundle\Entity\User;
12
use Chamilo\CoreBundle\Helpers\MessageHelper;
13
use Chamilo\CoreBundle\Repository\Node\CourseRepository;
14
use Chamilo\CoreBundle\Settings\SettingsManager;
15
use Chamilo\CourseBundle\Entity\CCalendarEvent;
16
use DateTime;
17
use DateTimeZone;
18
use Doctrine\ORM\EntityManagerInterface;
19
use Symfony\Component\Console\Attribute\AsCommand;
20
use Symfony\Component\Console\Command\Command;
21
use Symfony\Component\Console\Input\InputInterface;
22
use Symfony\Component\Console\Input\InputOption;
23
use Symfony\Component\Console\Output\OutputInterface;
24
use Symfony\Component\Console\Style\SymfonyStyle;
25
use Symfony\Contracts\Translation\TranslatorInterface;
26
27
use const PHP_EOL;
28
29
#[AsCommand(
30
    name: 'app:send-event-reminders',
31
    description: 'Send notification messages to users that have reminders from events in their agenda.',
32
)]
33
class SendEventRemindersCommand extends Command
34
{
35
    public function __construct(
36
        private readonly EntityManagerInterface $entityManager,
37
        private readonly SettingsManager $settingsManager,
38
        private readonly CourseRepository $courseRepository,
39
        private readonly TranslatorInterface $translator,
40
        private readonly MessageHelper $messageHelper
41
    ) {
42
        parent::__construct();
43
    }
44
45
    protected function configure(): void
46
    {
47
        $this
48
            ->addOption('debug', null, InputOption::VALUE_NONE, 'Enable debug mode')
49
            ->setHelp('This command sends notifications to users who have pending reminders for calendar events.')
50
        ;
51
    }
52
53
    protected function execute(InputInterface $input, OutputInterface $output): int
54
    {
55
        $io = new SymfonyStyle($input, $output);
56
        $debug = $input->getOption('debug');
57
        $now = new DateTime('now', new DateTimeZone('UTC'));
58
        $initialSentRemindersCount = 0;
59
        $sentRemindersCount = 0;
60
61
        if ($debug) {
62
            error_log('Debug mode activated');
63
            $io->note('Debug mode activated');
64
        }
65
66
        $remindersRepo = $this->entityManager->getRepository(AgendaReminder::class);
67
        $reminders = $remindersRepo->findBy(['sent' => false]);
68
69
        if ($debug) {
70
            error_log('Total reminders fetched: '.\count($reminders));
71
        }
72
73
        $senderId = $this->settingsManager->getSetting('agenda.agenda_reminders_sender_id');
74
        $senderId = (int) $senderId ?: $this->getFirstAdminId();
75
76
        foreach ($reminders as $reminder) {
77
            /** @var CCalendarEvent $event */
78
            $event = $reminder->getEvent();
79
80
            if (null === $event) {
81
                if ($debug) {
82
                    error_log('No event found for reminder ID: '.$reminder->getId());
83
                }
84
85
                continue;
86
            }
87
88
            $eventType = $event->determineType();
89
            $notificationDate = clone $event->getStartDate();
90
            $notificationDate->sub($reminder->getDateInterval());
91
            if ($notificationDate > $now) {
92
                continue;
93
            }
94
95
            // NOTE: keep this call (without user) if you rely on it elsewhere; it does not change behavior.
96
            // We now format per-user inside sendReminderMessage().
97
            $eventDetails = $this->generateEventDetails($event, null);
98
99
            $initialSentRemindersCount = $sentRemindersCount;
100
101
            if ('personal' === $eventType) {
102
                $creator = $event->getResourceNode()->getCreator();
103
                if ($creator) {
104
                    $this->sendReminderMessage($creator, $event, $senderId, $debug, $io, $sentRemindersCount);
105
                }
106
107
                $resourceLinks = $event->getResourceNode()->getResourceLinks();
108
                if (!$resourceLinks->isEmpty()) {
109
                    foreach ($resourceLinks as $link) {
110
                        if ($user = $link->getUser()) {
111
                            $this->sendReminderMessage($user, $event, $senderId, $debug, $io, $sentRemindersCount);
112
                        }
113
                    }
114
                }
115
            } else {
116
                $resourceLink = $event->getResourceNode()->getResourceLinks()->first();
117
                if (!$resourceLink) {
118
                    if ($debug) {
119
                        error_log("No ResourceLink found for event ID: {$event->getIid()}");
120
                    }
121
122
                    continue;
123
                }
124
125
                switch ($eventType) {
126
                    case 'global':
127
                        foreach ($event->getResourceNode()->getResourceLinks() as $link) {
128
                            if ($user = $link->getUser()) {
129
                                $this->sendReminderMessage($user, $event, $senderId, $debug, $io, $sentRemindersCount);
130
                            }
131
                        }
132
133
                        break;
134
135
                    case 'course':
136
                        if ($course = $resourceLink->getCourse()) {
137
                            $users = $this->courseRepository->getSubscribedUsers($course)->getQuery()->getResult();
138
                            foreach ($users as $user) {
139
                                $this->sendReminderMessage($user, $event, $senderId, $debug, $io, $sentRemindersCount);
140
                            }
141
                        }
142
143
                        break;
144
145
                    case 'session':
146
                        if ($session = $resourceLink->getSession()) {
147
                            $course = $resourceLink->getCourse();
148
                            if (!$course) {
149
                                if ($debug) {
150
                                    error_log("No course found for resource link in session ID: {$session->getId()}");
151
                                }
152
153
                                break;
154
                            }
155
156
                            $usersToNotify = [];
157
                            $studentSubscriptions = $session->getSessionRelCourseRelUsersByStatus($course, Session::STUDENT);
158
                            foreach ($studentSubscriptions as $studentSubscription) {
159
                                $usersToNotify[$studentSubscription->getUser()->getId()] = $studentSubscription->getUser();
160
                            }
161
162
                            $coachSubscriptions = $session->getSessionRelCourseRelUsersByStatus($course, Session::COURSE_COACH);
163
                            foreach ($coachSubscriptions as $coachSubscription) {
164
                                $usersToNotify[$coachSubscription->getUser()->getId()] = $coachSubscription->getUser();
165
                            }
166
167
                            $generalCoaches = $session->getGeneralCoaches();
168
                            foreach ($generalCoaches as $generalCoach) {
169
                                $usersToNotify[$generalCoach->getId()] = $generalCoach;
170
                            }
171
172
                            foreach ($usersToNotify as $user) {
173
                                $this->sendReminderMessage($user, $event, $senderId, $debug, $io, $sentRemindersCount);
174
                            }
175
                        }
176
177
                        break;
178
                }
179
            }
180
181
            if ($sentRemindersCount > $initialSentRemindersCount) {
182
                $reminder->setSent(true);
183
                $this->entityManager->persist($reminder);
184
            }
185
        }
186
187
        $this->entityManager->flush();
188
189
        if ($sentRemindersCount > 0) {
190
            $io->success(\sprintf('%d event reminders have been sent successfully.', $sentRemindersCount));
191
        } else {
192
            $io->warning('No event reminders were sent.');
193
        }
194
195
        return Command::SUCCESS;
196
    }
197
198
    /**
199
     * Resolve the DateTimeZone to use for a given user (CLI-safe, no legacy api_* calls).
200
     * Priority: user's timezone -> platform timezone setting -> UTC.
201
     */
202
    private function resolveTimezoneForUser(?User $user): DateTimeZone
203
    {
204
        // User explicit timezone (if present and valid)
205
        $tzId = $user?->getTimezone();
206
        if (\is_string($tzId) && $tzId !== '') {
207
            try {
208
                return new DateTimeZone($tzId);
209
            } catch (\Throwable) {
210
                // keep going
211
            }
212
        }
213
214
        // Platform timezone setting (equivalent to api_get_setting('platform.timezone', false, 'timezones'))
215
        $platformTz = (string) ($this->settingsManager->getSetting('platform.timezone', false, 'timezones') ?? '');
216
        if ($platformTz !== '') {
217
            try {
218
                return new DateTimeZone($platformTz);
219
            } catch (\Throwable) {
220
                // keep going
221
            }
222
        }
223
224
        return new DateTimeZone('UTC');
225
    }
226
227
    /**
228
     * Format a UTC DateTime into the user's local timezone, CLI-safe.
229
     */
230
    private function formatForUser(DateTime $utc, ?User $user): string
231
    {
232
        $dt = (clone $utc);
233
        $dt->setTimezone($this->resolveTimezoneForUser($user));
234
235
        // Keep the existing format as used previously.
236
        return $dt->format('Y-m-d H:i:s');
237
    }
238
239
    private function sendReminderMessage(User $user, CCalendarEvent $event, int $senderId, bool $debug, SymfonyStyle $io, int &$sentRemindersCount): void
240
    {
241
        $locale = $user->getLocale() ?: 'en';
242
        $this->translator->setLocale($locale);
243
244
        $messageSubject = \sprintf(
245
            $this->translator->trans('Reminder for event : %s'),
246
            $event->getTitle()
247
        );
248
249
        // IMPORTANT: build details with user's timezone applied
250
        $messageContent = implode(PHP_EOL, $this->generateEventDetails($event, $user));
251
252
        $this->messageHelper->sendMessage(
253
            $user->getId(),
254
            $messageSubject,
255
            $messageContent,
256
            [],
257
            [],
258
            0,
259
            0,
260
            0,
261
            $senderId,
262
            0,
263
            false,
264
            true
265
        );
266
267
        if ($debug) {
268
            error_log("Message sent to user ID: {$user->getId()} for event: {$event->getTitle()}");
269
            error_log("Message Subject: {$messageSubject}");
270
            error_log("Message Content: {$messageContent}");
271
        }
272
273
        $sentRemindersCount++;
274
    }
275
276
    private function getFirstAdminId(): int
277
    {
278
        $admin = $this->entityManager->getRepository(User::class)->findOneBy([]);
279
280
        return $admin && ($admin->isAdmin() || $admin->isSuperAdmin())
281
            ? $admin->getId()
282
            : 1;
283
    }
284
285
    /**
286
     * Build event details text. If $user is provided, dates are converted to user's local timezone.
287
     * Otherwise, keep UTC (backward compatible path).
288
     */
289
    private function generateEventDetails(CCalendarEvent $event, ?User $user = null): array
290
    {
291
        $details = [];
292
        $details[] = \sprintf('<p><strong>%s</strong></p>', $event->getTitle());
293
294
        if ($event->isAllDay()) {
295
            $details[] = \sprintf('<p class="small">%s</p>', $this->translator->trans('All day'));
296
        } else {
297
            $fromStr = $user
298
                ? $this->formatForUser($event->getStartDate(), $user)
299
                : $event->getStartDate()->format('Y-m-d H:i:s');
300
301
            $details[] = \sprintf(
302
                '<p class="small">%s</p>',
303
                $this->translator->trans('From %s', ['%s' => $fromStr])
304
            );
305
306
            if ($event->getEndDate()) {
307
                $untilStr = $user
308
                    ? $this->formatForUser($event->getEndDate(), $user)
309
                    : $event->getEndDate()->format('Y-m-d H:i:s');
310
311
                $details[] = \sprintf(
312
                    '<p class="small">%s</p>',
313
                    $this->translator->trans('Until %s', ['%s' => $untilStr])
314
                );
315
            }
316
        }
317
318
        if ($event->getContent()) {
319
            $cleanContent = strip_tags($event->getContent());
320
            $details[] = \sprintf('<p>%s</p>', $cleanContent);
321
        }
322
323
        return $details;
324
    }
325
}
326