mirror of
https://github.com/nextcloud/spreed.git
synced 2025-12-17 13:08:58 +01:00
feat: scheduled message API
Signed-off-by: Anna Larch <anna@nextcloud.com>
This commit is contained in:
parent
f8778c9734
commit
ef3265c9c7
30 changed files with 4181 additions and 21 deletions
|
|
@ -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.
|
||||
]]></description>
|
||||
|
||||
<version>23.0.0-dev.2</version>
|
||||
<version>23.0.0-dev.3</version>
|
||||
<licence>agpl</licence>
|
||||
|
||||
<author>Anna Larch</author>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ declare(strict_types=1);
|
|||
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 @@ use OCP\EventDispatcher\IEventListener;
|
|||
class Listener implements IEventListener {
|
||||
public function __construct(
|
||||
protected ChatManager $chatManager,
|
||||
protected ScheduledMessageMapper $scheduledMessageMapper,
|
||||
) {
|
||||
}
|
||||
|
||||
|
|
@ -26,6 +28,7 @@ class Listener implements IEventListener {
|
|||
public function handle(Event $event): void {
|
||||
if ($event instanceof RoomDeletedEvent) {
|
||||
$this->chatManager->deleteMessages($event->getRoom());
|
||||
$this->scheduledMessageMapper->deleteMessagesByRoom($event->getRoom());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ use OCA\Talk\Model\Attendee;
|
|||
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\ParticipantService;
|
|||
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 @@ use Psr\Log\LoggerInterface;
|
|||
* @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 @@ class ChatController extends AEnvironmentAwareOCSController {
|
|||
protected ITaskProcessingManager $taskProcessingManager,
|
||||
protected IAppConfig $appConfig,
|
||||
protected LoggerInterface $logger,
|
||||
protected ScheduledMessageService $scheduledMessageManager,
|
||||
) {
|
||||
parent::__construct($appName, $request);
|
||||
}
|
||||
|
|
@ -332,6 +336,260 @@ class ChatController extends AEnvironmentAwareOCSController {
|
|||
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<Http::STATUS_OK, list<TalkScheduledMessage>, array{}>|DataResponse<Http::STATUS_NOT_FOUND, array{error: 'actor'}, array{}>
|
||||
*
|
||||
* 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<Http::STATUS_CREATED, TalkScheduledMessage, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error: 'message'|'reply-to'|'send-at'}, array{}>|DataResponse<Http::STATUS_REQUEST_ENTITY_TOO_LARGE, array{error: 'message'}, array{}>|DataResponse<Http::STATUS_NOT_FOUND, array{error: 'actor'}, array{}>
|
||||
*
|
||||
* 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<Http::STATUS_ACCEPTED, TalkScheduledMessage, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error: 'message'|'send-at'|'thread-title'}, array{}>|DataResponse<Http::STATUS_REQUEST_ENTITY_TOO_LARGE, array{error: 'message'}, array{}>|DataResponse<Http::STATUS_NOT_FOUND, array{error: 'actor'|'message'}, array{}>
|
||||
*
|
||||
* 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<Http::STATUS_OK, array{}, array{}>|DataResponse<Http::STATUS_NOT_FOUND, array{error: 'actor'|'message'}, array{}>
|
||||
*
|
||||
* 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
|
||||
*
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ declare(strict_types=1);
|
|||
|
||||
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 @@ class RoomController extends AEnvironmentAwareOCSController {
|
|||
$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<Http::STATUS_NOT_FOUND, null, array{}> $response
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ use OCA\Talk\Manager;
|
|||
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 @@ class UserDeletedListener implements IEventListener {
|
|||
private Manager $manager,
|
||||
private PollService $pollService,
|
||||
private ConsentService $consentService,
|
||||
private ScheduledMessageService $messageManager,
|
||||
) {
|
||||
}
|
||||
|
||||
|
|
@ -41,5 +43,7 @@ class UserDeletedListener implements IEventListener {
|
|||
$this->pollService->neutralizeDeletedUser(Attendee::ACTOR_USERS, $user->getUID());
|
||||
|
||||
$this->consentService->deleteByActor(Attendee::ACTOR_USERS, $user->getUID());
|
||||
|
||||
$this->messageManager->deleteByActor(Attendee::ACTOR_USERS, $user->getUID());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
91
lib/Migration/Version23000Date20251105125333.php
Normal file
91
lib/Migration/Version23000Date20251105125333.php
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Talk\Migration;
|
||||
|
||||
use Closure;
|
||||
use OCP\DB\ISchemaWrapper;
|
||||
use OCP\DB\Types;
|
||||
use OCP\Migration\IOutput;
|
||||
use OCP\Migration\SimpleMigrationStep;
|
||||
use Override;
|
||||
|
||||
class Version23000Date20251105125333 extends SimpleMigrationStep {
|
||||
/**
|
||||
* @param IOutput $output
|
||||
* @param Closure(): ISchemaWrapper $schemaClosure
|
||||
* @param array $options
|
||||
* @return null|ISchemaWrapper
|
||||
*/
|
||||
#[Override]
|
||||
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
|
||||
$schema = $schemaClosure();
|
||||
if (!$schema->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;
|
||||
}
|
||||
}
|
||||
|
|
@ -72,6 +72,8 @@ use OCP\DB\Types;
|
|||
* @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 @@ class Attendee extends Entity {
|
|||
$this->addType('hasUnreadThreadMentions', Types::BOOLEAN);
|
||||
$this->addType('hasUnreadThreadDirects', Types::BOOLEAN);
|
||||
$this->addType('hiddenPinnedId', Types::BIGINT);
|
||||
$this->addType('hasScheduledMessages', Types::BOOLEAN);
|
||||
|
||||
}
|
||||
|
||||
public function getDisplayName(): string {
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
145
lib/Model/ScheduledMessage.php
Normal file
145
lib/Model/ScheduledMessage.php
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Talk\Model;
|
||||
|
||||
use OCA\Talk\Chat\ChatManager;
|
||||
use OCA\Talk\ResponseDefinitions;
|
||||
use OCP\AppFramework\Db\Entity;
|
||||
use OCP\Comments\MessageTooLongException;
|
||||
use OCP\DB\Types;
|
||||
|
||||
/**
|
||||
* @method string getId()
|
||||
* @method void setId(string $id)
|
||||
* @method void setRoomId(int $roomId)
|
||||
* @method int getRoomId()
|
||||
* @method void setActorId(string $actorId)
|
||||
* @method string getActorId()
|
||||
* @method void setActorType(string $actorType)
|
||||
* @method string getActorType()
|
||||
* @method int getThreadId()
|
||||
* @method void setThreadId(int $threadId)
|
||||
* @method int|null getParentId()
|
||||
* @method void setParentId(int|null $parentId)
|
||||
* @method string getMessage()
|
||||
* @method void setMessageType(string $messageType)
|
||||
* @method string getMessageType()
|
||||
* @method \DateTime getCreatedAt()
|
||||
* @method void setCreatedAt(\DateTime $createdAt)
|
||||
* @method void setSendAt(\DateTime|null $sendAt)
|
||||
* @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';
|
||||
public const METADATA_THREAD_ID = 'threadId';
|
||||
public const METADATA_SILENT = 'silent';
|
||||
public const METADATA_LAST_EDITED_TIME = 'lastEditedTime';
|
||||
|
||||
protected ?int $roomId = 0;
|
||||
protected string $actorId = '';
|
||||
protected string $actorType = '';
|
||||
protected int $threadId = 0;
|
||||
protected ?int $parentId = null;
|
||||
protected string $message = '';
|
||||
protected string $messageType = '';
|
||||
protected ?string $metaData = null;
|
||||
protected ?\DateTime $createdAt = null;
|
||||
protected ?\DateTime $sendAt = null;
|
||||
|
||||
public function __construct() {
|
||||
$this->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;
|
||||
}
|
||||
}
|
||||
137
lib/Model/ScheduledMessageMapper.php
Normal file
137
lib/Model/ScheduledMessageMapper.php
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Talk\Model;
|
||||
|
||||
use OCA\Talk\Room;
|
||||
use OCP\AppFramework\Db\DoesNotExistException;
|
||||
use OCP\AppFramework\Db\Entity;
|
||||
use OCP\AppFramework\Db\QBMapper;
|
||||
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||
use OCP\IDBConnection;
|
||||
use OCP\Snowflake\IGenerator;
|
||||
|
||||
/**
|
||||
* @method ScheduledMessage mapRowToEntity(array $row)
|
||||
* @method ScheduledMessage findEntity(IQueryBuilder $query)
|
||||
* @method list<ScheduledMessage> findEntities(IQueryBuilder $query)
|
||||
* @method ScheduledMessage update(ScheduledMessage $scheduledMessage)
|
||||
* @method ScheduledMessage delete(ScheduledMessage $scheduledMessage)
|
||||
* @template-extends QBMapper<ScheduledMessage>
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -50,6 +50,33 @@ class SelectHelper {
|
|||
])->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 .= '.';
|
||||
|
|
|
|||
|
|
@ -66,6 +66,15 @@ class Participant {
|
|||
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) {
|
||||
|
|
|
|||
|
|
@ -387,6 +387,8 @@ namespace OCA\Talk;
|
|||
* lastPinnedId: int,
|
||||
* // Required capability: `pinned-messages`
|
||||
* hiddenPinnedId: int,
|
||||
* // Required capability: `scheduled-messages` (local)
|
||||
* hasScheduledMessages: bool,
|
||||
* }
|
||||
*
|
||||
* @psalm-type TalkDashboardEventAttachment = array{
|
||||
|
|
@ -563,6 +565,30 @@ namespace OCA\Talk;
|
|||
* 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 {
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2381,4 +2381,10 @@ class ParticipantService {
|
|||
$this->cacheParticipant($room, $participant);
|
||||
return $participant;
|
||||
}
|
||||
|
||||
public function setHasScheduledMessages(Participant $participant, bool $hasScheduledMessages): void {
|
||||
$attendee = $participant->getAttendee();
|
||||
$attendee->setHasScheduledMessages($hasScheduledMessages);
|
||||
$this->attendeeMapper->update($attendee);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -158,6 +158,7 @@ class RoomFormatter {
|
|||
'isArchived' => false,
|
||||
'isImportant' => false,
|
||||
'isSensitive' => false,
|
||||
'hasScheduledMessages' => false,
|
||||
];
|
||||
|
||||
if ($room->isFederatedConversation()) {
|
||||
|
|
@ -345,6 +346,7 @@ class RoomFormatter {
|
|||
&& ($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();
|
||||
|
|
|
|||
200
lib/Service/ScheduledMessageService.php
Normal file
200
lib/Service/ScheduledMessageService.php
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Talk\Service;
|
||||
|
||||
use OCA\Talk\Chat\CommentsManager;
|
||||
use OCA\Talk\Chat\MessageParser;
|
||||
use OCA\Talk\Model\Message;
|
||||
use OCA\Talk\Model\ScheduledMessage;
|
||||
use OCA\Talk\Model\ScheduledMessageMapper;
|
||||
use OCA\Talk\Model\Thread;
|
||||
use OCA\Talk\Participant;
|
||||
use OCA\Talk\ResponseDefinitions;
|
||||
use OCA\Talk\Room;
|
||||
use OCP\AppFramework\Db\DoesNotExistException;
|
||||
use OCP\AppFramework\Utility\ITimeFactory;
|
||||
use OCP\Comments\IComment;
|
||||
use OCP\Comments\MessageTooLongException;
|
||||
use OCP\DB\Exception;
|
||||
use OCP\IL10N;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
/**
|
||||
* @psalm-import-type TalkScheduledMessage from ResponseDefinitions
|
||||
*/
|
||||
class ScheduledMessageService {
|
||||
public function __construct(
|
||||
private readonly ScheduledMessageMapper $scheduledMessageMapper,
|
||||
protected ThreadService $threadService,
|
||||
protected ITimeFactory $timeFactory,
|
||||
protected IL10N $l,
|
||||
protected LoggerInterface $logger,
|
||||
protected CommentsManager $commentsManager,
|
||||
protected MessageParser $messageParser,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws MessageTooLongException When the message is too long (~32k characters)
|
||||
*/
|
||||
public function scheduleMessage(
|
||||
Room $chat,
|
||||
Participant $participant,
|
||||
string $message,
|
||||
string $messageType,
|
||||
?IComment $parent,
|
||||
int $threadId,
|
||||
\DateTime $sendAt,
|
||||
array $metadata = [],
|
||||
): ScheduledMessage {
|
||||
$scheduledMessage = new ScheduledMessage();
|
||||
$scheduledMessage->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<TalkScheduledMessage>
|
||||
*/
|
||||
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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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)"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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)"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
1019
openapi-full.json
1019
openapi-full.json
File diff suppressed because it is too large
Load diff
1019
openapi.json
1019
openapi.json
File diff suppressed because it is too large
Load diff
|
|
@ -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"];
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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"];
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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<string, never>;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/** @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: {
|
||||
|
|
|
|||
|
|
@ -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<string, never>;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/** @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: {
|
||||
|
|
|
|||
|
|
@ -1934,6 +1934,140 @@ class FeatureContext implements Context, SnippetAcceptingContext {
|
|||
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);
|
||||
|
|
|
|||
111
tests/integration/features/chat-4/scheduled-messages.feature
Normal file
111
tests/integration/features/chat-4/scheduled-messages.feature
Normal file
|
|
@ -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} |
|
||||
|
|
@ -86,6 +86,9 @@ class ApiController extends OCSController {
|
|||
$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();
|
||||
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ use OCA\Talk\Service\ParticipantService;
|
|||
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 @@ class ChatControllerTest extends TestCase {
|
|||
$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 @@ class ChatControllerTest extends TestCase {
|
|||
});
|
||||
}
|
||||
|
||||
private function recreateChatController() {
|
||||
private function recreateChatController(): void {
|
||||
$this->controller = new ChatController(
|
||||
'spreed',
|
||||
$this->userId,
|
||||
|
|
@ -186,10 +189,11 @@ class ChatControllerTest extends TestCase {
|
|||
$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 @@ class ChatControllerTest extends TestCase {
|
|||
$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 @@ class ChatControllerTest extends TestCase {
|
|||
$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 @@ class ChatControllerTest extends TestCase {
|
|||
/** @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 @@ class ChatControllerTest extends TestCase {
|
|||
$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 @@ class ChatControllerTest extends TestCase {
|
|||
$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 @@ class ChatControllerTest extends TestCase {
|
|||
$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 @@ class ChatControllerTest extends TestCase {
|
|||
|
||||
public function testWaitForNewMessagesByUser(): void {
|
||||
$testUser = $this->createMock(IUser::class);
|
||||
$testUser->expects($this->any())
|
||||
$testUser
|
||||
->method('getUID')
|
||||
->willReturn('testUser');
|
||||
|
||||
|
|
@ -1044,7 +1042,7 @@ class ChatControllerTest extends TestCase {
|
|||
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 @@ class ChatControllerTest extends TestCase {
|
|||
->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 @@ class ChatControllerTest extends TestCase {
|
|||
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 @@ class ChatControllerTest extends TestCase {
|
|||
->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 @@ class ChatControllerTest extends TestCase {
|
|||
#[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);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue