From ef3265c9c7efe12982394965c0429489e6c9ef0d Mon Sep 17 00:00:00 2001 From: Anna Larch Date: Sat, 8 Nov 2025 12:58:40 +0100 Subject: [PATCH 1/5] feat: scheduled message API Signed-off-by: Anna Larch --- appinfo/info.xml | 2 +- docs/capabilities.md | 1 + lib/Capabilities.php | 2 + lib/Chat/Listener.php | 3 + lib/Controller/ChatController.php | 258 +++++ lib/Controller/RoomController.php | 3 +- lib/Listener/UserDeletedListener.php | 4 + .../Version23000Date20251105125333.php | 91 ++ lib/Model/Attendee.php | 5 + lib/Model/Message.php | 1 + lib/Model/ScheduledMessage.php | 145 +++ lib/Model/ScheduledMessageMapper.php | 137 +++ lib/Model/SelectHelper.php | 27 + lib/Participant.php | 9 + lib/ResponseDefinitions.php | 26 + lib/Service/ParticipantService.php | 6 + lib/Service/RoomFormatter.php | 2 + lib/Service/ScheduledMessageService.php | 200 ++++ openapi-backend-sipbridge.json | 7 +- openapi-federation.json | 7 +- openapi-full.json | 1019 ++++++++++++++++- openapi.json | 1019 ++++++++++++++++- .../openapi/openapi-backend-sipbridge.ts | 2 + src/types/openapi/openapi-federation.ts | 2 + src/types/openapi/openapi-full.ts | 475 ++++++++ src/types/openapi/openapi.ts | 475 ++++++++ .../features/bootstrap/FeatureContext.php | 134 +++ .../chat-4/scheduled-messages.feature | 111 ++ .../lib/Controller/ApiController.php | 3 + tests/php/Controller/ChatControllerTest.php | 26 +- 30 files changed, 4181 insertions(+), 21 deletions(-) create mode 100644 lib/Migration/Version23000Date20251105125333.php create mode 100644 lib/Model/ScheduledMessage.php create mode 100644 lib/Model/ScheduledMessageMapper.php create mode 100644 lib/Service/ScheduledMessageService.php create mode 100644 tests/integration/features/chat-4/scheduled-messages.feature diff --git a/appinfo/info.xml b/appinfo/info.xml index 72bdcbd1c20..40d1e52ef27 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -18,7 +18,7 @@ * 🌉 **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. ]]> - 23.0.0-dev.2 + 23.0.0-dev.3 agpl Anna Larch diff --git a/docs/capabilities.md b/docs/capabilities.md index e9a377011a5..fcb35a7efd0 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -202,3 +202,4 @@ * `pinned-messages` - Whether messages can be pinned * `federated-shared-items` - Whether shared items endpoints can be called in a federated conversation * `config => chat => style` (local) - User selected chat style (split or unified for now) +* `scheduled-messages` (local) - Whether a user can schedule messages diff --git a/lib/Capabilities.php b/lib/Capabilities.php index a46a19711b0..b31e0a38032 100644 --- a/lib/Capabilities.php +++ b/lib/Capabilities.php @@ -126,6 +126,7 @@ class Capabilities implements IPublicCapability { 'threads', 'pinned-messages', 'federated-shared-items', + 'scheduled-messages', ]; public const CONDITIONAL_FEATURES = [ @@ -156,6 +157,7 @@ class Capabilities implements IPublicCapability { 'mutual-calendar-events', 'upcoming-reminders', 'sensitive-conversations', + 'scheduled-messages', ]; public const LOCAL_CONFIGS = [ diff --git a/lib/Chat/Listener.php b/lib/Chat/Listener.php index c902ef66ca7..7e75ae00ce5 100644 --- a/lib/Chat/Listener.php +++ b/lib/Chat/Listener.php @@ -10,6 +10,7 @@ namespace OCA\Talk\Chat; use OCA\Talk\Events\RoomDeletedEvent; +use OCA\Talk\Model\ScheduledMessageMapper; use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventListener; @@ -19,6 +20,7 @@ class Listener implements IEventListener { public function __construct( protected ChatManager $chatManager, + protected ScheduledMessageMapper $scheduledMessageMapper, ) { } @@ -26,6 +28,7 @@ public function __construct( public function handle(Event $event): void { if ($event instanceof RoomDeletedEvent) { $this->chatManager->deleteMessages($event->getRoom()); + $this->scheduledMessageMapper->deleteMessagesByRoom($event->getRoom()); } } } diff --git a/lib/Controller/ChatController.php b/lib/Controller/ChatController.php index 11cf11c9759..c2f81294fb8 100644 --- a/lib/Controller/ChatController.php +++ b/lib/Controller/ChatController.php @@ -35,6 +35,7 @@ use OCA\Talk\Model\Bot; use OCA\Talk\Model\Message; use OCA\Talk\Model\Reminder; +use OCA\Talk\Model\ScheduledMessage; use OCA\Talk\Model\Session; use OCA\Talk\Model\Thread; use OCA\Talk\Participant; @@ -47,6 +48,7 @@ use OCA\Talk\Service\ProxyCacheMessageService; use OCA\Talk\Service\ReminderService; use OCA\Talk\Service\RoomFormatter; +use OCA\Talk\Service\ScheduledMessageService; use OCA\Talk\Service\SessionService; use OCA\Talk\Service\ThreadService; use OCA\Talk\Share\Helper\Preloader; @@ -93,6 +95,7 @@ * @psalm-import-type TalkChatReminderUpcoming from ResponseDefinitions * @psalm-import-type TalkRichObjectParameter from ResponseDefinitions * @psalm-import-type TalkRoom from ResponseDefinitions + * @psalm-import-type TalkScheduledMessage from ResponseDefinitions */ class ChatController extends AEnvironmentAwareOCSController { /** @var string[] */ @@ -135,6 +138,7 @@ public function __construct( protected ITaskProcessingManager $taskProcessingManager, protected IAppConfig $appConfig, protected LoggerInterface $logger, + protected ScheduledMessageService $scheduledMessageManager, ) { parent::__construct($appName, $request); } @@ -332,6 +336,260 @@ public function sendMessage(string $message, string $actorDisplayName = '', stri return $this->parseCommentToResponse($comment, $parentMessage); } + /** + * Get all scheduled messages of a given room and participant + * + * The author and timestamp are automatically set to the current user + * and time. + * + * Required capability: `scheduled-messages` + * + * @return DataResponse, array{}>|DataResponse + * + * 200: All scheduled messages for this room and participant + * 404: Actor not found + */ + #[NoAdminRequired] + #[RequireModeratorOrNoLobby] + #[RequireParticipant] + #[RequirePermission(permission: RequirePermission::CHAT)] + #[RequireReadWriteConversation] + #[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/chat/{token}/schedule', requirements: [ + 'apiVersion' => '(v1)', + 'token' => '[a-z0-9]{4,30}', + ])] + public function getScheduledMessages(): DataResponse { + if ($this->participant->isSelfJoinedOrGuest()) { + return new DataResponse(['error' => 'actor'], Http::STATUS_NOT_FOUND); + } + + $scheduledMessages = $this->scheduledMessageManager->getMessages( + $this->room, + $this->participant, + ); + + return new DataResponse($scheduledMessages, Http::STATUS_OK); + } + + /** + * Schedules the sending of a new chat message to the given room + * + * The author and timestamp are automatically set to the current user + * and time. + * + * Required capability: `scheduled-messages` + * + * @param string $message The message to send + * @param int $sendAt When to send the scheduled message + * @param int $replyTo Parent id which this scheduled message is a reply to + * @psalm-param non-negative-int $replyTo + * @param bool $silent If sent silent the scheduled message will not create any notifications when sent + * @param string $threadTitle Only supported when not replying, when given will create a thread (requires `threads` capability) + * @param int $threadId Thread id without quoting a specific message (requires `threads` capability) + * @return DataResponse|DataResponse|DataResponse|DataResponse + * + * 201: Message scheduled successfully + * 400: Scheduling the message is not possible + * 404: Actor not found + * 413: Message too long + */ + #[NoAdminRequired] + #[RequireModeratorOrNoLobby] + #[RequireParticipant] + #[RequirePermission(permission: RequirePermission::CHAT)] + #[RequireReadWriteConversation] + #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/chat/{token}/schedule', requirements: [ + 'apiVersion' => '(v1)', + 'token' => '[a-z0-9]{4,30}', + ])] + public function scheduleMessage( + string $message, + int $sendAt, + int $replyTo = 0, + bool $silent = false, + string $threadTitle = '', + int $threadId = 0, + ): DataResponse { + if ($this->participant->isSelfJoinedOrGuest()) { + return new DataResponse(['error' => 'actor'], Http::STATUS_NOT_FOUND); + } + + if ($sendAt <= $this->timeFactory->getTime()) { + return new DataResponse(['error' => 'send-at'], Http::STATUS_BAD_REQUEST); + } + + if (trim($message) === '') { + return new DataResponse(['error' => 'message'], Http::STATUS_BAD_REQUEST); + } + + $parent = $parentMessage = null; + if ($replyTo !== 0) { + try { + $parent = $this->chatManager->getParentComment($this->room, (string)$replyTo); + } catch (NotFoundException $e) { + // Someone is trying to reply cross-rooms or to a non-existing message + return new DataResponse(['error' => 'reply-to'], Http::STATUS_BAD_REQUEST); + } + + $parentMessage = $this->messageParser->createMessage($this->room, $this->participant, $parent, $this->l); + $this->messageParser->parseMessage($parentMessage); + if (!$parentMessage->isReplyable()) { + return new DataResponse(['error' => 'reply-to'], Http::STATUS_BAD_REQUEST); + } + } + + if ($threadId !== 0 && !$this->threadService->validateThread($this->room->getId(), $threadId)) { + return new DataResponse(['error' => 'reply-to'], Http::STATUS_BAD_REQUEST); + } + + $sendAtDateTime = $this->timeFactory->getDateTime('@' . $sendAt, new \DateTimeZone('UTC')); + try { + $createThread = $replyTo === 0 && $threadId === Thread::THREAD_NONE && $threadTitle !== ''; + $threadId = $createThread ? Thread::THREAD_CREATE : $threadId; + $scheduledMessage = $this->scheduledMessageManager->scheduleMessage( + $this->room, + $this->participant, + $message, + ChatManager::VERB_MESSAGE, + $parent, + $threadId, + $sendAtDateTime, + [ + ScheduledMessage::METADATA_THREAD_TITLE => $threadTitle, + ScheduledMessage::METADATA_SILENT => $silent, + ScheduledMessage::METADATA_THREAD_ID => $threadId, + ] + ); + $this->participantService->setHasScheduledMessages($this->participant, true); + } catch (MessageTooLongException) { + return new DataResponse(['error' => 'message'], Http::STATUS_REQUEST_ENTITY_TOO_LARGE); + } + + $data = $this->scheduledMessageManager->parseScheduledMessage($scheduledMessage, $parentMessage); + return new DataResponse($data, Http::STATUS_CREATED); + } + + /** + * Update a scheduled message + * + * Required capability: `scheduled-messages` + * + * @param int $messageId The scheduled message id + * @param string $message The scheduled message to send + * @param int $sendAt When to send the scheduled message + * @param bool $silent If sent silent the scheduled message will not create any notifications + * @param string $threadTitle The thread title if scheduled message is creating a thread + * @return DataResponse|DataResponse|DataResponse|DataResponse + * + * 202: Message updated successfully + * 400: Editing scheduled message is not possible + * 404: Actor not found + * 413: Message too long + */ + #[NoAdminRequired] + #[RequireModeratorOrNoLobby] + #[RequireLoggedInParticipant] + #[RequirePermission(permission: RequirePermission::CHAT)] + #[RequireReadWriteConversation] + #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/chat/{token}/schedule/{messageId}', requirements: [ + 'apiVersion' => '(v1)', + 'token' => '[a-z0-9]{4,30}', + 'messageId' => '[0-9]{4,30}', + ])] + public function editScheduledMessage( + int $messageId, + string $message, + int $sendAt, + bool $silent = false, + string $threadTitle = '', + ): DataResponse { + if ($this->participant->isSelfJoinedOrGuest()) { + return new DataResponse(['error' => 'actor'], Http::STATUS_NOT_FOUND); + } + + if ($sendAt <= $this->timeFactory->getTime()) { + return new DataResponse(['error' => 'send-at'], Http::STATUS_BAD_REQUEST); + } + + if (trim($message) === '') { + return new DataResponse(['error' => 'message'], Http::STATUS_BAD_REQUEST); + } + + $sendAtDateTime = $this->timeFactory->getDateTime('@' . $sendAt, new \DateTimeZone('UTC')); + try { + $scheduledMessage = $this->scheduledMessageManager->editMessage( + $this->room, + $messageId, + $this->participant, + $message, + $silent, + $sendAtDateTime, + $threadTitle + ); + $this->participantService->setHasScheduledMessages($this->participant, true); + } catch (MessageTooLongException) { + return new DataResponse(['error' => 'message'], Http::STATUS_REQUEST_ENTITY_TOO_LARGE); + } catch (\InvalidArgumentException) { + return new DataResponse(['error' => 'thread-title'], Http::STATUS_BAD_REQUEST); + } catch (DoesNotExistException) { + return new DataResponse(['error' => 'message'], Http::STATUS_NOT_FOUND); + } + + $parentMessage = null; + if ($scheduledMessage->getParentId() !== null) { + try { + $parent = $this->chatManager->getParentComment($this->room, (string)$scheduledMessage->getParentId()); + $parentMessage = $this->messageParser->createMessage($this->room, $this->participant, $parent, $this->l); + $this->messageParser->parseMessage($parentMessage); + } catch (NotFoundException) { + } + } + + $data = $this->scheduledMessageManager->parseScheduledMessage($scheduledMessage, $parentMessage); + return new DataResponse($data, Http::STATUS_ACCEPTED); + } + + /** + * Delete a scheduled message + * + * Required capability: `scheduled-messages` + * + * @param int $messageId The scheduled message ud + * @return DataResponse|DataResponse + * + * 200: Message deleted + * 404: Message not found + */ + #[NoAdminRequired] + #[RequireModeratorOrNoLobby] + #[RequireLoggedInParticipant] + #[RequirePermission(permission: RequirePermission::CHAT)] + #[RequireReadWriteConversation] + #[ApiRoute(verb: 'DELETE', url: '/api/{apiVersion}/chat/{token}/schedule/{messageId}', requirements: [ + 'apiVersion' => '(v1)', + 'token' => '[a-z0-9]{4,30}', + 'messageId' => '[0-9]{4,30}', + ])] + public function deleteScheduleMessage(int $messageId): DataResponse { + if ($this->participant->isSelfJoinedOrGuest()) { + return new DataResponse(['error' => 'actor'], Http::STATUS_NOT_FOUND); + } + + $deleted = $this->scheduledMessageManager->deleteMessage( + $this->room, + $messageId, + $this->participant, + ); + + if ($deleted === 0) { + return new DataResponse(['error' => 'message'], Http::STATUS_NOT_FOUND); + } + + $hasScheduledMessages = $this->scheduledMessageManager->getScheduledMessageCount($this->room, $this->participant) > 0; + $this->participantService->setHasScheduledMessages($this->participant, $hasScheduledMessages); + return new DataResponse([], Http::STATUS_OK); + } + /** * Sends a rich-object to the given room * diff --git a/lib/Controller/RoomController.php b/lib/Controller/RoomController.php index ef0036338f1..c7d76740652 100644 --- a/lib/Controller/RoomController.php +++ b/lib/Controller/RoomController.php @@ -8,7 +8,6 @@ namespace OCA\Talk\Controller; -use OCA\Circles\Model\Circle; use OCA\Talk\Capabilities; use OCA\Talk\Config; use OCA\Talk\Events\AAttendeeRemovedEvent; @@ -503,7 +502,7 @@ public function getSingleRoom(string $token): DataResponse { $statuses = $this->statusManager->getUserStatuses($userIds); } return new DataResponse($this->formatRoom($room, $participant, $statuses, $isSIPBridgeRequest), Http::STATUS_OK, $this->getTalkHashHeader()); - } catch (RoomNotFoundException $e) { + } catch (RoomNotFoundException) { /** * A hack to fix type collision * @var DataResponse $response diff --git a/lib/Listener/UserDeletedListener.php b/lib/Listener/UserDeletedListener.php index 7dbf4a1fca0..68e3a3ef967 100644 --- a/lib/Listener/UserDeletedListener.php +++ b/lib/Listener/UserDeletedListener.php @@ -12,6 +12,7 @@ use OCA\Talk\Model\Attendee; use OCA\Talk\Service\ConsentService; use OCA\Talk\Service\PollService; +use OCA\Talk\Service\ScheduledMessageService; use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventListener; use OCP\User\Events\UserDeletedEvent; @@ -25,6 +26,7 @@ public function __construct( private Manager $manager, private PollService $pollService, private ConsentService $consentService, + private ScheduledMessageService $messageManager, ) { } @@ -41,5 +43,7 @@ public function handle(Event $event): void { $this->pollService->neutralizeDeletedUser(Attendee::ACTOR_USERS, $user->getUID()); $this->consentService->deleteByActor(Attendee::ACTOR_USERS, $user->getUID()); + + $this->messageManager->deleteByActor(Attendee::ACTOR_USERS, $user->getUID()); } } diff --git a/lib/Migration/Version23000Date20251105125333.php b/lib/Migration/Version23000Date20251105125333.php new file mode 100644 index 00000000000..8eb9d9114cd --- /dev/null +++ b/lib/Migration/Version23000Date20251105125333.php @@ -0,0 +1,91 @@ +hasTable('talk_scheduled_msg')) { + $table = $schema->createTable('talk_scheduled_msg'); + $table->addColumn('id', Types::BIGINT, [ + 'notnull' => true, + 'unsigned' => true, + 'length' => 20 + ]); + $table->addColumn('room_id', Types::BIGINT, [ + 'notnull' => true, + 'length' => 20, + ]); + $table->addColumn('actor_id', Types::STRING, [ + 'notnull' => true, + 'length' => 64, + ]); + $table->addColumn('actor_type', Types::STRING, [ + 'notnull' => true, + 'length' => 64, + ]); + $table->addColumn('message', Types::TEXT, [ + 'notnull' => false, + 'default' => '', + ]); + $table->addColumn('message_type', Types::STRING, [ + 'notnull' => true, + 'length' => 64, + ]); + $table->addColumn('meta_data', Types::TEXT, [ + 'notnull' => false, + ]); + $table->addColumn('thread_id', Types::BIGINT, [ + 'notnull' => true, + 'default' => 0, + ]); + $table->addColumn('parent_id', Types::BIGINT, [ + 'notnull' => false, + 'default' => null, + ]); + $table->addColumn('created_at', Types::DATETIME, [ + 'notnull' => false, + 'default' => null, + ]); + $table->addColumn('send_at', Types::DATETIME, [ + 'notnull' => false, + 'default' => null, + ]); + $table->setPrimaryKey(['id']); + $table->addIndex(['room_id'], 'tt_room_sched'); + $table->addIndex(['room_id', 'actor_type', 'actor_id'], 'tt_actor_room_sched'); + $table->addIndex(['send_at'], 'tt_send_at_sched'); + } + + $table = $schema->getTable('talk_attendees'); + if (!$table->hasColumn('has_scheduled_messages')) { + $table->addColumn('has_scheduled_messages', Types::BOOLEAN, [ + 'notnull' => true, + 'default' => false, + ]); + } + + return $schema; + } +} diff --git a/lib/Model/Attendee.php b/lib/Model/Attendee.php index caf34e5e574..3e4b0cc0386 100644 --- a/lib/Model/Attendee.php +++ b/lib/Model/Attendee.php @@ -72,6 +72,8 @@ * @method bool getHasUnreadThreadDirects() * @method void setHiddenPinnedId(int $hiddenPinnedId) * @method int getHiddenPinnedId() + * @method void setHasScheduledMessages(bool $scheduledMessages) + * @method bool getHasScheduledMessages() */ class Attendee extends Entity { public const ACTOR_USERS = 'users'; @@ -145,6 +147,7 @@ class Attendee extends Entity { protected bool $hasUnreadThreadMentions = false; protected bool $hasUnreadThreadDirects = false; protected int $hiddenPinnedId = 0; + protected bool $hasScheduledMessages = false; public function __construct() { $this->addType('roomId', Types::BIGINT); @@ -177,6 +180,8 @@ public function __construct() { $this->addType('hasUnreadThreadMentions', Types::BOOLEAN); $this->addType('hasUnreadThreadDirects', Types::BOOLEAN); $this->addType('hiddenPinnedId', Types::BIGINT); + $this->addType('hasScheduledMessages', Types::BOOLEAN); + } public function getDisplayName(): string { diff --git a/lib/Model/Message.php b/lib/Model/Message.php index a27bade533a..2a7a1112a76 100644 --- a/lib/Model/Message.php +++ b/lib/Model/Message.php @@ -25,6 +25,7 @@ class Message { public const METADATA_SILENT = 'silent'; public const METADATA_CAN_MENTION_ALL = 'can_mention_all'; public const METADATA_THREAD_ID = 'thread_id'; + public const METADATA_THREAD_TITLE = 'thread_title'; public const METADATA_PINNED_BY_TYPE = 'pinned_by_type'; public const METADATA_PINNED_BY_ID = 'pinned_by_id'; public const METADATA_PINNED_BY_NAME = 'pinned_by_name'; diff --git a/lib/Model/ScheduledMessage.php b/lib/Model/ScheduledMessage.php new file mode 100644 index 00000000000..d4398d47eee --- /dev/null +++ b/lib/Model/ScheduledMessage.php @@ -0,0 +1,145 @@ +addType('room_id', Types::BIGINT); + $this->addType('actorId', Types::STRING); + $this->addType('actorType', Types::STRING); + $this->addType('threadId', Types::BIGINT); + $this->addType('parentId', Types::BIGINT); + $this->addType('message', Types::TEXT); + $this->addType('messageType', Types::STRING); + $this->addType('metaData', Types::TEXT); + $this->addType('sendAt', Types::DATETIME); + $this->addType('createdAt', Types::DATETIME); + } + + /** + * @return TalkScheduledMessageMetaData + */ + public function getDecodedMetaData(): array { + return json_decode($this->metaData, true, 512, JSON_THROW_ON_ERROR); + } + + public function setMetaData(?array $metaData): void { + $this->metaData = json_encode($metaData, JSON_THROW_ON_ERROR); + $this->markFieldUpdated('metaData'); + } + + /** + * @throws MessageTooLongException When the message is too long (~32k characters) + */ + public function setMessage(string $message): void { + $message = trim($message); + if (mb_strlen($message, 'UTF-8') > ChatManager::MAX_CHAT_LENGTH) { + throw new MessageTooLongException('Comment message must not exceed ' . ChatManager::MAX_CHAT_LENGTH . ' characters'); + } + $this->message = $message; + $this->markFieldUpdated('message'); + } + + #[\Override] + public function jsonSerialize(): array { + return [ + 'roomId' => $this->getRoomId(), + 'actorId' => $this->getActorId(), + 'actorType' => $this->getActorType(), + 'threadId' => $this->getThreadId(), + 'parentId' => $this->getParentId(), + 'message' => $this->getMessage(), + 'messageType' => $this->getMessageType(), + 'createdAt' => $this->getCreatedAt()->getTimestamp(), + 'sendAt' => $this->getSendAt()?->getTimestamp(), + 'metaData' => $this->getDecodedMetaData(), + ]; + } + + /** + * @return TalkScheduledMessage + */ + public function toArray(?Message $parent, ?Thread $thread) : array { + $data = [ + 'id' => $this->id, + 'roomId' => $this->getRoomId(), + 'actorId' => $this->getActorId(), + 'actorType' => $this->getActorType(), + 'threadId' => $this->getThreadId(), + 'parentId' => $this->getParentId(), + 'message' => $this->getMessage(), + 'messageType' => $this->getMessageType(), + 'createdAt' => $this->getCreatedAt()->getTimestamp(), + 'sendAt' => $this->getSendAt()?->getTimestamp(), + ]; + + if ($parent !== null) { + $data['parent'] = $parent->toArray('json', $thread); + } + + $metaData = $this->getDecodedMetaData(); + if ($thread !== null) { + $data['threadExists'] = true; + $data['threadTitle'] = $thread->getName(); + $metaData[self::METADATA_THREAD_TITLE] = $thread->getName(); + } elseif (isset($metaData[self::METADATA_THREAD_TITLE]) && $this->getThreadId() === Thread::THREAD_CREATE) { + $data['threadExists'] = false; + $data['threadTitle'] = (string)$metaData[self::METADATA_THREAD_TITLE]; + } + $data['metaData'] = $metaData; + return $data; + } +} diff --git a/lib/Model/ScheduledMessageMapper.php b/lib/Model/ScheduledMessageMapper.php new file mode 100644 index 00000000000..ecf73b1cee7 --- /dev/null +++ b/lib/Model/ScheduledMessageMapper.php @@ -0,0 +1,137 @@ + findEntities(IQueryBuilder $query) + * @method ScheduledMessage update(ScheduledMessage $scheduledMessage) + * @method ScheduledMessage delete(ScheduledMessage $scheduledMessage) + * @template-extends QBMapper + */ +class ScheduledMessageMapper extends QBMapper { + public function __construct( + IDBConnection $db, + protected IGenerator $generator, + ) { + parent::__construct($db, 'talk_scheduled_msg', ScheduledMessage::class); + } + + /** + * @throws DoesNotExistException + */ + public function findById(Room $chat, int $id, string $actorType, string $actorId): ScheduledMessage { + $query = $this->db->getQueryBuilder(); + $query->select('*') + ->from($this->getTableName()) + ->where($query->expr()->eq('id', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT))) + ->andWhere($query->expr()->eq('actor_type', $query->createNamedParameter($actorType, IQueryBuilder::PARAM_STR))) + ->andWhere($query->expr()->eq('actor_id', $query->createNamedParameter($actorId, IQueryBuilder::PARAM_STR))) + ->andWhere($query->expr()->eq('room_id', $query->createNamedParameter($chat->getId(), IQueryBuilder::PARAM_STR))); + + return $this->findEntity($query); + } + + public function findByRoomAndActor(Room $chat, string $actorType, string $actorId): array { + $query = $this->db->getQueryBuilder(); + $query->select('s.*'); + $helper = new SelectHelper(); + $helper->selectThreadsTable($query, aliasAll: true); + $query->from($this->getTableName(), 's') + ->where($query->expr()->eq('s.room_id', $query->createNamedParameter($chat->getId(), IQueryBuilder::PARAM_STR))) + ->andWhere($query->expr()->eq('s.actor_type', $query->createNamedParameter($actorType, IQueryBuilder::PARAM_STR))) + ->andWhere($query->expr()->eq('s.actor_id', $query->createNamedParameter($actorId, IQueryBuilder::PARAM_STR))) + ->leftJoin('s', 'talk_threads', 'th', $query->expr()->eq('s.thread_id', 'th.id')) + ->orderBy('s.send_at', 'ASC'); + + $cursor = $query->executeQuery(); + $result = $cursor->fetchAll(); + $cursor->closeCursor(); + + return $result; + } + + public function getCountByActorAndRoom(Room $chat, string $actorType, string $actorId): int { + $query = $this->db->getQueryBuilder(); + $query->select(['room_id', $query->func()->count('*')]) + ->from($this->getTableName()) + ->where($query->expr()->eq('actor_type', $query->createNamedParameter($actorType, IQueryBuilder::PARAM_STR))) + ->andWhere($query->expr()->eq('actor_id', $query->createNamedParameter($actorId, IQueryBuilder::PARAM_STR))) + ->andWhere($query->expr()->eq('room_id', $query->createNamedParameter($chat->getId(), IQueryBuilder::PARAM_STR))) + ->groupBy('room_id'); + + $result = $query->executeQuery(); + $count = $result->rowCount(); + $result->closeCursor(); + + return $count; + } + + public function deleteMessagesByRoomAndActor(Room $chat, string $actorType, string $actorId): int { + $query = $this->db->getQueryBuilder(); + $query->delete($this->getTableName()) + ->where($query->expr()->eq('room_id', $query->createNamedParameter($chat->getId(), IQueryBuilder::PARAM_STR))) + ->andWhere($query->expr()->eq('actor_type', $query->createNamedParameter($actorType, IQueryBuilder::PARAM_STR))) + ->andWhere($query->expr()->eq('actor_id', $query->createNamedParameter($actorId, IQueryBuilder::PARAM_STR))); + + return $query->executeStatement(); + } + + public function deleteMessagesByRoom(Room $chat): int { + $query = $this->db->getQueryBuilder(); + $query->delete($this->getTableName()) + ->where($query->expr()->eq('room_id', $query->createNamedParameter($chat->getId(), IQueryBuilder::PARAM_STR))); + return $query->executeStatement(); + } + + public function deleteById(Room $chat, int $id, string $actorType, string $actorId): int { + $query = $this->db->getQueryBuilder(); + $query->delete($this->getTableName()) + ->where($query->expr()->eq('room_id', $query->createNamedParameter($chat->getId(), IQueryBuilder::PARAM_STR))) + ->andWhere($query->expr()->eq('id', $query->createNamedParameter($id, IQueryBuilder::PARAM_STR))) + ->andWhere($query->expr()->eq('actor_type', $query->createNamedParameter($actorType, IQueryBuilder::PARAM_STR))) + ->andWhere($query->expr()->eq('actor_id', $query->createNamedParameter($actorId, IQueryBuilder::PARAM_STR))); + + return $query->executeStatement(); + } + + public function deleteByActor(string $actorType, string $actorId): int { + $query = $this->db->getQueryBuilder(); + $query->delete($this->getTableName()) + ->where($query->expr()->eq('actor_type', $query->createNamedParameter($actorType, IQueryBuilder::PARAM_STR))) + ->andWhere($query->expr()->eq('actor_id', $query->createNamedParameter($actorId, IQueryBuilder::PARAM_STR))); + + return $query->executeStatement(); + } + + public function getMessagesDue(\DateTime $dateTime): array { + $query = $this->db->getQueryBuilder(); + $query->select('*') + ->from($this->getTableName()) + ->where($query->expr()->lt('send_at', $query->createNamedParameter($dateTime, IQueryBuilder::PARAM_DATETIME_MUTABLE))); + + return $this->findEntities($query); + } + + #[\Override] + public function insert(Entity $entity): Entity { + /** @psalm-suppress InvalidArgument */ + $entity->setId($this->generator->nextId()); + return parent::insert($entity); + } +} diff --git a/lib/Model/SelectHelper.php b/lib/Model/SelectHelper.php index db6d97f977e..ea0b512cd87 100644 --- a/lib/Model/SelectHelper.php +++ b/lib/Model/SelectHelper.php @@ -50,6 +50,33 @@ public function selectRoomsTable(IQueryBuilder $query, string $alias = 'r'): voi ])->selectAlias($alias . 'id', 'r_id'); } + public function selectThreadsTable(IQueryBuilder $query, string $alias = 'th', bool $aliasAll = false): void { + if ($alias !== '') { + $alias .= '.'; + } + + if ($aliasAll) { + $query + ->selectAlias($alias . 'room_id', 'th_room_id') + ->selectAlias($alias . 'last_message_id', 'th_last_message_id') + ->selectAlias($alias . 'num_replies', 'th_num_replies') + ->selectAlias($alias . 'last_activity', 'th_last_activity') + ->selectAlias($alias . 'name', 'th_name') + ->selectAlias($alias . 'id', 'th_id'); + return; + } + + $query->addSelect([ + $alias . 'room_id', + $alias . 'last_message_id', + $alias . 'num_replies', + $alias . 'last_activity', + $alias . 'name', + ])->selectAlias($alias . 'id', 'th_id'); + + + } + public function selectAttendeesTable(IQueryBuilder $query, string $alias = 'a'): void { if ($alias !== '') { $alias .= '.'; diff --git a/lib/Participant.php b/lib/Participant.php index f56495491be..b5336cd893f 100644 --- a/lib/Participant.php +++ b/lib/Participant.php @@ -66,6 +66,15 @@ public function isGuest(): bool { return \in_array($participantType, [self::GUEST, self::GUEST_MODERATOR], true); } + public function isSelfJoinedOrGuest(): bool { + $participantType = $this->attendee->getParticipantType(); + return \in_array($participantType, [self::GUEST, self::GUEST_MODERATOR, self::USER_SELF_JOINED], true); + } + + public function getHasScheduledMessages(): bool { + return $this->attendee->getHasScheduledMessages(); + } + public function hasModeratorPermissions(bool $guestModeratorAllowed = true): bool { $participantType = $this->attendee->getParticipantType(); if (!$guestModeratorAllowed) { diff --git a/lib/ResponseDefinitions.php b/lib/ResponseDefinitions.php index f8c8028475b..2318442d5f8 100644 --- a/lib/ResponseDefinitions.php +++ b/lib/ResponseDefinitions.php @@ -387,6 +387,8 @@ * lastPinnedId: int, * // Required capability: `pinned-messages` * hiddenPinnedId: int, + * // Required capability: `scheduled-messages` (local) + * hasScheduledMessages: bool, * } * * @psalm-type TalkDashboardEventAttachment = array{ @@ -563,6 +565,30 @@ * rtl: bool, * }, * } + * + * @psalm-type TalkScheduledMessageMetaData = array{ + * threadId: int, + * threadTitle: string, + * silent: bool, + * lastEditedTime?: int, + * } + * + * @psalm-type TalkScheduledMessage = array{ + * id: int, + * roomId: int, + * actorId: string, + * actorType: string, + * threadId: int, + * threadExists?: boolean, + * threadTitle?: string, + * parentId: ?int, + * parent?: TalkChatMessage, + * message: string, + * messageType: string, + * createdAt: int, + * sendAt: ?int, + * metaData: TalkScheduledMessageMetaData, + * } */ class ResponseDefinitions { } diff --git a/lib/Service/ParticipantService.php b/lib/Service/ParticipantService.php index 50df51fa034..a190a290cc0 100644 --- a/lib/Service/ParticipantService.php +++ b/lib/Service/ParticipantService.php @@ -2381,4 +2381,10 @@ public function getParticipantByActor(Room $room, string $actorType, string $act $this->cacheParticipant($room, $participant); return $participant; } + + public function setHasScheduledMessages(Participant $participant, bool $hasScheduledMessages): void { + $attendee = $participant->getAttendee(); + $attendee->setHasScheduledMessages($hasScheduledMessages); + $this->attendeeMapper->update($attendee); + } } diff --git a/lib/Service/RoomFormatter.php b/lib/Service/RoomFormatter.php index 1868e9c8261..ba709b9cda5 100644 --- a/lib/Service/RoomFormatter.php +++ b/lib/Service/RoomFormatter.php @@ -158,6 +158,7 @@ public function formatRoomV4( 'isArchived' => false, 'isImportant' => false, 'isSensitive' => false, + 'hasScheduledMessages' => false, ]; if ($room->isFederatedConversation()) { @@ -345,6 +346,7 @@ public function formatRoomV4( && ($room->getType() === Room::TYPE_GROUP || $room->getType() === Room::TYPE_PUBLIC) && $currentParticipant->hasModeratorPermissions(false) && $this->talkConfig->canUserEnableSIP($currentUser); + $roomData['hasScheduledMessages'] = $currentParticipant->getHasScheduledMessages(); } } elseif ($attendee->getActorType() === Attendee::ACTOR_FEDERATED_USERS) { $lastReadMessage = $attendee->getLastReadMessage(); diff --git a/lib/Service/ScheduledMessageService.php b/lib/Service/ScheduledMessageService.php new file mode 100644 index 00000000000..eb094277889 --- /dev/null +++ b/lib/Service/ScheduledMessageService.php @@ -0,0 +1,200 @@ +setRoomId($chat->getId()); + $scheduledMessage->setActorId($participant->getAttendee()->getActorId()); + $scheduledMessage->setActorType($participant->getAttendee()->getActorType()); + $scheduledMessage->setSendAt($sendAt); + $scheduledMessage->setMessage($message); + $scheduledMessage->setMessageType($messageType); + if ($parent instanceof IComment) { + $scheduledMessage->setParentId((int)$parent->getId()); + } + $scheduledMessage->setThreadId($threadId); + $scheduledMessage->setMetaData($metadata); + $scheduledMessage->setCreatedAt($this->timeFactory->getDateTime()); + + $this->scheduledMessageMapper->insert($scheduledMessage); + + return $scheduledMessage; + } + + public function deleteMessage(Room $chat, int $id, Participant $participant): int { + return $this->scheduledMessageMapper->deleteById( + $chat, + $id, + $participant->getAttendee()->getActorType(), + $participant->getAttendee()->getActorId() + ); + } + + /** + * @throws DoesNotExistException + * @throws MessageTooLongException + * @throws \InvalidArgumentException + */ + public function editMessage( + Room $chat, + int $id, + Participant $participant, + string $text, + bool $isSilent, + \DateTime $sendAt, + string $threadTitle = '', + ): ScheduledMessage { + $message = $this->scheduledMessageMapper->findById( + $chat, + $id, + $participant->getAttendee()->getActorType(), + $participant->getAttendee()->getActorId() + ); + + $metaData = $message->getDecodedMetaData(); + if ($metaData[ScheduledMessage::METADATA_THREAD_ID] !== Thread::THREAD_CREATE && $threadTitle !== '') { + throw new \InvalidArgumentException('thread-title'); + } + + if ($metaData[ScheduledMessage::METADATA_THREAD_ID] === Thread::THREAD_CREATE && $threadTitle !== '') { + $metaData[ScheduledMessage::METADATA_THREAD_TITLE] = $threadTitle; + } + + $metaData[ScheduledMessage::METADATA_LAST_EDITED_TIME] = $this->timeFactory->getTime(); + $metaData[ScheduledMessage::METADATA_SILENT] = $isSilent; + $message->setMetaData($metaData); + $message->setMessage($text); + $message->setSendAt($sendAt); + $this->scheduledMessageMapper->update($message); + + return $message; + } + + public function deleteByActor(string $actorType, string $actorId): void { + $this->scheduledMessageMapper->deleteByActor($actorType, $actorId); + } + + /** + * @return list + */ + public function getMessages(Room $chat, Participant $participant): array { + $result = $this->scheduledMessageMapper->findByRoomAndActor( + $chat, + $participant->getAttendee()->getActorType(), + $participant->getAttendee()->getActorId() + ); + + $commentIds = array_filter(array_map(static function (array $result) { + return $result['parent_id']; + }, $result)); + try { + $comments = $this->commentsManager->getCommentsById($commentIds); + } catch (Exception) { + $comments = []; + } + + $messages = []; + foreach ($result as $row) { + $parent = $thread = null; + $entity = []; + foreach ($row as $field => $value) { + if (str_starts_with($field, 'th_')) { + $thread[substr($field, 3)] = $value; + continue; + } + $entity[$field] = $value; + } + + $scheduleMessage = ScheduledMessage::fromRow($entity); + if ($entity['parent_id'] !== null && isset($comments[$entity['parent_id']])) { + $parent = $this->messageParser->createMessage($chat, $participant, $comments[$entity['parent_id']], $this->l); + $this->messageParser->parseMessage($parent); + } + if (in_array($thread['id'], [null, Thread::THREAD_NONE, Thread::THREAD_CREATE], true)) { + $thread = null; + } else { + $thread = Thread::fromRow($thread); + } + $messages[] = $this->parseScheduledMessage($scheduleMessage, $parent, $thread); + } + + return $messages; + } + + public function parseScheduledMessage(ScheduledMessage $message, ?Message $parentMessage, ?Thread $thread = null): array { + if ($thread === null + && $message->getThreadId() !== Thread::THREAD_NONE + && $message->getThreadId() !== Thread::THREAD_CREATE + ) { + try { + $thread = $this->threadService->findByThreadId( + $message->getRoomId(), + $message->getThreadId(), + ); + } catch (DoesNotExistException $e) { + $this->logger->warning('Could not find thread ' . (string)$message->getThreadId() . ' for scheduled message', ['exception' => $e]); + $thread = null; + } + } + return $message->toArray($parentMessage, $thread ?? null); + } + + public function getScheduledMessageCount(Room $chat, Participant $participant): int { + return $this->scheduledMessageMapper->getCountByActorAndRoom( + $chat, + $participant->getAttendee()->getActorType(), + $participant->getAttendee()->getActorId(), + ); + } +} diff --git a/openapi-backend-sipbridge.json b/openapi-backend-sipbridge.json index 60325ee754a..a2b42977e1f 100644 --- a/openapi-backend-sipbridge.json +++ b/openapi-backend-sipbridge.json @@ -700,7 +700,8 @@ "isImportant", "isSensitive", "lastPinnedId", - "hiddenPinnedId" + "hiddenPinnedId", + "hasScheduledMessages" ], "properties": { "actorId": { @@ -1003,6 +1004,10 @@ "type": "integer", "format": "int64", "description": "Required capability: `pinned-messages`" + }, + "hasScheduledMessages": { + "type": "boolean", + "description": "Required capability: `scheduled-messages` (local)" } } }, diff --git a/openapi-federation.json b/openapi-federation.json index ff4a4a69990..9ce93b86cf1 100644 --- a/openapi-federation.json +++ b/openapi-federation.json @@ -754,7 +754,8 @@ "isImportant", "isSensitive", "lastPinnedId", - "hiddenPinnedId" + "hiddenPinnedId", + "hasScheduledMessages" ], "properties": { "actorId": { @@ -1057,6 +1058,10 @@ "type": "integer", "format": "int64", "description": "Required capability: `pinned-messages`" + }, + "hasScheduledMessages": { + "type": "boolean", + "description": "Required capability: `scheduled-messages` (local)" } } }, diff --git a/openapi-full.json b/openapi-full.json index 48dd07be52b..8337c076606 100644 --- a/openapi-full.json +++ b/openapi-full.json @@ -1578,7 +1578,8 @@ "isImportant", "isSensitive", "lastPinnedId", - "hiddenPinnedId" + "hiddenPinnedId", + "hasScheduledMessages" ], "properties": { "actorId": { @@ -1881,6 +1882,10 @@ "type": "integer", "format": "int64", "description": "Required capability: `pinned-messages`" + }, + "hasScheduledMessages": { + "type": "boolean", + "description": "Required capability: `scheduled-messages` (local)" } } }, @@ -1912,6 +1917,98 @@ } ] }, + "ScheduledMessage": { + "type": "object", + "required": [ + "id", + "roomId", + "actorId", + "actorType", + "threadId", + "parentId", + "message", + "messageType", + "createdAt", + "sendAt", + "metaData" + ], + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "roomId": { + "type": "integer", + "format": "int64" + }, + "actorId": { + "type": "string" + }, + "actorType": { + "type": "string" + }, + "threadId": { + "type": "integer", + "format": "int64" + }, + "threadExists": { + "type": "boolean" + }, + "threadTitle": { + "type": "string" + }, + "parentId": { + "type": "integer", + "format": "int64", + "nullable": true + }, + "parent": { + "$ref": "#/components/schemas/ChatMessage" + }, + "message": { + "type": "string" + }, + "messageType": { + "type": "string" + }, + "createdAt": { + "type": "integer", + "format": "int64" + }, + "sendAt": { + "type": "integer", + "format": "int64", + "nullable": true + }, + "metaData": { + "$ref": "#/components/schemas/ScheduledMessageMetaData" + } + } + }, + "ScheduledMessageMetaData": { + "type": "object", + "required": [ + "threadId", + "threadTitle", + "silent" + ], + "properties": { + "threadId": { + "type": "integer", + "format": "int64" + }, + "threadTitle": { + "type": "string" + }, + "silent": { + "type": "boolean" + }, + "lastEditedTime": { + "type": "integer", + "format": "int64" + } + } + }, "SignalingFederationSettings": { "type": "object", "required": [ @@ -7634,6 +7731,926 @@ } } }, + "/ocs/v2.php/apps/spreed/api/{apiVersion}/chat/{token}/schedule": { + "get": { + "operationId": "chat-scheduled-messages", + "summary": "Get all scheduled nessages of a given room and participant", + "description": "The author and timestamp are automatically set to the current user and time.\nRequired capability: `scheduled-messages`", + "tags": [ + "chat" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v1" + ], + "default": "v1" + } + }, + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string", + "pattern": "^[a-z0-9]{4,30}$" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "All scheduled messages for this room and participant", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ScheduledMessage" + } + } + } + } + } + } + } + } + }, + "400": { + "description": "Could not get scheduled messages", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string", + "enum": [ + "message" + ] + } + } + } + } + } + } + } + } + } + }, + "404": { + "description": "Actor not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string", + "enum": [ + "actor" + ] + } + } + } + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + }, + "post": { + "operationId": "chat-schedule-message", + "summary": "Schedules the sending of a new chat message to the given room", + "description": "The author and timestamp are automatically set to the current user and time.\nRequired capability: `scheduled-messages`", + "tags": [ + "chat" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "message", + "sendAt" + ], + "properties": { + "message": { + "type": "string", + "description": "The message to send" + }, + "sendAt": { + "type": "integer", + "format": "int64", + "description": "When to send the scheduled message" + }, + "replyTo": { + "type": "integer", + "format": "int64", + "default": 0, + "description": "Parent id which this scheduled message is a reply to", + "minimum": 0 + }, + "silent": { + "type": "boolean", + "default": false, + "description": "If sent silent the scheduled message will not create any notifications when sent" + }, + "threadTitle": { + "type": "string", + "default": "", + "description": "Only supported when not replying, when given will create a thread (requires `threads` capability)" + }, + "threadId": { + "type": "integer", + "format": "int64", + "default": 0, + "description": "Thread id without quoting a specific message (requires `threads` capability)" + } + } + } + } + } + }, + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v1" + ], + "default": "v1" + } + }, + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string", + "pattern": "^[a-z0-9]{4,30}$" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "201": { + "description": "Message scheduled successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "$ref": "#/components/schemas/ScheduledMessage" + } + } + } + } + } + } + } + }, + "400": { + "description": "Scheduling the message is not possible", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string", + "enum": [ + "message", + "reply-to", + "send-at" + ] + } + } + } + } + } + } + } + } + } + }, + "413": { + "description": "Message too long", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string", + "enum": [ + "message" + ] + } + } + } + } + } + } + } + } + } + }, + "404": { + "description": "Actor not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string", + "enum": [ + "actor" + ] + } + } + } + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/spreed/api/{apiVersion}/chat/{token}/schedule/{messageId}": { + "post": { + "operationId": "chat-edit-scheduled-message", + "summary": "Update a scheduled message", + "description": "Required capability: `scheduled-messages`", + "tags": [ + "chat" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "message", + "sendAt" + ], + "properties": { + "message": { + "type": "string", + "description": "The scheduled message to send" + }, + "sendAt": { + "type": "integer", + "format": "int64", + "description": "When to send the scheduled message" + }, + "silent": { + "type": "boolean", + "default": false, + "description": "If sent silent the scheduled message will not create any notifications" + }, + "threadTitle": { + "type": "string", + "default": "", + "description": "The thread title if scheduled message is creating a thread" + } + } + } + } + } + }, + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v1" + ], + "default": "v1" + } + }, + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string", + "pattern": "^[a-z0-9]{4,30}$" + } + }, + { + "name": "messageId", + "in": "path", + "description": "The scheduled message id", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "202": { + "description": "Message updated successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "$ref": "#/components/schemas/ScheduledMessage" + } + } + } + } + } + } + } + }, + "400": { + "description": "Editing scheduled message is not possible", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string", + "enum": [ + "message", + "send-at", + "thread-title" + ] + } + } + } + } + } + } + } + } + } + }, + "413": { + "description": "Message too long", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string", + "enum": [ + "message" + ] + } + } + } + } + } + } + } + } + } + }, + "404": { + "description": "Actor not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string", + "enum": [ + "actor" + ] + } + } + } + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + }, + "delete": { + "operationId": "chat-delete-schedule-message", + "summary": "Delete a scheduled message", + "description": "Required capability: `scheduled-messages`", + "tags": [ + "chat" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v1" + ], + "default": "v1" + } + }, + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string", + "pattern": "^[a-z0-9]{4,30}$" + } + }, + { + "name": "messageId", + "in": "path", + "description": "The scheduled message ud", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Message deleted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object" + } + } + } + } + } + } + } + }, + "404": { + "description": "Message not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string", + "enum": [ + "actor", + "message" + ] + } + } + } + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, "/ocs/v2.php/apps/spreed/api/{apiVersion}/chat/{token}/share": { "post": { "operationId": "chat-share-object-to-chat", diff --git a/openapi.json b/openapi.json index de72365fd15..62089789058 100644 --- a/openapi.json +++ b/openapi.json @@ -1483,7 +1483,8 @@ "isImportant", "isSensitive", "lastPinnedId", - "hiddenPinnedId" + "hiddenPinnedId", + "hasScheduledMessages" ], "properties": { "actorId": { @@ -1786,6 +1787,10 @@ "type": "integer", "format": "int64", "description": "Required capability: `pinned-messages`" + }, + "hasScheduledMessages": { + "type": "boolean", + "description": "Required capability: `scheduled-messages` (local)" } } }, @@ -1817,6 +1822,98 @@ } ] }, + "ScheduledMessage": { + "type": "object", + "required": [ + "id", + "roomId", + "actorId", + "actorType", + "threadId", + "parentId", + "message", + "messageType", + "createdAt", + "sendAt", + "metaData" + ], + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "roomId": { + "type": "integer", + "format": "int64" + }, + "actorId": { + "type": "string" + }, + "actorType": { + "type": "string" + }, + "threadId": { + "type": "integer", + "format": "int64" + }, + "threadExists": { + "type": "boolean" + }, + "threadTitle": { + "type": "string" + }, + "parentId": { + "type": "integer", + "format": "int64", + "nullable": true + }, + "parent": { + "$ref": "#/components/schemas/ChatMessage" + }, + "message": { + "type": "string" + }, + "messageType": { + "type": "string" + }, + "createdAt": { + "type": "integer", + "format": "int64" + }, + "sendAt": { + "type": "integer", + "format": "int64", + "nullable": true + }, + "metaData": { + "$ref": "#/components/schemas/ScheduledMessageMetaData" + } + } + }, + "ScheduledMessageMetaData": { + "type": "object", + "required": [ + "threadId", + "threadTitle", + "silent" + ], + "properties": { + "threadId": { + "type": "integer", + "format": "int64" + }, + "threadTitle": { + "type": "string" + }, + "silent": { + "type": "boolean" + }, + "lastEditedTime": { + "type": "integer", + "format": "int64" + } + } + }, "SignalingFederationSettings": { "type": "object", "required": [ @@ -7539,6 +7636,926 @@ } } }, + "/ocs/v2.php/apps/spreed/api/{apiVersion}/chat/{token}/schedule": { + "get": { + "operationId": "chat-scheduled-messages", + "summary": "Get all scheduled nessages of a given room and participant", + "description": "The author and timestamp are automatically set to the current user and time.\nRequired capability: `scheduled-messages`", + "tags": [ + "chat" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v1" + ], + "default": "v1" + } + }, + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string", + "pattern": "^[a-z0-9]{4,30}$" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "All scheduled messages for this room and participant", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ScheduledMessage" + } + } + } + } + } + } + } + } + }, + "400": { + "description": "Could not get scheduled messages", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string", + "enum": [ + "message" + ] + } + } + } + } + } + } + } + } + } + }, + "404": { + "description": "Actor not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string", + "enum": [ + "actor" + ] + } + } + } + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + }, + "post": { + "operationId": "chat-schedule-message", + "summary": "Schedules the sending of a new chat message to the given room", + "description": "The author and timestamp are automatically set to the current user and time.\nRequired capability: `scheduled-messages`", + "tags": [ + "chat" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "message", + "sendAt" + ], + "properties": { + "message": { + "type": "string", + "description": "The message to send" + }, + "sendAt": { + "type": "integer", + "format": "int64", + "description": "When to send the scheduled message" + }, + "replyTo": { + "type": "integer", + "format": "int64", + "default": 0, + "description": "Parent id which this scheduled message is a reply to", + "minimum": 0 + }, + "silent": { + "type": "boolean", + "default": false, + "description": "If sent silent the scheduled message will not create any notifications when sent" + }, + "threadTitle": { + "type": "string", + "default": "", + "description": "Only supported when not replying, when given will create a thread (requires `threads` capability)" + }, + "threadId": { + "type": "integer", + "format": "int64", + "default": 0, + "description": "Thread id without quoting a specific message (requires `threads` capability)" + } + } + } + } + } + }, + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v1" + ], + "default": "v1" + } + }, + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string", + "pattern": "^[a-z0-9]{4,30}$" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "201": { + "description": "Message scheduled successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "$ref": "#/components/schemas/ScheduledMessage" + } + } + } + } + } + } + } + }, + "400": { + "description": "Scheduling the message is not possible", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string", + "enum": [ + "message", + "reply-to", + "send-at" + ] + } + } + } + } + } + } + } + } + } + }, + "413": { + "description": "Message too long", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string", + "enum": [ + "message" + ] + } + } + } + } + } + } + } + } + } + }, + "404": { + "description": "Actor not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string", + "enum": [ + "actor" + ] + } + } + } + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/spreed/api/{apiVersion}/chat/{token}/schedule/{messageId}": { + "post": { + "operationId": "chat-edit-scheduled-message", + "summary": "Update a scheduled message", + "description": "Required capability: `scheduled-messages`", + "tags": [ + "chat" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "message", + "sendAt" + ], + "properties": { + "message": { + "type": "string", + "description": "The scheduled message to send" + }, + "sendAt": { + "type": "integer", + "format": "int64", + "description": "When to send the scheduled message" + }, + "silent": { + "type": "boolean", + "default": false, + "description": "If sent silent the scheduled message will not create any notifications" + }, + "threadTitle": { + "type": "string", + "default": "", + "description": "The thread title if scheduled message is creating a thread" + } + } + } + } + } + }, + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v1" + ], + "default": "v1" + } + }, + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string", + "pattern": "^[a-z0-9]{4,30}$" + } + }, + { + "name": "messageId", + "in": "path", + "description": "The scheduled message id", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "202": { + "description": "Message updated successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "$ref": "#/components/schemas/ScheduledMessage" + } + } + } + } + } + } + } + }, + "400": { + "description": "Editing scheduled message is not possible", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string", + "enum": [ + "message", + "send-at", + "thread-title" + ] + } + } + } + } + } + } + } + } + } + }, + "413": { + "description": "Message too long", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string", + "enum": [ + "message" + ] + } + } + } + } + } + } + } + } + } + }, + "404": { + "description": "Actor not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string", + "enum": [ + "actor" + ] + } + } + } + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + }, + "delete": { + "operationId": "chat-delete-schedule-message", + "summary": "Delete a scheduled message", + "description": "Required capability: `scheduled-messages`", + "tags": [ + "chat" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v1" + ], + "default": "v1" + } + }, + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string", + "pattern": "^[a-z0-9]{4,30}$" + } + }, + { + "name": "messageId", + "in": "path", + "description": "The scheduled message ud", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Message deleted", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object" + } + } + } + } + } + } + } + }, + "404": { + "description": "Message not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string", + "enum": [ + "actor", + "message" + ] + } + } + } + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, "/ocs/v2.php/apps/spreed/api/{apiVersion}/chat/{token}/share": { "post": { "operationId": "chat-share-object-to-chat", diff --git a/src/types/openapi/openapi-backend-sipbridge.ts b/src/types/openapi/openapi-backend-sipbridge.ts index 9b466d3ba2c..1cb25eb3f22 100644 --- a/src/types/openapi/openapi-backend-sipbridge.ts +++ b/src/types/openapi/openapi-backend-sipbridge.ts @@ -542,6 +542,8 @@ export type components = { * @description Required capability: `pinned-messages` */ hiddenPinnedId: number; + /** @description Required capability: `scheduled-messages` (local) */ + hasScheduledMessages: boolean; }; RoomLastMessage: components["schemas"]["ChatMessage"] | components["schemas"]["ChatProxyMessage"]; }; diff --git a/src/types/openapi/openapi-federation.ts b/src/types/openapi/openapi-federation.ts index ff302c98856..6ec535cc637 100644 --- a/src/types/openapi/openapi-federation.ts +++ b/src/types/openapi/openapi-federation.ts @@ -569,6 +569,8 @@ export type components = { * @description Required capability: `pinned-messages` */ hiddenPinnedId: number; + /** @description Required capability: `scheduled-messages` (local) */ + hasScheduledMessages: boolean; }; RoomLastMessage: components["schemas"]["ChatMessage"] | components["schemas"]["ChatProxyMessage"]; }; diff --git a/src/types/openapi/openapi-full.ts b/src/types/openapi/openapi-full.ts index cde479c18a7..8cdc9dcb2b7 100644 --- a/src/types/openapi/openapi-full.ts +++ b/src/types/openapi/openapi-full.ts @@ -421,6 +421,56 @@ export type paths = { patch?: never; trace?: never; }; + "/ocs/v2.php/apps/spreed/api/{apiVersion}/chat/{token}/schedule": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get all scheduled nessages of a given room and participant + * @description The author and timestamp are automatically set to the current user and time. + * Required capability: `scheduled-messages` + */ + get: operations["chat-scheduled-messages"]; + put?: never; + /** + * Schedules the sending of a new chat message to the given room + * @description The author and timestamp are automatically set to the current user and time. + * Required capability: `scheduled-messages` + */ + post: operations["chat-schedule-message"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/ocs/v2.php/apps/spreed/api/{apiVersion}/chat/{token}/schedule/{messageId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Update a scheduled message + * @description Required capability: `scheduled-messages` + */ + post: operations["chat-edit-scheduled-message"]; + /** + * Delete a scheduled message + * @description Required capability: `scheduled-messages` + */ + delete: operations["chat-delete-schedule-message"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/ocs/v2.php/apps/spreed/api/{apiVersion}/chat/{token}/share": { parameters: { query?: never; @@ -2952,11 +3002,43 @@ export type components = { * @description Required capability: `pinned-messages` */ hiddenPinnedId: number; + /** @description Required capability: `scheduled-messages` (local) */ + hasScheduledMessages: boolean; }; RoomLastMessage: components["schemas"]["ChatMessage"] | components["schemas"]["ChatProxyMessage"]; RoomWithInvalidInvitations: components["schemas"]["Room"] & { invalidParticipants: components["schemas"]["InvitationList"]; }; + ScheduledMessage: { + /** Format: int64 */ + id: number; + /** Format: int64 */ + roomId: number; + actorId: string; + actorType: string; + /** Format: int64 */ + threadId: number; + threadExists?: boolean; + threadTitle?: string; + /** Format: int64 */ + parentId: number | null; + parent?: components["schemas"]["ChatMessage"]; + message: string; + messageType: string; + /** Format: int64 */ + createdAt: number; + /** Format: int64 */ + sendAt: number | null; + metaData: components["schemas"]["ScheduledMessageMetaData"]; + }; + ScheduledMessageMetaData: { + /** Format: int64 */ + threadId: number; + threadTitle: string; + silent: boolean; + /** Format: int64 */ + lastEditedTime?: number; + }; SignalingFederationSettings: { server: string; nextcloudServer: string; @@ -5228,6 +5310,399 @@ export interface operations { }; }; }; + "chat-scheduled-messages": { + parameters: { + query?: never; + header: { + /** @description Required to be true for the API request to pass */ + "OCS-APIRequest": boolean; + }; + path: { + apiVersion: "v1"; + token: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description All scheduled messages for this room and participant */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: components["schemas"]["ScheduledMessage"][]; + }; + }; + }; + }; + /** @description Could not get scheduled messages */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: { + /** @enum {string} */ + error: "message"; + }; + }; + }; + }; + }; + /** @description Current user is not logged in */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: unknown; + }; + }; + }; + }; + /** @description Actor not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: { + /** @enum {string} */ + error: "actor"; + }; + }; + }; + }; + }; + }; + }; + "chat-schedule-message": { + parameters: { + query?: never; + header: { + /** @description Required to be true for the API request to pass */ + "OCS-APIRequest": boolean; + }; + path: { + apiVersion: "v1"; + token: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @description The message to send */ + message: string; + /** + * Format: int64 + * @description When to send the scheduled message + */ + sendAt: number; + /** + * Format: int64 + * @description Parent id which this scheduled message is a reply to + * @default 0 + */ + replyTo?: number; + /** + * @description If sent silent the scheduled message will not create any notifications when sent + * @default false + */ + silent?: boolean; + /** + * @description Only supported when not replying, when given will create a thread (requires `threads` capability) + * @default + */ + threadTitle?: string; + /** + * Format: int64 + * @description Thread id without quoting a specific message (requires `threads` capability) + * @default 0 + */ + threadId?: number; + }; + }; + }; + responses: { + /** @description Message scheduled successfully */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: components["schemas"]["ScheduledMessage"]; + }; + }; + }; + }; + /** @description Scheduling the message is not possible */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: { + /** @enum {string} */ + error: "message" | "reply-to" | "send-at"; + }; + }; + }; + }; + }; + /** @description Current user is not logged in */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: unknown; + }; + }; + }; + }; + /** @description Actor not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: { + /** @enum {string} */ + error: "actor"; + }; + }; + }; + }; + }; + /** @description Message too long */ + 413: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: { + /** @enum {string} */ + error: "message"; + }; + }; + }; + }; + }; + }; + }; + "chat-edit-scheduled-message": { + parameters: { + query?: never; + header: { + /** @description Required to be true for the API request to pass */ + "OCS-APIRequest": boolean; + }; + path: { + apiVersion: "v1"; + token: string; + /** @description The scheduled message id */ + messageId: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @description The scheduled message to send */ + message: string; + /** + * Format: int64 + * @description When to send the scheduled message + */ + sendAt: number; + /** + * @description If sent silent the scheduled message will not create any notifications + * @default false + */ + silent?: boolean; + /** + * @description The thread title if scheduled message is creating a thread + * @default + */ + threadTitle?: string; + }; + }; + }; + responses: { + /** @description Message updated successfully */ + 202: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: components["schemas"]["ScheduledMessage"]; + }; + }; + }; + }; + /** @description Editing scheduled message is not possible */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: { + /** @enum {string} */ + error: "message" | "send-at" | "thread-title"; + }; + }; + }; + }; + }; + /** @description Current user is not logged in */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: unknown; + }; + }; + }; + }; + /** @description Actor not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: { + /** @enum {string} */ + error: "actor"; + }; + }; + }; + }; + }; + /** @description Message too long */ + 413: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: { + /** @enum {string} */ + error: "message"; + }; + }; + }; + }; + }; + }; + }; + "chat-delete-schedule-message": { + parameters: { + query?: never; + header: { + /** @description Required to be true for the API request to pass */ + "OCS-APIRequest": boolean; + }; + path: { + apiVersion: "v1"; + token: string; + /** @description The scheduled message ud */ + messageId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Message deleted */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: Record; + }; + }; + }; + }; + /** @description Current user is not logged in */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: unknown; + }; + }; + }; + }; + /** @description Message not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: { + /** @enum {string} */ + error: "actor" | "message"; + }; + }; + }; + }; + }; + }; + }; "chat-get-objects-shared-in-room": { parameters: { query: { diff --git a/src/types/openapi/openapi.ts b/src/types/openapi/openapi.ts index 3801d59fc76..916c719eced 100644 --- a/src/types/openapi/openapi.ts +++ b/src/types/openapi/openapi.ts @@ -421,6 +421,56 @@ export type paths = { patch?: never; trace?: never; }; + "/ocs/v2.php/apps/spreed/api/{apiVersion}/chat/{token}/schedule": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get all scheduled nessages of a given room and participant + * @description The author and timestamp are automatically set to the current user and time. + * Required capability: `scheduled-messages` + */ + get: operations["chat-scheduled-messages"]; + put?: never; + /** + * Schedules the sending of a new chat message to the given room + * @description The author and timestamp are automatically set to the current user and time. + * Required capability: `scheduled-messages` + */ + post: operations["chat-schedule-message"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/ocs/v2.php/apps/spreed/api/{apiVersion}/chat/{token}/schedule/{messageId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Update a scheduled message + * @description Required capability: `scheduled-messages` + */ + post: operations["chat-edit-scheduled-message"]; + /** + * Delete a scheduled message + * @description Required capability: `scheduled-messages` + */ + delete: operations["chat-delete-schedule-message"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/ocs/v2.php/apps/spreed/api/{apiVersion}/chat/{token}/share": { parameters: { query?: never; @@ -2414,11 +2464,43 @@ export type components = { * @description Required capability: `pinned-messages` */ hiddenPinnedId: number; + /** @description Required capability: `scheduled-messages` (local) */ + hasScheduledMessages: boolean; }; RoomLastMessage: components["schemas"]["ChatMessage"] | components["schemas"]["ChatProxyMessage"]; RoomWithInvalidInvitations: components["schemas"]["Room"] & { invalidParticipants: components["schemas"]["InvitationList"]; }; + ScheduledMessage: { + /** Format: int64 */ + id: number; + /** Format: int64 */ + roomId: number; + actorId: string; + actorType: string; + /** Format: int64 */ + threadId: number; + threadExists?: boolean; + threadTitle?: string; + /** Format: int64 */ + parentId: number | null; + parent?: components["schemas"]["ChatMessage"]; + message: string; + messageType: string; + /** Format: int64 */ + createdAt: number; + /** Format: int64 */ + sendAt: number | null; + metaData: components["schemas"]["ScheduledMessageMetaData"]; + }; + ScheduledMessageMetaData: { + /** Format: int64 */ + threadId: number; + threadTitle: string; + silent: boolean; + /** Format: int64 */ + lastEditedTime?: number; + }; SignalingFederationSettings: { server: string; nextcloudServer: string; @@ -4690,6 +4772,399 @@ export interface operations { }; }; }; + "chat-scheduled-messages": { + parameters: { + query?: never; + header: { + /** @description Required to be true for the API request to pass */ + "OCS-APIRequest": boolean; + }; + path: { + apiVersion: "v1"; + token: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description All scheduled messages for this room and participant */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: components["schemas"]["ScheduledMessage"][]; + }; + }; + }; + }; + /** @description Could not get scheduled messages */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: { + /** @enum {string} */ + error: "message"; + }; + }; + }; + }; + }; + /** @description Current user is not logged in */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: unknown; + }; + }; + }; + }; + /** @description Actor not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: { + /** @enum {string} */ + error: "actor"; + }; + }; + }; + }; + }; + }; + }; + "chat-schedule-message": { + parameters: { + query?: never; + header: { + /** @description Required to be true for the API request to pass */ + "OCS-APIRequest": boolean; + }; + path: { + apiVersion: "v1"; + token: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @description The message to send */ + message: string; + /** + * Format: int64 + * @description When to send the scheduled message + */ + sendAt: number; + /** + * Format: int64 + * @description Parent id which this scheduled message is a reply to + * @default 0 + */ + replyTo?: number; + /** + * @description If sent silent the scheduled message will not create any notifications when sent + * @default false + */ + silent?: boolean; + /** + * @description Only supported when not replying, when given will create a thread (requires `threads` capability) + * @default + */ + threadTitle?: string; + /** + * Format: int64 + * @description Thread id without quoting a specific message (requires `threads` capability) + * @default 0 + */ + threadId?: number; + }; + }; + }; + responses: { + /** @description Message scheduled successfully */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: components["schemas"]["ScheduledMessage"]; + }; + }; + }; + }; + /** @description Scheduling the message is not possible */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: { + /** @enum {string} */ + error: "message" | "reply-to" | "send-at"; + }; + }; + }; + }; + }; + /** @description Current user is not logged in */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: unknown; + }; + }; + }; + }; + /** @description Actor not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: { + /** @enum {string} */ + error: "actor"; + }; + }; + }; + }; + }; + /** @description Message too long */ + 413: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: { + /** @enum {string} */ + error: "message"; + }; + }; + }; + }; + }; + }; + }; + "chat-edit-scheduled-message": { + parameters: { + query?: never; + header: { + /** @description Required to be true for the API request to pass */ + "OCS-APIRequest": boolean; + }; + path: { + apiVersion: "v1"; + token: string; + /** @description The scheduled message id */ + messageId: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @description The scheduled message to send */ + message: string; + /** + * Format: int64 + * @description When to send the scheduled message + */ + sendAt: number; + /** + * @description If sent silent the scheduled message will not create any notifications + * @default false + */ + silent?: boolean; + /** + * @description The thread title if scheduled message is creating a thread + * @default + */ + threadTitle?: string; + }; + }; + }; + responses: { + /** @description Message updated successfully */ + 202: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: components["schemas"]["ScheduledMessage"]; + }; + }; + }; + }; + /** @description Editing scheduled message is not possible */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: { + /** @enum {string} */ + error: "message" | "send-at" | "thread-title"; + }; + }; + }; + }; + }; + /** @description Current user is not logged in */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: unknown; + }; + }; + }; + }; + /** @description Actor not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: { + /** @enum {string} */ + error: "actor"; + }; + }; + }; + }; + }; + /** @description Message too long */ + 413: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: { + /** @enum {string} */ + error: "message"; + }; + }; + }; + }; + }; + }; + }; + "chat-delete-schedule-message": { + parameters: { + query?: never; + header: { + /** @description Required to be true for the API request to pass */ + "OCS-APIRequest": boolean; + }; + path: { + apiVersion: "v1"; + token: string; + /** @description The scheduled message ud */ + messageId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Message deleted */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: Record; + }; + }; + }; + }; + /** @description Current user is not logged in */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: unknown; + }; + }; + }; + }; + /** @description Message not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: { + /** @enum {string} */ + error: "actor" | "message"; + }; + }; + }; + }; + }; + }; + }; "chat-get-objects-shared-in-room": { parameters: { query: { diff --git a/tests/integration/features/bootstrap/FeatureContext.php b/tests/integration/features/bootstrap/FeatureContext.php index 84600943eec..78fcd39e72d 100644 --- a/tests/integration/features/bootstrap/FeatureContext.php +++ b/tests/integration/features/bootstrap/FeatureContext.php @@ -1934,6 +1934,140 @@ public function userDownloadsPeersInCall(string $user, string $identifier, strin Assert::assertEquals(implode("\n", $expected) . "\n", $this->response->getBody()->getContents()); } + #[When('/^user "([^"]*)" schedules a message to room "([^"]*)" with (\d+)(?: \((v1)\))?$/')] + public function userSchedulesMessageToRoom(string $user, string $identifier, int $statusCode, string $apiVersion = 'v1', ?TableNode $formData = null): void { + $row = $formData->getRowsHash(); + $row['sendAt'] = (int)$row['sendAt']; + if (isset($row['replyTo'])) { + $row['replyTo'] = self::$textToMessageId[$row['replyTo']]; + } + if (isset($row['threadId']) && $row['threadId'] !== '0' && $row['threadId'] !== '-1') { + $row['threadId'] = self::$titleToThreadId[$row['threadId']]; + } + + $this->setCurrentUser($user); + $this->sendRequest( + 'POST', + '/apps/spreed/api/' . $apiVersion . '/chat/' . self::$identifierToToken[$identifier] . '/schedule', + $row + ); + + $this->assertStatusCode($this->response, $statusCode); + sleep(1); // make sure Postgres manages the order of the messages + + $response = $this->getDataFromResponse($this->response); + if (isset($response['id'])) { + self::$textToMessageId[$row['message']] = $response['id']; + self::$messageIdToText[$response['id']] = $row['message']; + } + } + + #[When('/^user "([^"]*)" updates scheduled message "([^"]*)" in room "([^"]*)" with (\d+)(?: \((v1)\))?$/')] + public function userUpdatesScheduledMessageInRoom(string $user, string $message, string $identifier, int $statusCode, string $apiVersion = 'v1', ?TableNode $formData = null): void { + $row = $formData->getRowsHash(); + $id = self::$textToMessageId[$message]; + $row['sendAt'] = (int)$row['sendAt']; + + $this->setCurrentUser($user); + $this->sendRequest( + 'POST', + '/apps/spreed/api/' . $apiVersion . '/chat/' . self::$identifierToToken[$identifier] . '/schedule/' . $id, + $row + ); + + $this->assertStatusCode($this->response, $statusCode); + if ($this->response->getStatusCode() !== 202) { + return; + } + sleep(1); // make sure Postgres manages the order of the messages + + $response = $this->getDataFromResponse($this->response); + self::$textToMessageId[$row['message']] = $response['id']; + self::$messageIdToText[$response['id']] = $row['message']; + Assert::assertEquals($row['message'], $response['message']); + Assert::assertEquals($row['sendAt'], $response['sendAt']); + Assert::assertArrayHasKey('metaData', $response); + $metaData = $response['metaData']; + Assert::assertArrayHasKey('silent', $metaData); + Assert::assertArrayHasKey('threadTitle', $metaData); + Assert::assertArrayHasKey('threadId', $metaData); + Assert::assertArrayHasKey('lastEditedTime', $metaData); + if (isset($row['silent'])) { + Assert::assertEquals($metaData['silent'], (bool)$row['silent']); + } + if (isset($row['threadTitle'])) { + Assert::assertEquals($metaData['threadTitle'], (bool)$row['threadTitle']); + } + } + + #[When('/^user "([^"]*)" deletes scheduled message "([^"]*)" from room "([^"]*)" with (\d+)(?: \((v1)\))?$/')] + public function userDeletesScheduledMessageFromRoom(string $user, string $message, string $identifier, int $statusCode, string $apiVersion = 'v1', ?TableNode $formData = null): void { + $this->setCurrentUser($user); + $this->sendRequest( + 'DELETE', + '/apps/spreed/api/' . $apiVersion . '/chat/' . self::$identifierToToken[$identifier] . '/schedule/' . self::$textToMessageId[$message], + ); + + $this->assertStatusCode($this->response, $statusCode); + } + + #[Then('/^user "([^"]*)" sees the following scheduled messages in room "([^"]*)" with (\d+)(?: \((v1)\))?$/')] + public function userSeesTheFollowingScheduledMessagesInRoom(string $user, string $identifier, int $statusCode, string $apiVersion = 'v1', ?TableNode $formData = null): void { + $this->setCurrentUser($user); + $this->sendRequest( + 'GET', + '/apps/spreed/api/' . $apiVersion . '/chat/' . self::$identifierToToken[$identifier] . '/schedule', + ); + $this->assertStatusCode($this->response, $statusCode); + + if ($statusCode === 304) { + return; + } + + $data = $this->getDataFromResponse($this->response); + foreach ($data as &$message) { + Assert::assertArrayHasKey('createdAt', $message); + Assert::assertIsInt($message['createdAt']); + unset($message['createdAt']); + $metaData = $message['metaData']; + if (isset($metaData['lastEditedTime'])) { + $metaData['lastEditedTime'] = 0; + } + if (isset($message['parent'])) { + $parent = $message['parent']; + Assert::assertArrayHasKey('message', $parent); + Assert::assertArrayHasKey('actorId', $parent); + $message['parent'] = self::$messageIdToText[$parent['id']]; + } + $message['metaData'] = $metaData; + } + + $expected = $formData->getColumnsHash(); + foreach ($expected as &$row) { + $row['id'] = self::$textToMessageId[$row['message']]; + $row['sendAt'] = (int)$row['sendAt']; + $row['metaData'] = json_decode($row['metaData'], true); + $row['roomId'] = self::$identifierToId[$row['roomId']]; + $row['parentId'] = ($row['parentId'] === 'null' ? null : self::$textToMessageId[$row['parentId']]); + if (isset($row['parent'])) { + $parent = []; + } + if ($row['threadId'] === '-1') { + $row['threadId'] = -1; + $row['threadExists'] = false; + $row['threadTitle'] = $row['metaData']['threadTitle']; + } elseif ($row['threadId'] !== '0') { + $row['threadId'] = self::$titleToThreadId[$row['threadId']]; + $row['threadTitle'] = self::$threadIdToTitle[$row['threadId']]; + $row['threadExists'] = true; + $row['metaData']['threadId'] = $row['threadId']; + } else { + $row['threadId'] = (int)$row['threadId']; + } + } + Assert::assertEquals($expected, $data); + } + #[Then('/^user "([^"]*)" (silent sends|sends) message ("[^"]*"|\'[^\']*\') to room "([^"]*)" with (\d+)(?: \((v1)\))?$/')] public function userSendsMessageToRoom(string $user, string $sendingMode, string $message, string $identifier, int $statusCode, string $apiVersion = 'v1'): void { $this->userPostThreadToRoom($user, $sendingMode, '', $message, $identifier, $statusCode, $apiVersion); diff --git a/tests/integration/features/chat-4/scheduled-messages.feature b/tests/integration/features/chat-4/scheduled-messages.feature new file mode 100644 index 00000000000..50f576dacba --- /dev/null +++ b/tests/integration/features/chat-4/scheduled-messages.feature @@ -0,0 +1,111 @@ +Feature: chat-4/scheduling + Background: + Given user "participant1" exists + Given user "participant2" exists + Given user "participant1" creates room "room" (v4) + | roomType | 2 | + | roomName | room | + And user "participant1" adds user "participant2" to room "room" with 200 (v4) + And user "participant2" sends message "Message" to room "room" with 201 + + Scenario: Schedule a message + When user "participant1" schedules a message to room "room" with 201 + | message | Message 1 | + | sendAt | 1985514582 | + Then user "participant1" sees the following scheduled messages in room "room" with 200 + | id | roomId | actorType | actorId | threadId | parentId | message | messageType | sendAt | metaData | + | Message 1 | room | users | participant1 | 0 | null | Message 1 | comment | 1985514582 | {"threadTitle":"","silent":false,"threadId":0} | + + Scenario: Schedule a silent message + When user "participant1" schedules a message to room "room" with 201 + | message | Message 2 | + | sendAt | 1985514582 | + | silent | true | + Then user "participant1" sees the following scheduled messages in room "room" with 200 + | id | roomId | actorType | actorId | threadId | parentId | message | messageType | sendAt | metaData | + | Message 2 | room | users | participant1 | 0 | null | Message 2 | comment | 1985514582 | {"threadTitle":"","silent":true,"threadId":0} | + + Scenario: Schedule a message reply + When user "participant1" schedules a message to room "room" with 201 + | message | Message 3 | + | sendAt | 1985514582 | + | replyTo | Message | + Then user "participant1" sees the following scheduled messages in room "room" with 200 + | id | roomId | actorType | actorId | threadId | parentId | parent | message | messageType | sendAt | metaData | + | Message 3 | room | users | participant1 | 0 | Message | Message |Message 3 | comment | 1985514582 | {"threadTitle":"","silent":false,"threadId":0} | + + Scenario: Schedule a thread + When user "participant1" schedules a message to room "room" with 201 + | message | Message 4 | + | sendAt | 1985514582 | + | threadTitle | Scheduled Thread | + Then user "participant1" sees the following scheduled messages in room "room" with 200 + | id | roomId | actorType | actorId | threadId | parentId | message | messageType | sendAt | metaData | + | Message 4 | room | users | participant1 | -1 | null | Message 4 | comment | 1985514582 | {"threadTitle":"Scheduled Thread","silent":false,"threadId":-1} | + + Scenario: Schedule a thread reply + Given user "participant1" sends thread "Thread 1" with message "Message 0" to room "room" with 201 + When user "participant1" schedules a message to room "room" with 201 + | message | Message 5 | + | sendAt | 1985514582 | + | threadId | Thread 1 | + Then user "participant1" sees the following scheduled messages in room "room" with 200 + | id | roomId | actorType | actorId | threadId | parentId | message | messageType | sendAt | metaData | + | Message 5 | room | users | participant1 | Thread 1 | null | Message 5 | comment | 1985514582 | {"threadTitle":"Thread 1","silent":false,"threadId":"Thread 1"} | + + Scenario: Schedule a quoted thread reply + Given user "participant1" sends thread "Thread 1" with message "Message 0" to room "room" with 201 + When user "participant1" schedules a message to room "room" with 201 + | message | Message 5 | + | sendAt | 1985514582 | + | replyTo | Message 0 | + | threadId | Thread 1 | + Then user "participant1" sees the following scheduled messages in room "room" with 200 + | id | roomId | actorType | actorId | threadId | parentId | parent | message | messageType | sendAt | metaData | + | Message 5 | room | users | participant1 | Thread 1 | Message 0 | Message 0 | Message 5 | comment | 1985514582 | {"threadTitle":"Thread 1","silent":false,"threadId":"Thread 1"} | + + Scenario: Schedule two messages and delete the first + When user "participant1" schedules a message to room "room" with 201 + | message | Message 1 | + | sendAt | 1985514582 | + | silent | false | + When user "participant1" schedules a message to room "room" with 201 + | message |Message 2 | + | sendAt |1985514584 | + | silent | true | + Then user "participant1" sees the following scheduled messages in room "room" with 200 + | id | roomId | actorType | actorId | threadId | parentId | message | messageType | sendAt | metaData | + | Message 1 | room | users | participant1 | 0 | null | Message 1 | comment | 1985514582 | {"threadTitle":"","silent":false,"threadId":0} | + | Message 2 | room | users | participant1 | 0 | null | Message 2 | comment | 1985514584 | {"threadTitle":"","silent":true,"threadId":0} | + When user "participant1" deletes scheduled message "Message 1" from room "room" with 200 + Then user "participant1" sees the following scheduled messages in room "room" with 200 + | id | roomId | actorType | actorId | threadId | parentId | message | messageType | sendAt | metaData | + | Message 2 | room | users | participant1 | 0 | null | Message 2 | comment | 1985514584 | {"threadTitle":"","silent":true,"threadId":0} | + + Scenario: edit a scheduled message + When user "participant1" schedules a message to room "room" with 201 + | message | Message 1 | + | sendAt | 1985514582 | + Then user "participant1" sees the following scheduled messages in room "room" with 200 + | id | roomId | actorType | actorId | threadId | parentId | message | messageType | sendAt | metaData | + | Message 1 | room | users | participant1 | 0 | null | Message 1 | comment | 1985514582 | {"threadTitle":"","silent":false,"threadId":0} | + When user "participant1" updates scheduled message "Message 1" in room "room" with 202 + | message | Message 1 edited | + | sendAt | 1985514582 | + Then user "participant1" sees the following scheduled messages in room "room" with 200 + | id | roomId | actorType | actorId | threadId | parentId | message | messageType | sendAt | metaData | + | Message 1 edited | room | users | participant1 | 0 | null | Message 1 edited | comment | 1985514582 | {"threadTitle":"","silent":false,"threadId":0,"lastEditedTime":0} | + When user "participant1" updates scheduled message "Message 1" in room "room" with 202 + | message | Message 1 edited | + | sendAt | 1985514582 | + | silent | true | + Then user "participant1" sees the following scheduled messages in room "room" with 200 + | id | roomId | actorType | actorId | threadId | parentId | message | messageType | sendAt | metaData | + | Message 1 edited | room | users | participant1 | 0 | null | Message 1 edited | comment | 1985514582 | {"threadTitle":"","silent":true,"threadId":0,"lastEditedTime":0} | + When user "participant1" updates scheduled message "Message 1" in room "room" with 400 + | message | Message 1 edited | + | sendAt | 1985514582 | + | threadTitle | Abcd | + Then user "participant1" sees the following scheduled messages in room "room" with 200 + | id | roomId | actorType | actorId | threadId | parentId | message | messageType | sendAt | metaData | + | Message 1 edited | room | users | participant1 | 0 | null | Message 1 edited | comment | 1985514582 | {"threadTitle":"","silent":true,"threadId":0,"lastEditedTime":0} | diff --git a/tests/integration/spreedcheats/lib/Controller/ApiController.php b/tests/integration/spreedcheats/lib/Controller/ApiController.php index c9ef896e1c8..c1050cd5a85 100644 --- a/tests/integration/spreedcheats/lib/Controller/ApiController.php +++ b/tests/integration/spreedcheats/lib/Controller/ApiController.php @@ -86,6 +86,9 @@ public function resetSpreed(): DataResponse { $delete = $this->db->getQueryBuilder(); $delete->delete('talk_rooms')->executeStatement(); + $delete = $this->db->getQueryBuilder(); + $delete->delete('talk_scheduled_msg')->executeStatement(); + $delete = $this->db->getQueryBuilder(); $delete->delete('talk_sessions')->executeStatement(); diff --git a/tests/php/Controller/ChatControllerTest.php b/tests/php/Controller/ChatControllerTest.php index 937ec6cb377..e2fc8e7bf94 100644 --- a/tests/php/Controller/ChatControllerTest.php +++ b/tests/php/Controller/ChatControllerTest.php @@ -29,6 +29,7 @@ use OCA\Talk\Service\ProxyCacheMessageService; use OCA\Talk\Service\ReminderService; use OCA\Talk\Service\RoomFormatter; +use OCA\Talk\Service\ScheduledMessageService; use OCA\Talk\Service\SessionService; use OCA\Talk\Service\ThreadService; use OCA\Talk\Share\Helper\Preloader; @@ -97,6 +98,7 @@ class ChatControllerTest extends TestCase { private ?ChatController $controller = null; private Callback $newMessageDateTimeConstraint; + private MockObject&ScheduledMessageService $scheduledMessageService; public function setUp(): void { parent::setUp(); @@ -135,6 +137,7 @@ public function setUp(): void { $this->taskProcessingManager = $this->createMock(ITaskProcessingManager::class); $this->appConfig = $this->createMock(IAppConfig::class); $this->logger = $this->createMock(LoggerInterface::class); + $this->scheduledMessageService = $this->createMock(ScheduledMessageService::class); $this->room = $this->createMock(Room::class); @@ -148,7 +151,7 @@ public function setUp(): void { }); } - private function recreateChatController() { + private function recreateChatController(): void { $this->controller = new ChatController( 'spreed', $this->userId, @@ -186,10 +189,11 @@ private function recreateChatController() { $this->taskProcessingManager, $this->appConfig, $this->logger, + $this->scheduledMessageService, ); } - private function newComment($id, $actorType, $actorId, $creationDateTime, $message) { + private function newComment($id, $actorType, $actorId, $creationDateTime, $message): IComment&MockObject { $comment = $this->createMock(IComment::class); $comment->method('getId')->willReturn($id); @@ -209,7 +213,6 @@ public function testSendMessageByUser(): void { $this->timeFactory->expects($this->once()) ->method('getDateTime') ->willReturn($date); - /** @var IComment&MockObject $comment */ $comment = $this->newComment(42, 'user', $this->userId, $date, 'testMessage'); $this->chatManager->expects($this->once()) ->method('sendMessage') @@ -280,7 +283,6 @@ public function testSendMessageByUserWithReferenceId(): void { $this->timeFactory->expects($this->once()) ->method('getDateTime') ->willReturn($date); - /** @var IComment&MockObject $comment */ $comment = $this->newComment(42, 'user', $this->userId, $date, 'testMessage'); $this->chatManager->expects($this->once()) ->method('sendMessage') @@ -355,7 +357,6 @@ public function testSendReplyByUser(): void { /** @var IComment&MockObject $comment */ $parent = $this->newComment(23, 'users', $this->userId . '2', $date, 'testMessage original'); - /** @var IComment&MockObject $comment */ $comment = $this->newComment(42, 'users', $this->userId, $date, 'testMessage'); $this->chatManager->expects($this->once()) ->method('sendMessage') @@ -513,7 +514,6 @@ public function testSendMessageByUserNotJoinedButInRoom(): void { $this->timeFactory->expects($this->once()) ->method('getDateTime') ->willReturn($date); - /** @var IComment&MockObject $comment */ $comment = $this->newComment(23, 'user', $this->userId, $date, 'testMessage'); $this->chatManager->expects($this->once()) ->method('sendMessage') @@ -592,7 +592,6 @@ public function testSendMessageByGuest(): void { $this->timeFactory->expects($this->once()) ->method('getDateTime') ->willReturn($date); - /** @var IComment&MockObject $comment */ $comment = $this->newComment(64, 'guest', sha1('testSpreedSession'), $date, 'testMessage'); $this->chatManager->expects($this->once()) ->method('sendMessage') @@ -674,7 +673,6 @@ public function testShareObjectToChatByUser(): void { $this->timeFactory->expects($this->once()) ->method('getDateTime') ->willReturn($date); - /** @var IComment&MockObject $comment */ $comment = $this->newComment(42, 'user', $this->userId, $date, 'testMessage'); $this->chatManager->expects($this->once()) ->method('addSystemMessage') @@ -963,7 +961,7 @@ public function testReceiveMessagesByGuest(): void { public function testWaitForNewMessagesByUser(): void { $testUser = $this->createMock(IUser::class); - $testUser->expects($this->any()) + $testUser ->method('getUID') ->willReturn('testUser'); @@ -1044,7 +1042,7 @@ public function testWaitForNewMessagesByUser(): void { public function testWaitForNewMessagesTimeoutExpired(): void { $participant = $this->createMock(Participant::class); $testUser = $this->createMock(IUser::class); - $testUser->expects($this->any()) + $testUser ->method('getUID') ->willReturn('testUser'); @@ -1056,7 +1054,7 @@ public function testWaitForNewMessagesTimeoutExpired(): void { ->with($this->room, $offset, $limit, $timeout, $testUser) ->willReturn([]); - $this->userManager->expects($this->any()) + $this->userManager ->method('get') ->with('testUser') ->willReturn($testUser); @@ -1072,7 +1070,7 @@ public function testWaitForNewMessagesTimeoutExpired(): void { public function testWaitForNewMessagesTimeoutTooLarge(): void { $participant = $this->createMock(Participant::class); $testUser = $this->createMock(IUser::class); - $testUser->expects($this->any()) + $testUser ->method('getUID') ->willReturn('testUser'); @@ -1085,7 +1083,7 @@ public function testWaitForNewMessagesTimeoutTooLarge(): void { ->with($this->room, $offset, $limit, $maximumTimeout, $testUser) ->willReturn([]); - $this->userManager->expects($this->any()) + $this->userManager ->method('get') ->with('testUser') ->willReturn($testUser); @@ -1129,7 +1127,7 @@ public static function dataMentions(): array { #[DataProvider('dataMentions')] public function testMentions(string $search, int $limit, array $result, array $expected): void { $participant = $this->createMock(Participant::class); - $this->room->expects($this->any()) + $this->room ->method('getId') ->willReturn(1234); From 92a9e519912928d3cb6e1340a81209a6ad16d2bc Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Fri, 5 Dec 2025 14:29:33 +0100 Subject: [PATCH 2/5] fix(scheduled): Fix attendee has_scheduled_messages Signed-off-by: Joas Schilling --- lib/Model/AttendeeMapper.php | 1 + lib/Model/SelectHelper.php | 3 +- lib/Service/ParticipantService.php | 1 + .../features/bootstrap/FeatureContext.php | 9 ++- .../chat-4/scheduled-messages.feature | 62 +++++++++++++++---- tests/php/Chat/ChatManagerTest.php | 3 + 6 files changed, 64 insertions(+), 15 deletions(-) diff --git a/lib/Model/AttendeeMapper.php b/lib/Model/AttendeeMapper.php index 73bff825be3..eae1a24f146 100644 --- a/lib/Model/AttendeeMapper.php +++ b/lib/Model/AttendeeMapper.php @@ -315,6 +315,7 @@ public function createAttendeeFromRow(array $row): Attendee { 'has_unread_thread_mentions' => (bool)$row['has_unread_thread_mentions'], 'has_unread_thread_directs' => (bool)$row['has_unread_thread_directs'], 'hidden_pinned_id' => (int)$row['hidden_pinned_id'], + 'has_scheduled_messages' => (bool)$row['has_scheduled_messages'], ]); } } diff --git a/lib/Model/SelectHelper.php b/lib/Model/SelectHelper.php index ea0b512cd87..5db199b33e2 100644 --- a/lib/Model/SelectHelper.php +++ b/lib/Model/SelectHelper.php @@ -112,7 +112,8 @@ public function selectAttendeesTable(IQueryBuilder $query, string $alias = 'a'): $alias . 'has_unread_threads', $alias . 'has_unread_thread_mentions', $alias . 'has_unread_thread_directs', - $alias . 'hidden_pinned_id' + $alias . 'hidden_pinned_id', + $alias . 'has_scheduled_messages', ])->selectAlias($alias . 'id', 'a_id'); } diff --git a/lib/Service/ParticipantService.php b/lib/Service/ParticipantService.php index a190a290cc0..c4290a0c653 100644 --- a/lib/Service/ParticipantService.php +++ b/lib/Service/ParticipantService.php @@ -2385,6 +2385,7 @@ public function getParticipantByActor(Room $room, string $actorType, string $act public function setHasScheduledMessages(Participant $participant, bool $hasScheduledMessages): void { $attendee = $participant->getAttendee(); $attendee->setHasScheduledMessages($hasScheduledMessages); + $attendee->setLastAttendeeActivity($this->timeFactory->getTime()); $this->attendeeMapper->update($attendee); } } diff --git a/tests/integration/features/bootstrap/FeatureContext.php b/tests/integration/features/bootstrap/FeatureContext.php index 78fcd39e72d..f9c4548a1bc 100644 --- a/tests/integration/features/bootstrap/FeatureContext.php +++ b/tests/integration/features/bootstrap/FeatureContext.php @@ -514,6 +514,9 @@ private function assertRooms(array $rooms, TableNode $formData, bool $shouldOrde if (!empty($expectedRoom['lobbyTimer'])) { $data['lobbyTimer'] = (int)$room['lobbyTimer']; } + if (!empty($expectedRoom['hasScheduledMessages'])) { + $data['hasScheduledMessages'] = $room['hasScheduledMessages'] ? 'true' : 'false'; + } if (isset($expectedRoom['hiddenPinnedId'])) { if ($room['hiddenPinnedId'] === 0) { $data['hiddenPinnedId'] = 'EMPTY'; @@ -2020,11 +2023,13 @@ public function userSeesTheFollowingScheduledMessagesInRoom(string $user, string ); $this->assertStatusCode($this->response, $statusCode); - if ($statusCode === 304) { + $expected = $formData?->getColumnsHash(); + $data = $this->getDataFromResponse($this->response); + if (empty($expected)) { + Assert::assertEmpty($data); return; } - $data = $this->getDataFromResponse($this->response); foreach ($data as &$message) { Assert::assertArrayHasKey('createdAt', $message); Assert::assertIsInt($message['createdAt']); diff --git a/tests/integration/features/chat-4/scheduled-messages.feature b/tests/integration/features/chat-4/scheduled-messages.feature index 50f576dacba..46d32a603a1 100644 --- a/tests/integration/features/chat-4/scheduled-messages.feature +++ b/tests/integration/features/chat-4/scheduled-messages.feature @@ -9,12 +9,18 @@ Feature: chat-4/scheduling And user "participant2" sends message "Message" to room "room" with 201 Scenario: Schedule a message + Then user "participant1" is participant of the following rooms (v4) + | id | type | hasScheduledMessages | + | room | 2 | false | When user "participant1" schedules a message to room "room" with 201 | message | Message 1 | | sendAt | 1985514582 | Then user "participant1" sees the following scheduled messages in room "room" with 200 - | id | roomId | actorType | actorId | threadId | parentId | message | messageType | sendAt | metaData | + | id | roomId | actorType | actorId | threadId | parentId | message | messageType | sendAt | metaData | | Message 1 | room | users | participant1 | 0 | null | Message 1 | comment | 1985514582 | {"threadTitle":"","silent":false,"threadId":0} | + Then user "participant1" is participant of the following rooms (v4) + | id | type | hasScheduledMessages | + | room | 2 | true | Scenario: Schedule a silent message When user "participant1" schedules a message to room "room" with 201 @@ -22,8 +28,11 @@ Feature: chat-4/scheduling | sendAt | 1985514582 | | silent | true | Then user "participant1" sees the following scheduled messages in room "room" with 200 - | id | roomId | actorType | actorId | threadId | parentId | message | messageType | sendAt | metaData | + | id | roomId | actorType | actorId | threadId | parentId | message | messageType | sendAt | metaData | | Message 2 | room | users | participant1 | 0 | null | Message 2 | comment | 1985514582 | {"threadTitle":"","silent":true,"threadId":0} | + Then user "participant1" is participant of the following rooms (v4) + | id | type | hasScheduledMessages | + | room | 2 | true | Scenario: Schedule a message reply When user "participant1" schedules a message to room "room" with 201 @@ -31,8 +40,11 @@ Feature: chat-4/scheduling | sendAt | 1985514582 | | replyTo | Message | Then user "participant1" sees the following scheduled messages in room "room" with 200 - | id | roomId | actorType | actorId | threadId | parentId | parent | message | messageType | sendAt | metaData | + | id | roomId | actorType | actorId | threadId | parentId | parent | message | messageType | sendAt | metaData | | Message 3 | room | users | participant1 | 0 | Message | Message |Message 3 | comment | 1985514582 | {"threadTitle":"","silent":false,"threadId":0} | + Then user "participant1" is participant of the following rooms (v4) + | id | type | hasScheduledMessages | + | room | 2 | true | Scenario: Schedule a thread When user "participant1" schedules a message to room "room" with 201 @@ -40,8 +52,11 @@ Feature: chat-4/scheduling | sendAt | 1985514582 | | threadTitle | Scheduled Thread | Then user "participant1" sees the following scheduled messages in room "room" with 200 - | id | roomId | actorType | actorId | threadId | parentId | message | messageType | sendAt | metaData | + | id | roomId | actorType | actorId | threadId | parentId | message | messageType | sendAt | metaData | | Message 4 | room | users | participant1 | -1 | null | Message 4 | comment | 1985514582 | {"threadTitle":"Scheduled Thread","silent":false,"threadId":-1} | + Then user "participant1" is participant of the following rooms (v4) + | id | type | hasScheduledMessages | + | room | 2 | true | Scenario: Schedule a thread reply Given user "participant1" sends thread "Thread 1" with message "Message 0" to room "room" with 201 @@ -50,8 +65,11 @@ Feature: chat-4/scheduling | sendAt | 1985514582 | | threadId | Thread 1 | Then user "participant1" sees the following scheduled messages in room "room" with 200 - | id | roomId | actorType | actorId | threadId | parentId | message | messageType | sendAt | metaData | + | id | roomId | actorType | actorId | threadId | parentId | message | messageType | sendAt | metaData | | Message 5 | room | users | participant1 | Thread 1 | null | Message 5 | comment | 1985514582 | {"threadTitle":"Thread 1","silent":false,"threadId":"Thread 1"} | + Then user "participant1" is participant of the following rooms (v4) + | id | type | hasScheduledMessages | + | room | 2 | true | Scenario: Schedule a quoted thread reply Given user "participant1" sends thread "Thread 1" with message "Message 0" to room "room" with 201 @@ -61,8 +79,11 @@ Feature: chat-4/scheduling | replyTo | Message 0 | | threadId | Thread 1 | Then user "participant1" sees the following scheduled messages in room "room" with 200 - | id | roomId | actorType | actorId | threadId | parentId | parent | message | messageType | sendAt | metaData | + | id | roomId | actorType | actorId | threadId | parentId | parent | message | messageType | sendAt | metaData | | Message 5 | room | users | participant1 | Thread 1 | Message 0 | Message 0 | Message 5 | comment | 1985514582 | {"threadTitle":"Thread 1","silent":false,"threadId":"Thread 1"} | + Then user "participant1" is participant of the following rooms (v4) + | id | type | hasScheduledMessages | + | room | 2 | true | Scenario: Schedule two messages and delete the first When user "participant1" schedules a message to room "room" with 201 @@ -74,38 +95,55 @@ Feature: chat-4/scheduling | sendAt |1985514584 | | silent | true | Then user "participant1" sees the following scheduled messages in room "room" with 200 - | id | roomId | actorType | actorId | threadId | parentId | message | messageType | sendAt | metaData | + | id | roomId | actorType | actorId | threadId | parentId | message | messageType | sendAt | metaData | | Message 1 | room | users | participant1 | 0 | null | Message 1 | comment | 1985514582 | {"threadTitle":"","silent":false,"threadId":0} | | Message 2 | room | users | participant1 | 0 | null | Message 2 | comment | 1985514584 | {"threadTitle":"","silent":true,"threadId":0} | + Then user "participant1" is participant of the following rooms (v4) + | id | type | hasScheduledMessages | + | room | 2 | true | When user "participant1" deletes scheduled message "Message 1" from room "room" with 200 Then user "participant1" sees the following scheduled messages in room "room" with 200 - | id | roomId | actorType | actorId | threadId | parentId | message | messageType | sendAt | metaData | + | id | roomId | actorType | actorId | threadId | parentId | message | messageType | sendAt | metaData | | Message 2 | room | users | participant1 | 0 | null | Message 2 | comment | 1985514584 | {"threadTitle":"","silent":true,"threadId":0} | + Then user "participant1" is participant of the following rooms (v4) + | id | type | hasScheduledMessages | + | room | 2 | true | + When user "participant1" deletes scheduled message "Message 2" from room "room" with 200 + Then user "participant1" sees the following scheduled messages in room "room" with 200 + Then user "participant1" is participant of the following rooms (v4) + | id | type | hasScheduledMessages | + | room | 2 | false | Scenario: edit a scheduled message When user "participant1" schedules a message to room "room" with 201 | message | Message 1 | | sendAt | 1985514582 | Then user "participant1" sees the following scheduled messages in room "room" with 200 - | id | roomId | actorType | actorId | threadId | parentId | message | messageType | sendAt | metaData | + | id | roomId | actorType | actorId | threadId | parentId | message | messageType | sendAt | metaData | | Message 1 | room | users | participant1 | 0 | null | Message 1 | comment | 1985514582 | {"threadTitle":"","silent":false,"threadId":0} | + Then user "participant1" is participant of the following rooms (v4) + | id | type | hasScheduledMessages | + | room | 2 | true | When user "participant1" updates scheduled message "Message 1" in room "room" with 202 | message | Message 1 edited | | sendAt | 1985514582 | + Then user "participant1" is participant of the following rooms (v4) + | id | type | hasScheduledMessages | + | room | 2 | true | Then user "participant1" sees the following scheduled messages in room "room" with 200 - | id | roomId | actorType | actorId | threadId | parentId | message | messageType | sendAt | metaData | + | id | roomId | actorType | actorId | threadId | parentId | message | messageType | sendAt | metaData | | Message 1 edited | room | users | participant1 | 0 | null | Message 1 edited | comment | 1985514582 | {"threadTitle":"","silent":false,"threadId":0,"lastEditedTime":0} | When user "participant1" updates scheduled message "Message 1" in room "room" with 202 | message | Message 1 edited | | sendAt | 1985514582 | | silent | true | Then user "participant1" sees the following scheduled messages in room "room" with 200 - | id | roomId | actorType | actorId | threadId | parentId | message | messageType | sendAt | metaData | + | id | roomId | actorType | actorId | threadId | parentId | message | messageType | sendAt | metaData | | Message 1 edited | room | users | participant1 | 0 | null | Message 1 edited | comment | 1985514582 | {"threadTitle":"","silent":true,"threadId":0,"lastEditedTime":0} | When user "participant1" updates scheduled message "Message 1" in room "room" with 400 | message | Message 1 edited | | sendAt | 1985514582 | | threadTitle | Abcd | Then user "participant1" sees the following scheduled messages in room "room" with 200 - | id | roomId | actorType | actorId | threadId | parentId | message | messageType | sendAt | metaData | + | id | roomId | actorType | actorId | threadId | parentId | message | messageType | sendAt | metaData | | Message 1 edited | room | users | participant1 | 0 | null | Message 1 edited | comment | 1985514582 | {"threadTitle":"","silent":true,"threadId":0,"lastEditedTime":0} | diff --git a/tests/php/Chat/ChatManagerTest.php b/tests/php/Chat/ChatManagerTest.php index 47f644fc0ef..19a29540a56 100644 --- a/tests/php/Chat/ChatManagerTest.php +++ b/tests/php/Chat/ChatManagerTest.php @@ -443,6 +443,7 @@ public function testDeleteMessage(): void { 'has_unread_thread_mentions' => false, 'has_unread_thread_directs' => false, 'hidden_pinned_id' => 0, + 'has_scheduled_messages' => false, ]); $chat = $this->createMock(Room::class); $chat->expects($this->any()) @@ -512,6 +513,7 @@ public function testDeleteMessageFileShare(): void { 'has_unread_thread_mentions' => false, 'has_unread_thread_directs' => false, 'hidden_pinned_id' => 0, + 'has_scheduled_messages' => false, ]); $chat = $this->createMock(Room::class); $chat->expects($this->any()) @@ -603,6 +605,7 @@ public function testDeleteMessageFileShareNotFound(): void { 'has_unread_thread_mentions' => false, 'has_unread_thread_directs' => false, 'hidden_pinned_id' => 0, + 'has_scheduled_messages' => false, ]); $chat = $this->createMock(Room::class); $chat->expects($this->any()) From 494ce57ce55f88b811c442e230ddfff02ac3ca2c Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Fri, 5 Dec 2025 15:24:46 +0100 Subject: [PATCH 3/5] fix(scheduled): Remove roomId and parentId from response and make id a string Signed-off-by: Joas Schilling --- lib/Model/ScheduledMessage.php | 25 +++++----- lib/ResponseDefinitions.php | 33 +++++------- .../features/bootstrap/FeatureContext.php | 32 ++---------- .../chat-4/scheduled-messages.feature | 50 +++++++++---------- 4 files changed, 53 insertions(+), 87 deletions(-) diff --git a/lib/Model/ScheduledMessage.php b/lib/Model/ScheduledMessage.php index d4398d47eee..68ed5bebd0e 100644 --- a/lib/Model/ScheduledMessage.php +++ b/lib/Model/ScheduledMessage.php @@ -37,7 +37,6 @@ * @method \DateTime|null getSendAt() * * @psalm-import-type TalkScheduledMessage from ResponseDefinitions - * @psalm-import-type TalkScheduledMessageMetaData from ResponseDefinitions */ class ScheduledMessage extends Entity implements \JsonSerializable { public const METADATA_THREAD_TITLE = 'threadTitle'; @@ -70,13 +69,16 @@ public function __construct() { } /** - * @return TalkScheduledMessageMetaData + * @return array{silent: bool, threadId: int, threadTitle?: string, lastEditedTime?: int} */ public function getDecodedMetaData(): array { return json_decode($this->metaData, true, 512, JSON_THROW_ON_ERROR); } - public function setMetaData(?array $metaData): void { + /** + * @param array{silent: bool, threadId: int, threadTitle?: string, lastEditedTime?: int} $metaData + */ + public function setMetaData(array $metaData): void { $this->metaData = json_encode($metaData, JSON_THROW_ON_ERROR); $this->markFieldUpdated('metaData'); } @@ -110,36 +112,33 @@ public function jsonSerialize(): array { } /** + * @param string $format + * @psalm-param 'json'|'xml' $format * @return TalkScheduledMessage */ - public function toArray(?Message $parent, ?Thread $thread) : array { + public function toArray(string $format, ?Message $parent, ?Thread $thread) : array { + $metaData = $this->getDecodedMetaData(); $data = [ - 'id' => $this->id, - 'roomId' => $this->getRoomId(), + 'id' => (string)$this->id, 'actorId' => $this->getActorId(), 'actorType' => $this->getActorType(), 'threadId' => $this->getThreadId(), - 'parentId' => $this->getParentId(), 'message' => $this->getMessage(), 'messageType' => $this->getMessageType(), 'createdAt' => $this->getCreatedAt()->getTimestamp(), - 'sendAt' => $this->getSendAt()?->getTimestamp(), + 'sendAt' => $this->getSendAt()?->getTimestamp() ?? 0, + 'silent' => $metaData['silent'] ?? false, ]; if ($parent !== null) { $data['parent'] = $parent->toArray('json', $thread); } - $metaData = $this->getDecodedMetaData(); if ($thread !== null) { - $data['threadExists'] = true; $data['threadTitle'] = $thread->getName(); - $metaData[self::METADATA_THREAD_TITLE] = $thread->getName(); } elseif (isset($metaData[self::METADATA_THREAD_TITLE]) && $this->getThreadId() === Thread::THREAD_CREATE) { - $data['threadExists'] = false; $data['threadTitle'] = (string)$metaData[self::METADATA_THREAD_TITLE]; } - $data['metaData'] = $metaData; return $data; } } diff --git a/lib/ResponseDefinitions.php b/lib/ResponseDefinitions.php index 2318442d5f8..2de01080c14 100644 --- a/lib/ResponseDefinitions.php +++ b/lib/ResponseDefinitions.php @@ -566,28 +566,19 @@ * }, * } * - * @psalm-type TalkScheduledMessageMetaData = array{ - * threadId: int, - * threadTitle: string, - * silent: bool, - * lastEditedTime?: int, - * } - * * @psalm-type TalkScheduledMessage = array{ - * id: int, - * roomId: int, - * actorId: string, - * actorType: string, - * threadId: int, - * threadExists?: boolean, - * threadTitle?: string, - * parentId: ?int, - * parent?: TalkChatMessage, - * message: string, - * messageType: string, - * createdAt: int, - * sendAt: ?int, - * metaData: TalkScheduledMessageMetaData, + * // SnowflakeID + * id: string, + * actorId: string, + * actorType: string, + * threadId: int, + * threadTitle?: string, + * parent?: TalkChatMessage, + * message: string, + * messageType: string, + * createdAt: int, + * sendAt: int, + * silent: bool, * } */ class ResponseDefinitions { diff --git a/tests/integration/features/bootstrap/FeatureContext.php b/tests/integration/features/bootstrap/FeatureContext.php index f9c4548a1bc..bd84f5caebf 100644 --- a/tests/integration/features/bootstrap/FeatureContext.php +++ b/tests/integration/features/bootstrap/FeatureContext.php @@ -1989,18 +1989,6 @@ public function userUpdatesScheduledMessageInRoom(string $user, string $message, self::$messageIdToText[$response['id']] = $row['message']; Assert::assertEquals($row['message'], $response['message']); Assert::assertEquals($row['sendAt'], $response['sendAt']); - Assert::assertArrayHasKey('metaData', $response); - $metaData = $response['metaData']; - Assert::assertArrayHasKey('silent', $metaData); - Assert::assertArrayHasKey('threadTitle', $metaData); - Assert::assertArrayHasKey('threadId', $metaData); - Assert::assertArrayHasKey('lastEditedTime', $metaData); - if (isset($row['silent'])) { - Assert::assertEquals($metaData['silent'], (bool)$row['silent']); - } - if (isset($row['threadTitle'])) { - Assert::assertEquals($metaData['threadTitle'], (bool)$row['threadTitle']); - } } #[When('/^user "([^"]*)" deletes scheduled message "([^"]*)" from room "([^"]*)" with (\d+)(?: \((v1)\))?$/')] @@ -2034,38 +2022,26 @@ public function userSeesTheFollowingScheduledMessagesInRoom(string $user, string Assert::assertArrayHasKey('createdAt', $message); Assert::assertIsInt($message['createdAt']); unset($message['createdAt']); - $metaData = $message['metaData']; - if (isset($metaData['lastEditedTime'])) { - $metaData['lastEditedTime'] = 0; - } if (isset($message['parent'])) { $parent = $message['parent']; Assert::assertArrayHasKey('message', $parent); Assert::assertArrayHasKey('actorId', $parent); $message['parent'] = self::$messageIdToText[$parent['id']]; + } else { + $message['parent'] = 'null'; } - $message['metaData'] = $metaData; } $expected = $formData->getColumnsHash(); foreach ($expected as &$row) { $row['id'] = self::$textToMessageId[$row['message']]; $row['sendAt'] = (int)$row['sendAt']; - $row['metaData'] = json_decode($row['metaData'], true); - $row['roomId'] = self::$identifierToId[$row['roomId']]; - $row['parentId'] = ($row['parentId'] === 'null' ? null : self::$textToMessageId[$row['parentId']]); - if (isset($row['parent'])) { - $parent = []; - } + $row['silent'] = $row['silent'] === 'true'; if ($row['threadId'] === '-1') { $row['threadId'] = -1; - $row['threadExists'] = false; - $row['threadTitle'] = $row['metaData']['threadTitle']; } elseif ($row['threadId'] !== '0') { + $row['threadTitle'] = $row['threadId']; $row['threadId'] = self::$titleToThreadId[$row['threadId']]; - $row['threadTitle'] = self::$threadIdToTitle[$row['threadId']]; - $row['threadExists'] = true; - $row['metaData']['threadId'] = $row['threadId']; } else { $row['threadId'] = (int)$row['threadId']; } diff --git a/tests/integration/features/chat-4/scheduled-messages.feature b/tests/integration/features/chat-4/scheduled-messages.feature index 46d32a603a1..602ce355e4b 100644 --- a/tests/integration/features/chat-4/scheduled-messages.feature +++ b/tests/integration/features/chat-4/scheduled-messages.feature @@ -16,8 +16,8 @@ Feature: chat-4/scheduling | message | Message 1 | | sendAt | 1985514582 | Then user "participant1" sees the following scheduled messages in room "room" with 200 - | id | roomId | actorType | actorId | threadId | parentId | message | messageType | sendAt | metaData | - | Message 1 | room | users | participant1 | 0 | null | Message 1 | comment | 1985514582 | {"threadTitle":"","silent":false,"threadId":0} | + | id | actorType | actorId | threadId | parent | message | messageType | sendAt | silent | + | Message 1 | users | participant1 | 0 | null | Message 1 | comment | 1985514582 | false | Then user "participant1" is participant of the following rooms (v4) | id | type | hasScheduledMessages | | room | 2 | true | @@ -28,8 +28,8 @@ Feature: chat-4/scheduling | sendAt | 1985514582 | | silent | true | Then user "participant1" sees the following scheduled messages in room "room" with 200 - | id | roomId | actorType | actorId | threadId | parentId | message | messageType | sendAt | metaData | - | Message 2 | room | users | participant1 | 0 | null | Message 2 | comment | 1985514582 | {"threadTitle":"","silent":true,"threadId":0} | + | id | actorType | actorId | threadId | parent | message | messageType | sendAt | silent | + | Message 2 | users | participant1 | 0 | null | Message 2 | comment | 1985514582 | true | Then user "participant1" is participant of the following rooms (v4) | id | type | hasScheduledMessages | | room | 2 | true | @@ -40,8 +40,8 @@ Feature: chat-4/scheduling | sendAt | 1985514582 | | replyTo | Message | Then user "participant1" sees the following scheduled messages in room "room" with 200 - | id | roomId | actorType | actorId | threadId | parentId | parent | message | messageType | sendAt | metaData | - | Message 3 | room | users | participant1 | 0 | Message | Message |Message 3 | comment | 1985514582 | {"threadTitle":"","silent":false,"threadId":0} | + | id | actorType | actorId | threadId | parent | message | messageType | sendAt | silent | + | Message 3 | users | participant1 | 0 | Message |Message 3 | comment | 1985514582 | false | Then user "participant1" is participant of the following rooms (v4) | id | type | hasScheduledMessages | | room | 2 | true | @@ -52,8 +52,8 @@ Feature: chat-4/scheduling | sendAt | 1985514582 | | threadTitle | Scheduled Thread | Then user "participant1" sees the following scheduled messages in room "room" with 200 - | id | roomId | actorType | actorId | threadId | parentId | message | messageType | sendAt | metaData | - | Message 4 | room | users | participant1 | -1 | null | Message 4 | comment | 1985514582 | {"threadTitle":"Scheduled Thread","silent":false,"threadId":-1} | + | id | actorType | actorId | threadId | threadTitle | parent | message | messageType | sendAt | silent | + | Message 4 | users | participant1 | -1 | Scheduled Thread | null | Message 4 | comment | 1985514582 | false | Then user "participant1" is participant of the following rooms (v4) | id | type | hasScheduledMessages | | room | 2 | true | @@ -65,8 +65,8 @@ Feature: chat-4/scheduling | sendAt | 1985514582 | | threadId | Thread 1 | Then user "participant1" sees the following scheduled messages in room "room" with 200 - | id | roomId | actorType | actorId | threadId | parentId | message | messageType | sendAt | metaData | - | Message 5 | room | users | participant1 | Thread 1 | null | Message 5 | comment | 1985514582 | {"threadTitle":"Thread 1","silent":false,"threadId":"Thread 1"} | + | id | actorType | actorId | threadId | parent | message | messageType | sendAt | silent | + | Message 5 | users | participant1 | Thread 1 | null | Message 5 | comment | 1985514582 | false | Then user "participant1" is participant of the following rooms (v4) | id | type | hasScheduledMessages | | room | 2 | true | @@ -79,8 +79,8 @@ Feature: chat-4/scheduling | replyTo | Message 0 | | threadId | Thread 1 | Then user "participant1" sees the following scheduled messages in room "room" with 200 - | id | roomId | actorType | actorId | threadId | parentId | parent | message | messageType | sendAt | metaData | - | Message 5 | room | users | participant1 | Thread 1 | Message 0 | Message 0 | Message 5 | comment | 1985514582 | {"threadTitle":"Thread 1","silent":false,"threadId":"Thread 1"} | + | id | actorType | actorId | threadId | parent | message | messageType | sendAt | silent | + | Message 5 | users | participant1 | Thread 1 | Message 0 | Message 5 | comment | 1985514582 | false | Then user "participant1" is participant of the following rooms (v4) | id | type | hasScheduledMessages | | room | 2 | true | @@ -95,16 +95,16 @@ Feature: chat-4/scheduling | sendAt |1985514584 | | silent | true | Then user "participant1" sees the following scheduled messages in room "room" with 200 - | id | roomId | actorType | actorId | threadId | parentId | message | messageType | sendAt | metaData | - | Message 1 | room | users | participant1 | 0 | null | Message 1 | comment | 1985514582 | {"threadTitle":"","silent":false,"threadId":0} | - | Message 2 | room | users | participant1 | 0 | null | Message 2 | comment | 1985514584 | {"threadTitle":"","silent":true,"threadId":0} | + | id | actorType | actorId | threadId | parent | message | messageType | sendAt | silent | + | Message 1 | users | participant1 | 0 | null | Message 1 | comment | 1985514582 | false | + | Message 2 | users | participant1 | 0 | null | Message 2 | comment | 1985514584 | true | Then user "participant1" is participant of the following rooms (v4) | id | type | hasScheduledMessages | | room | 2 | true | When user "participant1" deletes scheduled message "Message 1" from room "room" with 200 Then user "participant1" sees the following scheduled messages in room "room" with 200 - | id | roomId | actorType | actorId | threadId | parentId | message | messageType | sendAt | metaData | - | Message 2 | room | users | participant1 | 0 | null | Message 2 | comment | 1985514584 | {"threadTitle":"","silent":true,"threadId":0} | + | id | actorType | actorId | threadId | parent | message | messageType | sendAt | silent | + | Message 2 | users | participant1 | 0 | null | Message 2 | comment | 1985514584 | true | Then user "participant1" is participant of the following rooms (v4) | id | type | hasScheduledMessages | | room | 2 | true | @@ -119,8 +119,8 @@ Feature: chat-4/scheduling | message | Message 1 | | sendAt | 1985514582 | Then user "participant1" sees the following scheduled messages in room "room" with 200 - | id | roomId | actorType | actorId | threadId | parentId | message | messageType | sendAt | metaData | - | Message 1 | room | users | participant1 | 0 | null | Message 1 | comment | 1985514582 | {"threadTitle":"","silent":false,"threadId":0} | + | id | actorType | actorId | threadId | parent | message | messageType | sendAt | silent | + | Message 1 | users | participant1 | 0 | null | Message 1 | comment | 1985514582 | false | Then user "participant1" is participant of the following rooms (v4) | id | type | hasScheduledMessages | | room | 2 | true | @@ -131,19 +131,19 @@ Feature: chat-4/scheduling | id | type | hasScheduledMessages | | room | 2 | true | Then user "participant1" sees the following scheduled messages in room "room" with 200 - | id | roomId | actorType | actorId | threadId | parentId | message | messageType | sendAt | metaData | - | Message 1 edited | room | users | participant1 | 0 | null | Message 1 edited | comment | 1985514582 | {"threadTitle":"","silent":false,"threadId":0,"lastEditedTime":0} | + | id | actorType | actorId | threadId | parent | message | messageType | sendAt | silent | + | Message 1 edited | users | participant1 | 0 | null | Message 1 edited | comment | 1985514582 | false | When user "participant1" updates scheduled message "Message 1" in room "room" with 202 | message | Message 1 edited | | sendAt | 1985514582 | | silent | true | Then user "participant1" sees the following scheduled messages in room "room" with 200 - | id | roomId | actorType | actorId | threadId | parentId | message | messageType | sendAt | metaData | - | Message 1 edited | room | users | participant1 | 0 | null | Message 1 edited | comment | 1985514582 | {"threadTitle":"","silent":true,"threadId":0,"lastEditedTime":0} | + | id | actorType | actorId | threadId | parent | message | messageType | sendAt | silent | + | Message 1 edited | users | participant1 | 0 | null | Message 1 edited | comment | 1985514582 | true | When user "participant1" updates scheduled message "Message 1" in room "room" with 400 | message | Message 1 edited | | sendAt | 1985514582 | | threadTitle | Abcd | Then user "participant1" sees the following scheduled messages in room "room" with 200 - | id | roomId | actorType | actorId | threadId | parentId | message | messageType | sendAt | metaData | - | Message 1 edited | room | users | participant1 | 0 | null | Message 1 edited | comment | 1985514582 | {"threadTitle":"","silent":true,"threadId":0,"lastEditedTime":0} | + | id | actorType | actorId | threadId | parent | message | messageType | sendAt | silent | + | Message 1 edited | users | participant1 | 0 | null | Message 1 edited | comment | 1985514582 | true | From ae0c19981c3d198e9832ad3bb7a77b55650c04ff Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Fri, 5 Dec 2025 15:32:29 +0100 Subject: [PATCH 4/5] fix(scheduled): Fix response format Signed-off-by: Joas Schilling --- lib/Controller/ChatController.php | 5 +++-- lib/Model/ScheduledMessage.php | 20 ++------------------ lib/Service/ScheduledMessageService.php | 8 ++++---- 3 files changed, 9 insertions(+), 24 deletions(-) diff --git a/lib/Controller/ChatController.php b/lib/Controller/ChatController.php index c2f81294fb8..4adec756fc8 100644 --- a/lib/Controller/ChatController.php +++ b/lib/Controller/ChatController.php @@ -366,6 +366,7 @@ public function getScheduledMessages(): DataResponse { $scheduledMessages = $this->scheduledMessageManager->getMessages( $this->room, $this->participant, + $this->getResponseFormat(), ); return new DataResponse($scheduledMessages, Http::STATUS_OK); @@ -465,7 +466,7 @@ public function scheduleMessage( return new DataResponse(['error' => 'message'], Http::STATUS_REQUEST_ENTITY_TOO_LARGE); } - $data = $this->scheduledMessageManager->parseScheduledMessage($scheduledMessage, $parentMessage); + $data = $this->scheduledMessageManager->parseScheduledMessage($this->getResponseFormat(), $scheduledMessage, $parentMessage); return new DataResponse($data, Http::STATUS_CREATED); } @@ -545,7 +546,7 @@ public function editScheduledMessage( } } - $data = $this->scheduledMessageManager->parseScheduledMessage($scheduledMessage, $parentMessage); + $data = $this->scheduledMessageManager->parseScheduledMessage($this->getResponseFormat(), $scheduledMessage, $parentMessage); return new DataResponse($data, Http::STATUS_ACCEPTED); } diff --git a/lib/Model/ScheduledMessage.php b/lib/Model/ScheduledMessage.php index 68ed5bebd0e..e74fdb58c2f 100644 --- a/lib/Model/ScheduledMessage.php +++ b/lib/Model/ScheduledMessage.php @@ -38,7 +38,7 @@ * * @psalm-import-type TalkScheduledMessage from ResponseDefinitions */ -class ScheduledMessage extends Entity implements \JsonSerializable { +class ScheduledMessage extends Entity { public const METADATA_THREAD_TITLE = 'threadTitle'; public const METADATA_THREAD_ID = 'threadId'; public const METADATA_SILENT = 'silent'; @@ -95,22 +95,6 @@ public function setMessage(string $message): void { $this->markFieldUpdated('message'); } - #[\Override] - public function jsonSerialize(): array { - return [ - 'roomId' => $this->getRoomId(), - 'actorId' => $this->getActorId(), - 'actorType' => $this->getActorType(), - 'threadId' => $this->getThreadId(), - 'parentId' => $this->getParentId(), - 'message' => $this->getMessage(), - 'messageType' => $this->getMessageType(), - 'createdAt' => $this->getCreatedAt()->getTimestamp(), - 'sendAt' => $this->getSendAt()?->getTimestamp(), - 'metaData' => $this->getDecodedMetaData(), - ]; - } - /** * @param string $format * @psalm-param 'json'|'xml' $format @@ -131,7 +115,7 @@ public function toArray(string $format, ?Message $parent, ?Thread $thread) : arr ]; if ($parent !== null) { - $data['parent'] = $parent->toArray('json', $thread); + $data['parent'] = $parent->toArray($format, $thread); } if ($thread !== null) { diff --git a/lib/Service/ScheduledMessageService.php b/lib/Service/ScheduledMessageService.php index eb094277889..c8a3dc1a04b 100644 --- a/lib/Service/ScheduledMessageService.php +++ b/lib/Service/ScheduledMessageService.php @@ -128,7 +128,7 @@ public function deleteByActor(string $actorType, string $actorId): void { /** * @return list */ - public function getMessages(Room $chat, Participant $participant): array { + public function getMessages(Room $chat, Participant $participant, string $format): array { $result = $this->scheduledMessageMapper->findByRoomAndActor( $chat, $participant->getAttendee()->getActorType(), @@ -166,13 +166,13 @@ public function getMessages(Room $chat, Participant $participant): array { } else { $thread = Thread::fromRow($thread); } - $messages[] = $this->parseScheduledMessage($scheduleMessage, $parent, $thread); + $messages[] = $this->parseScheduledMessage($format, $scheduleMessage, $parent, $thread); } return $messages; } - public function parseScheduledMessage(ScheduledMessage $message, ?Message $parentMessage, ?Thread $thread = null): array { + public function parseScheduledMessage(string $format, ScheduledMessage $message, ?Message $parentMessage, ?Thread $thread = null): array { if ($thread === null && $message->getThreadId() !== Thread::THREAD_NONE && $message->getThreadId() !== Thread::THREAD_CREATE @@ -187,7 +187,7 @@ public function parseScheduledMessage(ScheduledMessage $message, ?Message $paren $thread = null; } } - return $message->toArray($parentMessage, $thread ?? null); + return $message->toArray($format, $parentMessage, $thread ?? null); } public function getScheduledMessageCount(Room $chat, Participant $participant): int { From 55b6555e0784696f0fc014c10c633494fde81ba0 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Fri, 5 Dec 2025 15:36:59 +0100 Subject: [PATCH 5/5] chore(assets): Recompile assets Signed-off-by: Joas Schilling --- openapi-full.json | 93 +++---------------------------- openapi.json | 93 +++---------------------------- src/types/openapi/openapi-full.ts | 44 +++------------ src/types/openapi/openapi.ts | 44 +++------------ 4 files changed, 28 insertions(+), 246 deletions(-) diff --git a/openapi-full.json b/openapi-full.json index 8337c076606..31ce9dc4f93 100644 --- a/openapi-full.json +++ b/openapi-full.json @@ -1921,25 +1921,19 @@ "type": "object", "required": [ "id", - "roomId", "actorId", "actorType", "threadId", - "parentId", "message", "messageType", "createdAt", "sendAt", - "metaData" + "silent" ], "properties": { "id": { - "type": "integer", - "format": "int64" - }, - "roomId": { - "type": "integer", - "format": "int64" + "type": "string", + "description": "SnowflakeID" }, "actorId": { "type": "string" @@ -1951,17 +1945,9 @@ "type": "integer", "format": "int64" }, - "threadExists": { - "type": "boolean" - }, "threadTitle": { "type": "string" }, - "parentId": { - "type": "integer", - "format": "int64", - "nullable": true - }, "parent": { "$ref": "#/components/schemas/ChatMessage" }, @@ -1976,36 +1962,11 @@ "format": "int64" }, "sendAt": { - "type": "integer", - "format": "int64", - "nullable": true - }, - "metaData": { - "$ref": "#/components/schemas/ScheduledMessageMetaData" - } - } - }, - "ScheduledMessageMetaData": { - "type": "object", - "required": [ - "threadId", - "threadTitle", - "silent" - ], - "properties": { - "threadId": { "type": "integer", "format": "int64" }, - "threadTitle": { - "type": "string" - }, "silent": { "type": "boolean" - }, - "lastEditedTime": { - "type": "integer", - "format": "int64" } } }, @@ -7733,8 +7694,8 @@ }, "/ocs/v2.php/apps/spreed/api/{apiVersion}/chat/{token}/schedule": { "get": { - "operationId": "chat-scheduled-messages", - "summary": "Get all scheduled nessages of a given room and participant", + "operationId": "chat-get-scheduled-messages", + "summary": "Get all scheduled messages of a given room and participant", "description": "The author and timestamp are automatically set to the current user and time.\nRequired capability: `scheduled-messages`", "tags": [ "chat" @@ -7814,47 +7775,6 @@ } } }, - "400": { - "description": "Could not get scheduled messages", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": { - "type": "object", - "required": [ - "error" - ], - "properties": { - "error": { - "type": "string", - "enum": [ - "message" - ] - } - } - } - } - } - } - } - } - } - }, "404": { "description": "Actor not found", "content": { @@ -8447,7 +8367,8 @@ "error": { "type": "string", "enum": [ - "actor" + "actor", + "message" ] } } diff --git a/openapi.json b/openapi.json index 62089789058..ab7e788621a 100644 --- a/openapi.json +++ b/openapi.json @@ -1826,25 +1826,19 @@ "type": "object", "required": [ "id", - "roomId", "actorId", "actorType", "threadId", - "parentId", "message", "messageType", "createdAt", "sendAt", - "metaData" + "silent" ], "properties": { "id": { - "type": "integer", - "format": "int64" - }, - "roomId": { - "type": "integer", - "format": "int64" + "type": "string", + "description": "SnowflakeID" }, "actorId": { "type": "string" @@ -1856,17 +1850,9 @@ "type": "integer", "format": "int64" }, - "threadExists": { - "type": "boolean" - }, "threadTitle": { "type": "string" }, - "parentId": { - "type": "integer", - "format": "int64", - "nullable": true - }, "parent": { "$ref": "#/components/schemas/ChatMessage" }, @@ -1881,36 +1867,11 @@ "format": "int64" }, "sendAt": { - "type": "integer", - "format": "int64", - "nullable": true - }, - "metaData": { - "$ref": "#/components/schemas/ScheduledMessageMetaData" - } - } - }, - "ScheduledMessageMetaData": { - "type": "object", - "required": [ - "threadId", - "threadTitle", - "silent" - ], - "properties": { - "threadId": { "type": "integer", "format": "int64" }, - "threadTitle": { - "type": "string" - }, "silent": { "type": "boolean" - }, - "lastEditedTime": { - "type": "integer", - "format": "int64" } } }, @@ -7638,8 +7599,8 @@ }, "/ocs/v2.php/apps/spreed/api/{apiVersion}/chat/{token}/schedule": { "get": { - "operationId": "chat-scheduled-messages", - "summary": "Get all scheduled nessages of a given room and participant", + "operationId": "chat-get-scheduled-messages", + "summary": "Get all scheduled messages of a given room and participant", "description": "The author and timestamp are automatically set to the current user and time.\nRequired capability: `scheduled-messages`", "tags": [ "chat" @@ -7719,47 +7680,6 @@ } } }, - "400": { - "description": "Could not get scheduled messages", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": { - "type": "object", - "required": [ - "error" - ], - "properties": { - "error": { - "type": "string", - "enum": [ - "message" - ] - } - } - } - } - } - } - } - } - } - }, "404": { "description": "Actor not found", "content": { @@ -8352,7 +8272,8 @@ "error": { "type": "string", "enum": [ - "actor" + "actor", + "message" ] } } diff --git a/src/types/openapi/openapi-full.ts b/src/types/openapi/openapi-full.ts index 8cdc9dcb2b7..cd66b302347 100644 --- a/src/types/openapi/openapi-full.ts +++ b/src/types/openapi/openapi-full.ts @@ -429,11 +429,11 @@ export type paths = { cookie?: never; }; /** - * Get all scheduled nessages of a given room and participant + * Get all scheduled messages of a given room and participant * @description The author and timestamp are automatically set to the current user and time. * Required capability: `scheduled-messages` */ - get: operations["chat-scheduled-messages"]; + get: operations["chat-get-scheduled-messages"]; put?: never; /** * Schedules the sending of a new chat message to the given room @@ -3010,34 +3010,21 @@ export type components = { invalidParticipants: components["schemas"]["InvitationList"]; }; ScheduledMessage: { - /** Format: int64 */ - id: number; - /** Format: int64 */ - roomId: number; + /** @description SnowflakeID */ + id: string; actorId: string; actorType: string; /** Format: int64 */ threadId: number; - threadExists?: boolean; threadTitle?: string; - /** Format: int64 */ - parentId: number | null; parent?: components["schemas"]["ChatMessage"]; message: string; messageType: string; /** Format: int64 */ createdAt: number; /** Format: int64 */ - sendAt: number | null; - metaData: components["schemas"]["ScheduledMessageMetaData"]; - }; - ScheduledMessageMetaData: { - /** Format: int64 */ - threadId: number; - threadTitle: string; + sendAt: number; silent: boolean; - /** Format: int64 */ - lastEditedTime?: number; }; SignalingFederationSettings: { server: string; @@ -5310,7 +5297,7 @@ export interface operations { }; }; }; - "chat-scheduled-messages": { + "chat-get-scheduled-messages": { parameters: { query?: never; header: { @@ -5339,23 +5326,6 @@ export interface operations { }; }; }; - /** @description Could not get scheduled messages */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - ocs: { - meta: components["schemas"]["OCSMeta"]; - data: { - /** @enum {string} */ - error: "message"; - }; - }; - }; - }; - }; /** @description Current user is not logged in */ 401: { headers: { @@ -5614,7 +5584,7 @@ export interface operations { meta: components["schemas"]["OCSMeta"]; data: { /** @enum {string} */ - error: "actor"; + error: "actor" | "message"; }; }; }; diff --git a/src/types/openapi/openapi.ts b/src/types/openapi/openapi.ts index 916c719eced..a2cde05a0d4 100644 --- a/src/types/openapi/openapi.ts +++ b/src/types/openapi/openapi.ts @@ -429,11 +429,11 @@ export type paths = { cookie?: never; }; /** - * Get all scheduled nessages of a given room and participant + * Get all scheduled messages of a given room and participant * @description The author and timestamp are automatically set to the current user and time. * Required capability: `scheduled-messages` */ - get: operations["chat-scheduled-messages"]; + get: operations["chat-get-scheduled-messages"]; put?: never; /** * Schedules the sending of a new chat message to the given room @@ -2472,34 +2472,21 @@ export type components = { invalidParticipants: components["schemas"]["InvitationList"]; }; ScheduledMessage: { - /** Format: int64 */ - id: number; - /** Format: int64 */ - roomId: number; + /** @description SnowflakeID */ + id: string; actorId: string; actorType: string; /** Format: int64 */ threadId: number; - threadExists?: boolean; threadTitle?: string; - /** Format: int64 */ - parentId: number | null; parent?: components["schemas"]["ChatMessage"]; message: string; messageType: string; /** Format: int64 */ createdAt: number; /** Format: int64 */ - sendAt: number | null; - metaData: components["schemas"]["ScheduledMessageMetaData"]; - }; - ScheduledMessageMetaData: { - /** Format: int64 */ - threadId: number; - threadTitle: string; + sendAt: number; silent: boolean; - /** Format: int64 */ - lastEditedTime?: number; }; SignalingFederationSettings: { server: string; @@ -4772,7 +4759,7 @@ export interface operations { }; }; }; - "chat-scheduled-messages": { + "chat-get-scheduled-messages": { parameters: { query?: never; header: { @@ -4801,23 +4788,6 @@ export interface operations { }; }; }; - /** @description Could not get scheduled messages */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - ocs: { - meta: components["schemas"]["OCSMeta"]; - data: { - /** @enum {string} */ - error: "message"; - }; - }; - }; - }; - }; /** @description Current user is not logged in */ 401: { headers: { @@ -5076,7 +5046,7 @@ export interface operations { meta: components["schemas"]["OCSMeta"]; data: { /** @enum {string} */ - error: "actor"; + error: "actor" | "message"; }; }; };