Skip to content

Commit ce945d0

Browse files
committed
feat: add scheduled messages BG job
Signed-off-by: Anna Larch <anna@nextcloud.com>
1 parent e91534f commit ce945d0

7 files changed

Lines changed: 224 additions & 7 deletions

File tree

appinfo/info.xml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
* 🌉 **Sync with other chat solutions** With [Matterbridge](https://github.com/42wim/matterbridge/) being integrated in Talk, you can easily sync a lot of other chat solutions to Nextcloud Talk and vice-versa.
1919
]]></description>
2020

21-
<version>23.0.0-dev.3</version>
21+
<version>23.0.0-dev.4</version>
2222
<licence>agpl</licence>
2323

2424
<author>Anna Larch</author>
@@ -78,6 +78,7 @@
7878
<job>OCA\Talk\BackgroundJob\RemoveEmptyRooms</job>
7979
<job>OCA\Talk\BackgroundJob\ResetAssignedSignalingServer</job>
8080
<job>OCA\Talk\BackgroundJob\RetryNotificationsJob</job>
81+
<job>OCA\Talk\BackgroundJob\SendScheduledMessages</job>
8182
</background-jobs>
8283

8384
<repair-steps>
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\Talk\BackgroundJob;
11+
12+
use OCA\Talk\AppInfo\Application;
13+
use OCA\Talk\Chat\ChatManager;
14+
use OCA\Talk\Chat\MessageParser;
15+
use OCA\Talk\Exceptions\ParticipantNotFoundException;
16+
use OCA\Talk\Exceptions\RoomNotFoundException;
17+
use OCA\Talk\Manager;
18+
use OCA\Talk\Model\Attendee;
19+
use OCA\Talk\Model\ScheduledMessage;
20+
use OCA\Talk\Model\Thread;
21+
use OCA\Talk\Participant;
22+
use OCA\Talk\Service\ParticipantService;
23+
use OCA\Talk\Service\ScheduledMessageService;
24+
use OCA\Talk\Service\ThreadService;
25+
use OCP\AppFramework\Utility\ITimeFactory;
26+
use OCP\BackgroundJob\TimedJob;
27+
use OCP\Comments\MessageTooLongException;
28+
use OCP\Comments\NotFoundException;
29+
use OCP\IL10N;
30+
use OCP\L10N\IFactory;
31+
use Psr\Log\LoggerInterface;
32+
33+
/**
34+
* Class SendScheduledMessages
35+
*
36+
* @package OCA\Talk\BackgroundJob
37+
*/
38+
class SendScheduledMessages extends TimedJob {
39+
private readonly IL10N $l10n;
40+
public function __construct(
41+
private ScheduledMessageService $scheduledMessageService,
42+
private ParticipantService $participantService,
43+
private Manager $manager,
44+
private ChatManager $chatManager,
45+
private MessageParser $messageParser,
46+
private ThreadService $threadService,
47+
private IFactory $l10nFactory,
48+
private LoggerInterface $logger,
49+
ITimeFactory $time,
50+
) {
51+
// Every minute
52+
$this->setInterval(60);
53+
$this->l10n = $this->l10nFactory->get(Application::APP_ID);
54+
parent::__construct($time);
55+
}
56+
57+
/**
58+
* @inheritDoc
59+
*/
60+
#[\Override]
61+
protected function run($argument): void {
62+
$messages = $this->scheduledMessageService->getDue($this->time->getDateTime());
63+
if (empty($messages)) {
64+
return;
65+
}
66+
67+
$rooms = [];
68+
/** @var ScheduledMessage $message */
69+
foreach ($messages as $message) {
70+
if (!isset($rooms[$message->getRoomId()])) {
71+
try {
72+
$rooms[$message->getRoomId()] = $this->manager->getRoomById($message->getRoomId());
73+
} catch (RoomNotFoundException) {
74+
// This shouldn't happen
75+
// What to do, what to do
76+
continue;
77+
}
78+
}
79+
80+
$room = $rooms[$message->getRoomId()];
81+
try {
82+
$participant = $this->participantService->getParticipantByActor($room, $message->getActorType(), $message->getActorId());
83+
} catch (ParticipantNotFoundException) {
84+
$this->scheduledMessageService->deleteMessage($room, (int)$message->getId(), $message->getActorType(), $message->getActorId());
85+
continue;
86+
}
87+
88+
if ($room->isFederatedConversation()) {
89+
// skip for now
90+
continue;
91+
}
92+
93+
if (($participant->getPermissions() & Attendee::PERMISSIONS_CHAT) === 0) {
94+
$this->scheduledMessageService->deleteMessage($room, (int)$message->getId(), $message->getActorType(), $message->getActorId());
95+
continue;
96+
}
97+
98+
if ($room->getReadOnly()) {
99+
$this->scheduledMessageService->deleteMessage($room, (int)$message->getId(), $message->getActorType(), $message->getActorId());
100+
continue;
101+
}
102+
103+
$parent = $parentMessage = null;
104+
if ($message->getParentId() !== 0) {
105+
try {
106+
$parent = $this->chatManager->getParentComment($room, (string)$message->getParentId());
107+
} catch (NotFoundException $e) {
108+
// Log and continue or delete?
109+
continue;
110+
}
111+
112+
$parentMessage = $this->messageParser->createMessage($room, $participant, $parent, $this->l10n);
113+
$this->messageParser->parseMessage($parentMessage);
114+
if (!$parentMessage->isReplyable()) {
115+
// Log and continue or delete?
116+
continue;
117+
}
118+
} elseif ($message->getThreadId() > 0) {
119+
if (!$this->threadService->validateThread($room->getId(), $message->getThreadId())) {
120+
$message->setThreadId(0);
121+
}
122+
}
123+
124+
$this->participantService->ensureOneToOneRoomIsFilled($room);
125+
126+
try {
127+
$metaData = $message->getDecodedMetaData();
128+
$threadId = $message->getThreadId();
129+
$threadTitle = $metaData[ScheduledMessage::METADATA_THREAD_TITLE];
130+
$comment = $this->chatManager->sendMessage($room,
131+
$participant,
132+
$message->getActorType(),
133+
$message->getActorId(),
134+
$message->getMessage(),
135+
$this->time->getDateTime(),
136+
$parent,
137+
'',
138+
$metaData[ScheduledMessage::METADATA_SILENT] ?? false,
139+
threadId: $threadId);
140+
if ($threadId === Thread::THREAD_CREATE && $threadTitle !== '') {
141+
$thread = $this->threadService->createThread($room, (int)$comment->getId(), $threadTitle);
142+
// Add to subscribed threads list
143+
$this->threadService->setNotificationLevel($participant->getAttendee(), $thread->getId(), Participant::NOTIFY_DEFAULT);
144+
145+
$this->chatManager->addSystemMessage(
146+
$room,
147+
$participant,
148+
$participant->getAttendee()->getActorType(),
149+
$participant->getAttendee()->getActorId(),
150+
json_encode(['message' => 'thread_created', 'parameters' => ['thread' => (int)$comment->getId(), 'title' => $thread->getName()]]),
151+
$this->time->getDateTime(),
152+
false,
153+
null,
154+
$comment,
155+
true,
156+
true
157+
);
158+
}
159+
} catch (MessageTooLongException) {
160+
continue;
161+
} catch (\Exception $e) {
162+
$this->logger->warning($e->getMessage());
163+
continue;
164+
}
165+
166+
$this->scheduledMessageService->deleteMessage($room, (int)$message->getId(), $message->getActorType(), $message->getActorId());
167+
}
168+
}
169+
}

lib/Controller/ChatController.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -579,7 +579,8 @@ public function deleteScheduleMessage(int $messageId): DataResponse {
579579
$deleted = $this->scheduledMessageManager->deleteMessage(
580580
$this->room,
581581
$messageId,
582-
$this->participant,
582+
$this->participant->getAttendee()->getActorType(),
583+
$this->participant->getAttendee()->getActorId()
583584
);
584585

585586
if ($deleted === 0) {
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
/**
5+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
6+
* SPDX-License-Identifier: AGPL-3.0-or-later
7+
*/
8+
9+
namespace OCA\Talk\Listener;
10+
11+
use OCA\Talk\Events\RoomDeletedEvent;
12+
use OCA\Talk\Service\ParticipantService;
13+
use OCA\Talk\Service\ScheduledMessageService;
14+
use OCP\EventDispatcher\Event;
15+
use OCP\EventDispatcher\IEventListener;
16+
17+
/**
18+
* @template-implements IEventListener<Event>
19+
*/
20+
class DeleteScheduledMessagesListener implements IEventListener {
21+
public function __construct(
22+
protected readonly ScheduledMessageService $scheduledMessageService,
23+
protected readonly ParticipantService $participantService,
24+
) {
25+
}
26+
27+
#[\Override]
28+
public function handle(Event $event): void {
29+
if ($event instanceof RoomDeletedEvent) {
30+
$participants = $this->participantService->getParticipantsForRoom($event->getRoom());
31+
foreach ($participants as $participant) {
32+
$this->scheduledMessageService->deleteByActor($participant->getAttendee()->getActorType(), $participant->getAttendee()->getActorId());
33+
}
34+
}
35+
}
36+
}

lib/Model/ScheduledMessage.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,11 @@ public function getDecodedMetaData(): array {
7575
return json_decode($this->metaData, true, 512, JSON_THROW_ON_ERROR);
7676
}
7777

78+
public function getThreadTitle(): string {
79+
$metaData = $this->getDecodedMetaData();
80+
return $metaData[self::METADATA_THREAD_TITLE] ?? '';
81+
}
82+
7883
/**
7984
* @param array{silent: bool, threadId: int, threadTitle?: string, lastEditedTime?: int} $metaData
8085
*/
@@ -111,7 +116,7 @@ public function toArray(string $format, ?Message $parent, ?Thread $thread) : arr
111116
'messageType' => $this->getMessageType(),
112117
'createdAt' => $this->getCreatedAt()->getTimestamp(),
113118
'sendAt' => $this->getSendAt()?->getTimestamp() ?? 0,
114-
'silent' => $metaData['silent'] ?? false,
119+
'silent' => $metaData[self::METADATA_SILENT] ?? false,
115120
];
116121

117122
if ($parent !== null) {

lib/Model/ScheduledMessageMapper.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,8 @@ public function getMessagesDue(\DateTime $dateTime): array {
123123
$query = $this->db->getQueryBuilder();
124124
$query->select('*')
125125
->from($this->getTableName())
126-
->where($query->expr()->lt('send_at', $query->createNamedParameter($dateTime, IQueryBuilder::PARAM_DATETIME_MUTABLE)));
126+
->where($query->expr()->lt('send_at', $query->createNamedParameter($dateTime, IQueryBuilder::PARAM_DATETIME_MUTABLE)))
127+
->orderBy('send_at', 'ASC');
127128

128129
return $this->findEntities($query);
129130
}

lib/Service/ScheduledMessageService.php

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,12 +72,12 @@ public function scheduleMessage(
7272
return $scheduledMessage;
7373
}
7474

75-
public function deleteMessage(Room $chat, int $id, Participant $participant): int {
75+
public function deleteMessage(Room $chat, int $id, string $actorType, string $actorId): int {
7676
return $this->scheduledMessageMapper->deleteById(
7777
$chat,
7878
$id,
79-
$participant->getAttendee()->getActorType(),
80-
$participant->getAttendee()->getActorId()
79+
$actorType,
80+
$actorId,
8181
);
8282
}
8383

@@ -197,4 +197,8 @@ public function getScheduledMessageCount(Room $chat, Participant $participant):
197197
$participant->getAttendee()->getActorId(),
198198
);
199199
}
200+
201+
public function getDue(\DateTime $getDateTime): array {
202+
return $this->scheduledMessageMapper->getMessagesDue($getDateTime);
203+
}
200204
}

0 commit comments

Comments
 (0)